mirror of
https://github.com/mue/mue.git
synced 2026-07-26 18:21:27 +02:00
feat: stats by indexed db + tab hours clock
This commit is contained in:
@@ -36,6 +36,7 @@
|
|||||||
"react-icons": "^5.3.0",
|
"react-icons": "^5.3.0",
|
||||||
"react-modal": "3.16.1",
|
"react-modal": "3.16.1",
|
||||||
"react-toastify": "10.0.5",
|
"react-toastify": "10.0.5",
|
||||||
|
"recharts": "^2.13.3",
|
||||||
"use-debounce": "^10.0.3"
|
"use-debounce": "^10.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { loadSettings } from 'utils/settings';
|
|||||||
import EventBus from 'utils/eventbus';
|
import EventBus from 'utils/eventbus';
|
||||||
import variables from 'config/variables';
|
import variables from 'config/variables';
|
||||||
import Preview from 'features/helpers/preview/Preview';
|
import Preview from 'features/helpers/preview/Preview';
|
||||||
|
import Stats from 'features/stats/api/stats';
|
||||||
|
|
||||||
import Welcome from 'features/welcome/Welcome';
|
import Welcome from 'features/welcome/Welcome';
|
||||||
|
|
||||||
@@ -28,8 +29,6 @@ const useAppSetup = () => {
|
|||||||
|
|
||||||
EventBus.on('refresh', refreshHandler);
|
EventBus.on('refresh', refreshHandler);
|
||||||
|
|
||||||
variables.stats.postEvent('tabs-opened');
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
EventBus.off('refresh', refreshHandler);
|
EventBus.off('refresh', refreshHandler);
|
||||||
};
|
};
|
||||||
@@ -52,6 +51,9 @@ const App = () => {
|
|||||||
if (storedBackground === 'true' || storedBackground === true) {
|
if (storedBackground === 'true' || storedBackground === true) {
|
||||||
setShowBackground(true);
|
setShowBackground(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Post event immediately when the component mounts
|
||||||
|
Stats.postEvent('new-tab', 'tab', 'opened');
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useAppSetup();
|
useAppSetup();
|
||||||
|
|||||||
@@ -3,13 +3,16 @@ import variables from 'config/variables';
|
|||||||
import Tab from './Tab';
|
import Tab from './Tab';
|
||||||
import { useTab } from './TabContext';
|
import { useTab } from './TabContext';
|
||||||
import { MdOutlineWarning, MdRefresh, MdClose } from 'react-icons/md';
|
import { MdOutlineWarning, MdRefresh, MdClose } from 'react-icons/md';
|
||||||
|
import Stats from 'features/stats/api/stats';
|
||||||
|
|
||||||
const Sidebar = memo(({ sections, currentTab, setCurrentTab }) => {
|
const Sidebar = memo(({ sections, currentTab, setCurrentTab }) => {
|
||||||
const { subTab, setSubTab, setSubSection } = useTab();
|
const { subTab, setSubTab, setSubSection } = useTab();
|
||||||
const handleClick = useCallback(
|
const handleClick = useCallback(
|
||||||
(label) => () => {
|
(label) => () => {
|
||||||
setSubTab(variables.getMessage(label));
|
const newTab = variables.getMessage(label);
|
||||||
|
setSubTab(newTab);
|
||||||
setSubSection('');
|
setSubSection('');
|
||||||
|
Stats.postEvent('settings-tab', newTab, 'opened');
|
||||||
},
|
},
|
||||||
[setSubTab, setSubSection],
|
[setSubTab, setSubSection],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import './scss/index.scss';
|
|||||||
import { decodeBlurHash } from 'fast-blurhash';
|
import { decodeBlurHash } from 'fast-blurhash';
|
||||||
|
|
||||||
import defaults from './options/default';
|
import defaults from './options/default';
|
||||||
|
import Stats from 'features/stats/api/stats';
|
||||||
|
|
||||||
export default class Background extends PureComponent {
|
export default class Background extends PureComponent {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -71,9 +72,11 @@ export default class Background extends PureComponent {
|
|||||||
this.blob = URL.createObjectURL(await (await fetch(url)).blob());
|
this.blob = URL.createObjectURL(await (await fetch(url)).blob());
|
||||||
backgroundImage.classList.add('backgroundTransform');
|
backgroundImage.classList.add('backgroundTransform');
|
||||||
backgroundImage.style.backgroundImage = `url(${this.blob})`;
|
backgroundImage.style.backgroundImage = `url(${this.blob})`;
|
||||||
|
Stats.postEvent('feature', 'background-image', 'shown');
|
||||||
} else {
|
} else {
|
||||||
// custom colour
|
// custom colour
|
||||||
backgroundImage.setAttribute('style', this.state.style);
|
backgroundImage.setAttribute('style', this.state.style);
|
||||||
|
Stats.postEvent('background', 'colour', 'set');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,6 +124,7 @@ export default class Background extends PureComponent {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
// if requesting to the API fails, we get an offline image
|
// if requesting to the API fails, we get an offline image
|
||||||
this.setState(getOfflineImage('api'));
|
this.setState(getOfflineImage('api'));
|
||||||
|
Stats.postEvent('background', 'image', 'offline');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,6 +186,7 @@ export default class Background extends PureComponent {
|
|||||||
url,
|
url,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
Stats.postEvent('background', 'favourite', 'set');
|
||||||
};
|
};
|
||||||
|
|
||||||
const favourited = JSON.parse(localStorage.getItem('favourite'));
|
const favourited = JSON.parse(localStorage.getItem('favourite'));
|
||||||
@@ -193,7 +198,9 @@ export default class Background extends PureComponent {
|
|||||||
switch (type) {
|
switch (type) {
|
||||||
case 'api':
|
case 'api':
|
||||||
if (offline) {
|
if (offline) {
|
||||||
return this.setState(getOfflineImage('api'));
|
this.setState(getOfflineImage('api'));
|
||||||
|
Stats.postEvent('background', 'image', 'offline');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// API background
|
// API background
|
||||||
@@ -206,6 +213,7 @@ export default class Background extends PureComponent {
|
|||||||
'nextImage',
|
'nextImage',
|
||||||
JSON.stringify(await this.getAPIImageData(data.photoInfo.pun)),
|
JSON.stringify(await this.getAPIImageData(data.photoInfo.pun)),
|
||||||
); // pre-fetch data about the next image
|
); // pre-fetch data about the next image
|
||||||
|
Stats.postEvent('background', 'image', 'api');
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -227,11 +235,13 @@ export default class Background extends PureComponent {
|
|||||||
type: 'colour',
|
type: 'colour',
|
||||||
style: `background: ${customBackgroundColour || 'rgb(0,0,0)'}`,
|
style: `background: ${customBackgroundColour || 'rgb(0,0,0)'}`,
|
||||||
});
|
});
|
||||||
|
Stats.postEvent('background', 'colour', 'custom');
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'random_colour':
|
case 'random_colour':
|
||||||
case 'random_gradient':
|
case 'random_gradient':
|
||||||
this.setState(randomColourStyleBuilder(type));
|
this.setState(randomColourStyleBuilder(type));
|
||||||
|
Stats.postEvent('background', 'colour', 'random');
|
||||||
break;
|
break;
|
||||||
case 'custom':
|
case 'custom':
|
||||||
let customBackground = [];
|
let customBackground = [];
|
||||||
@@ -251,7 +261,9 @@ export default class Background extends PureComponent {
|
|||||||
|
|
||||||
// allow users to use offline images
|
// allow users to use offline images
|
||||||
if (offline && !customBackground.startsWith('data:')) {
|
if (offline && !customBackground.startsWith('data:')) {
|
||||||
return this.setState(getOfflineImage('custom'));
|
this.setState(getOfflineImage('custom'));
|
||||||
|
Stats.postEvent('background', 'image', 'offline');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -271,12 +283,15 @@ export default class Background extends PureComponent {
|
|||||||
this.setState(object);
|
this.setState(object);
|
||||||
|
|
||||||
localStorage.setItem('currentBackground', JSON.stringify(object));
|
localStorage.setItem('currentBackground', JSON.stringify(object));
|
||||||
|
Stats.postEvent('background', 'image', 'custom');
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'photo_pack':
|
case 'photo_pack':
|
||||||
if (offline) {
|
if (offline) {
|
||||||
return this.setState(getOfflineImage('photo'));
|
this.setState(getOfflineImage('photo'));
|
||||||
|
Stats.postEvent('background', 'image', 'offline');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const photoPack = [];
|
const photoPack = [];
|
||||||
@@ -292,7 +307,9 @@ export default class Background extends PureComponent {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (photoPack.length === 0) {
|
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)];
|
const photo = photoPack[Math.floor(Math.random() * photoPack.length)];
|
||||||
@@ -306,6 +323,7 @@ export default class Background extends PureComponent {
|
|||||||
photographer: photo.photographer,
|
photographer: photo.photographer,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
Stats.postEvent('background', 'image', 'photo_pack');
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useRef, useState, useEffect } from 'react';
|
|||||||
|
|
||||||
import { appendNth, convertTimezone } from 'utils/date';
|
import { appendNth, convertTimezone } from 'utils/date';
|
||||||
import EventBus from 'utils/eventbus';
|
import EventBus from 'utils/eventbus';
|
||||||
|
import Stats from 'features/stats/api/stats';
|
||||||
import defaults from './options/default';
|
import defaults from './options/default';
|
||||||
|
|
||||||
import './greeting.scss';
|
import './greeting.scss';
|
||||||
@@ -117,6 +118,9 @@ function Greeting() {
|
|||||||
// Set the state to the greeting string
|
// Set the state to the greeting string
|
||||||
setGreeting(`${message}${name}`);
|
setGreeting(`${message}${name}`);
|
||||||
|
|
||||||
|
// Log the event when a greeting is shown
|
||||||
|
Stats.postEvent('feature', 'greeting', 'shown');
|
||||||
|
|
||||||
getGreeting();
|
getGreeting();
|
||||||
}, time);
|
}, time);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ const ItemPage = () => {
|
|||||||
}, [installedItems, selectedItem.name]);
|
}, [installedItems, selectedItem.name]);
|
||||||
|
|
||||||
const locale = localStorage.getItem('language');
|
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 languageNames = new Intl.DisplayNames([shortLocale], { type: 'language' });
|
||||||
|
|
||||||
let dateObj, formattedDate;
|
let dateObj, formattedDate;
|
||||||
|
|||||||
@@ -88,7 +88,6 @@ const Overview = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem('order', JSON.stringify(items));
|
localStorage.setItem('order', JSON.stringify(items));
|
||||||
variables.stats.postEvent('setting', 'widget-order');
|
|
||||||
EventBus.emit('refresh', 'widgets');
|
EventBus.emit('refresh', 'widgets');
|
||||||
}, [items]);
|
}, [items]);
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
} from 'react-icons/md';
|
} from 'react-icons/md';
|
||||||
|
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
import Stats from 'features/stats/api/stats';
|
||||||
|
|
||||||
import { Tooltip } from 'components/Elements';
|
import { Tooltip } from 'components/Elements';
|
||||||
|
|
||||||
@@ -105,6 +106,7 @@ class Quote extends PureComponent {
|
|||||||
authorlink: this.getAuthorLink(quote.author),
|
authorlink: this.getAuthorLink(quote.author),
|
||||||
authorimg: '',
|
authorimg: '',
|
||||||
});
|
});
|
||||||
|
Stats.postEvent('feature', 'quote', 'shown');
|
||||||
}
|
}
|
||||||
|
|
||||||
getAuthorLink(author) {
|
getAuthorLink(author) {
|
||||||
@@ -198,13 +200,15 @@ class Quote extends PureComponent {
|
|||||||
if (favouriteQuote) {
|
if (favouriteQuote) {
|
||||||
let author = favouriteQuote.split(' - ')[1];
|
let author = favouriteQuote.split(' - ')[1];
|
||||||
const authorimgdata = await this.getAuthorImg(author);
|
const authorimgdata = await this.getAuthorImg(author);
|
||||||
return this.setState({
|
this.setState({
|
||||||
quote: favouriteQuote.split(' - ')[0],
|
quote: favouriteQuote.split(' - ')[0],
|
||||||
author,
|
author,
|
||||||
authorlink: this.getAuthorLink(author),
|
authorlink: this.getAuthorLink(author),
|
||||||
authorimg: authorimgdata.authorimg,
|
authorimg: authorimgdata.authorimg,
|
||||||
authorimglicense: authorimgdata.authorimglicense,
|
authorimglicense: authorimgdata.authorimglicense,
|
||||||
});
|
});
|
||||||
|
Stats.postEvent('feature', 'quote', 'shown');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (this.state.type) {
|
switch (this.state.type) {
|
||||||
@@ -229,13 +233,14 @@ class Quote extends PureComponent {
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (customQuote !== undefined && customQuote !== null) {
|
if (customQuote !== undefined && customQuote !== null) {
|
||||||
return this.setState({
|
this.setState({
|
||||||
quote: '"' + customQuote.quote + '"',
|
quote: '"' + customQuote.quote + '"',
|
||||||
author: customQuote.author,
|
author: customQuote.author,
|
||||||
authorlink: this.getAuthorLink(customQuote.author),
|
authorlink: this.getAuthorLink(customQuote.author),
|
||||||
authorimg: await this.getAuthorImg(customQuote.author),
|
authorimg: await this.getAuthorImg(customQuote.author),
|
||||||
noQuote: false,
|
noQuote: false,
|
||||||
});
|
});
|
||||||
|
Stats.postEvent('feature', 'quote', 'shown');
|
||||||
} else {
|
} else {
|
||||||
this.setState({
|
this.setState({
|
||||||
noQuote: true,
|
noQuote: true,
|
||||||
@@ -244,7 +249,8 @@ class Quote extends PureComponent {
|
|||||||
break;
|
break;
|
||||||
case 'quote_pack':
|
case 'quote_pack':
|
||||||
if (offline) {
|
if (offline) {
|
||||||
return this.doOffline();
|
this.doOffline();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const quotePack = [];
|
const quotePack = [];
|
||||||
@@ -261,18 +267,21 @@ class Quote extends PureComponent {
|
|||||||
|
|
||||||
if (quotePack) {
|
if (quotePack) {
|
||||||
const data = quotePack[Math.floor(Math.random() * quotePack.length)];
|
const data = quotePack[Math.floor(Math.random() * quotePack.length)];
|
||||||
return this.setState({
|
this.setState({
|
||||||
quote: '"' + data.quote + '"',
|
quote: '"' + data.quote + '"',
|
||||||
author: data.author,
|
author: data.author,
|
||||||
authorlink: this.getAuthorLink(data.author),
|
authorlink: this.getAuthorLink(data.author),
|
||||||
authorimg: data.fallbackauthorimg,
|
authorimg: data.fallbackauthorimg,
|
||||||
});
|
});
|
||||||
|
Stats.postEvent('feature', 'quote', 'shown');
|
||||||
} else {
|
} else {
|
||||||
return this.doOffline();
|
this.doOffline();
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
case 'api':
|
case 'api':
|
||||||
if (offline) {
|
if (offline) {
|
||||||
return this.doOffline();
|
this.doOffline();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getAPIQuoteData = async () => {
|
const getAPIQuoteData = async () => {
|
||||||
@@ -304,6 +313,7 @@ class Quote extends PureComponent {
|
|||||||
this.setState(data);
|
this.setState(data);
|
||||||
localStorage.setItem('currentQuote', JSON.stringify(data));
|
localStorage.setItem('currentQuote', JSON.stringify(data));
|
||||||
localStorage.setItem('nextQuote', JSON.stringify(await getAPIQuoteData())); // pre-fetch data about the next quote
|
localStorage.setItem('nextQuote', JSON.stringify(await getAPIQuoteData())); // pre-fetch data about the next quote
|
||||||
|
Stats.postEvent('feature', 'quote', 'shown');
|
||||||
} else {
|
} else {
|
||||||
this.doOffline();
|
this.doOffline();
|
||||||
}
|
}
|
||||||
@@ -318,7 +328,7 @@ class Quote extends PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
copyQuote() {
|
copyQuote() {
|
||||||
variables.stats.postEvent('feature', 'quote', 'copied');
|
Stats.postEvent('feature', 'quote', 'copied');
|
||||||
navigator.clipboard.writeText(`${this.state.quote} - ${this.state.author}`);
|
navigator.clipboard.writeText(`${this.state.quote} - ${this.state.author}`);
|
||||||
toast(variables.getMessage('toasts.quote'));
|
toast(variables.getMessage('toasts.quote'));
|
||||||
}
|
}
|
||||||
@@ -329,14 +339,14 @@ class Quote extends PureComponent {
|
|||||||
this.setState({
|
this.setState({
|
||||||
favourited: this.buttons.unfavourited,
|
favourited: this.buttons.unfavourited,
|
||||||
});
|
});
|
||||||
|
Stats.postEvent('feature', 'quote', 'favourite_removed');
|
||||||
} else {
|
} else {
|
||||||
localStorage.setItem('favouriteQuote', this.state.quote + ' - ' + this.state.author);
|
localStorage.setItem('favouriteQuote', this.state.quote + ' - ' + this.state.author);
|
||||||
this.setState({
|
this.setState({
|
||||||
favourited: this.buttons.favourited,
|
favourited: this.buttons.favourited,
|
||||||
});
|
});
|
||||||
|
Stats.postEvent('feature', 'quote', 'favourited');
|
||||||
}
|
}
|
||||||
|
|
||||||
variables.stats.postEvent('feature', 'quote', 'favourite');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { addEvent, getEvents } from 'utils/indexedDB';
|
||||||
import { newAchievements, getLocalisedAchievementData } from './achievements';
|
import { newAchievements, getLocalisedAchievementData } from './achievements';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import variables from 'config/variables';
|
import variables from 'config/variables';
|
||||||
@@ -20,35 +21,71 @@ export default class Stats {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static calculateNextLevelXp(level) {
|
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) {
|
static calculateXpForEvent(eventType, streak) {
|
||||||
let baseXp = 10; // Base XP for an event
|
let baseXp;
|
||||||
if (eventType === 'tabs-opened') {
|
|
||||||
baseXp = 5;
|
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 = '') {
|
static async addEvent(eventLog, event) {
|
||||||
const data = JSON.parse(localStorage.getItem('statsData')) || {};
|
await addEvent(event);
|
||||||
const timestamp = new Date().toISOString();
|
}
|
||||||
|
|
||||||
console.log(`Event Type: ${type}`);
|
static updateStatsData(statsData, xpGained) {
|
||||||
console.log(`Event Name: ${name}`);
|
statsData.totalXp += xpGained;
|
||||||
console.log(`Event Action: ${action}`);
|
statsData.currentLevelXp += xpGained;
|
||||||
|
|
||||||
if (!data[type]) {
|
while (statsData.currentLevelXp >= statsData.nextLevelXp) {
|
||||||
data[type] = { count: 0, events: [] };
|
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;
|
// Ensure XP values are integers
|
||||||
data[type].events.push({ name, action, timestamp });
|
statsData.totalXp = Math.round(statsData.totalXp);
|
||||||
|
statsData.currentLevelXp = Math.round(statsData.currentLevelXp);
|
||||||
|
|
||||||
// Calculate XP and level
|
localStorage.setItem('statsData', JSON.stringify(statsData));
|
||||||
const streak = data.streak?.current || 0;
|
}
|
||||||
const lastEventTimestamp = data[type].events[data[type].events.length - 2]?.timestamp;
|
|
||||||
|
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 lastEventDate = lastEventTimestamp ? new Date(lastEventTimestamp).toDateString() : null;
|
||||||
const currentEventDate = new Date(timestamp).toDateString();
|
const currentEventDate = new Date(timestamp).toDateString();
|
||||||
|
|
||||||
@@ -56,36 +93,60 @@ export default class Stats {
|
|||||||
const daysDifference =
|
const daysDifference =
|
||||||
(new Date(currentEventDate) - new Date(lastEventDate)) / (1000 * 60 * 60 * 24);
|
(new Date(currentEventDate) - new Date(lastEventDate)) / (1000 * 60 * 60 * 24);
|
||||||
if (daysDifference === 1) {
|
if (daysDifference === 1) {
|
||||||
data.streak = { current: streak + 1 };
|
data.streak.current = (data.streak.current || 0) + 1;
|
||||||
} else {
|
} else {
|
||||||
data.streak = { current: 1 };
|
data.streak.current = 1;
|
||||||
}
|
}
|
||||||
} else if (!lastEventDate) {
|
} else {
|
||||||
data.streak = { current: 1 };
|
data.streak.current = 1;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const xpGained = this.calculateXpForEvent(type, data.streak.current);
|
static async postEvent(type, name = '', action = '') {
|
||||||
data.totalXp = (data.totalXp || 0) + xpGained;
|
const eventLog = await getEvents();
|
||||||
data.xp = (data.xp || 0) + xpGained;
|
const statsData = JSON.parse(localStorage.getItem('statsData')) || {
|
||||||
data.nextLevelXp = data.nextLevelXp || this.calculateNextLevelXp(data.level || 1);
|
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(`Event Type: ${type}`);
|
||||||
console.log(`Total XP: ${data.totalXp}`);
|
console.log(`Event Name: ${name}`);
|
||||||
console.log(`Current Level XP: ${data.xp}`);
|
console.log(`Event Action: ${action}`);
|
||||||
console.log(`Next Level XP: ${data.nextLevelXp}`);
|
|
||||||
console.log(`Current Streak: ${data.streak.current}`);
|
|
||||||
|
|
||||||
while (data.xp >= data.nextLevelXp) {
|
const xpGained = this.calculateXpForEvent(type, statsData.streak.current);
|
||||||
data.xp -= data.nextLevelXp;
|
await this.addEvent(eventLog, { type, name, action, timestamp, xpGained });
|
||||||
data.level = (data.level || 1) + 1;
|
this.updateStatsData(statsData, xpGained);
|
||||||
data.nextLevelXp = this.calculateNextLevelXp(data.level);
|
this.calculateStreak(statsData, timestamp);
|
||||||
toast.info(`🎉 Level Up ${data.level}`, {
|
|
||||||
icon: false,
|
|
||||||
closeButton: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
localStorage.setItem('statsData', JSON.stringify(data));
|
localStorage.setItem('statsData', JSON.stringify(statsData));
|
||||||
this.achievementTrigger(data);
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { Header, CustomActions } from 'components/Layout/Settings';
|
|||||||
import { StatsOverview } from './sections/StatsOverview';
|
import { StatsOverview } from './sections/StatsOverview';
|
||||||
import { ClearModal } from './ClearModal';
|
import { ClearModal } from './ClearModal';
|
||||||
import Achievements from './sections/Achievements';
|
import Achievements from './sections/Achievements';
|
||||||
|
import StatsDashboard from './sections/StatsDashboard';
|
||||||
|
|
||||||
import { saveFile } from 'utils/saveFile';
|
import { saveFile } from 'utils/saveFile';
|
||||||
import variables from 'config/variables';
|
import variables from 'config/variables';
|
||||||
@@ -37,8 +38,15 @@ const Stats = () => {
|
|||||||
}, [achievements]);
|
}, [achievements]);
|
||||||
|
|
||||||
const resetStats = () => {
|
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('statsData', JSON.stringify(emptyStats));
|
||||||
|
localStorage.setItem('eventLog', JSON.stringify([]));
|
||||||
localStorage.setItem('achievements', JSON.stringify(initialAchievements));
|
localStorage.setItem('achievements', JSON.stringify(initialAchievements));
|
||||||
setStats(emptyStats);
|
setStats(emptyStats);
|
||||||
setClearmodal(false);
|
setClearmodal(false);
|
||||||
@@ -53,18 +61,11 @@ const Stats = () => {
|
|||||||
saveFile(JSON.stringify(stats, null, 2), filename);
|
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';
|
const STATS_SECTION = 'settings:sections.stats';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* <Header title={variables.getMessage(`${STATS_SECTION}.title`)} report={false}>
|
<Header title={variables.getMessage(`${STATS_SECTION}.title`)} report={false}>
|
||||||
<CustomActions>
|
<CustomActions>
|
||||||
<Button
|
<Button
|
||||||
type="settings"
|
type="settings"
|
||||||
@@ -79,7 +80,7 @@ const Stats = () => {
|
|||||||
label={variables.getMessage('settings:buttons.reset')}
|
label={variables.getMessage('settings:buttons.reset')}
|
||||||
/>
|
/>
|
||||||
</CustomActions>
|
</CustomActions>
|
||||||
</Header> */}
|
</Header>
|
||||||
<Modal
|
<Modal
|
||||||
closeTimeoutMS={100}
|
closeTimeoutMS={100}
|
||||||
onRequestClose={() => setClearmodal(false)}
|
onRequestClose={() => setClearmodal(false)}
|
||||||
@@ -97,50 +98,7 @@ const Stats = () => {
|
|||||||
STATS_SECTION={STATS_SECTION}
|
STATS_SECTION={STATS_SECTION}
|
||||||
variables={variables}
|
variables={variables}
|
||||||
/>
|
/>
|
||||||
<span class="text-2xl font-semibold pb-5">Statistics</span>
|
<StatsDashboard stats={stats} variables={variables} STATS_SECTION={STATS_SECTION} />
|
||||||
<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>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
274
src/features/stats/options/sections/StatsDashboard.jsx
Normal file
274
src/features/stats/options/sections/StatsDashboard.jsx
Normal 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;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
const StatsOverview = ({ stats }) => {
|
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 ProgressCircle = ({ percentage }) => {
|
||||||
const circleRadius = 50;
|
const circleRadius = 50;
|
||||||
@@ -36,12 +36,12 @@ const StatsOverview = ({ stats }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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">
|
<div className="bg-modal-content-light dark:bg-modal-content-dark py-6 px-10 rounded flex flex-row items-center">
|
||||||
<ProgressCircle percentage={xpPercentage} />
|
<ProgressCircle percentage={xpPercentage} />
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="text-2xl font-bold">Level {stats.level || 1}</span>
|
<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>
|
</div>
|
||||||
<div className="bg-modal-content-light dark:bg-modal-content-dark py-6 px-10 rounded flex flex-row items-center justify-center">
|
<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
61
src/utils/indexedDB.js
Normal 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);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user