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> = ({
};
export const TagContainer = styled.div`
min-height: 55px;
margin: 0 0 10px 0;
${ZenTag} {
margin: 0 10px 10px 0;
}
......
......@@ -46,7 +46,7 @@ export default () => (
<Switch>
<Route exact path="/login" component={Login} />
<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
path="/"
component={() => (
......
......@@ -74,6 +74,7 @@ interface SignUpMatchParams {
}
interface SignUpState {
redirect?: string | null;
currentStep: number;
user: User;
}
......@@ -94,23 +95,41 @@ const Interests = ({ active, interests, onTagClick }) => (
<Step2Section active={active}>
<H4>Interests</H4>
<TagContainer>
{interests.map(interest => (
<Tag
focused
closeable
key={interest}
onClick={() => onTagClick(interest)}
>
{interest}
</Tag>
))}
{interests.length
? interests.map(interest => (
<Tag
focused
closeable
key={interest}
onClick={() => onTagClick(interest)}
>
{interest}
</Tag>
))
: 'None selected'}
</TagContainer>
<Button onClick={() => alert('add interest clicked')}>Add interest</Button>
</Step2Section>
);
const Languages = ({ active, languages }) => (
<Step2Section active={active}>
<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>
);
......@@ -119,7 +138,7 @@ export default class SignUp extends React.Component<SignUpProps, SignUpState> {
_profileElem: HTMLElement;
state = {
state: SignUpState = {
currentStep: -1,
user: {
username: '',
......@@ -141,19 +160,24 @@ export default class SignUp extends React.Component<SignUpProps, SignUpState> {
this.state.currentStep = Number(props.match.params.step);
if (!this.state.user.username && this.state.currentStep > 1) {
this.props.history.replace('/sign-up');
this.state.redirect = '/sign-up/1';
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);
// FIXME an error occurs when methods are passed by ref after binding if:
// - user goes straight to /sign-up/2/
// - gets redirects to /sign-up/1
// - clicks something to invoke bound method (e.g. Continue btn which calls #goToNextStep)
// this.randomizeEmojiId = this.randomizeEmojiId.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) {
this.state.redirect = null;
this.state.currentStep = Number(nextProps.match.params.step);
this.scrollForStep(this.state.currentStep);
}
......@@ -168,11 +192,11 @@ export default class SignUp extends React.Component<SignUpProps, SignUpState> {
<Step
{...{
user: this.state.user,
goToNextStep: this.goToNextStep,
goToPreviousStep: this.goToPreviousStep,
randomizeEmojiId: this.randomizeEmojiId,
linkUserState: this.linkUserState,
toggleInterest: this.toggleUserInterest
goToNextStep: this.goToNextStep.bind(this),
goToPreviousStep: this.goToPreviousStep.bind(this),
randomizeEmojiId: this.randomizeEmojiId.bind(this),
linkUserState: this.linkUserState.bind(this),
toggleInterest: this.toggleUserInterest.bind(this)
}}
/>
);
......@@ -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
* state so it can be displayed without uploading.
* Create function that loads an image from the user file system and set
* as `imageTypeName` on the state so it can be displayed without uploading.
* @param imageTypeName the type of image being set
*/
setUserImage(
createSetUserImageCallback(
imageTypeName: 'profileImage' | 'avatarImage'
): (evt: React.SyntheticEvent) => void {
return (evt: any) => {
......@@ -303,6 +327,8 @@ export default class SignUp extends React.Component<SignUpProps, SignUpState> {
render() {
if (!('step' in this.props.match.params)) {
return <Redirect to="/sign-up/1" />;
} else if (this.state.redirect) {
return <Redirect to={this.state.redirect} />;
}
return (
......@@ -316,7 +342,7 @@ export default class SignUp extends React.Component<SignUpProps, SignUpState> {
<Logo link={false} />
<PreviousStep
active={this.state.currentStep > 1}
onClick={this.goToPreviousStep}
onClick={() => this.goToPreviousStep()}
title={`Go back to Step ${this.state.currentStep - 1}`}
>
&lt;
......@@ -340,13 +366,13 @@ export default class SignUp extends React.Component<SignUpProps, SignUpState> {
<Col style={{ display: 'flex', justifyContent: 'flex-end' }}>
{this.state.currentStep > 1 ? (
<>
<Button secondary onClick={this.goToNextStep}>
<Button secondary onClick={() => this.goToNextStep()}>
Skip
</Button>
<div style={{ height: '10px', width: '10px' }} />
</>
) : null}
<Button onClick={this.goToNextStep}>Continue</Button>
<Button onClick={() => this.goToNextStep()}>Continue</Button>
</Col>
</Row>
</Grid>
......@@ -354,12 +380,12 @@ export default class SignUp extends React.Component<SignUpProps, SignUpState> {
<UserProfile
innerRef={e => (this._profileElem = e)}
user={this.state.user}
setUserImage={this.setUserImage}
setUserImage={type => this.createSetUserImageCallback(type)}
body={({ containerProps }) => {
return (
<SignUpProfileSection {...containerProps}>
<Interests
onTagClick={this.toggleUserInterest}
onTagClick={interest => this.toggleUserInterest(interest)}
active={this.state.currentStep > 1}
interests={this.state.user.interests}
/>
......
......@@ -3,79 +3,162 @@ import { Col, Row } from '@zendeskgarden/react-grid';
import H6 from '../../components/typography/H6/H6';
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 styled from '../../themes/styled';
import User from '../../types/User';
//TODO get tags from the API
const words = `offer
segment
slave
duck
instant
market
degree
populate
chick
dear
enemy
reply
drink
occur
support
shell
neck`;
// https://stackoverflow.com/a/6274381/2039244
function shuffle(a) {
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
const words = `offer,segment,slave,duck,instant,market,degree,populate,chick,dear,enemy,reply,drink,occur,support,shell,neck`.split(
','
);
const InterestsSearchResultsContainer = styled.div`
margin: 20px 0 0 0;
`;
function InterestsSearchResults({ status, count, result, children }) {
if (status === SearchStatus.complete) {
return (
<InterestsSearchResultsContainer>
<P style={{ fontWeight: 'bold' }}>{count} Search Results</P>
{children}
</InterestsSearchResultsContainer>
);
}
if (status === SearchStatus.in_progress) {
return (
<InterestsSearchResultsContainer>
Searching...
</InterestsSearchResultsContainer>
);
}
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'));
const words2 = shuffle(words.split('\n'));
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 Step2Props = {
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;
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) {
//language=SCSS
'typography.lg': `
&& {
margin-block-start: .65em;
margin-block-end: .65em;
font-size: ${theme.fontSize.lg};
line-height: ${theme.lineHeight.lg};
font-weight: ${theme.fontWeight.bold};
......@@ -244,6 +246,7 @@ export default function createTheme(theme: MoodleThemeInterface) {
//language=SCSS
'tags.tag_view': `
&& {
min-height: 45px;
padding: 15px;
box-shadow: 0 0 0 2px transparent;
background-color: ${theme.colour.base5};
......
......@@ -4,7 +4,7 @@
"outDir": "build/dist",
"module": "esnext",
"target": "es5",
"lib": ["es6", "dom"],
"lib": ["es7", "dom"],
"sourceMap": true,
"allowJs": true,
"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