const registrationMode = document.getElementById('form_admin_settings_registrations_mode');
if (registrationMode) onChangeRegistrationMode(registrationMode);
-
- const React = require('react');
- const ReactDOM = require('react-dom');
-
- [].forEach.call(document.querySelectorAll('[data-admin-component]'), element => {
- const componentName = element.getAttribute('data-admin-component');
- const { locale, ...componentProps } = JSON.parse(element.getAttribute('data-props'));
-
- import('../mastodon/containers/admin_component').then(({ default: AdminComponent }) => {
- return import('../mastodon/components/admin/' + componentName).then(({ default: Component }) => {
- ReactDOM.render((
- <AdminComponent locale={locale}>
- <Component {...componentProps} />
- </AdminComponent>
- ), element);
- });
- }).catch(error => {
- console.error(error);
- });
- });
});
--- /dev/null
+import React from 'react';
+import PropTypes from 'prop-types';
+import api from 'flavours/glitch/util/api';
+import { FormattedNumber } from 'react-intl';
+import { Sparklines, SparklinesCurve } from 'react-sparklines';
+import classNames from 'classnames';
+import Skeleton from 'flavours/glitch/components/skeleton';
+
+const percIncrease = (a, b) => {
+ let percent;
+
+ if (b !== 0) {
+ if (a !== 0) {
+ percent = (b - a) / a;
+ } else {
+ percent = 1;
+ }
+ } else if (b === 0 && a === 0) {
+ percent = 0;
+ } else {
+ percent = - 1;
+ }
+
+ return percent;
+};
+
+export default class Counter extends React.PureComponent {
+
+ static propTypes = {
+ measure: PropTypes.string.isRequired,
+ start_at: PropTypes.string.isRequired,
+ end_at: PropTypes.string.isRequired,
+ label: PropTypes.string.isRequired,
+ href: PropTypes.string,
+ };
+
+ state = {
+ loading: true,
+ data: null,
+ };
+
+ componentDidMount () {
+ const { measure, start_at, end_at } = this.props;
+
+ api().post('/api/v1/admin/measures', { keys: [measure], start_at, end_at }).then(res => {
+ this.setState({
+ loading: false,
+ data: res.data,
+ });
+ }).catch(err => {
+ console.error(err);
+ });
+ }
+
+ render () {
+ const { label, href } = this.props;
+ const { loading, data } = this.state;
+
+ let content;
+
+ if (loading) {
+ content = (
+ <React.Fragment>
+ <span className='sparkline__value__total'><Skeleton width={43} /></span>
+ <span className='sparkline__value__change'><Skeleton width={43} /></span>
+ </React.Fragment>
+ );
+ } else {
+ const measure = data[0];
+ const percentChange = percIncrease(measure.previous_total * 1, measure.total * 1);
+
+ content = (
+ <React.Fragment>
+ <span className='sparkline__value__total'><FormattedNumber value={measure.total} /></span>
+ <span className={classNames('sparkline__value__change', { positive: percentChange > 0, negative: percentChange < 0 })}>{percentChange > 0 && '+'}<FormattedNumber value={percentChange} style='percent' /></span>
+ </React.Fragment>
+ );
+ }
+
+ const inner = (
+ <React.Fragment>
+ <div className='sparkline__value'>
+ {content}
+ </div>
+
+ <div className='sparkline__label'>
+ {label}
+ </div>
+
+ <div className='sparkline__graph'>
+ {!loading && (
+ <Sparklines width={259} height={55} data={data[0].data.map(x => x.value * 1)}>
+ <SparklinesCurve />
+ </Sparklines>
+ )}
+ </div>
+ </React.Fragment>
+ );
+
+ if (href) {
+ return (
+ <a href={href} className='sparkline'>
+ {inner}
+ </a>
+ );
+ } else {
+ return (
+ <div className='sparkline'>
+ {inner}
+ </div>
+ );
+ }
+ }
+
+}
--- /dev/null
+import React from 'react';
+import PropTypes from 'prop-types';
+import api from 'flavours/glitch/util/api';
+import { FormattedNumber } from 'react-intl';
+import { roundTo10 } from 'flavours/glitch/util/numbers';
+import Skeleton from 'flavours/glitch/components/skeleton';
+
+export default class Dimension extends React.PureComponent {
+
+ static propTypes = {
+ dimension: PropTypes.string.isRequired,
+ start_at: PropTypes.string.isRequired,
+ end_at: PropTypes.string.isRequired,
+ limit: PropTypes.number.isRequired,
+ label: PropTypes.string.isRequired,
+ };
+
+ state = {
+ loading: true,
+ data: null,
+ };
+
+ componentDidMount () {
+ const { start_at, end_at, dimension, limit } = this.props;
+
+ api().post('/api/v1/admin/dimensions', { keys: [dimension], start_at, end_at, limit }).then(res => {
+ this.setState({
+ loading: false,
+ data: res.data,
+ });
+ }).catch(err => {
+ console.error(err);
+ });
+ }
+
+ render () {
+ const { label, limit } = this.props;
+ const { loading, data } = this.state;
+
+ let content;
+
+ if (loading) {
+ content = (
+ <table>
+ <tbody>
+ {Array.from(Array(limit)).map((_, i) => (
+ <tr className='dimension__item' key={i}>
+ <td className='dimension__item__key'>
+ <Skeleton width={100} />
+ </td>
+
+ <td className='dimension__item__value'>
+ <Skeleton width={60} />
+ </td>
+ </tr>
+ ))}
+ </tbody>
+ </table>
+ );
+ } else {
+ const sum = data[0].data.reduce((sum, cur) => sum + (cur.value * 1), 0);
+
+ content = (
+ <table>
+ <tbody>
+ {data[0].data.map(item => (
+ <tr className='dimension__item' key={item.key}>
+ <td className='dimension__item__key'>
+ <span className={`dimension__item__indicator dimension__item__indicator--${roundTo10(((item.value * 1) / sum) * 100)}`} />
+ <span title={item.key}>{item.human_key}</span>
+ </td>
+
+ <td className='dimension__item__value'>
+ {typeof item.human_value !== 'undefined' ? item.human_value : <FormattedNumber value={item.value} />}
+ </td>
+ </tr>
+ ))}
+ </tbody>
+ </table>
+ );
+ }
+
+ return (
+ <div className='dimension'>
+ <h4>{label}</h4>
+
+ {content}
+ </div>
+ );
+ }
+
+}
--- /dev/null
+import React from 'react';
+import PropTypes from 'prop-types';
+import api from 'flavours/glitch/util/api';
+import { FormattedMessage, FormattedNumber, FormattedDate } from 'react-intl';
+import classNames from 'classnames';
+import { roundTo10 } from 'flavours/glitch/util/numbers';
+
+const dateForCohort = cohort => {
+ switch(cohort.frequency) {
+ case 'day':
+ return <FormattedDate value={cohort.period} month='long' day='2-digit' />;
+ default:
+ return <FormattedDate value={cohort.period} month='long' year='numeric' />;
+ }
+};
+
+export default class Retention extends React.PureComponent {
+
+ static propTypes = {
+ start_at: PropTypes.string,
+ end_at: PropTypes.string,
+ frequency: PropTypes.string,
+ };
+
+ state = {
+ loading: true,
+ data: null,
+ };
+
+ componentDidMount () {
+ const { start_at, end_at, frequency } = this.props;
+
+ api().post('/api/v1/admin/retention', { start_at, end_at, frequency }).then(res => {
+ this.setState({
+ loading: false,
+ data: res.data,
+ });
+ }).catch(err => {
+ console.error(err);
+ });
+ }
+
+ render () {
+ const { loading, data } = this.state;
+
+ let content;
+
+ if (loading) {
+ content = <FormattedMessage id='loading_indicator.label' defaultMessage='Loading...' />;
+ } else {
+ content = (
+ <table className='retention__table'>
+ <thead>
+ <tr>
+ <th>
+ <div className='retention__table__date retention__table__label'>
+ <FormattedMessage id='admin.dashboard.retention.cohort' defaultMessage='Sign-up month' />
+ </div>
+ </th>
+
+ <th>
+ <div className='retention__table__number retention__table__label'>
+ <FormattedMessage id='admin.dashboard.retention.cohort_size' defaultMessage='New users' />
+ </div>
+ </th>
+
+ {data[0].data.slice(1).map((retention, i) => (
+ <th key={retention.date}>
+ <div className='retention__table__number retention__table__label'>
+ {i + 1}
+ </div>
+ </th>
+ ))}
+ </tr>
+
+ <tr>
+ <td>
+ <div className='retention__table__date retention__table__average'>
+ <FormattedMessage id='admin.dashboard.retention.average' defaultMessage='Average' />
+ </div>
+ </td>
+
+ <td>
+ <div className='retention__table__size'>
+ <FormattedNumber value={data.reduce((sum, cohort, i) => sum + ((cohort.data[0].value * 1) - sum) / (i + 1), 0)} maximumFractionDigits={0} />
+ </div>
+ </td>
+
+ {data[0].data.slice(1).map((retention, i) => {
+ const average = data.reduce((sum, cohort, k) => cohort.data[i + 1] ? sum + (cohort.data[i + 1].percent - sum)/(k + 1) : sum, 0);
+
+ return (
+ <td key={retention.date}>
+ <div className={classNames('retention__table__box', 'retention__table__average', `retention__table__box--${roundTo10(average * 100)}`)}>
+ <FormattedNumber value={average} style='percent' />
+ </div>
+ </td>
+ );
+ })}
+ </tr>
+ </thead>
+
+ <tbody>
+ {data.slice(0, -1).map(cohort => (
+ <tr key={cohort.period}>
+ <td>
+ <div className='retention__table__date'>
+ {dateForCohort(cohort)}
+ </div>
+ </td>
+
+ <td>
+ <div className='retention__table__size'>
+ <FormattedNumber value={cohort.data[0].value} />
+ </div>
+ </td>
+
+ {cohort.data.slice(1).map(retention => (
+ <td key={retention.date}>
+ <div className={classNames('retention__table__box', `retention__table__box--${roundTo10(retention.percent * 100)}`)}>
+ <FormattedNumber value={retention.percent} style='percent' />
+ </div>
+ </td>
+ ))}
+ </tr>
+ ))}
+ </tbody>
+ </table>
+ );
+ }
+
+ return (
+ <div className='retention'>
+ <h4><FormattedMessage id='admin.dashboard.retention' defaultMessage='Retention' /></h4>
+
+ {content}
+ </div>
+ );
+ }
+
+}
--- /dev/null
+import React from 'react';
+import PropTypes from 'prop-types';
+import api from 'flavours/glitch/util/api';
+import { FormattedMessage } from 'react-intl';
+import classNames from 'classnames';
+import Hashtag from 'flavours/glitch/components/hashtag';
+
+export default class Trends extends React.PureComponent {
+
+ static propTypes = {
+ limit: PropTypes.number.isRequired,
+ };
+
+ state = {
+ loading: true,
+ data: null,
+ };
+
+ componentDidMount () {
+ const { limit } = this.props;
+
+ api().get('/api/v1/admin/trends', { params: { limit } }).then(res => {
+ this.setState({
+ loading: false,
+ data: res.data,
+ });
+ }).catch(err => {
+ console.error(err);
+ });
+ }
+
+ render () {
+ const { limit } = this.props;
+ const { loading, data } = this.state;
+
+ let content;
+
+ if (loading) {
+ content = (
+ <div>
+ {Array.from(Array(limit)).map((_, i) => (
+ <Hashtag key={i} />
+ ))}
+ </div>
+ );
+ } else {
+ content = (
+ <div>
+ {data.map(hashtag => (
+ <Hashtag
+ key={hashtag.name}
+ name={hashtag.name}
+ href={`/admin/tags/${hashtag.id}`}
+ people={hashtag.history[0].accounts * 1 + hashtag.history[1].accounts * 1}
+ uses={hashtag.history[0].uses * 1 + hashtag.history[1].uses * 1}
+ history={hashtag.history.reverse().map(day => day.uses)}
+ className={classNames(hashtag.requires_review && 'trends__item--requires-review', !hashtag.trendable && !hashtag.requires_review && 'trends__item--disabled')}
+ />
+ ))}
+ </div>
+ );
+ }
+
+ return (
+ <div className='trends trends--compact'>
+ <h4><FormattedMessage id='trends.trending_now' defaultMessage='Trending now' /></h4>
+
+ {content}
+ </div>
+ );
+ }
+
+}
import ImmutablePropTypes from 'react-immutable-proptypes';
import Permalink from './permalink';
import ShortNumber from 'flavours/glitch/components/short_number';
+import Skeleton from 'flavours/glitch/components/skeleton';
+import classNames from 'classnames';
class SilentErrorBoundary extends React.Component {
/>
);
-const Hashtag = ({ hashtag }) => (
- <div className='trends__item'>
+export const ImmutableHashtag = ({ hashtag }) => (
+ <Hashtag
+ name={hashtag.get('name')}
+ href={hashtag.get('url')}
+ to={`/tags/${hashtag.get('name')}`}
+ people={hashtag.getIn(['history', 0, 'accounts']) * 1 + hashtag.getIn(['history', 1, 'accounts']) * 1}
+ uses={hashtag.getIn(['history', 0, 'uses']) * 1 + hashtag.getIn(['history', 1, 'uses']) * 1}
+ history={hashtag.get('history').reverse().map((day) => day.get('uses')).toArray()}
+ />
+);
+
+ImmutableHashtag.propTypes = {
+ hashtag: ImmutablePropTypes.map.isRequired,
+};
+
+const Hashtag = ({ name, href, to, people, uses, history, className }) => (
+ <div className={classNames('trends__item', className)}>
<div className='trends__item__name'>
- <Permalink
- href={hashtag.get('url')}
- to={`/tags/${hashtag.get('name')}`}
- >
- #<span>{hashtag.get('name')}</span>
+ <Permalink href={href} to={to}>
+ {name ? <React.Fragment>#<span>{name}</span></React.Fragment> : <Skeleton width={50} />}
</Permalink>
- <ShortNumber
- value={
- hashtag.getIn(['history', 0, 'accounts']) * 1 +
- hashtag.getIn(['history', 1, 'accounts']) * 1
- }
- renderer={accountsCountRenderer}
- />
+ {typeof people !== 'undefined' ? <ShortNumber value={people} renderer={accountsCountRenderer} /> : <Skeleton width={100} />}
</div>
<div className='trends__item__current'>
- <ShortNumber
- value={
- hashtag.getIn(['history', 0, 'uses']) * 1 +
- hashtag.getIn(['history', 1, 'uses']) * 1
- }
- />
+ {typeof uses !== 'undefined' ? <ShortNumber value={uses} /> : <Skeleton width={42} height={36} />}
</div>
<div className='trends__item__sparkline'>
<SilentErrorBoundary>
- <Sparklines
- width={50}
- height={28}
- data={hashtag
- .get('history')
- .reverse()
- .map((day) => day.get('uses'))
- .toArray()}
- >
+ <Sparklines width={50} height={28} data={history ? history : Array.from(Array(7)).map(() => 0)}>
<SparklinesCurve style={{ fill: 'none' }} />
</Sparklines>
</SilentErrorBoundary>
);
Hashtag.propTypes = {
- hashtag: ImmutablePropTypes.map.isRequired,
+ name: PropTypes.string,
+ href: PropTypes.string,
+ to: PropTypes.string,
+ people: PropTypes.number,
+ uses: PropTypes.number,
+ history: PropTypes.arrayOf(PropTypes.number),
+ className: PropTypes.string,
};
export default Hashtag;
--- /dev/null
+import React from 'react';
+import PropTypes from 'prop-types';
+
+const Skeleton = ({ width, height }) => <span className='skeleton' style={{ width, height }}>‌</span>;
+
+Skeleton.propTypes = {
+ width: PropTypes.number,
+ height: PropTypes.number,
+};
+
+export default Skeleton;
--- /dev/null
+import React from 'react';
+import PropTypes from 'prop-types';
+import { IntlProvider, addLocaleData } from 'react-intl';
+import { getLocale } from 'mastodon/locales';
+
+const { localeData, messages } = getLocale();
+addLocaleData(localeData);
+
+export default class AdminComponent extends React.PureComponent {
+
+ static propTypes = {
+ locale: PropTypes.string.isRequired,
+ children: PropTypes.node.isRequired,
+ };
+
+ render () {
+ const { locale, children } = this.props;
+
+ return (
+ <IntlProvider locale={locale} messages={messages}>
+ {children}
+ </IntlProvider>
+ );
+ }
+
+}
import { getScrollbarWidth } from 'flavours/glitch/util/scrollbar';
import MediaGallery from 'flavours/glitch/components/media_gallery';
import Poll from 'flavours/glitch/components/poll';
-import Hashtag from 'flavours/glitch/components/hashtag';
+import { ImmutableHashtag as Hashtag } from 'flavours/glitch/components/hashtag';
import ModalRoot from 'flavours/glitch/components/modal_root';
import MediaModal from 'flavours/glitch/features/ui/components/media_modal';
import Video from 'flavours/glitch/features/video';
import AccountContainer from 'flavours/glitch/containers/account_container';
import StatusContainer from 'flavours/glitch/containers/status_container';
import ImmutablePureComponent from 'react-immutable-pure-component';
-import Hashtag from 'flavours/glitch/components/hashtag';
+import { ImmutableHashtag as Hashtag } from 'flavours/glitch/components/hashtag';
import Icon from 'flavours/glitch/components/icon';
import { searchEnabled } from 'flavours/glitch/util/initial_state';
import LoadMore from 'flavours/glitch/components/load_more';
import ImmutablePureComponent from 'react-immutable-pure-component';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
-import Hashtag from 'flavours/glitch/components/hashtag';
+import { ImmutableHashtag as Hashtag } from 'flavours/glitch/components/hashtag';
import { FormattedMessage } from 'react-intl';
export default class Trends extends ImmutablePureComponent {
--- /dev/null
+import 'packs/public-path';
+import ready from 'flavours/glitch/util/ready';
+
+ready(() => {
+ const React = require('react');
+ const ReactDOM = require('react-dom');
+
+ [].forEach.call(document.querySelectorAll('[data-admin-component]'), element => {
+ const componentName = element.getAttribute('data-admin-component');
+ const { locale, ...componentProps } = JSON.parse(element.getAttribute('data-props'));
+
+ import('flavours/glitch/containers/admin_component').then(({ default: AdminComponent }) => {
+ return import('flavours/glitch/components/admin/' + componentName).then(({ default: Component }) => {
+ ReactDOM.render((
+ <AdminComponent locale={locale}>
+ <Component {...componentProps} />
+ </AdminComponent>
+ ), element);
+ });
+ }).catch(error => {
+ console.error(error);
+ });
+ });
+});
+@use "sass:math";
+
$no-columns-breakpoint: 600px;
$sidebar-width: 240px;
$content-width: 840px;
}
}
+.dashboard__counters.admin-account-counters {
+ margin-top: 10px;
+}
+
.account-badges {
margin: -2px 0;
}
-.dashboard__counters.admin-account-counters {
- margin-top: 10px;
+.retention {
+ &__table {
+ &__number {
+ color: $secondary-text-color;
+ padding: 10px;
+ }
+
+ &__date {
+ white-space: nowrap;
+ padding: 10px 0;
+ text-align: left;
+ min-width: 120px;
+
+ &.retention__table__average {
+ font-weight: 700;
+ }
+ }
+
+ &__size {
+ text-align: center;
+ padding: 10px;
+ }
+
+ &__label {
+ font-weight: 700;
+ color: $darker-text-color;
+ }
+
+ &__box {
+ box-sizing: border-box;
+ background: $ui-highlight-color;
+ padding: 10px;
+ font-weight: 500;
+ color: $primary-text-color;
+ width: 52px;
+ margin: 1px;
+
+ @for $i from 0 through 10 {
+ &--#{10 * $i} {
+ background-color: rgba($ui-highlight-color, 1 * (math.div(max(1, $i), 10)));
+ }
+ }
+ }
+ }
+}
+
+.sparkline {
+ display: block;
+ text-decoration: none;
+ background: lighten($ui-base-color, 4%);
+ border-radius: 4px;
+ padding: 0;
+ position: relative;
+ padding-bottom: 55px + 20px;
+ overflow: hidden;
+
+ &__value {
+ display: flex;
+ line-height: 33px;
+ align-items: flex-end;
+ padding: 20px;
+ padding-bottom: 10px;
+
+ &__total {
+ display: block;
+ margin-right: 10px;
+ font-weight: 500;
+ font-size: 28px;
+ color: $primary-text-color;
+ }
+
+ &__change {
+ display: block;
+ font-weight: 500;
+ font-size: 18px;
+ color: $darker-text-color;
+ margin-bottom: -3px;
+
+ &.positive {
+ color: $valid-value-color;
+ }
+
+ &.negative {
+ color: $error-value-color;
+ }
+ }
+ }
+
+ &__label {
+ padding: 0 20px;
+ padding-bottom: 10px;
+ text-transform: uppercase;
+ color: $darker-text-color;
+ font-weight: 500;
+ }
+
+ &__graph {
+ position: absolute;
+ bottom: 0;
+
+ svg {
+ display: block;
+ margin: 0;
+ }
+
+ path:first-child {
+ fill: rgba($highlight-text-color, 0.25) !important;
+ fill-opacity: 1 !important;
+ }
+
+ path:last-child {
+ stroke: lighten($highlight-text-color, 6%) !important;
+ fill: none !important;
+ }
+ }
+}
+
+a.sparkline {
+ &:hover,
+ &:focus,
+ &:active {
+ background: lighten($ui-base-color, 6%);
+ }
+}
+
+.skeleton {
+ background-color: lighten($ui-base-color, 8%);
+ background-image: linear-gradient(90deg, lighten($ui-base-color, 8%), lighten($ui-base-color, 12%), lighten($ui-base-color, 8%));
+ background-size: 200px 100%;
+ background-repeat: no-repeat;
+ border-radius: 4px;
+ display: inline-block;
+ line-height: 1;
+ width: 100%;
+ animation: skeleton 1.2s ease-in-out infinite;
+}
+
+@keyframes skeleton {
+ 0% {
+ background-position: -200px 0;
+ }
+
+ 100% {
+ background-position: calc(200px + 100%) 0;
+ }
+}
+
+.dimension {
+ table {
+ width: 100%;
+ }
+
+ &__item {
+ border-bottom: 1px solid lighten($ui-base-color, 4%);
+
+ &__key {
+ font-weight: 500;
+ padding: 11px 10px;
+ }
+
+ &__value {
+ text-align: right;
+ color: $darker-text-color;
+ padding: 11px 10px;
+ }
+
+ &__indicator {
+ display: inline-block;
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background: $ui-highlight-color;
+ margin-right: 10px;
+
+ @for $i from 0 through 10 {
+ &--#{10 * $i} {
+ background-color: rgba($ui-highlight-color, 1 * (math.div(max(1, $i), 10)));
+ }
+ }
+ }
+
+ &:last-child {
+ border-bottom: 0;
+ }
+ }
}
&__current {
flex: 0 0 auto;
font-size: 24px;
- line-height: 36px;
font-weight: 500;
text-align: right;
padding-right: 15px;
fill: none !important;
}
}
+
+ &--requires-review {
+ .trends__item__name {
+ color: $gold-star;
+
+ a {
+ color: $gold-star;
+ }
+ }
+
+ .trends__item__current {
+ color: $gold-star;
+ }
+
+ .trends__item__sparkline {
+ path:first-child {
+ fill: rgba($gold-star, 0.25) !important;
+ }
+
+ path:last-child {
+ stroke: lighten($gold-star, 6%) !important;
+ }
+ }
+ }
+
+ &--disabled {
+ .trends__item__name {
+ color: lighten($ui-base-color, 12%);
+
+ a {
+ color: lighten($ui-base-color, 12%);
+ }
+ }
+
+ .trends__item__current {
+ color: lighten($ui-base-color, 12%);
+ }
+
+ .trends__item__sparkline {
+ path:first-child {
+ fill: rgba(lighten($ui-base-color, 12%), 0.25) !important;
+ }
+
+ path:last-child {
+ stroke: lighten(lighten($ui-base-color, 12%), 6%) !important;
+ }
+ }
+ }
+ }
+
+ &--compact &__item {
+ padding: 10px;
}
}
}
}
-.dashboard__widgets {
- display: flex;
- flex-wrap: wrap;
- margin: 0 -5px;
+.dashboard {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr);
+ grid-gap: 10px;
- & > div {
- flex: 0 0 33.333%;
- margin-bottom: 20px;
+ &__item {
+ &--span-double-column {
+ grid-column: span 2;
+ }
- & > div {
- padding: 0 5px;
+ &--span-double-row {
+ grid-row: span 2;
+ }
+
+ h4 {
+ padding-top: 20px;
}
}
- a:not(.name-tag) {
- color: $ui-secondary-color;
- font-weight: 500;
+ &__quick-access {
+ display: flex;
+ align-items: baseline;
+ border-radius: 4px;
+ background: $ui-highlight-color;
+ color: $primary-text-color;
+ transition: all 100ms ease-in;
+ font-size: 14px;
+ padding: 0 16px;
+ line-height: 36px;
+ height: 36px;
text-decoration: none;
+ margin-bottom: 4px;
+
+ &:active,
+ &:focus,
+ &:hover {
+ background-color: lighten($ui-highlight-color, 10%);
+ transition: all 200ms ease-out;
+ }
+
+ span {
+ flex: 1 1 auto;
+ }
+
+ .fa {
+ flex: 0 0 auto;
+ }
+
+ strong {
+ font-weight: 700;
+ }
}
}
# (REQUIRED) The location of the pack files.
pack:
about: packs/about.js
- admin: packs/public.js
+ admin: packs/admin.js
auth: packs/public.js
common:
filename: packs/common.js
return Math.trunc(sourceNumber / closestScale) * closestScale;
}
+
+/**
+ * @param {number} num
+ * @returns {number}
+ */
+export function roundTo10(num) {
+ return Math.round(num * 0.1) / 0.1;
+}
# (REQUIRED) The location of the pack files inside `pack_directory`.
pack:
about: about.js
- admin: public.js
+ admin: admin.js
auth: public.js
common:
filename: common.js
--- /dev/null
+import './public-path';
+import ready from '../mastodon/ready';
+
+ready(() => {
+ const React = require('react');
+ const ReactDOM = require('react-dom');
+
+ [].forEach.call(document.querySelectorAll('[data-admin-component]'), element => {
+ const componentName = element.getAttribute('data-admin-component');
+ const { locale, ...componentProps } = JSON.parse(element.getAttribute('data-props'));
+
+ import('../mastodon/containers/admin_component').then(({ default: AdminComponent }) => {
+ return import('../mastodon/components/admin/' + componentName).then(({ default: Component }) => {
+ ReactDOM.render((
+ <AdminComponent locale={locale}>
+ <Component {...componentProps} />
+ </AdminComponent>
+ ), element);
+ });
+ }).catch(error => {
+ console.error(error);
+ });
+ });
+});