feat: stats by indexed db + tab hours clock

This commit is contained in:
alexsparkes
2024-11-03 22:01:35 +00:00
parent ca0b4820e7
commit 2e2c8b41da
14 changed files with 508 additions and 117 deletions

View File

@@ -7,6 +7,7 @@ import { loadSettings } from 'utils/settings';
import EventBus from 'utils/eventbus';
import variables from 'config/variables';
import Preview from 'features/helpers/preview/Preview';
import Stats from 'features/stats/api/stats';
import Welcome from 'features/welcome/Welcome';
@@ -28,8 +29,6 @@ const useAppSetup = () => {
EventBus.on('refresh', refreshHandler);
variables.stats.postEvent('tabs-opened');
return () => {
EventBus.off('refresh', refreshHandler);
};
@@ -52,6 +51,9 @@ const App = () => {
if (storedBackground === 'true' || storedBackground === true) {
setShowBackground(true);
}
// Post event immediately when the component mounts
Stats.postEvent('new-tab', 'tab', 'opened');
}, []);
useAppSetup();

View File

@@ -3,13 +3,16 @@ import variables from 'config/variables';
import Tab from './Tab';
import { useTab } from './TabContext';
import { MdOutlineWarning, MdRefresh, MdClose } from 'react-icons/md';
import Stats from 'features/stats/api/stats';
const Sidebar = memo(({ sections, currentTab, setCurrentTab }) => {
const { subTab, setSubTab, setSubSection } = useTab();
const handleClick = useCallback(
(label) => () => {
setSubTab(variables.getMessage(label));
const newTab = variables.getMessage(label);
setSubTab(newTab);
setSubSection('');
Stats.postEvent('settings-tab', newTab, 'opened');
},
[setSubTab, setSubSection],
);

View File

@@ -16,6 +16,7 @@ import './scss/index.scss';
import { decodeBlurHash } from 'fast-blurhash';
import defaults from './options/default';
import Stats from 'features/stats/api/stats';
export default class Background extends PureComponent {
constructor() {
@@ -71,9 +72,11 @@ export default class Background extends PureComponent {
this.blob = URL.createObjectURL(await (await fetch(url)).blob());
backgroundImage.classList.add('backgroundTransform');
backgroundImage.style.backgroundImage = `url(${this.blob})`;
Stats.postEvent('feature', 'background-image', 'shown');
} else {
// custom colour
backgroundImage.setAttribute('style', this.state.style);
Stats.postEvent('background', 'colour', 'set');
}
}
@@ -121,6 +124,7 @@ export default class Background extends PureComponent {
} catch (e) {
// if requesting to the API fails, we get an offline image
this.setState(getOfflineImage('api'));
Stats.postEvent('background', 'image', 'offline');
return null;
}
@@ -182,6 +186,7 @@ export default class Background extends PureComponent {
url,
},
});
Stats.postEvent('background', 'favourite', 'set');
};
const favourited = JSON.parse(localStorage.getItem('favourite'));
@@ -193,7 +198,9 @@ export default class Background extends PureComponent {
switch (type) {
case 'api':
if (offline) {
return this.setState(getOfflineImage('api'));
this.setState(getOfflineImage('api'));
Stats.postEvent('background', 'image', 'offline');
return;
}
// API background
@@ -206,6 +213,7 @@ export default class Background extends PureComponent {
'nextImage',
JSON.stringify(await this.getAPIImageData(data.photoInfo.pun)),
); // pre-fetch data about the next image
Stats.postEvent('background', 'image', 'api');
}
break;
@@ -227,11 +235,13 @@ export default class Background extends PureComponent {
type: 'colour',
style: `background: ${customBackgroundColour || 'rgb(0,0,0)'}`,
});
Stats.postEvent('background', 'colour', 'custom');
break;
case 'random_colour':
case 'random_gradient':
this.setState(randomColourStyleBuilder(type));
Stats.postEvent('background', 'colour', 'random');
break;
case 'custom':
let customBackground = [];
@@ -251,7 +261,9 @@ export default class Background extends PureComponent {
// allow users to use offline images
if (offline && !customBackground.startsWith('data:')) {
return this.setState(getOfflineImage('custom'));
this.setState(getOfflineImage('custom'));
Stats.postEvent('background', 'image', 'offline');
return;
}
if (
@@ -271,12 +283,15 @@ export default class Background extends PureComponent {
this.setState(object);
localStorage.setItem('currentBackground', JSON.stringify(object));
Stats.postEvent('background', 'image', 'custom');
}
break;
case 'photo_pack':
if (offline) {
return this.setState(getOfflineImage('photo'));
this.setState(getOfflineImage('photo'));
Stats.postEvent('background', 'image', 'offline');
return;
}
const photoPack = [];
@@ -292,7 +307,9 @@ export default class Background extends PureComponent {
});
if (photoPack.length === 0) {
return this.setState(getOfflineImage('photo'));
this.setState(getOfflineImage('photo'));
Stats.postEvent('background', 'image', 'offline');
return;
}
const photo = photoPack[Math.floor(Math.random() * photoPack.length)];
@@ -306,6 +323,7 @@ export default class Background extends PureComponent {
photographer: photo.photographer,
},
});
Stats.postEvent('background', 'image', 'photo_pack');
break;
default:
break;

