Stats page design fixes (#21171)

[ANAL-95](https://linear.app/tryghost/issue/ANAL-95/internal-beta-qa)

Various design refinements and fixes for the Stats page:
- Updated scroll area in detail modals so that the Close button and the footer is never outside the viewport
- The detail modal didn't close after clicking on the filter values
- "Show all" button was displayed also when there were no new items in the detail modal
- Dropdown styles needed a visual update: the toggles were way too huge and inconsistent with other dropdowns
- If no audience was selected we still showed stats. Now it's displaying the default empty screen in this case
- Click through filter indicators had low discoverability
- Technical data styles needed some love: changed the alignment and color scheme
- Mobile size viewports were not handled
- The google favicon API returned 404 many times for sources. Swapped the service for another one that returns favicons more reliably
- Default favicon was not handled. Now it comes from static.ghost.org
This commit is contained in:
Peter Zimon 2024-10-01 17:51:55 +02:00 committed by GitHub
parent d8c4dfef99
commit 8aaac5abe1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 278 additions and 77 deletions

View File

@ -126,7 +126,7 @@
<ul class="nav-list">
{{#if (and this.post.isPage this.post.lexical)}}
<li class="nav-list-item">
<div class="for-switch x-small">
<div class="for-switch xs">
<label class="switch">
<span>
<Icons::EyeOpenClose class="feature" @closed={{not this.post.showTitleAndFeatureImage}} />
@ -154,7 +154,7 @@
{{/if}}
{{#unless this.session.user.isAuthorOrContributor}}
<li class="nav-list-item">
<div class="for-switch x-small">
<div class="for-switch xs">
<label class="switch" for="featured" {{action "toggleFeatured" bubbles="false"}}>
<span>
{{#if this.post.featured}}

View File

@ -18,7 +18,7 @@
{{svg-jar type.icon class="gh-member-activity-actions-menu-icon"}}
<span>{{type.name}}</span>
</label>
<div class="for-switch x-small">
<div class="for-switch xxs">
<label class="switch" for="type-{{idx}}">
<input
data-test-id="event-type-filter-checkbox-{{type.event}}"

View File

@ -54,7 +54,7 @@ export default class KpisComponent extends Component {
options={{
grid: {
left: '10px',
right: '10px',
right: '20px',
top: '10%',
bottom: 0,
containLabel: true

View File

@ -1 +1 @@
<div {{react-render this.ReactComponent props=(hash chartRange=@chartRange audience=@audience device=@device browser=@browser location=@location source=@source pathname=@pathname selected=@selected)}}></div>
<div class="gh-stats-technical-data" {{react-render this.ReactComponent props=(hash chartRange=@chartRange audience=@audience device=@device browser=@browser location=@location source=@source pathname=@pathname selected=@selected)}}></div>

View File

@ -29,7 +29,7 @@ export default class TechnicalComponent extends Component {
ReactComponent = (props) => {
const {selected} = props;
const colorPalette = statsStaticColors.slice(1, 5);
const colorPalette = statsStaticColors.slice(0, 5);
const params = getStatsParams(
this.config,
@ -66,34 +66,6 @@ export default class TechnicalComponent extends Component {
return (
<div className="gh-stats-piechart-container">
<table>
<thead>
<tr>
<th><span className="gh-stats-data-header">{tableHead}</span></th>
<th><span className="gh-stats-data-header">Visits</span></th>
</tr>
</thead>
<tbody>
{transformedData.map((item, index) => (
<tr key={index}>
<td>
<a
href="#"
onClick={(e) => {
e.preventDefault();
this.navigateToFilter(indexBy, item.name.toLowerCase());
}}
className="gh-stats-data-label"
>
<span style={{backgroundColor: item.color, display: 'inline-block', width: '10px', height: '10px', marginRight: '5px', borderRadius: '2px'}}></span>
{item.name}
</a>
</td>
<td><span className="gh-stats-data-value">{formatNumber(item.value)}</span></td>
</tr>
))}
</tbody>
</table>
<div className="gh-stats-piechart">
<DonutChart
data={data}
@ -135,8 +107,9 @@ export default class TechnicalComponent extends Component {
{
animation: true,
name: tableHead,
padAngle: 1.5,
type: 'pie',
radius: ['60%', '90%'],
radius: ['65%', '90%'],
center: ['50%', '50%'], // Adjusted to align the chart to the top
data: transformedData,
label: {
@ -157,6 +130,34 @@ export default class TechnicalComponent extends Component {
}}
/>
</div>
<table>
<thead>
<tr>
<th><span className="gh-stats-data-header">{tableHead}</span></th>
<th><span className="gh-stats-data-header">Visits</span></th>
</tr>
</thead>
<tbody>
{transformedData.map((item, index) => (
<tr key={index}>
<td>
<a
href="#"
onClick={(e) => {
e.preventDefault();
this.navigateToFilter(indexBy, item.name.toLowerCase());
}}
className="gh-stats-data-label"
>
<span style={{backgroundColor: item.color, display: 'inline-block', width: '10px', height: '10px', marginRight: '5px', borderRadius: '2px'}}></span>
{item.name}
</a>
</td>
<td><span className="gh-stats-data-value">{formatNumber(item.value)}</span></td>
</tr>
))}
</tbody>
</table>
</div>
);
};

View File

@ -3,8 +3,10 @@
<div {{react-render this.ReactComponent props=(hash chartRange=@chartRange audience=@audience device=@device browser=@browser location=@location source=@source pathname=@pathname)}}></div>
</div>
{{#if this.showSeeAll}}
<div class="gh-stats-see-all-container">
<button type="button" class="gh-btn gh-btn-link gh-stats-see-all-btn" {{on "click" (fn this.openSeeAll @chartRange @audience @device @browser @location @source @pathname)}}>
<span>See all &rarr;</span>
</button>
</div>
</div>
{{/if}}

View File

@ -9,12 +9,17 @@ import {barListColor, getCountryFlag, getStatsParams} from 'ghost-admin/utils/st
import {formatNumber} from 'ghost-admin/helpers/format-number';
import {inject} from 'ghost-admin/decorators/inject';
import {inject as service} from '@ember/service';
import {tracked} from '@glimmer/tracking';
const LIMIT = 5;
export default class TopLocations extends Component {
@inject config;
@service modals;
@service router;
@tracked showSeeAll = true;
@action
openSeeAll() {
this.modals.open(AllStatsModal, {
@ -36,6 +41,10 @@ export default class TopLocations extends Component {
this.router.transitionTo({queryParams: newQueryParams});
}
updateSeeAllVisibility(data) {
this.showSeeAll = data && data.length > LIMIT;
}
ReactComponent = (props) => {
const params = getStatsParams(
this.config,
@ -49,9 +58,11 @@ export default class TopLocations extends Component {
params
});
this.updateSeeAllVisibility(data);
return (
<BarList
data={data}
data={data ? data.slice(0, LIMIT) : []}
meta={meta}
error={error}
loading={loading}
@ -68,7 +79,7 @@ export default class TopLocations extends Component {
}}
className="gh-stats-domain"
>
{getCountryFlag(label)} {label || 'Unknown'}
<span title={label || 'Unknown'}>{getCountryFlag(label)} {label || 'Unknown'}</span>
</a>
</span>
)

View File

@ -21,8 +21,10 @@
<div {{react-render this.ReactComponent props=(hash chartRange=@chartRange audience=@audience device=@device browser=@browser location=@location source=@source pathname=@pathname)}}></div>
</div>
{{#if this.showSeeAll}}
<div class="gh-stats-see-all-container">
<button type="button" class="gh-btn gh-btn-link gh-stats-see-all-btn" {{on "click" (fn this.openSeeAll @chartRange @audience @device @browser @location @source @pathname)}}>
<span>See all &rarr;</span>
</button>
</div>
</div>
{{/if}}

View File

@ -11,6 +11,8 @@ import {inject} from 'ghost-admin/decorators/inject';
import {inject as service} from '@ember/service';
import {tracked} from '@glimmer/tracking';
const LIMIT = 7;
export default class TopPages extends Component {
@inject config;
@service modals;
@ -18,6 +20,7 @@ export default class TopPages extends Component {
@tracked contentOption = CONTENT_OPTIONS[0];
@tracked contentOptions = CONTENT_OPTIONS;
@tracked showSeeAll = true;
@action
openSeeAll(chartRange, audience) {
@ -45,11 +48,15 @@ export default class TopPages extends Component {
this.router.transitionTo({queryParams: newQueryParams});
}
updateSeeAllVisibility(data) {
this.showSeeAll = data && data.length > LIMIT;
}
ReactComponent = (props) => {
const params = getStatsParams(
this.config,
props,
{limit: 7}
{limit: LIMIT + 1}
);
const {data, meta, error, loading} = useQuery({
@ -58,9 +65,11 @@ export default class TopPages extends Component {
params
});
this.updateSeeAllVisibility(data);
return (
<BarList
data={data}
data={data ? data.slice(0, LIMIT) : []}
meta={meta}
error={error}
loading={loading}
@ -77,7 +86,7 @@ export default class TopPages extends Component {
}}
className="gh-stats-domain"
>
{label}
<span title={label}>{label}</span>
</a>
</span>
)

View File

@ -22,8 +22,10 @@
<div {{react-render this.ReactComponent props=(hash chartRange=@chartRange audience=@audience device=@device browser=@browser location=@location source=@source pathname=@pathname)}}></div>
</div>
{{#if this.showSeeAll}}
<div class="gh-stats-see-all-container">
<button type="button" class="gh-btn gh-btn-link gh-stats-see-all-btn" {{on "click" (fn this.openSeeAll @chartRange @audience @device @browser @location @source @pathname)}}>
<span>See all &rarr;</span>
</button>
</div>
</div>
{{/if}}

View File

@ -11,6 +11,9 @@ import {inject} from 'ghost-admin/decorators/inject';
import {inject as service} from '@ember/service';
import {tracked} from '@glimmer/tracking';
const LIMIT = 7;
const DEFAULT_ICON_URL = 'https://static.ghost.org/v5.0.0/images/globe-icon.svg';
export default class TopSources extends Component {
@inject config;
@service modals;
@ -18,6 +21,7 @@ export default class TopSources extends Component {
@tracked campaignOption = CAMPAIGN_OPTIONS[0];
@tracked campaignOptions = CAMPAIGN_OPTIONS;
@tracked showSeeAll = true;
@action
onCampaignOptionChange(selected) {
@ -45,6 +49,10 @@ export default class TopSources extends Component {
this.router.transitionTo({queryParams: newQueryParams});
}
updateSeeAllVisibility(data) {
this.showSeeAll = data && data.length > LIMIT;
}
ReactComponent = (props) => {
const {data, meta, error, loading} = useQuery({
endpoint: `${this.config.stats.endpoint}/v0/pipes/top_sources.json`,
@ -56,9 +64,11 @@ export default class TopSources extends Component {
)
});
this.updateSeeAllVisibility(data);
return (
<BarList
data={data}
data={data ? data.slice(0, LIMIT) : []}
meta={meta}
error={error}
loading={loading}
@ -75,8 +85,13 @@ export default class TopSources extends Component {
}}
className="gh-stats-domain"
>
<img src={`https://www.google.com/s2/favicons?domain=${label || 'direct'}&sz=32`} className="gh-stats-favicon" />
{label || 'Direct'}
<img
src={`https://www.faviconextractor.com/favicon/${label || 'direct'}?larger=true`}
className="gh-stats-favicon"
onError={(e) => {
e.target.src = DEFAULT_ICON_URL;
}} />
<span title={label || 'Direct'}>{label || 'Direct'}</span>
</a>
</span>
)

View File

@ -7,7 +7,7 @@
<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>
<div class="gh-stats-all-container" {{react-render this.ReactComponent props=(hash chartRange=this.chartRange audience=this.audience type=this.type)}}></div>
<footer class="modal-footer">
<button

View File

@ -12,6 +12,7 @@ import {inject as service} from '@ember/service';
export default class AllStatsModal extends Component {
@inject config;
@service router;
@service modals;
get type() {
return this.args.data.type;
@ -47,6 +48,7 @@ export default class AllStatsModal extends Component {
params.pathname = label;
}
this.args.close();
this.updateQueryParams(params);
}
@ -121,7 +123,7 @@ export default class AllStatsModal extends Component {
className="gh-stats-favicon"
/>
)}
{label || unknownOption}
<span title={label || unknownOption}>{label || unknownOption}</span>
</a>
</span>
)

View File

@ -1,19 +1,20 @@
<GhBasicDropdown @verticalPosition="below" as |dd|>
<dd.Trigger class="gh-btn gh-btn-icon gh-btn-action-icon" data-test-id="filter-events-button">
<dd.Trigger class="gh-btn gh-btn-icon gh-btn-action-icon gh-stats-btn-audience-filter" data-test-id="filter-events-button">
<span class={{if @excludedAudiences "gh-btn-label-green"}}>
{{svg-jar "members"}}
Audience
<span class="gh-stats-audience-filter-btn-label">Audience</span>
{{svg-jar "arrow-down-small" class="gh-btn-dropdown-arrow"}}
</span>
</dd.Trigger>
<dd.Content class="gh-member-activity-actions-menu dropdown-menu dropdown-triangle-top-right gh-member-activity-actions-menu--suppression">
<dd.Content class="gh-member-activity-actions-menu gh-stats-audience-filter-menu dropdown-menu dropdown-triangle-top-right gh-member-activity-actions-menu--suppression">
<ul class="ember-power-select-options" role="listbox">
{{#each this.audienceTypes as |type idx|}}
<li class="ember-power-select-option mb0 gh-member-activity-actions-menu-item">
<label for="type-{{idx}}">
<span>{{type.name}}</span>
</label>
<div class="for-switch x-small">
<div class="for-switch xxs">
<label class="switch" for="type-{{idx}}">
<input
data-test-id="audience-type-filter-checkbox-{{type.value}}"

View File

@ -1,4 +1,4 @@
<div>
<div class="gh-stats-technical-container">
<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}}>

View File

@ -25,6 +25,7 @@ export default class StatsController extends Controller {
*/
@tracked audience = [];
@tracked excludedAudiences = '';
@tracked showStats = true;
@action
onRangeChange(selected) {
@ -42,6 +43,16 @@ export default class StatsController extends Controller {
this.excludedAudiences = '';
this.audience = this.audienceOptions.map(a => a.value);
}
const excludedArray = this.excludedAudiences.split(',');
this.showStats = this.audienceOptions.length !== excludedArray.length;
}
@action
clearAudienceFilter() {
this.excludedAudiences = '';
this.audience = this.audienceOptions.map(a => a.value);
this.showStats = true;
}
@action

View File

@ -71,7 +71,7 @@
font-weight: 500;
line-height: 1.4em;
transition: none;
border-radius: 3px
border-radius: var(--border-radius);
}
.dropdown-menu li > button:disabled {

View File

@ -69,15 +69,20 @@
border-top: none;
}
.ember-basic-dropdown-trigger--below.ember-power-select-trigger[aria-expanded="true"] {
border-radius: var(--border-radius);
}
.ember-power-select-dropdown.ember-basic-dropdown-content--above {
border-top: 1px solid var(--input-border-color);
border-radius: var(--border-radius) var(--border-radius) 0 0;
border-radius: var(--border-radius);
}
.ember-power-select-option {
margin: 0;
padding: 6px 14px;
color: var(--darkgrey);
height: 32px;
}
.ember-power-select-option[aria-current="true"] {

View File

@ -319,7 +319,7 @@ li.nav-list-item .switch {
padding: 2rem 2.4rem;
}
li.nav-list-item .for-switch.x-small label {
li.nav-list-item .for-switch.xs label {
width: initial;
height: initial !important;
}

View File

@ -316,13 +316,19 @@
display: flex;
align-items: center;
justify-content: space-between;
font-size: 13px;
border-radius: calc(var(--border-radius) - 2px);
}
.gh-member-activity-actions-menu-item:hover {
background: color-mod(var(--whitegrey) a(60%) s(-12%));
}
.gh-member-activity-actions-menu--suppression {
min-width: 260px;
}
.gh-member-activity-actions-menu--suppression.gh-member-activity-actions-menu {
padding: 1rem 0;
padding: 4px 0;
}
.gh-member-activity-actions-menu--suppression.gh-member-activity-actions-menu .ember-power-select-option:first-child {
@ -332,13 +338,13 @@
padding-bottom: 0;
}
.gh-member-activity-actions-menu--suppression.gh-member-activity-actions-menu .for-switch.x-small label {
.gh-member-activity-actions-menu--suppression.gh-member-activity-actions-menu .for-switch.xs label {
width: 34px !important;
height: 20px !important;
padding: 0;
}
.gh-member-activity-actions-menu--suppression.gh-member-activity-actions-menu .for-switch.x-small {
.gh-member-activity-actions-menu--suppression.gh-member-activity-actions-menu .for-switch.xs {
display: flex;
align-items: center;
}
@ -355,7 +361,9 @@
}
.gh-member-activity-actions-menu--suppression.gh-member-activity-actions-menu .ember-power-select-option {
padding: 0.3rem 1.6rem;
padding: 0 1.0rem;
min-height: 32px;
height: 32px;
}
.gh-member-activity-actions-menu--suppression.gh-member-activity-actions-menu .ember-power-select-option label {
@ -365,8 +373,9 @@
.gh-member-activity-actions-menu--suppression .gh-member-activity-actions-menu-divider {
height: 1px;
margin: 1rem 0;
margin: 1rem -4px;
background: var(--lightgrey-l1);
padding: 0;
}
/* End of styles for dropdown for suppressionList feature */
@ -380,6 +389,7 @@
.gh-member-activity-actions-menu .ember-power-select-options[role=listbox] {
max-height: 60vh;
padding: 0 4px;
}
.gh-member-activity-actions-menu .ember-power-select-option {

View File

@ -161,7 +161,7 @@
background: var(--white);
}
.gh-post-history-footer .for-switch.x-small {
.gh-post-history-footer .for-switch.xs {
width: 100%;
}
@ -174,7 +174,7 @@
font-weight: 500;
}
.gh-post-history-footer .for-switch.x-small label {
.gh-post-history-footer .for-switch.xs label {
width: inherit !important;
height: inherit !important;
}

View File

@ -153,7 +153,7 @@
.gh-stats-piechart-container {
display: flex;
gap: 16px;
align-items: flex-start;
align-items: center;
width: 100%;
}
@ -174,7 +174,7 @@
width: 100%;
height: 100%;
flex-grow: 1;
margin-right: -20px;
margin-left: -20px;
}
.gh-stats-section-dropdown {
@ -261,6 +261,17 @@ a.gh-stats-data-label:hover {
z-index: 9999;
}
.gh-stats-all-container {
max-height: calc(100vh - 12vw - 172px);
overflow: auto;
margin: -4px -32px;
padding: 4px 32px;
}
.gh-stats-all-container > div > div {
overflow-y: unset !important;
}
.gh-stats-kpis-chart-container {
margin-top: -20px;
}
@ -270,6 +281,7 @@ a.gh-stats-data-label:hover {
align-items: center;
gap: 6px;
color: var(--black);
line-height: 1.3em;
}
.gh-stats-domain:hover {
@ -281,6 +293,13 @@ a.gh-stats-data-label:hover {
height: 16px;
}
.gh-stats-domain span {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
.gh-stats-tooltip-header {
font-weight: 600;
font-size: 13px;
@ -328,9 +347,11 @@ a.gh-stats-data-label:hover {
align-items: center;
font-size: 13px;
font-weight: 500;
border: 1px solid var(--whitegrey);
/* border: 1px solid var(--purple); */
border: 1px solid rgba(142,68,254,0.75);
border-radius: 999px;
padding: 4px 12px;
background-color: rgba(142,68,254,0.06);
}
.gh-stats-filter-pill .value {
@ -363,4 +384,70 @@ a.gh-stats-data-label:hover {
.gh-btn-clear-filters svg path {
stroke-width: 2px;
}
.gh-stats-audience-filter-menu {
max-width: 240px;
min-width: 220px;
}
.gh-stats-placeholder {
width: 60px;
color: var(--lightgrey);
margin-bottom: -8px;
}
.gh-stats-technical-container {
height: 100%;
display: flex;
flex-direction: column;
}
.gh-stats-technical-data {
flex-grow: 1;
display: flex;
flex-direction: row;
align-items: center;
}
.gh-stats-audience-filter-btn-label {
padding: 0 !important;
margin: 0 !important;
}
@media (max-width: 1440px) {
.gh-stats-filters {
margin-bottom: 0;
}
}
@media (max-width: 1140px) {
.gh-stats-grid.cols-2 {
grid-template-columns: 1fr;
}
.gh-stats-tab.is-selected:before {
display: none;
}
.gh-stats-tab.is-selected {
border-bottom: 2px solid var(--black);
}
}
@media (max-width: 990px) {
.gh-stats-tabs {
max-width: 100%;
overflow: auto;
}
}
@media (max-width: 390px) {
.gh-stats-audience-filter-btn-label {
display: none !important;
}
.gh-stats-btn-audience-filter svg {
margin-right: 0 !important;
}
}

View File

@ -377,6 +377,13 @@ svg.gh-btn-icon-right {
width: 100%;
}
.gh-btn-dropdown-arrow {
margin-left: 5px !important;
margin-right: -4px !important;
height: 6px !important;
width: auto !important;
}
/*
/* Button Variations

View File

@ -540,17 +540,12 @@ textarea {
display: inline-block;
}
.for-switch label:not(.x-small .switch),
.for-switch label:not(.xs .switch):not(.xxs .switch),
.for-switch .container {
width: 50px !important;
height: 28px !important;
}
.for-switch.x-small label {
width: 34px !important;
height: 20px !important;
}
.for-switch label p,
.for-switch .container p {
overflow: auto;
@ -576,7 +571,7 @@ textarea {
width: 48px !important;
height: 26px !important;
border-radius: 999px;
transition: background 0.15s ease-in-out, border-color 0.15s ease-in-out;
transition: all 0.15s ease-in-out;
}
.for-switch label:hover input:not(:checked) + .input-toggle-component,
@ -595,6 +590,7 @@ textarea {
transition: .3s;
box-shadow: 0 1px 3px rgba(0,0,0,.15);
border-radius: 999px;
transition: all 0.15s ease-in-out;
}
.for-switch input:checked + .input-toggle-component {
@ -625,22 +621,47 @@ textarea {
.for-switch.small input:checked + .input-toggle-component:before {
transform: translateX(16px);
transition: all 0.15s ease-in-out;
}
.for-switch.x-small .input-toggle-component {
.for-switch.xs label {
width: 34px !important;
height: 20px !important;
}
.for-switch.x-small .input-toggle-component:before {
.for-switch.xs .input-toggle-component {
width: 34px !important;
height: 20px !important;
}
.for-switch.xs .input-toggle-component:before {
height: 16px !important;
width: 16px !important;
}
.for-switch.x-small input:checked + .input-toggle-component:before {
.for-switch.xs input:checked + .input-toggle-component:before {
transform: translateX(14px);
}
.for-switch.xxs label {
width: 28px !important;
height: 16px !important;
}
.for-switch.xxs .input-toggle-component {
width: 28px !important;
height: 16px !important;
}
.for-switch.xxs .input-toggle-component:before {
height: 12px !important;
width: 12px !important;
}
.for-switch.xxs input:checked + .input-toggle-component:before {
transform: translateX(12px);
}
.for-switch.disabled {
opacity: 0.5;
pointer-events: none;

View File

@ -61,6 +61,8 @@
</GhCanvasHeader>
<section class="view-container">
{{#if this.showStats}}
<section class="gh-stats-container no-gap">
<Stats::KpisOverview
@chartRange={{this.chartRange}}
@ -121,6 +123,18 @@
/>
</div>
</section>
{{else}}
<div class="no-posts-box">
<div class="no-posts">
{{svg-jar "stats-placeholder" class="gh-stats-placeholder"}}
<h4>No stats available for the current filter</h4>
<button type="button" class="gh-btn" {{on "click" this.clearAudienceFilter}}>
<span>See all stats</span>
</button>
</div>
</div>
No audience selected
{{/if}}
</section>

View File

@ -111,7 +111,7 @@ export function generateMonochromePalette(baseColor, count = 10) {
export const barListColor = '#F1F3F4';
export const statsStaticColors = [
'#8E42FF', '#B07BFF', '#C7A0FF', '#DDC6FF', '#EBDDFF', '#F7EDFF'
'#A568FF', '#7B7BFF', '#B3CEFF', '#D4ECF7', '#EFFDFD', '#F7F7F7'
];
export const getCountryFlag = (countryCode) => {

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-1 -1 48 48" id="Analytics-Board-Graph-Line--Streamline-Ultimate" height="48" width="48"><desc>Analytics Board Graph Line Streamline Icon: https://streamlinehq.com</desc><defs></defs><title>analytics-board-graph-line</title><path d="M4.3125 7.1875h37.375s2.875 0 2.875 2.875v25.875s0 2.875 -2.875 2.875H4.3125s-2.875 0 -2.875 -2.875V10.0625s0 -2.875 2.875 -2.875" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></path><path d="M7.1875 28.75 13.225000000000001 18.6875 18.6875 28.75l5.75 -5.75 5.75 5.75 8.625 -12.9375" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></path></svg>

After

Width:  |  Height:  |  Size: 730 B