Add community guidelines modal to homepage after registration

This commit is contained in:
Kaloyan Manolov 2024-10-15 13:14:16 +03:00
parent ea3825b132
commit bdec4169eb
9 changed files with 232 additions and 79 deletions

View file

@ -1,6 +1,6 @@
import React, {useEffect} from 'react';
import {FormattedMessage} from 'react-intl';
import thumbnailUrl from '../../lib/user-thumbnail';
import thumbnailUrl from '../../lib/user-thumbnail.js';
import OnboardingNavigation from '../onboarding-navigation/onboarding-navigation.jsx';
import './community-guidelines.scss';
@ -60,16 +60,21 @@ export const communityGuidelines = [
}
];
const CommunityGuidelines = ({constructHeader = () => null, userId, currentPage, onNextPage, onBackPage}) => {
export const CommunityGuidelines = ({
constructHeader = () => null,
userId,
currentPage,
nextButtonText,
onNextPage,
onBackPage
}) => {
useEffect(() => {
communityGuidelines.forEach(guideline => {
new Image().src = `/images/onboarding/${guideline.image}`;
});
}, []);
console.log('==user id', userId);
const guideline = communityGuidelines[currentPage - 2];
const guideline = communityGuidelines[currentPage];
return (
<div className="onboarding col">
{constructHeader(guideline)}
@ -95,7 +100,7 @@ const CommunityGuidelines = ({constructHeader = () => null, userId, currentPage,
alt=""
src={`/images/onboarding/${guideline.image}`}
/>
{currentPage === 3 && <img
{currentPage === 1 && <img
className="security-avatar"
src={thumbnailUrl(userId, 100, 100)}
/>}
@ -106,6 +111,7 @@ const CommunityGuidelines = ({constructHeader = () => null, userId, currentPage,
<OnboardingNavigation
currentPage={currentPage}
totalDots={communityGuidelines.length}
nextButtonText={nextButtonText}
onNextPage={onNextPage}
onBackPage={onBackPage}
/>
@ -117,8 +123,7 @@ CommunityGuidelines.propTypes = {
currentPage: PropTypes.number,
userId: PropTypes.string,
constructHeader: PropTypes.func,
nextButtonText: PropTypes.string,
onNextPage: PropTypes.func,
onBackPage: PropTypes.func
};
export default CommunityGuidelines;

View file

@ -1,6 +1,11 @@
@import "../../colors";
@import "../../frameless";
.col{
display: flex;
flex-direction: column !important;
}
.onboarding {
display: flex;
flex-direction: row;
@ -14,7 +19,6 @@
margin-bottom: 20px;
}
// TODO: Can we remove?
button{
padding: .3em .9em;
}
@ -53,32 +57,6 @@
.image-inner-content{
position: relative;
}
// TODO: Can we remove?
.image-content-1{
display: flex;
flex: 4;
justify-content: center;
align-items: center;
@media #{$medium-and-smaller} {
flex: 12;
order: 1;
align-items: flex-end;
height: 250px;
}
img{
z-index: 100;
@media #{$medium-and-smaller} {
width: 300px;
height: 300px;
}
}
@media #{$medium-and-smaller} {
background-color: #E9F1FC;
}
}
.text-content{
flex: 8;

View file

@ -0,0 +1,50 @@
import React, {useCallback, useState, useEffect} from 'react';
import {CommunityGuidelines, communityGuidelines} from './community-guidelines.jsx';
import PropTypes from 'prop-types';
import {injectIntl} from 'react-intl';
const ReactModal = require('react-modal');
const CommunityGuidelinesModal = props => {
const [currentPage, setCurrentPage] = useState(0);
const onNextPage = useCallback(() => setCurrentPage(currentPage + 1), [currentPage]);
const onBackPage = useCallback(() => setCurrentPage(currentPage - 1), [currentPage]);
const onComplete = props.onComplete ?? (() => true);
useEffect(() => {
const body = document.querySelector('body');
body.style.overflow = props.isOpen ? 'hidden' : 'auto';
}, [props.isOpen]);
return (
<ReactModal
isOpen
style={{
content: {
inset: 0,
width: '100%',
height: '100%',
margin: 0,
padding: 0
},
overlay: {
zIndex: 1000
}
}}
>
<CommunityGuidelines
userId={props.userId}
currentPage={currentPage}
onNextPage={currentPage < communityGuidelines.length - 1 ? onNextPage : onComplete}
nextButtonText={currentPage === communityGuidelines.length - 1 ? 'I Understand' : null}
onBackPage={currentPage > 0 ? onBackPage : null}
/>
</ReactModal>);
};
CommunityGuidelinesModal.propTypes = {
userId: PropTypes.string,
onComplete: PropTypes.func,
isOpen: PropTypes.bool
};
export const IntlCommunityGuidelinesWrapper = injectIntl(CommunityGuidelinesModal);

View file

@ -4,8 +4,15 @@ import {FormattedMessage} from 'react-intl';
import PropTypes from 'prop-types';
import './onboarding-navigation.scss';
import classNames from 'classnames';
const OnboardingNavigation = ({currentPage, totalDots, onNextPage, onBackPage, nextButtonText}) => {
const OnboardingNavigation = ({
currentPage,
totalDots,
onNextPage,
onBackPage,
nextButtonText
}) => {
const dots = [];
useEffect(() => {
@ -13,31 +20,36 @@ const OnboardingNavigation = ({currentPage, totalDots, onNextPage, onBackPage, n
new Image().src = '/images/onboarding/left-arrow.svg';
}, []);
if (currentPage && totalDots){
if (currentPage >= 0 && totalDots){
for (let i = 0; i < totalDots; i++){
// First two pages don't have dots
dots.push(<div
key={`dot page-${currentPage} ${i}`}
className={`dot ${currentPage === i + 2 && 'active'}`}
className={`dot ${currentPage === i && 'active'}`}
/>);
}
}
return (
<div className="navigation">
<Button onClick={onBackPage}>
<img
className="left-arrow"
alt=""
src="/images/onboarding/left-arrow.svg"
/>
<span className="navText">
{<FormattedMessage
id={'becomeAScratcher.buttons.back'}
/>}
</span>
</Button>
{(currentPage && totalDots) &&
{
<Button
onClick={onBackPage}
className={classNames({
hidden: !onBackPage
})}
>
<img
className="left-arrow"
alt=""
src="/images/onboarding/left-arrow.svg"
/>
<span className="navText">
{<FormattedMessage
id={'becomeAScratcher.buttons.back'}
/>}
</span>
</Button> }
{(currentPage >= 0 && totalDots) &&
<div className="dotRow">
{dots}
</div>}

View file

@ -21,6 +21,10 @@
}
}
.hidden{
visibility: hidden;
}
.navigation{
display: flex;
width: 100%;
@ -57,4 +61,8 @@
display: none;
}
}
.buttonPlaceholder{
width: 20px,
}
}

View file

@ -479,5 +479,62 @@
"renameAccount.scratchsCommunityGuidelines": "Scratch's Community Guidelines",
"renameAccount.change": "Change",
"renameAccount.goToProfile": "Go to your profile",
"renameAccount.pastNotifications": "Here are your past admin notifications"
"renameAccount.pastNotifications": "Here are your past admin notifications",
"becomeAScratcher.buttons.back": "Back",
"becomeAScratcher.buttons.next": "Next",
"becomeAScratcher.buttons.communityGuidelines": "Community Guidelines",
"becomeAScratcher.buttons.getStarted": "Get Started",
"becomeAScratcher.buttons.finishLater": "Finish Later",
"becomeAScratcher.buttons.goBack": "Go Back",
"becomeAScratcher.buttons.iAgree": "I Agree",
"becomeAScratcher.buttons.takeMeBack": "Take me back to Scratch",
"becomeAScratcher.buttons.backToProfile": "Back to Profile Page",
"becomeAScratcher.buttons.finishLater": "Finish Later",
"becomeAScratcher.congratulations.header": "Congratulations, {username}! You have shown that you are ready to become a Scratcher.",
"becomeAScratcher.congratulations.body": "Scratch is a friendly and welcoming community for everyone, where people create, share, and learn together. We welcome people of all ages, races, ethnicities, religions, abilities, sexual orientations, and gender identities.",
"becomeAScratcher.toBeAScratcher.header": "What does it mean to be a Scratcher?",
"becomeAScratcher.toBeAScratcher.body": "You might notice on your profile page that you are currently a “New Scratcher”. Now that you have spent some time on Scratch, we invite you to become a “Scratcher”.",
"becomeAScratcher.toBeAScratcher.definition": "Scratchers have a bit more experience on Scratch and are excited to both contribute to the community and to make it a supportive and welcoming space for others.",
"becomeAScratcher.toBeAScratcher.canDo": "Here are some things Scratchers do:",
"becomeAScratcher.toBeAScratcher.createStudios": "Create studios",
"becomeAScratcher.toBeAScratcher.helpOut": "Help out in the community",
"becomeAScratcher.toBeAScratcher.communityGuidelines": "Next, we will take you through the community guidelines and explain what these are.",
"becomeAScratcher.guidelines.respectSection": "Become a Scratcher - Treat everyone with respect",
"becomeAScratcher.guidelines.respectHeader": "Scratchers treat everyone with respect.",
"becomeAScratcher.guidelines.respectBody": "Everyone on Scratch is encouraged to share things that excite them and are important to them—we hope that you find ways to celebrate your own identity on Scratch, and allow others to do the same.",
"becomeAScratcher.guidelines.safeSection": "Become a Scratcher - Be safe",
"becomeAScratcher.guidelines.safeHeader": "Scratchers are safe: we keep personal and contact information private.",
"becomeAScratcher.guidelines.safeBody": "This includes not sharing real last names, phone numbers, addresses, hometowns, school names, email addresses, usernames or links to social media sites, video chatting applications, or websites with private chat functionality.",
"becomeAScratcher.guidelines.feedbackSection": "Become a Scratcher - Give helpful feedback",
"becomeAScratcher.guidelines.feedbackHeader": "Scratchers give helpful feedback.",
"becomeAScratcher.guidelines.feedbackBody": "When commenting on a project, remember to say something you like about it, offer suggestions, and be kind, not critical.",
"becomeAScratcher.guidelines.remix1Section": "Become a Scratcher - Embrace remix culture",
"becomeAScratcher.guidelines.remix1Header": "Scratchers embrace remix culture.",
"becomeAScratcher.guidelines.remix1Body": "Remixing is when you build upon someone elses projects, code, ideas, images, or anything else they share on Scratch to make your own unique creation.",
"becomeAScratcher.guidelines.remix2Section": "Become a Scratcher - Embrace remix culture",
"becomeAScratcher.guidelines.remix2Header": "Remixing is a great way to collaborate and connect with other Scratchers.",
"becomeAScratcher.guidelines.remix2Body": "You are encouraged to use anything you find on Scratch in your own creations, as long as you provide credit to everyone whose work you used and make a meaningful change to it. ",
"becomeAScratcher.guidelines.remix3Section": "Become a Scratcher - Embrace remix culture",
"becomeAScratcher.guidelines.remix3Header": "Remixing means sharing with others.",
"becomeAScratcher.guidelines.remix3Body": "When you share something on Scratch, you are giving permission to all Scratchers to use your work in their creations, too.",
"becomeAScratcher.guidelines.honestSection": "Become a Scratcher - Be honest",
"becomeAScratcher.guidelines.honestHeader": "Scratchers are honest.",
"becomeAScratcher.guidelines.honestBody": "Its important to be honest and authentic when interacting with others on Scratch, and remember that there is a person behind every Scratch account.",
"becomeAScratcher.guidelines.friendlySection": "Become a Scratcher - Keep the site friendly",
"becomeAScratcher.guidelines.friendlyHeader": "Scratchers help keep the site friendly.",
"becomeAScratcher.guidelines.friendlyBody": "Its important to keep your creations and conversations friendly and appropriate for all ages. If you think something on Scratch is mean, insulting, too violent, or otherwise disruptive to the community, click “Report” to let us know about it.",
"becomeAScratcher.invitation.header": "{username}, we invite you to become a Scratcher.",
"becomeAScratcher.invitation.body": "Scratch is a friendly and welcoming community for everyone. If you agree to be respectful, be safe, give helpful feedback, embrace remix culture, be honest, and help keep the site friendly, click “I agree!”",
"becomeAScratcher.invitation.finishLater": "You get to decide if you want to become a Scratcher. If you do not want to be a Scratcher yet, just click “Finish Later” above.",
"registration.success.error": "Sorry, an unexpected error occurred.",
"becomeAScratcher.success.header": "Hooray! You are now officially a Scratcher.",
"becomeAScratcher.success.body": "Here are some links that might be helpful for you.",
"becomeAScratcher.success.communityGuidelines": "Community Guidelines",
"becomeAScratcher.success.createAProject": "Create a Project",
"becomeAScratcher.noInvitation.header": "Whoops! Looks like you havent received an invitation to become a Scratcher yet.",
"becomeAScratcher.noInvitation.body": "To become a Scratcher, you must be active on Scratch for a while, share several projects, and comment constructively in the community. After a few weeks, you will receive a notification inviting you to become a Scratcher. Scratch on!",
"becomeAScratcher.finishLater.header": "No worries, take your time!",
"becomeAScratcher.finishLater.body": "By leaving this page, you will not finish the process to become a Scratcher and will stay as a New Scratcher. If you change your mind later, you can always come back via your profile page.",
"becomeAScratcher.finishLater.clickBecomeAScratcher": "Just click on “★ Become a Scratcher!” below your username."
}

View file

@ -15,7 +15,9 @@ const Types = keyMirror({
TOGGLE_LOGIN_OPEN: null,
SET_CANCELED_DELETION_OPEN: null,
SET_REGISTRATION_OPEN: null,
HANDLE_REGISTRATION_REQUESTED: null
HANDLE_REGISTRATION_REQUESTED: null,
HANDLE_REGISTRATION_COMPLETED: null,
REVIEW_COMMUNITY_GUIDELINES: null
});
module.exports.getInitialState = () => ({
@ -25,6 +27,7 @@ module.exports.getInitialState = () => ({
loginError: null,
loginOpen: false,
registrationOpen: false,
shouldReviewCommunityGuidelines: false,
searchTerm: ''
});
@ -56,6 +59,10 @@ module.exports.navigationReducer = (state, action) => {
return state;
}
return defaults({registrationOpen: true}, state);
case Types.HANDLE_REGISTRATION_COMPLETED:
return defaults({shouldReviewCommunityGuidelines: true}, state);
case Types.REVIEW_COMMUNITY_GUIDELINES:
return defaults({shouldReviewCommunityGuidelines: false}, state);
default:
return state;
}
@ -103,6 +110,14 @@ module.exports.handleRegistrationRequested = () => ({
type: Types.HANDLE_REGISTRATION_REQUESTED
});
module.exports.handleRegistrationCompleted = () => ({
type: Types.HANDLE_REGISTRATION_COMPLETED
});
module.exports.reviewCommunityGuidelines = () => ({
type: Types.REVIEW_COMMUNITY_GUIDELINES
});
module.exports.handleCompleteRegistration = createProject => (dispatch => {
if (createProject) {
// TODO: Ideally this would take you to the editor with the getting started
@ -114,6 +129,7 @@ module.exports.handleCompleteRegistration = createProject => (dispatch => {
dispatch(module.exports.setRegistrationOpen(false))
);
}
dispatch(module.exports.handleRegistrationCompleted());
});
module.exports.handleLogIn = (formData, callback) => (dispatch => {

View file

@ -15,7 +15,10 @@ import Modal from '../../components/modal/base/modal.jsx';
import NotAvailable from '../../components/not-available/not-available.jsx';
import WarningBanner from '../../components/title-banner/warning-banner.jsx';
import OnboardingNavigation from '../../components/onboarding-navigation/onboarding-navigation.jsx';
import CommunityGuidelines, {communityGuidelines} from '../../components/community-guidelines/community-guidelines.jsx';
import {
CommunityGuidelines,
communityGuidelines
} from '../../components/community-guidelines/community-guidelines.jsx';
import './become-a-scratcher.scss';
@ -174,25 +177,25 @@ const BecomeAScratcher = ({user, invitedScratcher, scratcher, sessionStatus}) =>
return (<NotAvailable />);
}
// New scratcher who is not invited
if (!invitedScratcher && !scratcher){
return (<div className="no-invitation">
<img
className="profile-page-image"
src="/images/onboarding/invitation-illustration.svg"
/>
<h2>
<FormattedMessage
id={'becomeAScratcher.noInvitation.header'}
/>
</h2>
<div>
<FormattedMessage
id={'becomeAScratcher.noInvitation.body'}
/>
</div>
</div>);
}
// // New scratcher who is not invited
// if (!invitedScratcher && !scratcher){
// return (<div className="no-invitation">
// <img
// className="profile-page-image"
// src="/images/onboarding/invitation-illustration.svg"
// />
// <h2>
// <FormattedMessage
// id={'becomeAScratcher.noInvitation.header'}
// />
// </h2>
// <div>
// <FormattedMessage
// id={'becomeAScratcher.noInvitation.body'}
// />
// </div>
// </div>);
// }
// Invited Scratcher
if (currentPage === 0){
@ -289,7 +292,7 @@ const BecomeAScratcher = ({user, invitedScratcher, scratcher, sessionStatus}) =>
);
} else if (currentPage < 2 + communityGuidelines.length) {
return (<CommunityGuidelines
currentPage={currentPage}
currentPage={currentPage - 2}
userId={`${user.id}`}
constructHeader={constructHeader}
onNextPage={nextPage}

View file

@ -8,9 +8,14 @@ const log = require('../../lib/log');
const render = require('../../lib/render.jsx');
const sessionActions = require('../../redux/session.js');
const splashActions = require('../../redux/splash.js');
const navigationActions = require('../../redux/navigation.js');
const Page = require('../../components/page/www/page.jsx');
const SplashPresentation = require('./presentation.jsx');
const {injectIntl, intlShape} = require('react-intl');
const {
IntlCommunityGuidelinesWrapper
} = require('../../components/community-guidelines/community-guidelines-wrapper.jsx');
const SCRATCH_WEEK_START_TIME = 1621224000000; // 2021-05-17 00:00:00 -- No end time for now
// const HOC_START_TIME = 1638144000000; // 2021-11-29 00:00:00 GMT in ms
@ -29,6 +34,7 @@ class Splash extends React.Component {
'getNews',
'handleRefreshHomepageCache',
'getHomepageRefreshStatus',
'handleCommunityGuidelinesReview',
'handleCloseAdminPanel',
'handleCloseDonateBanner',
'handleOpenAdminPanel',
@ -137,6 +143,9 @@ class Splash extends React.Component {
if (!err) this.props.refreshSession();
});
}
handleCommunityGuidelinesReview () {
this.props.reviewCommunityGuidelines();
}
shouldShowWelcome () {
if (!this.props.user || !this.props.flags.show_welcome) return false;
return (
@ -191,6 +200,15 @@ class Splash extends React.Component {
const showWelcome = this.shouldShowWelcome();
const homepageRefreshStatus = this.getHomepageRefreshStatus();
const shouldReviewCommunityGuidelines = this.props.shouldReviewCommunityGuidelines;
if (shouldReviewCommunityGuidelines && this.props.user.id) {
return (<IntlCommunityGuidelinesWrapper
userId={`${this.props.user.id}`}
onComplete={this.handleCommunityGuidelinesReview}
/>);
}
return (
<SplashPresentation
activity={this.props.activity}
@ -249,9 +267,11 @@ Splash.propTypes = {
isEducator: PropTypes.bool,
loved: PropTypes.arrayOf(PropTypes.object).isRequired,
refreshSession: PropTypes.func.isRequired,
reviewCommunityGuidelines: PropTypes.func.isRequired,
sessionStatus: PropTypes.string,
setRows: PropTypes.func.isRequired,
shared: PropTypes.arrayOf(PropTypes.object).isRequired,
shouldReviewCommunityGuidelines: PropTypes.bool.isRequired,
studios: PropTypes.arrayOf(PropTypes.object).isRequired,
user: PropTypes.shape({
id: PropTypes.number,
@ -284,7 +304,8 @@ const mapStateToProps = state => ({
sessionStatus: state.session.status,
shared: state.splash.shared.rows,
studios: state.splash.studios.rows,
user: state.session.session.user
user: state.session.session.user,
shouldReviewCommunityGuidelines: state.navigation.shouldReviewCommunityGuidelines
});
const mapDispatchToProps = dispatch => ({
@ -308,19 +329,22 @@ const mapDispatchToProps = dispatch => ({
},
setRows: (type, rows) => {
dispatch(splashActions.setRows(type, rows));
},
reviewCommunityGuidelines: () => {
dispatch(navigationActions.reviewCommunityGuidelines());
}
});
const ConnectedSplash = connect(
const IntlConnectedSplash = injectIntl(connect(
mapStateToProps,
mapDispatchToProps
)(Splash);
)(Splash));
render(
<Page
showDonorRecognition
>
<ConnectedSplash />
<IntlConnectedSplash />
</Page>,
document.getElementById('app'),
{splash: splashActions.splashReducer}