Commit 23ea1c01 authored by Sam Gluck's avatar Sam Gluck
Browse files

sign up WIP

parent 9e3772b9
......@@ -9,10 +9,20 @@ const LogoH1 = styled.h1`
margin: 0;
`;
export default () => (
<LogoH1>
<Link to="/" title="MoodleNet">
<img src={moodleNetLogo} alt="MoodleNet" />
</Link>
</LogoH1>
);
type LogoProps = {
link?: boolean;
};
export default ({ link = true }: LogoProps) => {
let image = <img src={moodleNetLogo} alt="MoodleNet" />;
if (link) {
image = (
<Link to={link ? '/' : '#'} title="MoodleNet">
{image}
</Link>
);
}
return <LogoH1>{image}</LogoH1>;
};
import * as React from 'react';
import { Button as ZenButton } from '@zendeskgarden/react-buttons';
interface ButtonProps {
interface ButtonProps extends React.ButtonHTMLAttributes<object> {
children?: any;
secondary?: boolean;
className?: string;
......
import * as React from 'react';
import styled from '../../../themes/styled';
const Bounce = styled.div<any>`
background-color: ${props => props.theme.styles.colour.primary};
`;
export default () => {
return (
<div className="spinner">
<div className="bounce1" />
<div className="bounce2" />
<div className="bounce3" />
<Bounce className="bounce1" />
<Bounce className="bounce2" />
<Bounce className="bounce3" />
</div>
);
};
......@@ -2,6 +2,7 @@ import * as React from 'react';
import { Tag as ZenTag, Close } from '@zendeskgarden/react-tags';
export type TagProps = {
onClick?: Function;
closeable?: boolean;
onClose?: Function;
focused?: boolean;
......
......@@ -5,32 +5,47 @@ import {
Item
} from '@zendeskgarden/react-select';
import styled from '../../../themes/styled';
import styled, { StyledThemeInterface } from '../../../themes/styled';
import Flag from '../../elements/Flag/Flag';
type LanguageSelectState = {
selectedKey?: string;
};
type LanguageSelectProps = {
fullWidth?: boolean;
} & React.SelectHTMLAttributes<object>;
type StyledProps = StyledThemeInterface & LanguageSelectProps;
const SelectField = styled(ZenSelectField)`
max-width: 300px;
max-width: ${(props: StyledProps) => (props.fullWidth ? 'none' : '300px')};
width: ${(props: StyledProps) => (props.fullWidth ? '100%' : 'auto')};
`;
export const languageNames = {
'en-gb': 'English, United Kingdom',
'en-us': 'English, United States'
};
const options = [
<Item key="en-gb">
<Flag flag="gb" />
&nbsp; English, United Kingdom
&nbsp; {languageNames['en-gb']}
</Item>,
<Item key="en-us">
<Flag flag="us" />
&nbsp; English, United States
&nbsp; {languageNames['en-us']}
</Item>
];
/**
* TODO this is a dummy implementation of localisation toggling
*/
export default class extends React.Component<{}, LanguageSelectState> {
export default class extends React.Component<
LanguageSelectProps,
LanguageSelectState
> {
state = {
selectedKey: 'en-gb'
};
......@@ -41,7 +56,7 @@ export default class extends React.Component<{}, LanguageSelectState> {
render() {
return (
<SelectField>
<SelectField {...this.props}>
<Select
selectedKey={this.state.selectedKey}
onChange={selectedKey => this.setState({ selectedKey })}
......@@ -49,7 +64,9 @@ export default class extends React.Component<{}, LanguageSelectState> {
>
<Flag flag={this.state.selectedKey.split('-')[1]} />
&nbsp;&nbsp;
{this.state.selectedKey}
{this.props.fullWidth
? languageNames[this.state.selectedKey]
: this.state.selectedKey}
</Select>
</SelectField>
);
......
import * as React from 'react';
import { Input } from '@zendeskgarden/react-textfields';
export default function Text({ ...props }) {
import styled from '../../../themes/styled';
import { InputHTMLAttributes } from 'react';
import { ValidationType } from '../../../pages/login/types';
type TextArgs = {
button?: JSX.Element;
//TODO copy over zen garden props and use proper validation types
validation?: ValidationType | null;
} & InputHTMLAttributes<object>;
const WithButton = styled.div`
display: flex;
flex-direction: row;
& > input {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
border-right: 0;
}
& > button {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
`;
export default function Text({ button, ...props }: TextArgs) {
if (button) {
return (
<WithButton>
<Input {...props} />
{button}
</WithButton>
);
}
return <Input {...props} />;
}
......@@ -22,7 +22,9 @@ import '../../styles/flag-icons.css';
import '../../styles/loader.css';
export const AppStyles = styled.div`
font-family: ${props => props.theme.styles.fontFamily};
* {
font-family: ${props => props.theme.styles.fontFamily};
}
`;
export default class App extends React.Component {
......
import * as React from 'react';
import Loadable from 'react-loadable';
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
import styled from '../../themes/styled';
......@@ -6,6 +7,7 @@ import Home from '../../pages/home/Home';
import Login from '../../pages/login/Login';
import NotFound from '../../pages/not-found/NotFound';
import ProtectedRoute from './ProtectedRoute';
import Loader from '../../components/elements/Loader/Loader';
const AppInner = styled.div`
display: flex;
......@@ -14,12 +16,20 @@ const AppInner = styled.div`
height: 100%;
`;
const SignUp = Loadable({
loader: () => import('../../pages/sign-up/SignUp'),
loading: () => <Loader />,
delay: 300
});
export default () => (
<Router>
<AppInner>
<Switch>
<ProtectedRoute exact path="/" component={Home} />
<Route exact path="/login" component={Login} />
<Route exact path="/sign-up" component={SignUp} />
<Route exact path="/sign-up/:step([1-9])" component={SignUp} />
<Route component={NotFound} />
</Switch>
</AppInner>
......
......@@ -13,6 +13,10 @@ injectGlobal`
width: 100%;
height: 100%;
}
* {
box-sizing: border-box;
}
`;
ReactDOM.render(<App />, document.getElementById('root'));
......
import * as React from 'react';
import compose from 'recompose/compose';
import { graphql, OperationOption } from 'react-apollo';
import { Redirect, Route, RouteComponentProps } from 'react-router-dom';
import { Link, Redirect, Route, RouteComponentProps } from 'react-router-dom';
import { Col, Grid, Row } from '@zendeskgarden/react-grid';
import { withTheme } from '@zendeskgarden/react-theming';
......@@ -240,7 +240,7 @@ class Login extends React.Component<LoginProps, LoginState> {
pointerEvents: 'none'
}}
>
———
<span style={{ fontFamily: 'sans-serif' }}>———</span>
<span
style={{
color: 'grey',
......@@ -250,7 +250,7 @@ class Login extends React.Component<LoginProps, LoginState> {
>
OR
</span>
———
<span style={{ fontFamily: 'sans-serif' }}>———</span>
</P>
<LoginForm
validation={this.state.validation}
......@@ -289,7 +289,9 @@ class Login extends React.Component<LoginProps, LoginState> {
// paddingBottom: '5%',
}}
>
<Button>Create account</Button>
<Link to="/sign-up">
<Button>Create account</Button>
</Link>
<Spacer />
<Button secondary>Browse as guest</Button>
</Row>
......
......@@ -100,7 +100,7 @@ export default class extends React.Component<LoginFormProps, LoginFormState> {
placeholder="Enter your username"
value={this.state.username}
validation={this.getValidation(ValidationField.username)}
onChange={evt => {
onChange={(evt: any) => {
this.setState({
username: evt.target.value
});
......@@ -121,7 +121,7 @@ export default class extends React.Component<LoginFormProps, LoginFormState> {
placeholder="Enter your password"
value={this.state.password}
validation={this.getValidation(ValidationField.password)}
onChange={evt => {
onChange={(evt: any) => {
this.setState({
password: evt.target.value
});
......
import styled from '../../themes/styled';
interface PreviousStepProps {
theme?: object;
active: boolean;
}
const PreviousStep = styled.div<PreviousStepProps>`
position: absolute;
color: ${props => props.theme.styles.colour.primary};
font-weight: bold;
font-size: 1.5rem;
cursor: pointer;
padding: 10px;
top: -4px;
left: ${props => (props.active ? -23 : -15)}px;
opacity: ${props => (props.active ? 1 : 0)};
transition: all 0.2s linear;
`;
export default PreviousStep;
import * as React from 'react';
import { Redirect, RouteComponentProps } from 'react-router';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faImage, faUser } from '@fortawesome/free-solid-svg-icons';
import { Grid, Row, Col } from '@zendeskgarden/react-grid';
import styled, { StyledThemeInterface } from '../../themes/styled';
import Logo from '../../components/brand/Logo/Logo';
import Body from '../../components/chrome/Body/Body';
import H6 from '../../components/typography/H6/H6';
import PreviousStep from './PreviousStep';
import Step1 from './Step1';
import Step2 from './Step2';
import P from '../../components/typography/P/P';
const SignUpBody = styled(Body)`
display: flex;
flex-direction: row;
`;
//TODO media queries/responsivity
const Sidebar = styled.div`
width: 25%;
height: 100%;
padding: 25px;
background-color: ${(props: StyledThemeInterface) =>
props.theme.styles.colour.base5};
`;
//TODO media queries/responsivity
const UserProfile = styled.div`
width: 75%;
height: 100%;
margin-left: 1%;
background-color: ${(props: StyledThemeInterface) =>
props.theme.styles.colour.base5};
`;
//TODO don't use any props type
const UserProfileImage = styled.div<any>`
position: relative;
width: 100%;
height: 400px;
background-color: ${props => props.theme.styles.colour.base4};
background-image: ${props =>
props.backgroundImage
? `url(${props.backgroundImage}), linear-gradient(white, grey)`
: ''};
background-position: center;
background-size: 100% auto;
color: ${props => props.theme.styles.colour.base3};
display: flex;
align-items: center;
justify-content: center;
font-size: 6rem;
`;
const liftUserAvatar = -75;
//TODO don't use any props type
const UserAvatar = styled.div<any>`
overflow: hidden;
height: 150px;
width: 150px;
border-radius: 75px;
background-color: ${props => props.theme.styles.colour.base3};
background-image: ${props => `url(${props.backgroundImage})`};
background-position: center;
background-size: cover;
background-repeat: no-repeat;
color: ${props => props.theme.styles.colour.base5};
position: relative;
top: ${liftUserAvatar}px;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: center;
font-size: 4rem;
`;
const UserDetails = styled.div`
position: relative;
top: ${liftUserAvatar}px;
text-align: center;
`;
const FileInput = styled.input`
cursor: pointer;
height: calc(100% + 100px);
width: 100%;
position: absolute;
top: -100px;
left: 0;
`;
//TODO this should be offloaded to an API
function generateEmojiId(): string {
const emojis = [
'😀',
'😃',
'😣',
'😐',
'🤥',
'🍏',
'🍎',
'🍐',
'🍊',
'🍋',
'🐶',
'🐱',
'🐭',
'🐹',
'🐰',
'😀',
'😃',
'😄',
'😁',
'😆',
'🌹',
'💐',
'🍁',
'🐴'
];
let id = '';
for (let i = 0; i < 3; i++) {
id += emojis[Math.floor(Math.random() * emojis.length)];
}
return id;
}
interface SignUpMatchParams {
step: string;
}
interface SignUpState {
currentStep: number;
user: {
username: string;
password: string;
email: string;
emojiId: string;
avatarImage?: string;
profileImage?: string;
role: string;
location: string;
language: string;
interests: string[];
languages: string[];
};
}
interface SignUpProps extends RouteComponentProps<SignUpMatchParams> {}
const FadedFallbackText = ({ value, fallback }) =>
value ? (
<span>{value}</span>
) : (
<span style={{ color: 'lightgrey' }}>{fallback}</span>
);
const Interests = ({ interests }) => <div>interests</div>;
const Languages = ({ languages }) => <div>languages</div>;
export default class SignUp extends React.Component<SignUpProps, SignUpState> {
static stepComponents = [Step1, Step2];
state = {
currentStep: -1,
user: {
username: '',
password: '',
email: '',
emojiId: generateEmojiId(),
avatarImage: undefined,
profileImage: undefined,
role: '',
location: '',
language: 'en-gb',
interests: [] as string[],
languages: [] as string[]
}
};
constructor(props) {
super(props);
this.state.currentStep = Number(props.match.params.step);
//TODO reinstate when dev for sign up is done
// if (!this.state.user.username && this.state.currentStep > 1) {
// window.location.href = '/sign-up';
// return;
// }
this.randomizeEmojiId = this.randomizeEmojiId.bind(this);
this.linkUserState = this.linkUserState.bind(this);
this.goToNextStep = this.goToNextStep.bind(this);
this.goToPreviousStep = this.goToPreviousStep.bind(this);
this.toggleUserInterest = this.toggleUserInterest.bind(this);
this.setUserImage = this.setUserImage.bind(this);
}
componentWillReceiveProps(nextProps) {
this.state.currentStep = Number(nextProps.match.params.step);
}
getStepComponent() {
const stepProps = {
user: this.state.user,
goToNextStep: this.goToNextStep,
goToPreviousStep: this.goToPreviousStep,
randomizeEmojiId: this.randomizeEmojiId,
linkUserState: this.linkUserState,
toggleInterest: this.toggleUserInterest
};
const stepIdx = this.state.currentStep - 1;
let Step = SignUp.stepComponents[stepIdx];
if (!Step) {
return null;
}
return <Step {...stepProps} />;
}
randomizeEmojiId() {
this.setState({
user: {
...this.state.user,
emojiId: generateEmojiId()
}
});
}
goToPreviousStep() {
const prevStep = Number(this.state.currentStep) - 1;
if (prevStep > 0) {
this.props.history.push(`/sign-up/${prevStep}`);
}
}
goToNextStep() {
const nextStep = Number(this.state.currentStep) + 1;
if (nextStep > SignUp.stepComponents.length) {
// the user has completed sign up, set them in the local cache and
// redirect them to the homepage
//TODO set in local cache
this.props.history.push('/');
return;
}
this.props.history.push(`/sign-up/${nextStep}`);
}
/**
* Produce a callback for an input's onChange prop that sets
* the value of `field` at `this.state.user.{field}`.
* @param field {String} field to create setter callback for
**/
linkUserState(field) {
return evt =>
this.setState({
user: {
...this.state.user,
[field]: evt.target.value
}
});
}
toggleUserInterest(interest: string) {