View File

@@ -3,6 +3,7 @@ import { useRef, useState, useEffect } from 'react';
import { appendNth, convertTimezone } from 'utils/date';
import EventBus from 'utils/eventbus';
import Stats from 'features/stats/api/stats';
import defaults from './options/default';
import './greeting.scss';
@@ -117,6 +118,9 @@ function Greeting() {
// Set the state to the greeting string
setGreeting(`${message}${name}`);
// Log the event when a greeting is shown
Stats.postEvent('feature', 'greeting', 'shown');
getGreeting();
}, time);
};

View File

@@ -32,7 +32,7 @@ const ItemPage = () => {
}, [installedItems, selectedItem.name]);
const locale = localStorage.getItem('language');
const shortLocale = locale.includes('-') ? locale.split('-')[0] : locale;
const shortLocale = locale && locale.includes('-') ? locale.split('-')[0] : locale || 'en';
let languageNames = new Intl.DisplayNames([shortLocale], { type: 'language' });
let dateObj, formattedDate;

View File

@@ -88,7 +88,6 @@ const Overview = () => {
useEffect(() => {
localStorage.setItem('order', JSON.stringify(items));
variables.stats.postEvent('setting', 'widget-order');
EventBus.emit('refresh', 'widgets');
}, [items]);

View File

@@ -10,6 +10,7 @@ import {
} from 'react-icons/md';
import { toast } from 'react-toastify';
import Stats from 'features/stats/api/stats';
import { Tooltip } from 'components/Elements';
@@ -105,6 +106,7 @@ class Quote extends PureComponent {
authorlink: this.getAuthorLink(quote.author),
authorimg: '',
});
Stats.postEvent('feature', 'quote', 'shown');
}
getAuthorLink(author) {
@@ -198,13 +200,15 @@ class Quote extends PureComponent {
if (favouriteQuote) {
let author = favouriteQuote.split(' - ')[1];
const authorimgdata = await this.getAuthorImg(author);
return this.setState({
this.setState({
quote: favouriteQuote.split(' - ')[0],
author,
authorlink: this.getAuthorLink(author),
authorimg: authorimgdata.authorimg,
authorimglicense: authorimgdata.authorimglicense,
});
Stats.postEvent('feature', 'quote', 'shown');
return;
}
switch (this.state.type) {
@@ -229,13 +233,14 @@ class Quote extends PureComponent {
: null;
if (customQuote !== undefined && customQuote !== null) {
return this.setState({
this.setState({
quote: '"' + customQuote.quote + '"',
author: customQuote.author,
authorlink: this.getAuthorLink(customQuote.author),
authorimg: await this.getAuthorImg(customQuote.author),
noQuote: false,
});
Stats.postEvent('feature', 'quote', 'shown');
} else {
this.setState({
noQuote: true,
@@ -244,7 +249,8 @@ class Quote extends PureComponent {
break;
case 'quote_pack':
if (offline) {
return this.doOffline();
this.doOffline();
return;
}
const quotePack = [];
@@ -261,18 +267,21 @@ class Quote extends PureComponent {
if (quotePack) {
const data = quotePack[Math.floor(Math.random() * quotePack.length)];
return this.setState({
this.setState({
quote: '"' + data.quote + '"',
author: data.author,
authorlink: this.getAuthorLink(data.author),
authorimg: data.fallbackauthorimg,
});
Stats.postEvent('feature', 'quote', 'shown');
} else {
return this.doOffline();
this.doOffline();
}
break;
case 'api':
if (offline) {
return this.doOffline();
this.doOffline();
return;
}
const getAPIQuoteData = async () => {
@@ -304,6 +313,7 @@ class Quote extends PureComponent {
this.setState(data);
localStorage.setItem('currentQuote', JSON.stringify(data));
localStorage.setItem('nextQuote', JSON.stringify(await getAPIQuoteData())); // pre-fetch data about the next quote
Stats.postEvent('feature', 'quote', 'shown');
} else {
this.doOffline();
}
@@ -318,7 +328,7 @@ class Quote extends PureComponent {
}
copyQuote() {
variables.stats.postEvent('feature', 'quote', 'copied');
Stats.postEvent('feature', 'quote', 'copied');
navigator.clipboard.writeText(`${this.state.quote} - ${this.state.author}`);
toast(variables.getMessage('toasts.quote'));
}
@@ -329,14 +339,14 @@ class Quote extends PureComponent {
this.setState({
favourited: this.buttons.unfavourited,
});
Stats.postEvent('feature', 'quote', 'favourite_removed');
} else {
localStorage.setItem('favouriteQuote', this.state.quote + ' - ' + this.state.author);
this.setState({
favourited: this.buttons.favourited,
});
Stats.postEvent('feature', 'quote', 'favourited');
}
variables.stats.postEvent('feature', 'quote', 'favourite');
}
init() {

View File

@@ -1,3 +1,4 @@
import { addEvent, getEvents } from 'utils/indexedDB';
import { newAchievements, getLocalisedAchievementData } from './achievements';
import { toast } from 'react-toastify';
import variables from 'config/variables';
@@ -20,35 +21,71 @@ export default class Stats {
}
static calculateNextLevelXp(level) {
return Math.floor(100 * Math.pow(1.5, level - 1));
const baseXP = 100;
const scalingFactor = 1.2;
const softCap = 50;
if (level <= softCap) {
return Math.floor(baseXP * Math.pow(scalingFactor, level - 1));
} else {
const softCapXP = baseXP * Math.pow(scalingFactor, softCap - 1);
const extraLevels = level - softCap;
return Math.floor(softCapXP + baseXP * extraLevels * 0.5);
}
}
static calculateXpForEvent(eventType, streak) {
let baseXp = 10; // Base XP for an event
if (eventType === 'tabs-opened') {
baseXp = 5;
let baseXp;
switch (eventType) {
case 'new-tab':
baseXp = 5;
break;
case 'settings-tab':
baseXp = 1;
break;
case 'marketplace-install':
baseXp = 20;
break;
default:
baseXp = 10;
break;
}
return baseXp * (1 + streak * 0.1); // Increase XP by 10% for each day in the streak
return Math.round(baseXp * (1 + streak * 0.1));
}
static async postEvent(type, name = '', action = '') {
const data = JSON.parse(localStorage.getItem('statsData')) || {};
const timestamp = new Date().toISOString();
static async addEvent(eventLog, event) {
await addEvent(event);
}
console.log(`Event Type: ${type}`);
console.log(`Event Name: ${name}`);
console.log(`Event Action: ${action}`);
static updateStatsData(statsData, xpGained) {
statsData.totalXp += xpGained;
statsData.currentLevelXp += xpGained;
if (!data[type]) {
data[type] = { count: 0, events: [] };
while (statsData.currentLevelXp >= statsData.nextLevelXp) {
statsData.currentLevelXp -= statsData.nextLevelXp;
statsData.level += 1;
statsData.nextLevelXp = this.calculateNextLevelXp(statsData.level);
toast.info(`🎉 Level Up ${statsData.level}`, {
icon: false,
closeButton: false,
});
}
data[type].count += 1;
data[type].events.push({ name, action, timestamp });
// Ensure XP values are integers
statsData.totalXp = Math.round(statsData.totalXp);
statsData.currentLevelXp = Math.round(statsData.currentLevelXp);
// Calculate XP and level
const streak = data.streak?.current || 0;
const lastEventTimestamp = data[type].events[data[type].events.length - 2]?.timestamp;
localStorage.setItem('statsData', JSON.stringify(statsData));
}
static calculateStreak(data, timestamp) {
if (!data.events || data.events.length < 2) {
data.streak.current = 1;
return;
}
const lastEventTimestamp = data.events[data.events.length - 2]?.timestamp;
const lastEventDate = lastEventTimestamp ? new Date(lastEventTimestamp).toDateString() : null;
const currentEventDate = new Date(timestamp).toDateString();
@@ -56,36 +93,60 @@ export default class Stats {
const daysDifference =
(new Date(currentEventDate) - new Date(lastEventDate)) / (1000 * 60 * 60 * 24);
if (daysDifference === 1) {
data.streak = { current: streak + 1 };
data.streak.current = (data.streak.current || 0) + 1;
} else {
data.streak = { current: 1 };
data.streak.current = 1;
}
} else if (!lastEventDate) {
data.streak = { current: 1 };
} else {
data.streak.current = 1;
}
}
const xpGained = this.calculateXpForEvent(type, data.streak.current);
data.totalXp = (data.totalXp || 0) + xpGained;
data.xp = (data.xp || 0) + xpGained;
data.nextLevelXp = data.nextLevelXp || this.calculateNextLevelXp(data.level || 1);
static async postEvent(type, name = '', action = '') {
const eventLog = await getEvents();
const statsData = JSON.parse(localStorage.getItem('statsData')) || {
level: 1,
totalXp: 0,
currentLevelXp: 0,
nextLevelXp: this.calculateNextLevelXp(1),
streak: { current: 0 },
};
const timestamp = new Date().toISOString();
console.log(`XP Gained for ${type}: ${xpGained}`);
console.log(`Total XP: ${data.totalXp}`);
console.log(`Current Level XP: ${data.xp}`);
console.log(`Next Level XP: ${data.nextLevelXp}`);
console.log(`Current Streak: ${data.streak.current}`);
console.log(`Event Type: ${type}`);
console.log(`Event Name: ${name}`);
console.log(`Event Action: ${action}`);
while (data.xp >= data.nextLevelXp) {
data.xp -= data.nextLevelXp;
data.level = (data.level || 1) + 1;
data.nextLevelXp = this.calculateNextLevelXp(data.level);
toast.info(`🎉 Level Up ${data.level}`, {
icon: false,
closeButton: false,
});
}
const xpGained = this.calculateXpForEvent(type, statsData.streak.current);
await this.addEvent(eventLog, { type, name, action, timestamp, xpGained });
this.updateStatsData(statsData, xpGained);
this.calculateStreak(statsData, timestamp);
localStorage.setItem('statsData', JSON.stringify(data));
this.achievementTrigger(data);
localStorage.setItem('statsData', JSON.stringify(statsData));
this.achievementTrigger(statsData);
}
static async getStats(type, name, action, startDate, endDate) {
const eventLog = await getEvents();
return eventLog.filter((event) => {
const eventDate = new Date(event.timestamp);
return (
(!type || event.type === type) &&
(!name || event.name === name) &&
(!action || event.action === action) &&
(!startDate || eventDate >= new Date(startDate)) &&
(!endDate || eventDate <= new Date(endDate))
);
});
}
static async calculateXpBetweenDates(startDate, endDate) {
const eventLog = await getEvents();
return eventLog
.filter((event) => {
const eventDate = new Date(event.timestamp);
return eventDate >= new Date(startDate) && eventDate <= new Date(endDate);
})
.reduce((totalXp, event) => totalXp + (event.xpGained || 0), 0);
}
}

View File

@@ -8,6 +8,7 @@ import { Header, CustomActions } from 'components/Layout/Settings';
import { StatsOverview } from './sections/StatsOverview';
import { ClearModal } from './ClearModal';
import Achievements from './sections/Achievements';
import StatsDashboard from './sections/StatsDashboard';
import { saveFile } from 'utils/saveFile';
import variables from 'config/variables';
@@ -37,8 +38,15 @@ const Stats = () => {
}, [achievements]);
const resetStats = () => {
const emptyStats = {};
const emptyStats = {
level: 1,
totalXp: 0,
currentLevelXp: 0,
nextLevelXp: 100,
streak: { current: 0 },
};
localStorage.setItem('statsData', JSON.stringify(emptyStats));
localStorage.setItem('eventLog', JSON.stringify([]));
localStorage.setItem('achievements', JSON.stringify(initialAchievements));
setStats(emptyStats);
setClearmodal(false);
@@ -53,18 +61,11 @@ const Stats = () => {
saveFile(JSON.stringify(stats, null, 2), filename);
};
const StatsElement = ({ title, value }) => (
<div>
<span className="subtitle">{title}</span>
<span>{value}</span>
</div>
);
const STATS_SECTION = 'settings:sections.stats';
return (
<>
{/* <Header title={variables.getMessage(`${STATS_SECTION}.title`)} report={false}>
<Header title={variables.getMessage(`${STATS_SECTION}.title`)} report={false}>
<CustomActions>
<Button
type="settings"
@@ -79,7 +80,7 @@ const Stats = () => {
label={variables.getMessage('settings:buttons.reset')}
/>
</CustomActions>
</Header> */}
</Header>
<Modal
closeTimeoutMS={100}
onRequestClose={() => setClearmodal(false)}
@@ -97,50 +98,7 @@ const Stats = () => {
STATS_SECTION={STATS_SECTION}
variables={variables}
/>
<span class="text-2xl font-semibold pb-5">Statistics</span>
<div className="modalInfoPage stats">
<div className="statSection rightPanel">
<div className="statIcon">
<MdShowChart />
</div>
<div className="statGrid">
<StatsElement
title={variables.getMessage(`${STATS_SECTION}.sections.tabs_opened`)}
value={stats['tabs-opened']?.count || 0}
/>
<StatsElement
title={variables.getMessage(`${STATS_SECTION}.sections.backgrounds_favourited`)}
value={stats['background-favourite']?.count || 0}
/>
<StatsElement
title={variables.getMessage(`${STATS_SECTION}.sections.backgrounds_downloaded`)}
value={stats['background-download']?.count || 0}
/>
<StatsElement
title={variables.getMessage(`${STATS_SECTION}.sections.quotes_favourited`)}
value={stats['quoted-favourite']?.count || 0}
/>
<StatsElement
title={variables.getMessage(`${STATS_SECTION}.sections.quicklinks_added`)}
value={stats['quicklink-add']?.count || 0}
/>
<StatsElement
title={variables.getMessage(`${STATS_SECTION}.sections.settings_changed`)}
value={stats.setting ? Object.keys(stats.setting).length : 0}
/>
<StatsElement
title={variables.getMessage(`${STATS_SECTION}.sections.addons_installed`)}
value={stats.marketplace?.install?.length || 0}
/>
<StatsElement
title="XP Level"
value={`${stats.level || 1} (${stats.xp || 0}/${stats.nextLevelXp || 100} XP)`}
/>
<StatsElement title="Total XP" value={`${stats.totalXp || 0} XP`} />
<StatsElement title="Streak" value={`${stats.streak?.current || 0} days`} />
</div>
</div>
</div>
<StatsDashboard stats={stats} variables={variables} STATS_SECTION={STATS_SECTION} />
</>
);
};

View File

@@ -0,0 +1,274 @@
import React, { useState, useEffect } from 'react';
import { ResponsiveContainer } from 'recharts';
import { MdShowChart } from 'react-icons/md';
import Stats from 'features/stats/api/stats';
const StatsDashboard = ({ stats, variables, STATS_SECTION }) => {
const [timeView, setTimeView] = useState('week');
const [data, setData] = useState([]);
const [filteredStats, setFilteredStats] = useState({});
const [xpBetweenDates, setXpBetweenDates] = useState(0);
const CENTER_X = 250;
const CENTER_Y = 250;
const RADIUS = 200;
const INNER_RADIUS = 30;
useEffect(() => {
const parseData = async (timeView) => {
const now = new Date();
let startDate, endDate;
switch (timeView) {
case 'today':
startDate = new Date(now.setHours(0, 0, 0, 0));
endDate = new Date(now.setHours(23, 59, 59, 999));
break;
case 'yesterday':
startDate = new Date(now.setDate(now.getDate() - 1));
startDate.setHours(0, 0, 0, 0);
endDate = new Date(now.setHours(23, 59, 59, 999));
break;
case 'week':
startDate = new Date(now.setDate(now.getDate() - 7));
endDate = new Date();
break;
case 'month':
startDate = new Date(now.setMonth(now.getMonth() - 1));
endDate = new Date();
break;
case 'year':
startDate = new Date(now.setFullYear(now.getFullYear() - 1));
endDate = new Date();
break;
default:
break;
}
const filteredEvents = await Stats.getStats(null, null, null, startDate, endDate);
const data = Array.from({ length: 24 }, (_, hour) => ({
hour,
count: 0,
}));
filteredEvents.forEach((event) => {
const date = new Date(event.timestamp);
const hour = date.getHours();
data[hour].count += 1;
});
const filteredStats = {
'xp-earned': await Stats.calculateXpBetweenDates(startDate, endDate),
'new-tab': (await Stats.getStats('new-tab', null, null, startDate, endDate)).length,
'background-favourite': (
await Stats.getStats('background-favourite', null, null, startDate, endDate)
).length,
'background-download': (
await Stats.getStats('background-download', null, null, startDate, endDate)
).length,
'quoted-favourite': (
await Stats.getStats('quoted-favourite', null, null, startDate, endDate)
).length,
'quicklink-add': (await Stats.getStats('quicklink-add', null, null, startDate, endDate))
.length,
setting: (await Stats.getStats('setting', null, null, startDate, endDate)).length,
'marketplace-install': (
await Stats.getStats('marketplace-install', null, null, startDate, endDate)
).length,
'quotes-shown': (await Stats.getStats('feature', 'quote', 'shown', startDate, endDate))
.length,
'backgrounds-shown': (
await Stats.getStats('feature', 'background-image', 'shown', startDate, endDate)
).length,
level: stats.level || 1,
xp: stats.currentLevelXp || 0,
nextLevelXp: stats.nextLevelXp || 100,
totalXp: stats.totalXp || 0,
streak: stats.streak?.current || 0,
};
const xpBetweenDates = await Stats.calculateXpBetweenDates(startDate, endDate);
return { data, filteredStats, xpBetweenDates };
};
const fetchData = async () => {
const { data, filteredStats, xpBetweenDates } = await parseData(timeView);
setData(data);
setFilteredStats(filteredStats);
setXpBetweenDates(xpBetweenDates);
};
fetchData();
}, [timeView, stats]);
const renderClock = () => {
const elements = [];
const maxCount = Math.max(...data.map((d) => d.count));
for (let hour = 0; hour < 24; hour++) {
const angleRad = (hour * 15 - 90) * (Math.PI / 180);
const nextAngleRad = ((hour + 1) * 15 - 90) * (Math.PI / 180);
// Spoke line
const spokeEndX = CENTER_X + Math.cos(angleRad) * RADIUS;
const spokeEndY = CENTER_Y + Math.sin(angleRad) * RADIUS;
elements.push(
<line
key={`spoke-${hour}`}
x1={CENTER_X}
y1={CENTER_Y}
x2={spokeEndX}
y2={spokeEndY}
stroke="#ffffff"
strokeWidth={1}
/>,
);
// Hour number
const numberRadius = RADIUS + 20;
const numberX = CENTER_X + Math.cos(angleRad) * numberRadius;
const numberY = CENTER_Y + Math.sin(angleRad) * numberRadius;
elements.push(
<text
key={`hour-${hour}`}
x={numberX}
y={numberY}
fill="#ffffff"
fontSize="12"
textAnchor="middle"
dominantBaseline="middle"
>
{hour}
</text>,
);
// Activity sector
if (data[hour] && data[hour].count > 0) {
const sectorRadius = INNER_RADIUS + ((RADIUS - INNER_RADIUS) * data[hour].count) / maxCount;
const startX = CENTER_X + Math.cos(angleRad) * INNER_RADIUS;
const startY = CENTER_Y + Math.sin(angleRad) * INNER_RADIUS;
const endX = CENTER_X + Math.cos(nextAngleRad) * INNER_RADIUS;
const endY = CENTER_Y + Math.sin(nextAngleRad) * INNER_RADIUS;
const outerStartX = CENTER_X + Math.cos(angleRad) * sectorRadius;
const outerStartY = CENTER_Y + Math.sin(angleRad) * sectorRadius;
const outerEndX = CENTER_X + Math.cos(nextAngleRad) * sectorRadius;
const outerEndY = CENTER_Y + Math.sin(nextAngleRad) * sectorRadius;
const pathData = `
M ${startX} ${startY}
L ${outerStartX} ${outerStartY}
A ${sectorRadius} ${sectorRadius} 0 0 1 ${outerEndX} ${outerEndY}
L ${endX} ${endY}
A ${INNER_RADIUS} ${INNER_RADIUS} 0 0 0 ${startX} ${startY}
Z
`;
elements.push(
<path
key={`sector-${hour}`}
d={pathData}
fill="#D21A11"
stroke="none"
title={`Hour: ${hour}, Tabs Opened: ${data[hour].count}`}
/>,
);
}
}
return elements;
};
const StatsElement = ({ title, value }) => (
<div className="flex flex-col">
<span className="text-3xl">{value}</span>
<span className="text-xs text-neutral-400 leading-tight lowercase">{title}</span>
</div>
);
return (
<div>
<div className="flex flex-row justify-between mb-4 items-center">
<span className="text-2xl font-semibold pb-5">Statistics</span>
<div className="flex flex-row">
{['today', 'yesterday', 'week', 'month', 'year'].map((view) => (
<button
key={view}
className={`btn-view px-5 py-2 mx-1 rounded ${
timeView === view
? 'bg-white text-black'
: 'bg-modal-content-light dark:bg-modal-content-dark text-white'
}`}
onClick={() => setTimeView(view)}
>
{view.charAt(0).toUpperCase() + view.slice(1)}
</button>
))}
</div>
</div>
<div className="grid grid-cols-2 items-center">
<div className="flex flex-col items-center">
<ResponsiveContainer width="100%" height={400}>
<svg viewBox="0 0 500 500">
<circle
cx={CENTER_X}
cy={CENTER_Y}
r={INNER_RADIUS}
fill="#111111"
stroke="#333333"
strokeWidth="1"
/>
{renderClock()}
</svg>
</ResponsiveContainer>
</div>
<div className="bg-modal-content-light dark:bg-modal-content-dark my-5 p-10 rounded">
<div className="grid grid-cols-2 gap-10">
<StatsElement title="XP Earned" value={filteredStats['xp-earned'] || 0} />
<StatsElement
title={variables.getMessage(`${STATS_SECTION}.sections.tabs_opened`)}
value={filteredStats['new-tab'] || 0}
/>
<StatsElement title="Quotes Shown" value={filteredStats['quotes-shown'] || 0} />
<StatsElement
title={variables.getMessage(`${STATS_SECTION}.sections.settings_changed`)}
value={filteredStats.setting || 0}
/>
<StatsElement
title={variables.getMessage(`${STATS_SECTION}.sections.addons_installed`)}
value={filteredStats['marketplace-install'] || 0}
/>
<StatsElement
title="Background Images Shown"
value={filteredStats['backgrounds-shown'] || 0}
/>
<StatsElement
title={variables.getMessage(`${STATS_SECTION}.sections.backgrounds_favourited`)}
value={filteredStats['background-favourite'] || 0}
/>
<StatsElement
title={variables.getMessage(`${STATS_SECTION}.sections.backgrounds_downloaded`)}
value={filteredStats['background-download'] || 0}
/>
<StatsElement
title={variables.getMessage(`${STATS_SECTION}.sections.quotes_favourited`)}
value={filteredStats['quoted-favourite'] || 0}
/>
<StatsElement
title={variables.getMessage(`${STATS_SECTION}.sections.quicklinks_added`)}
value={filteredStats['quicklink-add'] || 0}
/>
</div>
</div>
</div>
<div className="bg-modal-content-light dark:bg-modal-content-dark my-5 p-10 rounded flex flex-row justify-around">
<StatsElement title="Total XP" value={filteredStats.totalXp || 0} />
<StatsElement title="XP" value={filteredStats.xp || 0} />
<StatsElement title="Next level XP" value={filteredStats.nextLevelXp || 100} />
</div>
</div>
);
};
export default StatsDashboard;

View File

@@ -1,5 +1,5 @@
const StatsOverview = ({ stats }) => {
const xpPercentage = Math.floor((stats.xp / stats.nextLevelXp) * 100);
const xpPercentage = Math.floor((stats.currentLevelXp / stats.nextLevelXp) * 100);
const ProgressCircle = ({ percentage }) => {
const circleRadius = 50;
@@ -36,12 +36,12 @@ const StatsOverview = ({ stats }) => {
};
return (
<div class="grid grid-cols-2 gap-10">
<div className="grid grid-cols-2 gap-10">
<div className="bg-modal-content-light dark:bg-modal-content-dark py-6 px-10 rounded flex flex-row items-center">
<ProgressCircle percentage={xpPercentage} />
<div className="flex flex-col">
<span className="text-2xl font-bold">Level {stats.level || 1}</span>
<span>{`${Math.round(stats.nextLevelXp - stats.xp) || 0} XP until next level`}</span>
<span>{`${Math.round(stats.nextLevelXp - stats.currentLevelXp) || 0} XP until next level`}</span>
</div>
</div>
<div className="bg-modal-content-light dark:bg-modal-content-dark py-6 px-10 rounded flex flex-row items-center justify-center">

61
src/utils/indexedDB.js Normal file
View File

@@ -0,0 +1,61 @@
// indexedDB.js
const DB_NAME = 'StatsDB';
const DB_VERSION = 1;
const STORE_NAME = 'eventLog';
export const openDB = () => {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, DB_VERSION);
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains(STORE_NAME)) {
db.createObjectStore(STORE_NAME, { keyPath: 'id', autoIncrement: true });
}
};
request.onsuccess = (event) => {
resolve(event.target.result);
};
request.onerror = (event) => {
reject(event.target.error);
};
});
};
export const addEvent = (event) => {
return openDB().then((db) => {
return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], 'readwrite');
const store = transaction.objectStore(STORE_NAME);
const request = store.add(event);
request.onsuccess = () => {
resolve();
};
request.onerror = (event) => {
reject(event.target.error);
};
});
});
};
export const getEvents = (query) => {
return openDB().then((db) => {
return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], 'readonly');
const store = transaction.objectStore(STORE_NAME);
const request = store.getAll();
request.onsuccess = (event) => {
resolve(event.target.result);
};
request.onerror = (event) => {
reject(event.target.error);
};
});
});
};