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,23 +95,41 @@ const Interests = ({ active, interests, onTagClick }) => ( ...@@ -94,23 +95,41 @@ const Interests = ({ active, interests, onTagClick }) => (
<Step2Section active={active}> <Step2Section active={active}>
<H4>Interests</H4> <H4>Interests</H4>
<TagContainer> <TagContainer>
{interests.map(interest => ( {interests.length
<Tag ? interests.map(interest => (
focused <Tag
closeable focused
key={interest} closeable
onClick={() => onTagClick(interest)} key={interest}
> onClick={() => onTagClick(interest)}
{interest} >
</Tag> {interest}
))} </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,79 +3,162 @@ import { Col, Row } from '@zendeskgarden/react-grid'; ...@@ -3,79 +3,162 @@ 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]]; );
} }
return a; if (status === SearchStatus.error) {
return (
<InterestsSearchResultsContainer>
Could not search at this time, please try again later. ({result.message}
)
</InterestsSearchResultsContainer>
);
}
// 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;
export default ({ user, goToNextStep, toggleInterest }) => {
return (
<>
<Row>
<Col>
<H6 style={{ borderBottom: '1px solid lightgrey' }}>
<span style={{ color: 'darkgrey', fontSize: '.7em' }}>2.</span> Your
Interests
</H6>
<P>
Tell us what you're interested in so we can make your MoodleNet
experience tailored to you.
</P>
<TagContainer>
{words2.map(word => (
<Tag
closeable={user.interests.includes(word)}
focused={user.interests.includes(word)}
key={word}
onClick={() => toggleInterest(word)}
>
{word}
</Tag>
))}
</TagContainer>
<P style={{ fontWeight: 'bold' }}>Popular on MoodleNet</P>
<P>These tags are popular on MoodleNet.</P>
<TagContainer>
{words1.map(word => (
<Tag
focused={user.interests.includes(word)}
key={word}
onClick={() => toggleInterest(word)}
>
{word}
</Tag>
))}
</TagContainer>
</Col>
</Row>
</>
);
}; };
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;
return (
<>
<Row>
<Col>
<H6 style={{ borderBottom: '1px solid lightgrey' }}>
<span style={{ color: 'darkgrey', fontSize: '.7em' }}>2.</span>{' '}
Your Interests
</H6>
<P>
Tell us what you're interested in so we can make your MoodleNet
experience tailored to you.
</P>
<form onSubmit={this.onInterestsSearchSubmit}>
<TextInput
placeholder="Search for tags"
button={<Button type="submit">Search</Button>}
/>
</form>
<InterestsSearchResults {...this.state.interestsSearch}>
<TagContainer>
{words.map(word => (
<Tag
focused={user.interests.includes(word)}
key={word}
onClick={() => toggleInterest(word)}
>
{word}
</Tag>
))}
</TagContainer>
</InterestsSearchResults>
<P style={{ fontWeight: 'bold' }}>Popular on MoodleNet</P>
<P>These tags are popular on MoodleNet.</P>
<TagContainer>
{words.map(word => (
<Tag
focused={user.interests.includes(word)}
key={word}
onClick={() => toggleInterest(word)}
>
{word}
</Tag>
))}
</TagContainer>
</Col>
</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",
......
Markdown is supported
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