Commit 1c5940cc authored by Ray Schamp's avatar Ray Schamp

Add student registration flow

parent 47ebef1b
......@@ -6,6 +6,7 @@ var intl = require('../../lib/intl.jsx');
var log = require('../../lib/log');
var smartyStreets = require('../../lib/smarty-streets');
var Avatar = require('../../components/avatar/avatar.jsx');
var Button = require('../../components/forms/button.jsx');
var Card = require('../../components/card/card.jsx');
var CharCount = require('../../components/forms/charcount.jsx');
......@@ -36,7 +37,7 @@ var NextStepButton = React.createClass({
},
render: function () {
return (
<Button type="submit" disabled={this.props.waiting} className="card-button">
<Button type="submit" disabled={this.props.waiting} className="card-button" {... this.props}>
{this.props.waiting ?
<Spinner /> :
this.props.text
......@@ -81,16 +82,16 @@ module.exports = {
return this.props.onNextStep(formData);
case 'username exists':
return invalidate({
'user.username': formatMessage({id: 'general.validationUsernameExists'})
'user.username': formatMessage({id: 'registration.validationUsernameExists'})
});
case 'bad username':
return invalidate({
'user.username': formatMessage({id: 'general.validationUsernameVulgar'})
'user.username': formatMessage({id: 'registration.validationUsernameVulgar'})
});
case 'invalid username':
default:
return invalidate({
'user.username': formatMessage({id: 'general.validationUsernameInvalid'})
'user.username': formatMessage({id: 'registration.validationUsernameInvalid'})
});
}
}.bind(this));
......@@ -98,14 +99,14 @@ module.exports = {
render: function () {
var formatMessage = this.props.intl.formatMessage;
return (
<h2><intl.FormattedMessage id="teacherRegistration.usernameStepTitle" /></h2>
<Slide className="registration-step username-step">
<h2><intl.FormattedMessage id="registration.usernameStepTitle" /></h2>
<p className="description">
<intl.FormattedMessage id="teacherRegistration.usernameStepDescription" />
<intl.FormattedMessage id="registration.usernameStepDescription" />
</p>
<Card>
<Form onValidSubmit={this.onValidSubmit}>
<Input label={formatMessage({id: 'general.createUsername'})}
<Input label={formatMessage({id: 'registration.createUsername'})}
className={this.state.validUsername}
type="text"
name="user.username"
......@@ -116,13 +117,13 @@ module.exports = {
}}
validationErrors={{
matchRegexp: formatMessage({
id: 'teacherRegistration.validationUsernameRegexp'
id: 'registration.validationUsernameRegexp'
}),
minLength: formatMessage({
id: 'teacherRegistration.validationUsernameMinLength'
id: 'registration.validationUsernameMinLength'
}),
maxLength: formatMessage({
id: 'teacherRegistration.validationUsernameMaxLength'
id: 'registration.validationUsernameMaxLength'
})
}}
required />
......@@ -136,24 +137,24 @@ module.exports = {
}}
validationErrors={{
minLength: formatMessage({
id: 'teacherRegistration.validationPasswordLength'
id: 'registration.validationPasswordLength'
}),
notEquals: formatMessage({
id: 'teacherRegistration.validationPasswordNotEquals'
id: 'registration.validationPasswordNotEquals'
}),
notEqualsField: formatMessage({
id: 'teacherRegistration.validationPasswordNotUsername'
id: 'registration.validationPasswordNotUsername'
})
}}
required />
<Checkbox label={formatMessage({id: 'teacherRegistration.showPassword'})}
<Checkbox label={formatMessage({id: 'registration.showPassword'})}
value={this.state.showPassword}
onChange={this.onChangeShowPassword}
help={null}
name="showPassword" />
<GeneralError name="all" />
<NextStepButton waiting={this.props.waiting || this.state.waiting}
text={<intl.FormattedMessage id="teacherRegistration.nextStep" />} />
text={<intl.FormattedMessage id="registration.nextStep" />} />
</Form>
</Card>
<StepNavigation steps={this.props.totalSteps - 1} active={this.props.activeStep} />
......@@ -195,12 +196,12 @@ module.exports = {
return (
<Slide className="registration-step demographics-step">
<h2>
<intl.FormattedMessage id="teacherRegistration.personalStepTitle" />
<intl.FormattedMessage id="registration.personalStepTitle" />
</h2>
<p className="description">
<intl.FormattedMessage id="teacherRegistration.personalStepDescription" />
<intl.FormattedMessage id="registration.personalStepDescription" />
<Tooltip title={'?'}
tipContent={formatMessage({id: 'teacherRegistration.nameStepTooltip'})} />
tipContent={formatMessage({id: 'registration.nameStepTooltip'})} />
</p>
<Card>
<Form onValidSubmit={this.props.onNextStep}>
......@@ -236,7 +237,7 @@ module.exports = {
label="I'm a robot!"
name="user.isRobot" />
<NextStepButton waiting={this.props.waiting}
text={<intl.FormattedMessage id="teacherRegistration.nextStep" />} />
text={<intl.FormattedMessage id="registration.nextStep" />} />
</Form>
</Card>
<StepNavigation steps={this.props.totalSteps - 1} active={this.props.activeStep} />
......@@ -260,7 +261,7 @@ module.exports = {
<p className="description">
<intl.FormattedMessage id="teacherRegistration.nameStepDescription" />
<Tooltip title={'?'}
tipContent={formatMessage({id: 'teacherRegistration.nameStepTooltip'})} />
tipContent={formatMessage({id: 'registration.nameStepTooltip'})} />
</p>
<Card>
<Form onValidSubmit={this.props.onNextStep}>
......@@ -273,7 +274,7 @@ module.exports = {
name="user.name.last"
required />
<NextStepButton waiting={this.props.waiting}
text={<intl.FormattedMessage id="teacherRegistration.nextStep" />} />
text={<intl.FormattedMessage id="registration.nextStep" />} />
</Form>
</Card>
<StepNavigation steps={this.props.totalSteps - 1} active={this.props.activeStep} />
......@@ -306,7 +307,7 @@ module.exports = {
<p className="description">
<intl.FormattedMessage id="teacherRegistration.phoneStepDescription" />
<Tooltip title={'?'}
tipContent={formatMessage({id: 'teacherRegistration.nameStepTooltip'})} />
tipContent={formatMessage({id: 'registration.nameStepTooltip'})} />
</p>
<Card>
<Form onValidSubmit={this.onValidSubmit}>
......@@ -321,7 +322,7 @@ module.exports = {
isFalse: formatMessage({id: 'teacherRegistration.validationPhoneConsent'})
}} />
<NextStepButton waiting={this.props.waiting}
text={<intl.FormattedMessage id="teacherRegistration.nextStep" />} />
text={<intl.FormattedMessage id="registration.nextStep" />} />
</Form>
</Card>
<StepNavigation steps={this.props.totalSteps - 1} active={this.props.activeStep} />
......@@ -382,7 +383,7 @@ module.exports = {
<p className="description">
<intl.FormattedMessage id="teacherRegistration.orgStepDescription" />
<Tooltip title={'?'}
tipContent={formatMessage({id: 'teacherRegistration.nameStepTooltip'})} />
tipContent={formatMessage({id: 'registration.nameStepTooltip'})} />
</p>
<Card>
<Form onValidSubmit={this.onValidSubmit}>
......@@ -419,7 +420,7 @@ module.exports = {
placeholder={'http://'} />
</div>
<NextStepButton waiting={this.props.waiting}
text={<intl.FormattedMessage id="teacherRegistration.nextStep" />} />
text={<intl.FormattedMessage id="registration.nextStep" />} />
</Form>
</Card>
<StepNavigation steps={this.props.totalSteps - 1} active={this.props.activeStep} />
......@@ -496,7 +497,7 @@ module.exports = {
<p className="description">
<intl.FormattedMessage id="teacherRegistration.addressStepDescription" />
<Tooltip title={'?'}
tipContent={formatMessage({id: 'teacherRegistration.nameStepTooltip'})} />
tipContent={formatMessage({id: 'registration.nameStepTooltip'})} />
</p>
<Card>
<Form onValidSubmit={this.onValidSubmit}>
......@@ -530,7 +531,7 @@ module.exports = {
required />
<GeneralError name="all" />
<NextStepButton waiting={this.props.waiting || this.state.waiting}
text={<intl.FormattedMessage id="teacherRegistration.nextStep" />} />
text={<intl.FormattedMessage id="registration.nextStep" />} />
</Form>
</Card>
<StepNavigation steps={this.props.totalSteps - 1} active={this.props.activeStep} />
......@@ -567,7 +568,7 @@ module.exports = {
<p className="description">
<intl.FormattedMessage id="teacherRegistration.useScratchStepDescription" />
<Tooltip title={'?'}
tipContent={formatMessage({id: 'teacherRegistration.nameStepTooltip'})} />
tipContent={formatMessage({id: 'registration.nameStepTooltip'})} />
</p>
<Card>
<Form onValidSubmit={this.props.onNextStep}>
......@@ -587,7 +588,7 @@ module.exports = {
<CharCount maxCharacters={this.props.maxCharacters}
currentCharacters={this.state.characterCount} />
<NextStepButton waiting={this.props.waiting}
text={<intl.FormattedMessage id="teacherRegistration.nextStep" />} />
text={<intl.FormattedMessage id="registration.nextStep" />} />
</Form>
</Card>
<StepNavigation steps={this.props.totalSteps - 1} active={this.props.activeStep} />
......@@ -634,7 +635,7 @@ module.exports = {
<p className="description">
<intl.FormattedMessage id="teacherRegistration.emailStepDescription" />
<Tooltip title={'?'}
tipContent={formatMessage({id: 'teacherRegistration.nameStepTooltip'})} />
tipContent={formatMessage({id: 'registration.nameStepTooltip'})} />
</p>
<Card>
<Form onValidSubmit={this.onValidSubmit}>
......@@ -654,7 +655,7 @@ module.exports = {
required />
<GeneralError name="all" />
<NextStepButton waiting={this.props.waiting}
text={<intl.FormattedMessage id="teacherRegistration.nextStep" />} />
text={<intl.FormattedMessage id="registration.nextStep" />} />
</Form>
</Card>
<StepNavigation steps={this.props.totalSteps - 1} active={this.props.activeStep} />
......@@ -709,6 +710,96 @@ module.exports = {
);
}
})),
ClassInviteStep: intl.injectIntl(React.createClass({
getDefaultProps: function () {
return {
classroom: {
title: '',
thumbnail: '',
educator: {
username: '',
profile: {
images: ''
}
}
},
messages: {
'general.getStarted': 'Get Started',
'registration.classroomInviteStepDescription': 'has invited you to join the class:'
},
waiting: false
};
},
onNextStep: function () {
console.log("onNextStep");
this.props.onNextStep();
},
render: function () {
return (
<Slide className="registration-step class-invite-step">
<Avatar className="invite-avatar" src={this.props.classroom.educator.profile.images['50x50']} />
<h2>{this.props.classroom.educator.username}</h2>
<p className="description">
{this.props.messages['registration.classroomInviteStepDescription']}
</p>
<Card>
<div className="contents">
<h3>{this.props.classroom.title}</h3>
<img className="class-image" src={this.props.classroom.image} />
</div>
<NextStepButton onClick={this.onNextStep}
waiting={this.props.waiting}
text={this.props.messages['general.getStarted']} />
</Card>
<StepNavigation steps={this.props.totalSteps - 1} active={this.props.activeStep} />
</Slide>
);
}
})),
ClassWelcomeStep: intl.injectIntl(React.createClass({
getDefaultProps: function () {
return {
classroom: {
title: '',
thumbnail: '',
educator: {
username: '',
profile: {
images: ''
}
}
},
messages: {
'registration.goToClass': 'Go to Class',
'registration.welcomeStepDescription': 'You have successfully set up a Scratch account! ' +
'You are now a member of the class:',
'registration.welcomeStepPrompt': 'To get started, click on the button below.',
'registration.welcomeStepTitle': 'Hurray! Welcome to Scratch!'
}
};
},
onNextStep: function () {
this.props.onNextStep();
},
render: function () {
return (
<Slide className="registration-step class-welcome-step">
<h2>{this.props.messages['registration.welcomeStepTitle']}</h2>
<p className="description">{this.props.messages['registration.welcomeStepDescription']}</p>
<Card>
<div className="contents">
<h3>{this.props.classroom.title}</h3>
<img className="class-image" src={this.props.classroom.image} />
<p>{this.props.messages['registration.welcomeStepPrompt']}</p>
</div>
<NextStepButton onClick={this.onNextStep}
waiting={this.props.waiting}
text={this.props.messages['registration.goToClass']} />
</Card>
</Slide>
);
}
})),
RegistrationError: intl.injectIntl(React.createClass({
render: function () {
return (
......
......@@ -110,6 +110,7 @@
"registration.confirmYourEmail": "Confirm Your Email",
"registration.confirmYourEmailDescription": "If you haven't already, please click the link in the confirmation email sent to:",
"registration.createUsername": "Create a Username",
"registration.goToClass": "Go to Class",
"registration.lastStepTitle": "Thank you for requesting a Scratch Teacher Account",
"registration.lastStepDescription": "We are currently processing your application. ",
"registration.nameStepTooltip": "This information is used for verification and to aggregate usage statistics.",
......@@ -130,5 +131,7 @@
"registration.validationUsernameInvalid": "Invalid username",
"registration.waitForApproval": "Wait for Approval",
"registration.waitForApprovalDescription": "Your information is being reviewed. Please be patient, the approval process can take up to 24 hours. You will receive an email with your login information once your account has been created.",
"registration.welcomeStepDescription": "You have successfully set up a Scratch account! You are now a member of the class:",
"registration.welcomeStepPrompt": "To get started, click on the button below.",
"registration.welcomeStepTitle": "Hurray! Welcome to Scratch!"
}
......@@ -12,60 +12,68 @@
"title": "About"
},
{
"name": "developers",
"pattern": "^/developers/?$",
"view": "developers/developers",
"title": "Developers"
"name": "guidelines",
"pattern": "^/community_guidelines/?$",
"view": "guidelines/guidelines",
"title": "Scratch Community Guidelines"
},
{
"name": "hoc",
"pattern": "^/hoc/?$",
"view": "hoc/hoc",
"title": "Hour of Code"
"name": "student-registration",
"pattern": "^/classes/:id/register/:token",
"view": "studentregistration/studentregistration",
"title": "Class Registration"
},
{
"name": "explore",
"pattern": "^/explore/:projects/:all/?$",
"routeAlias": "^/explore(?!/ajax)",
"view": "explore/explore",
"title": "Explore"
"name": "conference-index",
"pattern": "^/conference/?$",
"routeAlias": "^/conference(?!/201[4-5])",
"view": "conference/index/index",
"title": "Scratch Conference",
"viewportWidth": "device-width"
},
{
"name": "explore-redirect",
"pattern": "^/explore/?$",
"routeAlias": "^/explore(?!/ajax)",
"redirect": "/explore/projects/all"
"name": "conference-plan",
"pattern": "^/conference/plan/?$",
"routeAlias": "^/conference(?!/201[4-5])",
"view": "conference/plan/plan",
"title": "Plan Your Visit",
"viewportWidth": "device-width"
},
{
"name": "explore-projects-redirect",
"pattern": "^/explore/projects/?$",
"routeAlias": "^/explore(?!/ajax)",
"redirect": "/explore/projects/all"
"name": "conference-expectations",
"pattern": "^/conference/expect/?$",
"routeAlias": "^/conference(?!/201[4-5])",
"view": "conference/expect/expect",
"title": "What to Expect",
"viewportWidth": "device-width"
},
{
"name": "explore-studios-redirect",
"pattern": "^/explore/studios/?$",
"routeAlias": "^/explore(?!/ajax)",
"redirect": "/explore/studios/all"
"name": "conference-schedule",
"pattern": "^/conference/schedule/?$",
"routeAlias": "^/conference(?!/201[4-5])",
"view": "conference/schedule/schedule",
"title": "Conference Schedule",
"viewportWidth": "device-width"
},
{
"name": "search",
"pattern": "^/search/:projects?$/?$",
"routeAlias": "^/search",
"view": "search/search",
"title": "Search"
"name": "conference-details",
"pattern": "^/conference/:id/details/?$",
"routeAlias": "^/conference(?!/201[4-5])",
"view": "conference/details/details",
"title": "Event Details",
"viewportWidth": "device-width"
},
{
"name": "credits",
"pattern": "^/info/credits/?$",
"view": "credits/credits",
"title": "Credits"
"name": "developers",
"pattern": "^/developers/?$",
"view": "developers/developers",
"title": "Developers"
},
{
"name": "faq",
"pattern": "^/info/faq/?$",
"view": "faq/faq",
"title": "FAQ"
"name": "dmca",
"pattern": "^/DMCA/?$",
"view": "dmca/dmca",
"title": "DMCA"
},
{
"name": "educator-landing",
......@@ -79,24 +87,6 @@
"view": "teachers/faq/faq",
"title": "Teacher Accounts FAQ"
},
{
"name": "cards",
"pattern": "^/info/cards/?$",
"view": "cards/cards",
"title": "Cards"
},
{
"name": "communityblocks-interviews",
"pattern": "^/info/communityblocks-interviews/?$",
"view": "communityblocks-interviews/communityblocks-interviews",
"title": "Community Blocks Beta Tester Interviews"
},
{
"name": "jobs",
"pattern": "^/jobs/?$",
"view": "jobs/jobs",
"title": "Jobs"
},
{
"name": "teacherregistration",
"pattern": "^/educators/register$",
......@@ -111,67 +101,47 @@
"title": "Thank you for requesting a Scratch Teacher Account"
},
{
"name": "wedo2",
"pattern": "^/wedo/?$",
"view": "wedo2/wedo2",
"title": "LEGO WeDo 2.0"
},
{
"name": "conference-index",
"pattern": "^/conference/?$",
"routeAlias": "^/conference(?!/201[4-5])",
"view": "conference/index/index",
"title": "Scratch Conference",
"viewportWidth": "device-width"
},
{
"name": "conference-plan",
"pattern": "^/conference/plan/?$",
"routeAlias": "^/conference(?!/201[4-5])",
"view": "conference/plan/plan",
"title": "Plan Your Visit",
"viewportWidth": "device-width"
"name": "explore",
"pattern": "^/explore/:projects/:all/?$",
"routeAlias": "^/explore(?!/ajax)",
"view": "explore/explore",
"title": "Explore"
},
{
"name": "conference-expectations",
"pattern": "^/conference/expect/?$",
"routeAlias": "^/conference(?!/201[4-5])",
"view": "conference/expect/expect",
"title": "What to Expect",
"viewportWidth": "device-width"
"name": "hoc",
"pattern": "^/hoc/?$",
"view": "hoc/hoc",
"title": "Hour of Code"
},
{
"name": "conference-schedule",
"pattern": "^/conference/schedule/?$",
"routeAlias": "^/conference(?!/201[4-5])",
"view": "conference/schedule/schedule",
"title": "Conference Schedule",
"viewportWidth": "device-width"
"name": "cards",
"pattern": "^/info/cards/?$",
"view": "cards/cards",
"title": "Cards"
},
{
"name": "conference-details",
"pattern": "^/conference/:id/details/?$",
"routeAlias": "^/conference(?!/201[4-5])",
"view": "conference/details/details",
"title": "Event Details",
"viewportWidth": "device-width"
"name": "communityblocks-interviews",
"pattern": "^/info/communityblocks-interviews/?$",
"view": "communityblocks-interviews/communityblocks-interviews",
"title": "Community Blocks Beta Tester Interviews"
},
{
"name": "donate",
"pattern": "^/info/donate/?",
"redirect": "https://secure.donationpay.org/scratchfoundation/"
"name": "credits",
"pattern": "^/info/credits/?$",
"view": "credits/credits",
"title": "Credits"
},
{
"name": "dmca",
"pattern": "^/DMCA/?$",
"view": "dmca/dmca",
"title": "DMCA"
"name": "faq",
"pattern": "^/info/faq/?$",
"view": "faq/faq",
"title": "FAQ"
},
{
"name": "guidelines",
"pattern": "^/community_guidelines/?$",
"view": "guidelines/guidelines",
"title": "Scratch Community Guidelines"
"name": "jobs",
"pattern": "^/jobs/?$",
"view": "jobs/jobs",
"title": "Jobs"
},
{
"name": "privacypolicy",
......@@ -179,10 +149,46 @@
"view": "privacypolicy/privacypolicy",
"title": "Privacy Policy"
},
{
"name": "search",
"pattern": "^/search/:projects?$/?$",
"routeAlias": "^/search",
"view": "search/search",
"title": "Search"
},
{
"name": "terms",
"pattern": "^/terms_of_use/?$",
"view": "terms/terms",
"title": "Scratch Terms of Use"
},
{
"name": "wedo2",
"pattern": "^/wedo/?$",
"view": "wedo2/wedo2",
"title": "LEGO WeDo 2.0"
},
{
"name": "donate",
"pattern": "^/info/donate/?",
"redirect": "https://secure.donationpay.org/scratchfoundation/"
},
{
"name": "explore-redirect",
"pattern": "^/explore/?$",
"routeAlias": "^/explore(?!/ajax)",
"redirect": "/explore/projects/all"
},
{
"name": "explore-projects-redirect",
"pattern": "^/explore/projects/?$",
"routeAlias": "^/explore(?!/ajax)",
"redirect": "/explore/projects/all"
},
{
"name": "explore-studios-redirect",
"pattern": "^/explore/studios/?$",
"routeAlias": "^/explore(?!/ajax)",
"redirect": "/explore/studios/all"
}
]
var defaults = require('lodash.defaultsdeep');
var React = require('react');
var render = require('../../lib/render.jsx');
var api = require('../../lib/api');
var intl = require('../../lib/intl.jsx');
var Deck = require('../../components/deck/deck.jsx');
var Progression = require('../../components/progression/progression.jsx');
var Steps = require('../../components/registration/steps.jsx');
require('./studentregistration.scss');
var StudentRegistration = intl.injectIntl(React.createClass({
type: 'StudentRegistration',
getDefaultProps: function () {
return {
classroomId: null,
classroomToken: null
};
},
getInitialState: function () {
return {
formData: {},
registrationError: null,
step: 0,
waiting: false
};
},
advanceStep: function (formData) {
formData = formData || {};
this.setState({
step: this.state.step + 1,
formData: defaults({}, formData, this.state.formData)
});
},
componentDidMount: function () {
api({
uri: '/classrooms/' + this.props.classroomId + '/' + this.props.classroomToken
}, function (err, body, res) {
if (err) {
return this.setState({
registrationError: this.props.intl.formatMessage({
id: 'studentRegistration.classroomApiGeneralError',
defaultMessage: 'Sorry, we could not find the registration information for this class'
})
});
}
if (res.statusCode === 404) {
// TODO: Use react-router for this
return window.location = '/404';
}
this.setState({classroom: body});
}.bind(this));
},
register: function (formData) {
this.setState({waiting: true});
formData = defaults({}, formData || {}, this.state.formData);
api({
host: '',
uri: '/classes/register_new_student/',
method: 'post',
useCsrf: true,
formData: {
username: formData.user.username,
password: formData.user.password,
birth_month: formData.user.birth.month,
birth_year: formData.user.birth.year,
gender: (
formData.user.gender === 'other' ?
formData.user.genderOther :
formData.user.gender
),
country: formData.user.country,
is_robot: formData.user.isRobot,
classroom_id: this.props.classroomId,
classroom_token: this.props.classroomToken
}
}, function (err, res) {
this.setState({waiting: false});
if (err) return this.setState({registrationError: err});
if (res[0].success) return this.advanceStep(formData);
this.setState({registrationError: res[0].msg});
}.bind(this));
},
goToClass: function () {
window.location = '/classes/' + this.props.classroomId + '/';
},
render: function () {
return (
<Deck className="student-registration">
{this.state.registrationError ?
<Steps.RegistrationError {... this.state} />
:
<Progression {... this.state}>
<Steps.ClassInviteStep classroom={this.state.classroom}
messages={this.props.messages}
onNextStep={this.advanceStep}
waiting={this.state.waiting} />
<Steps.UsernameStep onNextStep={this.advanceStep}
waiting={this.state.waiting} />
<Steps.DemographicsStep onNextStep={this.register}
waiting={this.state.waiting} />
<Steps.ClassWelcomeStep classroom={this.state.classroom}
messages={this.props.messages}
onNextStep={this.goToClass}
waiting={this.state.waiting} />
</Progression>
}
</Deck>
);
}
}));
var [classroomId, _, classroomToken] = document.location.pathname.split('/')
.filter(function (p) {
if (p) return p;
})
.slice(-3);
var props = {classroomId, classroomToken};
render(<StudentRegistration {... props} />, document.getElementById('app'));
@import "../../colors";
@import "../../frameless";
@include responsive-layout (".student-registration", ".slide");
html,
body {
background-color: darken($ui-purple, 8%);
}
.teacher-registration {
background-color: $ui-purple;
}
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