Commit 70db89bb authored by Sam Gluck's avatar Sam Gluck
Browse files

Finalise implementation of wireframe for Sign Up step 2:

- Add "Add <what>" btns to Interests and Languages sections;
- Add placeholder "None selected" to Interests and Languages sections;
- Add search input for tags and dummy response;
- Add temp. bugfix to do with bound methods losing context (see code);
parent b19cb383
...@@ -29,6 +29,9 @@ const Tag: React.SFC<TagProps> = ({ ...@@ -29,6 +29,9 @@ const Tag: React.SFC<TagProps> = ({
}; };
export const TagContainer = styled.div` export const TagContainer = styled.div`
min-height: 55px;
margin: 0 0 10px 0;
${ZenTag} { ${ZenTag} {
margin: 0 10px 10px 0; margin: 0 10px 10px 0;
} }
......
...@@ -46,7 +46,7 @@ export default () => ( ...@@ -46,7 +46,7 @@ export default () => (
<Switch> <Switch>
<Route exact path="/login" component={Login} /> <Route exact path="/login" component={Login} />
<Route exact path="/sign-up" component={SignUp} /> <Route exact path="/sign-up" component={SignUp} />
<Route exact path="/sign-up/:step([12])" component={SignUp} /> <Route exact path="/sign-up/:step" component={SignUp} />
<ProtectedRoute <ProtectedRoute
path="/" path="/"
component={() => ( component={() => (
......
...@@ -74,6 +74,7 @@ interface SignUpMatchParams { ...@@ -74,6 +74,7 @@ interface SignUpMatchParams {
} }
interface SignUpState { interface SignUpState {
redirect?: string | null;
currentStep: number; currentStep: number;
user: User; user: User;
} }
...@@ -94,7 +95,8 @@ const Interests = ({ active, interests, onTagClick }) => ( ...@@ -94,7 +95,8 @@ const Interests = ({ active, interests, onTagClick }) => (
<Step2Section active={active}> <Step2Section active={active}>
<H4>Interests</H4> <H4>Interests</H4>
<TagContainer> <TagContainer>
{interests.map(interest => ( {interests.length
? interests.map(interest => (
<Tag <Tag
focused focused
closeable closeable
...@@ -103,14 +105,31 @@ const Interests = ({ active, interests, onTagClick }) => ( ...@@ -103,14 +105,31 @@ const Interests = ({ active, interests, onTagClick }) => (
> >
{interest} {interest}
</Tag> </Tag>
))} ))
: 'None selected'}
</TagContainer> </TagContainer>
<Button onClick={() => alert('add interest clicked')}>Add interest</Button>
</Step2Section> </Step2Section>
); );
const Languages = ({ active, languages }) => ( const Languages = ({ active, languages }) => (
<Step2Section active={active}> <Step2Section active={active}>
<H4>Languages</H4> <H4>Languages</H4>
<TagContainer>
{languages.length
? languages.map(lang => (
<Tag
focused
closeable
key={lang}
onClick={() => alert('lang clicked')}
>
{lang}
</Tag>
))
: 'None selected'}
</TagContainer>
<Button onClick={() => alert('add lang clicked')}>Add language</Button>
</Step2Section> </Step2Section>
); );
...@@ -119,7 +138,7 @@ export default class SignUp extends React.Component<SignUpProps, SignUpState> { ...@@ -119,7 +138,7 @@ export default class SignUp extends React.Component<SignUpProps, SignUpState> {
_profileElem: HTMLElement; _profileElem: HTMLElement;
state = { state: SignUpState = {
currentStep: -1, currentStep: -1,
user: { user: {
username: '', username: '',
...@@ -141,19 +160,24 @@ export default class SignUp extends React.Component<SignUpProps, SignUpState> { ...@@ -141,19 +160,24 @@ export default class SignUp extends React.Component<SignUpProps, SignUpState> {
this.state.currentStep = Number(props.match.params.step); this.state.currentStep = Number(props.match.params.step);
if (!this.state.user.username && this.state.currentStep > 1) { if (!this.state.user.username && this.state.currentStep > 1) {
this.props.history.replace('/sign-up'); this.state.redirect = '/sign-up/1';
return; return;
} }
this.randomizeEmojiId = this.randomizeEmojiId.bind(this); // FIXME an error occurs when methods are passed by ref after binding if:
this.linkUserState = this.linkUserState.bind(this); // - user goes straight to /sign-up/2/
this.goToNextStep = this.goToNextStep.bind(this); // - gets redirects to /sign-up/1
this.goToPreviousStep = this.goToPreviousStep.bind(this); // - clicks something to invoke bound method (e.g. Continue btn which calls #goToNextStep)
this.toggleUserInterest = this.toggleUserInterest.bind(this); // this.randomizeEmojiId = this.randomizeEmojiId.bind(this);
this.setUserImage = this.setUserImage.bind(this); // this.linkUserState = this.linkUserState.bind(this);
// this.goToPreviousStep = this.goToPreviousStep.bind(this);
// this.toggleUserInterest = this.toggleUserInterest.bind(this);
// this.createSetUserImageCallback = this.createSetUserImageCallback.bind(this);
// this.goToNextStep = this.goToNextStep.bind(this);
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
this.state.redirect = null;
this.state.currentStep = Number(nextProps.match.params.step); this.state.currentStep = Number(nextProps.match.params.step);
this.scrollForStep(this.state.currentStep); this.scrollForStep(this.state.currentStep);
} }
...@@ -168,11 +192,11 @@ export default class SignUp extends React.Component<SignUpProps, SignUpState> { ...@@ -168,11 +192,11 @@ export default class SignUp extends React.Component<SignUpProps, SignUpState> {
<Step <Step
{...{ {...{
user: this.state.user, user: this.state.user,
goToNextStep: this.goToNextStep, goToNextStep: this.goToNextStep.bind(this),
goToPreviousStep: this.goToPreviousStep, goToPreviousStep: this.goToPreviousStep.bind(this),
randomizeEmojiId: this.randomizeEmojiId, randomizeEmojiId: this.randomizeEmojiId.bind(this),
linkUserState: this.linkUserState, linkUserState: this.linkUserState.bind(this),
toggleInterest: this.toggleUserInterest toggleInterest: this.toggleUserInterest.bind(this)
}} }}
/> />
); );
...@@ -267,11 +291,11 @@ export default class SignUp extends React.Component<SignUpProps, SignUpState> { ...@@ -267,11 +291,11 @@ export default class SignUp extends React.Component<SignUpProps, SignUpState> {
} }
/** /**
* Load an image from the user file system and set as `imageTypeName` on the * Create function that loads an image from the user file system and set
* state so it can be displayed without uploading. * as `imageTypeName` on the state so it can be displayed without uploading.
* @param imageTypeName the type of image being set * @param imageTypeName the type of image being set
*/ */
setUserImage( createSetUserImageCallback(
imageTypeName: 'profileImage' | 'avatarImage' imageTypeName: 'profileImage' | 'avatarImage'
): (evt: React.SyntheticEvent) => void { ): (evt: React.SyntheticEvent) => void {
return (evt: any) => { return (evt: any) => {
...@@ -303,6 +327,8 @@ export default class SignUp extends React.Component<SignUpProps, SignUpState> { ...@@ -303,6 +327,8 @@ export default class SignUp extends React.Component<SignUpProps, SignUpState> {
render() { render() {
if (!('step' in this.props.match.params)) { if (!('step' in this.props.match.params)) {
return <Redirect to="/sign-up/1" />; return <Redirect to="/sign-up/1" />;
} else if (this.state.redirect) {
return <Redirect to={this.state.redirect} />;
} }
return ( return (
...@@ -316,7 +342,7 @@ export default class SignUp extends React.Component<SignUpProps, SignUpState> { ...@@ -316,7 +342,7 @@ export default class SignUp extends React.Component<SignUpProps, SignUpState> {
<Logo link={false} /> <Logo link={false} />
<PreviousStep <PreviousStep
active={this.state.currentStep > 1} active={this.state.currentStep > 1}
onClick={this.goToPreviousStep} onClick={() => this.goToPreviousStep()}
title={`Go back to Step ${this.state.currentStep - 1}`} title={`Go back to Step ${this.state.currentStep - 1}`}
> >
&lt; &lt;
...@@ -340,13 +366,13 @@ export default class SignUp extends React.Component<SignUpProps, SignUpState> { ...@@ -340,13 +366,13 @@ export default class SignUp extends React.Component<SignUpProps, SignUpState> {
<Col style={{ display: 'flex', justifyContent: 'flex-end' }}> <Col style={{ display: 'flex', justifyContent: 'flex-end' }}>
{this.state.currentStep > 1 ? ( {this.state.currentStep > 1 ? (
<> <>
<Button secondary onClick={this.goToNextStep}> <Button secondary onClick={() => this.goToNextStep()}>
Skip Skip
</Button> </Button>
<div style={{ height: '10px', width: '10px' }} /> <div style={{ height: '10px', width: '10px' }} />
</> </>
) : null} ) : null}
<Button onClick={this.goToNextStep}>Continue</Button> <Button onClick={() => this.goToNextStep()}>Continue</Button>
</Col> </Col>
</Row> </Row>
</Grid> </Grid>
...@@ -354,12 +380,12 @@ export default class SignUp extends React.Component<SignUpProps, SignUpState> { ...@@ -354,12 +380,12 @@ export default class SignUp extends React.Component<SignUpProps, SignUpState> {
<UserProfile <UserProfile
innerRef={e => (this._profileElem = e)} innerRef={e => (this._profileElem = e)}
user={this.state.user} user={this.state.user}
setUserImage={this.setUserImage} setUserImage={type => this.createSetUserImageCallback(type)}
body={({ containerProps }) => { body={({ containerProps }) => {
return ( return (
<SignUpProfileSection {...containerProps}> <SignUpProfileSection {...containerProps}>
<Interests <Interests
onTagClick={this.toggleUserInterest} onTagClick={interest => this.toggleUserInterest(interest)}
active={this.state.currentStep > 1} active={this.state.currentStep > 1}
interests={this.state.user.interests} interests={this.state.user.interests}
/> />
......
...@@ -3,56 +3,137 @@ import { Col, Row } from '@zendeskgarden/react-grid'; ...@@ -3,56 +3,137 @@ import { Col, Row } from '@zendeskgarden/react-grid';
import H6 from '../../components/typography/H6/H6'; import H6 from '../../components/typography/H6/H6';
import P from '../../components/typography/P/P'; import P from '../../components/typography/P/P';
import TextInput from '../../components/inputs/Text/Text';
import Button from '../../components/elements/Button/Button';
import Tag, { TagContainer } from '../../components/elements/Tag/Tag'; import Tag, { TagContainer } from '../../components/elements/Tag/Tag';
import styled from '../../themes/styled';
import User from '../../types/User';
//TODO get tags from the API //TODO get tags from the API
const words = `offer const words = `offer,segment,slave,duck,instant,market,degree,populate,chick,dear,enemy,reply,drink,occur,support,shell,neck`.split(
segment ','
slave );
duck
instant const InterestsSearchResultsContainer = styled.div`
market margin: 20px 0 0 0;
degree `;
populate
chick function InterestsSearchResults({ status, count, result, children }) {
dear if (status === SearchStatus.complete) {
enemy return (
reply <InterestsSearchResultsContainer>
drink <P style={{ fontWeight: 'bold' }}>{count} Search Results</P>
occur {children}
support </InterestsSearchResultsContainer>
shell );
neck`; }
if (status === SearchStatus.in_progress) {
// https://stackoverflow.com/a/6274381/2039244 return (
function shuffle(a) { <InterestsSearchResultsContainer>
for (let i = a.length - 1; i > 0; i--) { Searching...
const j = Math.floor(Math.random() * (i + 1)); </InterestsSearchResultsContainer>
[a[i], a[j]] = [a[j], a[i]]; );
}
if (status === SearchStatus.error) {
return (
<InterestsSearchResultsContainer>
Could not search at this time, please try again later. ({result.message}
)
</InterestsSearchResultsContainer>
);
} }
return a; // status === SearchStatus.idle
return null;
}
enum SearchStatus {
idle,
in_progress,
complete,
error
} }
const words1 = shuffle(words.split('\n')); type Step2Props = {
const words2 = shuffle(words.split('\n')); user: User;
toggleInterest: Function;
};
type Step2State = {
interestsSearch: {
status: SearchStatus;
count: number;
result: any;
};
};
export default class extends React.Component<Step2Props, Step2State> {
_searchTimeout: number = -1;
state = {
interestsSearch: {
status: SearchStatus.idle,
count: -1,
result: null
}
};
constructor(props) {
super(props);
this.onInterestsSearchSubmit = this.onInterestsSearchSubmit.bind(this);
}
// TODO search using API
onInterestsSearchSubmit(e) {
if (this._searchTimeout) {
clearTimeout(this._searchTimeout);
}
e.preventDefault();
this.setState({
interestsSearch: {
status: SearchStatus.in_progress,
count: -1,
result: null
}
});
this._searchTimeout = window.setTimeout(() => {
this.setState({
interestsSearch: {
status: SearchStatus.complete,
count: words.length,
result: words
}
});
}, 2000);
}
render() {
const { user, toggleInterest } = this.props;
export default ({ user, goToNextStep, toggleInterest }) => {
return ( return (
<> <>
<Row> <Row>
<Col> <Col>
<H6 style={{ borderBottom: '1px solid lightgrey' }}> <H6 style={{ borderBottom: '1px solid lightgrey' }}>
<span style={{ color: 'darkgrey', fontSize: '.7em' }}>2.</span> Your <span style={{ color: 'darkgrey', fontSize: '.7em' }}>2.</span>{' '}
Interests Your Interests
</H6> </H6>
<P> <P>
Tell us what you're interested in so we can make your MoodleNet Tell us what you're interested in so we can make your MoodleNet
experience tailored to you. experience tailored to you.
</P> </P>
<form onSubmit={this.onInterestsSearchSubmit}>
<TextInput
placeholder="Search for tags"
button={<Button type="submit">Search</Button>}
/>
</form>
<InterestsSearchResults {...this.state.interestsSearch}>
<TagContainer> <TagContainer>
{words2.map(word => ( {words.map(word => (
<Tag <Tag
closeable={user.interests.includes(word)}
focused={user.interests.includes(word)} focused={user.interests.includes(word)}
key={word} key={word}
onClick={() => toggleInterest(word)} onClick={() => toggleInterest(word)}
...@@ -61,10 +142,11 @@ export default ({ user, goToNextStep, toggleInterest }) => { ...@@ -61,10 +142,11 @@ export default ({ user, goToNextStep, toggleInterest }) => {
</Tag> </Tag>
))} ))}
</TagContainer> </TagContainer>
</InterestsSearchResults>
<P style={{ fontWeight: 'bold' }}>Popular on MoodleNet</P> <P style={{ fontWeight: 'bold' }}>Popular on MoodleNet</P>
<P>These tags are popular on MoodleNet.</P> <P>These tags are popular on MoodleNet.</P>
<TagContainer> <TagContainer>
{words1.map(word => ( {words.map(word => (
<Tag <Tag
focused={user.interests.includes(word)} focused={user.interests.includes(word)}
key={word} key={word}
...@@ -78,4 +160,5 @@ export default ({ user, goToNextStep, toggleInterest }) => { ...@@ -78,4 +160,5 @@ export default ({ user, goToNextStep, toggleInterest }) => {
</Row> </Row>
</> </>
); );
}; }
}
...@@ -163,6 +163,8 @@ export default function createTheme(theme: MoodleThemeInterface) { ...@@ -163,6 +163,8 @@ export default function createTheme(theme: MoodleThemeInterface) {
//language=SCSS //language=SCSS
'typography.lg': ` 'typography.lg': `
&& { && {
margin-block-start: .65em;
margin-block-end: .65em;
font-size: ${theme.fontSize.lg}; font-size: ${theme.fontSize.lg};
line-height: ${theme.lineHeight.lg}; line-height: ${theme.lineHeight.lg};
font-weight: ${theme.fontWeight.bold}; font-weight: ${theme.fontWeight.bold};
...@@ -244,6 +246,7 @@ export default function createTheme(theme: MoodleThemeInterface) { ...@@ -244,6 +246,7 @@ export default function createTheme(theme: MoodleThemeInterface) {
//language=SCSS //language=SCSS
'tags.tag_view': ` 'tags.tag_view': `
&& { && {
min-height: 45px;
padding: 15px; padding: 15px;
box-shadow: 0 0 0 2px transparent; box-shadow: 0 0 0 2px transparent;
background-color: ${theme.colour.base5}; background-color: ${theme.colour.base5};
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
"outDir": "build/dist", "outDir": "build/dist",
"module": "esnext", "module": "esnext",
"target": "es5", "target": "es5",
"lib": ["es6", "dom"], "lib": ["es7", "dom"],
"sourceMap": true, "sourceMap": true,
"allowJs": true, "allowJs": true,
"jsx": "react", "jsx": "react",
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment