mirror of
https://github.com/mue/mue.git
synced 2026-07-12 02:42:07 +02:00
Update achievements condition and improve stats tracking logic
This commit is contained in:
@@ -29,7 +29,7 @@ const Sidebar = memo(({ sections, currentTab, setCurrentTab }) => {
|
||||
<MdOutlineWarning />
|
||||
<span>{variables.getMessage('settings:reminder.message')}</span>
|
||||
</div>
|
||||
<div class="flex flex-row items-center gap-5">
|
||||
<div className="flex flex-row items-center gap-5">
|
||||
<button
|
||||
className="bg-neutral-900 border-neutral-800 border-2 px-8 py-2 flex flex-row items-center gap-2 rounded"
|
||||
onClick={() => window.location.reload()}
|
||||
|
||||
@@ -59,7 +59,7 @@ function SliderComponent(props) {
|
||||
{variables.getMessage('settings:buttons.reset')}
|
||||
</span>
|
||||
</span>
|
||||
<div class="mx-auto">
|
||||
<div className="mx-auto">
|
||||
<Slider
|
||||
value={Number(value)}
|
||||
onChange={handleChange}
|
||||
|
||||
@@ -375,7 +375,7 @@ const ItemPage = () => {
|
||||
/>
|
||||
</Modal>
|
||||
<div className="itemPage flex flex-row gap-8 2xl:gap-16 justify-between">
|
||||
<div class="flex flex-col-reverse xl:flex-row gap-8 2xl:gap-16">
|
||||
<div className="flex flex-col-reverse xl:flex-row gap-8 2xl:gap-16">
|
||||
<ItemDetails />
|
||||
<div className="itemShowcase">
|
||||
<div className="subHeader">{itemWarning()}</div>
|
||||
|
||||
@@ -113,8 +113,8 @@ function Changelog() {
|
||||
<div className="bg-modal-content-light dark:bg-modal-content-dark w-full ">
|
||||
<article className="rounded p-10 prose dark:prose-invert" ref={changelog}>
|
||||
<div>
|
||||
<h1 class="leading-tight mb-1">{title}</h1>
|
||||
<p class="leading-none mt-0">Published on {date}</p>
|
||||
<h1 className="leading-tight mb-1">{title}</h1>
|
||||
<p className="leading-none mt-0">Published on {date}</p>
|
||||
</div>
|
||||
<Markdown options={{ overrides: { a: { props: { target: '_blank' } } } }}>
|
||||
{content}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"id": "10tabs",
|
||||
"condition": {
|
||||
"type": "tabsOpened",
|
||||
"amount": 3
|
||||
"amount": 10
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -4,13 +4,13 @@ export function checkAchievements(stats) {
|
||||
achievements.forEach((achievement) => {
|
||||
switch (achievement.condition.type) {
|
||||
case 'tabsOpened':
|
||||
if (stats['tabs-opened'] >= achievement.condition.amount) {
|
||||
if (stats['tabs-opened']?.count >= achievement.condition.amount) {
|
||||
achievement.achieved = true;
|
||||
}
|
||||
break;
|
||||
case 'addonInstall':
|
||||
if (stats.marketplace) {
|
||||
if (stats.marketplace['install'] >= achievement.condition.amount) {
|
||||
if (stats.marketplace['install']?.count >= achievement.condition.amount) {
|
||||
achievement.achieved = true;
|
||||
}
|
||||
}
|
||||
@@ -30,21 +30,18 @@ export function newAchievements(stats) {
|
||||
const newAchievements = [];
|
||||
|
||||
checkedAchievements.forEach((achievement) => {
|
||||
if (achievement.achieved && !oldAchievements.includes(achievement.id)) {
|
||||
if (achievement.achieved && !oldAchievements.some((a) => a.id === achievement.id)) {
|
||||
newAchievements.push(achievement);
|
||||
}
|
||||
});
|
||||
|
||||
// add timestamp to new achievements
|
||||
newAchievements.forEach((achievement) => {
|
||||
achievement.timestamp = Date.now();
|
||||
achievement.timestamp = new Date().toISOString();
|
||||
});
|
||||
|
||||
// save new achievements to local storage
|
||||
localStorage.setItem(
|
||||
'achievements',
|
||||
JSON.stringify([...oldAchievements, ...newAchievements.map((achievement) => achievement.id)]),
|
||||
);
|
||||
localStorage.setItem('achievements', JSON.stringify([...oldAchievements, ...newAchievements]));
|
||||
|
||||
return newAchievements;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { checkAchievements, newAchievements } from './condition';
|
||||
function getLocalisedAchievementData(id) {
|
||||
return {
|
||||
name: variables.getMessage(`achievements:${id}.name`),
|
||||
description: variables.getMessage(`achievements:${id}.description`)
|
||||
description: variables.getMessage(`achievements:${id}.description`),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -19,28 +19,70 @@ export default class Stats {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* It takes three arguments, a type, a name, and an action, and then it increments the value of the name in the type
|
||||
* object in localStorage
|
||||
* @param type - The type of event you want to track. This can be anything you want, but I recommend
|
||||
* using something like "click" or "hover"
|
||||
* @param name - The name of the event.
|
||||
* @param action - The action of the event.
|
||||
*/
|
||||
static async postEvent(type, name, action) {
|
||||
static calculateNextLevelXp(level) {
|
||||
return Math.floor(100 * Math.pow(1.5, level - 1));
|
||||
}
|
||||
|
||||
static calculateXpForEvent(eventType, streak) {
|
||||
let baseXp = 10; // Base XP for an event
|
||||
if (eventType === 'tabs-opened') {
|
||||
baseXp = 5;
|
||||
}
|
||||
return baseXp * (1 + streak * 0.1); // Increase XP by 10% for each day in the streak
|
||||
}
|
||||
|
||||
static async postEvent(type, name = '', action = '') {
|
||||
const data = JSON.parse(localStorage.getItem('statsData')) || {};
|
||||
const timestamp = new Date().toISOString();
|
||||
|
||||
if (!name) {
|
||||
data[type] = data[type] + 1 || 1;
|
||||
} else {
|
||||
data[type] = data[type] || {};
|
||||
console.log(`Event Type: ${type}`);
|
||||
console.log(`Event Name: ${name}`);
|
||||
console.log(`Event Action: ${action}`);
|
||||
|
||||
if (action) {
|
||||
data[type][name] = data[type][name] || {};
|
||||
data[type][name][action] = data[type][name][action] + 1 || 1;
|
||||
if (!data[type]) {
|
||||
data[type] = { count: 0, events: [] };
|
||||
}
|
||||
|
||||
data[type].count += 1;
|
||||
data[type].events.push({ name, action, timestamp });
|
||||
|
||||
// Calculate XP and level
|
||||
const streak = data.streak?.current || 0;
|
||||
const lastEventTimestamp = data[type].events[data[type].events.length - 2]?.timestamp;
|
||||
const lastEventDate = lastEventTimestamp ? new Date(lastEventTimestamp).toDateString() : null;
|
||||
const currentEventDate = new Date(timestamp).toDateString();
|
||||
|
||||
if (lastEventDate && lastEventDate !== currentEventDate) {
|
||||
const daysDifference =
|
||||
(new Date(currentEventDate) - new Date(lastEventDate)) / (1000 * 60 * 60 * 24);
|
||||
if (daysDifference === 1) {
|
||||
data.streak = { current: streak + 1 };
|
||||
} else {
|
||||
data[type][name] = data[type][name] + 1 || 1;
|
||||
data.streak = { current: 1 };
|
||||
}
|
||||
} else if (!lastEventDate) {
|
||||
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);
|
||||
|
||||
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}`);
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
localStorage.setItem('statsData', JSON.stringify(data));
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
/* eslint-disable array-callback-return */
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { MdShowChart, MdRestartAlt, MdDownload, MdAccessTime, MdLock } from 'react-icons/md';
|
||||
import { FaTrophy } from 'react-icons/fa';
|
||||
import { MdShowChart, MdRestartAlt, MdDownload } from 'react-icons/md';
|
||||
import { toast } from 'react-toastify';
|
||||
import Modal from 'react-modal';
|
||||
|
||||
import { Button } from 'components/Elements';
|
||||
import { Header, CustomActions, PreferencesWrapper } from 'components/Layout/Settings';
|
||||
import { Header, CustomActions } from 'components/Layout/Settings';
|
||||
import { StatsOverview } from './sections/StatsOverview';
|
||||
import { ClearModal } from './ClearModal';
|
||||
import Achievements from './sections/Achievements';
|
||||
|
||||
import { saveFile } from 'utils/saveFile';
|
||||
import variables from 'config/variables';
|
||||
import {
|
||||
getLocalisedAchievementData,
|
||||
achievements as initialAchievements,
|
||||
checkAchievements,
|
||||
} from 'features/stats/api/achievements';
|
||||
|
||||
const Stats = () => {
|
||||
const [stats, setStats] = useState(() => JSON.parse(localStorage.getItem('statsData')) || {});
|
||||
const [achievements, setAchievements] = useState(initialAchievements);
|
||||
const [achievements, setAchievements] = useState(
|
||||
() => JSON.parse(localStorage.getItem('achievements')) || initialAchievements,
|
||||
);
|
||||
const [clearmodal, setClearmodal] = useState(false);
|
||||
|
||||
const updateAchievements = useCallback(() => {
|
||||
@@ -52,25 +53,6 @@ const Stats = () => {
|
||||
saveFile(JSON.stringify(stats, null, 2), filename);
|
||||
};
|
||||
|
||||
const AchievementElement = ({ key, id, achieved, timestamp }) => {
|
||||
const { name, description } = getLocalisedAchievementData(id);
|
||||
|
||||
return (
|
||||
<div className="achievement" key={key}>
|
||||
{achieved ? <FaTrophy className="trophy" /> : <MdLock className="trophyLocked" />}
|
||||
<div className={'achievementContent' + (achieved ? ' achieved' : '')}>
|
||||
{achieved && timestamp && (
|
||||
<span className="timestamp">
|
||||
<MdAccessTime /> {new Date(timestamp).toLocaleDateString()}
|
||||
</span>
|
||||
)}
|
||||
<span className="achievementTitle">{name}</span>
|
||||
<span className="subtitle">{achieved ? description : '?????'}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const StatsElement = ({ title, value }) => (
|
||||
<div>
|
||||
<span className="subtitle">{title}</span>
|
||||
@@ -82,7 +64,7 @@ const Stats = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header title={variables.getMessage(`${STATS_SECTION}.title`)} report={false}>
|
||||
{/* <Header title={variables.getMessage(`${STATS_SECTION}.title`)} report={false}>
|
||||
<CustomActions>
|
||||
<Button
|
||||
type="settings"
|
||||
@@ -97,7 +79,7 @@ const Stats = () => {
|
||||
label={variables.getMessage('settings:buttons.reset')}
|
||||
/>
|
||||
</CustomActions>
|
||||
</Header>
|
||||
</Header> */}
|
||||
<Modal
|
||||
closeTimeoutMS={100}
|
||||
onRequestClose={() => setClearmodal(false)}
|
||||
@@ -108,6 +90,14 @@ const Stats = () => {
|
||||
>
|
||||
<ClearModal modalClose={() => setClearmodal(false)} resetStats={resetStats} />
|
||||
</Modal>
|
||||
<StatsOverview stats={stats} />
|
||||
<Achievements
|
||||
achievements={achievements}
|
||||
getUnlockedCount={getUnlockedCount}
|
||||
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">
|
||||
@@ -116,23 +106,23 @@ const Stats = () => {
|
||||
<div className="statGrid">
|
||||
<StatsElement
|
||||
title={variables.getMessage(`${STATS_SECTION}.sections.tabs_opened`)}
|
||||
value={stats['tabs-opened'] || 0}
|
||||
value={stats['tabs-opened']?.count || 0}
|
||||
/>
|
||||
<StatsElement
|
||||
title={variables.getMessage(`${STATS_SECTION}.sections.backgrounds_favourited`)}
|
||||
value={stats['background-favourite'] || 0}
|
||||
value={stats['background-favourite']?.count || 0}
|
||||
/>
|
||||
<StatsElement
|
||||
title={variables.getMessage(`${STATS_SECTION}.sections.backgrounds_downloaded`)}
|
||||
value={stats.feature ? stats.feature['background-download'] || 0 : 0}
|
||||
value={stats['background-download']?.count || 0}
|
||||
/>
|
||||
<StatsElement
|
||||
title={variables.getMessage(`${STATS_SECTION}.sections.quotes_favourited`)}
|
||||
value={stats.feature ? stats.feature['quoted-favourite'] || 0 : 0}
|
||||
value={stats['quoted-favourite']?.count || 0}
|
||||
/>
|
||||
<StatsElement
|
||||
title={variables.getMessage(`${STATS_SECTION}.sections.quicklinks_added`)}
|
||||
value={stats.feature ? stats.feature['quicklink-add'] || 0 : 0}
|
||||
value={stats['quicklink-add']?.count || 0}
|
||||
/>
|
||||
<StatsElement
|
||||
title={variables.getMessage(`${STATS_SECTION}.sections.settings_changed`)}
|
||||
@@ -142,49 +132,14 @@ const Stats = () => {
|
||||
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 className="statSection leftPanel">
|
||||
<span className="title">{variables.getMessage(`${STATS_SECTION}.achievements`)}</span>
|
||||
<br />
|
||||
<span className="subtitle">
|
||||
{variables.getMessage(`${STATS_SECTION}.unlocked`, {
|
||||
count: `${getUnlockedCount}/${achievements.length}`,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<div className="achievements">
|
||||
<div className="achievementsGrid">
|
||||
{achievements.map((achievement, index) => {
|
||||
if (achievement.achieved) {
|
||||
return (
|
||||
<AchievementElement
|
||||
key={index}
|
||||
id={achievement.id}
|
||||
achieved={achievement.achieved}
|
||||
timestamp={achievement.timestamp}
|
||||
/>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
<span className="title">{variables.getMessage(`${STATS_SECTION}.locked`)}</span>
|
||||
<PreferencesWrapper>
|
||||
<div className="achievementsGrid preferencesInactive">
|
||||
{achievements.map((achievement, index) => {
|
||||
if (!achievement.achieved) {
|
||||
return (
|
||||
<AchievementElement
|
||||
key={index}
|
||||
id={achievement.id}
|
||||
achieved={achievement.achieved}
|
||||
/>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
</PreferencesWrapper>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
110
src/features/stats/options/sections/Achievements.jsx
Normal file
110
src/features/stats/options/sections/Achievements.jsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import React, { useState } from 'react';
|
||||
import { FaTrophy } from 'react-icons/fa';
|
||||
import { MdAccessTime, MdLock } from 'react-icons/md';
|
||||
import { PreferencesWrapper } from 'components/Layout/Settings';
|
||||
import { getLocalisedAchievementData } from 'features/stats/api/achievements';
|
||||
import { motion } from 'framer-motion';
|
||||
import { FaChevronDown, FaChevronUp } from 'react-icons/fa';
|
||||
|
||||
const AchievementElement = ({ achievementKey, id, achieved, timestamp }) => {
|
||||
if (!id) {
|
||||
return null; // Ensure id is not undefined
|
||||
}
|
||||
|
||||
const { name, description } = getLocalisedAchievementData(id);
|
||||
|
||||
return (
|
||||
<div className="achievement" key={achievementKey}>
|
||||
{achieved ? (
|
||||
<FaTrophy className="trophy flex-shrink-0" />
|
||||
) : (
|
||||
<MdLock className="trophyLocked flex-shrink-0" />
|
||||
)}
|
||||
<div className={'achievementContent' + (achieved ? ' achieved' : '')}>
|
||||
{achieved && timestamp && (
|
||||
<span className="timestamp">
|
||||
<MdAccessTime /> {new Date(timestamp).toLocaleDateString()}
|
||||
</span>
|
||||
)}
|
||||
<span className="achievementTitle">{name}</span>
|
||||
<span className="subtitle">{achieved ? description : '?????'}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Achievements = ({ achievements, getUnlockedCount, STATS_SECTION, variables }) => {
|
||||
const [showLocked, setShowLocked] = useState(false);
|
||||
|
||||
const toggleLocked = () => {
|
||||
setShowLocked(!showLocked);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="achievementsSection">
|
||||
<div className="flex flex-row justify-between items-center py-5">
|
||||
<span className="text-2xl font-semibold">
|
||||
{variables.getMessage(`${STATS_SECTION}.achievements`)}
|
||||
</span>
|
||||
<span className="text-neutral-200">
|
||||
{variables.getMessage(`${STATS_SECTION}.unlocked`, {
|
||||
count: `${getUnlockedCount}/${achievements.length}`,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<div className="achievements">
|
||||
<div className="achievementsGrid">
|
||||
{achievements.map((achievement, index) => {
|
||||
if (achievement.achieved) {
|
||||
return (
|
||||
<AchievementElement
|
||||
key={index}
|
||||
achievementKey={index}
|
||||
id={achievement.id}
|
||||
achieved={achievement.achieved}
|
||||
timestamp={achievement.timestamp}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null; // Ensure a value is returned
|
||||
})}
|
||||
</div>
|
||||
<div
|
||||
className="flex flex-row justify-between items-center py-5 cursor-pointer"
|
||||
onClick={toggleLocked}
|
||||
>
|
||||
<span className="text-xl font-semibold">
|
||||
{variables.getMessage(`${STATS_SECTION}.locked`)}
|
||||
</span>
|
||||
<span className="text-neutral-200">
|
||||
{showLocked ? <FaChevronUp /> : <FaChevronDown />}
|
||||
</span>
|
||||
</div>
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: showLocked ? 'auto' : 0, opacity: showLocked ? 1 : 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<div className="achievementsGrid preferencesInactive">
|
||||
{achievements.map((achievement, index) => {
|
||||
if (!achievement.achieved) {
|
||||
return (
|
||||
<AchievementElement
|
||||
key={index}
|
||||
achievementKey={index}
|
||||
id={achievement.id}
|
||||
achieved={achievement.achieved}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null; // Ensure a value is returned
|
||||
})}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Achievements;
|
||||
57
src/features/stats/options/sections/StatsOverview.jsx
Normal file
57
src/features/stats/options/sections/StatsOverview.jsx
Normal file
@@ -0,0 +1,57 @@
|
||||
const StatsOverview = ({ stats }) => {
|
||||
const xpPercentage = Math.floor((stats.xp / stats.nextLevelXp) * 100);
|
||||
|
||||
const ProgressCircle = ({ percentage }) => {
|
||||
const circleRadius = 50;
|
||||
const circleCircumference = 2 * Math.PI * circleRadius;
|
||||
const progress = circleCircumference - (percentage / 100) * circleCircumference;
|
||||
|
||||
return (
|
||||
<div className="relative flex items-center justify-center w-24 h-24 p-10">
|
||||
<svg className="absolute w-full h-full transform -rotate-90">
|
||||
<circle
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
r={circleRadius}
|
||||
stroke="currentColor"
|
||||
strokeWidth="8"
|
||||
className="text-gray-300"
|
||||
fill="none"
|
||||
/>
|
||||
<circle
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
r={circleRadius}
|
||||
stroke="currentColor"
|
||||
strokeWidth="8"
|
||||
className="text-[#D21A11]"
|
||||
fill="none"
|
||||
strokeDasharray={circleCircumference}
|
||||
strokeDashoffset={progress}
|
||||
/>
|
||||
</svg>
|
||||
<span className="absolute text-xl font-bold text-white">{percentage}%</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="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>
|
||||
</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="flex flex-col">
|
||||
<span className="text-2xl font-bold">Streak</span>
|
||||
<span>{`${stats.streak?.current || 0} ${stats.streak?.current === 1 ? 'day' : 'days'}`}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { StatsOverview as default, StatsOverview };
|
||||
Reference in New Issue
Block a user