Merge pull request #2570 from ClearlyClaire/glitch-soc/ports/onboarding-profile-setup
Add profile setup to onboarding in web UI
This commit is contained in:
commit
545c0041a0
22 changed files with 621 additions and 471 deletions
|
@ -714,6 +714,21 @@ export function fetchPinnedAccountsFail(error) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const updateAccount = ({ displayName, note, avatar, header, discoverable, indexable }) => (dispatch, getState) => {
|
||||||
|
const data = new FormData();
|
||||||
|
|
||||||
|
data.append('display_name', displayName);
|
||||||
|
data.append('note', note);
|
||||||
|
if (avatar) data.append('avatar', avatar);
|
||||||
|
if (header) data.append('header', header);
|
||||||
|
data.append('discoverable', discoverable);
|
||||||
|
data.append('indexable', indexable);
|
||||||
|
|
||||||
|
return api(getState).patch('/api/v1/accounts/update_credentials', data).then(response => {
|
||||||
|
dispatch(importFetchedAccount(response.data));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export function fetchPinnedAccountsSuggestions(q) {
|
export function fetchPinnedAccountsSuggestions(q) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
dispatch(fetchPinnedAccountsSuggestionsRequest());
|
dispatch(fetchPinnedAccountsSuggestionsRequest());
|
||||||
|
|
|
@ -20,6 +20,7 @@ export interface ApiAccountJSON {
|
||||||
bot: boolean;
|
bot: boolean;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
discoverable: boolean;
|
discoverable: boolean;
|
||||||
|
indexable: boolean;
|
||||||
display_name: string;
|
display_name: string;
|
||||||
emojis: ApiCustomEmojiJSON[];
|
emojis: ApiCustomEmojiJSON[];
|
||||||
fields: ApiAccountFieldJSON[];
|
fields: ApiAccountFieldJSON[];
|
||||||
|
|
|
@ -51,7 +51,7 @@ export default class Retention extends PureComponent {
|
||||||
let content;
|
let content;
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
content = <FormattedMessage id='loading_indicator.label' defaultMessage='Loading...' />;
|
content = <FormattedMessage id='loading_indicator.label' defaultMessage='Loading…' />;
|
||||||
} else {
|
} else {
|
||||||
content = (
|
content = (
|
||||||
<table className='retention__table'>
|
<table className='retention__table'>
|
||||||
|
|
|
@ -1,7 +1,23 @@
|
||||||
|
import { useIntl, defineMessages } from 'react-intl';
|
||||||
|
|
||||||
import { CircularProgress } from './circular_progress';
|
import { CircularProgress } from './circular_progress';
|
||||||
|
|
||||||
export const LoadingIndicator: React.FC = () => (
|
const messages = defineMessages({
|
||||||
<div className='loading-indicator'>
|
loading: { id: 'loading_indicator.label', defaultMessage: 'Loading…' },
|
||||||
<CircularProgress size={50} strokeWidth={6} />
|
});
|
||||||
</div>
|
|
||||||
);
|
export const LoadingIndicator: React.FC = () => {
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className='loading-indicator'
|
||||||
|
role='progressbar'
|
||||||
|
aria-busy
|
||||||
|
aria-live='polite'
|
||||||
|
aria-label={intl.formatMessage(messages.loading)}
|
||||||
|
>
|
||||||
|
<CircularProgress size={50} strokeWidth={6} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
@ -1,30 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { Fragment } from 'react';
|
|
||||||
|
|
||||||
import classNames from 'classnames';
|
|
||||||
|
|
||||||
import { ReactComponent as CheckIcon } from '@material-symbols/svg-600/outlined/done.svg';
|
|
||||||
|
|
||||||
import { Icon } from 'flavours/glitch/components/icon';
|
|
||||||
|
|
||||||
|
|
||||||
const ProgressIndicator = ({ steps, completed }) => (
|
|
||||||
<div className='onboarding__progress-indicator'>
|
|
||||||
{(new Array(steps)).fill().map((_, i) => (
|
|
||||||
<Fragment key={i}>
|
|
||||||
{i > 0 && <div className={classNames('onboarding__progress-indicator__line', { active: completed > i })} />}
|
|
||||||
|
|
||||||
<div className={classNames('onboarding__progress-indicator__step', { active: completed > i })}>
|
|
||||||
{completed > i && <Icon icon={CheckIcon} />}
|
|
||||||
</div>
|
|
||||||
</Fragment>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
ProgressIndicator.propTypes = {
|
|
||||||
steps: PropTypes.number.isRequired,
|
|
||||||
completed: PropTypes.number,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ProgressIndicator;
|
|
|
@ -1,11 +1,13 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import { ReactComponent as ArrowRightAltIcon } from '@material-symbols/svg-600/outlined/arrow_right_alt.svg';
|
import { ReactComponent as ArrowRightAltIcon } from '@material-symbols/svg-600/outlined/arrow_right_alt.svg';
|
||||||
import { ReactComponent as CheckIcon } from '@material-symbols/svg-600/outlined/done.svg';
|
import { ReactComponent as CheckIcon } from '@material-symbols/svg-600/outlined/done.svg';
|
||||||
|
|
||||||
import { Icon } from 'flavours/glitch/components/icon';
|
import { Icon } from 'flavours/glitch/components/icon';
|
||||||
|
|
||||||
const Step = ({ label, description, icon, iconComponent, completed, onClick, href }) => {
|
export const Step = ({ label, description, icon, iconComponent, completed, onClick, href, to }) => {
|
||||||
const content = (
|
const content = (
|
||||||
<>
|
<>
|
||||||
<div className='onboarding__steps__item__icon'>
|
<div className='onboarding__steps__item__icon'>
|
||||||
|
@ -29,6 +31,12 @@ const Step = ({ label, description, icon, iconComponent, completed, onClick, hre
|
||||||
{content}
|
{content}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
|
} else if (to) {
|
||||||
|
return (
|
||||||
|
<Link to={to} className='onboarding__steps__item'>
|
||||||
|
{content}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -45,7 +53,6 @@ Step.propTypes = {
|
||||||
iconComponent: PropTypes.func,
|
iconComponent: PropTypes.func,
|
||||||
completed: PropTypes.bool,
|
completed: PropTypes.bool,
|
||||||
href: PropTypes.string,
|
href: PropTypes.string,
|
||||||
|
to: PropTypes.string,
|
||||||
onClick: PropTypes.func,
|
onClick: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Step;
|
|
||||||
|
|
|
@ -1,79 +1,62 @@
|
||||||
import PropTypes from 'prop-types';
|
import { useEffect } from 'react';
|
||||||
import { PureComponent } from 'react';
|
|
||||||
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import { Link } from 'react-router-dom';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
|
||||||
|
|
||||||
import { fetchSuggestions } from 'flavours/glitch/actions/suggestions';
|
import { fetchSuggestions } from 'flavours/glitch/actions/suggestions';
|
||||||
import { markAsPartial } from 'flavours/glitch/actions/timelines';
|
import { markAsPartial } from 'flavours/glitch/actions/timelines';
|
||||||
import Column from 'flavours/glitch/components/column';
|
|
||||||
import { ColumnBackButton } from 'flavours/glitch/components/column_back_button';
|
import { ColumnBackButton } from 'flavours/glitch/components/column_back_button';
|
||||||
import { EmptyAccount } from 'flavours/glitch/components/empty_account';
|
import { EmptyAccount } from 'flavours/glitch/components/empty_account';
|
||||||
import Account from 'flavours/glitch/containers/account_container';
|
import Account from 'flavours/glitch/containers/account_container';
|
||||||
|
import { useAppSelector } from 'flavours/glitch/store';
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
export const Follows = () => {
|
||||||
suggestions: state.getIn(['suggestions', 'items']),
|
const dispatch = useDispatch();
|
||||||
isLoading: state.getIn(['suggestions', 'isLoading']),
|
const isLoading = useAppSelector(state => state.getIn(['suggestions', 'isLoading']));
|
||||||
});
|
const suggestions = useAppSelector(state => state.getIn(['suggestions', 'items']));
|
||||||
|
|
||||||
class Follows extends PureComponent {
|
useEffect(() => {
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
onBack: PropTypes.func,
|
|
||||||
dispatch: PropTypes.func.isRequired,
|
|
||||||
suggestions: ImmutablePropTypes.list,
|
|
||||||
isLoading: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount () {
|
|
||||||
const { dispatch } = this.props;
|
|
||||||
dispatch(fetchSuggestions(true));
|
dispatch(fetchSuggestions(true));
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
dispatch(markAsPartial('home'));
|
||||||
|
};
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
let loadedContent;
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
loadedContent = (new Array(8)).fill().map((_, i) => <EmptyAccount key={i} />);
|
||||||
|
} else if (suggestions.isEmpty()) {
|
||||||
|
loadedContent = <div className='follow-recommendations__empty'><FormattedMessage id='onboarding.follows.empty' defaultMessage='Unfortunately, no results can be shown right now. You can try using search or browsing the explore page to find people to follow, or try again later.' /></div>;
|
||||||
|
} else {
|
||||||
|
loadedContent = suggestions.map(suggestion => <Account id={suggestion.get('account')} key={suggestion.get('account')} withBio />);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount () {
|
return (
|
||||||
const { dispatch } = this.props;
|
<>
|
||||||
dispatch(markAsPartial('home'));
|
<ColumnBackButton />
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
<div className='scrollable privacy-policy'>
|
||||||
const { onBack, isLoading, suggestions } = this.props;
|
<div className='column-title'>
|
||||||
|
<h3><FormattedMessage id='onboarding.follows.title' defaultMessage='Popular on Mastodon' /></h3>
|
||||||
let loadedContent;
|
<p><FormattedMessage id='onboarding.follows.lead' defaultMessage='You curate your own home feed. The more people you follow, the more active and interesting it will be. These profiles may be a good starting point—you can always unfollow them later!' /></p>
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
loadedContent = (new Array(8)).fill().map((_, i) => <EmptyAccount key={i} />);
|
|
||||||
} else if (suggestions.isEmpty()) {
|
|
||||||
loadedContent = <div className='follow-recommendations__empty'><FormattedMessage id='onboarding.follows.empty' defaultMessage='Unfortunately, no results can be shown right now. You can try using search or browsing the explore page to find people to follow, or try again later.' /></div>;
|
|
||||||
} else {
|
|
||||||
loadedContent = suggestions.map(suggestion => <Account id={suggestion.get('account')} key={suggestion.get('account')} withBio />);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Column>
|
|
||||||
<ColumnBackButton onClick={onBack} />
|
|
||||||
|
|
||||||
<div className='scrollable privacy-policy'>
|
|
||||||
<div className='column-title'>
|
|
||||||
<h3><FormattedMessage id='onboarding.follows.title' defaultMessage='Popular on Mastodon' /></h3>
|
|
||||||
<p><FormattedMessage id='onboarding.follows.lead' defaultMessage='You curate your own home feed. The more people you follow, the more active and interesting it will be. These profiles may be a good starting point—you can always unfollow them later!' /></p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='follow-recommendations'>
|
|
||||||
{loadedContent}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className='onboarding__lead'><FormattedMessage id='onboarding.tips.accounts_from_other_servers' defaultMessage='<strong>Did you know?</strong> Since Mastodon is decentralized, some profiles you come across will be hosted on servers other than yours. And yet you can interact with them seamlessly! Their server is in the second half of their username!' values={{ strong: chunks => <strong>{chunks}</strong> }} /></p>
|
|
||||||
|
|
||||||
<div className='onboarding__footer'>
|
|
||||||
<button className='link-button' onClick={onBack}><FormattedMessage id='onboarding.actions.back' defaultMessage='Take me back' /></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Column>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
<div className='follow-recommendations'>
|
||||||
|
{loadedContent}
|
||||||
|
</div>
|
||||||
|
|
||||||
export default connect(mapStateToProps)(Follows);
|
<p className='onboarding__lead'><FormattedMessage id='onboarding.tips.accounts_from_other_servers' defaultMessage='<strong>Did you know?</strong> Since Mastodon is decentralized, some profiles you come across will be hosted on servers other than yours. And yet you can interact with them seamlessly! Their server is in the second half of their username!' values={{ strong: chunks => <strong>{chunks}</strong> }} /></p>
|
||||||
|
|
||||||
|
<div className='onboarding__footer'>
|
||||||
|
<Link className='link-button' to='/start'><FormattedMessage id='onboarding.actions.back' defaultMessage='Take me back' /></Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
@ -1,153 +1,90 @@
|
||||||
import PropTypes from 'prop-types';
|
import { useCallback } from 'react';
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
|
import { FormattedMessage, useIntl, defineMessages } from 'react-intl';
|
||||||
|
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import { Link, withRouter } from 'react-router-dom';
|
import { Link, Switch, Route, useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import { ReactComponent as AccountCircleIcon } from '@material-symbols/svg-600/outlined/account_circle.svg';
|
import { ReactComponent as AccountCircleIcon } from '@material-symbols/svg-600/outlined/account_circle.svg';
|
||||||
import { ReactComponent as ArrowRightAltIcon } from '@material-symbols/svg-600/outlined/arrow_right_alt.svg';
|
import { ReactComponent as ArrowRightAltIcon } from '@material-symbols/svg-600/outlined/arrow_right_alt.svg';
|
||||||
import { ReactComponent as ContentCopyIcon } from '@material-symbols/svg-600/outlined/content_copy.svg';
|
import { ReactComponent as ContentCopyIcon } from '@material-symbols/svg-600/outlined/content_copy.svg';
|
||||||
import { ReactComponent as EditNoteIcon } from '@material-symbols/svg-600/outlined/edit_note.svg';
|
import { ReactComponent as EditNoteIcon } from '@material-symbols/svg-600/outlined/edit_note.svg';
|
||||||
import { ReactComponent as PersonAddIcon } from '@material-symbols/svg-600/outlined/person_add.svg';
|
import { ReactComponent as PersonAddIcon } from '@material-symbols/svg-600/outlined/person_add.svg';
|
||||||
import { debounce } from 'lodash';
|
|
||||||
|
|
||||||
import { fetchAccount } from 'flavours/glitch/actions/accounts';
|
|
||||||
import { focusCompose } from 'flavours/glitch/actions/compose';
|
import { focusCompose } from 'flavours/glitch/actions/compose';
|
||||||
import { closeOnboarding } from 'flavours/glitch/actions/onboarding';
|
|
||||||
import { Icon } from 'flavours/glitch/components/icon';
|
import { Icon } from 'flavours/glitch/components/icon';
|
||||||
import Column from 'flavours/glitch/features/ui/components/column';
|
import Column from 'flavours/glitch/features/ui/components/column';
|
||||||
import { me } from 'flavours/glitch/initial_state';
|
import { me } from 'flavours/glitch/initial_state';
|
||||||
import { makeGetAccount } from 'flavours/glitch/selectors';
|
import { useAppSelector } from 'flavours/glitch/store';
|
||||||
import { assetHost } from 'flavours/glitch/utils/config';
|
import { assetHost } from 'flavours/glitch/utils/config';
|
||||||
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
|
|
||||||
import illustration from 'mastodon/../images/elephant_ui_conversation.svg';
|
import illustration from 'mastodon/../images/elephant_ui_conversation.svg';
|
||||||
|
|
||||||
import Step from './components/step';
|
import { Step } from './components/step';
|
||||||
import Follows from './follows';
|
import { Follows } from './follows';
|
||||||
import Share from './share';
|
import { Profile } from './profile';
|
||||||
|
import { Share } from './share';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
template: { id: 'onboarding.compose.template', defaultMessage: 'Hello #Mastodon!' },
|
template: { id: 'onboarding.compose.template', defaultMessage: 'Hello #Mastodon!' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapStateToProps = () => {
|
const Onboarding = () => {
|
||||||
const getAccount = makeGetAccount();
|
const account = useAppSelector(state => state.getIn(['accounts', me]));
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const intl = useIntl();
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
return state => ({
|
const handleComposeClick = useCallback(() => {
|
||||||
account: getAccount(state, me),
|
dispatch(focusCompose(history, intl.formatMessage(messages.template)));
|
||||||
});
|
}, [dispatch, intl, history]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Column>
|
||||||
|
<Switch>
|
||||||
|
<Route path='/start' exact>
|
||||||
|
<div className='scrollable privacy-policy'>
|
||||||
|
<div className='column-title'>
|
||||||
|
<img src={illustration} alt='' className='onboarding__illustration' />
|
||||||
|
<h3><FormattedMessage id='onboarding.start.title' defaultMessage="You've made it!" /></h3>
|
||||||
|
<p><FormattedMessage id='onboarding.start.lead' defaultMessage="Your new Mastodon account is ready to go. Here's how you can make the most of it:" /></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='onboarding__steps'>
|
||||||
|
<Step to='/start/profile' completed={(!account.get('avatar').endsWith('missing.png')) || (account.get('display_name').length > 0 && account.get('note').length > 0)} icon='address-book-o' iconComponent={AccountCircleIcon} label={<FormattedMessage id='onboarding.steps.setup_profile.title' defaultMessage='Customize your profile' />} description={<FormattedMessage id='onboarding.steps.setup_profile.body' defaultMessage='Others are more likely to interact with you with a filled out profile.' />} />
|
||||||
|
<Step to='/start/follows' completed={(account.get('following_count') * 1) >= 1} icon='user-plus' iconComponent={PersonAddIcon} label={<FormattedMessage id='onboarding.steps.follow_people.title' defaultMessage='Find at least {count, plural, one {one person} other {# people}} to follow' values={{ count: 7 }} />} description={<FormattedMessage id='onboarding.steps.follow_people.body' defaultMessage="You curate your own home feed. Let's fill it with interesting people." />} />
|
||||||
|
<Step onClick={handleComposeClick} completed={(account.get('statuses_count') * 1) >= 1} icon='pencil-square-o' iconComponent={EditNoteIcon} label={<FormattedMessage id='onboarding.steps.publish_status.title' defaultMessage='Make your first post' />} description={<FormattedMessage id='onboarding.steps.publish_status.body' defaultMessage='Say hello to the world.' values={{ emoji: <img className='emojione' alt='🐘' src={`${assetHost}/emoji/1f418.svg`} /> }} />} />
|
||||||
|
<Step to='/start/share' icon='copy' iconComponent={ContentCopyIcon} label={<FormattedMessage id='onboarding.steps.share_profile.title' defaultMessage='Share your profile' />} description={<FormattedMessage id='onboarding.steps.share_profile.body' defaultMessage='Let your friends know how to find you on Mastodon!' />} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className='onboarding__lead'><FormattedMessage id='onboarding.start.skip' defaultMessage="Don't need help getting started?" /></p>
|
||||||
|
|
||||||
|
<div className='onboarding__links'>
|
||||||
|
<Link to='/explore' className='onboarding__link'>
|
||||||
|
<FormattedMessage id='onboarding.actions.go_to_explore' defaultMessage='Take me to trending' />
|
||||||
|
<Icon icon={ArrowRightAltIcon} />
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link to='/home' className='onboarding__link'>
|
||||||
|
<FormattedMessage id='onboarding.actions.go_to_home' defaultMessage='Take me to my home feed' />
|
||||||
|
<Icon icon={ArrowRightAltIcon} />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Route>
|
||||||
|
|
||||||
|
<Route path='/start/profile' component={Profile} />
|
||||||
|
<Route path='/start/follows' component={Follows} />
|
||||||
|
<Route path='/start/share' component={Share} />
|
||||||
|
</Switch>
|
||||||
|
|
||||||
|
<Helmet>
|
||||||
|
<meta name='robots' content='noindex' />
|
||||||
|
</Helmet>
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
class Onboarding extends ImmutablePureComponent {
|
export default Onboarding;
|
||||||
static propTypes = {
|
|
||||||
dispatch: PropTypes.func.isRequired,
|
|
||||||
account: ImmutablePropTypes.record,
|
|
||||||
...WithRouterPropTypes,
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
|
||||||
step: null,
|
|
||||||
profileClicked: false,
|
|
||||||
shareClicked: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
handleClose = () => {
|
|
||||||
const { dispatch, history } = this.props;
|
|
||||||
|
|
||||||
dispatch(closeOnboarding());
|
|
||||||
history.push('/home');
|
|
||||||
};
|
|
||||||
|
|
||||||
handleProfileClick = () => {
|
|
||||||
this.setState({ profileClicked: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
handleFollowClick = () => {
|
|
||||||
this.setState({ step: 'follows' });
|
|
||||||
};
|
|
||||||
|
|
||||||
handleComposeClick = () => {
|
|
||||||
const { dispatch, intl, history } = this.props;
|
|
||||||
|
|
||||||
dispatch(focusCompose(history, intl.formatMessage(messages.template)));
|
|
||||||
};
|
|
||||||
|
|
||||||
handleShareClick = () => {
|
|
||||||
this.setState({ step: 'share', shareClicked: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
handleBackClick = () => {
|
|
||||||
this.setState({ step: null });
|
|
||||||
};
|
|
||||||
|
|
||||||
handleWindowFocus = debounce(() => {
|
|
||||||
const { dispatch, account } = this.props;
|
|
||||||
dispatch(fetchAccount(account.get('id')));
|
|
||||||
}, 1000, { trailing: true });
|
|
||||||
|
|
||||||
componentDidMount () {
|
|
||||||
window.addEventListener('focus', this.handleWindowFocus, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount () {
|
|
||||||
window.removeEventListener('focus', this.handleWindowFocus);
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { account } = this.props;
|
|
||||||
const { step, shareClicked } = this.state;
|
|
||||||
|
|
||||||
switch(step) {
|
|
||||||
case 'follows':
|
|
||||||
return <Follows onBack={this.handleBackClick} />;
|
|
||||||
case 'share':
|
|
||||||
return <Share onBack={this.handleBackClick} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Column>
|
|
||||||
<div className='scrollable privacy-policy'>
|
|
||||||
<div className='column-title'>
|
|
||||||
<img src={illustration} alt='' className='onboarding__illustration' />
|
|
||||||
<h3><FormattedMessage id='onboarding.start.title' defaultMessage="You've made it!" /></h3>
|
|
||||||
<p><FormattedMessage id='onboarding.start.lead' defaultMessage="Your new Mastodon account is ready to go. Here's how you can make the most of it:" /></p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='onboarding__steps'>
|
|
||||||
<Step onClick={this.handleProfileClick} href='/settings/profile' completed={(!account.get('avatar').endsWith('missing.png')) || (account.get('display_name').length > 0 && account.get('note').length > 0)} icon='address-book-o' iconComponent={AccountCircleIcon} label={<FormattedMessage id='onboarding.steps.setup_profile.title' defaultMessage='Customize your profile' />} description={<FormattedMessage id='onboarding.steps.setup_profile.body' defaultMessage='Others are more likely to interact with you with a filled out profile.' />} />
|
|
||||||
<Step onClick={this.handleFollowClick} completed={(account.get('following_count') * 1) >= 7} icon='user-plus' iconComponent={PersonAddIcon} label={<FormattedMessage id='onboarding.steps.follow_people.title' defaultMessage='Find at least {count, plural, one {one person} other {# people}} to follow' values={{ count: 7 }} />} description={<FormattedMessage id='onboarding.steps.follow_people.body' defaultMessage="You curate your own home feed. Let's fill it with interesting people." />} />
|
|
||||||
<Step onClick={this.handleComposeClick} completed={(account.get('statuses_count') * 1) >= 1} icon='pencil-square-o' iconComponent={EditNoteIcon} label={<FormattedMessage id='onboarding.steps.publish_status.title' defaultMessage='Make your first post' />} description={<FormattedMessage id='onboarding.steps.publish_status.body' defaultMessage='Say hello to the world.' values={{ emoji: <img className='emojione' alt='🐘' src={`${assetHost}/emoji/1f418.svg`} /> }} />} />
|
|
||||||
<Step onClick={this.handleShareClick} completed={shareClicked} icon='copy' iconComponent={ContentCopyIcon} label={<FormattedMessage id='onboarding.steps.share_profile.title' defaultMessage='Share your profile' />} description={<FormattedMessage id='onboarding.steps.share_profile.body' defaultMessage='Let your friends know how to find you on Mastodon!' />} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className='onboarding__lead'><FormattedMessage id='onboarding.start.skip' defaultMessage="Don't need help getting started?" /></p>
|
|
||||||
|
|
||||||
<div className='onboarding__links'>
|
|
||||||
<Link to='/explore' className='onboarding__link'>
|
|
||||||
<FormattedMessage id='onboarding.actions.go_to_explore' defaultMessage='Take me to trending' />
|
|
||||||
<Icon icon={ArrowRightAltIcon} />
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<Link to='/home' className='onboarding__link'>
|
|
||||||
<FormattedMessage id='onboarding.actions.go_to_home' defaultMessage='Take me to my home feed' />
|
|
||||||
<Icon icon={ArrowRightAltIcon} />
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Helmet>
|
|
||||||
<meta name='robots' content='noindex' />
|
|
||||||
</Helmet>
|
|
||||||
</Column>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withRouter(connect(mapStateToProps)(injectIntl(Onboarding)));
|
|
||||||
|
|
162
app/javascript/flavours/glitch/features/onboarding/profile.jsx
Normal file
162
app/javascript/flavours/glitch/features/onboarding/profile.jsx
Normal file
|
@ -0,0 +1,162 @@
|
||||||
|
import { useState, useMemo, useCallback, createRef } from 'react';
|
||||||
|
|
||||||
|
import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
|
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
|
||||||
|
import { ReactComponent as AddPhotoAlternateIcon } from '@material-symbols/svg-600/outlined/add_photo_alternate.svg';
|
||||||
|
import { ReactComponent as EditIcon } from '@material-symbols/svg-600/outlined/edit.svg';
|
||||||
|
import Toggle from 'react-toggle';
|
||||||
|
|
||||||
|
import { updateAccount } from 'flavours/glitch/actions/accounts';
|
||||||
|
import { Button } from 'flavours/glitch/components/button';
|
||||||
|
import { ColumnBackButton } from 'flavours/glitch/components/column_back_button';
|
||||||
|
import { Icon } from 'flavours/glitch/components/icon';
|
||||||
|
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
|
||||||
|
import { me } from 'flavours/glitch/initial_state';
|
||||||
|
import { useAppSelector } from 'flavours/glitch/store';
|
||||||
|
import { unescapeHTML } from 'flavours/glitch/utils/html';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
uploadHeader: { id: 'onboarding.profile.upload_header', defaultMessage: 'Upload profile header' },
|
||||||
|
uploadAvatar: { id: 'onboarding.profile.upload_avatar', defaultMessage: 'Upload profile picture' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const nullIfMissing = path => path.endsWith('missing.png') ? null : path;
|
||||||
|
|
||||||
|
export const Profile = () => {
|
||||||
|
const account = useAppSelector(state => state.getIn(['accounts', me]));
|
||||||
|
const [displayName, setDisplayName] = useState(account.get('display_name'));
|
||||||
|
const [note, setNote] = useState(unescapeHTML(account.get('note')));
|
||||||
|
const [avatar, setAvatar] = useState(null);
|
||||||
|
const [header, setHeader] = useState(null);
|
||||||
|
const [discoverable, setDiscoverable] = useState(account.get('discoverable'));
|
||||||
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
const [errors, setErrors] = useState();
|
||||||
|
const avatarFileRef = createRef();
|
||||||
|
const headerFileRef = createRef();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const intl = useIntl();
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const handleDisplayNameChange = useCallback(e => {
|
||||||
|
setDisplayName(e.target.value);
|
||||||
|
}, [setDisplayName]);
|
||||||
|
|
||||||
|
const handleNoteChange = useCallback(e => {
|
||||||
|
setNote(e.target.value);
|
||||||
|
}, [setNote]);
|
||||||
|
|
||||||
|
const handleDiscoverableChange = useCallback(e => {
|
||||||
|
setDiscoverable(e.target.checked);
|
||||||
|
}, [setDiscoverable]);
|
||||||
|
|
||||||
|
const handleAvatarChange = useCallback(e => {
|
||||||
|
setAvatar(e.target?.files?.[0]);
|
||||||
|
}, [setAvatar]);
|
||||||
|
|
||||||
|
const handleHeaderChange = useCallback(e => {
|
||||||
|
setHeader(e.target?.files?.[0]);
|
||||||
|
}, [setHeader]);
|
||||||
|
|
||||||
|
const avatarPreview = useMemo(() => avatar ? URL.createObjectURL(avatar) : nullIfMissing(account.get('avatar')), [avatar, account]);
|
||||||
|
const headerPreview = useMemo(() => header ? URL.createObjectURL(header) : nullIfMissing(account.get('header')), [header, account]);
|
||||||
|
|
||||||
|
const handleSubmit = useCallback(() => {
|
||||||
|
setIsSaving(true);
|
||||||
|
|
||||||
|
dispatch(updateAccount({
|
||||||
|
displayName,
|
||||||
|
note,
|
||||||
|
avatar,
|
||||||
|
header,
|
||||||
|
discoverable,
|
||||||
|
indexable: discoverable,
|
||||||
|
})).then(() => history.push('/start/follows')).catch(err => {
|
||||||
|
setIsSaving(false);
|
||||||
|
setErrors(err.response.data.details);
|
||||||
|
});
|
||||||
|
}, [dispatch, displayName, note, avatar, header, discoverable, history]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ColumnBackButton />
|
||||||
|
|
||||||
|
<div className='scrollable privacy-policy'>
|
||||||
|
<div className='column-title'>
|
||||||
|
<h3><FormattedMessage id='onboarding.profile.title' defaultMessage='Profile setup' /></h3>
|
||||||
|
<p><FormattedMessage id='onboarding.profile.lead' defaultMessage='You can always complete this later in the settings, where even more customization options are available.' /></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='simple_form'>
|
||||||
|
<div className='onboarding__profile'>
|
||||||
|
<label className={classNames('app-form__header-input', { selected: !!headerPreview, invalid: !!errors?.header })} title={intl.formatMessage(messages.uploadHeader)}>
|
||||||
|
<input
|
||||||
|
type='file'
|
||||||
|
hidden
|
||||||
|
ref={headerFileRef}
|
||||||
|
accept='image/*'
|
||||||
|
onChange={handleHeaderChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{headerPreview && <img src={headerPreview} alt='' />}
|
||||||
|
|
||||||
|
<Icon icon={headerPreview ? EditIcon : AddPhotoAlternateIcon} />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label className={classNames('app-form__avatar-input', { selected: !!avatarPreview, invalid: !!errors?.avatar })} title={intl.formatMessage(messages.uploadAvatar)}>
|
||||||
|
<input
|
||||||
|
type='file'
|
||||||
|
hidden
|
||||||
|
ref={avatarFileRef}
|
||||||
|
accept='image/*'
|
||||||
|
onChange={handleAvatarChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{avatarPreview && <img src={avatarPreview} alt='' />}
|
||||||
|
|
||||||
|
<Icon icon={avatarPreview ? EditIcon : AddPhotoAlternateIcon} />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={classNames('input with_block_label', { field_with_errors: !!errors?.display_name })}>
|
||||||
|
<label htmlFor='display_name'><FormattedMessage id='onboarding.profile.display_name' defaultMessage='Display name' /></label>
|
||||||
|
<span className='hint'><FormattedMessage id='onboarding.profile.display_name_hint' defaultMessage='Your full name or your fun name…' /></span>
|
||||||
|
<div className='label_input'>
|
||||||
|
<input id='display_name' type='text' value={displayName} onChange={handleDisplayNameChange} maxLength={30} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={classNames('input with_block_label', { field_with_errors: !!errors?.note })}>
|
||||||
|
<label htmlFor='note'><FormattedMessage id='onboarding.profile.note' defaultMessage='Bio' /></label>
|
||||||
|
<span className='hint'><FormattedMessage id='onboarding.profile.note_hint' defaultMessage='You can @mention other people or #hashtags…' /></span>
|
||||||
|
<div className='label_input'>
|
||||||
|
<textarea id='note' value={note} onChange={handleNoteChange} maxLength={500} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label className='app-form__toggle'>
|
||||||
|
<div className='app-form__toggle__label'>
|
||||||
|
<strong><FormattedMessage id='onboarding.profile.discoverable' defaultMessage='Make my profile discoverable' /></strong> <span className='recommended'><FormattedMessage id='recommended' defaultMessage='Recommended' /></span>
|
||||||
|
<span className='hint'><FormattedMessage id='onboarding.profile.discoverable_hint' defaultMessage='When you opt in to discoverability on Mastodon, your posts may appear in search results and trending, and your profile may be suggested to people with similar interests to you.' /></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='app-form__toggle__toggle'>
|
||||||
|
<div>
|
||||||
|
<Toggle checked={discoverable} onChange={handleDiscoverableChange} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='onboarding__footer'>
|
||||||
|
<Button block onClick={handleSubmit} disabled={isSaving}>{isSaving ? <LoadingIndicator /> : <FormattedMessage id='onboarding.profile.save_and_continue' defaultMessage='Save and continue' />}</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,31 +1,25 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { PureComponent } from 'react';
|
import { PureComponent } from 'react';
|
||||||
|
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import { ReactComponent as ArrowRightAltIcon } from '@material-symbols/svg-600/outlined/arrow_right_alt.svg';
|
import { ReactComponent as ArrowRightAltIcon } from '@material-symbols/svg-600/outlined/arrow_right_alt.svg';
|
||||||
import { ReactComponent as ContentCopyIcon } from '@material-symbols/svg-600/outlined/content_copy.svg';
|
import { ReactComponent as ContentCopyIcon } from '@material-symbols/svg-600/outlined/content_copy.svg';
|
||||||
import SwipeableViews from 'react-swipeable-views';
|
import SwipeableViews from 'react-swipeable-views';
|
||||||
|
|
||||||
import Column from 'flavours/glitch/components/column';
|
|
||||||
import { ColumnBackButton } from 'flavours/glitch/components/column_back_button';
|
import { ColumnBackButton } from 'flavours/glitch/components/column_back_button';
|
||||||
import { Icon } from 'flavours/glitch/components/icon';
|
import { Icon } from 'flavours/glitch/components/icon';
|
||||||
import { me, domain } from 'flavours/glitch/initial_state';
|
import { me, domain } from 'flavours/glitch/initial_state';
|
||||||
|
import { useAppSelector } from 'flavours/glitch/store';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
shareableMessage: { id: 'onboarding.share.message', defaultMessage: 'I\'m {username} on #Mastodon! Come follow me at {url}' },
|
shareableMessage: { id: 'onboarding.share.message', defaultMessage: 'I\'m {username} on #Mastodon! Come follow me at {url}' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
|
||||||
account: state.getIn(['accounts', me]),
|
|
||||||
});
|
|
||||||
|
|
||||||
class CopyPasteText extends PureComponent {
|
class CopyPasteText extends PureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -141,59 +135,47 @@ class TipCarousel extends PureComponent {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class Share extends PureComponent {
|
export const Share = () => {
|
||||||
|
const account = useAppSelector(state => state.getIn(['accounts', me]));
|
||||||
|
const intl = useIntl();
|
||||||
|
const url = (new URL(`/@${account.get('username')}`, document.baseURI)).href;
|
||||||
|
|
||||||
static propTypes = {
|
return (
|
||||||
onBack: PropTypes.func,
|
<>
|
||||||
account: ImmutablePropTypes.record,
|
<ColumnBackButton />
|
||||||
intl: PropTypes.object,
|
|
||||||
};
|
|
||||||
|
|
||||||
render () {
|
<div className='scrollable privacy-policy'>
|
||||||
const { onBack, account, intl } = this.props;
|
<div className='column-title'>
|
||||||
|
<h3><FormattedMessage id='onboarding.share.title' defaultMessage='Share your profile' /></h3>
|
||||||
const url = (new URL(`/@${account.get('username')}`, document.baseURI)).href;
|
<p><FormattedMessage id='onboarding.share.lead' defaultMessage='Let people know how they can find you on Mastodon!' /></p>
|
||||||
|
|
||||||
return (
|
|
||||||
<Column>
|
|
||||||
<ColumnBackButton onClick={onBack} />
|
|
||||||
|
|
||||||
<div className='scrollable privacy-policy'>
|
|
||||||
<div className='column-title'>
|
|
||||||
<h3><FormattedMessage id='onboarding.share.title' defaultMessage='Share your profile' /></h3>
|
|
||||||
<p><FormattedMessage id='onboarding.share.lead' defaultMessage='Let people know how they can find you on Mastodon!' /></p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<CopyPasteText value={intl.formatMessage(messages.shareableMessage, { username: `@${account.get('username')}@${domain}`, url })} />
|
|
||||||
|
|
||||||
<TipCarousel>
|
|
||||||
<div><p className='onboarding__lead'><FormattedMessage id='onboarding.tips.verification' defaultMessage='<strong>Did you know?</strong> You can verify your account by putting a link to your Mastodon profile on your own website and adding the website to your profile. No fees or documents necessary!' values={{ strong: chunks => <strong>{chunks}</strong> }} /></p></div>
|
|
||||||
<div><p className='onboarding__lead'><FormattedMessage id='onboarding.tips.migration' defaultMessage='<strong>Did you know?</strong> If you feel like {domain} is not a great server choice for you in the future, you can move to another Mastodon server without losing your followers. You can even host your own server!' values={{ domain, strong: chunks => <strong>{chunks}</strong> }} /></p></div>
|
|
||||||
<div><p className='onboarding__lead'><FormattedMessage id='onboarding.tips.2fa' defaultMessage='<strong>Did you know?</strong> You can secure your account by setting up two-factor authentication in your account settings. It works with any TOTP app of your choice, no phone number necessary!' values={{ strong: chunks => <strong>{chunks}</strong> }} /></p></div>
|
|
||||||
</TipCarousel>
|
|
||||||
|
|
||||||
<p className='onboarding__lead'><FormattedMessage id='onboarding.share.next_steps' defaultMessage='Possible next steps:' /></p>
|
|
||||||
|
|
||||||
<div className='onboarding__links'>
|
|
||||||
<Link to='/home' className='onboarding__link'>
|
|
||||||
<FormattedMessage id='onboarding.actions.go_to_home' defaultMessage='Take me to my home feed' />
|
|
||||||
<Icon icon={ArrowRightAltIcon} />
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<Link to='/explore' className='onboarding__link'>
|
|
||||||
<FormattedMessage id='onboarding.actions.go_to_explore' defaultMessage='Take me to trending' />
|
|
||||||
<Icon icon={ArrowRightAltIcon} />
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='onboarding__footer'>
|
|
||||||
<button className='link-button' onClick={onBack}><FormattedMessage id='onboarding.action.back' defaultMessage='Take me back' /></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Column>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
<CopyPasteText value={intl.formatMessage(messages.shareableMessage, { username: `@${account.get('username')}@${domain}`, url })} />
|
||||||
|
|
||||||
export default connect(mapStateToProps)(injectIntl(Share));
|
<TipCarousel>
|
||||||
|
<div><p className='onboarding__lead'><FormattedMessage id='onboarding.tips.verification' defaultMessage='<strong>Did you know?</strong> You can verify your account by putting a link to your Mastodon profile on your own website and adding the website to your profile. No fees or documents necessary!' values={{ strong: chunks => <strong>{chunks}</strong> }} /></p></div>
|
||||||
|
<div><p className='onboarding__lead'><FormattedMessage id='onboarding.tips.migration' defaultMessage='<strong>Did you know?</strong> If you feel like {domain} is not a great server choice for you in the future, you can move to another Mastodon server without losing your followers. You can even host your own server!' values={{ domain, strong: chunks => <strong>{chunks}</strong> }} /></p></div>
|
||||||
|
<div><p className='onboarding__lead'><FormattedMessage id='onboarding.tips.2fa' defaultMessage='<strong>Did you know?</strong> You can secure your account by setting up two-factor authentication in your account settings. It works with any TOTP app of your choice, no phone number necessary!' values={{ strong: chunks => <strong>{chunks}</strong> }} /></p></div>
|
||||||
|
</TipCarousel>
|
||||||
|
|
||||||
|
<p className='onboarding__lead'><FormattedMessage id='onboarding.share.next_steps' defaultMessage='Possible next steps:' /></p>
|
||||||
|
|
||||||
|
<div className='onboarding__links'>
|
||||||
|
<Link to='/home' className='onboarding__link'>
|
||||||
|
<FormattedMessage id='onboarding.actions.go_to_home' defaultMessage='Take me to my home feed' />
|
||||||
|
<Icon icon={ArrowRightAltIcon} />
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link to='/explore' className='onboarding__link'>
|
||||||
|
<FormattedMessage id='onboarding.actions.go_to_explore' defaultMessage='Take me to trending' />
|
||||||
|
<Icon icon={ArrowRightAltIcon} />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='onboarding__footer'>
|
||||||
|
<Link className='link-button' to='/start'><FormattedMessage id='onboarding.action.back' defaultMessage='Take me back' /></Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
@ -219,7 +219,7 @@ class SwitchingColumnsArea extends PureComponent {
|
||||||
<WrappedRoute path='/bookmarks' component={BookmarkedStatuses} content={children} />
|
<WrappedRoute path='/bookmarks' component={BookmarkedStatuses} content={children} />
|
||||||
<WrappedRoute path='/pinned' component={PinnedStatuses} content={children} />
|
<WrappedRoute path='/pinned' component={PinnedStatuses} content={children} />
|
||||||
|
|
||||||
<WrappedRoute path='/start' exact component={Onboarding} content={children} />
|
<WrappedRoute path='/start' component={Onboarding} content={children} />
|
||||||
<WrappedRoute path='/directory' component={Directory} content={children} />
|
<WrappedRoute path='/directory' component={Directory} content={children} />
|
||||||
<WrappedRoute path={['/explore', '/search']} component={Explore} content={children} />
|
<WrappedRoute path={['/explore', '/search']} component={Explore} content={children} />
|
||||||
<WrappedRoute path={['/publish', '/statuses/new']} component={Compose} content={children} />
|
<WrappedRoute path={['/publish', '/statuses/new']} component={Compose} content={children} />
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
"about.fork_disclaimer": "Glitch-soc is free open source software forked from Mastodon.",
|
"about.fork_disclaimer": "Glitch-soc is free open source software forked from Mastodon.",
|
||||||
"account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
|
"account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
|
||||||
"account.follows": "Follows",
|
"account.follows": "Follows",
|
||||||
|
"account.follows_you": "Follows you",
|
||||||
"account.joined": "Joined {date}",
|
"account.joined": "Joined {date}",
|
||||||
"account.suspended_disclaimer_full": "This user has been suspended by a moderator.",
|
"account.suspended_disclaimer_full": "This user has been suspended by a moderator.",
|
||||||
"account.view_full_profile": "View full profile",
|
"account.view_full_profile": "View full profile",
|
||||||
|
|
|
@ -67,6 +67,7 @@ export const accountDefaultValues: AccountShape = {
|
||||||
bot: false,
|
bot: false,
|
||||||
created_at: '',
|
created_at: '',
|
||||||
discoverable: false,
|
discoverable: false,
|
||||||
|
indexable: false,
|
||||||
display_name: '',
|
display_name: '',
|
||||||
display_name_html: '',
|
display_name_html: '',
|
||||||
emojis: List<CustomEmoji>(),
|
emojis: List<CustomEmoji>(),
|
||||||
|
|
|
@ -161,13 +161,20 @@ body {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
&:focus {
|
||||||
|
border-radius: 4px;
|
||||||
|
outline: $ui-button-focus-outline;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus:not(:focus-visible) {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&:focus {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-holder {
|
.app-holder {
|
||||||
|
|
|
@ -245,6 +245,8 @@ $ui-header-height: 55px;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
border: 0;
|
||||||
|
border-left: 4px solid transparent;
|
||||||
|
|
||||||
&:hover,
|
&:hover,
|
||||||
&:focus,
|
&:focus,
|
||||||
|
@ -256,6 +258,11 @@ $ui-header-height: 55px;
|
||||||
outline: 0;
|
outline: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
border-color: $ui-button-focus-outline-color;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
&--transparent {
|
&--transparent {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: $ui-secondary-color;
|
color: $ui-secondary-color;
|
||||||
|
@ -339,7 +346,6 @@ $ui-header-height: 55px;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
outline: 0;
|
outline: 0;
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
& > button {
|
& > button {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -356,6 +362,10 @@ $ui-header-height: 55px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: $ui-button-icon-focus-outline;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
& > .column-header__back-button {
|
& > .column-header__back-button {
|
||||||
|
@ -420,10 +430,18 @@ $ui-header-height: 55px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
padding: 0 15px;
|
padding: 0 15px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-start-end-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: lighten($darker-text-color, 7%);
|
color: lighten($darker-text-color, 7%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: $ui-button-focus-outline;
|
||||||
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
color: $primary-text-color;
|
color: $primary-text-color;
|
||||||
background: lighten($ui-base-color, 8%);
|
background: lighten($ui-base-color, 8%);
|
||||||
|
@ -434,11 +452,6 @@ $ui-header-height: 55px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// glitch - added focus ring for keyboard navigation
|
|
||||||
&:focus {
|
|
||||||
text-shadow: 0 0 4px darken($ui-highlight-color, 5%);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled {
|
&:disabled {
|
||||||
color: $dark-text-color;
|
color: $dark-text-color;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
|
@ -898,7 +911,7 @@ $ui-header-height: 55px;
|
||||||
|
|
||||||
.column-title {
|
.column-title {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding-bottom: 40px;
|
padding-bottom: 32px;
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
|
@ -1083,58 +1096,6 @@ $ui-header-height: 55px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.onboarding__progress-indicator {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
position: sticky;
|
|
||||||
background: $ui-base-color;
|
|
||||||
|
|
||||||
@media screen and (width >= 600) {
|
|
||||||
padding: 0 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__line {
|
|
||||||
height: 4px;
|
|
||||||
flex: 1 1 auto;
|
|
||||||
background: lighten($ui-base-color, 4%);
|
|
||||||
}
|
|
||||||
|
|
||||||
&__step {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
background: lighten($ui-base-color, 4%);
|
|
||||||
border-radius: 50%;
|
|
||||||
color: $primary-text-color;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
width: 15px;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
background: $valid-value-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__step.active,
|
|
||||||
&__line.active {
|
|
||||||
background: $valid-value-color;
|
|
||||||
background-image: linear-gradient(
|
|
||||||
90deg,
|
|
||||||
$valid-value-color,
|
|
||||||
lighten($valid-value-color, 8%),
|
|
||||||
$valid-value-color
|
|
||||||
);
|
|
||||||
background-size: 200px 100%;
|
|
||||||
animation: skeleton 1.2s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.follow-recommendations {
|
.follow-recommendations {
|
||||||
background: darken($ui-base-color, 4%);
|
background: darken($ui-base-color, 4%);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
@ -1211,6 +1172,28 @@ $ui-header-height: 55px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.onboarding__profile {
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 40px + 20px;
|
||||||
|
|
||||||
|
.app-form__avatar-input {
|
||||||
|
border: 2px solid $ui-base-color;
|
||||||
|
position: absolute;
|
||||||
|
inset-inline-start: -2px;
|
||||||
|
bottom: -40px;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-form__header-input {
|
||||||
|
margin: 0 -20px;
|
||||||
|
border-radius: 0;
|
||||||
|
|
||||||
|
img {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.compose-form__highlightable {
|
.compose-form__highlightable {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
@ -526,7 +526,7 @@
|
||||||
|
|
||||||
.privacy-dropdown__dropdown {
|
.privacy-dropdown__dropdown {
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
|
box-shadow: var(--dropdown-shadow);
|
||||||
background: $simple-background-color;
|
background: $simple-background-color;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transform-origin: 50% 0;
|
transform-origin: 50% 0;
|
||||||
|
@ -581,7 +581,6 @@
|
||||||
column-gap: 5px;
|
column-gap: 5px;
|
||||||
|
|
||||||
.compose-form__publish-button-wrapper {
|
.compose-form__publish-button-wrapper {
|
||||||
overflow: hidden;
|
|
||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
|
|
||||||
button {
|
button {
|
||||||
|
@ -604,7 +603,7 @@
|
||||||
.language-dropdown {
|
.language-dropdown {
|
||||||
&__dropdown {
|
&__dropdown {
|
||||||
background: $simple-background-color;
|
background: $simple-background-color;
|
||||||
box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
|
box-shadow: var(--dropdown-shadow);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
|
|
@ -129,9 +129,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.navigation-bar__profile {
|
.navigation-bar__profile {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
margin-inline-start: 8px;
|
margin-inline-start: 8px;
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.drawer--results {
|
.drawer--results {
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
.emoji-picker-dropdown__menu {
|
.emoji-picker-dropdown__menu {
|
||||||
background: $simple-background-color;
|
background: $simple-background-color;
|
||||||
position: relative;
|
position: relative;
|
||||||
box-shadow: 4px 4px 6px rgba($base-shadow-color, 0.4);
|
box-shadow: var(--dropdown-shadow);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
@ -79,11 +79,6 @@
|
||||||
outline: 0;
|
outline: 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&:active,
|
|
||||||
&:focus {
|
|
||||||
outline: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
img {
|
||||||
filter: grayscale(100%);
|
filter: grayscale(100%);
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
|
@ -99,6 +94,13 @@
|
||||||
img {
|
img {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
filter: none;
|
filter: none;
|
||||||
|
border-radius: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
img {
|
||||||
|
outline: $ui-button-icon-focus-outline;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -77,6 +77,10 @@
|
||||||
background-color: $ui-button-focus-background-color;
|
background-color: $ui-button-focus-background-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: $ui-button-icon-focus-outline;
|
||||||
|
}
|
||||||
|
|
||||||
&--destructive {
|
&--destructive {
|
||||||
&:active,
|
&:active,
|
||||||
&:focus,
|
&:focus,
|
||||||
|
@ -200,12 +204,10 @@
|
||||||
&:focus {
|
&:focus {
|
||||||
color: lighten($action-button-color, 7%);
|
color: lighten($action-button-color, 7%);
|
||||||
background-color: rgba($action-button-color, 0.15);
|
background-color: rgba($action-button-color, 0.15);
|
||||||
transition: all 200ms ease-out;
|
|
||||||
transition-property: background-color, color;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus {
|
&:focus-visible {
|
||||||
background-color: rgba($action-button-color, 0.3);
|
outline: $ui-button-icon-focus-outline;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.disabled {
|
&.disabled {
|
||||||
|
@ -227,16 +229,6 @@
|
||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&::-moz-focus-inner {
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-moz-focus-inner,
|
|
||||||
&:focus,
|
|
||||||
&:active {
|
|
||||||
outline: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.inverted {
|
&.inverted {
|
||||||
color: $lighter-text-color;
|
color: $lighter-text-color;
|
||||||
|
|
||||||
|
@ -247,21 +239,28 @@
|
||||||
background-color: rgba($lighter-text-color, 0.15);
|
background-color: rgba($lighter-text-color, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus {
|
&:focus-visible {
|
||||||
background-color: rgba($lighter-text-color, 0.3);
|
outline: $ui-button-icon-focus-outline;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.disabled {
|
&.disabled {
|
||||||
color: lighten($lighter-text-color, 7%);
|
color: lighten($lighter-text-color, 7%);
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
|
color: $highlight-text-color;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:active,
|
||||||
|
&:focus {
|
||||||
color: $highlight-text-color;
|
color: $highlight-text-color;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
&.disabled {
|
&.disabled {
|
||||||
color: lighten($highlight-text-color, 13%);
|
color: lighten($highlight-text-color, 13%);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -301,21 +300,16 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 0 3px;
|
padding: 0 3px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
outline: 0;
|
|
||||||
transition: all 100ms ease-in;
|
|
||||||
transition-property: background-color, color;
|
|
||||||
|
|
||||||
&:hover,
|
&:hover,
|
||||||
&:active,
|
&:active,
|
||||||
&:focus {
|
&:focus {
|
||||||
color: darken($lighter-text-color, 7%);
|
color: darken($lighter-text-color, 7%);
|
||||||
background-color: rgba($lighter-text-color, 0.15);
|
background-color: rgba($lighter-text-color, 0.15);
|
||||||
transition: all 200ms ease-out;
|
|
||||||
transition-property: background-color, color;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
background-color: rgba($lighter-text-color, 0.3);
|
outline: $ui-button-icon-focus-outline;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.disabled {
|
&.disabled {
|
||||||
|
@ -326,16 +320,13 @@
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
color: $highlight-text-color;
|
color: $highlight-text-color;
|
||||||
}
|
|
||||||
|
|
||||||
&::-moz-focus-inner {
|
&:hover,
|
||||||
border: 0;
|
&:active,
|
||||||
}
|
&:focus {
|
||||||
|
color: $highlight-text-color;
|
||||||
&::-moz-focus-inner,
|
background-color: transparent;
|
||||||
&:focus,
|
}
|
||||||
&:active {
|
|
||||||
outline: 0 !important;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -580,7 +571,7 @@ body > [data-popper-placement] {
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
line-height: inherit;
|
line-height: inherit;
|
||||||
|
|
||||||
&:focus {
|
&:focus-visible {
|
||||||
outline: 1px dotted;
|
outline: 1px dotted;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -855,6 +846,7 @@ body > [data-popper-placement] {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border: 0;
|
border: 0;
|
||||||
|
border-radius: 10px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
-webkit-tap-highlight-color: rgba($base-overlay-background, 0);
|
-webkit-tap-highlight-color: rgba($base-overlay-background, 0);
|
||||||
|
@ -879,81 +871,41 @@ body > [data-popper-placement] {
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-toggle-track {
|
.react-toggle-track {
|
||||||
width: 50px;
|
width: 32px;
|
||||||
height: 24px;
|
height: 20px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border-radius: 30px;
|
border-radius: 10px;
|
||||||
background-color: $ui-base-color;
|
background-color: #626982;
|
||||||
transition: background-color 0.2s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-toggle:is(:hover, :focus-within):not(.react-toggle--disabled)
|
.react-toggle--focus {
|
||||||
.react-toggle-track {
|
outline: $ui-button-focus-outline;
|
||||||
background-color: darken($ui-base-color, 10%);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-toggle--checked .react-toggle-track {
|
.react-toggle--checked .react-toggle-track {
|
||||||
background-color: darken($ui-highlight-color, 2%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.react-toggle--checked:is(:hover, :focus-within):not(.react-toggle--disabled)
|
|
||||||
.react-toggle-track {
|
|
||||||
background-color: $ui-highlight-color;
|
background-color: $ui-highlight-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-toggle-track-check {
|
.react-toggle-track-check,
|
||||||
position: absolute;
|
|
||||||
width: 14px;
|
|
||||||
height: 10px;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
margin-top: auto;
|
|
||||||
margin-bottom: auto;
|
|
||||||
line-height: 0;
|
|
||||||
inset-inline-start: 8px;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.25s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.react-toggle--checked .react-toggle-track-check {
|
|
||||||
opacity: 1;
|
|
||||||
transition: opacity 0.25s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.react-toggle-track-x {
|
.react-toggle-track-x {
|
||||||
position: absolute;
|
display: none;
|
||||||
width: 10px;
|
|
||||||
height: 10px;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
margin-top: auto;
|
|
||||||
margin-bottom: auto;
|
|
||||||
line-height: 0;
|
|
||||||
inset-inline-end: 10px;
|
|
||||||
opacity: 1;
|
|
||||||
transition: opacity 0.25s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.react-toggle--checked .react-toggle-track-x {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-toggle-thumb {
|
.react-toggle-thumb {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 1px;
|
top: 2px;
|
||||||
inset-inline-start: 1px;
|
inset-inline-start: 2px;
|
||||||
width: 22px;
|
width: 16px;
|
||||||
height: 22px;
|
height: 16px;
|
||||||
border: 1px solid $ui-base-color;
|
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background-color: darken($simple-background-color, 2%);
|
background-color: $primary-text-color;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
transition: all 0.25s ease;
|
transition: all 0.25s ease;
|
||||||
transition-property: border-color, left;
|
transition-property: border-color, left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-toggle--checked .react-toggle-thumb {
|
.react-toggle--checked .react-toggle-thumb {
|
||||||
inset-inline-start: 27px;
|
inset-inline-start: 32px - 16px - 2px;
|
||||||
border-color: $ui-highlight-color;
|
border-color: $ui-highlight-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1239,6 +1191,17 @@ body > [data-popper-placement] {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button .loading-indicator {
|
||||||
|
position: static;
|
||||||
|
transform: none;
|
||||||
|
|
||||||
|
.circular-progress {
|
||||||
|
color: $primary-text-color;
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.circular-progress {
|
.circular-progress {
|
||||||
color: lighten($ui-base-color, 26%);
|
color: lighten($ui-base-color, 26%);
|
||||||
animation: 1.4s linear 0s infinite normal none running simple-rotate;
|
animation: 1.4s linear 0s infinite normal none running simple-rotate;
|
||||||
|
|
|
@ -697,12 +697,14 @@
|
||||||
&__toggle {
|
&__toggle {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 16px;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
& > span {
|
& > span {
|
||||||
font-size: 17px;
|
display: block;
|
||||||
|
font-size: 14px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
margin-inline-start: 10px;
|
line-height: 20px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,7 @@ code {
|
||||||
}
|
}
|
||||||
|
|
||||||
.input {
|
.input {
|
||||||
margin-bottom: 15px;
|
margin-bottom: 16px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
&.hidden {
|
&.hidden {
|
||||||
|
@ -267,12 +267,13 @@ code {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: $primary-text-color;
|
color: $primary-text-color;
|
||||||
display: block;
|
display: block;
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
padding-top: 5px;
|
line-height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hint {
|
.hint {
|
||||||
margin-bottom: 15px;
|
line-height: 16px;
|
||||||
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
|
@ -428,7 +429,8 @@ code {
|
||||||
input[type='datetime-local'],
|
input[type='datetime-local'],
|
||||||
textarea {
|
textarea {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 16px;
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
color: $primary-text-color;
|
color: $primary-text-color;
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -436,9 +438,9 @@ code {
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
resize: vertical;
|
resize: vertical;
|
||||||
background: darken($ui-base-color, 10%);
|
background: darken($ui-base-color, 10%);
|
||||||
border: 1px solid darken($ui-base-color, 14%);
|
border: 1px solid darken($ui-base-color, 10%);
|
||||||
border-radius: 4px;
|
border-radius: 8px;
|
||||||
padding: 10px;
|
padding: 10px 16px;
|
||||||
|
|
||||||
&::placeholder {
|
&::placeholder {
|
||||||
color: lighten($darker-text-color, 4%);
|
color: lighten($darker-text-color, 4%);
|
||||||
|
@ -452,14 +454,13 @@ code {
|
||||||
border-color: $valid-value-color;
|
border-color: $valid-value-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border-color: darken($ui-base-color, 20%);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active,
|
&:active,
|
||||||
&:focus {
|
&:focus {
|
||||||
border-color: $highlight-text-color;
|
border-color: $highlight-text-color;
|
||||||
background: darken($ui-base-color, 8%);
|
}
|
||||||
|
|
||||||
|
@media screen and (width <= 600px) {
|
||||||
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -525,12 +526,11 @@ code {
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background: $ui-button-background-color;
|
background: $ui-button-background-color;
|
||||||
color: $ui-button-color;
|
color: $ui-button-color;
|
||||||
font-size: 18px;
|
font-size: 15px;
|
||||||
line-height: inherit;
|
line-height: 22px;
|
||||||
height: auto;
|
height: auto;
|
||||||
padding: 10px;
|
padding: 7px 18px;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
text-transform: uppercase;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
@ -1222,3 +1222,115 @@ code {
|
||||||
background: $highlight-text-color;
|
background: $highlight-text-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app-form {
|
||||||
|
&__avatar-input,
|
||||||
|
&__header-input {
|
||||||
|
display: block;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--dropdown-background-color);
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
img {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 8px;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
position: absolute;
|
||||||
|
inset-inline-start: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
color: $darker-text-color;
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selected .icon {
|
||||||
|
color: $primary-text-color;
|
||||||
|
transform: none;
|
||||||
|
inset-inline-start: auto;
|
||||||
|
inset-inline-end: 8px;
|
||||||
|
top: auto;
|
||||||
|
bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.invalid img {
|
||||||
|
outline: 1px solid $error-value-color;
|
||||||
|
outline-offset: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.invalid::before {
|
||||||
|
display: block;
|
||||||
|
content: '';
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
background: rgba($error-value-color, 0.25);
|
||||||
|
z-index: 2;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--dropdown-border-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__avatar-input {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__header-input {
|
||||||
|
aspect-ratio: 580/193;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
color: $darker-text-color;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__label {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
|
||||||
|
strong {
|
||||||
|
color: $primary-text-color;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recommended {
|
||||||
|
position: absolute;
|
||||||
|
margin: 0 4px;
|
||||||
|
margin-top: -2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__toggle {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__toggle > div {
|
||||||
|
display: flex;
|
||||||
|
border-inline-start: 1px solid lighten($ui-base-color, 8%);
|
||||||
|
padding-inline-start: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ $red-600: #b7253d !default; // Deep Carmine
|
||||||
$red-500: #df405a !default; // Cerise
|
$red-500: #df405a !default; // Cerise
|
||||||
$blurple-600: #563acc; // Iris
|
$blurple-600: #563acc; // Iris
|
||||||
$blurple-500: #6364ff; // Brand purple
|
$blurple-500: #6364ff; // Brand purple
|
||||||
|
$blurple-400: #7477fd; // Medium slate blue
|
||||||
$blurple-300: #858afa; // Faded Blue
|
$blurple-300: #858afa; // Faded Blue
|
||||||
$grey-600: #4e4c5a; // Trout
|
$grey-600: #4e4c5a; // Trout
|
||||||
$grey-100: #dadaf3; // Topaz
|
$grey-100: #dadaf3; // Topaz
|
||||||
|
@ -42,6 +43,8 @@ $ui-highlight-color: $classic-highlight-color !default;
|
||||||
$ui-button-color: $white !default;
|
$ui-button-color: $white !default;
|
||||||
$ui-button-background-color: $blurple-500 !default;
|
$ui-button-background-color: $blurple-500 !default;
|
||||||
$ui-button-focus-background-color: $blurple-600 !default;
|
$ui-button-focus-background-color: $blurple-600 !default;
|
||||||
|
$ui-button-focus-outline-color: $blurple-400 !default;
|
||||||
|
$ui-button-focus-outline: solid 2px $ui-button-focus-outline-color !default;
|
||||||
|
|
||||||
$ui-button-secondary-color: $grey-100 !default;
|
$ui-button-secondary-color: $grey-100 !default;
|
||||||
$ui-button-secondary-border-color: $grey-100 !default;
|
$ui-button-secondary-border-color: $grey-100 !default;
|
||||||
|
@ -56,6 +59,9 @@ $ui-button-tertiary-focus-color: $white !default;
|
||||||
$ui-button-destructive-background-color: $red-500 !default;
|
$ui-button-destructive-background-color: $red-500 !default;
|
||||||
$ui-button-destructive-focus-background-color: $red-600 !default;
|
$ui-button-destructive-focus-background-color: $red-600 !default;
|
||||||
|
|
||||||
|
$ui-button-icon-focus-outline: $ui-button-focus-outline !default;
|
||||||
|
$ui-button-icon-hover-background-color: rgba(140, 141, 255, 40%) !default;
|
||||||
|
|
||||||
// Variables for texts
|
// Variables for texts
|
||||||
$primary-text-color: $white !default;
|
$primary-text-color: $white !default;
|
||||||
$darker-text-color: $ui-primary-color !default;
|
$darker-text-color: $ui-primary-color !default;
|
||||||
|
|
Loading…
Reference in a new issue