diff --git a/bun.lockb b/bun.lockb index c6799fb8..addcca42 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/src/components/Form/Settings/Radio/Radio.jsx b/src/components/Form/Settings/Radio/Radio.jsx index a9e3af85..cfea573a 100644 --- a/src/components/Form/Settings/Radio/Radio.jsx +++ b/src/components/Form/Settings/Radio/Radio.jsx @@ -59,18 +59,18 @@ function Radio(props) { key={option.name} label={option.name} value={option.value} - className="data-[checked]:bg-white/10 group relative flex cursor-pointer rounded-lg bg-white/5 data-[checked]:hover:bg-neutral-700 hover:bg-neutral-700 py-4 px-5 text-white shadow-md transition focus:outline-none data-[focus]:outline-1 data-[focus]:outline-white" + className="data-[checked]:bg-white/10 group relative flex cursor-pointer rounded-lg bg-white/5 data-[checked]:hover:bg-neutral-700 hover:bg-neutral-700 py-4 px-5 dark:text-white text-black shadow-md transition focus:outline-none data-[focus]:outline-1 data-[focus]:outline-white" >
-

{option.name}

-
+

{option.name}

+
{option.subname}
10%
- +
))} diff --git a/src/components/Form/Settings/Text/Text.jsx b/src/components/Form/Settings/Text/Text.jsx index 4228bfb6..4413d552 100644 --- a/src/components/Form/Settings/Text/Text.jsx +++ b/src/components/Form/Settings/Text/Text.jsx @@ -1,20 +1,25 @@ import variables from 'config/variables'; -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { toast } from 'react-toastify'; import { Field, Label, Textarea, Input } from '@headlessui/react'; import { MdRefresh } from 'react-icons/md'; - import clsx from 'clsx'; - import EventBus from 'utils/eventbus'; function Text(props) { const [value, setValue] = useState(localStorage.getItem(props.name) || ''); + useEffect(() => { + if (props.code) { + // import('prismjs/themes/prism.css'); + const Prism = require('prismjs'); + Prism.highlightAll(); + } + }, [value, props.code]); + const handleChange = (e) => { let { value } = e.target; - // Alex wanted font to work with montserrat and Montserrat, so I made it work if (props.upperCaseFirst === true) { value = value.charAt(0).toUpperCase() + value.slice(1); } @@ -62,6 +67,10 @@ function Text(props) { rows={4} /> + ) : props.code ? ( +
+          {value}
+        
) : ( <> favourite()} className="topicons" /> - ) : ( - favourite()} className="topicons" /> - ), - ); + const [favourited, setFavourited] = useState(false); - const buttons = { - favourited: favourite()} className="topicons" />, - unfavourited: favourite()} className="topicons" />, - }; + useEffect(() => { + const isFavourited = localStorage.getItem('favourite') !== null; + setFavourited(isFavourited); + tooltipText( + isFavourited + ? variables.getMessage('widgets.quote.unfavourite') + : variables.getMessage('widgets.quote.favourite'), + ); + }, [tooltipText]); - async function favourite() { + const favourite = async () => { if (localStorage.getItem('favourite')) { localStorage.removeItem('favourite'); - setFavourited(buttons.unfavourited); + setFavourited(false); tooltipText(variables.getMessage('widgets.quote.favourite')); variables.stats.postEvent('feature', 'background', 'favourite'); } else { @@ -82,31 +81,30 @@ function Favourite({ credit, offline, pun, tooltipText }) { pun, }), ); - - setFavourited(buttons.favourited); - - tooltipText(variables.getMessage('widgets.quote.unfavourite')); - - variables.stats.postEvent('feature', 'background', 'unfavourite'); } } - return setFavourited(buttons.favourited); + setFavourited(true); + tooltipText(variables.getMessage('widgets.quote.unfavourite')); + variables.stats.postEvent('feature', 'background', 'unfavourite'); + return; } + + setFavourited(true); + tooltipText(variables.getMessage('widgets.quote.unfavourite')); + variables.stats.postEvent('feature', 'background', 'unfavourite'); } - } + }; if (localStorage.getItem('backgroundType') === 'colour') { return null; } - tooltipText( - localStorage.getItem('favourite') - ? variables.getMessage('widgets.quote.unfavourite') - : variables.getMessage('widgets.quote.favourite'), + return favourited ? ( + + ) : ( + ); - - return favourited; } export default Favourite; diff --git a/src/features/misc/sections/About.jsx b/src/features/misc/sections/About.jsx index cad4a910..ce8bfd51 100644 --- a/src/features/misc/sections/About.jsx +++ b/src/features/misc/sections/About.jsx @@ -1,5 +1,5 @@ import variables from 'config/variables'; -import { useState, useEffect } from 'react'; +import { useState, useEffect, useCallback, useMemo } from 'react'; import { MdEmail, MdContactPage } from 'react-icons/md'; import { FaDiscord } from 'react-icons/fa'; import { SiGithubsponsors, SiOpencollective, SiX } from 'react-icons/si'; @@ -18,15 +18,20 @@ function About() { ); const [loading, setLoading] = useState(variables.getMessage('modals.main.loading')); - const controller = new AbortController(); + const controller = useMemo(() => new AbortController(), []); + useEffect(() => { return () => { controller.abort(); }; - }, []); + }, [controller]); - const getGitHubData = async () => { - let contributors, sponsors, photographers, curators, versionData; + const getGitHubData = useCallback(async () => { + let contributors = []; + let sponsors = []; + let photographers = []; + let curators = []; + let versionData = []; try { versionData = await ( @@ -81,7 +86,7 @@ function About() { setLoading(variables.getMessage('settings:sections.about.version.error.description')); } - if (sponsors.length === 0) { + if (!sponsors || sponsors.length === 0) { sponsors = [{ handle: 'empty' }]; } @@ -89,7 +94,7 @@ function About() { return; } - const newVersion = versionData[0].tag_name; + const newVersion = versionData[0]?.tag_name || ''; let update = variables.getMessage('settings:sections.about.version.no_update'); @@ -108,7 +113,7 @@ function About() { setPhotographers(photographers); setCurators(curators); setLoading(null); - }; + }, [controller]); useEffect(() => { if (navigator.onLine === false || localStorage.getItem('offlineMode') === 'true') { @@ -118,11 +123,11 @@ function About() { } getGitHubData(); - }, []); + }, [getGitHubData]); - return ( -
-
+ const Header = () => { + return ( +
Logo
@@ -179,9 +184,15 @@ function About() {
+ ); + }; -
- {variables.getMessage('settings:sections.about.contact_us')} + const ContactBlock = () => { + return ( +
+ + {variables.getMessage('settings:sections.about.contact_us')} +
+ ); + }; -
- {variables.getMessage('settings:sections.about.support_mue')} + const SponsorBlock = () => { + return ( +
+ + {variables.getMessage('settings:sections.about.support_mue')} +

{variables.getMessage('settings:sections.about.support_subtitle')}

+ ); + }; + + return ( +
+
+
+ + +
+ +
+ + {variables.getMessage('settings:sections.about.contributors')} + +

{loading}

+
+ {contributors.map(({ login, id }) => ( + + + {login} + + + ))} +
+
-
- - {variables.getMessage('settings:sections.about.contributors')} - -

{loading}

-
- {contributors.map(({ login, id }) => ( - - - {login} - - - ))} -
-
-
{variables.getMessage('settings:sections.about.supporters')}

{loading}

diff --git a/src/features/misc/sections/Advanced.jsx b/src/features/misc/sections/Advanced.jsx index 65931363..c73fe729 100644 --- a/src/features/misc/sections/Advanced.jsx +++ b/src/features/misc/sections/Advanced.jsx @@ -119,7 +119,7 @@ function AdvancedOptions() { {variables.getMessage('settings:sections.advanced.custom_css')} - + )} {subSection === '' && ( diff --git a/src/features/stats/api/achievements/condition.js b/src/features/stats/api/achievements/condition.js index cc137b7d..e273ba59 100644 --- a/src/features/stats/api/achievements/condition.js +++ b/src/features/stats/api/achievements/condition.js @@ -1,21 +1,60 @@ import { achievements } from './index'; +import statsModule from '../stats'; + +export async function checkAchievements() { + // First get the events data which contains all user actions + const events = await statsModule.getStats(); + + // Calculate aggregated stats + const aggregatedStats = { + 'new-tab': events.filter((event) => event.type === 'new-tab').length, + 'marketplace-install': events.filter((event) => event.type === 'marketplace-install').length, + totalXp: events.reduce((sum, event) => sum + (event.xpGained || 0), 0), + level: statsModule.level || 1, + }; + + console.log('Checking achievements with aggregated stats:', aggregatedStats); -export function checkAchievements(stats) { achievements.forEach((achievement) => { switch (achievement.condition.type) { case 'tabsOpened': - if (stats['tabs-opened']?.count >= achievement.condition.amount) { + console.log( + `Checking achievement: ${achievement.id}, Condition: ${achievement.condition.type}, Required: ${achievement.condition.amount}, Current: ${aggregatedStats['new-tab']}`, + ); + if (aggregatedStats['new-tab'] >= achievement.condition.amount) { achievement.achieved = true; } break; + case 'addonInstall': - if (stats.marketplace) { - if (stats.marketplace['install']?.count >= achievement.condition.amount) { - achievement.achieved = true; - } + console.log( + `Checking achievement: ${achievement.id}, Condition: ${achievement.condition.type}, Required: ${achievement.condition.amount}, Current: ${aggregatedStats['marketplace-install']}`, + ); + if (aggregatedStats['marketplace-install'] >= achievement.condition.amount) { + achievement.achieved = true; } break; + + case 'reachLevel': + console.log( + `Checking achievement: ${achievement.id}, Condition: ${achievement.condition.type}, Required: ${achievement.condition.amount}, Current: ${aggregatedStats.level}`, + ); + if (aggregatedStats.level >= achievement.condition.amount) { + achievement.achieved = true; + } + break; + + case 'earnXp': + console.log( + `Checking achievement: ${achievement.id}, Condition: ${achievement.condition.type}, Required: ${achievement.condition.amount}, Current: ${aggregatedStats.totalXp}`, + ); + if (aggregatedStats.totalXp >= achievement.condition.amount) { + achievement.achieved = true; + } + break; + default: + console.warn(`Unknown achievement condition type: ${achievement.condition.type}`); break; } }); @@ -23,25 +62,62 @@ export function checkAchievements(stats) { return achievements; } -export function newAchievements(stats) { - // calculate new achievements +export async function newAchievements() { + // Get previously achieved achievements const oldAchievements = JSON.parse(localStorage.getItem('achievements')) || []; - const checkedAchievements = checkAchievements(stats); + + // Check for new achievements + const checkedAchievements = await checkAchievements(); const newAchievements = []; checkedAchievements.forEach((achievement) => { - if (achievement.achieved && !oldAchievements.some((a) => a.id === achievement.id)) { - newAchievements.push(achievement); + const isNewAchievement = + achievement.achieved && !oldAchievements.some((a) => a.id === achievement.id); + + if (isNewAchievement) { + const newAchievement = { + ...achievement, + timestamp: new Date().toISOString(), + }; + newAchievements.push(newAchievement); } }); - // add timestamp to new achievements - newAchievements.forEach((achievement) => { - achievement.timestamp = new Date().toISOString(); - }); - - // save new achievements to local storage - localStorage.setItem('achievements', JSON.stringify([...oldAchievements, ...newAchievements])); + // Update stored achievements + if (newAchievements.length > 0) { + const updatedAchievements = [...oldAchievements, ...newAchievements]; + localStorage.setItem('achievements', JSON.stringify(updatedAchievements)); + } return newAchievements; } + +// Helper function to get achievement progress +export async function getAchievementProgress(achievementId) { + const achievement = achievements.find((a) => a.id === achievementId); + if (!achievement) return null; + + const events = await statsModule.getStats(); + let current = 0; + + switch (achievement.condition.type) { + case 'tabsOpened': + current = events.filter((event) => event.type === 'new-tab').length; + break; + case 'addonInstall': + current = events.filter((event) => event.type === 'marketplace-install').length; + break; + case 'reachLevel': + current = statsModule.level || 1; + break; + case 'earnXp': + current = events.reduce((sum, event) => sum + (event.xpGained || 0), 0); + break; + } + + return { + current, + required: achievement.condition.amount, + percentage: Math.min(100, Math.floor((current / achievement.condition.amount) * 100)), + }; +} diff --git a/src/features/stats/api/stats.js b/src/features/stats/api/stats.js index b19a3fdb..cdf2c539 100644 --- a/src/features/stats/api/stats.js +++ b/src/features/stats/api/stats.js @@ -5,7 +5,7 @@ import variables from 'config/variables'; export default class Stats { static async achievementTrigger(stats) { - const newAchievement = newAchievements(stats); + const newAchievement = await newAchievements(stats); newAchievement.forEach((achievement) => { if (achievement) { const { name } = getLocalisedAchievementData(achievement.id); @@ -54,7 +54,7 @@ export default class Stats { return Math.round(baseXp * (1 + streak * 0.1)); } - static async addEvent(eventLog, event) { + static async addEvent(event) { await addEvent(event); } @@ -118,12 +118,14 @@ export default class Stats { console.log(`Event Action: ${action}`); const xpGained = this.calculateXpForEvent(type, statsData.streak.current); - await this.addEvent(eventLog, { type, name, action, timestamp, xpGained }); + await this.addEvent({ type, name, action, timestamp, xpGained }); this.updateStatsData(statsData, xpGained); this.calculateStreak(statsData, timestamp); + console.log(`Updated Stats Data: ${JSON.stringify(statsData)}`); + localStorage.setItem('statsData', JSON.stringify(statsData)); - this.achievementTrigger(statsData); + await this.achievementTrigger(statsData); } static async getStats(type, name, action, startDate, endDate) { diff --git a/src/features/stats/options/StatsOptions.jsx b/src/features/stats/options/StatsOptions.jsx index 5fe5ee8e..67f6793a 100644 --- a/src/features/stats/options/StatsOptions.jsx +++ b/src/features/stats/options/StatsOptions.jsx @@ -24,10 +24,10 @@ const Stats = () => { ); const [clearmodal, setClearmodal] = useState(false); - const updateAchievements = useCallback(() => { - const achieved = checkAchievements(stats); + const updateAchievements = useCallback(async () => { + const achieved = await checkAchievements(); setAchievements(achieved); - }, [stats]); + }, []); useEffect(() => { updateAchievements();