mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-03-11 02:12:21 -05:00
Stats UI updates (#20946)
[ANAL-43](https://linear.app/tryghost/issue/ANAL-43/implement-all-possible-ui-for-10) - The BarList component wasn't using the parameters provided in its latest release - Number formatting was missing on all numbers - "See all" links were missing in Content/Sources/Locations - Empty/default values was showing [blank] - Flags were missing for country values
This commit is contained in:
parent
681deb18fc
commit
b16c80259e
14 changed files with 340 additions and 70 deletions
ghost/admin/app
23
ghost/admin/app/components/modal-stats-all.hbs
Normal file
23
ghost/admin/app/components/modal-stats-all.hbs
Normal file
|
@ -0,0 +1,23 @@
|
|||
<div class="modal-content" data-test-publish-flow="complete">
|
||||
<header class="modal-header">
|
||||
<h1>
|
||||
{{this.modalTitle}}
|
||||
</h1>
|
||||
</header>
|
||||
|
||||
<button type="button" class="close" title="Close" {{on "click" @close}} data-test-button="close-publish-flow">{{svg-jar "close"}}<span class="hidden">Close</span></button>
|
||||
|
||||
<div {{react-render this.ReactComponent props=(hash chartRange=this.chartRange audience=this.audience type=this.type)}}></div>
|
||||
|
||||
<footer class="modal-footer">
|
||||
<button
|
||||
class="gh-btn gh-btn-primary dismiss"
|
||||
type="button"
|
||||
{{on "click" @close}}
|
||||
{{on "mousedown" (optional this.noop)}}
|
||||
>
|
||||
<span>Close</span>
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
|
116
ghost/admin/app/components/modal-stats-all.js
Normal file
116
ghost/admin/app/components/modal-stats-all.js
Normal file
|
@ -0,0 +1,116 @@
|
|||
'use client';
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import React from 'react';
|
||||
import moment from 'moment-timezone';
|
||||
import {BarList, useQuery} from '@tinybirdco/charts';
|
||||
import {formatNumber} from 'ghost-admin/helpers/format-number';
|
||||
import {getCountryFlag} from 'ghost-admin/utils/stats';
|
||||
import {inject} from 'ghost-admin/decorators/inject';
|
||||
import {statsStaticColors} from 'ghost-admin/utils/stats';
|
||||
|
||||
export default class AllStatsModal extends Component {
|
||||
@inject config;
|
||||
|
||||
get type() {
|
||||
return this.args.data.type;
|
||||
}
|
||||
|
||||
get chartRange() {
|
||||
return this.args.data.chartRange;
|
||||
}
|
||||
|
||||
get audience() {
|
||||
return this.args.data.audience;
|
||||
}
|
||||
|
||||
get modalTitle() {
|
||||
switch (this.type) {
|
||||
case 'top-sources':
|
||||
return 'Sources';
|
||||
case 'top-locations':
|
||||
return 'Locations';
|
||||
default:
|
||||
return 'Content';
|
||||
}
|
||||
}
|
||||
|
||||
ReactComponent = (props) => {
|
||||
let chartRange = props.chartRange;
|
||||
let audience = props.audience || [];
|
||||
let type = props.type;
|
||||
|
||||
const endDate = moment().endOf('day');
|
||||
const startDate = moment().subtract(chartRange - 1, 'days').startOf('day');
|
||||
|
||||
/**
|
||||
* @typedef {Object} Params
|
||||
* @property {string} cid
|
||||
* @property {string} [date_from]
|
||||
* @property {string} [date_to]
|
||||
* @property {string} [member_status]
|
||||
* @property {number} [limit]
|
||||
* @property {number} [skip]
|
||||
*/
|
||||
const params = {
|
||||
site_uuid: this.config.stats.id,
|
||||
date_from: startDate.format('YYYY-MM-DD'),
|
||||
date_to: endDate.format('YYYY-MM-DD'),
|
||||
member_status: audience.length === 0 ? null : audience.join(',')
|
||||
};
|
||||
|
||||
let endpoint;
|
||||
let labelText;
|
||||
let indexBy;
|
||||
let unknownOption = 'Unknown';
|
||||
switch (type) {
|
||||
case 'top-sources':
|
||||
endpoint = `${this.config.stats.endpoint}/v0/pipes/top_sources.json`;
|
||||
labelText = 'Source';
|
||||
indexBy = 'referrer';
|
||||
unknownOption = 'Direct';
|
||||
break;
|
||||
case 'top-locations':
|
||||
endpoint = `${this.config.stats.endpoint}/v0/pipes/top_locations.json`;
|
||||
labelText = 'Country';
|
||||
indexBy = 'location';
|
||||
unknownOption = 'Unknown';
|
||||
break;
|
||||
default:
|
||||
endpoint = `${this.config.stats.endpoint}/v0/pipes/top_pages.json`;
|
||||
labelText = 'Post or page';
|
||||
indexBy = 'pathname';
|
||||
break;
|
||||
}
|
||||
|
||||
const {data, meta, error, loading} = useQuery({
|
||||
endpoint: endpoint,
|
||||
token: this.config.stats.token,
|
||||
params
|
||||
});
|
||||
|
||||
return (
|
||||
<BarList
|
||||
data={data}
|
||||
meta={meta}
|
||||
error={error}
|
||||
loading={loading}
|
||||
index={indexBy}
|
||||
indexConfig={{
|
||||
label: <span className="gh-stats-detail-header">{labelText}</span>,
|
||||
renderBarContent: ({label}) => (
|
||||
<span className="gh-stats-detail-label">{(type === 'top-locations') && getCountryFlag(label)} {label || unknownOption}</span>
|
||||
)
|
||||
}}
|
||||
categories={['hits']}
|
||||
categoryConfig={{
|
||||
hits: {
|
||||
label: <span className="gh-stats-detail-header">Visits</span>,
|
||||
renderValue: ({value}) => <span className="gh-stats-detail-value">{formatNumber(value)}</span>
|
||||
}
|
||||
}}
|
||||
colorPalette={[statsStaticColors[4]]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
}
|
|
@ -4,6 +4,7 @@ import Component from '@glimmer/component';
|
|||
import React from 'react';
|
||||
import moment from 'moment-timezone';
|
||||
import {AreaChart, useQuery} from '@tinybirdco/charts';
|
||||
import {formatNumber} from '../../../helpers/format-number';
|
||||
import {hexToRgba} from 'ghost-admin/utils/stats';
|
||||
import {inject} from 'ghost-admin/decorators/inject';
|
||||
import {statsStaticColors} from '../../../utils/stats';
|
||||
|
@ -165,7 +166,7 @@ export default class KpisComponent extends Component {
|
|||
break;
|
||||
default:
|
||||
tooltipTitle = 'Unique visitors';
|
||||
displayValue = fparams[0].value[1] !== null && fparams[0].value[1];
|
||||
displayValue = fparams[0].value[1] !== null && formatNumber(fparams[0].value[1]);
|
||||
break;
|
||||
}
|
||||
if (!displayValue) {
|
||||
|
|
|
@ -1 +1 @@
|
|||
<div {{react-render this.ReactComponent props=(hash chartRange=@chartRange audience=@audience selected=@selected)}}></div>
|
||||
<div {{react-render this.ReactComponent props=(hash chartRange=@chartRange audience=@audience selected=@selected)}}></div>
|
||||
|
|
|
@ -1,2 +1,10 @@
|
|||
<h5 class="gh-stats-metric-label">Locations</h5>
|
||||
<div {{react-render this.ReactComponent props=(hash chartRange=@chartRange audience=@audience)}}></div>
|
||||
<div>
|
||||
<div class="gh-stats-metric-header"><h5 class="gh-stats-metric-label">Locations</h5></div>
|
||||
<div {{react-render this.ReactComponent props=(hash chartRange=@chartRange audience=@audience)}}></div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button type="button" class="gh-btn gh-btn-link gh-stats-see-all-btn" {{on "click" (fn this.openSeeAll @chartRange @audience)}}>
|
||||
<span>See all →</span>
|
||||
</button>
|
||||
</div>
|
|
@ -1,11 +1,28 @@
|
|||
'use client';
|
||||
|
||||
import AllStatsModal from '../../modal-stats-all';
|
||||
import Component from '@glimmer/component';
|
||||
import React from 'react';
|
||||
import moment from 'moment-timezone';
|
||||
import {BarList, useQuery} from '@tinybirdco/charts';
|
||||
import {action} from '@ember/object';
|
||||
import {formatNumber} from '../../../helpers/format-number';
|
||||
import {getCountryFlag, statsStaticColors} from 'ghost-admin/utils/stats';
|
||||
import {inject} from 'ghost-admin/decorators/inject';
|
||||
import {statsStaticColors} from 'ghost-admin/utils/stats';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
export default class TopLocations extends Component {
|
||||
@inject config;
|
||||
@service modals;
|
||||
|
||||
@action
|
||||
openSeeAll() {
|
||||
this.modals.open(AllStatsModal, {
|
||||
type: 'top-locations',
|
||||
chartRange: this.args.chartRange,
|
||||
audience: this.args.audience
|
||||
});
|
||||
}
|
||||
|
||||
ReactComponent = (props) => {
|
||||
let chartRange = props.chartRange;
|
||||
|
@ -28,7 +45,7 @@ export default class TopLocations extends Component {
|
|||
date_from: startDate.format('YYYY-MM-DD'),
|
||||
date_to: endDate.format('YYYY-MM-DD'),
|
||||
member_status: audience.length === 0 ? null : audience.join(','),
|
||||
limit: 6
|
||||
limit: 8
|
||||
};
|
||||
|
||||
const {data, meta, error, loading} = useQuery({
|
||||
|
@ -44,7 +61,19 @@ export default class TopLocations extends Component {
|
|||
error={error}
|
||||
loading={loading}
|
||||
index="location"
|
||||
indexConfig={{
|
||||
label: <span className="gh-stats-detail-header">Country</span>,
|
||||
renderBarContent: ({label}) => (
|
||||
<span className="gh-stats-detail-label">{getCountryFlag(label)} {label || 'Unknown'}</span>
|
||||
)
|
||||
}}
|
||||
categories={['hits']}
|
||||
categoryConfig={{
|
||||
hits: {
|
||||
label: <span className="gh-stats-detail-header">Visits</span>,
|
||||
renderValue: ({value}) => <span className="gh-stats-detail-value">{formatNumber(value)}</span>
|
||||
}
|
||||
}}
|
||||
colorPalette={[statsStaticColors[4]]}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -1,21 +1,28 @@
|
|||
<div class="gh-stats-metric-header">
|
||||
<h5 class="gh-stats-metric-label">Content</h5>
|
||||
<div>
|
||||
<PowerSelect
|
||||
@selected={{this.contentOption}}
|
||||
@options={{this.contentOptions}}
|
||||
@searchEnabled={{false}}
|
||||
@onChange={{this.onContentOptionChange}}
|
||||
@triggerComponent={{component "gh-power-select/trigger"}}
|
||||
@triggerClass="gh-btn gh-stats-section-dropdown"
|
||||
@dropdownClass="gh-contentfilter-menu-dropdown is-narrow"
|
||||
@matchTriggerWidth={{false}}
|
||||
@horizontalPosition="right"
|
||||
as |option|
|
||||
>
|
||||
{{#if option.name}}{{option.name}}{{else}}<span class="red">Unknown option</span>{{/if}}
|
||||
</PowerSelect>
|
||||
<div>
|
||||
<div class="gh-stats-metric-header">
|
||||
<h5 class="gh-stats-metric-label">Content</h5>
|
||||
<div>
|
||||
<PowerSelect
|
||||
@selected={{this.contentOption}}
|
||||
@options={{this.contentOptions}}
|
||||
@searchEnabled={{false}}
|
||||
@onChange={{this.onContentOptionChange}}
|
||||
@triggerComponent={{component "gh-power-select/trigger"}}
|
||||
@triggerClass="gh-btn gh-stats-section-dropdown"
|
||||
@dropdownClass="gh-contentfilter-menu-dropdown is-narrow"
|
||||
@matchTriggerWidth={{false}}
|
||||
@horizontalPosition="right"
|
||||
as |option|
|
||||
>
|
||||
{{#if option.name}}{{option.name}}{{else}}<span class="red">Unknown option</span>{{/if}}
|
||||
</PowerSelect>
|
||||
</div>
|
||||
</div>
|
||||
<div {{react-render this.ReactComponent props=(hash chartRange=@chartRange audience=@audience)}}></div>
|
||||
</div>
|
||||
|
||||
<div {{react-render this.ReactComponent props=(hash chartRange=@chartRange audience=@audience)}}></div>
|
||||
<div>
|
||||
<button type="button" class="gh-btn gh-btn-link gh-stats-see-all-btn" {{on "click" (fn this.openSeeAll @chartRange @audience)}}>
|
||||
<span>See all →</span>
|
||||
</button>
|
||||
</div>
|
|
@ -1,12 +1,15 @@
|
|||
'use client';
|
||||
|
||||
import AllStatsModal from '../../modal-stats-all';
|
||||
import Component from '@glimmer/component';
|
||||
import React from 'react';
|
||||
import moment from 'moment-timezone';
|
||||
import {BarList, useQuery} from '@tinybirdco/charts';
|
||||
import {CONTENT_OPTIONS} from 'ghost-admin/utils/stats';
|
||||
import {action} from '@ember/object';
|
||||
import {formatNumber} from '../../../helpers/format-number';
|
||||
import {inject} from 'ghost-admin/decorators/inject';
|
||||
import {inject as service} from '@ember/service';
|
||||
import {statsStaticColors} from 'ghost-admin/utils/stats';
|
||||
import {tracked} from '@glimmer/tracking';
|
||||
|
||||
|
@ -16,6 +19,17 @@ export default class TopPages extends Component {
|
|||
@tracked contentOption = CONTENT_OPTIONS[0];
|
||||
@tracked contentOptions = CONTENT_OPTIONS;
|
||||
|
||||
@service modals;
|
||||
|
||||
@action
|
||||
openSeeAll(chartRange, audience) {
|
||||
this.modals.open(AllStatsModal, {
|
||||
type: 'top-pages',
|
||||
chartRange,
|
||||
audience
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
onContentOptionChange(selected) {
|
||||
this.contentOption = selected;
|
||||
|
@ -42,7 +56,7 @@ export default class TopPages extends Component {
|
|||
date_from: startDate.format('YYYY-MM-DD'),
|
||||
date_to: endDate.format('YYYY-MM-DD'),
|
||||
member_status: audience.length === 0 ? null : audience.join(','),
|
||||
limit: 6
|
||||
limit: 8
|
||||
};
|
||||
|
||||
const {data, meta, error, loading} = useQuery({
|
||||
|
@ -59,17 +73,19 @@ export default class TopPages extends Component {
|
|||
loading={loading}
|
||||
index="pathname"
|
||||
indexConfig={{
|
||||
label: <span style={{fontSize: '12px', fontWeight: 'bold'}}>URL</span>
|
||||
label: <span className="gh-stats-detail-header">Post or page</span>,
|
||||
renderBarContent: ({label}) => (
|
||||
<span className="gh-stats-detail-label">{label}</span>
|
||||
)
|
||||
}}
|
||||
categories={['hits']}
|
||||
categoryConfig={{
|
||||
hits: {
|
||||
label: <span>Visits</span>
|
||||
// renderValue: ({ value }) => <span>{formatNumber(value)}</span>
|
||||
label: <span className="gh-stats-detail-header">Visits</span>,
|
||||
renderValue: ({value}) => <span className="gh-stats-detail-value">{formatNumber(value)}</span>
|
||||
}
|
||||
}}
|
||||
colorPalette={[statsStaticColors[4]]}
|
||||
height="300px"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,21 +1,29 @@
|
|||
<div class="gh-stats-metric-header">
|
||||
<h5 class="gh-stats-metric-label">Sources</h5>
|
||||
<div>
|
||||
<PowerSelect
|
||||
@selected={{this.campaignOption}}
|
||||
@options={{this.campaignOptions}}
|
||||
@searchEnabled={{false}}
|
||||
@onChange={{this.onCampaignOptionChange}}
|
||||
@triggerComponent={{component "gh-power-select/trigger"}}
|
||||
@triggerClass="gh-btn gh-stats-section-dropdown"
|
||||
@dropdownClass="gh-contentfilter-menu-dropdown is-narrow"
|
||||
@matchTriggerWidth={{false}}
|
||||
@horizontalPosition="right"
|
||||
as |option|
|
||||
>
|
||||
{{#if option.name}}{{option.name}}{{else}}<span class="red">Unknown option</span>{{/if}}
|
||||
</PowerSelect>
|
||||
<div>
|
||||
<div class="gh-stats-metric-header">
|
||||
<h5 class="gh-stats-metric-label">Sources</h5>
|
||||
<div>
|
||||
<PowerSelect
|
||||
@selected={{this.campaignOption}}
|
||||
@options={{this.campaignOptions}}
|
||||
@searchEnabled={{false}}
|
||||
@onChange={{this.onCampaignOptionChange}}
|
||||
@triggerComponent={{component "gh-power-select/trigger"}}
|
||||
@triggerClass="gh-btn gh-stats-section-dropdown"
|
||||
@dropdownClass="gh-contentfilter-menu-dropdown is-narrow"
|
||||
@matchTriggerWidth={{false}}
|
||||
@horizontalPosition="right"
|
||||
as |option|
|
||||
>
|
||||
{{#if option.name}}{{option.name}}{{else}}<span class="red">Unknown option</span>{{/if}}
|
||||
</PowerSelect>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div {{react-render this.ReactComponent props=(hash chartRange=@chartRange audience=@audience)}}></div>
|
||||
</div>
|
||||
|
||||
<div {{react-render this.ReactComponent props=(hash chartRange=@chartRange audience=@audience)}}></div>
|
||||
<div>
|
||||
<button type="button" class="gh-btn gh-btn-link gh-stats-see-all-btn" {{on "click" (fn this.openSeeAll @chartRange @audience)}}>
|
||||
<span>See all →</span>
|
||||
</button>
|
||||
</div>
|
|
@ -1,17 +1,21 @@
|
|||
'use client';
|
||||
|
||||
import AllStatsModal from '../../modal-stats-all';
|
||||
import Component from '@glimmer/component';
|
||||
import React from 'react';
|
||||
import moment from 'moment-timezone';
|
||||
import {BarList, useQuery} from '@tinybirdco/charts';
|
||||
import {CAMPAIGN_OPTIONS} from 'ghost-admin/utils/stats';
|
||||
import {action} from '@ember/object';
|
||||
import {formatNumber} from '../../../helpers/format-number';
|
||||
import {inject} from 'ghost-admin/decorators/inject';
|
||||
import {inject as service} from '@ember/service';
|
||||
import {statsStaticColors} from 'ghost-admin/utils/stats';
|
||||
import {tracked} from '@glimmer/tracking';
|
||||
|
||||
export default class TopPages extends Component {
|
||||
@inject config;
|
||||
@service modals;
|
||||
|
||||
@tracked campaignOption = CAMPAIGN_OPTIONS[0];
|
||||
@tracked campaignOptions = CAMPAIGN_OPTIONS;
|
||||
|
@ -21,6 +25,15 @@ export default class TopPages extends Component {
|
|||
this.campaignOption = selected;
|
||||
}
|
||||
|
||||
@action
|
||||
openSeeAll() {
|
||||
this.modals.open(AllStatsModal, {
|
||||
type: 'top-sources',
|
||||
chartRange: this.args.chartRange,
|
||||
audience: this.args.audience
|
||||
});
|
||||
}
|
||||
|
||||
ReactComponent = (props) => {
|
||||
let chartRange = props.chartRange;
|
||||
let audience = props.audience;
|
||||
|
@ -41,14 +54,14 @@ export default class TopPages extends Component {
|
|||
site_uuid: this.config.stats.id,
|
||||
date_from: startDate.format('YYYY-MM-DD'),
|
||||
date_to: endDate.format('YYYY-MM-DD'),
|
||||
member_status: audience.length === 0 ? null : audience.join(',')
|
||||
member_status: audience.length === 0 ? null : audience.join(','),
|
||||
limit: 8
|
||||
};
|
||||
|
||||
const {data, meta, error, loading} = useQuery({
|
||||
endpoint: `${this.config.stats.endpoint}/v0/pipes/top_sources.json`,
|
||||
token: this.config.stats.token,
|
||||
params,
|
||||
limit: 6
|
||||
params
|
||||
});
|
||||
|
||||
return (
|
||||
|
@ -58,9 +71,21 @@ export default class TopPages extends Component {
|
|||
error={error}
|
||||
loading={loading}
|
||||
index="referrer"
|
||||
indexConfig={{
|
||||
label: <span className="gh-stats-detail-header">Source</span>,
|
||||
renderBarContent: ({label}) => (
|
||||
<span className="gh-stats-detail-label">{label || 'Direct'}</span>
|
||||
)
|
||||
}}
|
||||
categories={['hits']}
|
||||
categoryConfig={{
|
||||
hits: {
|
||||
label: <span className="gh-stats-detail-header">Visits</span>,
|
||||
renderValue: ({value}) => <span className="gh-stats-detail-value">{formatNumber(value)}</span>
|
||||
}
|
||||
}}
|
||||
colorPalette={[statsStaticColors[4]]}
|
||||
height="300px"
|
||||
// height="300px"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import Component from '@glimmer/component';
|
||||
import fetch from 'fetch';
|
||||
import {action} from '@ember/object';
|
||||
import {formatNumber} from 'ghost-admin/helpers/format-number';
|
||||
import {inject} from 'ghost-admin/decorators/inject';
|
||||
import {task} from 'ember-concurrency';
|
||||
import {tracked} from '@glimmer/tracking';
|
||||
|
@ -83,7 +84,7 @@ export default class KpisOverview extends Component {
|
|||
const _KPITotal = kpi => queryData.reduce((prev, curr) => (curr[kpi] ?? 0) + prev, 0);
|
||||
|
||||
// Get total number of sessions
|
||||
const totalVisits = _KPITotal('visits');
|
||||
const totalVisits = formatNumber(_KPITotal('visits'));
|
||||
|
||||
// Sum total KPI value from the trend, ponderating using sessions
|
||||
const _ponderatedKPIsTotal = kpi => queryData.reduce((prev, curr) => prev + ((curr[kpi] ?? 0) * curr.visits / totalVisits), 0);
|
||||
|
|
|
@ -1,20 +1,22 @@
|
|||
<div class="gh-stats-tabs-header">
|
||||
<div class="gh-stats-tabs">
|
||||
<button type="button" class="gh-stats-tab {{if this.devicesTabSelected 'is-selected'}}" {{on "click" this.changeTabToDevices}}>
|
||||
<Stats::Parts::Metric
|
||||
@label="Devices" />
|
||||
</button>
|
||||
<div>
|
||||
<div class="gh-stats-tabs-header">
|
||||
<div class="gh-stats-tabs">
|
||||
<button type="button" class="gh-stats-tab {{if this.devicesTabSelected 'is-selected'}}" {{on "click" this.changeTabToDevices}}>
|
||||
<Stats::Parts::Metric
|
||||
@label="Devices" />
|
||||
</button>
|
||||
|
||||
<button type="button" class="gh-stats-tab {{if this.browsersTabSelected 'is-selected'}}" {{on "click" this.changeTabToBrowsers}}>
|
||||
<Stats::Parts::Metric
|
||||
@label="Browsers" />
|
||||
</button>
|
||||
<button type="button" class="gh-stats-tab {{if this.browsersTabSelected 'is-selected'}}" {{on "click" this.changeTabToBrowsers}}>
|
||||
<Stats::Parts::Metric
|
||||
@label="Browsers" />
|
||||
</button>
|
||||
|
||||
{{!-- <button type="button" class="gh-stats-tab {{if this.osTabSelected 'is-selected'}}" {{on "click" this.changeTabToOSs}}>
|
||||
<Stats::Parts::Metric
|
||||
@label="Operating systems" />
|
||||
</button> --}}
|
||||
{{!-- <button type="button" class="gh-stats-tab {{if this.osTabSelected 'is-selected'}}" {{on "click" this.changeTabToOSs}}>
|
||||
<Stats::Parts::Metric
|
||||
@label="Operating systems" />
|
||||
</button> --}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Stats::Charts::Technical @chartRange={{@chartRange}} @audience={{@audience}} @selected={{this.selected}} />
|
||||
<Stats::Charts::Technical @chartRange={{@chartRange}} @audience={{@audience}} @selected={{this.selected}} />
|
||||
</div>
|
|
@ -23,6 +23,10 @@
|
|||
}
|
||||
|
||||
.gh-stats-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
padding: 20px;
|
||||
border: 1px solid var(--whitegrey);
|
||||
border-radius: 8px;
|
||||
|
@ -32,7 +36,7 @@
|
|||
|
||||
.gh-stats-container > .gh-stats-metric-label,
|
||||
.gh-stats-metric-header {
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.gh-stats-container:hover {
|
||||
|
@ -162,6 +166,7 @@
|
|||
border: none !important;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
margin-right: -8px;
|
||||
}
|
||||
|
||||
.gh-stats-section-dropdown.ember-power-select-trigger.gh-btn span {
|
||||
|
@ -184,4 +189,25 @@
|
|||
.gh-stats-metric-label {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.gh-stats-detail-header {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.gh-stats-see-all-btn span {
|
||||
height: unset;
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
color: var(--black);
|
||||
}
|
||||
|
||||
.gh-stats-see-all-btn:hover span {
|
||||
color: var(--green);
|
||||
}
|
||||
|
||||
.gh-stats-detail-label,
|
||||
.gh-stats-detail-value {
|
||||
font-size: 13.5px;
|
||||
}
|
|
@ -108,4 +108,12 @@ export function generateMonochromePalette(baseColor, count = 10) {
|
|||
|
||||
export const statsStaticColors = [
|
||||
'#8E42FF', '#B07BFF', '#C7A0FF', '#DDC6FF', '#EBDDFF', '#F7EDFF'
|
||||
];
|
||||
];
|
||||
|
||||
export const getCountryFlag = (countryCode) => {
|
||||
if (!countryCode) {
|
||||
return '🏳️';
|
||||
}
|
||||
return countryCode.toUpperCase().replace(/./g, char => String.fromCodePoint(char.charCodeAt(0) + 127397)
|
||||
);
|
||||
};
|
Loading…
Add table
Reference in a new issue