From e8e1e1f26e994aa7413a2db7acee7aaa8a9a9814 Mon Sep 17 00:00:00 2001 From: Martina Date: Tue, 8 Jun 2021 15:53:30 -0700 Subject: [PATCH] squashing commits --- .vscode/settings.json | 3 + common/actions.js | 151 +- common/constants.js | 13 +- common/custom-events.js | 2 + common/hooks.js | 187 + common/logo.js | 50 +- common/messages.js | 44 +- common/navigation-data.js | 5 +- common/svg.js | 35 +- common/user-behaviors.js | 33 +- common/utilities.js | 15 + common/validations.js | 99 +- components/core/Alert.js | 5 + components/core/Application.js | 37 +- components/core/Auth/Initial.js | 227 + components/core/Auth/ResetPassword.js | 183 + components/core/Auth/Signin.js | 229 + components/core/Auth/Signup.js | 100 + components/core/Auth/TwitterSignup.js | 122 + .../core/Auth/components/AuthCheckBox.js | 64 + .../core/Auth/components/SignUpPopover.js | 74 + components/core/Auth/components/Toggle.js | 75 + .../core/Auth/components/Verification.js | 126 + components/core/Auth/components/index.js | 4 + components/core/Auth/index.js | 5 + components/core/Field.js | 140 + .../core/FontFrame/Settings/Controls.js | 2 +- .../core/FontFrame/Settings/FixedControls.js | 2 +- components/core/FontFrame/Settings/index.js | 7 +- components/core/FontFrame/Views/Paragraph.js | 2 +- components/core/FontFrame/Views/Sentence.js | 2 +- components/core/FontFrame/Views/index.js | 6 +- components/core/FontFrame/hooks.js | 2 +- components/core/FontFrame/index.js | 11 +- components/core/MarkdownFrame.js | 4 +- components/core/NewWebsitePrototypeHeader.js | 240 + .../core/Selectable/doObjectsCollide.js | 6 +- components/core/Selectable/groupSelectable.js | 2 +- components/core/Selectable/index.js | 4 +- components/core/Selectable/selectable.js | 2 +- components/core/SignIn.js | 16 +- components/core/WebsitePrototypeHeader.js | 231 +- .../core/WebsitePrototypeHeaderGeneric.js | 104 + components/system/components/Buttons.js | 2 +- components/system/components/CheckBox.js | 6 +- components/system/components/Divider.js | 15 + components/system/components/Input.js | 192 +- components/system/index.js | 2 + node_common/constants.js | 3 + node_common/data/index.js | 32 + .../data/methods/create-twitter-token.js | 24 + node_common/data/methods/create-user.js | 5 +- .../data/methods/create-verification.js | 29 + .../methods/delete-verification-by-email.js | 18 + .../methods/delete-verification-by-sid.js | 18 + node_common/data/methods/get-twitter-token.js | 26 + node_common/data/methods/get-user-by-email.js | 29 + .../data/methods/get-user-by-twitter-id.js | 27 + .../data/methods/get-verification-by-email.js | 26 + .../data/methods/get-verification-by-sid.js | 25 + .../data/methods/prune-verifications.js | 21 + .../data/methods/update-twitter-token.js | 27 + node_common/data/methods/update-user-by-id.js | 24 +- .../data/methods/update-verification.js | 25 + node_common/environment.js | 15 +- node_common/managers/emails.js | 72 + node_common/managers/twitter.js | 73 + node_common/serializers.js | 4 + node_common/utilities.js | 3 + package-lock.json | 4326 +++++++---------- package.json | 5 + pages/_/index.js | 2 +- pages/_/profile[dep].js | 102 + pages/_/slate[dep].js | 375 ++ pages/api/hydrate.js | 3 +- pages/api/sign-in.js | 51 +- pages/api/twitter/authenticate.js | 119 + pages/api/twitter/request-token.js | 29 + pages/api/twitter/signup-with-verification.js | 143 + pages/api/twitter/signup.js | 125 + pages/api/users/check-email.js | 20 + pages/api/users/create.js | 55 +- pages/api/users/get-version.js | 40 + pages/api/users/migrate.js | 67 + pages/api/users/reset-password.js | 75 + pages/api/users/update.js | 17 + pages/api/verifications/create.js | 61 + pages/api/verifications/legacy/create.js | 98 + .../verifications/password-reset/create.js | 70 + .../verifications/password-reset/verify.js | 58 + pages/api/verifications/prune.js | 20 + pages/api/verifications/resend.js | 55 + pages/api/verifications/twitter/create.js | 76 + pages/api/verifications/verify.js | 63 + pages/community.js | 2 +- pages/guidelines.js | 2 +- pages/index.js | 6 +- pages/slate-for-chrome.js | 2 +- pages/terms.js | 2 +- scenes/SceneActivity.js | 30 +- scenes/SceneAuth/hooks.js | 373 ++ scenes/SceneAuth/index.js | 149 + scenes/SceneSignIn.js | 172 - scripts/adjust.js | 46 +- scripts/seed-database.js | 33 + 105 files changed, 7202 insertions(+), 3084 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 common/hooks.js create mode 100644 components/core/Auth/Initial.js create mode 100644 components/core/Auth/ResetPassword.js create mode 100644 components/core/Auth/Signin.js create mode 100644 components/core/Auth/Signup.js create mode 100644 components/core/Auth/TwitterSignup.js create mode 100644 components/core/Auth/components/AuthCheckBox.js create mode 100644 components/core/Auth/components/SignUpPopover.js create mode 100644 components/core/Auth/components/Toggle.js create mode 100644 components/core/Auth/components/Verification.js create mode 100644 components/core/Auth/components/index.js create mode 100644 components/core/Auth/index.js create mode 100644 components/core/Field.js create mode 100644 components/core/NewWebsitePrototypeHeader.js create mode 100644 components/core/WebsitePrototypeHeaderGeneric.js create mode 100644 components/system/components/Divider.js create mode 100644 node_common/data/methods/create-twitter-token.js create mode 100644 node_common/data/methods/create-verification.js create mode 100644 node_common/data/methods/delete-verification-by-email.js create mode 100644 node_common/data/methods/delete-verification-by-sid.js create mode 100644 node_common/data/methods/get-twitter-token.js create mode 100644 node_common/data/methods/get-user-by-email.js create mode 100644 node_common/data/methods/get-user-by-twitter-id.js create mode 100644 node_common/data/methods/get-verification-by-email.js create mode 100644 node_common/data/methods/get-verification-by-sid.js create mode 100644 node_common/data/methods/prune-verifications.js create mode 100644 node_common/data/methods/update-twitter-token.js create mode 100644 node_common/data/methods/update-verification.js create mode 100644 node_common/managers/emails.js create mode 100644 node_common/managers/twitter.js create mode 100644 pages/_/profile[dep].js create mode 100644 pages/_/slate[dep].js create mode 100644 pages/api/twitter/authenticate.js create mode 100644 pages/api/twitter/request-token.js create mode 100644 pages/api/twitter/signup-with-verification.js create mode 100644 pages/api/twitter/signup.js create mode 100644 pages/api/users/check-email.js create mode 100644 pages/api/users/get-version.js create mode 100644 pages/api/users/migrate.js create mode 100644 pages/api/users/reset-password.js create mode 100644 pages/api/verifications/create.js create mode 100644 pages/api/verifications/legacy/create.js create mode 100644 pages/api/verifications/password-reset/create.js create mode 100644 pages/api/verifications/password-reset/verify.js create mode 100644 pages/api/verifications/prune.js create mode 100644 pages/api/verifications/resend.js create mode 100644 pages/api/verifications/twitter/create.js create mode 100644 pages/api/verifications/verify.js create mode 100644 scenes/SceneAuth/hooks.js create mode 100644 scenes/SceneAuth/index.js delete mode 100644 scenes/SceneSignIn.js diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..23fd35f0 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "editor.formatOnSave": true +} \ No newline at end of file diff --git a/common/actions.js b/common/actions.js index de761993..0bab2703 100644 --- a/common/actions.js +++ b/common/actions.js @@ -87,6 +87,35 @@ export const checkUsername = async (data) => { }); }; +export const checkEmail = async (data) => { + return await returnJSON(`/api/users/check-email`, { + ...DEFAULT_OPTIONS, + body: JSON.stringify({ data }), + }); +}; + +//NOTE(toast): this functionality comes with the upgraded sg plan +// export const validateEmail = async (data) => { +// return await returnJSON("/api/emails/validate", { +// ...DEFAULT_OPTIONS, +// body: JSON.stringify({ data }), +// }); +// }; + +export const sendEmail = async (data) => { + return await returnJSON("/api/emails/send-email", { + ...DEFAULT_OPTIONS, + body: JSON.stringify({ data }), + }); +}; + +export const sendTemplateEmail = async (data) => { + return await returnJSON("/api/emails/send-template", { + ...DEFAULT_OPTIONS, + body: JSON.stringify({ data }), + }); +}; + export const archive = async (data) => { await Websockets.checkWebsocket(); return await returnJSON(`/api/data/archive`, { @@ -188,6 +217,33 @@ export const addFileToSlate = async (data) => { }); }; +export const requestTwitterToken = async () => { + return await returnJSON(`/api/twitter/request-token`, { + ...DEFAULT_OPTIONS, + }); +}; + +export const authenticateViaTwitter = async (data) => { + return await returnJSON(`/api/twitter/authenticate`, { + ...DEFAULT_OPTIONS, + body: JSON.stringify({ data }), + }); +}; + +export const createUserViaTwitter = async (data) => { + return await returnJSON(`/api/twitter/signup`, { + ...DEFAULT_OPTIONS, + body: JSON.stringify({ data }), + }); +}; + +export const createUserViaTwitterWithVerification = async (data) => { + return await returnJSON(`/api/twitter/signup-with-verification`, { + ...DEFAULT_OPTIONS, + body: JSON.stringify({ data }), + }); +}; + export const updateViewer = async (data) => { await Websockets.checkWebsocket(); return await returnJSON(`/api/users/update`, { @@ -382,62 +438,139 @@ export const getZipFilePaths = async (data) => { }; export const cleanDatabase = async () => { - return await returnJSON(`api/clean-up/users`, { + return await returnJSON(`/api/clean-up/users`, { ...DEFAULT_OPTIONS, }); }; export const v1GetSlate = async (data) => { - return await returnJSON(`api/v1/get-slate`, { + return await returnJSON(`/api/v1/get-slate`, { ...API_OPTIONS, body: JSON.stringify({ data }), }); }; export const v1Get = async (data) => { - return await returnJSON(`api/v1/get`, { + return await returnJSON(`/api/v1/get`, { ...API_OPTIONS, body: JSON.stringify({ data }), }); }; export const v1UpdateSlate = async (data) => { - return await returnJSON(`api/v1/update-slate`, { + return await returnJSON(`/api/v1/update-slate`, { ...API_OPTIONS, body: JSON.stringify({ data }), }); }; export const v2GetSlate = async (data) => { - return await returnJSON(`api/v2/get-slate`, { + return await returnJSON(`/api/v2/get-slate`, { ...API_OPTIONS, body: JSON.stringify({ data }), }); }; export const v2GetUser = async (data) => { - return await returnJSON(`api/v2/get-user`, { + return await returnJSON(`/api/v2/get-user`, { ...API_OPTIONS, body: JSON.stringify({ data }), }); }; export const v2Get = async (data) => { - return await returnJSON(`api/v2/get`, { + return await returnJSON(`/api/v2/get`, { ...API_OPTIONS, body: JSON.stringify({ data }), }); }; export const v2UpdateSlate = async (data) => { - return await returnJSON(`api/v2/update-slate`, { + return await returnJSON(`/api/v2/update-slate`, { ...API_OPTIONS, body: JSON.stringify({ data }), }); }; export const v2UpdateFile = async (data) => { - return await returnJSON(`api/v2/update-file`, { + return await returnJSON(`/api/v2/update-file`, { + ...API_OPTIONS, + body: JSON.stringify({ data }), + }); +}; + +export const createTwitterEmailVerification = async (data) => { + return await returnJSON(`/api/verifications/twitter/create`, { + ...API_OPTIONS, + body: JSON.stringify({ data }), + }); +}; + +export const verifyTwitterEmail = async (data) => { + return await returnJSON(`/api/verifications/twitter/verify`, { + ...API_OPTIONS, + body: JSON.stringify({ data }), + }); +}; + +export const createPasswordResetVerification = async (data) => { + return await returnJSON(`/api/verifications/password-reset/create`, { + ...API_OPTIONS, + body: JSON.stringify({ data }), + }); +}; + +export const verifyPasswordResetEmail = async (data) => { + return await returnJSON(`/api/verifications/password-reset/verify`, { + ...API_OPTIONS, + body: JSON.stringify({ data }), + }); +}; + +export const resetPassword = async (data) => { + return await returnJSON(`/api/users/reset-password`, { + ...API_OPTIONS, + body: JSON.stringify({ data }), + }); +}; + +export const createLegacyVerification = async (data) => { + return await returnJSON(`/api/verifications/legacy/create`, { + ...API_OPTIONS, + body: JSON.stringify({ data }), + }); +}; + +export const migrateUser = async (data) => { + return await returnJSON(`/api/users/migrate`, { + ...API_OPTIONS, + body: JSON.stringify({ data }), + }); +}; + +export const createVerification = async (data) => { + return await returnJSON(`/api/verifications/create`, { + ...API_OPTIONS, + body: JSON.stringify({ data }), + }); +}; + +export const verifyEmail = async (data) => { + return await returnJSON(`/api/verifications/verify`, { + ...API_OPTIONS, + body: JSON.stringify({ data }), + }); +}; + +export const resendVerification = async (data) => { + return await returnJSON(`/api/verifications/resend`, { + ...API_OPTIONS, + body: JSON.stringify({ data }), + }); +}; + +export const getUserVersion = async (data) => { + return await returnJSON(`/api/users/get-version`, { ...API_OPTIONS, body: JSON.stringify({ data }), }); diff --git a/common/constants.js b/common/constants.js index a937cfce..33eabe2c 100644 --- a/common/constants.js +++ b/common/constants.js @@ -32,21 +32,21 @@ export const system = { newYellow: "#F2B256", newRed: "#BE5234", shadow: "rgba(15, 14, 18, 0.03)", - blue: "#0061BB", - green: "#006837", + blue: "#0084FF", + green: "#34D159", yellow: "#FAB413", - red: "#C71313", + red: "#FF4530", black: "#0F0E12", newBlack: "#000000", - bgGrayLight: "#F8F8F8", + bgGrayLight: "#E5E5EA", bgGray: "#F2F2F2", bgBlue: "#C0D8EE", bgGreen: "#C0DACD", bgYellow: "#FEEDC4", bgRed: "#F1C4C4", - textGray: "#878688", + textGray: "#8E8E93", textGrayLight: "#C3C3C4", - textGrayDark: "#4B4A4D", + textGrayDark: "#48484A", textBlack: "#0F0E12", gray80: "#4B4A4D", gray70: "#868688", @@ -61,6 +61,7 @@ export const system = { active: "#00BB00", blurBlack: "#262626", bgBlurGray: "#403F42", + bgBlurWhiteTRN: "rgba(255,255,255,0.3)", grayLight2: "#AEAEB2", }; diff --git a/common/custom-events.js b/common/custom-events.js index 03f9b262..d5959e9d 100644 --- a/common/custom-events.js +++ b/common/custom-events.js @@ -4,6 +4,8 @@ export const dispatchCustomEvent = ({ name, detail }) => { }; export const hasError = (response) => { + console.log("insdie has error"); + console.log(response); if (!response) { dispatchCustomEvent({ name: "create-alert", diff --git a/common/hooks.js b/common/hooks.js new file mode 100644 index 00000000..bcc6235b --- /dev/null +++ b/common/hooks.js @@ -0,0 +1,187 @@ +import * as React from "react"; +import * as Events from "~/common/custom-events"; + +export const useMounted = () => { + const isMounted = React.useRef(true); + React.useLayoutEffect(() => { + return () => { + isMounted.current = false; + }; + }, []); + return isMounted.current; +}; + +/** NOTE(amine): + * useForm handles three main responsabilities + * - control inputs + * - control form + * - add validations + * + * For validations + * - Validate each field when onBlur event is triggered + * - Validate all fields before submit + * - font submit if there is errors + */ + +export const useForm = ({ + onSubmit, + validate, + initialValues, + validateOnBlur = true, + validateOnSubmit = true, +}) => { + const [state, setState] = React.useState({ + isSubmitting: false, + values: initialValues, + errors: {}, + touched: {}, + }); + + const _hasError = (obj) => Object.keys(obj).some((name) => obj[name]); + const _mergeEventHandlers = (events = []) => (e) => + events.forEach((event) => { + if (event) event(e); + }); + + /** ---------- NOTE(amine): Input Handlers ---------- */ + const handleFieldChange = (e) => + setState((prev) => ({ + ...prev, + values: { ...prev.values, [e.target.name]: e.target.value }, + errors: { ...prev.errors, [e.target.name]: undefined }, + touched: { ...prev.touched, [e.target.name]: false }, + })); + + const handleOnBlur = (e) => { + // NOTE(amine): validate the inputs onBlur and touch the current input + let errors = {}; + if (validateOnBlur && validate) errors = validate(state.values, { ...state.errors }); + setState((prev) => ({ + ...prev, + touched: { ...prev.touched, [e.target.name]: validateOnBlur }, + errors, + })); + }; + + // Note(Amine): this prop getter will capture the field state + const getFieldProps = (name, { onChange, onBlur, error } = {}) => ({ + name: name, + value: state.values[name], + error: error || state.errors[name], + touched: state.touched[name], + onChange: _mergeEventHandlers([onChange, handleFieldChange]), + onBlur: _mergeEventHandlers([onBlur, handleOnBlur]), + }); + + /** ---------- NOTE(amine): Form Handlers ---------- */ + const handleFormOnSubmit = (e) => { + e.preventDefault(); + //NOTE(amine): touch all inputs + setState((prev) => { + const touched = Object.keys(prev.values).reduce((acc, key) => ({ ...acc, [key]: true }), {}); + return { ...prev, touched }; + }); + + // NOTE(amine): validate inputs + if (validateOnSubmit && validate) { + const errors = validate(state.values, { ...state.errors }); + setState((prev) => ({ ...prev, errors })); + if (_hasError(errors)) return; + } + + // NOTE(amine): submit the form + if (!onSubmit) return; + setState((prev) => ({ ...prev, isSubmitting: true })); + onSubmit(state.values) + .then(() => { + setState((prev) => ({ ...prev, isSubmitting: false })); + }) + .catch(() => { + setState((prev) => ({ ...prev, isSubmitting: false })); + }); + }; + + // Note(Amine): this prop getter will overide the form onSubmit handler + const getFormProps = () => ({ + onSubmit: handleFormOnSubmit, + }); + + return { getFieldProps, getFormProps, values: state.values, isSubmitting: state.isSubmitting }; +}; + +/** NOTE(amine): Since we can use on our design system an input onSubmit, + * useField is a special case of useForm + */ +export const useField = ({ + onSubmit, + validate, + initialValue, + onChange, + onBlur, + validateOnBlur = true, + validateOnSubmit = true, +}) => { + const [state, setState] = React.useState({ + isSubmitting: false, + value: initialValue, + error: undefined, + touched: undefined, + }); + + const _mergeEventHandlers = (events = []) => (e) => + events.forEach((event) => { + if (event) event(e); + }); + + /** ---------- NOTE(amine): Input Handlers ---------- */ + const handleFieldChange = (e) => + setState((prev) => ({ + ...prev, + value: e.target.value, + error: undefined, + touched: false, + })); + + const handleOnBlur = (e) => { + // NOTE(amine): validate the inputs onBlur and touch the current input + let error = {}; + if (validateOnBlur && validate) error = validate(state.value); + setState((prev) => ({ ...prev, touched: validateOnBlur, error })); + }; + + const handleFormOnSubmit = (e) => { + //NOTE(amine): touch all inputs + setState((prev) => ({ ...prev, touched: true })); + + // NOTE(amine): validate inputs + if (validateOnSubmit && validate) { + const error = validate(state.value); + setState((prev) => ({ ...prev, error })); + if (error) return; + } + + // NOTE(amine): submit the form + if (!onSubmit) return; + setState((prev) => ({ ...prev, isSubmitting: true })); + onSubmit(state.value) + .then(() => { + setState((prev) => ({ ...prev, isSubmitting: false })); + }) + .catch(() => { + setState((prev) => ({ ...prev, isSubmitting: false })); + }); + }; + + // Note(Amine): this prop getter will capture the field state + const getFieldProps = (name) => ({ + name: name, + value: state.value, + error: state.error, + touched: state.touched, + onChange: _mergeEventHandlers([onChange, handleFieldChange]), + onBlur: _mergeEventHandlers([onBlur, handleOnBlur]), + onSubmit: handleFormOnSubmit, + }); + + return { getFieldProps, value: state.value, isSubmitting: state.isSubmitting }; +}; diff --git a/common/logo.js b/common/logo.js index 09f7287f..f923fa8e 100644 --- a/common/logo.js +++ b/common/logo.js @@ -1,9 +1,5 @@ export const Logo = (props) => ( - + @@ -17,11 +13,7 @@ export const Logo = (props) => ( export const Symbol = (props) => { return ( - + { ); }; +export const DarkSymbol = (props) => { + return ( + + + + + + + + + + + + + + + + + + ); +}; + export default Logo; diff --git a/common/messages.js b/common/messages.js index addba8ba..600d5a0d 100644 --- a/common/messages.js +++ b/common/messages.js @@ -131,12 +131,50 @@ export const error = { SERVER_CREATE_USER_ACCEPT_TERMS: "You must accept the terms of service to create an account", SERVER_CREATE_USER_USERNAME_TAKEN: "There is already an account with that username", SERVER_CREATE_USER_INVALID_USERNAME: "Please choose a valid username", - SERVER_CREATE_USER_INVALID_PASSWORD: "Please chooose a valid password", + SERVER_CREATE_USER_INVALID_PASSWORD: "Please choose a valid password", + SERVER_CREATE_USER_INVALID_EMAIL: "Please choose a valid email", SERVER_CREATE_USER_BUCKET_INIT_FAILURE: "We're having trouble setting up your storage, please try again later", SERVER_CREATE_USER_FAILED: "We're having trouble creating your account right now. Please try again later", + // Twitter + SERVER_CREATE_USER_TWITTER_EXISTS: "There is already an account linked with your twitter", + SERVER_TWITTER_OAUTH_NOT_ALLOWED: "You can only authenticate via twitter while on slate.host", + SERVER_TWITTER_LOGIN_ONLY: + "This login is associated with a Twitter account. Please continue with Twitter instead", + + // Email Verifications + SERVER_EMAIL_VERIFICATION_INVALID_PIN: "Please enter a valid pin", + SERVER_EMAIL_VERIFICATION_FAILED: + "We're having trouble with verifying your email, please try again later", + SERVER_CREATE_VERIFICATION_NOT_ALLOWED: + "You can only send a verification pin while on slate.host", + SERVER_CREATE_VERIFICATION_INVALID_EMAIL: "Please choose a valid email", + SERVER_CREATE_VERIFICATION_EMAIL_TAKEN: "There is already an account with this email", + SERVER_CREATE_VERIFICATION_FAILED: + "We're having touble sending a verification pin right now, please try again later", + SERVER_EMAIL_VERIFICATION_NOT_ALLOWED: "You can only verify an email while on slate.host", + + // Email Verification Legacy account + SERVER_CREATE_VERIFICATION_INVALID_USERNAME: "Please choose a valid username", + SERVER_CREATE_VERIFICATION_INVALID_PASSWORD: "Please choose a valid password", + SERVER_CREATE_VERIFICATION_WRONG_PASSWORD: + "We were unable to locate that account with those credentials", + + // Password Reset + SERVER_RESET_PASSWORD_NOT_ALLOWED: "You can only reset your password while on slate.host", + SERVER_CREATE_VERIFICATION_USER_NOT_FOUND: "We were not able to locate a user with this email", + SERVER_RESET_PASSWORD_FAILED: + "We're having touble resetting your password, please try again late", + SERVER_RESET_PASSWORD_NO_PASSWORD: "Please choose a valid password", + + // Migrate User + SERVER_MIGRATE_USER_NOT_ALLOWED: "You can only verify an email while on slate.host", + SERVER_MIGRATE_USER_NO_TOKEN: + "We're having touble sending a verification pin, please try again late", + SERVER_MIGRATE_USER_INVALID_PIN: "Please enter a valid pin", + //Get user SERVER_GET_USER_NO_USER_PROVIDED: "We were not able to fetch that user because no user was specified", @@ -159,7 +197,7 @@ export const error = { SERVER_USER_UPDATE_USERNAME_IS_TAKEN: "There is already an account with that username", SERVER_USER_UPDATE_DEFAULT_ARCHIVE_CONFIG: "We're having trouble updating your settings right now", - SERVER_USER_UPDATE_INVALID_PASSWORD: "Please chooose a valid password", + SERVER_USER_UPDATE_INVALID_PASSWORD: "Please choose a valid password", //Zip files GET_ZIP_FILES_PATHS_BUCKET_CHECK_FAILED: "We're having trouble locating those files right now", @@ -178,7 +216,7 @@ export const error = { SERVER_SIGN_IN_NO_USERNAME: "Please provide a username to sign in", SERVER_SIGN_IN_NO_PASSWORD: "Please provide a password to sign in", SERVER_SIGN_IN_USER_NOT_FOUND: "We were unable to locate that account with those credentials", - SERVER_SIGN_IN_WRONG_PASSWORD: "We were unable to locate that account with those credentials", + SERVER_SIGN_IN_WRONG_CREDENTIALS: "You have entered an invalid username or password", //Subscribe SERVER_SUBSCRIBE_MUST_PROVIDE_SLATE_OR_USER: "No collection or user to follow specified", diff --git a/common/navigation-data.js b/common/navigation-data.js index 6781dcbf..16ab21f9 100644 --- a/common/navigation-data.js +++ b/common/navigation-data.js @@ -34,10 +34,6 @@ export const getByHref = (href, viewer) => { return { page: { ...activityPage } }; } - if (viewer && pathname === "/_/auth") { - return { page: { ...activityPage } }; //NOTE(martina): authenticated users should be redirected to the home page rather than the - } - let page = navigation.find((each) => pathname.startsWith(each.pathname)); let details; @@ -98,6 +94,7 @@ const authPage = { pageTitle: "Sign in & Sign up", ignore: true, pathname: "/_/auth", + externalAllowed: true, }; const dataPage = { diff --git a/common/svg.js b/common/svg.js index ce3eaa7a..3ef49277 100644 --- a/common/svg.js +++ b/common/svg.js @@ -432,8 +432,8 @@ export const OldWallet = (props) => ( ); -export const NavigationArrow = (props) => ( - +export const RightArrow = (props) => ( + ( /> ); + +export const MehCircle = (props) => ( + + + +); + +export const SmileCircle = (props) => ( + + + + +); diff --git a/common/user-behaviors.js b/common/user-behaviors.js index a0db9ed3..af121ab4 100644 --- a/common/user-behaviors.js +++ b/common/user-behaviors.js @@ -52,7 +52,38 @@ export const authenticate = async (state) => { return response; }; -// NOTE(jim): Signs a user out +export const authenticateViaTwitter = (response) => { + // NOTE(jim): Kills existing session cookie if there is one. + const jwt = cookies.get(Credentials.session.key); + + if (jwt) { + cookies.remove(Credentials.session.key, { + path: "/", + maxAge: 3600 * 24 * 7, + sameSite: "strict", + }); + } + + if (Events.hasError(response)) { + return false; + } + + if (response.token) { + // NOTE(jim): + // + One week. + // + Only requests to the same site. + // + Not using sessionStorage so the cookie doesn't leave when the browser dies. + cookies.set(Credentials.session.key, response.token, { + path: "/", + maxAge: 3600 * 24 * 7, + sameSite: "strict", + }); + } + + return response; +}; + +// NOTE(jim): Signs a user out and redirects to the sign in screen. export const signOut = async ({ viewer }) => { let wsclient = Websockets.getClient(); if (wsclient) { diff --git a/common/utilities.js b/common/utilities.js index 2bcba5ee..5f0ec281 100644 --- a/common/utilities.js +++ b/common/utilities.js @@ -1,3 +1,5 @@ +import BCrypt from "bcryptjs"; + //NOTE(martina): this file is for utility functions that do not involve API calls //For API related utility functions, see common/user-behaviors.js //And for uploading related utility functions, see common/file-utilities.js @@ -42,6 +44,19 @@ export const endsWithAny = (options, string) => } }); +export const encryptPasswordClient = async (text) => { + const salt = "$2a$06$Yl.tEYt9ZxMcem5e6AbeUO"; + let hash = text; + const rounds = 5; + + for (let i = 1; i <= rounds; i++) { + hash = await BCrypt.hash(text, salt); + console.log(`\nPASSWORD HASH ROUND ${i} COMPLETE\n`); + } + + return hash; +}; + export const coerceToArray = (input) => { if (!input) { return []; diff --git a/common/validations.js b/common/validations.js index 003058df..2cec007d 100644 --- a/common/validations.js +++ b/common/validations.js @@ -5,7 +5,12 @@ import JSZip from "jszip"; const USERNAME_REGEX = new RegExp("^[a-zA-Z0-9_]{0,}[a-zA-Z]+[0-9]*$"); const MIN_PASSWORD_LENGTH = 8; -const EMAIL_REGEX = /^[\w-]+@[a-zA-Z0-9_]+?\.[a-zA-Z]{2,50}$/; +const EMAIL_REGEX = /^[\w.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9_]+?\.[a-zA-Z]{2,50}$/; +const CONTAINS_DIGIT_REGEX = /\d/; +const CONTAINS_UPPERCASE_REGEX = /[A-Z]/; +const CONTAINS_LOWERCASE_REGEX = /[a-z]/; +const CONTAINS_SYMBOL_REGEX = /[ !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~]/; +const PIN_REGEX = /^[0-9]{6}$/; // TODO(jim): Regex should cover some of this. const REJECT_LIST = [ @@ -98,7 +103,30 @@ export const username = (text) => { return true; }; -export const password = (text) => { +export const email = (text) => { + if (Strings.isEmpty(text)) { + return false; + } + + if (text.length > 254 || text.length < 5) { + return false; + } + + if (!EMAIL_REGEX.test(text)) { + return false; + } + + //NOTE(toast): add this if the sendgrid plan is upgraded + // const sgEmailValidation = validateEmail({ email: text }); + // if (sgEmailValidation.verdict !== "Valid") { + // return false; + // } + + return true; +}; + +// NOTE(amine): used to validate old users password (currently only used in signin) +export const legacyPassword = (text) => { if (Strings.isEmpty(text)) { return false; } @@ -110,36 +138,59 @@ export const password = (text) => { return true; }; -export const isFileTypeAllowed = (type = "") => { - if (type.startsWith("text/")) { - return true; +export const passwordForm = (text) => { + const validations = { + validLength: false, + containsLowerCase: false, + containsUpperCase: false, + containsSymbol: false, + containsNumbers: false, + }; + + if (Strings.isEmpty(text)) return validations; + + if (text.length >= MIN_PASSWORD_LENGTH) validations.validLength = true; + if (CONTAINS_DIGIT_REGEX.test(text)) validations.containsNumbers = true; + if (CONTAINS_LOWERCASE_REGEX.test(text)) validations.containsLowerCase = true; + if (CONTAINS_UPPERCASE_REGEX.test(text)) validations.containsUpperCase = true; + if (CONTAINS_SYMBOL_REGEX.test(text)) validations.containsSymbol = true; + + return validations; +}; + +export const password = (text) => { + if (Strings.isEmpty(text)) { + return false; } - if (type.startsWith("model/")) { - return true; + if (text.length <= MIN_PASSWORD_LENGTH) { + return false; } - if (type.startsWith("font/")) { - return true; + let reqCount = 0; + + if (CONTAINS_DIGIT_REGEX.test(text)) { + reqCount += 1; + } + if (CONTAINS_LOWERCASE_REGEX.test(text)) { + reqCount += 1; + } + if (CONTAINS_UPPERCASE_REGEX.test(text)) { + reqCount += 1; + } + if (CONTAINS_SYMBOL_REGEX.test(text)) { + reqCount += 1; } - if (type.startsWith("application/")) { - return true; + return reqCount === 4; +}; + +export const verificationPin = (pin) => { + if (Strings.isEmpty(pin)) { + return false; } - if (type.startsWith("audio/")) { - return true; - } - - if (type.startsWith("image/")) { - return true; - } - - if (type.startsWith("video/")) { - return true; - } - - return false; + return PIN_REGEX.test(pin); }; export const isPreviewableImage = (type = "") => { diff --git a/components/core/Alert.js b/components/core/Alert.js index 56489234..0099934a 100644 --- a/components/core/Alert.js +++ b/components/core/Alert.js @@ -74,6 +74,11 @@ const STYLES_TEXT = css` const STYLES_MESSAGE_BOX = css` display: flex; align-items: center; + + //Note(amine): remove bottom padding from svg + svg { + display: block; + } `; export class Alert extends React.Component { diff --git a/components/core/Application.js b/components/core/Application.js index 083b5611..0341997d 100644 --- a/components/core/Application.js +++ b/components/core/Application.js @@ -22,7 +22,7 @@ import SceneFilesFolder from "~/scenes/SceneFilesFolder"; import SceneSettings from "~/scenes/SceneSettings"; import SceneSlates from "~/scenes/SceneSlates"; import SceneSettingsDeveloper from "~/scenes/SceneSettingsDeveloper"; -import SceneSignIn from "~/scenes/SceneSignIn"; +import SceneAuth from "~/scenes/SceneAuth"; import SceneSlate from "~/scenes/SceneSlate"; import SceneActivity from "~/scenes/SceneActivity"; import SceneDirectory from "~/scenes/SceneDirectory"; @@ -77,7 +77,7 @@ const SIDEBARS = { const SCENES = { NAV_ERROR: , - NAV_SIGN_IN: , + NAV_SIGN_IN: , NAV_ACTIVITY: , NAV_DIRECTORY: , NAV_PROFILE: , @@ -461,27 +461,30 @@ export default class ApplicationPage extends React.Component { e.preventDefault(); }; - _handleCreateUser = async (state) => { - let response = await Actions.createUser(state); + // _handleCreateUser = async (state) => { + // let response = await Actions.createUser(state); - if (!response || response.error) { + // if (Events.hasError(response)) { + // return; + // } + + // return this._handleAuthenticate(state, true); + // }; + + _withAuthenticationBehavior = (authenticate) => async (state, newAccount) => { + let response = await authenticate(state); + if (Events.hasError(response)) { return response; } - - return this._handleAuthenticate(state, true); - }; - - _handleAuthenticate = async (state, newAccount) => { - let response = await UserBehaviors.authenticate(state); - if (!response || response.error) { + if (response.shouldMigrate) { return response; } let viewer = await UserBehaviors.hydrate(); - if (!viewer || viewer.error) { + if (Events.hasError(viewer)) { return viewer; } - this.setState({ viewer }); + this.setState({ viewer }, () => console.log(this.state.viewer)); await this._handleSetupWebsocket(); let unseenAnnouncements = []; @@ -516,6 +519,8 @@ export default class ApplicationPage extends React.Component { // if (!redirected) { // this._handleAction({ type: "NAVIGATE", value: "NAV_DATA" }); // } + window.location.replace("/_/activity"); + return response; }; @@ -714,8 +719,8 @@ export default class ApplicationPage extends React.Component { viewer: this.state.viewer, selected: this.state.selected, onSelectedChange: this._handleSelectedChange, - onAuthenticate: this._handleAuthenticate, - onCreateUser: this._handleCreateUser, + onAuthenticate: this._withAuthenticationBehavior(UserBehaviors.authenticate), + onTwitterAuthenticate: this._withAuthenticationBehavior(UserBehaviors.authenticateViaTwitter), onAction: this._handleAction, onUpload: this._handleUploadFiles, isMobile: this.state.isMobile, diff --git a/components/core/Auth/Initial.js b/components/core/Auth/Initial.js new file mode 100644 index 00000000..69d57239 --- /dev/null +++ b/components/core/Auth/Initial.js @@ -0,0 +1,227 @@ +import * as React from "react"; +import * as System from "~/components/system"; +import * as SVG from "~/common/svg"; +import * as Actions from "~/common/actions"; +import * as Validations from "~/common/validations"; +import * as Events from "~/common/custom-events"; +import * as Strings from "~/common/strings"; +import * as Styles from "~/common/styles"; + +import { css } from "@emotion/react"; +import { motion, AnimateSharedLayout } from "framer-motion"; +import { useForm, useField } from "~/common/hooks"; +import Field from "~/components/core/Field"; +import { Toggle, SignUpPopover } from "~/components/core/Auth/components"; + +const STYLES_INITIAL_CONTAINER = css` + display: flex; + flex-direction: column; + height: 100%; +`; +const STYLES_LINK_ITEM = (theme) => css` + display: block; + text-decoration: none; + font-weight: 400; + font-size: 14px; + font-family: ${theme.font.medium}; + user-select: none; + cursor: pointer; + margin-top: 2px; + color: ${theme.system.black}; + transition: 200ms ease all; + word-wrap: break-word; + + :visited { + color: ${theme.system.black}; + } + + :hover { + color: ${theme.system.brand}; + } +`; + +// NOTE(amine): used to remove content jumping +// when switching between signin/signup in mobile +const STYLES_SPACER = (theme) => css` + height: 20px; + @media (max-width: ${theme.sizes.mobile}px) { + height: 10vh; + } +`; + +const useToggler = ({ params }) => { + const TOGGLE_OPTIONS = [ + { value: "signin", label: "Sign in" }, + { value: "signup", label: "Sign up" }, + ]; + const [state, setState] = React.useState(params?.tab || "signin"); + const handleToggleChange = (value) => setState(value); + return { toggleValue: state, handleToggleChange, TOGGLE_OPTIONS }; +}; + +export default function Initial({ + isSigninViaTwitter, + goToSigninScene, + onTwitterSignin, + goToSignupScene, + createVerification, + initialEmail, + page, +}) { + const { TOGGLE_OPTIONS, toggleValue, handleToggleChange } = useToggler(page); + + // NOTE(amine): Signup view form + const { getFieldProps, getFormProps, isSubmitting: isCheckingEmail } = useForm({ + validateOnBlur: false, + initialValues: { email: initialEmail || "" }, + validate: ({ email }, errors) => { + if (Strings.isEmpty(email)) { + errors.email = "Please provide an email"; + } else if (!Validations.email(email)) { + errors.email = "Invalid email address"; + } + return errors; + }, + onSubmit: async ({ email }) => { + const response = await Actions.checkEmail({ email }); + if (response?.data?.twitter) { + Events.dispatchMessage({ + message: "This email is associated with a Twitter account, please log in with Twitter", + }); + return; + } + if (response?.data?.email) { + goToSigninScene({ + emailOrUsername: email, + message: `There is already an account associated with ${email}`, + }); + return; + } + + const verificationResponse = await createVerification({ email }); + if (!verificationResponse) return; + goToSignupScene({ email }); + }, + }); + + // NOTE(amine): Signin view form + const { getFieldProps: getSigninFieldProps } = useField({ + validateOnBlur: false, + onSubmit: async (emailOrUsername) => goToSigninScene({ emailOrUsername }), + validate: (emailOrUsername) => { + if (Strings.isEmpty(emailOrUsername)) return "Please enter a username or email"; + if (!Validations.username(emailOrUsername) && !Validations.email(emailOrUsername)) + return "Invalid email/username"; + }, + initialValue: "", + }); + + return ( + Discover, experience, share files on Slate} + style={{ paddingBottom: 24 }} + > +
+
+
+ + Continue with Twitter + + + +
+ {toggleValue === "signin" ? ( + <> + + +
+ +
+ Terms of service +
+
+ + +
+ Community guidelines +
+
+
+ + ) : ( + +
+ + + + + Send verification link + + + + +
+ )} +
+ + ); +} diff --git a/components/core/Auth/ResetPassword.js b/components/core/Auth/ResetPassword.js new file mode 100644 index 00000000..8e4add2d --- /dev/null +++ b/components/core/Auth/ResetPassword.js @@ -0,0 +1,183 @@ +import * as React from "react"; +import * as System from "~/components/system"; +import * as Validations from "~/common/validations"; +import * as Actions from "~/common/actions"; +import * as Utilities from "~/common/utilities"; +import * as SVG from "~/common/svg"; + +import Field from "~/components/core/Field"; + +import { AnimateSharedLayout, motion } from "framer-motion"; +import { P } from "~/components/system"; +import { useForm } from "~/common/hooks"; +import { css } from "@emotion/react"; + +import { SignUpPopover, Verification } from "~/components/core/Auth/components"; + +const STYLES_BACK_BUTTON = css` + background: none; + border-style: none; + cursor: pointer; + margin-top: auto; + margin-right: auto; + span { + display: flex; + align-items: center; + justify-content: center; + & > * + * { + margin-left: 8px; + } + } +`; + +const handleValidation = ({ email }, errors) => { + if (!Validations.email(email)) errors.email = "Invalid email"; + return errors; +}; +const handleNewPasswordValidation = ({ password }, errors) => { + if (!Validations.password(password)) errors.password = "Invalid Password"; + return errors; +}; + +const usePasswordReset = () => { + // NOTE(amine): can be either initial || verification || new_password + const [scene, setScene] = React.useState("initial"); + const handlers = React.useMemo( + () => ({ + goToVerificationScene: () => setScene("verification"), + goToNewPasswordScene: () => setScene("new_password"), + }), + [] + ); + return { ...handlers, scene }; +}; +export default function ResetPassword({ + goBack, + createVerification, + verifyEmail, + resetPassword, + resendEmailVerification, +}) { + const [passwordValidations, setPasswordValidations] = React.useState( + Validations.passwordForm("") + ); + + const { scene, goToNewPasswordScene, goToVerificationScene } = usePasswordReset(); + + // NOTE(amine): Asking for email scene form + const { getFieldProps, getFormProps, isSubmitting, values } = useForm({ + validateOnBlur: false, + initialValues: { email: "" }, + validate: handleValidation, + onSubmit: async ({ email }) => { + const response = await createVerification({ email }); + if (!response) return; + goToVerificationScene(); + }, + }); + + // NOTE(amine): New password scene form + const { + getFieldProps: getNewPasswordFieldProps, + getFormProps: getNewPasswordFormProps, + isSubmitting: isNewPasswordFormSubmitting, + } = useForm({ + validateOnBlur: false, + initialValues: { password: "" }, + validate: handleNewPasswordValidation, + onSubmit: async ({ password }) => { + await resetPassword({ password }); + }, + }); + + if (scene === "verification") { + const handleVerification = async ({ pin }) => { + const response = await verifyEmail({ pin }); + if (!response) return; + goToNewPasswordScene(); + }; + return ( + + ); + } + + if (scene === "new_password") { + return ( + Enter new password}> +
+ { + const validations = Validations.passwordForm(e.target.value); + setPasswordValidations(validations); + }, + })} + style={{ backgroundColor: "rgba(242,242,247,0.5)" }} + /> + + + + Log in with new password + + + + +
+ ); + } + + return ( + + Enter your email
to reset password + + } + > +
+ + + + + Send password reset code + + + + + +
+ ); +} diff --git a/components/core/Auth/Signin.js b/components/core/Auth/Signin.js new file mode 100644 index 00000000..3055a151 --- /dev/null +++ b/components/core/Auth/Signin.js @@ -0,0 +1,229 @@ +import * as React from "react"; +import * as System from "~/components/system"; +import * as Validations from "~/common/validations"; +import * as Strings from "~/common/strings"; +import * as Constants from "~/common/constants"; +import * as SVG from "~/common/svg"; +import * as Styles from "~/common/styles"; + +import Field from "~/components/core/Field"; + +import { P } from "~/components/system"; +import { AnimateSharedLayout, motion } from "framer-motion"; +import { useForm } from "~/common/hooks"; +import { css } from "@emotion/react"; +import { SignUpPopover, Verification } from "~/components/core/Auth/components"; + +const STYLES_BACK_BUTTON = css` + color: ${Constants.system.textGrayDark}; + background: none; + border-style: none; + cursor: pointer; + margin-top: auto; + margin-right: auto; + + span { + display: flex; + align-items: center; + justify-content: center; + & > * + * { + margin-left: 4px; + } + } +`; + +const STYLES_FORGOT_PASSWORD_BUTTON = (theme) => css` + display: block; + background: none; + border: none; + margin: 16px auto 0px; + padding: 8px 0; + cursor: pointer; + & > * { + font-size: ${theme.typescale.lvl0}; + color: ${theme.system.blue}; + } +`; + +const STYLES_MESSAGE = (theme) => css` + display: flex; + align-items: flex-start; + justify-content: space-between; + margin-top: 16px; + border: 1px solid ${theme.system.white}; + background-color: white; + @supports ((-webkit-backdrop-filter: blur(25px)) or (backdrop-filter: blur(25px))) { + background-color: ${theme.system.bgBlurWhiteTRN}; + backdrop-filter: blur(75px); + } + padding: 8px 12px; + border-radius: 8px; + & > * + * { + margin-left: 16px; + } +`; + +const STYLES_MESSAGE_PARAGRAPH = (theme) => css` + font-size: ${theme.typescale.lvlN1}; + color: ${theme.system.blue}; +`; + +const STYLES_MESSAGE_BUTTON = (theme) => css` + background: none; + border: none; + padding: 0; + margin: 0; + svg { + color: ${theme.system.textGrayDark}; + height: 16px; + width: 16px; + } +`; + +const handleValidation = ({ password }, errors) => { + if (!Validations.legacyPassword(password)) errors.password = "Incorrect password"; + return errors; +}; + +const handleNewPasswordValidation = ({ email }, errors) => { + if (Strings.isEmpty(email)) errors.email = "Please enter an email"; + if (!Validations.email(email)) errors.email = "Invalid email"; + return errors; +}; + +const useSignin = () => { + // NOTE(amine): can be either initial || email_request || verification + const [scene, setScene] = React.useState("initial"); + const handlers = React.useMemo( + () => ({ + goToEmailRequestScene: () => setScene("email_request"), + goToVerificationScene: () => setScene("verification"), + }), + [] + ); + return { ...handlers, scene }; +}; + +export default function Signin({ + emailOrUsername = "", + message, + signin, + createVerification, + migrateAccount, + goToResetPassword, + resendEmailVerification, + goBack, + clearMessages, +}) { + const { scene, goToEmailRequestScene, goToVerificationScene } = useSignin(); + + const [showPassword, toggleShowPassword] = React.useState(false); + + // NOTE(amine): Signin Form + const { getFieldProps, getFormProps, isSubmitting } = useForm({ + validateOnBlur: false, + initialValues: { username: emailOrUsername, password: "" }, + validate: handleValidation, + onSubmit: async ({ username, password }) => { + const response = await signin({ username, password }); + if (response?.shouldMigrate) goToEmailRequestScene(); + }, + }); + + // NOTE(amine): verify email scene form + const { + getFieldProps: getEmailFieldProps, + getFormProps: getEmailFormProps, + isSubmitting: isEmailFormSubmitting, + } = useForm({ + validateOnBlur: false, + initialValues: { email: "" }, + validate: handleNewPasswordValidation, + onSubmit: async ({ email }) => { + const response = await createVerification({ email }); + if (!response) return; + goToVerificationScene(); + }, + }); + + if (scene === "verification") { + const handleVerification = async ({ pin }) => await migrateAccount({ pin }); + return ; + } + + if (scene === "email_request") { + return ( + +
+ + + + + Send verification code + + + + +
+ ); + } + return ( + +
+ {message && ( +
+

{message}

+ +
+ )} + toggleShowPassword(!showPassword)} + icon={showPassword ? SVG.EyeOff : SVG.Eye} + {...getFieldProps("password")} + style={{ backgroundColor: "rgba(242,242,247,0.5)" }} + /> + + + + Sign in + + + + + + +
+ ); +} diff --git a/components/core/Auth/Signup.js b/components/core/Auth/Signup.js new file mode 100644 index 00000000..7435086b --- /dev/null +++ b/components/core/Auth/Signup.js @@ -0,0 +1,100 @@ +import * as React from "react"; +import * as System from "~/components/system"; +import * as Validations from "~/common/validations"; + +import Field from "~/components/core/Field"; + +import { AnimateSharedLayout, motion } from "framer-motion"; +import { useForm } from "~/common/hooks"; +import { SignUpPopover, Verification, AuthCheckBox } from "~/components/core/Auth/components"; + +const useSignup = () => { + const [scene, setScene] = React.useState("verification"); + const handlers = React.useMemo( + () => ({ + goToAccountCreationScene: () => setScene("accountCreation"), + }), + [] + ); + return { ...handlers, scene }; +}; + +const handleValidation = ({ username, password, acceptTerms }, errors) => { + if (!Validations.username(username)) errors.username = "Invalid username"; + // Note(amine): username should not be an email + if (Validations.email(username)) errors.username = "Username shouldn't be an email"; + + if (!Validations.password(password)) errors.password = "Incorrect password"; + + if (!acceptTerms) errors.acceptTerms = "Must accept terms and conditions"; + return errors; +}; + +export default function Signup({ verifyEmail, createUser, resendEmailVerification }) { + const [passwordValidations, setPasswordValidations] = React.useState( + Validations.passwordForm("") + ); + const { goToAccountCreationScene, scene } = useSignup(); + + const { getFieldProps, getFormProps, isSubmitting } = useForm({ + initialValues: { username: "", password: "", acceptTerms: false }, + validate: handleValidation, + onSubmit: async ({ username, password }) => await createUser({ username, password }), + }); + + if (scene === "verification") { + const handleVerification = async ({ pin }) => { + const response = await verifyEmail({ pin }); + if (response) { + goToAccountCreationScene(); + } + }; + return ; + } + + return ( + + +
+ + + + { + const validations = Validations.passwordForm(e.target.value); + setPasswordValidations(validations); + }, + })} + style={{ backgroundColor: "rgba(242,242,247,0.5)" }} + /> + + + + + Create account + + + +
+
+ ); +} diff --git a/components/core/Auth/TwitterSignup.js b/components/core/Auth/TwitterSignup.js new file mode 100644 index 00000000..7deccb1d --- /dev/null +++ b/components/core/Auth/TwitterSignup.js @@ -0,0 +1,122 @@ +import * as React from "react"; +import * as System from "~/components/system"; +import * as Validations from "~/common/validations"; + +import Field from "~/components/core/Field"; + +import { AnimateSharedLayout, motion } from "framer-motion"; +import { css } from "@emotion/react"; +import { useForm } from "~/common/hooks"; + +import { SignUpPopover, Verification, AuthCheckBox } from "~/components/core/Auth/components"; + +const STYLES_SMALL = (theme) => css` + font-size: ${theme.typescale.lvlN1}; + text-align: center; + color: ${theme.system.textGrayDark}; + max-width: 228px; + margin: 0 auto; +`; + +const useTwitterSignup = () => { + const [scene, setScene] = React.useState("accountCreation"); + const handlers = React.useMemo(() => ({ goToVerification: () => setScene("verification") }), []); + return { ...handlers, scene }; +}; + +const handleValidation = ({ username, email, acceptTerms }, errors) => { + if (!Validations.username(username)) errors.username = "Invalid username"; + // Note(amine): username should not be an email + if (Validations.email(username)) errors.username = "Username shouldn't be an email"; + + if (!Validations.email(email)) errors.email = "Invalid email"; + + if (!acceptTerms) errors.acceptTerms = "Must accept terms and conditions"; + + return errors; +}; + +const MotionLayout = ({ children, ...props }) => ( + + {children} + +); + +export default function TwitterSignup({ + initialEmail, + onSignup, + createVerification, + onSignupWithVerification, +}) { + const { scene, goToVerification } = useTwitterSignup(); + const { + getFieldProps, + getFormProps, + values: { email, username }, + isSubmitting, + } = useForm({ + initialValues: { username: "", email: initialEmail, acceptTerms: false }, + validate: handleValidation, + onSubmit: async ({ username, email }) => { + if (email !== initialEmail) { + const response = await createVerification({ email }); + if (!response) return; + goToVerification(); + return; + } + await onSignup({ email, username }); + }, + }); + + if (scene === "verification") { + const handleVerification = async ({ pin }) => { + await onSignupWithVerification({ username, pin }); + }; + return ; + } + + return ( + +
+ + + + + + + + Create account + + + {(!initialEmail || initialEmail !== email) && ( + + + You will receive a code to verify your email at this address + + + )} + + +
+ ); +} diff --git a/components/core/Auth/components/AuthCheckBox.js b/components/core/Auth/components/AuthCheckBox.js new file mode 100644 index 00000000..82a961e5 --- /dev/null +++ b/components/core/Auth/components/AuthCheckBox.js @@ -0,0 +1,64 @@ +import * as React from "react"; +import * as System from "~/components/system"; +import { css } from "@emotion/react"; + +const STYLES_CHECKBOX_LABEL = (theme) => css` + padding-left: 8px; + a, + a:link, + a:visited { + color: ${theme.system.blue}; + text-decoration: none; + } + a:hover { + text-decoration: underline; + } +`; + +const STYLES_CHECKBOX_INPUT = css` + background-color: rgba(242, 242, 247, 0.5); + height: 16px; + width: 16px; +`; + +const STYLES_CHECKBOX_ERROR = (theme) => css` + background-color: rgba(242, 242, 247, 0.5); + border: 1px solid ${theme.system.red}; + height: 16px; + width: 16px; +`; + +const STYLES_CHECKBOX_SUCCESS = (theme) => css` + background-color: rgba(242, 242, 247, 0.5); + border: 1px solid ${theme.system.green}; + height: 16px; + width: 16px; +`; + +const STYLES_CHECKBOX_WRAPPER = css` + align-items: center; +`; + +export default function AuthCheckBox({ touched, error, ...props }) { + const showError = touched && error; + const showSuccess = touched && !error; + + const STYLES_CHECKBOX = React.useMemo(() => { + if (showError) return STYLES_CHECKBOX_ERROR; + return STYLES_CHECKBOX_INPUT; + }, [touched, error]); + + return ( + + I agree to the Slate{" "} + + terms of service + + + ); +} diff --git a/components/core/Auth/components/SignUpPopover.js b/components/core/Auth/components/SignUpPopover.js new file mode 100644 index 00000000..87c4f410 --- /dev/null +++ b/components/core/Auth/components/SignUpPopover.js @@ -0,0 +1,74 @@ +import * as React from "react"; +import * as System from "~/components/system"; + +import { DarkSymbol } from "~/common/logo"; +import { css } from "@emotion/react"; + +const STYLES_POPOVER = (theme) => css` + display: flex; + flex-direction: column; + box-sizing: border-box; + max-width: 432px; + height: 544px; + width: 95vw; + border-radius: 8px; + padding: 36px 32px; + + @media (max-width: ${theme.sizes.mobile}px) { + flex-grow: 1; + margin-bottom: auto; + max-width: 100%; + } + + background-color: white; + + @supports ((-webkit-backdrop-filter: blur(25px)) or (backdrop-filter: blur(25px))) { + background: radial-gradient( + 80.79% 80.79% at 50% 50%, + rgba(242, 242, 247, 0.5) 0%, + rgba(242, 242, 247, 0) 100% + ); + backdrop-filter: blur(75px); + } + + @keyframes authentication-popover-fade-in { + from { + transform: translateY(-8px); + opacity: 0; + } + + to { + transform: translateY(0px); + opacity: 1; + } + } + + animation: authentication-popover-fade-in 400ms ease; +`; + +const STYLES_POPOVER_BODY = (theme) => css` + display: flex; + height: 100%; + flex-direction: column; + justify-content: flex-start; + @media (max-width: ${theme.sizes.mobile}px) { + justify-content: center; + } +`; +export default function SignUpPopover({ children, title, logoStyle, titleStyle, props }) { + return ( +
+
+
+ +
+ + {title} + +
+
{children}
+
+ ); +} diff --git a/components/core/Auth/components/Toggle.js b/components/core/Auth/components/Toggle.js new file mode 100644 index 00000000..f82bad6c --- /dev/null +++ b/components/core/Auth/components/Toggle.js @@ -0,0 +1,75 @@ +import * as React from "react"; + +import { motion, AnimateSharedLayout } from "framer-motion"; +import { css } from "@emotion/react"; + +const STYLES_WRAPPER = (theme) => css` + width: fit-content; + display: flex; + background-color: ${theme.system.bgGrayLight}; + border-radius: 8px; +`; + +const STYLES_BUTTON = (theme) => css` + border-style: none; + padding: 8px 16px; + border-radius: 8px; + font-family: ${theme.font.medium}; + font-size: ${theme.typescale.lvl0}; + color: ${theme.system.textGray}; + background-color: transparent; + outline-style: none; + cursor: pointer; + transition: 0.2 color; +`; + +const STYLES_ACTIVE = (theme) => css` + background-color: ${theme.system.white}; + color: ${theme.system.textBlack}; +`; + +const STYLES_BUTTON_ACTIVE = (theme) => css` + color: ${theme.system.textBlack}; +`; + +export default function Toggle({ options = [], onChange, toggleValue = "signin", ...props }) { + const initialValue = toggleValue === "signup" ? 1 : 0; + const [currentOption, setOption] = React.useState(options[initialValue]); + const handleChange = (nextValue) => { + if (onChange) onChange(nextValue.value); + setOption(nextValue); + }; + return ( + +
+ {options.map((option) => ( +
+ + {option.value === currentOption.value && ( + + )} +
+ ))} +
+
+ ); +} diff --git a/components/core/Auth/components/Verification.js b/components/core/Auth/components/Verification.js new file mode 100644 index 00000000..a6abfd82 --- /dev/null +++ b/components/core/Auth/components/Verification.js @@ -0,0 +1,126 @@ +import * as React from "react"; +import * as System from "~/components/system"; +import * as SVG from "~/common/svg"; +import * as Validations from "~/common/validations"; + +import Field from "~/components/core/Field"; + +import { AnimateSharedLayout, motion } from "framer-motion"; +import { LoaderSpinner } from "~/components/system/components/Loaders"; +import { css } from "@emotion/react"; +import { useField } from "~/common/hooks"; +import { SignUpPopover } from "~/components/core/Auth/components"; + +const STYLES_HELPER = (theme) => css` + text-align: center; + margin-top: 8px; + font-size: ${theme.typescale.lvl0}; + color: ${theme.system.textGrayDark}; +`; + +const STYLES_RESEND_BUTTON = (theme) => css` + padding: 0; + margin: 0; + font-size: ${theme.typescale.lvl0}; + color: ${theme.system.blue}; + border: none; + background: transparent; + cursor: pointer; +`; + +const getResendText = ({ status, timeLeft }) => { + if (status === "ready") return "Resend code."; + if (status === "sending") return "Sending code..."; + if (status === "sent") return "Code sent."; + return `Resend code in ${timeLeft}s`; +}; + +const ResendButton = ({ onResend }) => { + // NOTE(amine): we have 4 status: ready, sending, sent, timeout + const [status, setStatus] = React.useState("ready"); + const [timer, setTimer] = React.useState(35); + + const handleResend = async () => { + if (status === "ready") { + setStatus("sending"); + await onResend(); + setStatus("sent"); + return; + } + + if (status === "sent") { + setStatus("timeout"); + } + }; + + // NOTE(amine): when the timer hits 0, + React.useEffect(() => { + if (timer === 0) setStatus("ready"); + }, [timer]); + + React.useEffect(() => { + let interval; + // NOTE(amine): start a timer when the email is sent + if (status === "sent") { + // NOTE(amine): reset timer to 35s + setTimer(35); + interval = setInterval(() => { + setTimer((prev) => prev - 1); + //NOTE(amine): clear interval when the timer is over. + if (timer === 0) clearInterval(interval); + }, 1000); + } + }, [status]); + + return ( + + {getResendText({ status, timeLeft: timer })} + + ); +}; + +const DEFAULT_TITLE = ( + <> + Verification code sent, +
+ please check your inbox. + +); + +export default function Verification({ onVerify, title = DEFAULT_TITLE, onResend }) { + const { getFieldProps, isSubmitting } = useField({ + validateOnBlur: false, + initialValue: "", + onSubmit: async (pin) => onVerify({ pin }), + validate: (pin) => { + if (!Validations.verificationPin(pin)) return "Invalid pin"; + }, + }); + + return ( + + + : SVG.RightArrow + } + containerStyle={{ marginTop: "28px" }} + style={{ backgroundColor: "rgba(242,242,247,0.5)" }} + name="pin" + type="pin" + {...getFieldProps()} + /> + + + + Didn’t receive an email? + + + + + ); +} diff --git a/components/core/Auth/components/index.js b/components/core/Auth/components/index.js new file mode 100644 index 00000000..3cb104d2 --- /dev/null +++ b/components/core/Auth/components/index.js @@ -0,0 +1,4 @@ +export { default as Toggle } from "~/components/core/Auth/components/Toggle"; +export { default as SignUpPopover } from "~/components/core/Auth/components/SignUpPopover"; +export { default as Verification } from "~/components/core/Auth/components/Verification"; +export { default as AuthCheckBox } from "~/components/core/Auth/components/AuthCheckBox"; diff --git a/components/core/Auth/index.js b/components/core/Auth/index.js new file mode 100644 index 00000000..436404b6 --- /dev/null +++ b/components/core/Auth/index.js @@ -0,0 +1,5 @@ +export { default as Signin } from "~/components/core/Auth/Signin"; +export { default as Signup } from "~/components/core/Auth/Signup"; +export { default as Initial } from "~/components/core/Auth/Initial"; +export { default as TwitterSignup } from "~/components/core/Auth/TwitterSignup"; +export { default as ResetPassword } from "~/components/core/Auth/ResetPassword"; diff --git a/components/core/Field.js b/components/core/Field.js new file mode 100644 index 00000000..55c0f5a8 --- /dev/null +++ b/components/core/Field.js @@ -0,0 +1,140 @@ +import * as React from "react"; +import * as SVG from "~/common/svg"; + +import { P } from "~/components/system/components/Typography"; +import { Input } from "~/components/system"; +import { css } from "@emotion/react"; + +const STYLES_SMALL_TEXT = (theme) => css` + font-size: ${theme.typescale.lvlN1}; + line-height: 15.6px; +`; + +const STYLES_PASSWORD_VALIDATIONS = (theme) => css` + margin-top: 16px; + border: 1px solid ${theme.system.white}; + background-color: white; + @supports ((-webkit-backdrop-filter: blur(25px)) or (backdrop-filter: blur(25px))) { + background-color: ${theme.system.bgBlurWhiteTRN}; + backdrop-filter: blur(75px); + } + padding: 8px 12px; + border-radius: 8px; +`; + +const STYLES_PASSWORD_VALIDATION = (theme) => + css` + display: flex; + align-items: center; + & > * + * { + margin-left: 8px; + } + `; + +const STYLES_CIRCLE = (theme) => css` + height: 8px; + width: 8px; + border-radius: 50%; + border: 1.25px solid ${theme.system.grayLight2}; +`; + +const STYLES_CIRCLE_SUCCESS = (theme) => css` + border: 1.25px solid ${theme.system.green}; +`; + +const STYLES_INPUT = (theme) => css` + background-color: rgba(242, 242, 247, 0.5); + border-radius: 8px; + &::placeholder { + color: ${theme.system.textGrayDark}; + } +`; +const STYLES_INPUT_ERROR = (theme) => css` + background-color: rgba(242, 242, 247, 0.5); + border: 1px solid ${theme.system.red}; + &::placeholder { + color: ${theme.system.textGrayDark}; + } +`; +const STYLES_INPUT_SUCCESS = (theme) => css` + background-color: rgba(242, 242, 247, 0.5); + border: 1px solid ${theme.system.green}; + &::placeholder { + color: ${theme.system.textGrayDark}; + } +`; + +const PasswordValidations = ({ validations }) => { + return ( +
+

Passwords should

+
+
+

Be at least 8 characters long

+
+
+
+

Contain both uppercase and lowercase letters

+
+
+
+

Contain at least 1 number

+
+
+
+

Contain at least 1 symbol

+
+
+ ); +}; + +export default function Field({ + touched, + error, + icon, + validations, + errorAs, + containerAs, + ...props +}) { + const showError = touched && error; + const showSuccess = touched && !error; + + // const Icon = React.useMemo(() => { + // if (icon) return showError ? SVG.MehCircle : icon; + + // if (showError) return SVG.MehCircle; + + // if (showSuccess) return SVG.SmileCircle; + // }, [touched, error]); + + const STYLES = React.useMemo(() => { + if (showError) return STYLES_INPUT_ERROR; + if (showSuccess) return STYLES_INPUT_SUCCESS; + return STYLES_INPUT; + }, [touched, error]); + + const ContainerComponent = containerAs || "div"; + const ErrorWrapper = errorAs || "div"; + return ( +
+ + + + {props.type === "password" && validations ? ( + + ) : ( + +

+ {showError && error} +

+
+ )} +
+ ); +} diff --git a/components/core/FontFrame/Settings/Controls.js b/components/core/FontFrame/Settings/Controls.js index f063e803..31ecdbf4 100644 --- a/components/core/FontFrame/Settings/Controls.js +++ b/components/core/FontFrame/Settings/Controls.js @@ -6,7 +6,7 @@ import { P } from "~/components/system/components/Typography"; import { Slider } from "~/components/system/components/Slider"; import { css } from "@emotion/react"; -import Select from "./Select"; +import Select from "~/components/core/FontFrame/Settings/Select"; const STYLES_LABEL = (theme) => css` font-size: 0.875rem; diff --git a/components/core/FontFrame/Settings/FixedControls.js b/components/core/FontFrame/Settings/FixedControls.js index eea6f8b0..60392986 100644 --- a/components/core/FontFrame/Settings/FixedControls.js +++ b/components/core/FontFrame/Settings/FixedControls.js @@ -4,7 +4,7 @@ import * as Strings from "~/common/strings"; import { css } from "@emotion/react"; -import { ContentControl } from "./Controls"; +import { ContentControl } from "~/components/core/FontFrame/Settings/Controls"; const CONTROLS_STYLES_WRAPPER = (theme) => css` width: fit-content; diff --git a/components/core/FontFrame/Settings/index.js b/components/core/FontFrame/Settings/index.js index 5a7f0d26..48aeb838 100644 --- a/components/core/FontFrame/Settings/index.js +++ b/components/core/FontFrame/Settings/index.js @@ -3,7 +3,12 @@ import * as Strings from "~/common/strings"; import { css } from "@emotion/react"; -import { Controller, AlignmentControl, ContentControl, SettingsControl } from "./Controls"; +import { + Controller, + AlignmentControl, + ContentControl, + SettingsControl, +} from "~/components/core/FontFrame/Settings/Controls"; const STYLES_CONTROLLER_WRAPPER = (theme) => css` diff --git a/components/core/FontFrame/Views/Paragraph.js b/components/core/FontFrame/Views/Paragraph.js index 5863df58..d3230015 100644 --- a/components/core/FontFrame/Views/Paragraph.js +++ b/components/core/FontFrame/Views/Paragraph.js @@ -1,6 +1,6 @@ import * as React from "react"; -import ContentEditable from "./ContentEditable"; +import ContentEditable from "~/components/core/FontFrame/Views/ContentEditable"; import { css } from "@emotion/react"; diff --git a/components/core/FontFrame/Views/Sentence.js b/components/core/FontFrame/Views/Sentence.js index 1cbd015c..532fee54 100644 --- a/components/core/FontFrame/Views/Sentence.js +++ b/components/core/FontFrame/Views/Sentence.js @@ -2,7 +2,7 @@ import * as React from "react"; import { css } from "@emotion/react"; -import ContentEditable from "./ContentEditable"; +import ContentEditable from "~/components/core/FontFrame/Views/ContentEditable"; const STYLES_SENTENCE_WRAPPER = (theme) => css` .font_frame_sentence { diff --git a/components/core/FontFrame/Views/index.js b/components/core/FontFrame/Views/index.js index 525f1c21..94d44f9d 100644 --- a/components/core/FontFrame/Views/index.js +++ b/components/core/FontFrame/Views/index.js @@ -1,6 +1,6 @@ -import Glyphs from "./Glyphs"; -import Sentence from "./Sentence"; -import Paragraph from "./Paragraph"; +import Glyphs from "~/components/core/FontFrame/Views/Glyphs"; +import Sentence from "~/components/core/FontFrame/Views/Sentence"; +import Paragraph from "~/components/core/FontFrame/Views/Paragraph"; export default function FontView({ settings, diff --git a/components/core/FontFrame/hooks.js b/components/core/FontFrame/hooks.js index 0f958c64..ce7b3e17 100644 --- a/components/core/FontFrame/hooks.js +++ b/components/core/FontFrame/hooks.js @@ -1,6 +1,6 @@ import * as React from "react"; import * as Events from "~/common/custom-events"; -import * as Content from "./Views/content"; +import * as Content from "~/components/core/FontFrame/Views/content"; import * as Strings from "~/common/strings"; import { generateNumberByStep } from "~/common/utilities"; diff --git a/components/core/FontFrame/index.js b/components/core/FontFrame/index.js index dce6db14..481bbf7c 100644 --- a/components/core/FontFrame/index.js +++ b/components/core/FontFrame/index.js @@ -3,10 +3,13 @@ import * as Strings from "~/common/strings"; import { css } from "@emotion/react"; -import { useFont, useFontControls } from "./hooks"; -import { Controls } from "./Settings/index"; -import { FixedControls, MobileFixedControls } from "./Settings/FixedControls"; -import FontView from "./Views/index"; +import { useFont, useFontControls } from "~/components/core/FontFrame/hooks"; +import { Controls } from "~/components/core/FontFrame/Settings/index"; +import { + FixedControls, + MobileFixedControls, +} from "~/components/core/FontFrame/Settings/FixedControls"; +import FontView from "~/components/core/FontFrame/Views/index"; const GET_STYLES_CONTAINER = (theme) => css` position: relative; diff --git a/components/core/MarkdownFrame.js b/components/core/MarkdownFrame.js index 97d2d877..fc9f655b 100644 --- a/components/core/MarkdownFrame.js +++ b/components/core/MarkdownFrame.js @@ -77,7 +77,7 @@ const STYLES_META = (theme) => css` margin-bottom: 12px; `; -const STYLES_DEVIDER = (theme) => css` +const STYLES_DIVIDER = (theme) => css` position: sticky; // Note(Amine): asset padding top: -120px; @@ -155,7 +155,7 @@ export default function MarkdownFrame({ url, date }) {
{Strings.toDate(date)} / {readTime} min read
-
+
diff --git a/components/core/NewWebsitePrototypeHeader.js b/components/core/NewWebsitePrototypeHeader.js new file mode 100644 index 00000000..a24a45f7 --- /dev/null +++ b/components/core/NewWebsitePrototypeHeader.js @@ -0,0 +1,240 @@ +import React, { useState, useEffect } from "react"; +import { css } from "@emotion/react"; +import { Logo } from "~/common/logo.js"; +import { Link } from "~/components/core/Link"; + +import * as Constants from "~/common/constants"; + +const STYLES_ROOT = css` + position: -webkit-sticky; + position: sticky; + top: 0; + padding: 24px 64px; + width: 100%; + height: 100%; + margin: 0 auto; + mix-blend-mode: difference; + z-index: ${Constants.zindex.header}; + + @media (max-width: ${Constants.sizes.mobile}px) { + padding: 16px 24px; + mix-blend-mode: normal; + } +`; +const STYLES_CONTAINER = css` + max-width: 1440px; + margin: 0 auto; + font-family: ${Constants.font.text}; + font-size: 1rem; + width: 100%; + display: flex; + justify-content: space-between; + + @media (max-width: ${Constants.sizes.mobile}px) { + display: flex; + justify-content: space-between; + } +`; + +const STYLES_LINK = css` + color: ${Constants.system.darkGray}; + text-decoration: none; + transition: 200ms ease color; + + :hover { + color: ${Constants.system.newBlue}; + } + + @media (max-width: ${Constants.sizes.mobile}px) { + color: ${Constants.system.slate}; + } +`; + +const STYLES_LEFT = css` + flex-shrink: 0; + height: 24px; +`; + +const STYLES_RIGHT = css` + min-width: 10%; + width: 100%; + display: flex; + justify-content: flex-end; + + @media (max-width: ${Constants.sizes.mobile}px) { + display: none; + } +`; + +const STYLES_MOBILENAV = css` + display: none; + + @media (max-width: ${Constants.sizes.mobile}px) { + display: flex; + } +`; + +const STYLES_BURGER = css` + display: none; + + @media (max-width: ${Constants.sizes.mobile}px) { + z-index: ${Constants.zindex.modal}; + position: absolute; + top: 12px; + right: 24px; + display: flex; + flex-direction: column; + justify-content: space-around; + width: 24px; + height: 24px; + background: transparent; + border: none; + cursor: pointer; + } +`; + +const STYLES_BURGER_BUN = css` + width: 20px; + height: 2px; + background: ${Constants.system.darkGray}; + transition: all 0.2s linear; + position: relative; + transform-origin: 1.5px; + transform: rotate(0); + transistion-property: transform; + + @media (max-width: ${Constants.sizes.mobile}px) { + background: ${Constants.system.slate}; + } +`; + +const openBurgerBun = { + transform: `rotate(45deg)`, +}; + +const STYLES_BURGER_BUN2 = css` + width: 20px; + height: 2px; + background: ${Constants.system.darkGray}; + transition: all 0.2s linear; + position: relative; + transform-origin: 1.5px; + transform: rotate(0); + transistion-property: transform; + + @media (max-width: ${Constants.sizes.mobile}px) { + background: ${Constants.system.slate}; + } +`; + +const openBurgerBun2 = { + transform: `rotate(-45deg)`, +}; + +const STYLES_MENU = css` + display: none; + + @media (max-width: ${Constants.sizes.mobile}px) { + display: none; + flex-direction: column; + justify-content: center; + background: ${Constants.system.wall}; + height: 100vh; + width: 100vw; + text-align: left; + padding: 24px; + position: absolute; + top: 0; + right: 0; + transform: translateX(100%); + transition: 200ms ease-in-out; + transition-property: transform, width; + } +`; + +const STYLES_NAVLINK = css` + display: none; + + @media (max-width: ${Constants.sizes.mobile}px) { + padding: 8px 0; + color: ${Constants.system.slate}; + text-decoration: none; + transition: color 0.3s linear; + transition-property: transform; + font-family: ${Constants.font.medium}; + font-weight: 400; + font-size: ${Constants.typescale.lvl2}; + letter-spacing: -0.017rem; + line-height: 1.3; + text-align: left; + + :hover { + color: ${Constants.system.darkGray}; + } + } +`; + +const openMenu = { + display: `flex`, + transform: `translateX(0)`, +}; + +const openNavLink = { + display: `flex`, +}; + +const NewWebsitePrototypeHeader = (props) => { + const [open, setOpen] = useState(false); + + useEffect(() => { + window.addEventListener("resize", handleOpen); + return () => window.removeEventListener("resize", handleOpen); + }); + + const handleOpen = () => { + setOpen(false); + }; + + const communityURL = "https://github.com/filecoin-project/slate"; + const signInURL = "/_/auth"; + const styleMenu = open ? openMenu : null; + const styleBurgerBun = open ? openBurgerBun : null; + const styleBurgerBun2 = open ? openBurgerBun2 : null; + const styleNavLink = open ? openNavLink : null; + + return ( +
+
+
+ + + +
+ +
+
setOpen(!open)} css={STYLES_BURGER}> + +
+
+ ); +}; + +export default NewWebsitePrototypeHeader; diff --git a/components/core/Selectable/doObjectsCollide.js b/components/core/Selectable/doObjectsCollide.js index e78d4084..aa0fc8cb 100644 --- a/components/core/Selectable/doObjectsCollide.js +++ b/components/core/Selectable/doObjectsCollide.js @@ -23,7 +23,7 @@ const coordsCollide = (aTop, aLeft, bTop, bLeft, aWidth, aHeight, bWidth, bHeigh ); }; -const doCoordsCollide = (a, b, tolerance = 0) => { +export default function DoObjectsCollide(a, b, tolerance = 0) { const aObj = a instanceof HTMLElement ? getBoundsForNode(a) : a; const bObj = b instanceof HTMLElement ? getBoundsForNode(b) : b; @@ -38,6 +38,4 @@ const doCoordsCollide = (a, b, tolerance = 0) => { bObj.offsetHeight, tolerance ); -}; - -export default doCoordsCollide; +} diff --git a/components/core/Selectable/groupSelectable.js b/components/core/Selectable/groupSelectable.js index 4b7a7727..8a59534a 100644 --- a/components/core/Selectable/groupSelectable.js +++ b/components/core/Selectable/groupSelectable.js @@ -2,7 +2,7 @@ import * as React from "react"; import * as Constants from "~/common/constants"; import { css } from "@emotion/react"; -import doObjectsCollide from "./doObjectsCollide"; +import doObjectsCollide from "~/components/core/Selectable/doObjectsCollide"; const Context = React.createContext(); diff --git a/components/core/Selectable/index.js b/components/core/Selectable/index.js index f52e6c82..7944231e 100644 --- a/components/core/Selectable/index.js +++ b/components/core/Selectable/index.js @@ -1,2 +1,2 @@ -export { default as GroupSelectable } from "./groupSelectable"; -export { default as Selectable } from "./selectable"; +export { default as GroupSelectable } from "~/components/core/Selectable/groupSelectable"; +export { default as Selectable } from "~/components/core/Selectable/selectable"; diff --git a/components/core/Selectable/selectable.js b/components/core/Selectable/selectable.js index ede2c039..eeea84d8 100644 --- a/components/core/Selectable/selectable.js +++ b/components/core/Selectable/selectable.js @@ -1,5 +1,5 @@ import * as React from "react"; -import { useSelectable } from "./groupSelectable"; +import { useSelectable } from "~/components/core/Selectable/groupSelectable"; export default function Selectable({ children, selectableKey, style, ...props }) { const ref = React.useRef(); diff --git a/components/core/SignIn.js b/components/core/SignIn.js index 5ef33508..6db291fe 100644 --- a/components/core/SignIn.js +++ b/components/core/SignIn.js @@ -146,8 +146,7 @@ export class SignIn extends React.Component { password: this.state.password, }); } - - if (!Events.hasError(response)) { + if (response && !response.error) { window.location.replace("/_/activity"); } this.setState({ loading: false }); @@ -273,8 +272,10 @@ export class SignIn extends React.Component { onChange={this._handleChange} onSubmit={this._handleSubmit} style={{ paddingRight: 42 }} + onClickIcon={() => this.setState({ showPassword: !this.state.showPassword })} + icon={this.state.showPassword ? SVG.EyeOff : SVG.Eye} /> -
this.setState({ showPassword: !this.state.showPassword })} > @@ -291,7 +292,7 @@ export class SignIn extends React.Component { style={{ color: Constants.system.grayBlack }} /> )} -
+
*/}
- ⭢ Already have an account? + Already have an account?
@@ -371,6 +372,8 @@ export class SignIn extends React.Component { value={this.state.password} onChange={this._handleChange} onSubmit={this._handleSubmit} + onClickIcon={() => this.setState({ showPassword: !this.state.showPassword })} + icon={this.state.showPassword ? SVG.EyeOff : SVG.Eye} /> - ⭢ Not registered? Sign up instead + Not registered? Sign up + instead
diff --git a/components/core/WebsitePrototypeHeader.js b/components/core/WebsitePrototypeHeader.js index 5bafbffa..23e272a1 100644 --- a/components/core/WebsitePrototypeHeader.js +++ b/components/core/WebsitePrototypeHeader.js @@ -1,57 +1,52 @@ -import React, { useState, useEffect } from "react"; -import { css } from "@emotion/react"; -import { Logo } from "~/common/logo.js"; -import { Link } from "~/components/core/Link"; - +import * as React from "react"; import * as Constants from "~/common/constants"; -const STYLES_ROOT = css` +import { Logo } from "~/common/logo.js"; +import { css } from "@emotion/react"; + +const STYLES_CONTAINER = css` position: -webkit-sticky; position: sticky; top: 0; - padding: 24px 64px; + padding: 16px 32px; width: 100%; margin: 0 auto; - mix-blend-mode: difference; z-index: ${Constants.zindex.header}; - - @media (max-width: ${Constants.sizes.mobile}px) { - padding: 16px 24px; - mix-blend-mode: normal; - } -`; -const STYLES_CONTAINER = css` - max-width: 1440px; - margin: 0 auto; font-family: ${Constants.font.text}; - font-size: 1rem; + font-weight: 400; + font-size: ${Constants.typescale.lvl0}; width: 100%; display: flex; + align-items: center; justify-content: space-between; - + @supports ((-webkit-backdrop-filter: blur(25px)) or (backdrop-filter: blur(25px))) { + -webkit-backdrop-filter: blur(25px); + backdrop-filter: blur(25px); + background-color: rgba(248, 248, 248, 0.7); + } @media (max-width: ${Constants.sizes.mobile}px) { - display: flex; - justify-content: space-between; + padding: 12px 24px; } `; const STYLES_LINK = css` - color: ${Constants.system.darkGray}; + color: ${Constants.system.grayBlack}; text-decoration: none; transition: 200ms ease color; + display: block; + display: flex; + align-items: center; + height: 100%; :hover { - color: ${Constants.system.newBlue}; - } - - @media (max-width: ${Constants.sizes.mobile}px) { - color: ${Constants.system.slate}; + color: ${Constants.system.brand}; } `; const STYLES_LEFT = css` flex-shrink: 0; height: 24px; + height: 100%; `; const STYLES_RIGHT = css` @@ -59,179 +54,25 @@ const STYLES_RIGHT = css` width: 100%; display: flex; justify-content: flex-end; - - @media (max-width: ${Constants.sizes.mobile}px) { - display: none; - } + text-align: left; `; -const STYLES_MOBILENAV = css` - display: none; - - @media (max-width: ${Constants.sizes.mobile}px) { - display: flex; - } -`; - -const STYLES_BURGER = css` - display: none; - - @media (max-width: ${Constants.sizes.mobile}px) { - z-index: ${Constants.zindex.modal}; - position: absolute; - top: 12px; - right: 24px; - display: flex; - flex-direction: column; - justify-content: space-around; - width: 24px; - height: 24px; - background: transparent; - border: none; - cursor: pointer; - } -`; - -const STYLES_BURGER_BUN = css` - width: 20px; - height: 2px; - background: ${Constants.system.darkGray}; - transition: all 0.2s linear; - position: relative; - transform-origin: 1.5px; - transform: rotate(0); - transistion-property: transform; - - @media (max-width: ${Constants.sizes.mobile}px) { - background: ${Constants.system.slate}; - } -`; - -const openBurgerBun = { - transform: `rotate(45deg)`, -}; - -const STYLES_BURGER_BUN2 = css` - width: 20px; - height: 2px; - background: ${Constants.system.darkGray}; - transition: all 0.2s linear; - position: relative; - transform-origin: 1.5px; - transform: rotate(0); - transistion-property: transform; - - @media (max-width: ${Constants.sizes.mobile}px) { - background: ${Constants.system.slate}; - } -`; - -const openBurgerBun2 = { - transform: `rotate(-45deg)`, -}; - -const STYLES_MENU = css` - display: none; - - @media (max-width: ${Constants.sizes.mobile}px) { - display: none; - flex-direction: column; - justify-content: center; - background: ${Constants.system.wall}; - height: 100vh; - width: 100vw; - text-align: left; - padding: 24px; - position: absolute; - top: 0; - right: 0; - transform: translateX(100%); - transition: 200ms ease-in-out; - transition-property: transform, width; - } -`; - -const STYLES_NAVLINK = css` - display: none; - - @media (max-width: ${Constants.sizes.mobile}px) { - padding: 8px 0; - color: ${Constants.system.slate}; - text-decoration: none; - transition: color 0.3s linear; - transition-property: transform; - font-family: ${Constants.font.medium}; - font-weight: 400; - font-size: ${Constants.typescale.lvl2}; - letter-spacing: -0.017rem; - line-height: 1.3; - text-align: left; - - :hover { - color: ${Constants.system.darkGray}; - } - } -`; - -const openMenu = { - display: `flex`, - transform: `translateX(0)`, -}; - -const openNavLink = { - display: `flex`, -}; - const WebsitePrototypeHeader = (props) => { - const [open, setOpen] = useState(false); - - useEffect(() => { - window.addEventListener("resize", handleOpen); - return () => window.removeEventListener("resize", handleOpen); - }); - - const handleOpen = () => { - setOpen(false); - }; - - const communityURL = "https://github.com/filecoin-project/slate"; - const signInURL = "/_/auth?tab=signin"; - const styleMenu = open ? openMenu : null; - const styleBurgerBun = open ? openBurgerBun : null; - const styleBurgerBun2 = open ? openBurgerBun2 : null; - const styleNavLink = open ? openNavLink : null; - return ( -
-
-
- - - -
- -
-
setOpen(!open)} css={STYLES_BURGER}> - + ); }; diff --git a/components/core/WebsitePrototypeHeaderGeneric.js b/components/core/WebsitePrototypeHeaderGeneric.js new file mode 100644 index 00000000..5a89cf33 --- /dev/null +++ b/components/core/WebsitePrototypeHeaderGeneric.js @@ -0,0 +1,104 @@ +import * as React from "react"; +import * as Constants from "~/common/constants"; +import * as SVGLogo from "~/common/logo"; + +import { css } from "@emotion/react"; + +const STYLES_ROOT = css` + padding: 24px 88px 24px 64px; + width: 100%; + margin: 0 auto; + z-index: ${Constants.zindex.header}; + + @media (max-width: ${Constants.sizes.mobile}px) { + padding: 16px 24px; + } +`; + +const STYLES_CONTAINER = css` + max-width: 1440px; + margin: 0 auto; + font-family: ${Constants.font.text}; + font-weight: 400; + font-size: ${Constants.typescale.lvl1}; + width: 100%; +`; + +const STYLES_NAV_CONTAINER = css` + display: flex; + justify-content: space-between; + margin-bottom: 8px; +`; + +const STYLES_LINK = css` + color: ${Constants.system.grayBlack}; + text-decoration: none; + transition: 200ms ease color; + text-align: left; + display: block; + + :hover { + color: ${Constants.system.brand}; + } +`; + +const STYLES_PARAGRAPH = css` + font-family: ${Constants.font.text}; + color: ${Constants.system.pitchBlack}; + text-decoration: none; + transition: 200ms ease color; + overflow-wrap: break-word; + max-width: 50%; + min-width: 10%; + text-align: left; + margin-left: 30px; + display: block; + + @media (max-width: ${Constants.sizes.mobile}px) { + max-width: 100%; + } +`; + +const STYLES_LEFT = css` + flex-shrink: 0; + display: flex; + max-width: 70%; +`; + +const STYLES_RIGHT = css` + min-width: 10%; + width: 100%; + display: flex; + justify-content: flex-end; + text-align: left; +`; + +const WebsitePrototypeHeaderGeneric = (props) => { + return ( +
+
+ +
{props.children}
+
+
+ ); +}; + +export default WebsitePrototypeHeaderGeneric; diff --git a/components/system/components/Buttons.js b/components/system/components/Buttons.js index ff92f992..a24b4a49 100644 --- a/components/system/components/Buttons.js +++ b/components/system/components/Buttons.js @@ -26,7 +26,7 @@ const STYLES_BUTTON = ` const STYLES_BUTTON_PRIMARY = css` ${STYLES_BUTTON} cursor: pointer; - background-color: ${Constants.system.brand}; + background-color: ${Constants.system.blue}; color: ${Constants.system.white}; :hover { diff --git a/components/system/components/CheckBox.js b/components/system/components/CheckBox.js index ca134cea..70a81513 100644 --- a/components/system/components/CheckBox.js +++ b/components/system/components/CheckBox.js @@ -89,9 +89,9 @@ export class CheckBox extends React.Component { render() { return ( -