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..95a50e38e
--- /dev/null
+++ b/app/soapbox/features/admin/components/admin_nav.js
@@ -0,0 +1,22 @@
+import React from 'react';
+import Icon from 'soapbox/components/icon';
+import { NavLink } from 'react-router-dom';
+import { FormattedMessage } from 'react-intl';
+
+export default
+class AdminNav extends React.PureComponent {
+
+ render() {
+ return (
+
+ );
+ }
+
+}
diff --git a/app/soapbox/features/admin/index.js b/app/soapbox/features/admin/index.js
new file mode 100644
index 000000000..96115b7b6
--- /dev/null
+++ b/app/soapbox/features/admin/index.js
@@ -0,0 +1,78 @@
+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'),
+});
+
+export default @connect(mapStateToProps)
+@injectIntl
+class Dashboard extends ImmutablePureComponent {
+
+ static propTypes = {
+ intl: PropTypes.object.isRequired,
+ instance: ImmutablePropTypes.map.isRequired,
+ };
+
+ render() {
+ const { intl, instance } = this.props;
+ const v = parseVersion(instance.get('version'));
+
+ return (
+
+
+
+
+ );
+ }
+
+}
diff --git a/app/soapbox/features/ui/index.js b/app/soapbox/features/ui/index.js
index 8b714fe7e..6d6080ef7 100644
--- a/app/soapbox/features/ui/index.js
+++ b/app/soapbox/features/ui/index.js
@@ -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,7 @@ import {
ChatIndex,
ChatRoom,
ServerInfo,
+ Dashboard,
} from './util/async-components';
// Dummy import, to make sure that ends up in the application bundle.
@@ -154,6 +156,14 @@ const LAYOUT = {
,
],
},
+ ADMIN: {
+ LEFT: [
+ ,
+ ],
+ RIGHT: [
+ ,
+ ],
+ },
STATUS: {
TOP: null,
LEFT: null,
@@ -274,6 +284,8 @@ class SwitchingColumnsArea extends React.PureComponent {
+
+
diff --git a/app/soapbox/features/ui/util/async-components.js b/app/soapbox/features/ui/util/async-components.js
index 3cb9e5142..3421f2d9e 100644
--- a/app/soapbox/features/ui/util/async-components.js
+++ b/app/soapbox/features/ui/util/async-components.js
@@ -217,3 +217,7 @@ export function ChatRoom() {
export function ServerInfo() {
return import(/* webpackChunkName: "features/server_info" */'../../server_info');
}
+
+export function Dashboard() {
+ return import(/* webpackChunkName: "features/admin" */'../../admin');
+}
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..5d04d77ea
--- /dev/null
+++ b/app/styles/components/admin.scss
@@ -0,0 +1,69 @@
+.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: #999;
+ padding-bottom: 8px;
+ margin-bottom: 8px;
+ border-bottom: 1px solid #cfeaf3;
+ }
+}