From aa6fe210a6c5a3034047fcdb206d44a488abb8c3 Mon Sep 17 00:00:00 2001 From: PedroFonsecaDEV Date: Fri, 19 Mar 2021 11:41:09 -0400 Subject: [PATCH] SignUp Page Co-authored-by: Meneguini Co-authored-by: DukeManh --- src/web/next.config.js | 10 +- src/web/package.json | 5 +- src/web/src/components/BannerButtons.tsx | 18 +- .../SignUp/FormFields/CheckBoxInput.tsx | 48 ++++ .../SignUp/FormFields/TextInput.tsx | 49 ++++ .../components/SignUp/FormFields/index.tsx | 4 + .../src/components/SignUp/Forms/BasicInfo.tsx | 163 +++++++++++ .../components/SignUp/Forms/GitHubAccount.tsx | 172 +++++++++++ .../src/components/SignUp/Forms/Overview.tsx | 104 +++++++ .../src/components/SignUp/Forms/RSSFeeds.tsx | 264 +++++++++++++++++ .../src/components/SignUp/Forms/Review.tsx | 140 +++++++++ src/web/src/components/SignUp/Forms/index.tsx | 6 + .../components/SignUp/Schema/FormModel.tsx | 54 ++++ .../components/SignUp/Schema/FormSchema.tsx | 68 +++++ src/web/src/config.ts | 4 + src/web/src/interfaces/index.ts | 16 ++ src/web/src/pages/signup.tsx | 271 ++++++++++++++++++ 17 files changed, 1386 insertions(+), 10 deletions(-) create mode 100644 src/web/src/components/SignUp/FormFields/CheckBoxInput.tsx create mode 100644 src/web/src/components/SignUp/FormFields/TextInput.tsx create mode 100644 src/web/src/components/SignUp/FormFields/index.tsx create mode 100644 src/web/src/components/SignUp/Forms/BasicInfo.tsx create mode 100644 src/web/src/components/SignUp/Forms/GitHubAccount.tsx create mode 100644 src/web/src/components/SignUp/Forms/Overview.tsx create mode 100644 src/web/src/components/SignUp/Forms/RSSFeeds.tsx create mode 100644 src/web/src/components/SignUp/Forms/Review.tsx create mode 100644 src/web/src/components/SignUp/Forms/index.tsx create mode 100644 src/web/src/components/SignUp/Schema/FormModel.tsx create mode 100644 src/web/src/components/SignUp/Schema/FormSchema.tsx create mode 100644 src/web/src/pages/signup.tsx diff --git a/src/web/next.config.js b/src/web/next.config.js index 9fd9eadd08..e62df20c44 100644 --- a/src/web/next.config.js +++ b/src/web/next.config.js @@ -13,7 +13,15 @@ const dotenv = require('dotenv'); const loadApiUrlFromEnv = (envFile) => dotenv.config({ path: envFile }); // ENV Variables we need to forward to next by prefix with NEXT_PUBLIC_* -const envVarsToForward = ['WEB_URL', 'API_URL', 'IMAGE_URL', 'POSTS_URL', 'AUTH_URL']; +const envVarsToForward = [ + 'WEB_URL', + 'API_URL', + 'IMAGE_URL', + 'POSTS_URL', + 'AUTH_URL', + 'FEED_DISCOVERY_URL', + 'USERS_URL', +]; // Copy an existing ENV Var so it's visible to next: API_URL -> NEXT_PUBLIC_API_URL const forwardToNext = (envVar) => { diff --git a/src/web/package.json b/src/web/package.json index 49d3cf209c..d45cfd9dc9 100644 --- a/src/web/package.json +++ b/src/web/package.json @@ -15,7 +15,9 @@ "@mdx-js/loader": "^1.6.22", "@next/mdx": "^10.1.3", "@types/smoothscroll-polyfill": "^0.3.1", + "@types/yup": "^0.29.11", "dotenv": "^8.2.0", + "formik": "^2.2.6", "highlight.js": "10.7.2", "jwt-decode": "^3.1.2", "nanoid": "^3.1.22", @@ -25,7 +27,8 @@ "react-use": "^17.2.1", "smoothscroll-polyfill": "^0.4.4", "swr": "^0.5.5", - "valid-url": "^1.0.9" + "valid-url": "^1.0.9", + "yup": "^0.32.9" }, "devDependencies": { "@testing-library/react": "^11.2.6", diff --git a/src/web/src/components/BannerButtons.tsx b/src/web/src/components/BannerButtons.tsx index 2c98114b01..660eba5dd1 100644 --- a/src/web/src/components/BannerButtons.tsx +++ b/src/web/src/components/BannerButtons.tsx @@ -73,14 +73,16 @@ const LandingButtons = () => { > Sign in - + + + )} diff --git a/src/web/src/components/SignUp/FormFields/CheckBoxInput.tsx b/src/web/src/components/SignUp/FormFields/CheckBoxInput.tsx new file mode 100644 index 0000000000..c049fea730 --- /dev/null +++ b/src/web/src/components/SignUp/FormFields/CheckBoxInput.tsx @@ -0,0 +1,48 @@ +import { createStyles, makeStyles } from '@material-ui/core'; +import { useField } from 'formik'; +import FormControl from '@material-ui/core/FormControl'; +import FormGroup from '@material-ui/core/FormGroup'; +import FormHelperText from '@material-ui/core/FormHelperText'; +import FormControlLabel from '@material-ui/core/FormControlLabel'; +import Checkbox from '@material-ui/core/Checkbox'; + +const useStyles = makeStyles(() => + createStyles({ + formInputLabel: { + fontSize: '1.4em', + color: 'black', + }, + formControlLabel: { + fontSize: '1em', + color: '#474747', + }, + }) +); + +type CheckboxProps = { + name: string; + label: string; + checked: boolean; +}; + +const CheckBoxInput = (props: CheckboxProps) => { + const classes = useStyles(); + + const { label, name, checked, ...rest } = props; + const [field, meta] = useField(props); + + return ( + + + } + label={{label}} + name={name} + /> + + {meta.error && meta.touched ? meta.error : ''} + + ); +}; + +export default CheckBoxInput; diff --git a/src/web/src/components/SignUp/FormFields/TextInput.tsx b/src/web/src/components/SignUp/FormFields/TextInput.tsx new file mode 100644 index 0000000000..191be17a3b --- /dev/null +++ b/src/web/src/components/SignUp/FormFields/TextInput.tsx @@ -0,0 +1,49 @@ +import { useField, FieldHookConfig } from 'formik'; +import TextField, { TextFieldProps } from '@material-ui/core/TextField'; +import { createStyles, makeStyles } from '@material-ui/core'; + +const useStyles = makeStyles(() => + createStyles({ + formInput: { + fontSize: '1.1em', + color: 'black', + }, + formInputLabel: { + fontSize: '1.2em', + color: 'black', + }, + }) +); + +const TextInput = (props: TextFieldProps & FieldHookConfig) => { + const classes = useStyles(); + + const { helperText, error, ...rest } = props; + const [field, meta] = useField(props); + + const renderHelperText = () => + error || (meta.touched && meta.error) ? meta.error || helperText : ''; + + return ( + + ); +}; + +export default TextInput; diff --git a/src/web/src/components/SignUp/FormFields/index.tsx b/src/web/src/components/SignUp/FormFields/index.tsx new file mode 100644 index 0000000000..fb223458ff --- /dev/null +++ b/src/web/src/components/SignUp/FormFields/index.tsx @@ -0,0 +1,4 @@ +import TextInput from './TextInput'; +import CheckBoxInput from './CheckBoxInput'; + +export { TextInput, CheckBoxInput }; diff --git a/src/web/src/components/SignUp/Forms/BasicInfo.tsx b/src/web/src/components/SignUp/Forms/BasicInfo.tsx new file mode 100644 index 0000000000..b6b377092b --- /dev/null +++ b/src/web/src/components/SignUp/Forms/BasicInfo.tsx @@ -0,0 +1,163 @@ +import { ReactNode } from 'react'; +import { createStyles, makeStyles } from '@material-ui/core'; +import { connect } from 'formik'; + +import { SignUpForm } from '../../../interfaces'; +import useAuth from '../../../hooks/use-auth'; +import formModels from '../Schema/FormModel'; +import { TextInput } from '../FormFields'; + +const { firstName, lastName, displayName, email } = formModels; + +const useStyles = makeStyles((theme) => + createStyles({ + root: { + padding: '0', + margin: '0', + width: '100%', + position: 'relative', + minHeight: '100%', + }, + container: { + display: 'grid', + gridTemplateColumns: '1fr', + justifyItems: 'center', + textAlign: 'center', + alignItems: 'center', + width: '100%', + position: 'absolute', + minHeight: '100%', + [theme.breakpoints.down(600)]: { + width: '95%', + marginLeft: '2.5%', + }, + }, + helloMessage: { + fontSize: '0.8em', + }, + userInfo: { + color: '#292929', + margin: '0', + padding: '.5%', + width: '90%', + height: '80%', + fontSize: '0.8em', + display: 'grid', + gridTemplateColumns: '1fr 1fr', + justifyContent: 'center', + alignItems: 'center', + border: '1px solid #C5EB98', + background: 'rgba(197, 235, 152, 0.2)', + borderRadius: '5px', + [theme.breakpoints.down(600)]: { + gridTemplateColumns: '1fr', + }, + '& span': { + color: '#525252', + }, + }, + userInfoLabel: { + gridColumnStart: '1', + gridColumnEnd: '3', + [theme.breakpoints.down(600)]: { + gridColumnStart: '1', + gridColumnEnd: '2', + }, + }, + displayNameTitle: { + fontSize: '0.85em', + }, + button: { + fontSize: '0.8em', + height: '35px', + width: '50%', + background: '#121D59', + color: '#A0D1FB', + marginLeft: '5%', + '&:hover': { + color: 'black', + border: '1px solid #121D59', + }, + }, + displayNameInfo: { + textAlign: 'start', + gridColumnStart: '1', + gridColumnEnd: '3', + fontSize: '1em', + }, + inputContainer: { + display: 'grid', + alignItems: 'center', + justifyItems: 'center', + width: '90%', + gridTemplateColumns: '80% 20%', + '& .MuiFormHelperText-root': { + fontSize: '0.9em', + color: 'black', + }, + '& .MuiFormLabel-root': { + color: 'black', + }, + '& .MuiInputBase-input.Mui-disabled': { + marginTop: '16px', + }, + }, + }) +); + +type Props = { + children: ReactNode; +}; + +const InputContainer = ({ children }: Props) => { + const classes = useStyles(); + + return
{children}
; +}; + +const BasicInfo = connect<{}, SignUpForm>((props) => { + const classes = useStyles(); + const { values } = props.formik; + const { user } = useAuth(); + + return ( +
+
+
+

Hello {user?.name || values.displayName}

+
+
+

+ The following information is what we already have: +

+

+ Display name: + {user?.name || values.displayName} +

+

+ Email: + {values.email} +

+
+ + + + + + + + + + + + +
+
+ ); +}); + +export default BasicInfo; diff --git a/src/web/src/components/SignUp/Forms/GitHubAccount.tsx b/src/web/src/components/SignUp/Forms/GitHubAccount.tsx new file mode 100644 index 0000000000..cb0975239f --- /dev/null +++ b/src/web/src/components/SignUp/Forms/GitHubAccount.tsx @@ -0,0 +1,172 @@ +/* eslint-disable camelcase */ +import { useEffect, useState } from 'react'; +import { createStyles, makeStyles, Theme } from '@material-ui/core'; +import { connect } from 'formik'; +import useSWR from 'swr'; + +import { SignUpForm } from '../../../interfaces'; +import formModels from '../Schema/FormModel'; +import { TextInput, CheckBoxInput } from '../FormFields'; +import PostAvatar from '../../Posts/PostAvatar'; + +const { githubUsername, github: githubModel, githubOwnership } = formModels; + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + root: { + padding: '0', + margin: '0', + width: '100%', + position: 'relative', + minHeight: '100%', + }, + container: { + display: 'grid', + gridTemplateAreas: '1fr', + textAlign: 'center', + justifyItems: 'center', + alignItems: 'start', + width: '100%', + position: 'absolute', + minHeight: '100%', + [theme.breakpoints.down(600)]: { + width: '90%', + marginLeft: '5%', + }, + }, + titlePage: { + fontSize: '1.5em', + }, + subtitlePage: { + fontSize: '1.1em', + lineHeight: '1.8em', + }, + infoContainer: { + display: 'grid', + gridTemplateColumns: '65% 35%', + gridGap: '1%', + textAlign: 'center', + justifyItems: 'center', + alignItems: 'center', + width: '90%', + [theme.breakpoints.down(600)]: { + gridTemplateColumns: '1fr', + }, + }, + inputsContainer: { + width: '100%', + display: 'grid', + gridTemplateColumns: '100%', + '& .MuiFormHelperText-root': { + fontSize: '0.9em', + color: 'black', + }, + '& .MuiFormLabel-root': { + color: 'black', + }, + }, + avatarPreview: { + textAlign: 'center', + justifyItems: 'center', + alignItems: 'center', + justifySelf: 'end', + padding: '6%', + borderRadius: '5px', + [theme.breakpoints.down(600)]: { + justifySelf: 'center', + padding: '3%', + marginTop: '5%', + }, + }, + username: { + fontSize: '1.1em', + }, + }) +); + +const gitHubApiUrl = 'https://api.github.com/users'; + +const GitHubAccount = connect<{}, SignUpForm>((props) => { + const classes = useStyles(); + const { values, setFieldValue } = props.formik; + const [username, setUsername] = useState(values.githubUsername); + const [inputTimeout, setInputTimeout] = useState(setTimeout(() => {}, 0)); + + const { data: github, error } = useSWR( + values.githubUsername ? `${gitHubApiUrl}/${values.githubUsername}` : null, + async (u) => { + try { + const response = await fetch(u); + if (!response.ok) { + throw new Error(response.statusText); + } + return response.json(); + } catch (err) { + throw err; + } + } + ); + + const handleInputChange = (e: React.ChangeEvent) => { + setUsername(e.target.value); + clearTimeout(inputTimeout); + + // Update githubUsername 1000ms after input change + setInputTimeout( + setTimeout(() => { + setFieldValue('githubUsername', e.target.value); + }, 1000) + ); + }; + + useEffect(() => { + if (error) { + setFieldValue('github', {}, true); + } + + if (github) { + setFieldValue( + 'github', + { + username: github.login, + avatarUrl: github.avatar_url, + }, + true + ); + } + }, [github, error, setFieldValue]); + + return ( +
+
+

GitHub Account

+

Enter Github username and verify your profile

+
+
+ +
+ {!error && github && ( +
+ +

{github.login}

+
+ )} +
+ +
+
+ ); +}); + +export default GitHubAccount; diff --git a/src/web/src/components/SignUp/Forms/Overview.tsx b/src/web/src/components/SignUp/Forms/Overview.tsx new file mode 100644 index 0000000000..25b130858a --- /dev/null +++ b/src/web/src/components/SignUp/Forms/Overview.tsx @@ -0,0 +1,104 @@ +import { createStyles, makeStyles, Theme } from '@material-ui/core'; +import Button from '@material-ui/core/Button'; + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + root: { + padding: '0', + margin: '0', + width: '100%', + position: 'relative', + minHeight: '100%', + }, + container: { + display: 'grid', + gridTemplateColumns: '1fr', + justifyItems: 'center', + textAlign: 'center', + alignItems: 'center', + width: '100%', + position: 'absolute', + minHeight: '100%', + [theme.breakpoints.down(600)]: { + width: '90%', + marginLeft: '5%', + }, + }, + welcomeMessage: { + fontSize: '0.8em', + }, + telescopeInfo: { + fontSize: '0.8em', + lineHeight: '2.5em', + }, + helpText: { + fontSize: '0.8em', + lineHeight: '2.5em', + }, + helpButtons: { + display: 'flex', + justifyContent: 'center', + width: '100%', + }, + button: { + padding: '0 0.5em', + background: '#121D59', + color: '#A0D1FB', + fontSize: '0.9em', + margin: '0 0.5em 0em 1em', + '&:hover': { + color: 'black', + borderColor: '#121D59', + }, + height: '30px', + }, + text: { + fontSize: '1.04em', + alignSelf: 'end', + lineHeight: '2.5em', + }, + helpStartText: { + color: '#474747', + }, + }) +); + +const Overview = () => { + const classes = useStyles(); + + return ( +
+
+
+

Welcome

+
+
+

+ Telescope requires a number of pieces of user information, for example your Seneca + email, a GitHub account, a Blog, and a user display name. In the following steps we will + gather this information and create your account. +

+
+
+

If you need help to create a GitHub account and a blog page please check:

+
+
+ + +
+
+

Click Next to complete your Telescope account

+

+ * After clicking Next you will be prompted to login to your Seneca account{' '} +

+
+
+
+ ); +}; + +export default Overview; diff --git a/src/web/src/components/SignUp/Forms/RSSFeeds.tsx b/src/web/src/components/SignUp/Forms/RSSFeeds.tsx new file mode 100644 index 0000000000..75741738f2 --- /dev/null +++ b/src/web/src/components/SignUp/Forms/RSSFeeds.tsx @@ -0,0 +1,264 @@ +import { useState, useEffect } from 'react'; +import { Button, createStyles, makeStyles, Theme } from '@material-ui/core'; +import { connect } from 'formik'; +import FormControl from '@material-ui/core/FormControl'; +import FormGroup from '@material-ui/core/FormGroup'; +import FormControlLabel from '@material-ui/core/FormControlLabel'; +import FormHelperText from '@material-ui/core/FormHelperText'; +import Checkbox from '@material-ui/core/Checkbox'; + +import { feedDiscoveryServiceUrl } from '../../../config'; +import useAuth from '../../../hooks/use-auth'; +import { SignUpForm } from '../../../interfaces'; +import { TextInput, CheckBoxInput } from '../FormFields'; +import formModels from '../Schema/FormModel'; + +const { blogUrl, blogOwnership } = formModels; + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + root: { + padding: '0', + margin: '0', + position: 'relative', + width: '100%', + minHeight: '100%', + }, + container: { + display: 'grid', + gridTemplateAreas: '1fr', + textAlign: 'center', + justifyItems: 'center', + alignItems: 'center', + position: 'absolute', + minHeight: '100%', + width: '100%', + [theme.breakpoints.down(600)]: { + width: '95%', + marginLeft: '2.5%', + }, + }, + blogPageTitle: { + fontSize: '1.5em', + }, + helpText: { + fontSize: '1.1em', + lineHeight: '1.8em', + }, + infoContainer: { + display: 'grid', + textAlign: 'center', + gridGap: '10%', + justifyItems: 'center', + alignItems: 'center', + width: '90%', + }, + inputsContainer: { + width: '100%', + display: 'grid', + gridTemplateColumns: '75% 25%', + '& .MuiFormHelperText-root': { + fontSize: '0.9em', + color: 'black', + }, + '& .MuiFormLabel-root': { + color: 'black', + }, + [theme.breakpoints.down(600)]: { + gridTemplateColumns: '80% 20%', + }, + }, + helpMessage: { + fontSize: '.9em', + color: 'black', + }, + button: { + height: '35px', + width: '50%', + alignSelf: 'center', + fontSize: '0.8em', + marginLeft: '5%', + background: '#121D59', + color: '#A0D1FB', + '&:hover': { + color: 'black', + border: '1px solid #121D59', + }, + '&.Mui-disabled': { + backgroundColor: 'inherit', + }, + }, + RssButtonContainer: { + width: '90%', + display: 'grid', + }, + infoRSSContainer: { + minHeight: '120px', + maxHeight: '120px', + width: '100%', + overflowY: 'auto', + }, + noBlogMessage: { + fontSize: '1em', + color: '#474747', + marginTop: '40px', + }, + text: { + fontSize: '0.9em', + alignSelf: 'end', + color: '#474747', + }, + RssButtonWrapper: { + width: '100%', + }, + RssButton: { + width: '101%', + borderRadius: '0', + background: '#121D59', + color: '#A0D1FB', + '&:hover': { + color: 'black', + border: '1px solid #121D59', + }, + }, + agreeMessage: { + [theme.breakpoints.down(600)]: { + alignSelf: 'end', + }, + }, + formControlLabel: { + fontSize: '.9em', + height: '10px', + color: '#474747', + }, + }) +); + +const RSSFeeds = connect<{}, SignUpForm>((props) => { + const classes = useStyles(); + const { values, errors, setFieldValue } = props.formik; + const { token } = useAuth(); + + const [feedUrls, setFeedUrls] = useState>([]); + const [blogUrlError, setBlogUrlError] = useState(''); + const [validating, setValidating] = useState(false); + + // A controller to cancel undesired requests + const [controller, setController] = useState(new AbortController()); + + const validateBlog = async () => { + if (!errors.blogUrl && feedDiscoveryServiceUrl) { + try { + setValidating(true); + const abortController = new AbortController(); + setController(abortController); + const signal = abortController.signal; + const response = await fetch(feedDiscoveryServiceUrl, { + signal, + method: 'post', + headers: { + Authorization: `bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + blogUrl: values.blogUrl, + }), + }); + console.log(values.blogUrl); + if (!response.ok) { + throw new Error(response.statusText); + } + const res = await response.json(); + + setBlogUrlError(''); + setFeedUrls(res.feedUrls); + setValidating(false); + } catch (err) { + console.error(err, 'Unable to discover feeds'); + setBlogUrlError(`Unable to find RSS link at ${values.blogUrl}`); + setFeedUrls([]); + setValidating(false); + } + } else { + setFieldValue('feeds', [], true); + } + }; + + const handleCheck = (url: string) => { + const selectedFeeds = values.feeds.includes(url) + ? values.feeds.filter((val: string) => val !== url) + : [...values.feeds, url]; + + setFieldValue('feeds', selectedFeeds, true); + }; + + useEffect(() => { + if (errors.blogUrl) { + validateBlog(); + } + + // Abort feed-discovery on component unmounting + return () => { + controller.abort(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( +
+
+

Blog and RSS

+

+ Enter your blog URL and select the RSS you want to use in Telescope ecosystem. +

+
+
+ + +
+
+
+ {feedUrls.length ? ( + + + {feedUrls.map((url) => ( + handleCheck(url)} + /> + } + label={

{url}

} + /> + ))} +
+ + {errors.feeds || ''} + +
+ ) : ( +

Please validate your blog URL

+ )} +
+
+
+ +
+
+ ); +}); + +export default RSSFeeds; diff --git a/src/web/src/components/SignUp/Forms/Review.tsx b/src/web/src/components/SignUp/Forms/Review.tsx new file mode 100644 index 0000000000..e3703bda82 --- /dev/null +++ b/src/web/src/components/SignUp/Forms/Review.tsx @@ -0,0 +1,140 @@ +import { createStyles, makeStyles, Theme } from '@material-ui/core'; +import { connect } from 'formik'; +import PostAvatar from '../../Posts/PostAvatar'; + +import { SignUpForm } from '../../../interfaces'; + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + root: { + padding: '0', + margin: '0', + width: '100%', + position: 'relative', + minHeight: '100%', + }, + container: { + display: 'grid', + gridTemplateRows: '10% auto 10%', + textAlign: 'center', + justifyItems: 'center', + alignItems: 'center', + width: '100%', + position: 'absolute', + minHeight: '100%', + [theme.breakpoints.down(600)]: { + width: '90%', + marginLeft: '5%', + }, + }, + titlePage: { + fontSize: '1.5em', + }, + contentContainer: { + width: '90%', + display: 'grid', + gridTemplateColumns: 'auto auto', + gridTemplateRows: 'auto auto', + gridGap: '5%', + alignSelf: 'start', + [theme.breakpoints.down(600)]: { + gridTemplateColumns: '1fr', + height: '100%', + alignItems: 'center', + }, + }, + avatar: { + height: '110px', + display: 'grid', + gridTemplateColumns: '1fr', + textAlign: 'center', + justifyItems: 'center', + alignItems: 'center', + padding: '6%', + fontSize: '0.7em', + [theme.breakpoints.down(600)]: { + height: '85px', + padding: '0', + }, + }, + gitHubInfo: { + marginTop: '8%', + [theme.breakpoints.down(600)]: { + marginTop: '0', + textAlign: 'start', + }, + }, + senecaBlogInfo: { + textAlign: 'start', + }, + blogUrl: { + textAlign: 'start', + }, + titleRss: { + textAlign: 'start', + }, + blogRss: { + textAlign: 'start', + padding: '1%', + minHeight: '60px', + maxHeight: '60px', + overflowY: 'auto', + [theme.breakpoints.down(600)]: { + width: '90%', + }, + }, + text: { + fontSize: '0.9em', + alignSelf: 'end', + color: '#474747', + }, + }) +); + +const Review = connect<{}, SignUpForm>((props) => { + const classes = useStyles(); + + const { feeds, displayName, firstName, lastName, email, github, blogUrl } = props.formik.values; + + return ( +
+
+

Review your Information

+
+
+ +

+ Display Name: +

{displayName}

+ +
+
+

From seneca:

+

Full Name: {displayName || `${firstName} ${lastName}`}

+

Email : {email}

+

Blog URL:

+

{blogUrl}

+
+
+
+

GitHub Account:

+

{github.username}

+
+
+
+

Blog RSS:

+
+
+ {feeds.map((rss) => ( +

{rss}

+ ))} +
+
+
+
+
+
+ ); +}); + +export default Review; diff --git a/src/web/src/components/SignUp/Forms/index.tsx b/src/web/src/components/SignUp/Forms/index.tsx new file mode 100644 index 0000000000..a914de41ae --- /dev/null +++ b/src/web/src/components/SignUp/Forms/index.tsx @@ -0,0 +1,6 @@ +import BasicInfo from './BasicInfo'; +import GitHubAccount from './GitHubAccount'; +import Overview from './Overview'; +import Review from './Review'; + +export { BasicInfo, GitHubAccount, Overview, Review }; diff --git a/src/web/src/components/SignUp/Schema/FormModel.tsx b/src/web/src/components/SignUp/Schema/FormModel.tsx new file mode 100644 index 0000000000..5bb637630f --- /dev/null +++ b/src/web/src/components/SignUp/Schema/FormModel.tsx @@ -0,0 +1,54 @@ +export default { + displayName: { + name: 'displayName', + label: 'Display Name', + invalidErrorMsg: 'Make sure display name contains 2-16 characters', + }, + firstName: { + name: 'firstName', + label: 'First name', + requiredErrorMsg: 'First name is required', + invalidErrorMsg: 'Make sure first name contains 2-16 characters', + }, + lastName: { + name: 'lastName', + label: 'Last name', + requiredErrorMsg: 'Last name is required', + invalidErrorMsg: 'Make sure last name contains 2-16 characters', + }, + email: { + name: 'email', + label: 'Email', + }, + github: { + name: 'github', + label: 'Github Data`', + invalidErrorMsg: 'Invalid GitHub profile', + }, + githubUsername: { + name: 'githubUsername', + label: 'Github username', + requiredErrorMsg: 'Github account is required', + }, + githubOwnership: { + name: 'githubOwnership', + label: 'I declare I’m the owner and the maintainer of this GitHub account', + invalidErrorMsg: 'You must be the owner of this account', + }, + blogUrl: { + name: 'blogUrl', + label: 'Blog URl', + requiredErrorMsg: 'Blog Url is required', + invalidErrorMsg: 'Invalid URL', + }, + feeds: { + name: 'feeds', + label: 'RSS Feeds', + requiredErrorMsg: 'Please select at least one URL', + }, + blogOwnership: { + name: 'blogOwnership', + label: 'I declare I’m the owner and the maintainer of this blog account', + invalidErrorMsg: 'You must be the owner of this account', + }, +}; diff --git a/src/web/src/components/SignUp/Schema/FormSchema.tsx b/src/web/src/components/SignUp/Schema/FormSchema.tsx new file mode 100644 index 0000000000..1f8bedc468 --- /dev/null +++ b/src/web/src/components/SignUp/Schema/FormSchema.tsx @@ -0,0 +1,68 @@ +import * as Yup from 'yup'; + +import formModels from './FormModel'; + +const { + firstName, + lastName, + displayName, + githubUsername, + github, + githubOwnership, + feeds, + blogUrl, + blogOwnership, +} = formModels; + +const validateLength = (min: number, max: number) => (val: string | undefined): boolean => + !!val && val.length >= min && val.length <= max; + +const validateCheckBox = (val: boolean | undefined) => !!val; + +// Each signup step has one validation schema +export default [ + // First step has no validation logic + Yup.object().shape({}), + + Yup.object().shape({ + [firstName.name]: Yup.string() + .required(`${firstName.requiredErrorMsg}`) + .test('len', firstName.invalidErrorMsg, validateLength(2, 16)), + [lastName.name]: Yup.string() + .required(`${lastName.requiredErrorMsg}`) + .test('len', lastName.invalidErrorMsg, validateLength(2, 16)), + [displayName.name]: Yup.string().test( + 'len', + displayName.invalidErrorMsg, + validateLength(2, 16) + ), + }), + + Yup.object().shape({ + [githubUsername.name]: Yup.string().required(`${githubUsername.requiredErrorMsg}`), + [github.name]: Yup.object() + .shape({ + username: Yup.string().required(), + avatarUrl: Yup.string().url().required(), + }) + .required(github.invalidErrorMsg), + [githubOwnership.name]: Yup.boolean().test( + 'agreed', + githubOwnership.invalidErrorMsg, + validateCheckBox + ), + }), + + Yup.object().shape({ + [blogUrl.name]: Yup.string().url().required(`${blogUrl.requiredErrorMsg}`), + [feeds.name]: Yup.array().of(Yup.string()).min(1, feeds.requiredErrorMsg), + [blogOwnership.name]: Yup.boolean().test( + 'agreed', + blogOwnership.invalidErrorMsg, + validateCheckBox + ), + }), + + // Reviewing step has no validation logic + Yup.object().shape({}), +]; diff --git a/src/web/src/config.ts b/src/web/src/config.ts index 7dcb719cbd..dbb21ce1b5 100644 --- a/src/web/src/config.ts +++ b/src/web/src/config.ts @@ -11,6 +11,8 @@ const webUrl = process.env.NEXT_PUBLIC_WEB_URL; const imageServiceUrl = process.env.NEXT_PUBLIC_IMAGE_URL; const authServiceUrl = process.env.NEXT_PUBLIC_AUTH_URL; const postsServiceUrl = process.env.NEXT_PUBLIC_POSTS_URL; +const feedDiscoveryServiceUrl = process.env.NEXT_PUBLIC_FEED_DISCOVERY_URL; +const usersServiceUrl = process.env.NEXT_PUBLIC_USERS_URL; const title = `Telescope`; const description = `A tool for tracking blogs in orbit around Seneca's open source involvement`; @@ -39,4 +41,6 @@ export { image, imageAlt, postsServiceUrl, + feedDiscoveryServiceUrl, + usersServiceUrl, }; diff --git a/src/web/src/interfaces/index.ts b/src/web/src/interfaces/index.ts index f5c7e14657..d611cea2aa 100644 --- a/src/web/src/interfaces/index.ts +++ b/src/web/src/interfaces/index.ts @@ -15,4 +15,20 @@ export type Post = { html: string; }; +export type SignUpForm = { + displayName: string; + firstName: string; + lastName: string; + email: string; + github: { + username: string; + avatarUrl: string; + }; + githubUsername: string; + githubOwnership: boolean; + blogUrl: string; + feeds: Array; + blogOwnership: boolean; +}; + export type ThemeName = 'light' | 'dark'; diff --git a/src/web/src/pages/signup.tsx b/src/web/src/pages/signup.tsx new file mode 100644 index 0000000000..def31d066f --- /dev/null +++ b/src/web/src/pages/signup.tsx @@ -0,0 +1,271 @@ +import { createStyles, makeStyles, Theme } from '@material-ui/core'; +import { useState, useEffect } from 'react'; + +import Button from '@material-ui/core/Button'; +import { Formik, Form, FormikHelpers } from 'formik'; + +import useAuth from '../hooks/use-auth'; +import Overview from '../components/SignUp/Forms/Overview'; +import BasicInfo from '../components/SignUp/Forms/BasicInfo'; +import GitHubAccount from '../components/SignUp/Forms/GitHubAccount'; +import RSSFeeds from '../components/SignUp/Forms/RSSFeeds'; +import Review from '../components/SignUp/Forms/Review'; +import DynamicImage from '../components/DynamicImage'; + +import { SignUpForm } from '../interfaces'; +import formModels from '../components/SignUp/Schema/FormModel'; +import formSchema from '../components/SignUp/Schema/FormSchema'; +import { usersServiceUrl, webUrl } from '../config'; + +const { + firstName, + lastName, + displayName, + githubUsername, + github, + githubOwnership, + blogUrl, + feeds, + email, + blogOwnership, +} = formModels; + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + root: { + padding: '0', + margin: '0', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + minHeight: '100vh', + width: '100vw', + boxSizing: 'border-box', + position: 'relative', + fontSize: '1.1rem', + }, + imageContainer: { + minHeight: '100vh', + width: '100vw', + position: 'absolute', + top: '0', + bottom: '0', + zIndex: -1, + [theme.breakpoints.down(600)]: { + display: 'none', + }, + }, + signUpContainer: { + margin: '1% 0 1% 0', + display: 'grid', + gridTemplateRows: '10% auto 15%', + gridGap: '2%', + justifyItems: 'center', + fontFamily: 'Spartan', + height: '510px', + width: '510px', + padding: '1%', + borderRadius: '5px', + boxShadow: '2px 4px 4px 1px rgba(0, 0, 0, 0.1)', + background: '#ECF5FE', + '@media (max-height: 500px) and (max-width: 1024px)': { + margin: '0 0 65px 0', + }, + [theme.breakpoints.down(600)]: { + background: 'none', + boxShadow: 'none', + minHeight: '650px', + height: '600px', + position: 'absolute', + top: '0px', + width: '100%', + margin: '0', + padding: '0', + gridTemplateRows: '8% auto 17%', + }, + }, + title: { + color: '#121D59', + fontSize: '22px', + }, + infoContainer: { + width: '100%', + position: 'relative', + }, + buttonsWrapper: { + margin: '0 auto', + width: '50%', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + }, + button: { + height: '4rem', + width: '40%', + fontSize: '1.1em', + padding: '0.7em', + margin: '5px', + background: '#E0C05A', + '&:hover': { + color: 'black', + background: '#EBD898', + }, + }, + buttonLogin: { + height: '4rem', + width: '40%', + fontSize: '1.1em', + padding: '0.7em', + margin: '5px', + background: '#FF0000', + color: '#FFF', + '&:hover': { + background: '#FF7070', + }, + }, + text: { + textAlign: 'center', + fontSize: '0.9em', + color: '#474747', + }, + }) +); + +const SignUpPage = () => { + const classes = useStyles(); + const [activeStep, setActiveStep] = useState(0); + const currentSchema = formSchema[activeStep]; + const { user, token, login } = useAuth(); + const [loggedIn, setLoggedIn] = useState(false); + + const handleNext = () => { + setActiveStep(activeStep + 1); + }; + + const handlePrevious = () => { + setActiveStep(activeStep - 1); + }; + + useEffect(() => { + if (user) { + setLoggedIn(true); + } + }, [user]); + + const handleSubmit = async (values: SignUpForm, actions: FormikHelpers) => { + if (activeStep === 4) { + try { + const { firstName, lastName, email, displayName, github, feeds } = values; + const telescopeUser = { + firstName, + lastName, + email, + displayName, + github, + feeds, + }; + const response = await fetch(`${usersServiceUrl}/${user?.id}`, { + method: 'post', + headers: { + Authorization: `bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(telescopeUser), + }); + + if (!response.ok) { + alert('Unable to create account.'); + throw new Error('Unable to post - create account'); + } + login(); + return; + } catch (err) { + console.error(err, 'Unable to Post'); + window.location.href = `${webUrl}`; + } + } else { + handleNext(); + actions.setTouched({}); + actions.setSubmitting(false); + } + }; + + const renderForm = () => { + switch (activeStep) { + case 0: + return ; + case 1: + return ; + case 2: + return ; + case 3: + return ; + case 4: + return ; + default: + return null; + } + }; + + return ( +
+
+ +
+
+

Telescope Account

+ , + [blogOwnership.name]: false, + } as SignUpForm + } + > + {({ isSubmitting }) => ( + <> +
+ {renderForm()} +
+

Click NEXT to continue

+
+
+ {!loggedIn && ( + + )} + {activeStep > 0 && loggedIn && ( + + )} + {activeStep < 5 && loggedIn && ( + + )} +
+
+ + )} +
+
+
+ ); +}; + +export default SignUpPage;