first commit
Some checks failed
Build / run (push) Has been cancelled

This commit is contained in:
maher
2025-10-29 11:42:25 +01:00
commit 703f50a09d
4595 changed files with 385164 additions and 0 deletions

View File

@@ -0,0 +1,110 @@
import {
LocationDatasetItem,
ReportMetric,
} from '@common/admin/analytics/report-metric';
import React, {useMemo, useRef} from 'react';
import {useGoogleGeoChart} from './use-google-geo-chart';
import {ChartLayout, ChartLayoutProps} from '@common/charts/chart-layout';
import {Trans} from '@common/i18n/trans';
import {ChartLoadingIndicator} from '@common/charts/chart-loading-indicator';
import {Button} from '@common/ui/buttons/button';
import {ArrowBackIcon} from '@common/icons/material/ArrowBack';
import clsx from 'clsx';
import {InfoDialogTrigger} from '@common/ui/overlays/dialog/info-dialog-trigger/info-dialog-trigger';
import {FormattedCountryName} from '@common/i18n/formatted-country-name';
interface GeoChartData extends Partial<ChartLayoutProps> {
data?: ReportMetric<LocationDatasetItem>;
onCountrySelected?: (countryCode: string | undefined) => void;
country?: string;
}
export function GeoChart({
data: metricData,
isLoading,
onCountrySelected,
country,
...layoutProps
}: GeoChartData) {
const placeholderRef = useRef<HTMLDivElement>(null);
const regionInteractivity = !!onCountrySelected;
// memo data to avoid redrawing chart on rerender
const initialData = metricData?.datasets[0].data;
const data = useMemo(() => {
return initialData || [];
}, [initialData]);
useGoogleGeoChart({placeholderRef, data, country, onCountrySelected});
return (
<ChartLayout
{...layoutProps}
className="min-w-500"
title={
<div className="flex items-center">
<Trans message="Top Locations" />
{country ? (
<span className="pl-4">
({<FormattedCountryName code={country} />})
</span>
) : null}
{regionInteractivity && <InfoTrigger />}
</div>
}
contentIsFlex={isLoading}
>
{isLoading && <ChartLoadingIndicator />}
<div className="flex gap-24">
<div
ref={placeholderRef}
className="flex-auto w-[480px] min-h-[340px]"
/>
<div className="w-[170px]">
<div className="text-sm max-h-[340px] w-full flex-initial overflow-y-auto">
{data.map(location => (
<div
key={location.label}
className={clsx(
'flex items-center gap-4 mb-4',
regionInteractivity && 'cursor-pointer hover:underline'
)}
role={regionInteractivity ? 'button' : undefined}
onClick={() => {
onCountrySelected?.(location.code);
}}
>
<div className="max-w-110 whitespace-nowrap overflow-hidden overflow-ellipsis">
{location.label}
</div>
<div>({location.percentage})%</div>
</div>
))}
</div>
{country && (
<Button
variant="outline"
size="xs"
className="mt-14"
startIcon={<ArrowBackIcon />}
onClick={() => {
onCountrySelected?.(undefined);
}}
>
<Trans message="Back to countries" />
</Button>
)}
</div>
</div>
</ChartLayout>
);
}
function InfoTrigger() {
return (
<InfoDialogTrigger
title={<Trans message="Zooming in" />}
body={
<Trans message="Click on a country inside the map or country list to zoom in and see city data for that country." />
}
/>
);
}

View File

@@ -0,0 +1,116 @@
import lazyLoader from '../../../utils/http/lazy-loader';
import {useSettings} from '@common/core/settings/use-settings';
import {RefObject, useCallback, useEffect, useRef} from 'react';
import {useThemeSelector} from '@common/ui/themes/theme-selector-context';
import {themeValueToHex} from '@common/ui/themes/utils/theme-value-to-hex';
import {useTrans} from '@common/i18n/use-trans';
import {message} from '@common/i18n/message';
import {LocationDatasetItem} from '@common/admin/analytics/report-metric';
const loaderUrl = 'https://www.gstatic.com/charts/loader.js';
interface UseGoogleGeoChartProps {
placeholderRef: RefObject<HTMLDivElement>;
data: LocationDatasetItem[];
onCountrySelected?: (countryCode: string) => void;
country?: string;
}
export function useGoogleGeoChart({
placeholderRef,
data,
country,
onCountrySelected,
}: UseGoogleGeoChartProps) {
const {trans} = useTrans();
const {analytics} = useSettings();
const apiKey = analytics?.gchart_api_key;
const {selectedTheme} = useThemeSelector();
const geoChartRef = useRef<google.visualization.GeoChart>();
// only allow selecting countries, not cities
const regionInteractivity = !!onCountrySelected && !country;
const drawGoogleChart = useCallback(() => {
if (typeof google === 'undefined') return;
const seedData = data.map(location => [location.label, location.value]);
seedData.unshift([
country ? trans(message('City')) : trans(message('Country')),
trans(message('Clicks')),
]);
const backgroundColor = `${themeValueToHex(
selectedTheme.values['--be-paper'],
)}`;
const chartColor = `${themeValueToHex(
selectedTheme.values['--be-primary'],
)}`;
const options: google.visualization.GeoChartOptions = {
colorAxis: {colors: [chartColor]},
backgroundColor,
region: country ? country.toUpperCase() : undefined,
resolution: country ? 'provinces' : 'countries',
displayMode: country ? 'markers' : 'regions',
enableRegionInteractivity: regionInteractivity,
};
if (
!geoChartRef.current &&
placeholderRef.current &&
google?.visualization?.GeoChart
) {
geoChartRef.current = new google.visualization.GeoChart(
placeholderRef.current,
);
}
geoChartRef.current?.draw(
google.visualization.arrayToDataTable(seedData),
options,
);
}, [
selectedTheme,
data,
placeholderRef,
trans,
country,
regionInteractivity,
]);
const initGoogleGeoChart = useCallback(async () => {
if (lazyLoader.isLoadingOrLoaded(loaderUrl)) return;
await lazyLoader.loadAsset(loaderUrl, {type: 'js', id: 'google-charts-js'});
await google.charts.load('current', {
packages: ['geochart'],
mapsApiKey: apiKey,
});
drawGoogleChart();
}, [apiKey, drawGoogleChart]);
useEffect(() => {
if (geoChartRef.current && onCountrySelected) {
google.visualization.events.addListener(
geoChartRef.current,
'regionClick',
(a: {region: string}) => onCountrySelected?.(a.region),
);
}
return () => {
if (geoChartRef.current) {
google.visualization.events.removeAllListeners(geoChartRef.current);
}
};
// this will correctly run when geochart instance is set on ref
}, [onCountrySelected, geoChartRef.current]);
// on component load: load chart library then draw, otherwise just draw
useEffect(() => {
initGoogleGeoChart();
}, [initGoogleGeoChart]);
// redraw chart if data or theme changes
useEffect(() => {
drawGoogleChart();
}, [selectedTheme, drawGoogleChart, data]);
return {drawGoogleChart};
}