Files
com.blitzwallet/app/screens/inAccount/explorePage.js
T
2026-01-19 11:50:58 -05:00

442 lines
13 KiB
JavaScript

import { ThemeText } from '../../functions/CustomElements';
import React, { useEffect, useMemo, useState } from 'react';
import {
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
import GetThemeColors from '../../hooks/themeColors';
import { BLITZ_GOAL_USER_COUNT, CENTER, COLORS, SIZES } from '../../constants';
import DateCountdown from '../../components/admin/homeComponents/explore/dateCountdown';
import {
DAY_IN_MILLS,
MONTH_GROUPING,
MONTH_IN_MILLS,
WEEK_IN_MILLS,
WEEK_OPTIONS,
YEAR_IN_MILLS,
} from '../../components/admin/homeComponents/explore/constants';
import {
formatBalanceAmount,
getLocalStorageItem,
setLocalStorageItem,
} from '../../functions';
import { useGlobalContextProvider } from '../../../context-store/context';
import NoDataView from '../../components/admin/homeComponents/explore/noDataView';
import { FONT, INSET_WINDOW_WIDTH } from '../../constants/theme';
import { useGlobalThemeContext } from '../../../context-store/theme';
import { findLargestByVisualWidth } from '../../components/admin/homeComponents/explore/largestNumber';
import { useTranslation } from 'react-i18next';
import ModernLineChart from '../../functions/CustomElements/modernLineChart';
import FullLoadingScreen from '../../functions/CustomElements/loadingScreen';
import { shouldLoadExploreData } from '../../functions/initializeUserSettingsHelpers';
import fetchBackend from '../../../db/handleBackend';
import { useKeysContext } from '../../../context-store/keys';
import {
useServerTime,
useServerTimeOnly,
} from '../../../context-store/serverTime';
import { formatLocalTimeNumericMonthDay } from '../../functions/timeFormatter';
export default function ExploreUsers() {
const { contactsPrivateKey, publicKey } = useKeysContext();
const [timeFrame, setTimeFrame] = useState('day');
const [isLoading, setIsLoading] = useState(true);
const { masterInfoObject, toggleMasterInfoObject } =
useGlobalContextProvider();
const { backgroundOffset, textColor, backgroundColor } = GetThemeColors();
const { theme, darkModeType } = useGlobalThemeContext();
const { t } = useTranslation();
const [yAxisWidth, setYAxisWidth] = useState(0);
const [chartWidth, setChartWidth] = useState(0);
const dataObject = masterInfoObject.exploreData
? JSON.parse(JSON.stringify(masterInfoObject.exploreData))
: false;
const data = dataObject ? dataObject[timeFrame].reverse() : [];
const getServerTime = useServerTimeOnly();
const currentTime = getServerTime();
const currentTimeZoneOffsetInHours = -6;
const formattedCurrentTime = useMemo(() => {
// Convert to target timezone (UTC-6) by adding offset in milliseconds
const targetTimezoneMs =
currentTime + currentTimeZoneOffsetInHours * 60 * 60 * 1000;
const targetDate = new Date(targetTimezoneMs);
targetDate.setUTCHours(12, 0, 0, 0); // Set to 12pm of the current day
return targetDate.getTime();
}, [currentTime]);
const min = data.reduce((prev, current) => {
if (current?.value < prev) return current.value;
else return prev;
}, BLITZ_GOAL_USER_COUNT);
const max = data.reduce((prev, current) => {
if (current?.value > prev) return current.value;
else return prev;
}, 0);
const totalYesterday = masterInfoObject.exploreData?.['day']?.[1]?.value || 0;
const largestNumber = useMemo(() => {
return findLargestByVisualWidth(
Math.round(min * 0.95),
Math.round(max * 1.05),
7,
);
}, [min, max]);
const timeFrameElements = useMemo(() => {
return ['day', 'week', 'month', 'year'].map(item => {
return (
<TouchableOpacity
key={item}
onPress={() => setTimeFrame(item)}
style={{
backgroundColor:
item === timeFrame
? theme && darkModeType
? COLORS.darkModeText
: COLORS.primary
: 'transparent',
borderColor:
theme && darkModeType ? COLORS.darkModeText : COLORS.primary,
...styles.timeFrameElement,
}}
>
<ThemeText
styles={{
color:
item === timeFrame
? theme && darkModeType
? COLORS.lightModeText
: COLORS.darkModeText
: textColor,
...styles.timeFrameElementText,
}}
content={t(`constants.${item}`)}
/>
</TouchableOpacity>
);
});
}, [timeFrame, textColor, theme, darkModeType]);
const xLabels = useMemo(() => {
return [0, 1, 2, 3, 4, 5, 6].map((_, index) => {
if (timeFrame === 'year') {
const now = formattedCurrentTime;
return `${new Date(
now - YEAR_IN_MILLS * Math.abs(6 - index),
).getFullYear()}`;
} else if (timeFrame === 'month') {
const now = formattedCurrentTime;
const dateIndex = new Date(
now - MONTH_IN_MILLS * Math.abs(6 - index),
).getMonth();
return t(`months.${MONTH_GROUPING[dateIndex]}`).slice(0, 3);
} else if (timeFrame === 'day') {
const now = formattedCurrentTime - DAY_IN_MILLS;
const dateIndex = new Date(
now - DAY_IN_MILLS * Math.abs(7 - index),
).getDay();
return t(`weekdays.${WEEK_OPTIONS[dateIndex]}`).slice(0, 3);
} else {
const now = new Date(formattedCurrentTime);
const todayDay = now.getDay();
const daysToSunday = 7 - (todayDay === 0 ? 7 : todayDay);
const endOfWeek = new Date(now.getTime() + daysToSunday * DAY_IN_MILLS);
const dateIndex = new Date(
endOfWeek - WEEK_IN_MILLS * Math.abs(6 - index),
);
return formatLocalTimeNumericMonthDay(dateIndex);
}
});
}, [timeFrame, t, formattedCurrentTime]);
useEffect(() => {
let mounted = true;
async function loadExploreData() {
try {
const pastExploreData = await getLocalStorageItem(
'savedExploreData',
).then(data => JSON.parse(data));
const shouldLoadExporeDataResp = shouldLoadExploreData(
pastExploreData,
currentTime,
);
if (masterInfoObject.exploreData && !shouldLoadExporeDataResp) return;
if (!shouldLoadExporeDataResp) {
if (!mounted) return;
toggleMasterInfoObject({ exploreData: pastExploreData.data });
throw new Error('Blocking call since data is up to date');
}
const freshExploreData = await fetchBackend(
'getTotalUserCount',
{ data: publicKey },
contactsPrivateKey,
publicKey,
);
if (freshExploreData) {
if (!mounted) return;
toggleMasterInfoObject({ exploreData: freshExploreData });
await setLocalStorageItem(
'savedExploreData',
JSON.stringify({
lastUpdated: currentTime,
data: freshExploreData,
}),
);
}
} catch (err) {
console.log(err);
} finally {
setIsLoading(false);
}
}
loadExploreData();
return () => {
mounted = false;
};
}, []);
if (isLoading) {
return <FullLoadingScreen />;
}
if (
!masterInfoObject.exploreData ||
!Object.keys(masterInfoObject.exploreData).length
) {
return <NoDataView />;
}
return (
<ScrollView
showsVerticalScrollIndicator={false}
contentContainerStyle={styles.scrollView}
>
<Text
onLayout={event => {
setYAxisWidth(Math.round(event.nativeEvent.layout.width));
}}
style={styles.sizingText}
>
{largestNumber}
</Text>
<ThemeText
styles={styles.statsCardHeader}
content={t('screens.inAccount.explorePage.title')}
/>
{/* Today's Stats Card */}
<View style={{ ...styles.todayCard, backgroundColor: backgroundOffset }}>
<View style={styles.cardHeader}>
<ThemeText
CustomNumberOfLines={1}
styles={{ ...styles.cardLabel, color: textColor }}
content={t('constants.today')}
/>
<DateCountdown
currentTimeZoneOffsetInHours={currentTimeZoneOffsetInHours}
getServerTime={getServerTime}
/>
</View>
<ThemeText
styles={{ ...styles.mainNumber, color: textColor }}
content={formatBalanceAmount(max, undefined, masterInfoObject)}
/>
<ThemeText
styles={{ ...styles.subText, color: textColor }}
content={t('screens.inAccount.explorePage.goalUserCount', {
goalUserCount: formatBalanceAmount(
BLITZ_GOAL_USER_COUNT,
undefined,
masterInfoObject,
),
})}
/>
{/* Progress Bar */}
<View
style={{
backgroundColor: backgroundColor,
...styles.progressBarContainer,
}}
>
<View
style={{
width: `${(max / BLITZ_GOAL_USER_COUNT) * 100}%`,
backgroundColor:
theme && darkModeType ? COLORS.darkModeText : COLORS.primary,
...styles.progressBarFill,
}}
/>
</View>
<ThemeText
styles={{ ...styles.percentageText, color: textColor }}
content={t('screens.inAccount.explorePage.percentOfGoal', {
goalPercent: ((max / BLITZ_GOAL_USER_COUNT) * 100).toFixed(2),
})}
/>
</View>
{/* Growth Indicator */}
<View style={styles.growthIndicator}>
<ThemeText
styles={{ ...styles.growthText, color: textColor }}
content={t('screens.inAccount.explorePage.numAddedUsers', {
numNewUsers: formatBalanceAmount(
max - totalYesterday,
false,
masterInfoObject,
),
})}
/>
</View>
{/* Chart */}
<View
onLayout={e => {
setChartWidth(e.nativeEvent.layout.width);
}}
style={styles.chartContainer}
>
<ModernLineChart
data={data.map(d => d.value)}
width={chartWidth}
leftPadding={yAxisWidth * 1.1}
height={250}
min={Math.round(min * 0.95)}
max={Math.round(max * (timeFrame !== 'day' ? 1.2 : 1.05))}
xLabels={xLabels}
strokeColor={
theme && darkModeType ? COLORS.darkModeText : COLORS.primary
}
textColor={textColor}
showGradient={true}
showGrid={true}
showDots={true}
strokeWidth={2.5}
/>
</View>
{/* Time Frame Buttons */}
<View style={styles.timeFrameElementsContainer}>{timeFrameElements}</View>
</ScrollView>
);
}
const styles = StyleSheet.create({
scrollView: {
width: INSET_WINDOW_WIDTH,
paddingTop: 20,
...CENTER,
},
sizingText: {
fontSize: SIZES.small,
fontFamily: FONT.Title_Regular,
position: 'absolute',
zIndex: -1,
opacity: 0,
},
statsCardHeader: {
marginBottom: 12,
textAlign: 'center',
},
todayCard: {
borderRadius: 10,
padding: 16,
marginBottom: 12,
},
cardHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 12,
flexWrap: 'wrap',
gap: 8,
},
cardLabel: {
fontSize: SIZES.small,
opacity: 0.7,
textTransform: 'uppercase',
includeFontPadding: false,
},
mainNumber: {
fontSize: SIZES.xxLarge,
// fontWeight: '500',
// marginBottom: 12,
includeFontPadding: false,
},
subText: {
fontSize: SIZES.smedium,
opacity: 0.6,
marginBottom: 16,
includeFontPadding: false,
},
progressBarContainer: {
height: 25,
borderRadius: 15,
overflow: 'hidden',
marginBottom: 8,
},
progressBarFill: {
height: '100%',
borderRadius: 15,
},
percentageText: {
fontSize: SIZES.smedium,
textAlign: 'center',
includeFontPadding: false,
},
growthIndicator: {
alignItems: 'center',
// paddingVertical: 8,
},
growthText: {
fontSize: SIZES.smedium,
// fontWeight: '500',
opacity: 0.7,
includeFontPadding: false,
},
chartContainer: {
height: 250,
flexDirection: 'row',
marginBottom: 12,
},
timeFrameElementsContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
alignContent: 'center',
width: '100%',
rowGap: 10,
columnGap: 10,
flexWrap: 'wrap',
...CENTER,
},
timeFrameElement: {
flexGrow: 1,
borderRadius: 8,
borderWidth: 2,
alignItems: 'center',
justifyContent: 'center',
},
timeFrameElementText: {
textTransform: 'capitalize',
includeFontPadding: false,
padding: 10,
},
});