Ghost/ghost/admin/app/utils/stats.js
Peter Zimon 8aaac5abe1
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
2024-10-01 17:51:55 +02:00

168 lines
4.7 KiB
JavaScript

import moment from 'moment-timezone';
export const RANGE_OPTIONS = [
{name: 'Last 24 hours', value: 1},
{name: 'Last 7 days', value: 7},
{name: 'Last 30 days', value: 30 + 1},
{name: 'Last 3 months', value: 90 + 1},
{name: 'Year to date', value: 365 + 1},
{name: 'Last 12 months', value: 12 * (30 + 1)},
{name: 'All time', value: 1000}
];
export const CONTENT_OPTIONS = [
{name: 'Posts & pages', value: 'all'},
{name: 'Posts', value: 'posts'},
{name: 'Pages', value: 'pages'}
];
export const CAMPAIGN_OPTIONS = [
{name: 'All campaigns', value: 'all'},
{name: 'UTM Medium', value: 'utm-medium'},
{name: 'UTM Source', value: 'utm-source'},
{name: 'UTM Campaign', value: 'utm-campaign'},
{name: 'UTM Content', value: 'utm-content'},
{name: 'UTM Term', value: 'utm-term'}
];
export const AUDIENCE_TYPES = [
{name: 'Logged out visitors', value: 'undefined'},
{name: 'Free members', value: 'free'},
{name: 'Paid members', value: 'paid'}
];
export function hexToRgba(hex, alpha = 1) {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
}
export function generateMonochromePalette(baseColor, count = 10) {
// Convert hex to RGB
let r = parseInt(baseColor.slice(1, 3), 16);
let g = parseInt(baseColor.slice(3, 5), 16);
let b = parseInt(baseColor.slice(5, 7), 16);
// Convert RGB to HSL
r /= 255, g /= 255, b /= 255;
let max = Math.max(r, g, b), min = Math.min(r, g, b);
let h, s, l = (max + min) / 2;
if (max === min) {
h = s = 0; // achromatic
} else {
let d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
h /= 6;
}
// Generate palette
let palette = [];
for (let i = 0; i < count; i++) {
// Adjust the range based on the base color's lightness
let rangeStart, rangeEnd;
if (l < 0.5) {
// For darker base colors
rangeStart = 0.1;
rangeEnd = 0.7;
} else {
// For lighter base colors
rangeStart = 0.3;
rangeEnd = 0.9;
}
let newL = rangeStart + (i / (count - 1)) * (rangeEnd - rangeStart);
// Convert back to RGB
let c = (1 - Math.abs(2 * newL - 1)) * s;
let x = c * (1 - Math.abs((h * 6) % 2 - 1));
let m = newL - c / 2;
if (0 <= h && h < 1 / 6) {
[r, g, b] = [c, x, 0];
} else if (1 / 6 <= h && h < 2 / 6) {
[r, g, b] = [x, c, 0];
} else if (2 / 6 <= h && h < 3 / 6) {
[r, g, b] = [0, c, x];
} else if (3 / 6 <= h && h < 4 / 6) {
[r, g, b] = [0, x, c];
} else if (4 / 6 <= h && h < 5 / 6) {
[r, g, b] = [x, 0, c];
} else {
[r, g, b] = [c, 0, x];
}
r = Math.round((r + m) * 255);
g = Math.round((g + m) * 255);
b = Math.round((b + m) * 255);
palette.push(`#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`);
}
return palette;
}
export const barListColor = '#F1F3F4';
export const statsStaticColors = [
'#A568FF', '#7B7BFF', '#B3CEFF', '#D4ECF7', '#EFFDFD', '#F7F7F7'
];
export const getCountryFlag = (countryCode) => {
if (!countryCode) {
return '🏳️';
}
return countryCode.toUpperCase().replace(/./g, char => String.fromCodePoint(char.charCodeAt(0) + 127397)
);
};
export function getDateRange(chartRange) {
const endDate = moment().endOf('day');
const startDate = moment().subtract(chartRange - 1, 'days').startOf('day');
return {startDate, endDate};
}
export function getStatsParams(config, props, additionalParams = {}) {
const {chartRange, audience, device, browser, location, source, pathname} = props;
const {startDate, endDate} = getDateRange(chartRange);
const params = {
site_uuid: config.stats.id,
date_from: startDate.format('YYYY-MM-DD'),
date_to: endDate.format('YYYY-MM-DD'),
...additionalParams
};
if (audience.length > 0) {
params.member_status = audience.join(',');
}
if (device) {
params.device = device;
}
if (browser) {
params.browser = browser;
}
if (location) {
params.location = location;
}
if (source) {
params.source = source === 'direct' ? '' : source;
}
if (pathname) {
params.pathname = pathname;
}
return params;
}