diff --git a/app/soapbox/actions/admin.js b/app/soapbox/actions/admin.js
index cf3a5d0e7..4329aa670 100644
--- a/app/soapbox/actions/admin.js
+++ b/app/soapbox/actions/admin.js
@@ -8,6 +8,18 @@ export const ADMIN_REPORTS_FETCH_REQUEST = 'ADMIN_REPORTS_FETCH_REQUEST';
export const ADMIN_REPORTS_FETCH_SUCCESS = 'ADMIN_REPORTS_FETCH_SUCCESS';
export const ADMIN_REPORTS_FETCH_FAIL = 'ADMIN_REPORTS_FETCH_FAIL';
+export const ADMIN_USERS_FETCH_REQUEST = 'ADMIN_USERS_FETCH_REQUEST';
+export const ADMIN_USERS_FETCH_SUCCESS = 'ADMIN_USERS_FETCH_SUCCESS';
+export const ADMIN_USERS_FETCH_FAIL = 'ADMIN_USERS_FETCH_FAIL';
+
+export const ADMIN_USERS_DELETE_REQUEST = 'ADMIN_USERS_DELETE_REQUEST';
+export const ADMIN_USERS_DELETE_SUCCESS = 'ADMIN_USERS_DELETE_SUCCESS';
+export const ADMIN_USERS_DELETE_FAIL = 'ADMIN_USERS_DELETE_FAIL';
+
+export const ADMIN_USERS_APPROVE_REQUEST = 'ADMIN_USERS_APPROVE_REQUEST';
+export const ADMIN_USERS_APPROVE_SUCCESS = 'ADMIN_USERS_APPROVE_SUCCESS';
+export const ADMIN_USERS_APPROVE_FAIL = 'ADMIN_USERS_APPROVE_FAIL';
+
export function updateAdminConfig(params) {
return (dispatch, getState) => {
dispatch({ type: ADMIN_CONFIG_UPDATE_REQUEST });
@@ -33,3 +45,42 @@ export function fetchReports(params) {
});
};
}
+
+export function fetchUsers(params) {
+ return (dispatch, getState) => {
+ dispatch({ type: ADMIN_USERS_FETCH_REQUEST, params });
+ return api(getState)
+ .get('/api/pleroma/admin/users', { params })
+ .then(({ data }) => {
+ dispatch({ type: ADMIN_USERS_FETCH_SUCCESS, data, params });
+ }).catch(error => {
+ dispatch({ type: ADMIN_USERS_FETCH_FAIL, error, params });
+ });
+ };
+}
+
+export function deleteUsers(nicknames) {
+ return (dispatch, getState) => {
+ dispatch({ type: ADMIN_USERS_DELETE_REQUEST, nicknames });
+ return api(getState)
+ .delete('/api/pleroma/admin/users', { data: { nicknames } })
+ .then(({ data: nicknames }) => {
+ dispatch({ type: ADMIN_USERS_DELETE_SUCCESS, nicknames });
+ }).catch(error => {
+ dispatch({ type: ADMIN_USERS_DELETE_FAIL, error, nicknames });
+ });
+ };
+}
+
+export function approveUsers(nicknames) {
+ return (dispatch, getState) => {
+ dispatch({ type: ADMIN_USERS_APPROVE_REQUEST, nicknames });
+ return api(getState)
+ .patch('/api/pleroma/admin/users/approve', { nicknames })
+ .then(({ data: { users } }) => {
+ dispatch({ type: ADMIN_USERS_APPROVE_SUCCESS, users, nicknames });
+ }).catch(error => {
+ dispatch({ type: ADMIN_USERS_APPROVE_FAIL, error, nicknames });
+ });
+ };
+}
diff --git a/app/soapbox/components/icon_with_badge.js b/app/soapbox/components/icon_with_badge.js
deleted file mode 100644
index 741401982..000000000
--- a/app/soapbox/components/icon_with_badge.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { shortNumberFormat } from 'soapbox/utils/numbers';
-
-const IconWithBadge = ({ id, count, className }) => {
- if (count < 1) return null;
-
- return (
-
- {count > 0 && {shortNumberFormat(count)}}
-
- );
-};
-
-IconWithBadge.propTypes = {
- id: PropTypes.string.isRequired,
- count: PropTypes.number.isRequired,
- className: PropTypes.string,
-};
-
-export default IconWithBadge;
diff --git a/app/soapbox/components/icon_with_counter.js b/app/soapbox/components/icon_with_counter.js
new file mode 100644
index 000000000..0daf608da
--- /dev/null
+++ b/app/soapbox/components/icon_with_counter.js
@@ -0,0 +1,23 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import Icon from 'soapbox/components/icon';
+import { shortNumberFormat } from 'soapbox/utils/numbers';
+
+const IconWithCounter = ({ icon, count, fixedWidth }) => {
+ return (
+
+
+ {count > 0 &&
+ {shortNumberFormat(count)}
+ }
+
+ );
+};
+
+IconWithCounter.propTypes = {
+ icon: PropTypes.string.isRequired,
+ count: PropTypes.number.isRequired,
+ fixedWidth: PropTypes.bool,
+};
+
+export default IconWithCounter;
diff --git a/app/soapbox/features/admin/awaiting_approval.js b/app/soapbox/features/admin/awaiting_approval.js
new file mode 100644
index 000000000..4486f4b27
--- /dev/null
+++ b/app/soapbox/features/admin/awaiting_approval.js
@@ -0,0 +1,83 @@
+import React from 'react';
+import { defineMessages, injectIntl } from 'react-intl';
+import { connect } from 'react-redux';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import PropTypes from 'prop-types';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import Column from '../ui/components/column';
+import IconButton from 'soapbox/components/icon_button';
+import ScrollableList from 'soapbox/components/scrollable_list';
+import { fetchUsers, deleteUsers, approveUsers } from 'soapbox/actions/admin';
+
+const messages = defineMessages({
+ heading: { id: 'column.admin.awaiting_approval', defaultMessage: 'Awaiting Approval' },
+ emptyMessage: { id: 'admin.awaiting_approval.empty_message', defaultMessage: 'There is nobody waiting for approval. When a new user signs up, you can review them here.' },
+});
+
+const mapStateToProps = state => {
+ const nicknames = state.getIn(['admin', 'awaitingApproval']);
+ return {
+ users: nicknames.toList().map(nickname => state.getIn(['admin', 'users', nickname])),
+ };
+};
+
+export default @connect(mapStateToProps)
+@injectIntl
+class AwaitingApproval extends ImmutablePureComponent {
+
+ static propTypes = {
+ intl: PropTypes.object.isRequired,
+ users: ImmutablePropTypes.list.isRequired,
+ };
+
+ state = {
+ isLoading: true,
+ }
+
+ componentDidMount() {
+ const { dispatch } = this.props;
+ const params = { page: 1, filters: 'local,need_approval' };
+ dispatch(fetchUsers(params))
+ .then(() => this.setState({ isLoading: false }))
+ .catch(() => {});
+ }
+
+ handleApprove = nickname => {
+ const { dispatch } = this.props;
+ return e => {
+ dispatch(approveUsers([nickname]));
+ };
+ }
+
+ handleReject = nickname => {
+ const { dispatch } = this.props;
+ return e => {
+ dispatch(deleteUsers([nickname]));
+ };
+ }
+
+ render() {
+ const { intl, users } = this.props;
+ const { isLoading } = this.state;
+
+ return (
+
+
+ {users.map((user, i) => (
+
+
+
@{user.get('nickname')}
+
{user.get('registration_reason')}
+
+
+
+
+
+
+ ))}
+
+
+ );
+ }
+
+}
diff --git a/app/soapbox/features/admin/components/admin_nav.js b/app/soapbox/features/admin/components/admin_nav.js
new file mode 100644
index 000000000..06aa236f5
--- /dev/null
+++ b/app/soapbox/features/admin/components/admin_nav.js
@@ -0,0 +1,98 @@
+import React from 'react';
+import { connect } from 'react-redux';
+import PropTypes from 'prop-types';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import Icon from 'soapbox/components/icon';
+import IconWithCounter from 'soapbox/components/icon_with_counter';
+import { NavLink } from 'react-router-dom';
+import { FormattedMessage } from 'react-intl';
+
+const mapStateToProps = (state, props) => ({
+ instance: state.get('instance'),
+ approvalCount: state.getIn(['admin', 'awaitingApproval']).count(),
+ reportsCount: state.getIn(['admin', 'open_report_count']),
+});
+
+export default @connect(mapStateToProps)
+class AdminNav extends React.PureComponent {
+
+ static propTypes = {
+ instance: ImmutablePropTypes.map.isRequired,
+ approvalCount: PropTypes.number,
+ reportsCount: PropTypes.number,
+ };
+
+ render() {
+ const { instance, approvalCount, reportsCount } = this.props;
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+ {instance.get('approval_required') && (
+
+
+
+
+ )}
+ {!instance.get('registrations') && (
+ {/*
+
+
+ */}
+ )}
+ {/*
+
+
+ */}
+
+
+ {/*
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
*/}
+ >
+ );
+ }
+
+}
diff --git a/app/soapbox/features/admin/index.js b/app/soapbox/features/admin/index.js
new file mode 100644
index 000000000..eb91ff2a5
--- /dev/null
+++ b/app/soapbox/features/admin/index.js
@@ -0,0 +1,81 @@
+import React from 'react';
+import { defineMessages, injectIntl, FormattedMessage, FormattedNumber } from 'react-intl';
+import { connect } from 'react-redux';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import PropTypes from 'prop-types';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import Column from '../ui/components/column';
+import { parseVersion } from 'soapbox/utils/features';
+
+const messages = defineMessages({
+ heading: { id: 'column.admin.dashboard', defaultMessage: 'Dashboard' },
+});
+
+const mapStateToProps = (state, props) => ({
+ instance: state.get('instance'),
+ openReportCount: state.getIn(['admin', 'open_report_count']),
+});
+
+export default @connect(mapStateToProps)
+@injectIntl
+class Dashboard extends ImmutablePureComponent {
+
+ static propTypes = {
+ intl: PropTypes.object.isRequired,
+ instance: ImmutablePropTypes.map.isRequired,
+ openReportCount: PropTypes.number,
+ };
+
+ render() {
+ const { intl, instance } = this.props;
+ const v = parseVersion(instance.get('version'));
+
+ return (
+
+
+
+
+
+ {/* TODO: Awaiting approval users count */}
+
+
+
+ );
+ }
+
+}
diff --git a/app/soapbox/features/ui/components/chats_counter_icon.js b/app/soapbox/features/ui/components/chats_counter_icon.js
deleted file mode 100644
index bb6d32907..000000000
--- a/app/soapbox/features/ui/components/chats_counter_icon.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import { connect } from 'react-redux';
-import IconWithBadge from 'soapbox/components/icon_with_badge';
-
-const mapStateToProps = state => ({
- count: state.get('chats').reduce((acc, curr) => acc + Math.min(curr.get('unread', 0), 1), 0),
- id: 'comment',
-});
-
-export default connect(mapStateToProps)(IconWithBadge);
diff --git a/app/soapbox/features/ui/components/follow_requests_nav_link.js b/app/soapbox/features/ui/components/follow_requests_nav_link.js
deleted file mode 100644
index 132637fa0..000000000
--- a/app/soapbox/features/ui/components/follow_requests_nav_link.js
+++ /dev/null
@@ -1,46 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { fetchFollowRequests } from 'soapbox/actions/accounts';
-import { connect } from 'react-redux';
-import { NavLink, withRouter } from 'react-router-dom';
-import IconWithBadge from 'soapbox/components/icon_with_badge';
-import { List as ImmutableList } from 'immutable';
-import { FormattedMessage } from 'react-intl';
-
-const mapStateToProps = state => {
- const me = state.get('me');
- return {
- locked: state.getIn(['accounts', me, 'locked']),
- count: state.getIn(['user_lists', 'follow_requests', 'items'], ImmutableList()).size,
- };
-};
-
-export default @withRouter
-@connect(mapStateToProps)
-class FollowRequestsNavLink extends React.Component {
-
- static propTypes = {
- dispatch: PropTypes.func.isRequired,
- locked: PropTypes.bool,
- count: PropTypes.number.isRequired,
- };
-
- componentDidMount() {
- const { dispatch, locked } = this.props;
-
- if (locked) {
- dispatch(fetchFollowRequests());
- }
- }
-
- render() {
- const { locked, count } = this.props;
-
- if (!locked || count === 0) {
- return null;
- }
-
- return ;
- }
-
-}
diff --git a/app/soapbox/features/ui/components/notifications_counter_icon.js b/app/soapbox/features/ui/components/notifications_counter_icon.js
deleted file mode 100644
index a2c3039cb..000000000
--- a/app/soapbox/features/ui/components/notifications_counter_icon.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import { connect } from 'react-redux';
-import IconWithBadge from 'soapbox/components/icon_with_badge';
-
-const mapStateToProps = state => ({
- count: state.getIn(['notifications', 'unread']),
- id: 'bell',
-});
-
-export default connect(mapStateToProps)(IconWithBadge);
diff --git a/app/soapbox/features/ui/components/reports_counter_icon.js b/app/soapbox/features/ui/components/reports_counter_icon.js
deleted file mode 100644
index 14a121887..000000000
--- a/app/soapbox/features/ui/components/reports_counter_icon.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import { connect } from 'react-redux';
-import IconWithBadge from 'soapbox/components/icon_with_badge';
-
-const mapStateToProps = state => ({
- count: state.getIn(['admin', 'open_report_count']),
- id: 'gavel',
-});
-
-export default connect(mapStateToProps)(IconWithBadge);
diff --git a/app/soapbox/features/ui/components/tabs_bar.js b/app/soapbox/features/ui/components/tabs_bar.js
index a87bf7429..013eef313 100644
--- a/app/soapbox/features/ui/components/tabs_bar.js
+++ b/app/soapbox/features/ui/components/tabs_bar.js
@@ -5,9 +5,7 @@ import { Link, NavLink, withRouter } from 'react-router-dom';
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
import { connect } from 'react-redux';
import classNames from 'classnames';
-import NotificationsCounterIcon from './notifications_counter_icon';
-import ReportsCounterIcon from './reports_counter_icon';
-import ChatsCounterIcon from './chats_counter_icon';
+import IconWithCounter from 'soapbox/components/icon_with_counter';
import SearchContainer from 'soapbox/features/compose/containers/search_container';
import Avatar from '../../../components/avatar';
import ActionBar from 'soapbox/features/compose/components/action_bar';
@@ -32,6 +30,9 @@ class TabsBar extends React.PureComponent {
onOpenSidebar: PropTypes.func.isRequired,
logo: PropTypes.string,
account: ImmutablePropTypes.map,
+ dashboardCount: PropTypes.number,
+ notificationCount: PropTypes.number,
+ chatsCount: PropTypes.number,
}
state = {
@@ -52,7 +53,7 @@ class TabsBar extends React.PureComponent {
}
getNavLinks() {
- const { intl: { formatMessage }, logo, account } = this.props;
+ const { intl: { formatMessage }, logo, account, dashboardCount, notificationCount, chatsCount } = this.props;
let links = [];
if (logo) {
links.push(
@@ -69,26 +70,23 @@ class TabsBar extends React.PureComponent {
if (account) {
links.push(
-
-
+
);
}
if (account) {
links.push(
-
-
+
);
}
if (account && isStaff(account)) {
links.push(
-
-
-
-
- );
+
+
+
+ );
}
links.push(
@@ -156,9 +154,14 @@ class TabsBar extends React.PureComponent {
const mapStateToProps = state => {
const me = state.get('me');
+ const reportsCount = state.getIn(['admin', 'open_report_count']);
+ const approvalCount = state.getIn(['admin', 'awaitingApproval']).count();
return {
account: state.getIn(['accounts', me]),
logo: getSoapboxConfig(state).get('logo'),
+ notificationCount: state.getIn(['notifications', 'unread']),
+ chatsCount: state.get('chats').reduce((acc, curr) => acc + Math.min(curr.get('unread', 0), 1), 0),
+ dashboardCount: reportsCount + approvalCount,
};
};
diff --git a/app/soapbox/features/ui/index.js b/app/soapbox/features/ui/index.js
index 8b714fe7e..313f60607 100644
--- a/app/soapbox/features/ui/index.js
+++ b/app/soapbox/features/ui/index.js
@@ -16,7 +16,7 @@ import { debounce } from 'lodash';
import { uploadCompose, resetCompose } from '../../actions/compose';
import { expandHomeTimeline } from '../../actions/timelines';
import { expandNotifications } from '../../actions/notifications';
-import { fetchReports } from '../../actions/admin';
+import { fetchReports, fetchUsers } from '../../actions/admin';
import { fetchFilters } from '../../actions/filters';
import { fetchChats } from 'soapbox/actions/chats';
import { clearHeight } from '../../actions/height_cache';
@@ -39,6 +39,7 @@ import Icon from 'soapbox/components/icon';
import { isStaff } from 'soapbox/utils/accounts';
import ChatPanes from 'soapbox/features/chats/components/chat_panes';
import ProfileHoverCard from 'soapbox/components/profile_hover_card';
+import AdminNav from 'soapbox/features/admin/components/admin_nav';
import {
Status,
@@ -86,6 +87,8 @@ import {
ChatIndex,
ChatRoom,
ServerInfo,
+ Dashboard,
+ AwaitingApproval,
} from './util/async-components';
// Dummy import, to make sure that ends up in the application bundle.
@@ -154,6 +157,14 @@ const LAYOUT = {
,
],
},
+ ADMIN: {
+ LEFT: [
+ ,
+ ],
+ RIGHT: [
+ ,
+ ],
+ },
STATUS: {
TOP: null,
LEFT: null,
@@ -274,6 +285,9 @@ class SwitchingColumnsArea extends React.PureComponent {
+
+
+
@@ -448,8 +462,10 @@ class UI extends React.PureComponent {
this.props.dispatch(expandNotifications());
this.props.dispatch(fetchChats());
// this.props.dispatch(fetchGroups('member'));
- if (isStaff(account))
+ if (isStaff(account)) {
this.props.dispatch(fetchReports({ state: 'open' }));
+ this.props.dispatch(fetchUsers({ page: 1, filters: 'local,need_approval' }));
+ }
setTimeout(() => this.props.dispatch(fetchFilters()), 500);
}
diff --git a/app/soapbox/features/ui/util/async-components.js b/app/soapbox/features/ui/util/async-components.js
index 3cb9e5142..6e313b4d8 100644
--- a/app/soapbox/features/ui/util/async-components.js
+++ b/app/soapbox/features/ui/util/async-components.js
@@ -217,3 +217,11 @@ export function ChatRoom() {
export function ServerInfo() {
return import(/* webpackChunkName: "features/server_info" */'../../server_info');
}
+
+export function Dashboard() {
+ return import(/* webpackChunkName: "features/admin" */'../../admin');
+}
+
+export function AwaitingApproval() {
+ return import(/* webpackChunkName: "features/admin/awaiting_approval" */'../../admin/awaiting_approval');
+}
diff --git a/app/soapbox/reducers/__tests__/admin-test.js b/app/soapbox/reducers/__tests__/admin-test.js
index aa32ace8f..891b8cabe 100644
--- a/app/soapbox/reducers/__tests__/admin-test.js
+++ b/app/soapbox/reducers/__tests__/admin-test.js
@@ -1,11 +1,13 @@
import reducer from '../admin';
-import { fromJS } from 'immutable';
+import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
describe('admin reducer', () => {
it('should return the initial state', () => {
expect(reducer(undefined, {})).toEqual(fromJS({
reports: [],
open_report_count: 0,
+ users: ImmutableMap(),
+ awaitingApproval: ImmutableOrderedSet(),
}));
});
});
diff --git a/app/soapbox/reducers/admin.js b/app/soapbox/reducers/admin.js
index 2a41666f6..c7c6be507 100644
--- a/app/soapbox/reducers/admin.js
+++ b/app/soapbox/reducers/admin.js
@@ -1,11 +1,54 @@
-import { ADMIN_REPORTS_FETCH_SUCCESS } from '../actions/admin';
-import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
+import {
+ ADMIN_REPORTS_FETCH_SUCCESS,
+ ADMIN_USERS_FETCH_SUCCESS,
+ ADMIN_USERS_DELETE_REQUEST,
+ ADMIN_USERS_DELETE_SUCCESS,
+ ADMIN_USERS_APPROVE_REQUEST,
+ ADMIN_USERS_APPROVE_SUCCESS,
+} from '../actions/admin';
+import {
+ Map as ImmutableMap,
+ List as ImmutableList,
+ OrderedSet as ImmutableOrderedSet,
+ fromJS,
+} from 'immutable';
const initialState = ImmutableMap({
reports: ImmutableList(),
+ users: ImmutableMap(),
open_report_count: 0,
+ awaitingApproval: ImmutableOrderedSet(),
});
+function importUsers(state, users) {
+ return state.withMutations(state => {
+ users.forEach(user => {
+ if (user.approval_pending) {
+ state.update('awaitingApproval', orderedSet => orderedSet.add(user.nickname));
+ }
+ state.setIn(['users', user.nickname], fromJS(user));
+ });
+ });
+}
+
+function deleteUsers(state, nicknames) {
+ return state.withMutations(state => {
+ nicknames.forEach(nickname => {
+ state.update('awaitingApproval', orderedSet => orderedSet.delete(nickname));
+ state.deleteIn(['users', nickname]);
+ });
+ });
+}
+
+function approveUsers(state, users) {
+ return state.withMutations(state => {
+ users.forEach(user => {
+ state.update('awaitingApproval', orderedSet => orderedSet.delete(user.nickname));
+ state.setIn(['users', user.nickname], fromJS(user));
+ });
+ });
+}
+
export default function admin(state = initialState, action) {
switch(action.type) {
case ADMIN_REPORTS_FETCH_SUCCESS:
@@ -16,6 +59,15 @@ export default function admin(state = initialState, action) {
} else {
return state.set('reports', fromJS(action.data.reports));
}
+ case ADMIN_USERS_FETCH_SUCCESS:
+ return importUsers(state, action.data.users);
+ case ADMIN_USERS_DELETE_REQUEST:
+ case ADMIN_USERS_DELETE_SUCCESS:
+ return deleteUsers(state, action.nicknames);
+ case ADMIN_USERS_APPROVE_REQUEST:
+ return state.update('awaitingApproval', set => set.subtract(action.nicknames));
+ case ADMIN_USERS_APPROVE_SUCCESS:
+ return approveUsers(state, action.users);
default:
return state;
}
diff --git a/app/styles/application.scss b/app/styles/application.scss
index 3841d231e..91f5ec192 100644
--- a/app/styles/application.scss
+++ b/app/styles/application.scss
@@ -79,6 +79,7 @@
@import 'components/snackbar';
@import 'components/accordion';
@import 'components/server-info';
+@import 'components/admin';
// Holiday
@import 'holiday/halloween';
diff --git a/app/styles/components/admin.scss b/app/styles/components/admin.scss
new file mode 100644
index 000000000..2712016e2
--- /dev/null
+++ b/app/styles/components/admin.scss
@@ -0,0 +1,96 @@
+.dashcounters {
+ display: flex;
+ flex-wrap: wrap;
+ margin: 0 -5px 0;
+ padding: 20px;
+}
+
+.dashcounter {
+ box-sizing: border-box;
+ flex: 0 0 33.333%;
+ padding: 0 5px;
+ margin-bottom: 10px;
+
+ > a,
+ > div {
+ text-decoration: none;
+ color: inherit;
+ display: block;
+ padding: 20px;
+ background: var(--accent-color--faint);
+ border-radius: 4px;
+ transition: 0.2s;
+ }
+
+ > a:hover {
+ background: var(--accent-color--med);
+ transform: translateY(-2px);
+ }
+
+ &__num,
+ &__text {
+ text-align: center;
+ font-weight: 500;
+ font-size: 24px;
+ line-height: 30px;
+ color: var(--primary-text-color);
+ margin-bottom: 10px;
+ }
+
+ &__label {
+ font-size: 14px;
+ color: hsla(var(--primary-text-color_hsl), 0.6);
+ text-align: center;
+ font-weight: 500;
+ }
+}
+
+.dashwidgets {
+ display: flex;
+ flex-wrap: wrap;
+ margin: 0 -5px;
+ padding: 0 20px 20px 20px;
+}
+
+.dashwidget {
+ flex: 1;
+ margin-bottom: 20px;
+ padding: 0 5px;
+
+ h4 {
+ text-transform: uppercase;
+ font-size: 13px;
+ font-weight: 700;
+ color: hsla(var(--primary-text-color_hsl), 0.6);
+ padding-bottom: 8px;
+ margin-bottom: 8px;
+ border-bottom: 1px solid var(--accent-color--med);
+ }
+}
+
+.unapproved-account {
+ padding: 15px 20px;
+ font-size: 14px;
+ display: flex;
+
+ &__nickname {
+ font-weight: bold;
+ }
+
+ &__reason {
+ padding: 5px 0 5px 15px;
+ border-left: 3px solid hsla(var(--primary-text-color_hsl), 0.4);
+ color: var(--primary-text-color--faint);
+ }
+
+ &__actions {
+ margin-left: auto;
+ padding-left: 20px;
+ display: flex;
+ flex-wrap: nowrap;
+ }
+}
+
+.slist .item-list article:nth-child(2n-1) .unapproved-account {
+ background-color: hsla(var(--accent-color_hsl), 0.07);
+}
diff --git a/app/styles/components/promo-panel.scss b/app/styles/components/promo-panel.scss
index e2030cee6..78625e338 100644
--- a/app/styles/components/promo-panel.scss
+++ b/app/styles/components/promo-panel.scss
@@ -28,7 +28,8 @@
}
}
- &__icon {
+ &__icon,
+ .icon-with-counter {
margin-right: 12px;
}
}
diff --git a/app/styles/components/tabs-bar.scss b/app/styles/components/tabs-bar.scss
index 8553b3df6..326f688dd 100644
--- a/app/styles/components/tabs-bar.scss
+++ b/app/styles/components/tabs-bar.scss
@@ -213,6 +213,12 @@
}
}
+ .icon-with-counter__counter {
+ @media screen and (min-width: 895px) {
+ left: 5px;
+ }
+ }
+
&.optional {
display: none;
@media screen and (max-width: $nav-breakpoint-2) {
diff --git a/app/styles/ui.scss b/app/styles/ui.scss
index a2ca21a06..3f6637b49 100644
--- a/app/styles/ui.scss
+++ b/app/styles/ui.scss
@@ -699,3 +699,25 @@
text-align: center;
}
}
+
+.icon-with-counter {
+ position: relative;
+ display: inline;
+
+ &__counter {
+ @include font-montserrat;
+ @include font-size(14);
+ @include line-height(14);
+ position: absolute;
+ box-sizing: border-box;
+ left: 8px;
+ top: -12px;
+ min-width: 16px;
+ height: 16px;
+ padding: 1px 3px 0;
+ border-radius: 8px;
+ text-align: center;
+ color: #fff;
+ background: var(--accent-color);
+ }
+}