From ed4ef5929ff93ef375f60decf10417bba682c749 Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Sun, 8 Dec 2024 22:28:20 +0700 Subject: [PATCH 01/95] New daisyUI commit - new UI - this commit for Sounds - decouple & organization - add Sonner Toast --- package-lock.json | 1125 ++++++++++++++--- package.json | 36 +- postcss.config.js | 6 + public/styles/style.css | 1 + src/App.jsx | 126 +- src/components/Homepage.jsx | 106 +- src/components/general/AccentDropdown.jsx | 44 +- src/components/general/LoadingOverlay.jsx | 22 +- src/components/general/ToastNotification.jsx | 20 - src/components/general/TopNavBar.jsx | 153 ++- src/components/setting_page/Appearance.jsx | 133 +- src/components/setting_page/Settings.jsx | 14 +- src/components/sound_page/PracticeSound.jsx | 341 +++-- src/components/sound_page/ReviewCard.jsx | 77 +- src/components/sound_page/SoundList.jsx | 262 ++-- .../sound_page/SoundPracticeCard.jsx | 183 ++- src/components/sound_page/SoundVideoModal.jsx | 70 + src/components/sound_page/WatchVideoCard.jsx | 46 + .../{ => hooks}/usePlaybackFunction.jsx | 18 +- .../{ => hooks}/useRecordingFunction.jsx | 21 +- src/components/sound_page/hooks/useReview.jsx | 40 + .../{ => hooks}/useSoundVideoMapping.jsx | 2 +- src/index.jsx | 2 +- src/styles/index.css | 7 + src/ui/Container.jsx | 16 + src/utils/ThemeContext/ThemeProvider.jsx | 57 + .../ThemeContext/ThemeProviderContext.jsx | 8 + src/utils/ThemeContext/useTheme.jsx | 12 + src/utils/ThemeProvider.jsx | 50 - src/utils/ThemeSwitcher.jsx | 39 - src/utils/phonemeUtils.jsx | 9 + src/utils/sonnerCustomToast.jsx | 29 + src/utils/useTheme.jsx | 4 - tailwind.config.js | 33 + 34 files changed, 2106 insertions(+), 1006 deletions(-) create mode 100644 postcss.config.js delete mode 100644 src/components/general/ToastNotification.jsx create mode 100644 src/components/sound_page/SoundVideoModal.jsx create mode 100644 src/components/sound_page/WatchVideoCard.jsx rename src/components/sound_page/{ => hooks}/usePlaybackFunction.jsx (91%) rename src/components/sound_page/{ => hooks}/useRecordingFunction.jsx (88%) create mode 100644 src/components/sound_page/hooks/useReview.jsx rename src/components/sound_page/{ => hooks}/useSoundVideoMapping.jsx (98%) create mode 100644 src/styles/index.css create mode 100644 src/ui/Container.jsx create mode 100644 src/utils/ThemeContext/ThemeProvider.jsx create mode 100644 src/utils/ThemeContext/ThemeProviderContext.jsx create mode 100644 src/utils/ThemeContext/useTheme.jsx delete mode 100644 src/utils/ThemeProvider.jsx delete mode 100644 src/utils/ThemeSwitcher.jsx create mode 100644 src/utils/phonemeUtils.jsx create mode 100644 src/utils/sonnerCustomToast.jsx delete mode 100644 src/utils/useTheme.jsx create mode 100644 tailwind.config.js diff --git a/package-lock.json b/package-lock.json index 7b0acbcd0..208f6f569 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,39 +1,40 @@ { "name": "ispeakerreact", - "version": "2.4.1", + "version": "3.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ispeakerreact", - "version": "2.4.1", + "version": "3.0.0", "license": "Apache-2.0", "dependencies": { - "@dnd-kit/core": "^6.2.0", - "@dnd-kit/sortable": "^9.0.0", - "@react-spring/web": "^9.7.5", + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", "bootstrap": "^5.3.3", "cors": "^2.8.5", "dexie": "^4.0.10", - "electron-log": "^5.2.3", + "electron-log": "^5.2.4", "electron-squirrel-startup": "^1.0.1", - "express": "^4.21.1", + "express": "^4.21.2", "he": "^1.2.0", "i18next": "^24.0.5", - "i18next-browser-languagedetector": "^8.0.0", + "i18next-browser-languagedetector": "^8.0.1", "i18next-http-backend": "^3.0.1", "lodash": "^4.17.21", "masonry-layout": "^4.2.2", "mime": "^4.0.4", "nprogress": "^0.2.0", - "react": "^18.3.1", + "react": "^19.0.0", "react-bootstrap": "^2.10.6", - "react-bootstrap-icons": "^1.11.4", - "react-dom": "^18.3.1", + "react-bootstrap-icons": "^1.11.5", + "react-dom": "^19.0.0", "react-flip-toolkit": "^7.2.4", "react-i18next": "^15.1.3", + "react-icons": "^5.4.0", "react-loading-skeleton": "^3.5.0", - "react-router-dom": "^7.0.2" + "react-router-dom": "^7.0.2", + "sonner": "^1.7.1" }, "devDependencies": { "@electron-forge/cli": "^7.6.0", @@ -46,25 +47,42 @@ "@electron-forge/plugin-fuses": "^7.6.0", "@electron/fuses": "^1.8.0", "@eslint/js": "^9.16.0", - "@types/react": "^18.3.12", - "@types/react-dom": "^18.3.1", + "@tailwindcss/typography": "^0.5.15", + "@types/react": "^19.0.1", + "@types/react-dom": "^19.0.1", "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.20", "concurrently": "^9.1.0", "cross-env": "^7.0.3", + "daisyui": "^4.12.14", "electron": "^33.2.1", "eslint": "^9.16.0", "eslint-plugin-react": "^7.37.2", - "eslint-plugin-react-hooks": "^5.1.0-rc.0", + "eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-react-refresh": "^0.4.16", "globals": "^15.13.0", + "postcss": "^8.4.49", "rollup-plugin-visualizer": "^5.12.0", - "vite": "^6.0.2", + "tailwindcss": "^3.4.16", + "vite": "^6.0.3", "wait-on": "^8.0.1" }, "optionalDependencies": { "appdmg": "latest" } }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", @@ -364,9 +382,9 @@ } }, "node_modules/@dnd-kit/core": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.2.0.tgz", - "integrity": "sha512-KVK/CJmaYGTxTPU6P0+Oy4itgffTUa80B8317sXzfOr1qUzSL29jE7Th11llXiu2haB7B9Glpzo2CDElin+geQ==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", @@ -378,15 +396,15 @@ } }, "node_modules/@dnd-kit/sortable": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-9.0.0.tgz", - "integrity": "sha512-3/9r8Mmba0nKTbo8kPnVSFZKf/VSy94nXZ3aUwzPEh78j/LooQ/EFKRZENak4PHKBkN53mgTF/z+Sd8H+FcAnQ==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz", + "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==", "dependencies": { "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { - "@dnd-kit/core": "^6.2.0", + "@dnd-kit/core": "^6.3.0", "react": ">=16.8.0" } }, @@ -2527,6 +2545,102 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", @@ -2671,6 +2785,16 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@popperjs/core": { "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", @@ -2694,72 +2818,6 @@ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@react-spring/animated": { - "version": "9.7.5", - "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.5.tgz", - "integrity": "sha512-Tqrwz7pIlsSDITzxoLS3n/v/YCUHQdOIKtOJf4yL6kYVSDTSmVK1LI1Q3M/uu2Sx4X3pIWF3xLUhlsA6SPNTNg==", - "dependencies": { - "@react-spring/shared": "~9.7.5", - "@react-spring/types": "~9.7.5" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/@react-spring/core": { - "version": "9.7.5", - "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.7.5.tgz", - "integrity": "sha512-rmEqcxRcu7dWh7MnCcMXLvrf6/SDlSokLaLTxiPlAYi11nN3B5oiCUAblO72o+9z/87j2uzxa2Inm8UbLjXA+w==", - "dependencies": { - "@react-spring/animated": "~9.7.5", - "@react-spring/shared": "~9.7.5", - "@react-spring/types": "~9.7.5" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/react-spring/donate" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/@react-spring/rafz": { - "version": "9.7.5", - "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.7.5.tgz", - "integrity": "sha512-5ZenDQMC48wjUzPAm1EtwQ5Ot3bLIAwwqP2w2owG5KoNdNHpEJV263nGhCeKKmuA3vG2zLLOdu3or6kuDjA6Aw==" - }, - "node_modules/@react-spring/shared": { - "version": "9.7.5", - "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.7.5.tgz", - "integrity": "sha512-wdtoJrhUeeyD/PP/zo+np2s1Z820Ohr/BbuVYv+3dVLW7WctoiN7std8rISoYoHpUXtbkpesSKuPIw/6U1w1Pw==", - "dependencies": { - "@react-spring/rafz": "~9.7.5", - "@react-spring/types": "~9.7.5" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/@react-spring/types": { - "version": "9.7.5", - "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.7.5.tgz", - "integrity": "sha512-HVj7LrZ4ReHWBimBvu2SKND3cDVUPWKLqRTmWe/fNY6o1owGOX0cAHbdPDTMelgBlVbrTKrre6lFkhqGZErK/g==" - }, - "node_modules/@react-spring/web": { - "version": "9.7.5", - "resolved": "https://registry.npmjs.org/@react-spring/web/-/web-9.7.5.tgz", - "integrity": "sha512-lmvqGwpe+CSttsWNZVr+Dg62adtKhauGwLyGE/RRyZ8AAMLgb9x3NDMA5RMElXo+IMyTkPp7nxTB8ZQlmhb6JQ==", - "dependencies": { - "@react-spring/animated": "~9.7.5", - "@react-spring/core": "~9.7.5", - "@react-spring/shared": "~9.7.5", - "@react-spring/types": "~9.7.5" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/@restart/hooks": { "version": "0.4.16", "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.16.tgz", @@ -3097,6 +3155,34 @@ "node": ">=10" } }, + "node_modules/@tailwindcss/typography": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.15.tgz", + "integrity": "sha512-AqhlCXl+8grUz8uqExv5OTtgpjuVIwFTSXTrh8y9/pw6q2ek7fJ+Y8ZEVw7EB2DCcuCOtEjf9w3+J3rzts01uA==", + "dev": true, + "dependencies": { + "lodash.castarray": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.merge": "^4.6.2", + "postcss-selector-parser": "6.0.10" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20" + } + }, + "node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -3236,24 +3322,18 @@ "undici-types": "~6.19.2" } }, - "node_modules/@types/prop-types": { - "version": "15.7.12", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", - "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==" - }, "node_modules/@types/react": { - "version": "18.3.12", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz", - "integrity": "sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==", + "version": "19.0.1", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.1.tgz", + "integrity": "sha512-YW6614BDhqbpR5KtUYzTA+zlA7nayzJRA9ljz9CQoxthR0sDisYZLuvSMsil36t4EH/uAt8T52Xb4sVw17G+SQ==", "dependencies": { - "@types/prop-types": "*", "csstype": "^3.0.2" } }, "node_modules/@types/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==", + "version": "19.0.1", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.1.tgz", + "integrity": "sha512-hljHij7MpWPKF6u5vojuyfV0YA4YURsQG7KT6SzV0Zs2BXAtgdTxG6A229Ub/xiWV4w/7JL8fi6aAyjshH4meA==", "dev": true, "dependencies": { "@types/react": "*" @@ -3462,6 +3542,25 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/appdmg": { "version": "0.6.6", "resolved": "https://registry.npmjs.org/appdmg/-/appdmg-0.6.6.tgz", @@ -3490,6 +3589,12 @@ "node": ">=8.5" } }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -3694,6 +3799,43 @@ "node": ">=0.8" } }, + "node_modules/autoprefixer": { + "version": "10.4.20", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", + "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "browserslist": "^4.23.3", + "caniuse-lite": "^1.0.30001646", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -3755,6 +3897,18 @@ } ] }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -4087,6 +4241,15 @@ "node": ">=6" } }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001686", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001686.tgz", @@ -4123,6 +4286,42 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/chownr": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", @@ -4509,11 +4708,61 @@ "node": ">=12.10" } }, + "node_modules/css-selector-tokenizer": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.8.0.tgz", + "integrity": "sha512-Jd6Ig3/pe62/qe5SBPTN8h8LeUg/pT4lLgtavPf7updwwHpvFzxvOQBHYj2LZDMjUnBzgvIUSjRcf6oT5HzHFg==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "fastparse": "^1.1.2" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, + "node_modules/culori": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/culori/-/culori-3.3.0.tgz", + "integrity": "sha512-pHJg+jbuFsCjz9iclQBqyL3B2HLCBF71BwVNujUYEvCeQMvV97R59MNK3R2+jgJ3a1fcZgI9B3vYgz8lzr/BFQ==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/daisyui": { + "version": "4.12.14", + "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-4.12.14.tgz", + "integrity": "sha512-hA27cdBasdwd4/iEjn+aidoCrRroDuo3G5W9NDKaVCJI437Mm/3eSL/2u7MkZ0pt8a+TrYF3aT2pFVemTS3how==", + "dev": true, + "dependencies": { + "css-selector-tokenizer": "^0.8", + "culori": "^3", + "picocolors": "^1", + "postcss-js": "^4" + }, + "engines": { + "node": ">=16.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/daisyui" + } + }, "node_modules/data-view-buffer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", @@ -4738,6 +4987,12 @@ "resolved": "https://registry.npmjs.org/dexie/-/dexie-4.0.10.tgz", "integrity": "sha512-eM2RzuR3i+M046r2Q0Optl3pS31qTWf8aFuA7H9wnsHTwl8EPvroVLwvQene/6paAs39Tbk6fWZcn2aZaHkc/w==" }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true + }, "node_modules/dir-compare": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", @@ -4748,6 +5003,12 @@ "p-limit": "^3.1.0 " } }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, "node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -5192,9 +5453,9 @@ } }, "node_modules/electron-log": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/electron-log/-/electron-log-5.2.3.tgz", - "integrity": "sha512-BabCiEV+p362LzY0EFE8hyzeGknzKDWSbhS0VFfRYQGA4FHWXWSfaKJlvTR9LFepNoORXxc/BWvqBXIPgsVFgA==", + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/electron-log/-/electron-log-5.2.4.tgz", + "integrity": "sha512-iX12WXc5XAaKeHg2QpiFjVwL+S1NVHPFd3V5RXtCmKhpAzXsVQnR3UEc0LovM6p6NkUQxDWnkdkaam9FNUVmCA==", "engines": { "node": ">= 14" } @@ -5642,9 +5903,9 @@ } }, "node_modules/eslint-plugin-react-hooks": { - "version": "5.1.0-rc-fb9a90fa48-20240614", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.1.0-rc-fb9a90fa48-20240614.tgz", - "integrity": "sha512-xsiRwaDNF5wWNC4ZHLut+x/YcAxksUd9Rizt7LaEn3bV8VyYRpXnRJQlLOfYaVy9esk4DFP4zPPnoNVjq5Gc0w==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.1.0.tgz", + "integrity": "sha512-mpJRtPgHN2tNAvZ35AMfqeB3Xqeo273QxrHJsbBEPWODRM4r0yB6jfoROqKEYrOn27UtRPpcpHc2UqyBSuUNTw==", "dev": true, "engines": { "node": ">=10" @@ -5896,9 +6157,9 @@ "dev": true }, "node_modules/express": { - "version": "4.21.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", - "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -5919,7 +6180,7 @@ "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.10", + "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", @@ -5934,6 +6195,10 @@ }, "engines": { "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/express/node_modules/debug": { @@ -6015,8 +6280,14 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, - "node_modules/fastq": { - "version": "1.17.1", + "node_modules/fastparse": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz", + "integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", "dev": true, @@ -6254,6 +6525,34 @@ "is-callable": "^1.1.3" } }, + "node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", @@ -6276,6 +6575,19 @@ "node": ">= 0.6" } }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -6976,9 +7288,9 @@ } }, "node_modules/i18next-browser-languagedetector": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.0.0.tgz", - "integrity": "sha512-zhXdJXTTCoG39QsrOCiOabnWj2jecouOqbchu3EfhtSHxIB5Uugnm9JaizenOy39h7ne3+fLikIjeW88+rgszw==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.0.1.tgz", + "integrity": "sha512-z9ZuWA7qxbww+cPtdJTgV0O2H9+qlLpQnb37RpnwfsWnUmrO+q92gbVKVtfBL7jRvxfmVMOUKxKGg6VBqO49Pg==", "dependencies": { "@babel/runtime": "^7.23.2" } @@ -7216,6 +7528,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/is-boolean-object": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", @@ -7659,6 +7983,30 @@ "node": ">= 0.4" } }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "1.21.6", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", + "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==", + "dev": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, "node_modules/joi": { "version": "17.13.3", "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", @@ -7808,6 +8156,24 @@ "node": ">= 0.8.0" } }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, "node_modules/listr2": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/listr2/-/listr2-7.0.2.tgz", @@ -7939,12 +8305,24 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash.castarray": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz", + "integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==", + "dev": true + }, "node_modules/lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", "dev": true }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -8453,6 +8831,17 @@ "imul": "^1.0.0" } }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, "node_modules/nan": { "version": "2.20.0", "resolved": "https://registry.npmjs.org/nan/-/nan-2.20.0.tgz", @@ -8620,6 +9009,24 @@ "semver": "bin/semver" } }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/normalize-url": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", @@ -8666,6 +9073,15 @@ "node": ">=0.10.0" } }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", @@ -8970,6 +9386,12 @@ "node": ">=6" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -9071,10 +9493,41 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, + "node_modules/path-scurry/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/path-to-regexp": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", - "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" }, "node_modules/path-type": { "version": "2.0.0", @@ -9135,6 +9588,15 @@ "node": ">=0.10.0" } }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/pkg-dir": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", @@ -9250,6 +9712,138 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-import/node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, "node_modules/postject": { "version": "1.0.0-alpha.6", "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", @@ -9469,12 +10063,9 @@ } }, "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "dependencies": { - "loose-envify": "^1.1.0" - }, + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", + "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", "engines": { "node": ">=0.10.0" } @@ -9509,9 +10100,9 @@ } }, "node_modules/react-bootstrap-icons": { - "version": "1.11.4", - "resolved": "https://registry.npmjs.org/react-bootstrap-icons/-/react-bootstrap-icons-1.11.4.tgz", - "integrity": "sha512-lnkOpNEZ/Zr7mNxvjA9efuarCPSgtOuGA55XiRj7ASJnBjb1wEAdtJOd2Aiv9t07r7FLI1IgyZPg9P6jqWD/IA==", + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/react-bootstrap-icons/-/react-bootstrap-icons-1.11.5.tgz", + "integrity": "sha512-eOhtFJMUqw98IJcfKJsSMZkFHCeNPTTwXZAe9V9d4mT22ARmbrISxPO9GmtWWuf72zQctLeZMGodX/q6wrbYYg==", "dependencies": { "prop-types": "^15.7.2" }, @@ -9520,15 +10111,14 @@ } }, "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz", + "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==", "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" + "scheduler": "^0.25.0" }, "peerDependencies": { - "react": "^18.3.1" + "react": "^19.0.0" } }, "node_modules/react-flip-toolkit": { @@ -9569,6 +10159,14 @@ } } }, + "node_modules/react-icons": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.4.0.tgz", + "integrity": "sha512-7eltJxgVt7X64oHh6wSWNwwbKTCtMfK35hcjvJS0yxEAhPM8oUKdS3+kqaW1vicIltw+kR2unHaa12S9pPALoQ==", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -9669,6 +10267,15 @@ "read-binary-file-arch": "cli.js" } }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "dependencies": { + "pify": "^2.3.0" + } + }, "node_modules/read-pkg": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", @@ -9777,6 +10384,18 @@ "node": ">= 6" } }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/rechoir": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", @@ -10190,12 +10809,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "dependencies": { - "loose-envify": "^1.1.0" - } + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz", + "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==" }, "node_modules/semver": { "version": "6.3.1", @@ -10475,6 +11091,15 @@ "node": ">= 10" } }, + "node_modules/sonner": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.7.1.tgz", + "integrity": "sha512-b6LHBfH32SoVasRFECrdY8p8s7hXPDn3OHUFbZZbiB1ctLS9Gdh6rpX2dVrpQA0kiL5jcRzDDldwwLkSKk3+QQ==", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/source-map": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", @@ -10602,6 +11227,21 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string.prototype.matchall": { "version": "4.0.11", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz", @@ -10699,6 +11339,19 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -10741,6 +11394,81 @@ "node": ">=0.10.0" } }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/sucrase/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sucrase/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sucrase/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/sudo-prompt": { "version": "9.2.1", "resolved": "https://registry.npmjs.org/sudo-prompt/-/sudo-prompt-9.2.1.tgz", @@ -10783,6 +11511,60 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tailwindcss": { + "version": "3.4.16", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.16.tgz", + "integrity": "sha512-TI4Cyx7gDiZ6r44ewaJmt0o6BrMCT5aK5e0rmJ/G9Xq3w7CX/5VXl/zIPEJZFUK5VEqwByyhqNPycPlvcK4ZNw==", + "dev": true, + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.6", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss/node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/tar": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", @@ -10856,6 +11638,27 @@ "rimraf": "bin.js" } }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/tiny-each-async": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/tiny-each-async/-/tiny-each-async-2.0.3.tgz", @@ -10947,6 +11750,12 @@ "node": ">=0.10.0" } }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -11237,9 +12046,9 @@ } }, "node_modules/vite": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.0.2.tgz", - "integrity": "sha512-XdQ+VsY2tJpBsKGs0wf3U/+azx8BBpYRHFAyKm5VeEZNOJZRB63q7Sc8Iup3k0TrN3KO6QgyzFf+opSbfY1y0g==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.0.3.tgz", + "integrity": "sha512-Cmuo5P0ENTN6HxLSo6IHsjCLn/81Vgrp81oaiFFMRa8gGDj5xEjIcEpf2ZymZtZR8oU0P2JX5WuUp/rlXcHkAw==", "dev": true, "dependencies": { "esbuild": "^0.24.0", @@ -11485,6 +12294,24 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -11524,6 +12351,18 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true }, + "node_modules/yaml": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.1.tgz", + "integrity": "sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==", + "dev": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", diff --git a/package.json b/package.json index 317d0ca35..aede82620 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "author": "yell0wsuit", "description": "An English-learning interactive tool written in React, designed to help learners practice speaking and listening", "private": true, - "version": "2.4.1", + "version": "3.0.0", "type": "module", "main": "main.cjs", "license": "Apache-2.0", @@ -21,31 +21,32 @@ "appxarm": "vite build --mode electron && electron-forge package --arch=arm64 && cd ./out && electron-windows-store --input-directory ./iSpeakerReact-win32-arm64 --output-directory ./ispeakerreact-appx --package-version ${npm_package_version}.0 --package-name ispeakerreact --assets ../public/images/icons/windows11 -m ./appxmanifest.xml" }, "dependencies": { - "@dnd-kit/core": "^6.2.0", - "@dnd-kit/sortable": "^9.0.0", - "@react-spring/web": "^9.7.5", + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", "bootstrap": "^5.3.3", "cors": "^2.8.5", "dexie": "^4.0.10", - "electron-log": "^5.2.3", + "electron-log": "^5.2.4", "electron-squirrel-startup": "^1.0.1", - "express": "^4.21.1", + "express": "^4.21.2", "he": "^1.2.0", "i18next": "^24.0.5", - "i18next-browser-languagedetector": "^8.0.0", + "i18next-browser-languagedetector": "^8.0.1", "i18next-http-backend": "^3.0.1", "lodash": "^4.17.21", "masonry-layout": "^4.2.2", "mime": "^4.0.4", "nprogress": "^0.2.0", - "react": "^18.3.1", + "react": "^19.0.0", "react-bootstrap": "^2.10.6", - "react-bootstrap-icons": "^1.11.4", - "react-dom": "^18.3.1", + "react-bootstrap-icons": "^1.11.5", + "react-dom": "^19.0.0", "react-flip-toolkit": "^7.2.4", "react-i18next": "^15.1.3", + "react-icons": "^5.4.0", "react-loading-skeleton": "^3.5.0", - "react-router-dom": "^7.0.2" + "react-router-dom": "^7.0.2", + "sonner": "^1.7.1" }, "devDependencies": { "@electron-forge/cli": "^7.6.0", @@ -58,19 +59,24 @@ "@electron-forge/plugin-fuses": "^7.6.0", "@electron/fuses": "^1.8.0", "@eslint/js": "^9.16.0", - "@types/react": "^18.3.12", - "@types/react-dom": "^18.3.1", + "@tailwindcss/typography": "^0.5.15", + "@types/react": "^19.0.1", + "@types/react-dom": "^19.0.1", "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.20", "concurrently": "^9.1.0", "cross-env": "^7.0.3", + "daisyui": "^4.12.14", "electron": "^33.2.1", "eslint": "^9.16.0", "eslint-plugin-react": "^7.37.2", - "eslint-plugin-react-hooks": "^5.1.0-rc.0", + "eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-react-refresh": "^0.4.16", "globals": "^15.13.0", + "postcss": "^8.4.49", "rollup-plugin-visualizer": "^5.12.0", - "vite": "^6.0.2", + "tailwindcss": "^3.4.16", + "vite": "^6.0.3", "wait-on": "^8.0.1" }, "optionalDependencies": { diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 000000000..2e7af2b7f --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/public/styles/style.css b/public/styles/style.css index b82d4d76d..70b7ee906 100644 --- a/public/styles/style.css +++ b/public/styles/style.css @@ -23,6 +23,7 @@ html { font-family: Inter, system-ui; font-variation-settings: "opsz" 15; + font-feature-settings: "cv02", "cv03", "cv04", "cv05", "cv08", "cv10", "cv11"; } @font-face { diff --git a/src/App.jsx b/src/App.jsx index 688162426..240c7a709 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,12 +1,12 @@ import { Suspense, lazy } from "react"; -import { Container } from "react-bootstrap"; -import { Route, BrowserRouter, HashRouter, Routes } from "react-router-dom"; +import { BrowserRouter, HashRouter, Route, Routes } from "react-router-dom"; import LoadingOverlay from "./components/general/LoadingOverlay"; import NotFound from "./components/general/NotFound"; import Homepage from "./components/Homepage"; import { isElectron } from "./utils/isElectron"; -import { ThemeProvider } from "./utils/ThemeProvider"; -import ThemeSwitcher from "./utils/ThemeSwitcher"; +import { ThemeProvider } from "./utils/ThemeContext/ThemeProvider"; +import { Toaster } from "sonner"; +import { useTheme } from "./utils/ThemeContext/useTheme"; const SoundList = lazy(() => import("./components/sound_page/SoundList")); const ConversationMenu = lazy(() => import("./components/conversation_page/ConversationMenu")); @@ -14,63 +14,73 @@ const ExamPage = lazy(() => import("./components/exam_page/ExamPage")); const ExercisePage = lazy(() => import("./components/exercise_page/ExercisePage")); const SettingsPage = lazy(() => import("./components/setting_page/Settings")); -const App = () => { - const RouterComponent = isElectron() ? HashRouter : BrowserRouter; - const baseUrl = import.meta.env.BASE_URL; +const RouterComponent = isElectron() ? HashRouter : BrowserRouter; +const baseUrl = import.meta.env.BASE_URL; + +const AppContent = () => { + const { theme } = useTheme(); + const toastTheme = + theme === "dark" || (theme === "auto" && window.matchMedia("(prefers-color-scheme: dark)").matches) + ? "dark" + : "light"; return ( - - - - - } /> - }> - - - } - /> - }> - - - } - /> - }> - - - } - /> - }> - - - } - /> - }> - - - } - /> - } /> - - - - - + <> + + + } /> + }> + + + } + /> + }> + + + } + /> + }> + + + } + /> + }> + + + } + /> + }> + + + } + /> + } /> + + + + ); }; +const App = () => ( + + + +); + export default App; diff --git a/src/components/Homepage.jsx b/src/components/Homepage.jsx index 11e451a34..148e61445 100644 --- a/src/components/Homepage.jsx +++ b/src/components/Homepage.jsx @@ -1,12 +1,42 @@ -import { useEffect } from "react"; -import { Button, Card, Col, Row } from "react-bootstrap"; +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; +import Container from "../ui/Container"; +import { useTheme } from "../utils/ThemeContext/useTheme"; import TopNavBar from "./general/TopNavBar"; -import { useTranslation } from "react-i18next"; function Homepage() { const { t } = useTranslation(); + const navigate = useNavigate(); + const { theme } = useTheme(); + const [currentTheme, setCurrentTheme] = useState(theme); + const [isDarkMode, setIsDarkMode] = useState(false); + + useEffect(() => { + const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); + + const updateTheme = () => { + if (theme === "auto") { + const systemPrefersDark = mediaQuery.matches; + setCurrentTheme(systemPrefersDark ? "dark" : "light"); + setIsDarkMode(systemPrefersDark); + } else { + setCurrentTheme(theme); + setIsDarkMode(theme === "dark"); + } + }; + + // Initial check and listener + updateTheme(); + if (theme === "auto") { + mediaQuery.addEventListener("change", updateTheme); + } + + return () => { + mediaQuery.removeEventListener("change", updateTheme); + }; + }, [theme]); const handleNavigate = (path) => { navigate(path); @@ -16,6 +46,10 @@ function Homepage() { document.title = `${t("navigation.home")} | iSpeakerReact v${__APP_VERSION__}`; }, [t]); + const logoSrc = isDarkMode + ? `${import.meta.env.BASE_URL}images/logos/ispeakerreact-no-background-darkmode.svg` + : `${import.meta.env.BASE_URL}images/logos/ispeakerreact-no-background.svg`; + const cardsInfo = [ { title: "Sounds", @@ -49,39 +83,45 @@ function Homepage() { }, ]; + useEffect(() => { + window.scrollTo(0, 0); + }, []); + return ( <> - - -

iSpeakerReact

- -
- - {cardsInfo.map((card, idx) => ( - - - - {card.title} - {card.description} - - {`${card.title} - -
- -
-
-
- - ))} -
+ +
+
+ iSpeakerReact logo +

iSpeakerReact

+
+

v{__APP_VERSION__}

+
+
+ {cardsInfo.map((card, idx) => ( +
+
+ {`${card.title} +
+
+

{card.title}

+

{card.description}

+
+
+ +
+
+ ))} +
+
); } diff --git a/src/components/general/AccentDropdown.jsx b/src/components/general/AccentDropdown.jsx index 68094a2d5..6b1f1c391 100644 --- a/src/components/general/AccentDropdown.jsx +++ b/src/components/general/AccentDropdown.jsx @@ -1,7 +1,6 @@ import { useEffect } from "react"; -import { Dropdown } from "react-bootstrap"; -import AccentLocalStorage from "../../utils/AccentLocalStorage"; import { useTranslation } from "react-i18next"; +import AccentLocalStorage from "../../utils/AccentLocalStorage"; const AccentDropdown = ({ onAccentChange }) => { const [selectedAccent, setSelectedAccent] = AccentLocalStorage(); @@ -24,22 +23,31 @@ const AccentDropdown = ({ onAccentChange }) => { }; return ( - - - {t("accent.accentSettings")}:{" "} - {selectedAccentOptions.find((item) => item.value === selectedAccent).name} - - - {selectedAccentOptions.map((item) => ( - handleAccentChange(item.value)} - active={selectedAccent === item.value}> - {item.name} - - ))} - - + <> +
+

{t("accent.accentSettings")}:

+
+
+ {selectedAccentOptions.find((item) => item.value === selectedAccent).name} +
+
    + {selectedAccentOptions.map((item) => ( +
  • + +
  • + ))} +
+
+
+ ); }; diff --git a/src/components/general/LoadingOverlay.jsx b/src/components/general/LoadingOverlay.jsx index a81caa3bb..1d37130c5 100644 --- a/src/components/general/LoadingOverlay.jsx +++ b/src/components/general/LoadingOverlay.jsx @@ -1,25 +1,11 @@ -import { Spinner } from "react-bootstrap"; - const LoadingOverlay = () => { return (
- - Loading... - + className="loading-overlay fixed inset-0 flex items-center justify-center z-[10000] bg-base-100"> + + Loading... +
); }; diff --git a/src/components/general/ToastNotification.jsx b/src/components/general/ToastNotification.jsx deleted file mode 100644 index ee535efa3..000000000 --- a/src/components/general/ToastNotification.jsx +++ /dev/null @@ -1,20 +0,0 @@ -import { Toast, ToastContainer } from "react-bootstrap"; -import CloseButton from "react-bootstrap/CloseButton"; - -const ToastNotification = ({ show, onClose, message, variant, autohide = true, delay = 10000 }) => { - return ( - - - {message} - - - - ); -}; - -export default ToastNotification; diff --git a/src/components/general/TopNavBar.jsx b/src/components/general/TopNavBar.jsx index 3b7f9af62..f183cf963 100644 --- a/src/components/general/TopNavBar.jsx +++ b/src/components/general/TopNavBar.jsx @@ -1,88 +1,117 @@ -import { useContext, useEffect, useState } from "react"; -import { Container, Nav, Navbar } from "react-bootstrap"; -import { CardChecklist, ChatDots, ClipboardCheck, House, Mic, Gear } from "react-bootstrap-icons"; -import { NavLink } from "react-router-dom"; -import { ThemeContext } from "../../utils/ThemeProvider"; +import { useEffect, useState } from "react"; +import { CardChecklist, ChatDots, ClipboardCheck, Gear, House, Mic } from "react-bootstrap-icons"; import { useTranslation } from "react-i18next"; +import { CgMenuLeft } from "react-icons/cg"; +import { FaGithub } from "react-icons/fa"; +import { FiExternalLink } from "react-icons/fi"; +import { NavLink } from "react-router-dom"; +import { useTheme } from "../../utils/ThemeContext/useTheme"; const TopNavBar = () => { const { t } = useTranslation(); - const { theme } = useContext(ThemeContext); + const { theme } = useTheme(); const [currentTheme, setCurrentTheme] = useState(theme); + const [isDarkMode, setIsDarkMode] = useState(false); useEffect(() => { const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); - // Function to update the logo based on the theme or system preference const updateTheme = () => { if (theme === "auto") { - // If theme is auto, check if system prefers dark mode - setCurrentTheme(mediaQuery.matches ? "dark" : "light"); + const systemPrefersDark = mediaQuery.matches; + setCurrentTheme(systemPrefersDark ? "dark" : "light"); + setIsDarkMode(systemPrefersDark); } else { setCurrentTheme(theme); + setIsDarkMode(theme === "dark"); } }; - // Initial check + // Initial check and listener updateTheme(); + if (theme === "auto") { + mediaQuery.addEventListener("change", updateTheme); + } - // Add listener for system theme changes if "auto" is selected - mediaQuery.addEventListener("change", updateTheme); - - // Cleanup the listener on unmount - return () => mediaQuery.removeEventListener("change", updateTheme); + return () => { + mediaQuery.removeEventListener("change", updateTheme); + }; }, [theme]); - const logoSrc = - currentTheme === "dark" - ? `${import.meta.env.BASE_URL}images/logos/ispeakerreact-no-background-darkmode.svg` - : `${import.meta.env.BASE_URL}images/logos/ispeakerreact-no-background.svg`; + const logoSrc = isDarkMode + ? `${import.meta.env.BASE_URL}images/logos/ispeakerreact-no-background-darkmode.svg` + : `${import.meta.env.BASE_URL}images/logos/ispeakerreact-no-background.svg`; + + const navbarClass = isDarkMode ? "bg-slate-600/50" : "bg-lime-300/50"; + + const menuItems = [ + { to: "/", icon: , label: t("navigation.home") }, + { to: "/sounds", icon: , label: t("navigation.sounds") }, + { to: "/exercises", icon: , label: t("navigation.exercises") }, + { to: "/conversations", icon: , label: t("navigation.conversations") }, + { to: "/exams", icon: , label: t("navigation.exams") }, + { to: "/settings", icon: , label: t("navigation.settings") }, + ]; return ( -
- - - - iSpeakerReact logo{" "} - iSpeakerReact - - - - - - - + + ))} + +
+ + {/* GitHub Link */} + ); }; diff --git a/src/components/setting_page/Appearance.jsx b/src/components/setting_page/Appearance.jsx index e0cf299dd..dee1cb77e 100644 --- a/src/components/setting_page/Appearance.jsx +++ b/src/components/setting_page/Appearance.jsx @@ -1,103 +1,76 @@ -import { useContext } from "react"; -import { Card, Col, Dropdown, Form, Row } from "react-bootstrap"; -import { Check2 } from "react-bootstrap-icons"; import { useTranslation } from "react-i18next"; -import { ThemeContext } from "../../utils/ThemeProvider"; +import { useTheme } from "../../utils/ThemeContext/useTheme"; const AppearanceSettings = () => { const { t } = useTranslation(); + const { theme, setTheme } = useTheme(); - const { theme, setTheme, showToggleButton, setShowToggleButton } = useContext(ThemeContext); - // Function to handle theme change const handleThemeSelect = (selectedTheme) => { setTheme(selectedTheme); }; - // Function to handle the visibility of the "Toggle theme" button - const handleToggleButtonVisibility = (e) => { - setShowToggleButton(e.target.checked); - }; - - function getThemeOptionLabel(theme) { - switch (theme) { + const getThemeOptionLabel = (currentTheme) => { + switch (currentTheme) { case "auto": return t("settingPage.appearanceSettings.themeAuto"); case "light": return t("settingPage.appearanceSettings.themeLight"); - default: + case "dark": return t("settingPage.appearanceSettings.themeDark"); + default: + return t("settingPage.appearanceSettings.themeAuto"); } - } + }; return ( <> -

{t("settingPage.appearanceSettings.appearanceHeading")}

- - - - - - - - - - - {getThemeOptionLabel(theme)} - - - - {t("settingPage.appearanceSettings.appearanceHeading")} +
+
+
+

{t("settingPage.appearanceSettings.themeOption")}

+
+
+ {getThemeOptionLabel(theme)} +
+
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
- - - +
+
+
); }; diff --git a/src/components/setting_page/Settings.jsx b/src/components/setting_page/Settings.jsx index 31c75dfc2..07049c72f 100644 --- a/src/components/setting_page/Settings.jsx +++ b/src/components/setting_page/Settings.jsx @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; -import { Col, Row } from "react-bootstrap"; import { useTranslation } from "react-i18next"; +import Container from "../../ui/Container"; import { isElectron } from "../../utils/isElectron"; import TopNavBar from "../general/TopNavBar"; import AppearanceSettings from "./Appearance"; @@ -38,9 +38,9 @@ const SettingsPage = () => { return ( <> -
- - + +
+
{currentPage === "settings" && ( <>

{t("settingPage.heading")}

@@ -79,9 +79,9 @@ const SettingsPage = () => { )} {currentPage === "video-download" && } - - -
+
+
+ ); }; diff --git a/src/components/sound_page/PracticeSound.jsx b/src/components/sound_page/PracticeSound.jsx index 82e4d53a0..d4ec169e0 100644 --- a/src/components/sound_page/PracticeSound.jsx +++ b/src/components/sound_page/PracticeSound.jsx @@ -1,39 +1,36 @@ import he from "he"; import { useCallback, useEffect, useState } from "react"; -import { Alert, Button, Card, Col, Modal, Ratio, Row } from "react-bootstrap"; -import { ArrowLeftCircle, RecordCircleFill } from "react-bootstrap-icons"; +import { BsArrowLeftCircle, BsRecordCircleFill } from "react-icons/bs"; +import { IoInformationCircleOutline } from "react-icons/io5"; +import { MdOutlineOndemandVideo, MdKeyboardVoice, MdChecklist } from "react-icons/md"; + import { Trans, useTranslation } from "react-i18next"; -import Skeleton from "react-loading-skeleton"; -import "react-loading-skeleton/dist/skeleton.css"; import { checkRecordingExists } from "../../utils/databaseOperations"; import { isElectron } from "../../utils/isElectron"; import LoadingOverlay from "../general/LoadingOverlay"; -import ToastNotification from "../general/ToastNotification"; import ReviewCard from "./ReviewCard"; import SoundPracticeCard from "./SoundPracticeCard"; -import { usePlaybackFunction } from "./usePlaybackFunction"; -import { useRecordingFunction } from "./useRecordingFunction"; -import { useSoundVideoMapping } from "./useSoundVideoMapping"; +import { usePlaybackFunction } from "./hooks/usePlaybackFunction"; +import { useRecordingFunction } from "./hooks/useRecordingFunction"; +import { useSoundVideoMapping } from "./hooks/useSoundVideoMapping"; +import { WatchVideoCard } from "./WatchVideoCard"; const PracticeSound = ({ sound, accent, onBack, index, soundsData }) => { const { t } = useTranslation(); + const [activeTab, setActiveTab] = useState("watchTab"); const accentKey = accent === "american" ? "a" : "b"; const accentData = sound[accentKey][0]; const findPhonemeDetails = useCallback( (phoneme) => { - let index = soundsData.consonants.findIndex((p) => p.phoneme === phoneme); - if (index !== -1) { - return { index, type: "consonant" }; - } + let phonemeIndex = soundsData.consonants.findIndex((p) => p.phoneme === phoneme); + if (phonemeIndex !== -1) return { index: phonemeIndex, type: "consonant" }; - index = soundsData.vowels_n_diphthongs.findIndex((p) => p.phoneme === phoneme); - if (index !== -1) { - return { index, type: "vowel" }; - } + phonemeIndex = soundsData.vowels_n_diphthongs.findIndex((p) => p.phoneme === phoneme); + if (phonemeIndex !== -1) return { index: phonemeIndex, type: "vowel" }; - return { index: -1, type: null }; // Not found + return { index: -1, type: null }; }, [soundsData.consonants, soundsData.vowels_n_diphthongs] ); @@ -47,65 +44,25 @@ const PracticeSound = ({ sound, accent, onBack, index, soundsData }) => { ? `${import.meta.env.BASE_URL}images/ispeaker/sound_images/sounds_american.jpg` : `${import.meta.env.BASE_URL}images/ispeaker/sound_images/sounds_british.jpg`; - const [review, setReview] = useState(null); - - const handleReviewClick = (newReview) => { - const { type } = findPhonemeDetails(sound.phoneme); - const reviewKey = `${type}${index + 1}`; // Adding 1 to make it 1-indexed, matching "consonant1", "vowel1", etc. - - // Fetch current ispeaker data or initialize - const ispeakerData = JSON.parse(localStorage.getItem("ispeaker") || "{}"); - // Ensure soundReview object exists for both accents - ispeakerData.soundReview = ispeakerData.soundReview || {}; - ispeakerData.soundReview[accent] = ispeakerData.soundReview[accent] || {}; - // Save the new review under the correct accent and review key - ispeakerData.soundReview[accent][reviewKey] = newReview; - - localStorage.setItem("ispeaker", JSON.stringify(ispeakerData)); - setReview(newReview); - }; - useEffect(() => { window.scrollTo(0, 0); - // Fetch reviews from localStorage - const reviews = JSON.parse(localStorage.getItem("ispeaker") || "{}").soundReview || {}; - const { type } = findPhonemeDetails(sound.phoneme); - const reviewKey = `${type}${index + 1}`; - - // Try to load the review for the current accent and phoneme - const soundReview = reviews[accent] ? reviews[accent][reviewKey] : null; - if (soundReview) { - setReview(soundReview); - } - }, [sound.phoneme, findPhonemeDetails, accent, index]); - - const emojiStyle = (reviewType) => { - const styles = { - good: "text-success", - neutral: "text-warning", - bad: "text-danger", - }; - return review === reviewType ? styles[reviewType] : ""; - }; + }, []); // Video modals - const [show, setShow] = useState(false); const [selectedVideoUrl, setSelectedVideoUrl] = useState(""); const [selectedVideoModalIndex, setSelectedVideoModalIndex] = useState(""); const handleShow = (videoIndex) => { setSelectedVideoModalIndex(videoIndex); setSelectedVideoUrl(videoUrls[videoIndex]); - setShow(true); setIframeLoadingStates((prevStates) => ({ ...prevStates, modalIframe: true, // Reset the modal iframe loading state to true when modal is opened })); + document.getElementById("sound_video_modal").showModal(); }; - const handleClose = () => setShow(false); - // iframe loading const [iframeLoadingStates, setIframeLoadingStates] = useState({ mainIframe: true, @@ -132,17 +89,11 @@ const PracticeSound = ({ sound, accent, onBack, index, soundsData }) => { const [currentAudioSource, setCurrentAudioSource] = useState(null); // For AudioContext source node const [currentAudioElement, setCurrentAudioElement] = useState(null); // For Audio element (fallback) - // Notification state - const [showToast, setShowToast] = useState(false); - const [toastMessage, setToastMessage] = useState(""); - const { getRecordingKey, isRecordingPlayingActive, isRecordingAvailable, handleRecording } = useRecordingFunction( activeRecordingCard, setActiveRecordingCard, setIsRecording, setMediaRecorder, - setToastMessage, - setShowToast, setRecordingAvailability, isRecording, mediaRecorder, @@ -180,9 +131,7 @@ const PracticeSound = ({ sound, accent, onBack, index, soundsData }) => { setCurrentAudioElement, setIsRecordingPlaying, setActivePlaybackCard, - setPlayingRecordings, - setToastMessage, - setShowToast + setPlayingRecordings ); useEffect(() => { @@ -219,158 +168,156 @@ const PracticeSound = ({ sound, accent, onBack, index, soundsData }) => { return ( <> - - -

+
+
+

{t("sound_page.soundTop")} {he.decode(sound.phoneme)}

-

+

{t("accent.accentSettings")}:{" "} {accent == "american" ? t("accent.accentAmerican") : t("accent.accentBritish")}

{accentData && ( <> -

{t("sound_page.exampleWords")}

+

{t("sound_page.exampleWords")}

{["initial", "medial", "final"].map((position) => (

))} )} - - - - - {t("sound_page.watchCard")} - - -
- {isElectron() && - videoUrl?.isLocal && - videoUrl.value.includes("http://localhost") ? ( - - ) : ( + +
+
+
+ {/* Menu */} +
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+
+ + {/* Tab Content */} +
+ {activeTab === "watchTab" && ( + + )} + {activeTab === "practieTab" && ( +
+
+

+ , + ]} + /> +

+ +
+
+ )} + {activeTab === "reviewTab" && ( + + )} +
+
+
+ + +
+

+ {t("sound_page.clipModalTitle")} #{selectedVideoModalIndex} +

+
+
+
+ {isElectron() && selectedVideoUrl.includes("localhost:8998") ? ( + + ) : ( + selectedVideoUrl && ( <> - {iframeLoadingStates.mainIframe && ( - + {iframeLoadingStates.modalIframe && ( +
)} + className="w-full h-full"> - )} -
- - {isElectron() && !videoUrl?.value.includes("http://localhost") ? ( - - {t("alert.alertOnlineVideo")} - - ) : ( - "" - )} - - - - {t("sound_page.practiceCard")} - - - ]} - /> - - - - - - - - - - - {t("sound_page.clipModalTitle")} #{selectedVideoModalIndex} - - - - -
- {isElectron() && selectedVideoUrl.includes("localhost:8998") ? ( - - ) : ( - <> - {iframeLoadingStates.modalIframe && ( - - )} - - - )} + ) + )} +
+
+
+ {isElectron() && !selectedVideoUrl.startsWith("http://localhost") && ( +
+ + {t("alert.alertOnlineVideo")}
- - {isElectron() && !selectedVideoUrl.startsWith("http://localhost") ? ( - - {t("alert.alertOnlineVideo")} - - ) : ( - "" )} - - - - - - - setShowToast(false)} - message={toastMessage} - variant="warning" - /> +
+
+ +
+
+
+
); }; diff --git a/src/components/sound_page/ReviewCard.jsx b/src/components/sound_page/ReviewCard.jsx index f9eb5cccb..e513c70af 100644 --- a/src/components/sound_page/ReviewCard.jsx +++ b/src/components/sound_page/ReviewCard.jsx @@ -1,33 +1,62 @@ import he from "he"; -import { Card, Col, Row } from "react-bootstrap"; -import { EmojiFrown, EmojiNeutral, EmojiSmile } from "react-bootstrap-icons"; import { useTranslation } from "react-i18next"; +import { + BsEmojiFrown, + BsEmojiFrownFill, + BsEmojiNeutral, + BsEmojiNeutralFill, + BsEmojiSmile, + BsEmojiSmileFill, +} from "react-icons/bs"; +import { useReview } from "./hooks/useReview"; -const ReviewCard = ({ sound, handleReviewClick, emojiStyle }) => { +const ReviewCard = ({ sound, accent, index, soundsData }) => { const { t } = useTranslation(); + // Integrate the useReview hook + const { review, handleReviewClick } = useReview(sound, accent, index, soundsData); + + // Local function for emoji styling + const emojiStyle = (reviewType) => { + const styles = { + good: "text-success", + neutral: "text-warning", + bad: "text-error", + }; + return review === reviewType ? styles[reviewType] : ""; + }; + return ( - - {t("sound_page.reviewCard")} - - {t("sound_page.reviewInstructions", { phoneme: he.decode(sound.phoneme) })} - - handleReviewClick("good")}> - - - handleReviewClick("neutral")}> - - - handleReviewClick("bad")}> - - - - - + ); }; diff --git a/src/components/sound_page/SoundList.jsx b/src/components/sound_page/SoundList.jsx index 8b2c446ef..027523db8 100644 --- a/src/components/sound_page/SoundList.jsx +++ b/src/components/sound_page/SoundList.jsx @@ -1,7 +1,7 @@ import he from "he"; import { Suspense, lazy, useEffect, useState } from "react"; -import { Badge, Button, Card, Col, Row } from "react-bootstrap"; import { useTranslation } from "react-i18next"; +import Container from "../../ui/Container"; import AccentLocalStorage from "../../utils/AccentLocalStorage"; import { isElectron } from "../../utils/isElectron"; import AccentDropdown from "../general/AccentDropdown"; @@ -16,6 +16,7 @@ const SoundList = () => { const [selectedSound, setSelectedSound] = useState(null); const [loading, setLoading] = useState(true); const [selectedAccent, setSelectedAccent] = AccentLocalStorage(); + const [activeTab, setActiveTab] = useState("tab1"); const [soundsData, setSoundsData] = useState({ consonants: [], @@ -58,24 +59,18 @@ const SoundList = () => { }; const getBadgeColor = (sound, index) => { - // Retrieve the entire data structure from localStorage const ispeakerData = JSON.parse(localStorage.getItem("ispeaker") || "{}"); const accentReviews = ispeakerData.soundReview ? ispeakerData.soundReview[selectedAccent] || {} : {}; - - // Generate the correct review key for this sound based on its index const reviewKey = getReviewKey(sound, index); - const review = accentReviews[reviewKey]; // Access the specific review by its key + const review = accentReviews[reviewKey]; - switch (review) { - case "good": - return "success"; - case "neutral": - return "warning"; - case "bad": - return "danger"; - default: - return "secondary"; // No review found or the key does not exist - } + const badgeColors = { + good: "badge-success", + neutral: "badge-warning", + bad: "badge-error", + }; + + return badgeColors[review] || null; // Return the Tailwind class or null }; const getReviewText = (review) => { @@ -139,94 +134,155 @@ const SoundList = () => { return ( <> -

{t("navigation.sounds")}

- {selectedSound ? ( - }> - handleGoBack()} - /> - - ) : ( - <> - -
- {loading ? ( - - ) : ( - <> -

{t("sound_page.consonants")}

- - {soundsData.consonants - .filter( - (sound) => - (selectedAccent === "british" && sound.b_s === "yes") || - (selectedAccent === "american" && sound.a_s === "yes") - ) - .map((sound, index) => ( - - - - - {getReviewText(reviews[`${getReviewKey(sound, index)}`])} - - {he.decode(sound.phoneme)} - {sound.example_word} - - - - - ))} - -
-

{t("sound_page.vowels_dipthongs")}

- - {soundsData.vowels_n_diphthongs - .filter( - (sound) => - (selectedAccent === "british" && sound.b_s === "yes") || - (selectedAccent === "american" && sound.a_s === "yes") - ) - .map((sound, index) => ( - - - - - {getReviewText(reviews[`${getReviewKey(sound, index)}`])} - - {he.decode(sound.phoneme)} - {sound.example_word} - - - - - ))} - - - )} -
- - )} + +

{t("navigation.sounds")}

+ {selectedSound ? ( + }> + handleGoBack()} + /> + + ) : ( + <> + +
+ {loading ? ( + + ) : ( + <> +
+ {/* Menu */} +
    +
  • + +
  • +
  • + +
  • +
+
+ {/* Tab Content */} +
+ {activeTab === "tab1" && ( + <> +
+ {soundsData.consonants + .filter( + (sound) => + (selectedAccent === "british" && sound.b_s === "yes") || + (selectedAccent === "american" && sound.a_s === "yes") + ) + .map((sound, index) => ( +
+ {getBadgeColor(sound, index) && ( + + {getReviewText( + reviews[`${getReviewKey(sound, index)}`] + )} + + )} +
+
+

+ {he.decode(sound.phoneme)}{" "} +

+

{sound.example_word}

+
+
+ +
+
+
+ ))} +
+ + )} + {activeTab === "tab2" && ( + <> +
+ {soundsData.vowels_n_diphthongs + .filter( + (sound) => + (selectedAccent === "british" && sound.b_s === "yes") || + (selectedAccent === "american" && sound.a_s === "yes") + ) + .map((sound, index) => ( +
+ {getBadgeColor(sound, index) && ( + + {getReviewText( + reviews[`${getReviewKey(sound, index)}`] + )} + + )} +
+
+

+ {he.decode(sound.phoneme)} +

+

{sound.example_word}

+
+
+ +
+
+
+ ))} +
+ + )} +
+ + )} +
+ + )} +
); }; diff --git a/src/components/sound_page/SoundPracticeCard.jsx b/src/components/sound_page/SoundPracticeCard.jsx index 933818133..f62c1f774 100644 --- a/src/components/sound_page/SoundPracticeCard.jsx +++ b/src/components/sound_page/SoundPracticeCard.jsx @@ -1,6 +1,5 @@ import he from "he"; -import { Card, Col, Row } from "react-bootstrap"; -import { PlayCircleFill, RecordCircleFill } from "react-bootstrap-icons"; +import { BsRecordCircleFill, BsPlayCircleFill } from "react-icons/bs"; const SoundCardItem = ({ id, @@ -14,53 +13,56 @@ const SoundCardItem = ({ isRecordingAvailable, handleRecording, handlePlayRecording, -}) => ( - - - - - - - - - - - handleRecording(id)} - /> - (isRecordingAvailable(id) ? handlePlayRecording(id) : null)} - disabled={isRecordingPlaying && activePlaybackCard !== id} - /> - - - - -); +}) => { + const isRecordingDisabled = activeRecordingCard !== null || isRecordingPlaying; + const isPlayingDisabled = + (isRecordingPlaying && activePlaybackCard !== id) || !isRecordingAvailable(id) || activeRecordingCard !== null; + + const recordIconClasses = `me-2${ + activeRecordingCard === id ? " text-success" : isRecordingDisabled ? " pointer-events-none opacity-25" : "" + }`; + const playIconClasses = `me-2${ + isRecordingPlaying && activePlaybackCard !== id + ? " pointer-events-none opacity-25" + : isRecordingPlayingActive(id) + ? " text-success" + : isPlayingDisabled + ? " pointer-events-none opacity-25" + : "" + }`; + + return ( +
+
+
+
+ + +
+ handleRecording(id)} + /> + (isRecordingAvailable(id) ? handlePlayRecording(id) : null)} + disabled={isRecordingPlaying && activePlaybackCard !== id} + /> +
+
+
+
+
+ ); +}; const SoundPracticeCard = ({ sound, @@ -75,67 +77,32 @@ const SoundPracticeCard = ({ handleRecording, handlePlayRecording, }) => { + const commonProps = { + imgPhonemeThumbSrc, + handleShow, + activeRecordingCard, + isRecordingPlaying, + activePlaybackCard, + isRecordingPlayingActive, + isRecordingAvailable, + handleRecording, + handlePlayRecording, + }; + + const items = [ + { id: 1, textContent: sound.phoneme, shouldShow: sound.shouldShow !== false }, + { id: 2, textContent: accentData.initial, shouldShow: !!accentData.initial }, + { id: 3, textContent: accentData.medial, shouldShow: !!accentData.medial }, + { id: 4, textContent: accentData.final, shouldShow: !!accentData.final }, + ]; + return ( <> - {sound.shouldShow !== false && ( - - )} - {accentData.initial && ( - - )} - {accentData.medial && ( - - )} - {accentData.final && ( - + {items.map( + (item) => + item.shouldShow && ( + + ) )} ); diff --git a/src/components/sound_page/SoundVideoModal.jsx b/src/components/sound_page/SoundVideoModal.jsx new file mode 100644 index 000000000..538b960c0 --- /dev/null +++ b/src/components/sound_page/SoundVideoModal.jsx @@ -0,0 +1,70 @@ +import { useState } from "react"; +import { isElectron } from "../../utils/isElectron"; +import { useTranslation } from "react-i18next"; + +const SoundVideoModal = ({ videoUrls, videoUrl, videoLoading }) => { + const { t } = useTranslation(); + + const [selectedVideoUrl, setSelectedVideoUrl] = useState(""); + const [selectedVideoModalIndex, setSelectedVideoModalIndex] = useState(""); + const [iframeLoadingStates, setIframeLoadingStates] = useState({ + mainIframe: true, + modalIframe: true, + }); + + const handleClose = () => { + document.getElementById("sound_video_modal").close(); + }; + + const handleIframeLoad = (iframeKey) => { + setIframeLoadingStates((prevStates) => ({ + ...prevStates, + [iframeKey]: false, + })); + }; + + return ( + +
+

+ {t("sound_page.clipModalTitle")} #{selectedVideoModalIndex} +

+
+
+
+ {isElectron() && selectedVideoUrl.includes("localhost:8998") ? ( + + ) : ( + <> + {iframeLoadingStates.modalIframe &&
} + + + )} +
+
+
+ {isElectron() && !selectedVideoUrl.startsWith("http://localhost") && ( +
{t("alert.alertOnlineVideo")}
+ )} +
+
+ +
+
+
+
+ ); +}; + +export default SoundVideoModal; diff --git a/src/components/sound_page/WatchVideoCard.jsx b/src/components/sound_page/WatchVideoCard.jsx new file mode 100644 index 000000000..56c7380d3 --- /dev/null +++ b/src/components/sound_page/WatchVideoCard.jsx @@ -0,0 +1,46 @@ +import { IoInformationCircleOutline } from "react-icons/io5"; +import { isElectron } from "../../utils/isElectron"; + +export const WatchVideoCard = ({ t, videoUrl, iframeLoadingStates, handleIframeLoad }) => { + return ( +
+
+
+
+ {isElectron() && videoUrl?.isLocal && videoUrl.value.includes("http://localhost") ? ( + + ) : ( + <> + {iframeLoadingStates.mainIframe &&
} + + + )} +
+
+ {isElectron() && !videoUrl?.value.includes("http://localhost") ? ( +
+ + {t("alert.alertOnlineVideo")} +
+ ) : ( + "" + )} +
+
+ ); +}; diff --git a/src/components/sound_page/usePlaybackFunction.jsx b/src/components/sound_page/hooks/usePlaybackFunction.jsx similarity index 91% rename from src/components/sound_page/usePlaybackFunction.jsx rename to src/components/sound_page/hooks/usePlaybackFunction.jsx index 8015306a0..52a7cd812 100644 --- a/src/components/sound_page/usePlaybackFunction.jsx +++ b/src/components/sound_page/hooks/usePlaybackFunction.jsx @@ -1,6 +1,7 @@ import { useTranslation } from "react-i18next"; -import { playRecording } from "../../utils/databaseOperations"; -import { isElectron } from "../../utils/isElectron"; +import { toast } from "sonner"; +import { playRecording } from "../../../utils/databaseOperations"; +import { isElectron } from "../../../utils/isElectron"; export function usePlaybackFunction( getRecordingKey, @@ -12,9 +13,7 @@ export function usePlaybackFunction( setCurrentAudioElement, setIsRecordingPlaying, setActivePlaybackCard, - setPlayingRecordings, - setToastMessage, - setShowToast + setPlayingRecordings ) { const { t } = useTranslation(); const handlePlayRecording = async (cardIndex) => { @@ -54,8 +53,7 @@ export function usePlaybackFunction( } } catch (error) { console.error("Failed to resume AudioContext:", error); - setToastMessage("Error resuming audio playback: " + error.message); - setShowToast(true); + toast.error(`Error resuming audio playback: ${error.message}`); } } else { // AudioContext is already running @@ -82,8 +80,7 @@ export function usePlaybackFunction( (error) => { console.error("Error during playback:", error); isElectron() && window.electron.log("error", `Error during playback: ${error}`); - setToastMessage(`${t("toast.playbackError")} ${error.message}`); - setShowToast(true); + toast.error(`${t("toast.playbackError")} ${error.message}`); setIsRecordingPlaying(false); setActivePlaybackCard(null); setPlayingRecordings((prev) => ({ ...prev, [key]: false })); @@ -115,8 +112,7 @@ export function usePlaybackFunction( (error) => { console.error("Error during playback:", error); isElectron() && window.electron.log("error", `Error during playback: ${error}`); - setToastMessage(`${t("toast.playbackError")} ${error.message}`); - setShowToast(true); + toast.error(`${t("toast.playbackError")} ${error.message}`); setIsRecordingPlaying(false); setActivePlaybackCard(null); setPlayingRecordings((prev) => ({ ...prev, [key]: false })); diff --git a/src/components/sound_page/useRecordingFunction.jsx b/src/components/sound_page/hooks/useRecordingFunction.jsx similarity index 88% rename from src/components/sound_page/useRecordingFunction.jsx rename to src/components/sound_page/hooks/useRecordingFunction.jsx index 146c6805c..8cb25f03b 100644 --- a/src/components/sound_page/useRecordingFunction.jsx +++ b/src/components/sound_page/hooks/useRecordingFunction.jsx @@ -1,15 +1,14 @@ import { useCallback } from "react"; import { useTranslation } from "react-i18next"; -import { saveRecording } from "../../utils/databaseOperations"; -import { isElectron } from "../../utils/isElectron"; +import { saveRecording } from "../../../utils/databaseOperations"; +import { isElectron } from "../../../utils/isElectron"; +import { sonnerErrorToast, sonnerSuccessToast, sonnerWarningToast } from "../../../utils/sonnerCustomToast"; export function useRecordingFunction( activeRecordingCard, setActiveRecordingCard, setIsRecording, setMediaRecorder, - setToastMessage, - setShowToast, setRecordingAvailability, isRecording, mediaRecorder, @@ -65,8 +64,7 @@ export function useRecordingFunction( const recordingDuration = Date.now() - recordingStartTime; if (recordingDuration > MAX_RECORDING_DURATION_MS) { mediaRecorder.stop(); - setToastMessage(t("toast.recordingExceeded")); - setShowToast(true); + sonnerWarningToast(t("toast.recordingExceeded")); setIsRecording(false); setActiveRecordingCard(null); } @@ -74,8 +72,7 @@ export function useRecordingFunction( if (mediaRecorder.state === "inactive") { const audioBlob = new Blob(audioChunks, { type: mimeType }); saveRecording(audioBlob, recordingDataIndex, mimeType); - setToastMessage(t("toast.recordingSuccess")); - setShowToast(true); + sonnerSuccessToast(t("toast.recordingSuccess")); setRecordingAvailability((prev) => ({ ...prev, [recordingDataIndex]: true })); audioChunks = []; } @@ -85,8 +82,7 @@ export function useRecordingFunction( setTimeout(() => { if (mediaRecorder.state !== "inactive") { mediaRecorder.stop(); - setToastMessage(t("toast.recordingExceeded")); - setShowToast(true); + sonnerWarningToast(t("toast.recordingExceeded")); setIsRecording(false); setActiveRecordingCard(null); } @@ -95,8 +91,7 @@ export function useRecordingFunction( .catch((err) => { console.error("Error accessing the microphone.", err); isElectron() && window.electron.log("error", `Error accessing the microphone: ${err}`); - setToastMessage(`${t("toast.recordingFailed")} ${err.message}`); - setShowToast(true); + sonnerErrorToast(`${t("toast.recordingFailed")} ${err.message}`); setIsRecording(false); setActiveRecordingCard(null); }); @@ -122,8 +117,6 @@ export function useRecordingFunction( setActiveRecordingCard, setIsRecording, setMediaRecorder, - setToastMessage, - setShowToast, setRecordingAvailability, t, ] diff --git a/src/components/sound_page/hooks/useReview.jsx b/src/components/sound_page/hooks/useReview.jsx new file mode 100644 index 000000000..92f4bdca8 --- /dev/null +++ b/src/components/sound_page/hooks/useReview.jsx @@ -0,0 +1,40 @@ +import { useState, useEffect, useCallback } from "react"; + +export const useReview = (sound, accent, index, soundsData) => { + const findPhonemeDetails = useCallback( + (phoneme) => { + let phonemeIndex = soundsData.consonants.findIndex((p) => p.phoneme === phoneme); + if (phonemeIndex !== -1) return { index: phonemeIndex, type: "consonant" }; + + phonemeIndex = soundsData.vowels_n_diphthongs.findIndex((p) => p.phoneme === phoneme); + if (phonemeIndex !== -1) return { index: phonemeIndex, type: "vowel" }; + + return { index: -1, type: null }; + }, + [soundsData.consonants, soundsData.vowels_n_diphthongs] + ); + + const { index: phonemeIndex, type } = findPhonemeDetails(sound.phoneme); + + const reviewKey = `${type}${index + 1}`; + const [review, setReview] = useState(null); + + const handleReviewClick = (newReview) => { + setReview(newReview); + + const ispeakerData = JSON.parse(localStorage.getItem("ispeaker") || "{}"); + ispeakerData.soundReview = ispeakerData.soundReview || {}; + ispeakerData.soundReview[accent] = ispeakerData.soundReview[accent] || {}; + ispeakerData.soundReview[accent][reviewKey] = newReview; + + localStorage.setItem("ispeaker", JSON.stringify(ispeakerData)); + }; + + useEffect(() => { + const reviews = JSON.parse(localStorage.getItem("ispeaker") || "{}").soundReview || {}; + const soundReview = reviews[accent]?.[reviewKey]; + if (soundReview) setReview(soundReview); + }, [accent, reviewKey]); + + return { review, handleReviewClick }; +}; diff --git a/src/components/sound_page/useSoundVideoMapping.jsx b/src/components/sound_page/hooks/useSoundVideoMapping.jsx similarity index 98% rename from src/components/sound_page/useSoundVideoMapping.jsx rename to src/components/sound_page/hooks/useSoundVideoMapping.jsx index b289b31e6..da0a09365 100644 --- a/src/components/sound_page/useSoundVideoMapping.jsx +++ b/src/components/sound_page/hooks/useSoundVideoMapping.jsx @@ -1,5 +1,5 @@ import { useEffect, useState } from "react"; -import { isElectron } from "../../utils/isElectron"; +import { isElectron } from "../../../utils/isElectron"; export function useSoundVideoMapping(type, accent, soundsData, phonemeIndex) { const [videoUrl, setVideoUrl] = useState(null); diff --git a/src/index.jsx b/src/index.jsx index 188f7f2b1..7a03c277c 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -1,4 +1,4 @@ -import "bootstrap/dist/css/bootstrap.min.css"; +import "./styles/index.css"; import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App"; diff --git a/src/styles/index.css b/src/styles/index.css new file mode 100644 index 000000000..edba4f0de --- /dev/null +++ b/src/styles/index.css @@ -0,0 +1,7 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:where([data-sonner-toast]) :where([data-title]) { + @apply text-base; /* For success toasts */ +} diff --git a/src/ui/Container.jsx b/src/ui/Container.jsx new file mode 100644 index 000000000..1884f031e --- /dev/null +++ b/src/ui/Container.jsx @@ -0,0 +1,16 @@ +import PropTypes from "prop-types"; + +const Container = ({ children, className = "", ...props }) => { + return ( +
+ {children} +
+ ); +}; + +Container.propTypes = { + children: PropTypes.node.isRequired, + className: PropTypes.string, +}; + +export default Container; diff --git a/src/utils/ThemeContext/ThemeProvider.jsx b/src/utils/ThemeContext/ThemeProvider.jsx new file mode 100644 index 000000000..890f11b89 --- /dev/null +++ b/src/utils/ThemeContext/ThemeProvider.jsx @@ -0,0 +1,57 @@ +import { useEffect, useState } from "react"; +import ThemeProviderContext from "./ThemeProviderContext"; + +export function ThemeProvider({ children, defaultTheme = "auto", storageKey = "ispeakerreact-ui-theme" }) { + const [theme, setTheme] = useState(() => { + return localStorage.getItem(storageKey) || defaultTheme; + }); + + useEffect(() => { + const root = window.document.documentElement; + + // Function to update the theme + const updateTheme = () => { + const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; + + switch (theme) { + case "auto": + root.setAttribute("data-theme", systemTheme === "dark" ? "dim" : systemTheme); + break; + case "dark": + root.setAttribute("data-theme", "dim"); + break; + default: + root.setAttribute("data-theme", theme); + break; + } + }; + + // Initial theme setup + updateTheme(); + + // Add event listener for system theme changes + const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); + const handleChange = () => { + if (theme === "auto") { + updateTheme(); + } + }; + + mediaQuery.addEventListener("change", handleChange); + + // Cleanup event listener + return () => { + mediaQuery.removeEventListener("change", handleChange); + }; + }, [theme]); + + const value = { + theme, + setTheme: (newTheme) => { + localStorage.setItem(storageKey, newTheme); + setTheme(newTheme); + }, + }; + + return {children}; +} diff --git a/src/utils/ThemeContext/ThemeProviderContext.jsx b/src/utils/ThemeContext/ThemeProviderContext.jsx new file mode 100644 index 000000000..1b8d92714 --- /dev/null +++ b/src/utils/ThemeContext/ThemeProviderContext.jsx @@ -0,0 +1,8 @@ +import { createContext } from "react"; + +const ThemeProviderContext = createContext({ + theme: "auto", + setTheme: () => null, +}); + +export default ThemeProviderContext; diff --git a/src/utils/ThemeContext/useTheme.jsx b/src/utils/ThemeContext/useTheme.jsx new file mode 100644 index 000000000..f2e172902 --- /dev/null +++ b/src/utils/ThemeContext/useTheme.jsx @@ -0,0 +1,12 @@ +import { useContext } from "react"; +import ThemeProviderContext from "./ThemeProviderContext"; + +export const useTheme = () => { + const context = useContext(ThemeProviderContext); + + if (!context) { + throw new Error("useTheme must be used within a ThemeProvider"); + } + + return context; +}; diff --git a/src/utils/ThemeProvider.jsx b/src/utils/ThemeProvider.jsx deleted file mode 100644 index bdb44b826..000000000 --- a/src/utils/ThemeProvider.jsx +++ /dev/null @@ -1,50 +0,0 @@ -import { createContext, useEffect, useState } from "react"; - -export const ThemeContext = createContext(); - -export const ThemeProvider = ({ children }) => { - const [theme, setTheme] = useState(localStorage.getItem("theme") || "auto"); - - // Initialize showToggleButton from localStorage or default to true - const [showToggleButton, setShowToggleButton] = useState(() => { - const savedSettings = JSON.parse(localStorage.getItem("ispeaker")); - return savedSettings && savedSettings.showToggleButton !== undefined ? savedSettings.showToggleButton : true; // Default to true if not found - }); - - // Effect to apply the theme and save it to localStorage - useEffect(() => { - const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); - const applyTheme = (themeValue) => { - const newTheme = themeValue === "auto" ? (mediaQuery.matches ? "dark" : "light") : themeValue; - document.documentElement.setAttribute("data-bs-theme", newTheme); - localStorage.setItem("theme", themeValue); - }; - - // Initial theme apply - applyTheme(theme); - - // Listener for system theme changes - const handleChange = () => { - if (theme === "auto") { - applyTheme("auto"); - } - }; - mediaQuery.addEventListener("change", handleChange); - - // Cleanup - return () => mediaQuery.removeEventListener("change", handleChange); - }, [theme]); - - // Effect to save showToggleButton to localStorage - useEffect(() => { - const savedSettings = JSON.parse(localStorage.getItem("ispeaker")) || {}; - savedSettings.showToggleButton = showToggleButton; - localStorage.setItem("ispeaker", JSON.stringify(savedSettings)); - }, [showToggleButton]); - - return ( - - {children} - - ); -}; diff --git a/src/utils/ThemeSwitcher.jsx b/src/utils/ThemeSwitcher.jsx deleted file mode 100644 index 32ca535de..000000000 --- a/src/utils/ThemeSwitcher.jsx +++ /dev/null @@ -1,39 +0,0 @@ -import { Dropdown } from "react-bootstrap"; -import { CircleHalf, MoonStarsFill, SunFill } from "react-bootstrap-icons"; -import { useContext } from "react"; -import { ThemeContext } from "../utils/ThemeProvider"; - -const ThemeSwitcher = () => { - const { theme, setTheme, showToggleButton } = useContext(ThemeContext); - - const themeItems = [ - { name: "Light", value: "light", icon: }, - { name: "Dark", value: "dark", icon: }, - { name: "Auto", value: "auto", icon: }, - ]; - - return ( - <> - - - {themeItems.find((item) => item.value === theme).icon} - Toggle theme - - - - {themeItems.map((item) => ( - setTheme(item.value)} - active={theme === item.value}> - {item.icon} - {item.name} - - ))} - - - - ); -}; - -export default ThemeSwitcher; diff --git a/src/utils/phonemeUtils.jsx b/src/utils/phonemeUtils.jsx new file mode 100644 index 000000000..a61a27863 --- /dev/null +++ b/src/utils/phonemeUtils.jsx @@ -0,0 +1,9 @@ +export const findPhonemeDetails = (phoneme, soundsData) => { + let phonemeIndex = soundsData.consonants.findIndex((p) => p.phoneme === phoneme); + if (phonemeIndex !== -1) return { index: phonemeIndex, type: "consonant" }; + + phonemeIndex = soundsData.vowels_n_diphthongs.findIndex((p) => p.phoneme === phoneme); + if (phonemeIndex !== -1) return { index: phonemeIndex, type: "vowel" }; + + return { index: -1, type: null }; // Not found +}; diff --git a/src/utils/sonnerCustomToast.jsx b/src/utils/sonnerCustomToast.jsx new file mode 100644 index 000000000..464b897d3 --- /dev/null +++ b/src/utils/sonnerCustomToast.jsx @@ -0,0 +1,29 @@ +import { IoAlertCircleOutline, IoCheckmarkCircleOutline, IoWarningOutline } from "react-icons/io5"; +import { toast } from "sonner"; + +export const sonnerSuccessToast = (message) => { + toast.custom(() => ( +
+ + {message} +
+ )); +}; + +export const sonnerWarningToast = (message) => { + toast.custom(() => ( +
+ + {message} +
+ )); +}; + +export const sonnerErrorToast = (message) => { + toast.custom(() => ( +
+ + {message} +
+ )); +}; diff --git a/src/utils/useTheme.jsx b/src/utils/useTheme.jsx deleted file mode 100644 index 37b675644..000000000 --- a/src/utils/useTheme.jsx +++ /dev/null @@ -1,4 +0,0 @@ -import { useContext } from "react"; -import { ThemeContext } from "./ThemeProvider"; - -export const useTheme = () => useContext(ThemeContext); diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 000000000..11cde2119 --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,33 @@ +/** @type {import('tailwindcss').Config} */ +import typography from "@tailwindcss/typography"; +import daisyui from "daisyui"; + +export default { + content: ["./index.html", "./src/**/*.{html,js,jsx}"], + theme: { + extend: {}, + }, + plugins: [typography, daisyui], + daisyui: { + themes: [ + { + light: { + "color-scheme": "light", + primary: "oklch(64.23% 0.1467 133.01)", + secondary: "oklch(83.66% 0.1165 66.29)", + accent: "oklch(85.39% 0.201 100.73)", + neutral: "oklch(30.98% 0.075 108.6)", + "base-100": "oklch(98.71% 0.02 123.72)", + info: "oklch(86.19% 0.047 224.14)", + success: "oklch(52.73% 0.1371 150.07)", + "success-content": "white", + warning: "oklch(78.39% 0.1719 68.09)", + error: "oklch(50.6% 0.1927 27.7)", + }, + }, + "dim", + ], + darkTheme: "dim", + }, + darkMode: ["selector", '[data-theme="dim"]'], +}; From 85f4179320491b5fc4b63430e1595770b5dc7b74 Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 10 Dec 2024 10:42:52 +0700 Subject: [PATCH 02/95] Update packages --- package-lock.json | 36 ++++++++++++++++++------------------ package.json | 8 ++++---- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/package-lock.json b/package-lock.json index 208f6f569..bd7de6896 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ "express": "^4.21.2", "he": "^1.2.0", "i18next": "^24.0.5", - "i18next-browser-languagedetector": "^8.0.1", + "i18next-browser-languagedetector": "^8.0.2", "i18next-http-backend": "^3.0.1", "lodash": "^4.17.21", "masonry-layout": "^4.2.2", @@ -30,7 +30,7 @@ "react-bootstrap-icons": "^1.11.5", "react-dom": "^19.0.0", "react-flip-toolkit": "^7.2.4", - "react-i18next": "^15.1.3", + "react-i18next": "^15.1.4", "react-icons": "^5.4.0", "react-loading-skeleton": "^3.5.0", "react-router-dom": "^7.0.2", @@ -49,12 +49,12 @@ "@eslint/js": "^9.16.0", "@tailwindcss/typography": "^0.5.15", "@types/react": "^19.0.1", - "@types/react-dom": "^19.0.1", + "@types/react-dom": "^19.0.2", "@vitejs/plugin-react": "^4.3.4", "autoprefixer": "^10.4.20", "concurrently": "^9.1.0", "cross-env": "^7.0.3", - "daisyui": "^4.12.14", + "daisyui": "^4.12.20", "electron": "^33.2.1", "eslint": "^9.16.0", "eslint-plugin-react": "^7.37.2", @@ -3331,12 +3331,12 @@ } }, "node_modules/@types/react-dom": { - "version": "19.0.1", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.1.tgz", - "integrity": "sha512-hljHij7MpWPKF6u5vojuyfV0YA4YURsQG7KT6SzV0Zs2BXAtgdTxG6A229Ub/xiWV4w/7JL8fi6aAyjshH4meA==", + "version": "19.0.2", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.2.tgz", + "integrity": "sha512-c1s+7TKFaDRRxr1TxccIX2u7sfCnc3RxkVyBIUA2lCpyqCF+QoAwQ/CBg7bsMdVwP120HEH143VQezKtef5nCg==", "dev": true, - "dependencies": { - "@types/react": "*" + "peerDependencies": { + "@types/react": "^19.0.0" } }, "node_modules/@types/react-transition-group": { @@ -4745,9 +4745,9 @@ } }, "node_modules/daisyui": { - "version": "4.12.14", - "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-4.12.14.tgz", - "integrity": "sha512-hA27cdBasdwd4/iEjn+aidoCrRroDuo3G5W9NDKaVCJI437Mm/3eSL/2u7MkZ0pt8a+TrYF3aT2pFVemTS3how==", + "version": "4.12.20", + "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-4.12.20.tgz", + "integrity": "sha512-uHr3SQsd2yTjRdVuswTiqGFvZTxX0sGSBRa8JJdbKgmZCk/kRFh4B7Z2jg9vLIdwsHTHPyPgCkZadQo1ce0tAw==", "dev": true, "dependencies": { "css-selector-tokenizer": "^0.8", @@ -7288,9 +7288,9 @@ } }, "node_modules/i18next-browser-languagedetector": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.0.1.tgz", - "integrity": "sha512-z9ZuWA7qxbww+cPtdJTgV0O2H9+qlLpQnb37RpnwfsWnUmrO+q92gbVKVtfBL7jRvxfmVMOUKxKGg6VBqO49Pg==", + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.0.2.tgz", + "integrity": "sha512-shBvPmnIyZeD2VU5jVGIOWP7u9qNG3Lj7mpaiPFpbJ3LVfHZJvVzKR4v1Cb91wAOFpNw442N+LGPzHOHsten2g==", "dependencies": { "@babel/runtime": "^7.23.2" } @@ -10139,9 +10139,9 @@ } }, "node_modules/react-i18next": { - "version": "15.1.3", - "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.1.3.tgz", - "integrity": "sha512-J11oA30FbM3NZegUZjn8ySK903z6PLBz/ZuBYyT1JMR0QPrW6PFXvl1WoUhortdGi9dM0m48/zJQlPskVZXgVw==", + "version": "15.1.4", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.1.4.tgz", + "integrity": "sha512-2tai71gmehbvl9ZIqPMqlCCkm/cbeV1G4STpmM3C8Uzo6T2l8jDvZxEVSsQKt8blP9X34iRFP/k1ROqG2296MQ==", "dependencies": { "@babel/runtime": "^7.25.0", "html-parse-stringify": "^3.0.1" diff --git a/package.json b/package.json index aede82620..be9460570 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "express": "^4.21.2", "he": "^1.2.0", "i18next": "^24.0.5", - "i18next-browser-languagedetector": "^8.0.1", + "i18next-browser-languagedetector": "^8.0.2", "i18next-http-backend": "^3.0.1", "lodash": "^4.17.21", "masonry-layout": "^4.2.2", @@ -42,7 +42,7 @@ "react-bootstrap-icons": "^1.11.5", "react-dom": "^19.0.0", "react-flip-toolkit": "^7.2.4", - "react-i18next": "^15.1.3", + "react-i18next": "^15.1.4", "react-icons": "^5.4.0", "react-loading-skeleton": "^3.5.0", "react-router-dom": "^7.0.2", @@ -61,12 +61,12 @@ "@eslint/js": "^9.16.0", "@tailwindcss/typography": "^0.5.15", "@types/react": "^19.0.1", - "@types/react-dom": "^19.0.1", + "@types/react-dom": "^19.0.2", "@vitejs/plugin-react": "^4.3.4", "autoprefixer": "^10.4.20", "concurrently": "^9.1.0", "cross-env": "^7.0.3", - "daisyui": "^4.12.14", + "daisyui": "^4.12.20", "electron": "^33.2.1", "eslint": "^9.16.0", "eslint-plugin-react": "^7.37.2", From e289b28e0787e5e71b89de4d57370d1b0b2b0912 Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 10 Dec 2024 13:41:52 +0700 Subject: [PATCH 03/95] Update index.jsx --- src/index.jsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/index.jsx b/src/index.jsx index 7a03c277c..8448c9216 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -1,12 +1,12 @@ -import "./styles/index.css"; -import React from "react"; -import ReactDOM from "react-dom/client"; -import App from "./App"; +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; import "../i18n"; +import App from "./App"; +import "./styles/index.css"; -const root = ReactDOM.createRoot(document.getElementById("root")); +const root = createRoot(document.getElementById("root")); root.render( - + - + ); From 86bac249c4264afcfdb75d4b60d4f3e4ef133a29 Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 10 Dec 2024 13:42:05 +0700 Subject: [PATCH 04/95] Add footer --- src/components/Homepage.jsx | 40 +++----------------- src/components/general/Footer.jsx | 44 ++++++++++++++++++++++ src/components/general/LogoLightOrDark.jsx | 37 ++++++++++++++++++ 3 files changed, 86 insertions(+), 35 deletions(-) create mode 100644 src/components/general/Footer.jsx create mode 100644 src/components/general/LogoLightOrDark.jsx diff --git a/src/components/Homepage.jsx b/src/components/Homepage.jsx index 148e61445..0d4ac571f 100644 --- a/src/components/Homepage.jsx +++ b/src/components/Homepage.jsx @@ -1,42 +1,15 @@ -import { useEffect, useState } from "react"; +import { useEffect } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import Container from "../ui/Container"; -import { useTheme } from "../utils/ThemeContext/useTheme"; +import Footer from "./general/Footer"; +import LogoLightOrDark from "./general/LogoLightOrDark"; import TopNavBar from "./general/TopNavBar"; function Homepage() { const { t } = useTranslation(); const navigate = useNavigate(); - const { theme } = useTheme(); - const [currentTheme, setCurrentTheme] = useState(theme); - const [isDarkMode, setIsDarkMode] = useState(false); - - useEffect(() => { - const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); - - const updateTheme = () => { - if (theme === "auto") { - const systemPrefersDark = mediaQuery.matches; - setCurrentTheme(systemPrefersDark ? "dark" : "light"); - setIsDarkMode(systemPrefersDark); - } else { - setCurrentTheme(theme); - setIsDarkMode(theme === "dark"); - } - }; - - // Initial check and listener - updateTheme(); - if (theme === "auto") { - mediaQuery.addEventListener("change", updateTheme); - } - - return () => { - mediaQuery.removeEventListener("change", updateTheme); - }; - }, [theme]); const handleNavigate = (path) => { navigate(path); @@ -46,10 +19,6 @@ function Homepage() { document.title = `${t("navigation.home")} | iSpeakerReact v${__APP_VERSION__}`; }, [t]); - const logoSrc = isDarkMode - ? `${import.meta.env.BASE_URL}images/logos/ispeakerreact-no-background-darkmode.svg` - : `${import.meta.env.BASE_URL}images/logos/ispeakerreact-no-background.svg`; - const cardsInfo = [ { title: "Sounds", @@ -93,7 +62,7 @@ function Homepage() {
- iSpeakerReact logo +

iSpeakerReact

v{__APP_VERSION__}

@@ -122,6 +91,7 @@ function Homepage() { ))}
+
); } diff --git a/src/components/general/Footer.jsx b/src/components/general/Footer.jsx new file mode 100644 index 000000000..302763166 --- /dev/null +++ b/src/components/general/Footer.jsx @@ -0,0 +1,44 @@ +import LogoLightOrDark from "./LogoLightOrDark"; + +const Footer = () => { + return ( +
+ + +
+ ); +}; + +export default Footer; diff --git a/src/components/general/LogoLightOrDark.jsx b/src/components/general/LogoLightOrDark.jsx new file mode 100644 index 000000000..ef44fd4c0 --- /dev/null +++ b/src/components/general/LogoLightOrDark.jsx @@ -0,0 +1,37 @@ +import { useTheme } from "../../utils/ThemeContext/useTheme"; +import { useState, useEffect } from "react"; + +const LogoLightOrDark = ({ width, height }) => { + const { theme } = useTheme(); + const [isDarkMode, setIsDarkMode] = useState(false); + + useEffect(() => { + const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); + + const updateTheme = () => { + const systemPrefersDark = mediaQuery.matches; + const newIsDarkMode = theme === "auto" ? systemPrefersDark : theme === "dark"; + + if (newIsDarkMode !== isDarkMode) { + setIsDarkMode(newIsDarkMode); + } + }; + + updateTheme(); + if (theme === "auto") { + mediaQuery.addEventListener("change", updateTheme); + } + + return () => { + mediaQuery.removeEventListener("change", updateTheme); + }; + }, [theme]); + + const logoSrc = isDarkMode + ? `${import.meta.env.BASE_URL}images/logos/ispeakerreact-no-background-darkmode.svg` + : `${import.meta.env.BASE_URL}images/logos/ispeakerreact-no-background.svg`; + + return iSpeakerReact logo; +}; + +export default LogoLightOrDark; From c53e92e927e3c51ae087f7d92cc859dea1532932 Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 10 Dec 2024 13:42:27 +0700 Subject: [PATCH 05/95] Make the tab stick to top when scrolling --- src/components/sound_page/PracticeSound.jsx | 55 +++++++++++---------- src/components/sound_page/SoundList.jsx | 46 +++++++++-------- 2 files changed, 54 insertions(+), 47 deletions(-) diff --git a/src/components/sound_page/PracticeSound.jsx b/src/components/sound_page/PracticeSound.jsx index d4ec169e0..809ebe516 100644 --- a/src/components/sound_page/PracticeSound.jsx +++ b/src/components/sound_page/PracticeSound.jsx @@ -193,38 +193,39 @@ const PracticeSound = ({ sound, accent, onBack, index, soundsData }) => {
-
- {/* Menu */} -
    -
  • - -
  • -
  • - -
  • -
  • - -
  • -
+
+
+ {/* Menu */} +
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+
{/* Tab Content */} -
+
{activeTab === "watchTab" && ( { ) : ( <> -
- {/* Menu */} -
    -
  • - -
  • -
  • - -
  • -
+
+
+ {/* Menu */} +
    +
  • + +
  • +
  • + +
  • +
+
{/* Tab Content */}
From 8d256289259299996f6ca5dde879b3042860d034 Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 10 Dec 2024 13:42:39 +0700 Subject: [PATCH 06/95] Prevent overflow when iframe is loading --- src/components/sound_page/WatchVideoCard.jsx | 72 ++++++++++---------- 1 file changed, 37 insertions(+), 35 deletions(-) diff --git a/src/components/sound_page/WatchVideoCard.jsx b/src/components/sound_page/WatchVideoCard.jsx index 56c7380d3..3a232ea32 100644 --- a/src/components/sound_page/WatchVideoCard.jsx +++ b/src/components/sound_page/WatchVideoCard.jsx @@ -3,43 +3,45 @@ import { isElectron } from "../../utils/isElectron"; export const WatchVideoCard = ({ t, videoUrl, iframeLoadingStates, handleIframeLoad }) => { return ( -
-
-
-
- {isElectron() && videoUrl?.isLocal && videoUrl.value.includes("http://localhost") ? ( - - ) : ( - <> - {iframeLoadingStates.mainIframe &&
} - - - )} +
+
+
+
+
+ {isElectron() && videoUrl?.isLocal && videoUrl.value.includes("http://localhost") ? ( + + ) : ( + <> + {iframeLoadingStates.mainIframe &&
} + + + )} +
+ {isElectron() && !videoUrl?.value.includes("http://localhost") ? ( +
+ + {t("alert.alertOnlineVideo")} +
+ ) : ( + "" + )}
- {isElectron() && !videoUrl?.value.includes("http://localhost") ? ( -
- - {t("alert.alertOnlineVideo")} -
- ) : ( - "" - )}
); From 6ddaed4e26eebbef013e1ff3831c4c30955a1462 Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 10 Dec 2024 16:42:02 +0700 Subject: [PATCH 07/95] Minor tweak to tabs for Sounds --- src/components/sound_page/PracticeSound.jsx | 18 ++++++++++++------ src/components/sound_page/SoundList.jsx | 4 ++-- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/components/sound_page/PracticeSound.jsx b/src/components/sound_page/PracticeSound.jsx index 809ebe516..48a737fdf 100644 --- a/src/components/sound_page/PracticeSound.jsx +++ b/src/components/sound_page/PracticeSound.jsx @@ -193,28 +193,34 @@ const PracticeSound = ({ sound, accent, onBack, index, soundsData }) => {
-
+
{/* Menu */}
  • @@ -223,7 +229,7 @@ const PracticeSound = ({ sound, accent, onBack, index, soundsData }) => {
{/* Tab Content */} -
+
{activeTab === "watchTab" && ( {

diff --git a/src/components/sound_page/SoundList.jsx b/src/components/sound_page/SoundList.jsx index da3a1c163..bc29e18e4 100644 --- a/src/components/sound_page/SoundList.jsx +++ b/src/components/sound_page/SoundList.jsx @@ -163,7 +163,7 @@ const SoundList = () => { type="button" onClick={() => setActiveTab("tab1")} className={`md:text-base ${ - activeTab === "tab1" ? "active" : "" + activeTab === "tab1" ? "active font-semibold" : "" }`}> {t("sound_page.consonants")} @@ -173,7 +173,7 @@ const SoundList = () => { type="button" onClick={() => setActiveTab("tab2")} className={`md:text-base ${ - activeTab === "tab2" ? "active" : "" + activeTab === "tab2" ? "active font-semibold" : "" }`}> {t("sound_page.vowels_dipthongs")} From 25a9653461e18abc57a174d28e5e7334864c8897 Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 10 Dec 2024 22:32:09 +0700 Subject: [PATCH 08/95] Update back icon for Sounds --- src/components/sound_page/PracticeSound.jsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/sound_page/PracticeSound.jsx b/src/components/sound_page/PracticeSound.jsx index 48a737fdf..bf3418aa7 100644 --- a/src/components/sound_page/PracticeSound.jsx +++ b/src/components/sound_page/PracticeSound.jsx @@ -1,18 +1,18 @@ import he from "he"; import { useCallback, useEffect, useState } from "react"; -import { BsArrowLeftCircle, BsRecordCircleFill } from "react-icons/bs"; -import { IoInformationCircleOutline } from "react-icons/io5"; -import { MdOutlineOndemandVideo, MdKeyboardVoice, MdChecklist } from "react-icons/md"; +import { BsRecordCircleFill } from "react-icons/bs"; +import { IoChevronBackOutline, IoInformationCircleOutline } from "react-icons/io5"; +import { MdChecklist, MdKeyboardVoice, MdOutlineOndemandVideo } from "react-icons/md"; import { Trans, useTranslation } from "react-i18next"; import { checkRecordingExists } from "../../utils/databaseOperations"; import { isElectron } from "../../utils/isElectron"; import LoadingOverlay from "../general/LoadingOverlay"; -import ReviewCard from "./ReviewCard"; -import SoundPracticeCard from "./SoundPracticeCard"; import { usePlaybackFunction } from "./hooks/usePlaybackFunction"; import { useRecordingFunction } from "./hooks/useRecordingFunction"; import { useSoundVideoMapping } from "./hooks/useSoundVideoMapping"; +import ReviewCard from "./ReviewCard"; +import SoundPracticeCard from "./SoundPracticeCard"; import { WatchVideoCard } from "./WatchVideoCard"; const PracticeSound = ({ sound, accent, onBack, index, soundsData }) => { @@ -189,7 +189,7 @@ const PracticeSound = ({ sound, accent, onBack, index, soundsData }) => { )}

From fb3ea933a1e458ecfe5a4dd7c1a66e72007dd91b Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 10 Dec 2024 23:02:10 +0700 Subject: [PATCH 09/95] Update PracticeSound.jsx --- src/components/sound_page/PracticeSound.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/sound_page/PracticeSound.jsx b/src/components/sound_page/PracticeSound.jsx index bf3418aa7..faa9e2d17 100644 --- a/src/components/sound_page/PracticeSound.jsx +++ b/src/components/sound_page/PracticeSound.jsx @@ -1,7 +1,7 @@ import he from "he"; import { useCallback, useEffect, useState } from "react"; -import { BsRecordCircleFill } from "react-icons/bs"; -import { IoChevronBackOutline, IoInformationCircleOutline } from "react-icons/io5"; +import { BsChevronLeft, BsRecordCircleFill } from "react-icons/bs"; +import { IoChevronBackOutline } from "react-icons/io5"; import { MdChecklist, MdKeyboardVoice, MdOutlineOndemandVideo } from "react-icons/md"; import { Trans, useTranslation } from "react-i18next"; @@ -314,7 +314,7 @@ const PracticeSound = ({ sound, accent, onBack, index, soundsData }) => {
{isElectron() && !selectedVideoUrl.startsWith("http://localhost") && (
- + {t("alert.alertOnlineVideo")}
)} From 5de6cd59ebfc25d7bbcfbac4923efffd8924f657 Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 10 Dec 2024 23:02:24 +0700 Subject: [PATCH 10/95] Accent dropdown: add emojis --- src/components/general/AccentDropdown.jsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/general/AccentDropdown.jsx b/src/components/general/AccentDropdown.jsx index 6b1f1c391..ad9f5700d 100644 --- a/src/components/general/AccentDropdown.jsx +++ b/src/components/general/AccentDropdown.jsx @@ -7,8 +7,8 @@ const AccentDropdown = ({ onAccentChange }) => { const { t } = useTranslation(); const selectedAccentOptions = [ - { name: `${t("accent.accentAmerican")}`, value: "american" }, - { name: `${t("accent.accentBritish")}`, value: "british" }, + { name: `${t("accent.accentAmerican")}`, value: "american", emoji: "🇺🇸" }, + { name: `${t("accent.accentBritish")}`, value: "british", emoji: "🇬🇧" }, ]; useEffect(() => { @@ -28,6 +28,9 @@ const AccentDropdown = ({ onAccentChange }) => {

{t("accent.accentSettings")}:

+ + {selectedAccentOptions.find((item) => item.value === selectedAccent).emoji} + {" "} {selectedAccentOptions.find((item) => item.value === selectedAccent).name}
    { selectedAccent === item.value ? "btn-active" : "" } btn btn-sm btn-block btn-ghost justify-start`} onClick={() => handleAccentChange(item.value)}> - {item.name} + {item.emoji} {item.name} ))} From 14a9815d00ad3bc132009b3d6056d190ce38580d Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Wed, 11 Dec 2024 22:15:26 +0700 Subject: [PATCH 11/95] Update tailwind.config.js --- tailwind.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tailwind.config.js b/tailwind.config.js index 11cde2119..76cc87cea 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -13,7 +13,7 @@ export default { { light: { "color-scheme": "light", - primary: "oklch(64.23% 0.1467 133.01)", + primary: "oklch(65.88% 0.1467 133.01)", secondary: "oklch(83.66% 0.1165 66.29)", accent: "oklch(85.39% 0.201 100.73)", neutral: "oklch(30.98% 0.075 108.6)", From 2cab2328d1f3086313c44ba8563667bf244f8cdb Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Wed, 11 Dec 2024 22:15:35 +0700 Subject: [PATCH 12/95] Update en.json --- public/locales/en.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/public/locales/en.json b/public/locales/en.json index d1bc025f9..f43920bbf 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -90,6 +90,7 @@ }, "exercise_page": { "exerciseSubheading": "Select an exercise to get started.", + "modalInfoHeader": "Exercise info", "dictationHeading": "Dictation", "matchUpHeading": "Match-up", "reorderingHeading": "Reordering", @@ -121,6 +122,7 @@ "quitBtn": "Quit", "restartBtn": "Restart quiz", "backBtn": "Back to exercise list", + "instructionBtn": "Instructions", "collapseBtn": "Collapse instructions", "expandBtn": "View instructions" }, From d095f0ed789230b577a78d2dfaf3f29c0c5fcb60 Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Wed, 11 Dec 2024 22:15:51 +0700 Subject: [PATCH 13/95] Update dictation quiz UI --- .../exercise_page/DictationQuiz.jsx | 167 +++++++----- .../exercise_page/ExerciseDetailPage.jsx | 250 ++++++++++-------- src/components/exercise_page/ExercisePage.jsx | 151 ++++++----- 3 files changed, 325 insertions(+), 243 deletions(-) diff --git a/src/components/exercise_page/DictationQuiz.jsx b/src/components/exercise_page/DictationQuiz.jsx index 755a6f9c9..24b8b139f 100644 --- a/src/components/exercise_page/DictationQuiz.jsx +++ b/src/components/exercise_page/DictationQuiz.jsx @@ -1,9 +1,10 @@ import _ from "lodash"; import { useEffect, useRef, useState } from "react"; -import { Alert, Button, Card, Col, Form, Row, Spinner } from "react-bootstrap"; -import { ArrowRightCircle, Check2Circle, VolumeUp, VolumeUpFill, XCircle } from "react-bootstrap-icons"; -import useCountdownTimer from "../../utils/useCountdownTimer"; import { useTranslation } from "react-i18next"; +import { AiOutlineCheckCircle, AiOutlineCloseCircle } from "react-icons/ai"; +import { IoInformationCircleOutline, IoVolumeHigh, IoVolumeHighOutline } from "react-icons/io5"; +import { LiaCheckCircle, LiaChevronCircleRightSolid, LiaTimesCircle } from "react-icons/lia"; +import useCountdownTimer from "../../utils/useCountdownTimer"; const DictationQuiz = ({ quiz, timer, onAnswer, onQuit, setTimeIsUp }) => { const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0); @@ -75,7 +76,7 @@ const DictationQuiz = ({ quiz, timer, onAnswer, onQuit, setTimeIsUp }) => { "" ) : ( <> - {t("exercise_page.result.correctAnswer")} {correctAnswer} + {t("exercise_page.result.correctAnswer")} {correctAnswer} ) ); @@ -167,27 +168,47 @@ const DictationQuiz = ({ quiz, timer, onAnswer, onQuit, setTimeIsUp }) => { return currentWords.map((word, index) => { if (word.value) { - return {word.value}; + return ( + + {word.value} + + ); } if (word.textbox) { + const isCorrect = answer.trim().toLowerCase() === word.textbox.toLowerCase(); + return ( - { - setAnswer(e.target.value); - startTimer(); - }} - isInvalid={validationVariant === "danger" && showValidation} - isValid={validationVariant === "success" && showValidation} - autoComplete="off" - spellCheck="false" - disabled={isTextboxDisabled} - className={`px-0 text-center${hasValueAndTextbox ? " mx-2" : " w-50 mx-auto"}`} - style={hasValueAndTextbox ? { width: "40%", display: "inline-block" } : {}} - /> + className={`inline-block my-2 ${hasValueAndTextbox ? "w-48" : "w-full lg:w-3/4"}`}> + + ); } @@ -197,57 +218,69 @@ const DictationQuiz = ({ quiz, timer, onAnswer, onQuit, setTimeIsUp }) => { return ( <> - -
    -
    {t("exercise_page.questionNo")} #{currentQuestionIndex + 1}
    - {timer > 0 &&
    {t("exercise_page.timer")} {formatTime()}
    } +
    +
    + {timer > 0 ? ( +
    +
    + {t("exercise_page.questionNo")} #{currentQuestionIndex + 1} +
    +
    + {t("exercise_page.timer")} {formatTime()} +
    +
    + ) : ( +

    + {t("exercise_page.questionNo")} #{currentQuestionIndex + 1} +

    + )}
    - - - - - - - -
    - -
    {renderWords()}
    -
    -
    - -
    +
    +
    + +
    +
    +
    {renderWords()}
    +
    {showValidation && validationVariant === "danger" && ( - - {validationMessage} - +
    +
    + + {validationMessage} +
    +
    )} -
    - - {currentQuestionIndex < quiz.length - 1 && ( - - )} - +
    +
    + + {currentQuestionIndex < quiz.length - 1 && ( + + )} + +
    - +
    ); }; diff --git a/src/components/exercise_page/ExerciseDetailPage.jsx b/src/components/exercise_page/ExerciseDetailPage.jsx index 78a04f8e8..52c796007 100644 --- a/src/components/exercise_page/ExerciseDetailPage.jsx +++ b/src/components/exercise_page/ExerciseDetailPage.jsx @@ -1,8 +1,8 @@ import _ from "lodash"; import { Suspense, lazy, useCallback, useEffect, useState } from "react"; -import { Button, Card, Col, Collapse, Row } from "react-bootstrap"; -import { ArrowCounterclockwise, ArrowLeftCircle, ChevronDown, ChevronUp } from "react-bootstrap-icons"; import { useTranslation } from "react-i18next"; +import { BsChevronLeft } from "react-icons/bs"; +import { PiArrowsCounterClockwise } from "react-icons/pi"; import { isElectron } from "../../utils/isElectron"; import LoadingOverlay from "../general/LoadingOverlay"; import { getFileFromIndexedDB, saveFileToIndexedDB } from "../setting_page/offlineStorageDb"; @@ -221,45 +221,45 @@ const ExerciseDetailPage = ({ heading, id, title, accent, file, onBack }) => { {t("exercise_page.encouragementMsg.level0")} 🚀 ); + const percentage = (score / totalAnswered) * 100; - if (percentage === 100) { - return ( - <> - {t("exercise_page.encouragementMsg.level6")} 🎉 - - ); - } else if (percentage >= 80) { - return ( - <> - {t("exercise_page.encouragementMsg.level5")} 👍 - - ); - } else if (percentage >= 60) { - return ( - <> - {t("exercise_page.encouragementMsg.level4")} 😊 - - ); - } else if (percentage >= 40) { - return ( - <> - {t("exercise_page.encouragementMsg.level3")} 💪 - - ); - } else if (percentage >= 20) { - return ( - <> - {t("exercise_page.encouragementMsg.level2")} 🌱 - - ); - } else { - return ( - <> - {t("exercise_page.encouragementMsg.level1")} 🛤️ - - ); + let level; + switch (true) { + case percentage === 100: + level = 6; + break; + case percentage >= 80: + level = 5; + break; + case percentage >= 60: + level = 4; + break; + case percentage >= 40: + level = 3; + break; + case percentage >= 20: + level = 2; + break; + default: + level = 1; } + + const emojis = { + 6: "🎉", + 5: "👍", + 4: "😊", + 3: "💪", + 2: "🌱", + 1: "🛤️", + }; + + return ( + <> + {t(`exercise_page.encouragementMsg.level${level}`)}{" "} + {emojis[level]} + + ); }; const encouragementMessage = quizCompleted && totalAnswered > 0 ? getEncouragementMessage() : null; @@ -295,7 +295,7 @@ const ExerciseDetailPage = ({ heading, id, title, accent, file, onBack }) => { onMatchFinished={handleMatchFinished} /> ) : ( - This quiz type is not yet implemented. +
    This quiz type is not yet implemented.
    )} ); @@ -307,71 +307,87 @@ const ExerciseDetailPage = ({ heading, id, title, accent, file, onBack }) => { ) : ( <> -

    {t(heading)}

    - - - - {title} - -

    - {t("accent.accentSettings")}:{" "} - {accent === "American English" - ? t("accent.accentAmerican") - : t("accent.accentBritish")} -

    - -
    - - -
    - - {instructions && - Array.isArray(instructions) && - instructions.length > 0 ? ( - instructions.map((instruction, index) => ( -

    - {instruction} -

    - )) - ) : ( -

    - [Instructions for this type of exercise is not yet - translated. Please update accordingly.] -

    - )} -
    -
    -
    +

    {t(heading)}

    +

    {title}

    +
    +
    +

    + {t("accent.accentSettings")}:{" "} + {accent === "American English" ? t("accent.accentAmerican") : t("accent.accentBritish")} +

    + + +
    +

    {t("exercise_page.buttons.instructionBtn")}

    +
    + {instructions && Array.isArray(instructions) && instructions.length > 0 ? ( + instructions.map((instruction, index) => ( +

    + {instruction} +

    + )) + ) : ( +

    + [Instructions for this type of exercise is not yet translated. Please + update accordingly.] +

    + )}
    - - - - - - - - +
    +
    + +
    +
    +
    +
    + + + +
    + + +
    + {instructions && Array.isArray(instructions) && instructions.length > 0 ? ( + instructions.map((instruction, index) => ( +

    + {instruction} +

    + )) + ) : ( +

    + [Instructions for this type of exercise is not yet translated. Please update + accordingly.] +

    + )} +
    +
    + + +
    + +
    +
    {timeIsUp || quizCompleted || onMatchFinished ? ( <> - - {t("exercise_page.result.cardHeading")} - - +
    +
    + {t("exercise_page.result.cardHeading")} +
    +
    {onMatchFinished ?

    {t("exercise_page.result.matchUpFinished")}

    : ""} {timeIsUp && !onMatchFinished ? (

    {t("exercise_page.result.timeUp")}

    @@ -394,24 +410,30 @@ const ExerciseDetailPage = ({ heading, id, title, accent, file, onBack }) => {

    {t("exercise_page.result.answerBottom")}

    ) : ( -

    {t("exercise_page.buttons.answerBottom")}

    +

    {t("exercise_page.result.answerBottom")}

    )} - - +
    + +
    +
    ) : ( <>{renderQuizComponent()} )} - +
    {timeIsUp || quizCompleted || currentExerciseType == "memory_match" ? ( "" ) : ( - - Review - +
    +
    +
    + {t("sound_page.reviewCard")} +
    +
    {score === 0 && totalAnswered === 0 ? ( "" ) : ( @@ -425,11 +447,11 @@ const ExerciseDetailPage = ({ heading, id, title, accent, file, onBack }) => { ) : (

    {t("exercise_page.result.tryAgainBottom")}

    )} - - +
    +
    )} - - +
    +
    )} diff --git a/src/components/exercise_page/ExercisePage.jsx b/src/components/exercise_page/ExercisePage.jsx index db6bda6fd..f4444a02f 100644 --- a/src/components/exercise_page/ExercisePage.jsx +++ b/src/components/exercise_page/ExercisePage.jsx @@ -1,7 +1,7 @@ import { useEffect, useState } from "react"; -import { Button, Card, Col, OverlayTrigger, Row, Tooltip } from "react-bootstrap"; -import { InfoCircle } from "react-bootstrap-icons"; +import { IoInformationCircleOutline } from "react-icons/io5"; import { useTranslation } from "react-i18next"; +import Container from "../../ui/Container"; import AccentLocalStorage from "../../utils/AccentLocalStorage"; import { isElectron } from "../../utils/isElectron"; import AccentDropdown from "../general/AccentDropdown"; @@ -17,12 +17,6 @@ const ExercisePage = () => { const [selectedAccent, setSelectedAccent] = AccentLocalStorage(); const [selectedExercise, setSelectedExercise] = useState(null); - const TooltipIcon = ({ info }) => ( - {info}} trigger={["hover", "focus"]}> - - - ); - const selectedAccentOptions = [ { name: "American English", value: "american" }, { name: "British English", value: "british" }, @@ -54,31 +48,62 @@ const ExercisePage = () => { return exercise.infoKey ? t(exercise.infoKey) : t(defaultInfoKey); }; + const TooltipIcon = ({ info, modalId }) => { + return ( + <> + {/* Tooltip for larger screens */} +
    + +
    + + {/* Modal for small screens */} + + +
    +

    {t("exercise_page.modalInfoHeader")}

    +

    {info}

    +
    +
    + +
    +
    +
    +
    + + ); + }; + const ExerciseCard = ({ heading, titles, infoKey, file }) => ( - - - {t(heading)} - - {titles - .filter(({ american, british }) => { - if (selectedAccent === "american" && american === false) return false; - if (selectedAccent === "british" && british === false) return false; - return true; - }) - .map((exercise, index) => ( - - - - - ))} - - - + + +
    + ); + })} +
    +
    ); useEffect(() => { @@ -136,38 +161,40 @@ const ExercisePage = () => { return ( <> -

    {t("navigation.exercises")}

    - {selectedExercise ? ( - - ) : ( - <> - -

    {t("exercise_page.exerciseSubheading")}

    - {loading ? ( - - ) : ( - - {data.map((section, index) => ( - - ))} - - )} - - )} + +

    {t("navigation.exercises")}

    + {selectedExercise ? ( + + ) : ( + <> + +

    {t("exercise_page.exerciseSubheading")}

    + {loading ? ( + + ) : ( +
    + {data.map((section, index) => ( + + ))} +
    + )} + + )} +
    ); }; From 98f5792608771aad75b3cfbd38a07c6be0cba93b Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Thu, 12 Dec 2024 17:41:06 +0700 Subject: [PATCH 14/95] Fix some components to make it more WAI-ARIA compliant --- src/components/Homepage.jsx | 1 + src/components/general/AccentDropdown.jsx | 1 + src/components/general/TopNavBar.jsx | 4 ++-- src/components/setting_page/Appearance.jsx | 3 +++ src/components/sound_page/PracticeSound.jsx | 7 +++++-- src/components/sound_page/SoundVideoModal.jsx | 2 +- src/ui/Container.jsx | 4 ++-- 7 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/components/Homepage.jsx b/src/components/Homepage.jsx index 0d4ac571f..5af676145 100644 --- a/src/components/Homepage.jsx +++ b/src/components/Homepage.jsx @@ -81,6 +81,7 @@ function Homepage() {
@@ -199,6 +199,7 @@ const PracticeSound = ({ sound, accent, onBack, index, soundsData }) => {
  • +
diff --git a/src/components/sound_page/SoundVideoModal.jsx b/src/components/sound_page/SoundVideoModal.jsx index 538b960c0..aa929f487 100644 --- a/src/components/sound_page/SoundVideoModal.jsx +++ b/src/components/sound_page/SoundVideoModal.jsx @@ -59,7 +59,7 @@ const SoundVideoModal = ({ videoUrls, videoUrl, videoLoading }) => { )}
- +
diff --git a/src/ui/Container.jsx b/src/ui/Container.jsx index 1884f031e..e3a8b3bbe 100644 --- a/src/ui/Container.jsx +++ b/src/ui/Container.jsx @@ -2,9 +2,9 @@ import PropTypes from "prop-types"; const Container = ({ children, className = "", ...props }) => { return ( -
+
{children} -
+ ); }; From 7c550b4d34062c0c343bb3084e8c7ff1936708ba Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Thu, 12 Dec 2024 17:41:13 +0700 Subject: [PATCH 15/95] Update en.json --- public/locales/en.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/locales/en.json b/public/locales/en.json index f43920bbf..8346533c1 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -124,7 +124,8 @@ "backBtn": "Back to exercise list", "instructionBtn": "Instructions", "collapseBtn": "Collapse instructions", - "expandBtn": "View instructions" + "expandBtn": "See instructions", + "playAudioBtn": "Play audio" }, "exerciseInfo": { "dictationWord": "Test your listening and spelling with word dictations", From fc1199b5688e00c3973e253dc40ab5ea3c531a51 Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Thu, 12 Dec 2024 22:49:32 +0700 Subject: [PATCH 16/95] Update ExerciseDetailPage.jsx --- .../exercise_page/ExerciseDetailPage.jsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/exercise_page/ExerciseDetailPage.jsx b/src/components/exercise_page/ExerciseDetailPage.jsx index 52c796007..0eb5ce164 100644 --- a/src/components/exercise_page/ExerciseDetailPage.jsx +++ b/src/components/exercise_page/ExerciseDetailPage.jsx @@ -339,13 +339,13 @@ const ExerciseDetailPage = ({ heading, id, title, accent, file, onBack }) => {
- +
-
@@ -374,7 +374,7 @@ const ExerciseDetailPage = ({ heading, id, title, accent, file, onBack }) => {
-
@@ -384,7 +384,7 @@ const ExerciseDetailPage = ({ heading, id, title, accent, file, onBack }) => { {timeIsUp || quizCompleted || onMatchFinished ? ( <>
-
+
{t("exercise_page.result.cardHeading")}
@@ -413,7 +413,7 @@ const ExerciseDetailPage = ({ heading, id, title, accent, file, onBack }) => {

{t("exercise_page.result.answerBottom")}

)}
- @@ -430,7 +430,7 @@ const ExerciseDetailPage = ({ heading, id, title, accent, file, onBack }) => { ) : (
-
+
{t("sound_page.reviewCard")}
From 45803df6452a5c3eedcf0e4396c0683784aa216e Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Thu, 12 Dec 2024 22:49:34 +0700 Subject: [PATCH 17/95] Update DictationQuiz.jsx --- .../exercise_page/DictationQuiz.jsx | 33 +++++++++---------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/src/components/exercise_page/DictationQuiz.jsx b/src/components/exercise_page/DictationQuiz.jsx index 24b8b139f..3a90ba327 100644 --- a/src/components/exercise_page/DictationQuiz.jsx +++ b/src/components/exercise_page/DictationQuiz.jsx @@ -71,15 +71,7 @@ const DictationQuiz = ({ quiz, timer, onAnswer, onQuit, setTimeIsUp }) => { setHasAnswered(true); setShowValidation(true); setValidationVariant(isCorrect ? "success" : "danger"); - setValidationMessage( - isCorrect ? ( - "" - ) : ( - <> - {t("exercise_page.result.correctAnswer")} {correctAnswer} - - ) - ); + setValidationMessage(isCorrect ? "" : correctAnswer); onAnswer(isCorrect, "single"); }; @@ -218,11 +210,11 @@ const DictationQuiz = ({ quiz, timer, onAnswer, onQuit, setTimeIsUp }) => { return ( <> -
-
+
+
{timer > 0 ? (
-
+
{t("exercise_page.questionNo")} #{currentQuestionIndex + 1}
@@ -238,6 +230,8 @@ const DictationQuiz = ({ quiz, timer, onAnswer, onQuit, setTimeIsUp }) => {
{currentQuestionIndex < quiz.length - 1 && ( - )} -
From bee750439ba79fedbe15fda983f5607d1db1e112 Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Thu, 12 Dec 2024 22:49:36 +0700 Subject: [PATCH 18/95] Update ExercisePage.jsx --- src/components/exercise_page/ExercisePage.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/exercise_page/ExercisePage.jsx b/src/components/exercise_page/ExercisePage.jsx index f4444a02f..fc611bacc 100644 --- a/src/components/exercise_page/ExercisePage.jsx +++ b/src/components/exercise_page/ExercisePage.jsx @@ -57,7 +57,7 @@ const ExercisePage = () => {
{/* Modal for small screens */} - +
From 1cf0eb60129d98a39a26141c90e8890a7b8a8e65 Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Thu, 12 Dec 2024 22:49:39 +0700 Subject: [PATCH 19/95] Update MatchUp.jsx --- src/components/exercise_page/MatchUp.jsx | 128 +++++++++++++---------- 1 file changed, 72 insertions(+), 56 deletions(-) diff --git a/src/components/exercise_page/MatchUp.jsx b/src/components/exercise_page/MatchUp.jsx index d8c9053e8..443913ed7 100644 --- a/src/components/exercise_page/MatchUp.jsx +++ b/src/components/exercise_page/MatchUp.jsx @@ -16,15 +16,15 @@ import { } from "@dnd-kit/sortable"; import _ from "lodash"; import { useCallback, useEffect, useRef, useState } from "react"; -import { Button, Card, Col, Row, Spinner } from "react-bootstrap"; -import { ArrowRightCircle, Check2Circle, VolumeUp, VolumeUpFill, XCircle } from "react-bootstrap-icons"; import { useTranslation } from "react-i18next"; +import { IoVolumeHigh, IoVolumeHighOutline } from "react-icons/io5"; +import { LiaCheckCircle, LiaChevronCircleRightSolid, LiaTimesCircle } from "react-icons/lia"; import { ShuffleArray } from "../../utils/ShuffleArray"; import useCountdownTimer from "../../utils/useCountdownTimer"; import SortableWord from "./SortableWord"; const MatchUp = ({ quiz, timer, onAnswer, onQuit, setTimeIsUp }) => { - const [currentQuizIndex, setCurrentQuizIndex] = useState(0); + const [currentQuestionIndex, setcurrentQuestionIndex] = useState(0); const [shuffledQuiz, setShuffledQuiz] = useState([]); const [shuffledWords, setShuffledWords] = useState([]); const [audioItems, setAudioItems] = useState([]); @@ -72,16 +72,16 @@ const MatchUp = ({ quiz, timer, onAnswer, onQuit, setTimeIsUp }) => { useEffect(() => { if (quiz?.length > 0) { setShuffledQuiz(filterAndShuffleQuiz(quiz)); - setCurrentQuizIndex(0); + setcurrentQuestionIndex(0); } }, [quiz, filterAndShuffleQuiz]); useEffect(() => { - if (shuffledQuiz.length > 0 && currentQuizIndex < shuffledQuiz.length) { - loadQuiz(shuffledQuiz[currentQuizIndex]); - setIsLoading(new Array(shuffledQuiz[currentQuizIndex].audio.length).fill(false)); + if (shuffledQuiz.length > 0 && currentQuestionIndex < shuffledQuiz.length) { + loadQuiz(shuffledQuiz[currentQuestionIndex]); + setIsLoading(new Array(shuffledQuiz[currentQuestionIndex].audio.length).fill(false)); } - }, [shuffledQuiz, currentQuizIndex, loadQuiz]); + }, [shuffledQuiz, currentQuestionIndex, loadQuiz]); useEffect(() => { // Initialize the audioRef @@ -219,8 +219,8 @@ const MatchUp = ({ quiz, timer, onAnswer, onQuit, setTimeIsUp }) => { // Reset audio state and element stopAudio(); - if (currentQuizIndex < shuffledQuiz.length - 1) { - setCurrentQuizIndex(currentQuizIndex + 1); + if (currentQuestionIndex < shuffledQuiz.length - 1) { + setcurrentQuestionIndex(currentQuestionIndex + 1); } else { onQuit(); stopAudio(); @@ -236,48 +236,58 @@ const MatchUp = ({ quiz, timer, onAnswer, onQuit, setTimeIsUp }) => { return ( <> - -
-
- {t("exercise_page.questionNo")} #{currentQuizIndex + 1} -
- {timer > 0 && ( -
- {t("exercise_page.timer")} {formatTime()} +
+
+ {timer > 0 ? ( +
+
+ {t("exercise_page.questionNo")} #{currentQuestionIndex + 1} +
+
+ {t("exercise_page.timer")} {formatTime()} +
+ ) : ( +

+ {t("exercise_page.questionNo")} #{currentQuestionIndex + 1} +

)}
- - - - -
+
+
+ {/* Audio Buttons Column */} +
+
{audioItems.map((audio, index) => ( -
- +
))}
- - - -
+
+ +
+ {/* Sortable Words Column */} + +
word.id)} strategy={verticalListSortingStrategy}> @@ -303,24 +313,30 @@ const MatchUp = ({ quiz, timer, onAnswer, onQuit, setTimeIsUp }) => { ) : null}
- -
- - -
- - {currentQuizIndex < shuffledQuiz.length - 1 && ( - - )} - + +
+
+ +
+
+ + {currentQuestionIndex < shuffledQuiz.length - 1 && ( + + )} + +
- +
); }; From 71ecf4d61853e0f569a50dabba13ce374b610aa2 Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Thu, 12 Dec 2024 22:49:41 +0700 Subject: [PATCH 20/95] Update Reordering.jsx --- src/components/exercise_page/Reordering.jsx | 187 +++++++++++--------- 1 file changed, 106 insertions(+), 81 deletions(-) diff --git a/src/components/exercise_page/Reordering.jsx b/src/components/exercise_page/Reordering.jsx index 05f883776..68d3aba6e 100644 --- a/src/components/exercise_page/Reordering.jsx +++ b/src/components/exercise_page/Reordering.jsx @@ -11,15 +11,15 @@ import { SortableContext, arrayMove, horizontalListSortingStrategy } from "@dnd- import he from "he"; import _ from "lodash"; import { useCallback, useEffect, useRef, useState } from "react"; -import { Alert, Button, Card, Col, Row, Spinner, Stack } from "react-bootstrap"; -import { ArrowRightCircle, Check2Circle, VolumeUp, VolumeUpFill, XCircle } from "react-bootstrap-icons"; import { useTranslation } from "react-i18next"; +import { IoInformationCircleOutline, IoVolumeHigh, IoVolumeHighOutline } from "react-icons/io5"; +import { LiaCheckCircle, LiaChevronCircleRightSolid, LiaTimesCircle } from "react-icons/lia"; import { ShuffleArray } from "../../utils/ShuffleArray"; import useCountdownTimer from "../../utils/useCountdownTimer"; import SortableWord from "./SortableWord"; const Reordering = ({ quiz, onAnswer, onQuit, timer, setTimeIsUp }) => { - const [currentQuizIndex, setCurrentQuizIndex] = useState(0); + const [currentQuestionIndex, setcurrentQuestionIndex] = useState(0); const [shuffledItems, setShuffledItems] = useState([]); const [activeId, setActiveId] = useState(null); const [isPlaying, setIsPlaying] = useState(false); @@ -100,16 +100,16 @@ const Reordering = ({ quiz, onAnswer, onQuit, timer, setTimeIsUp }) => { // Filter out unique items and shuffle the quiz array const uniqueShuffledQuiz = filterAndShuffleQuiz(quiz); setShuffledQuizArray(uniqueShuffledQuiz); - // Reset currentQuizIndex to 0 - setCurrentQuizIndex(0); + // Reset currentQuestionIndex to 0 + setcurrentQuestionIndex(0); } }, [quiz]); useEffect(() => { - if (shuffledQuizArray.length > 0 && currentQuizIndex < shuffledQuizArray.length) { - loadQuiz(shuffledQuizArray[currentQuizIndex]); + if (shuffledQuizArray.length > 0 && currentQuestionIndex < shuffledQuizArray.length) { + loadQuiz(shuffledQuizArray[currentQuestionIndex]); } - }, [shuffledQuizArray, currentQuizIndex, loadQuiz]); + }, [shuffledQuizArray, currentQuestionIndex, loadQuiz]); useEffect(() => { return () => { @@ -233,10 +233,10 @@ const Reordering = ({ quiz, onAnswer, onQuit, timer, setTimeIsUp }) => { stopAudio(); - if (currentQuizIndex < shuffledQuizArray.length - 1) { - setCurrentQuizIndex(currentQuizIndex + 1); + if (currentQuestionIndex < shuffledQuizArray.length - 1) { + setcurrentQuestionIndex(currentQuestionIndex + 1); setShowAlert(false); - loadQuiz(shuffledQuizArray[currentQuizIndex + 1]); + loadQuiz(shuffledQuizArray[currentQuestionIndex + 1]); } else { onQuit(); stopAudio(); @@ -252,81 +252,106 @@ const Reordering = ({ quiz, onAnswer, onQuit, timer, setTimeIsUp }) => { return ( <> - -
-
{t("exercise_page.questionNo")} #{currentQuizIndex + 1}
- {timer > 0 &&
{t("exercise_page.timer")} {formatTime()}
} -
-
- - - - - - -
- - - {shuffledItems.map((item) => ( - - ))} - - - {activeId ? ( - item.id === activeId)?.value || "" - ), - id: activeId, - }} - isOverlay={true} - /> - ) : null} - - +
+
+ {timer > 0 ? ( +
+
+ {t("exercise_page.questionNo")} #{currentQuestionIndex + 1} +
+
+ {t("exercise_page.timer")} {formatTime()} +
- - + ) : ( +

+ {t("exercise_page.questionNo")} #{currentQuestionIndex + 1} +

+ )} +
+
+ +
+ +
+ +
+ + + {shuffledItems.map((item) => ( + + ))} + + + {activeId ? ( + item.id === activeId)?.value || "" + ), + id: activeId, + }} + isOverlay={true} + /> + ) : null} + + +
+ {showAlert && ( - - {t("exercise_page.result.correctAnswer")} {correctAnswer} - +
+
+ +
+

{t("exercise_page.result.correctAnswer")}

+

{correctAnswer}

+
+
+
)} -
- - - {currentQuizIndex < quiz.length - 1 && ( - +
+
+ + {currentQuestionIndex < quiz.length - 1 && ( + )} - - + +
- +
); }; From 0221a4908359b52188439521d0fc708940b258df Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Thu, 12 Dec 2024 22:49:43 +0700 Subject: [PATCH 21/95] Update SortableWord.jsx --- src/components/exercise_page/SortableWord.jsx | 25 +++++++------------ 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/src/components/exercise_page/SortableWord.jsx b/src/components/exercise_page/SortableWord.jsx index e07b1c619..e03178662 100644 --- a/src/components/exercise_page/SortableWord.jsx +++ b/src/components/exercise_page/SortableWord.jsx @@ -2,8 +2,7 @@ import { useSortable } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import he from "he"; import React, { useEffect, useState } from "react"; -import { Button } from "react-bootstrap"; -import { CheckCircleFill, XCircleFill } from "react-bootstrap-icons"; +import { BsCheckCircleFill, BsXCircleFill } from "react-icons/bs"; const SortableWord = ({ word, item, isCorrect, disabled, isOverlay }) => { const [itemWidth, setItemWidth] = useState(null); @@ -31,26 +30,21 @@ const SortableWord = ({ word, item, isCorrect, disabled, isOverlay }) => { opacity: isDragging ? 0.5 : 1, }; - const variant = isOverlay - ? "secondary" - : isCorrect === null - ? "outline-secondary" - : isCorrect - ? "success" - : "danger"; + const btnVariant = isOverlay ? "" : isCorrect === null ? "outline" : isCorrect ? "success" : "error"; const trueFalse = isOverlay ? ( "" ) : isCorrect === null ? ( "" ) : isCorrect ? ( - + ) : ( - + ); return ( - + ); }; From d6ae721df8078d9eb4134e4d83218a90c7c8a804 Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Thu, 12 Dec 2024 22:49:46 +0700 Subject: [PATCH 22/95] Update SoundAndSpelling.jsx --- .../exercise_page/SoundAndSpelling.jsx | 161 ++++++++++-------- 1 file changed, 88 insertions(+), 73 deletions(-) diff --git a/src/components/exercise_page/SoundAndSpelling.jsx b/src/components/exercise_page/SoundAndSpelling.jsx index 5f45f96bc..cc0a8e492 100644 --- a/src/components/exercise_page/SoundAndSpelling.jsx +++ b/src/components/exercise_page/SoundAndSpelling.jsx @@ -1,14 +1,15 @@ import he from "he"; import _ from "lodash"; import { useCallback, useEffect, useRef, useState } from "react"; -import { Button, Card, Col, Row, Spinner, Stack } from "react-bootstrap"; -import { ArrowRightCircle, CheckCircleFill, VolumeUp, VolumeUpFill, XCircle, XCircleFill } from "react-bootstrap-icons"; import { useTranslation } from "react-i18next"; +import { BsCheckCircleFill, BsXCircleFill } from "react-icons/bs"; +import { IoVolumeHigh, IoVolumeHighOutline } from "react-icons/io5"; +import { LiaChevronCircleRightSolid, LiaTimesCircle } from "react-icons/lia"; import { ShuffleArray } from "../../utils/ShuffleArray"; import useCountdownTimer from "../../utils/useCountdownTimer"; const SoundAndSpelling = ({ quiz, onAnswer, onQuit, timer, setTimeIsUp }) => { - const [currentQuizIndex, setCurrentQuizIndex] = useState(0); + const [currentQuestionIndex, setcurrentQuestionIndex] = useState(0); const [shuffledQuiz, setShuffledQuiz] = useState([]); const [shuffledOptions, setShuffledOptions] = useState([]); const [isPlaying, setIsPlaying] = useState(false); @@ -48,8 +49,8 @@ const SoundAndSpelling = ({ quiz, onAnswer, onQuit, timer, setTimeIsUp }) => { // Filter out unique items and shuffle the quiz array const uniqueShuffledQuiz = filterAndShuffleQuiz(quiz); setShuffledQuiz(uniqueShuffledQuiz); - // Reset currentQuizIndex to 0 - setCurrentQuizIndex(0); + // Reset currentQuestionIndex to 0 + setcurrentQuestionIndex(0); } }, [quiz]); @@ -75,10 +76,10 @@ const SoundAndSpelling = ({ quiz, onAnswer, onQuit, timer, setTimeIsUp }) => { }, []); useEffect(() => { - if (shuffledQuiz.length > 0 && currentQuizIndex < shuffledQuiz.length) { - loadQuiz(shuffledQuiz[currentQuizIndex]); + if (shuffledQuiz.length > 0 && currentQuestionIndex < shuffledQuiz.length) { + loadQuiz(shuffledQuiz[currentQuestionIndex]); } - }, [shuffledQuiz, currentQuizIndex, loadQuiz]); + }, [shuffledQuiz, currentQuestionIndex, loadQuiz]); const handleAudioPlay = () => { if (isPlaying) { @@ -131,8 +132,8 @@ const SoundAndSpelling = ({ quiz, onAnswer, onQuit, timer, setTimeIsUp }) => { const handleNextQuiz = () => { stopAudio(); - if (currentQuizIndex < shuffledQuiz.length - 1) { - setCurrentQuizIndex((prevIndex) => prevIndex + 1); + if (currentQuestionIndex < shuffledQuiz.length - 1) { + setcurrentQuestionIndex((prevIndex) => prevIndex + 1); } else { onQuit(); stopAudio(); @@ -148,78 +149,92 @@ const SoundAndSpelling = ({ quiz, onAnswer, onQuit, timer, setTimeIsUp }) => { return ( <> - -
-
- {t("exercise_page.questionNo")} #{currentQuizIndex + 1} -
- {timer > 0 && ( -
- {t("exercise_page.timer")} {formatTime()} +
+
+ {timer > 0 ? ( +
+
+ {t("exercise_page.questionNo")} #{currentQuestionIndex + 1} +
+
+ {t("exercise_page.timer")} {formatTime()} +
+ ) : ( +

+ {t("exercise_page.questionNo")} #{currentQuestionIndex + 1} +

)}
- - - - - - - - +
+ +
+ {he.decode(currentQuestionText)} + {selectedOption ? ( + selectedOption.isCorrect ? ( + ) : ( - "" - )} - - - - - {shuffledOptions.map((option, index) => ( - - ))} - - - -
- {currentQuizIndex < shuffledQuiz.length - 1 && ( - - )} - + + ) + ) : ( + "" + )} +
+ +
+ {shuffledOptions.map((option, index) => ( + + ))} +
+
+
+
+ {currentQuestionIndex < shuffledQuiz.length - 1 && ( + + )} + +
- +
); }; From fba8233c9d67ca3d34137e5174906441ad4288da Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Fri, 13 Dec 2024 12:44:59 +0700 Subject: [PATCH 23/95] Update packages --- package-lock.json | 24 ++++++++++++------------ package.json | 6 +++--- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/package-lock.json b/package-lock.json index bd7de6896..defca49d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "electron-squirrel-startup": "^1.0.1", "express": "^4.21.2", "he": "^1.2.0", - "i18next": "^24.0.5", + "i18next": "^24.1.0", "i18next-browser-languagedetector": "^8.0.2", "i18next-http-backend": "^3.0.1", "lodash": "^4.17.21", @@ -30,7 +30,7 @@ "react-bootstrap-icons": "^1.11.5", "react-dom": "^19.0.0", "react-flip-toolkit": "^7.2.4", - "react-i18next": "^15.1.4", + "react-i18next": "^15.2.0", "react-icons": "^5.4.0", "react-loading-skeleton": "^3.5.0", "react-router-dom": "^7.0.2", @@ -54,7 +54,7 @@ "autoprefixer": "^10.4.20", "concurrently": "^9.1.0", "cross-env": "^7.0.3", - "daisyui": "^4.12.20", + "daisyui": "^4.12.22", "electron": "^33.2.1", "eslint": "^9.16.0", "eslint-plugin-react": "^7.37.2", @@ -4745,9 +4745,9 @@ } }, "node_modules/daisyui": { - "version": "4.12.20", - "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-4.12.20.tgz", - "integrity": "sha512-uHr3SQsd2yTjRdVuswTiqGFvZTxX0sGSBRa8JJdbKgmZCk/kRFh4B7Z2jg9vLIdwsHTHPyPgCkZadQo1ce0tAw==", + "version": "4.12.22", + "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-4.12.22.tgz", + "integrity": "sha512-HDLWbmTnXxhE1MrMgSWjVgdRt+bVYHvfNbW3GTsyIokRSqTHonUTrxV3RhpPDjGIWaHt+ELtDCTYCtUFgL2/Nw==", "dev": true, "dependencies": { "css-selector-tokenizer": "^0.8", @@ -7258,9 +7258,9 @@ } }, "node_modules/i18next": { - "version": "24.0.5", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-24.0.5.tgz", - "integrity": "sha512-1jSdEzgFPGLZRsQwydoMFCBBaV+PmrVEO5WhANllZPX4y2JSGTxUjJ+xVklHIsiS95uR8gYc/y0hYZWevucNjg==", + "version": "24.1.0", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-24.1.0.tgz", + "integrity": "sha512-suKlX82AlptkMUO5YRfaAeH4FQyyKvR66jNaubTMiyPPMx7INU6PXAiy3PGULc0q6K+t9nxmDf/TRj9KjAivmw==", "funding": [ { "type": "individual", @@ -10139,9 +10139,9 @@ } }, "node_modules/react-i18next": { - "version": "15.1.4", - "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.1.4.tgz", - "integrity": "sha512-2tai71gmehbvl9ZIqPMqlCCkm/cbeV1G4STpmM3C8Uzo6T2l8jDvZxEVSsQKt8blP9X34iRFP/k1ROqG2296MQ==", + "version": "15.2.0", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.2.0.tgz", + "integrity": "sha512-iJNc8111EaDtVTVMKigvBtPHyrJV+KblWG73cUxqp+WmJCcwkzhWNFXmkAD5pwP2Z4woeDj/oXDdbjDsb3Gutg==", "dependencies": { "@babel/runtime": "^7.25.0", "html-parse-stringify": "^3.0.1" diff --git a/package.json b/package.json index be9460570..4c5c66213 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "electron-squirrel-startup": "^1.0.1", "express": "^4.21.2", "he": "^1.2.0", - "i18next": "^24.0.5", + "i18next": "^24.1.0", "i18next-browser-languagedetector": "^8.0.2", "i18next-http-backend": "^3.0.1", "lodash": "^4.17.21", @@ -42,7 +42,7 @@ "react-bootstrap-icons": "^1.11.5", "react-dom": "^19.0.0", "react-flip-toolkit": "^7.2.4", - "react-i18next": "^15.1.4", + "react-i18next": "^15.2.0", "react-icons": "^5.4.0", "react-loading-skeleton": "^3.5.0", "react-router-dom": "^7.0.2", @@ -66,7 +66,7 @@ "autoprefixer": "^10.4.20", "concurrently": "^9.1.0", "cross-env": "^7.0.3", - "daisyui": "^4.12.20", + "daisyui": "^4.12.22", "electron": "^33.2.1", "eslint": "^9.16.0", "eslint-plugin-react": "^7.37.2", From 9be67719feb7843da8c07e72a935bce8c37cd361 Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Fri, 13 Dec 2024 12:53:02 +0700 Subject: [PATCH 24/95] Fix missing toast component to pass the build --- .../ConversationDetailPage.jsx | 20 +----------- .../conversation_page/ListeningTab.jsx | 16 ++-------- .../conversation_page/PracticeTab.jsx | 32 ++++++++----------- src/components/exam_page/ExamDetailPage.jsx | 12 ------- src/components/exam_page/ListeningTab.jsx | 16 ++-------- src/components/exam_page/PracticeTab.jsx | 30 +++++++---------- 6 files changed, 32 insertions(+), 94 deletions(-) diff --git a/src/components/conversation_page/ConversationDetailPage.jsx b/src/components/conversation_page/ConversationDetailPage.jsx index 8941e8755..9405078ae 100644 --- a/src/components/conversation_page/ConversationDetailPage.jsx +++ b/src/components/conversation_page/ConversationDetailPage.jsx @@ -4,7 +4,6 @@ import { ArrowLeftCircle, CameraVideo, CardChecklist, ChatDots, Headphones } fro import { useTranslation } from "react-i18next"; import { isElectron } from "../../utils/isElectron"; import LoadingOverlay from "../general/LoadingOverlay"; -import ToastNotification from "../general/ToastNotification"; import { getFileFromIndexedDB, saveFileToIndexedDB } from "../setting_page/offlineStorageDb"; import ListeningTab from "./ListeningTab"; import PracticeTab from "./PracticeTab"; @@ -18,9 +17,6 @@ const ConversationDetailPage = ({ id, accent, title, onBack }) => { const [loading, setLoading] = useState(true); const [accentData, setAccentData] = useState(null); - const [showToast, setShowToast] = useState(false); - const [toastMessage, setToastMessage] = useState(""); - const [videoUrl, setVideoUrl] = useState(null); const [videoLoading, setVideoLoading] = useState(true); @@ -164,14 +160,7 @@ const ConversationDetailPage = ({ id, accent, title, onBack }) => { {activeTab === "#listen" && } - {activeTab === "#practice" && ( - - )} + {activeTab === "#practice" && } {activeTab === "#review" && ( @@ -179,13 +168,6 @@ const ConversationDetailPage = ({ id, accent, title, onBack }) => { )} - - setShowToast(false)} - message={toastMessage} - variant="warning" - /> ); }; diff --git a/src/components/conversation_page/ListeningTab.jsx b/src/components/conversation_page/ListeningTab.jsx index 4c93714d9..4f2c89904 100644 --- a/src/components/conversation_page/ListeningTab.jsx +++ b/src/components/conversation_page/ListeningTab.jsx @@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from "react"; import { Card, Col, ListGroup, Row, Spinner } from "react-bootstrap"; import { VolumeUp, VolumeUpFill } from "react-bootstrap-icons"; import { useTranslation } from "react-i18next"; -import ToastNotification from "../general/ToastNotification"; +import { sonnerErrorToast } from "../../utils/sonnerCustomToast"; const ListeningTab = ({ sentences }) => { const { t } = useTranslation(); @@ -11,9 +11,6 @@ const ListeningTab = ({ sentences }) => { const [playingIndex, setPlayingIndex] = useState(null); const [loadingIndex, setLoadingIndex] = useState(null); - const [showToast, setShowToast] = useState(false); - const [toastMessage, setToastMessage] = useState(""); - const handlePlayPause = (index, audioSrc) => { if (loadingIndex === index) { // Cancel the loading process if clicked again @@ -73,7 +70,7 @@ const ListeningTab = ({ sentences }) => { setCurrentAudio(null); setPlayingIndex(null); console.log("Audio loading error"); - alert(t("toast.audioPlayFailed")); + sonnerErrorToast(t("toast.audioPlayFailed")); }; setCurrentAudio(audio); @@ -83,8 +80,7 @@ const ListeningTab = ({ sentences }) => { console.log("Audio loading aborted"); } else { console.error("Error loading audio:", error); - setToastMessage(t("toast.audioPlayFailed")); - setShowToast(true); + sonnerErrorToast(t("toast.audioPlayFailed")); } setLoadingIndex(null); setCurrentAudio(null); @@ -153,12 +149,6 @@ const ListeningTab = ({ sentences }) => { ))} - setShowToast(false)} - message={toastMessage} - variant="warning" - /> ); }; diff --git a/src/components/conversation_page/PracticeTab.jsx b/src/components/conversation_page/PracticeTab.jsx index ebb574d98..1bab7c2a4 100644 --- a/src/components/conversation_page/PracticeTab.jsx +++ b/src/components/conversation_page/PracticeTab.jsx @@ -4,8 +4,9 @@ import { Floppy, PlayCircle, RecordCircle, StopCircle, Trash } from "react-boots import { useTranslation } from "react-i18next"; import { checkRecordingExists, openDatabase, playRecording, saveRecording } from "../../utils/databaseOperations"; import { isElectron } from "../../utils/isElectron"; +import { sonnerErrorToast, sonnerSuccessToast, sonnerWarningToast } from "../../utils/sonnerCustomToast"; -const PracticeTab = ({ accent, conversationId, setToastMessage, setShowToast }) => { +const PracticeTab = ({ accent, conversationId }) => { const { t } = useTranslation(); const [textValue, setTextValue] = useState(""); @@ -70,13 +71,11 @@ const PracticeTab = ({ accent, conversationId, setToastMessage, setShowToast }) const request = store.put({ id: textKey, text: textValue }); request.onsuccess = () => { - setToastMessage(t("toast.textSaveSuccess")); - setShowToast(true); + sonnerSuccessToast(t("toast.textSaveSuccess")); }; request.onerror = (error) => { isElectron() && window.electron.log("error", `Error saving text: ${error}`); - setToastMessage(t("toast.textSaveFailed") + error.message); - setShowToast(true); + sonnerErrorToast(t("toast.textSaveFailed") + error.message); }; } catch (error) { console.error("Error saving text: ", error); @@ -94,19 +93,16 @@ const PracticeTab = ({ accent, conversationId, setToastMessage, setShowToast }) request.onsuccess = () => { setTextValue(""); - setToastMessage(t("toast.textClearSuccess")); - setShowToast(true); + sonnerSuccessToast(t("toast.textClearSuccess")); }; request.onerror = (error) => { - setToastMessage(t("toast.textClearFailed") + error.message); + sonnerErrorToast(t("toast.textClearFailed") + error.message); isElectron() && window.electron.log("error", `Error clearing text: ${error}`); - setShowToast(true); }; } catch (error) { console.error("Error clearing text: ", error); isElectron() && window.electron.log("error", `Error clearing text: ${error}`); - setToastMessage(t("toast.textClearFailed") + error.message); - setShowToast(true); + sonnerErrorToast(t("toast.textClearFailed") + error.message); } }; @@ -131,9 +127,9 @@ const PracticeTab = ({ accent, conversationId, setToastMessage, setShowToast }) if (mediaRecorder.state === "inactive") { const audioBlob = new Blob(audioChunks, { type: event.data.type }); saveRecording(audioBlob, recordingKey, event.data.type); - setToastMessage(t("toast.recordingSuccess")); + sonnerSuccessToast(t("toast.recordingSuccess")); isElectron() && window.electron.log("log", `Recording saved: ${recordingKey}`); - setShowToast(true); + setRecordingExists(true); audioChunks = []; } @@ -143,16 +139,14 @@ const PracticeTab = ({ accent, conversationId, setToastMessage, setShowToast }) setTimeout(() => { if (mediaRecorder.state !== "inactive") { mediaRecorder.stop(); - setToastMessage(t("toast.recordingExceeded")); - setShowToast(true); + sonnerWarningToast(t("toast.recordingExceeded")); setIsRecording(false); } }, 15 * 60 * 1000); }) .catch((error) => { - setToastMessage(t("toast.recordingFailed") + error.message); + sonnerErrorToast(t("toast.recordingFailed") + error.message); isElectron() && window.electron.log("error", `Recording failed: ${error}`); - setShowToast(true); }); } else { // Stop recording @@ -186,9 +180,9 @@ const PracticeTab = ({ accent, conversationId, setToastMessage, setShowToast }) } }, (error) => { - setToastMessage(t("toast.playbackError") + error.message); + sonnerErrorToast(t("toast.playbackError") + error.message); isElectron() && window.electron.log("error", `Error saving text: ${error}`); - setShowToast(true); + setIsRecordingPlaying(false); }, () => { diff --git a/src/components/exam_page/ExamDetailPage.jsx b/src/components/exam_page/ExamDetailPage.jsx index 630a8d22d..ef47ad26b 100644 --- a/src/components/exam_page/ExamDetailPage.jsx +++ b/src/components/exam_page/ExamDetailPage.jsx @@ -4,7 +4,6 @@ import { ArrowLeftCircle, CameraVideo, CardChecklist, ChatDots, Headphones, Info import { useTranslation } from "react-i18next"; import { isElectron } from "../../utils/isElectron"; import LoadingOverlay from "../general/LoadingOverlay"; -import ToastNotification from "../general/ToastNotification"; import { getFileFromIndexedDB, saveFileToIndexedDB } from "../setting_page/offlineStorageDb"; import ListeningTab from "./ListeningTab"; import PracticeTab from "./PracticeTab"; @@ -18,9 +17,6 @@ const ExamDetailPage = ({ id, title, onBack, accent }) => { const [examData, setExamData] = useState(null); const [loading, setLoading] = useState(true); - const [showToast, setShowToast] = useState(false); - const [toastMessage, setToastMessage] = useState(""); - const [videoUrl, setVideoUrl] = useState(null); const [videoLoading, setVideoLoading] = useState(true); @@ -196,19 +192,11 @@ const ExamDetailPage = ({ id, title, onBack, accent }) => { accent={accent} taskData={examDetails.practise.task} tips={examDetails.practise.tips} - setToastMessage={setToastMessage} - setShowToast={setShowToast} /> )} {activeTab === "#review" && } - setShowToast(false)} - message={toastMessage} - variant="warning" - /> ); }; diff --git a/src/components/exam_page/ListeningTab.jsx b/src/components/exam_page/ListeningTab.jsx index dc1ac5ca5..62932a7ea 100644 --- a/src/components/exam_page/ListeningTab.jsx +++ b/src/components/exam_page/ListeningTab.jsx @@ -3,7 +3,7 @@ import { useEffect, useRef, useState } from "react"; import { Card, Col, ListGroup, Row, Spinner } from "react-bootstrap"; import { VolumeUp, VolumeUpFill } from "react-bootstrap-icons"; import { useTranslation } from "react-i18next"; -import ToastNotification from "../general/ToastNotification"; +import { sonnerErrorToast } from "../../utils/sonnerCustomToast"; const ListeningTab = ({ subtopicsBre, subtopicsAme, currentAccent }) => { const { t } = useTranslation(); @@ -12,9 +12,6 @@ const ListeningTab = ({ subtopicsBre, subtopicsAme, currentAccent }) => { const [currentAudio, setCurrentAudio] = useState(null); const [loadingIndex, setLoadingIndex] = useState(null); - const [showToast, setShowToast] = useState(false); - const [toastMessage, setToastMessage] = useState(""); - const subtopics = currentAccent === "american" ? subtopicsAme : subtopicsBre; const handlePlayPause = (index, audioSrc) => { @@ -76,7 +73,7 @@ const ListeningTab = ({ subtopicsBre, subtopicsAme, currentAccent }) => { setCurrentAudio(null); setPlayingIndex(null); console.log("Audio loading error"); - alert(t("toast.audioPlayFailed")); + sonnerErrorToast(t("toast.audioPlayFailed")); }; setCurrentAudio(audio); @@ -86,8 +83,7 @@ const ListeningTab = ({ subtopicsBre, subtopicsAme, currentAccent }) => { console.log("Audio loading aborted"); } else { console.error("Error loading audio:", error); - setToastMessage(t("toast.audioPlayFailed")); - setShowToast(true); + sonnerErrorToast(t("toast.audioPlayFailed")); } setLoadingIndex(null); setCurrentAudio(null); @@ -165,12 +161,6 @@ const ListeningTab = ({ subtopicsBre, subtopicsAme, currentAccent }) => { ))} - setShowToast(false)} - message={toastMessage} - variant="warning" - /> ); }; diff --git a/src/components/exam_page/PracticeTab.jsx b/src/components/exam_page/PracticeTab.jsx index 5de0b316a..c5a4973bd 100644 --- a/src/components/exam_page/PracticeTab.jsx +++ b/src/components/exam_page/PracticeTab.jsx @@ -4,8 +4,9 @@ import { Floppy, PlayCircle, RecordCircle, StopCircle, Trash } from "react-boots import { useTranslation } from "react-i18next"; import { checkRecordingExists, openDatabase, playRecording, saveRecording } from "../../utils/databaseOperations"; import { isElectron } from "../../utils/isElectron"; +import { sonnerErrorToast, sonnerSuccessToast, sonnerWarningToast } from "../../utils/sonnerCustomToast"; -const PracticeTab = ({ accent, examId, taskData, tips, setToastMessage, setShowToast }) => { +const PracticeTab = ({ accent, examId, taskData, tips }) => { const { t } = useTranslation(); const [textValues, setTextValues] = useState(() => taskData.map(() => "")); @@ -77,14 +78,12 @@ const PracticeTab = ({ accent, examId, taskData, tips, setToastMessage, setShowT const request = store.put({ id: textKey, text: textValues[index] }); request.onsuccess = () => { - setToastMessage(t("toast.textSaveSuccess")); - setShowToast(true); + sonnerSuccessToast(t("toast.textSaveSuccess")); }; request.onerror = (error) => { console.error("Error saving text: ", error); isElectron() && window.electron.log("error", `Error saving text: ${error}`); - setToastMessage(t("toast.textSaveFailed") + error.message); - setShowToast(true); + sonnerErrorToast(t("toast.textSaveFailed") + error.message); }; } catch (error) { console.error("Error saving text: ", error); @@ -108,20 +107,17 @@ const PracticeTab = ({ accent, examId, taskData, tips, setToastMessage, setShowT return updated; }); console.log("Text cleared successfully."); - setToastMessage(t("toast.textClearSuccess")); - setShowToast(true); + sonnerSuccessToast(t("toast.textClearSuccess")); }; request.onerror = (error) => { console.error("Error clearing text: ", error); isElectron() && window.electron.log("error", `Error clearing text: ${error}`); - setToastMessage(t("toast.textClearFailed") + error.message); - setShowToast(true); + sonnerErrorToast(t("toast.textClearFailed") + error.message); }; } catch (error) { console.error("Error clearing text: ", error); isElectron() && window.electron.log("error", `Error clearing text: ${error}`); - setToastMessage(t("toast.textClearFailed") + error.message); - setShowToast(true); + sonnerErrorToast(t("toast.textClearFailed") + error.message); } }; @@ -146,9 +142,9 @@ const PracticeTab = ({ accent, examId, taskData, tips, setToastMessage, setShowT const audioBlob = new Blob(audioChunks, { type: event.data.type }); const recordingKey = `${accent}-exam-${examId}-${index}`; saveRecording(audioBlob, recordingKey, event.data.type); - setToastMessage(t("toast.recordingSuccess")); + sonnerSuccessToast(t("toast.recordingSuccess")); isElectron() && window.electron.log("log", `Recording saved: ${recordingKey}`); - setShowToast(true); + setRecordingExists((prev) => { const updatedExists = [...prev]; updatedExists[index] = true; @@ -163,15 +159,13 @@ const PracticeTab = ({ accent, examId, taskData, tips, setToastMessage, setShowT mediaRecorder.stop(); setIsRecording(false); setActiveTaskIndex(null); - setToastMessage(t("toast.recordingExceeded")); - setShowToast(true); + sonnerWarningToast(t("toast.recordingExceeded")); } }, 15 * 60 * 1000); // 15 minutes limit }) .catch((error) => { - setToastMessage(t("toast.recordingFailed") + error.message); + sonnerErrorToast(t("toast.recordingFailed") + error.message); isElectron() && window.electron.log("error", `Recording failed: ${error}`); - setShowToast(true); }); } else { mediaRecorder.stop(); @@ -207,7 +201,7 @@ const PracticeTab = ({ accent, examId, taskData, tips, setToastMessage, setShowT } }, (error) => { - setToastMessage(t("toast.playbackError") + error.message); + sonnerErrorToast(t("toast.playbackError") + error.message); isElectron() && window.electron.log("error", `Error during playback: ${error}`); setIsRecordingPlaying(false); setActiveTaskIndex(null); From 0e4c34eae30edffad276e24c9ddfbc43cca36289 Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Fri, 13 Dec 2024 13:01:05 +0700 Subject: [PATCH 25/95] test fix base url for netlify --- src/App.jsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/App.jsx b/src/App.jsx index 240c7a709..027431778 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -15,7 +15,18 @@ const ExercisePage = lazy(() => import("./components/exercise_page/ExercisePage" const SettingsPage = lazy(() => import("./components/setting_page/Settings")); const RouterComponent = isElectron() ? HashRouter : BrowserRouter; -const baseUrl = import.meta.env.BASE_URL; +// Ensure baseUrl does not add unnecessary slashes +const baseUrl = isElectron() + ? "" + : (() => { + switch (import.meta.env.BASE_URL) { + case "/": + case "./": + return ""; // Use no basename for "/" or "./" + default: + return import.meta.env.BASE_URL; + } + })(); const AppContent = () => { const { theme } = useTheme(); From e36440f7d64d3a7c0632e01f2cda3a0a74e22740 Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Fri, 13 Dec 2024 13:18:08 +0700 Subject: [PATCH 26/95] Adjust dynamic class names --- src/components/exercise_page/SortableWord.jsx | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/components/exercise_page/SortableWord.jsx b/src/components/exercise_page/SortableWord.jsx index e03178662..98a8ac78f 100644 --- a/src/components/exercise_page/SortableWord.jsx +++ b/src/components/exercise_page/SortableWord.jsx @@ -30,17 +30,12 @@ const SortableWord = ({ word, item, isCorrect, disabled, isOverlay }) => { opacity: isDragging ? 0.5 : 1, }; - const btnVariant = isOverlay ? "" : isCorrect === null ? "outline" : isCorrect ? "success" : "error"; + const btnVariant = isOverlay ? "" : isCorrect === null ? "btn-outline" : isCorrect ? "btn-success" : "btn-error"; - const trueFalse = isOverlay ? ( - "" - ) : isCorrect === null ? ( - "" - ) : isCorrect ? ( - - ) : ( - - ); + const renderTrueFalseIcon = () => { + if (isOverlay || isCorrect === null) return null; + return isCorrect ? : ; + }; return ( ); }; From 461f1bc371d04fdffa9cee01261a46758ca3d044 Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Fri, 13 Dec 2024 16:51:04 +0700 Subject: [PATCH 27/95] Optimize Sound section code --- src/components/sound_page/PracticeSound.jsx | 10 +- src/components/sound_page/SoundList.jsx | 254 ++++++------------- src/components/sound_page/WatchVideoCard.jsx | 4 +- 3 files changed, 91 insertions(+), 177 deletions(-) diff --git a/src/components/sound_page/PracticeSound.jsx b/src/components/sound_page/PracticeSound.jsx index ea05c9bba..d2d3a0b20 100644 --- a/src/components/sound_page/PracticeSound.jsx +++ b/src/components/sound_page/PracticeSound.jsx @@ -13,7 +13,7 @@ import { useRecordingFunction } from "./hooks/useRecordingFunction"; import { useSoundVideoMapping } from "./hooks/useSoundVideoMapping"; import ReviewCard from "./ReviewCard"; import SoundPracticeCard from "./SoundPracticeCard"; -import { WatchVideoCard } from "./WatchVideoCard"; +import WatchVideoCard from "./WatchVideoCard"; const PracticeSound = ({ sound, accent, onBack, index, soundsData }) => { const { t } = useTranslation(); @@ -63,6 +63,10 @@ const PracticeSound = ({ sound, accent, onBack, index, soundsData }) => { document.getElementById("sound_video_modal").showModal(); }; + const handleClose = () => { + document.getElementById("sound_video_modal").close(); + }; + // iframe loading const [iframeLoadingStates, setIframeLoadingStates] = useState({ mainIframe: true, @@ -323,7 +327,9 @@ const PracticeSound = ({ sound, accent, onBack, index, soundsData }) => { )}
- +
diff --git a/src/components/sound_page/SoundList.jsx b/src/components/sound_page/SoundList.jsx index bc29e18e4..e2cc831b5 100644 --- a/src/components/sound_page/SoundList.jsx +++ b/src/components/sound_page/SoundList.jsx @@ -1,5 +1,5 @@ import he from "he"; -import { Suspense, lazy, useEffect, useState } from "react"; +import { Suspense, lazy, useEffect, useState, useMemo } from "react"; import { useTranslation } from "react-i18next"; import Container from "../../ui/Container"; import AccentLocalStorage from "../../utils/AccentLocalStorage"; @@ -11,6 +11,31 @@ import { getFileFromIndexedDB, saveFileToIndexedDB } from "../setting_page/offli const PracticeSound = lazy(() => import("./PracticeSound")); +const SoundCard = ({ sound, index, accent, handlePracticeClick, getBadgeColor, getReviewText, t }) => { + const badgeColor = getBadgeColor(sound, index); + const reviewText = badgeColor ? getReviewText(reviews[getReviewKey(sound, index)]) : null; + + return ( +
+ {badgeColor && {reviewText}} +
+
+

{he.decode(sound.phoneme)}

+

{sound.example_word}

+
+
+ +
+
+
+ ); +}; + const SoundList = () => { const { t } = useTranslation(); const [selectedSound, setSelectedSound] = useState(null); @@ -21,31 +46,18 @@ const SoundList = () => { const [soundsData, setSoundsData] = useState({ consonants: [], vowels_n_diphthongs: [], - consonants_b: [], - consonants_a: [], - vowels_b: [], - vowels_a: [], }); - const handlePracticeClick = (sound, accent, index) => { - setSelectedSound({ sound, accent, index }); - }; - - // Trigger re-render when the review option is updated const [reviews, setReviews] = useState({}); - const [reviewsUpdateTrigger, setReviewsUpdateTrigger] = useState(0); useEffect(() => { const ispeakerData = JSON.parse(localStorage.getItem("ispeaker") || "{}"); - const accentReviews = ispeakerData.soundReview ? ispeakerData.soundReview[selectedAccent] || {} : {}; + const accentReviews = ispeakerData.soundReview?.[selectedAccent] || {}; setReviews(accentReviews); }, [selectedAccent, reviewsUpdateTrigger]); - // Method to trigger re-render - const triggerReviewsUpdate = () => { - setReviewsUpdateTrigger(Date.now()); - }; + const triggerReviewsUpdate = () => setReviewsUpdateTrigger((prev) => prev + 1); const handleGoBack = () => { setSelectedSound(null); @@ -54,76 +66,50 @@ const SoundList = () => { const getReviewKey = (sound, index) => { const type = soundsData.consonants.some((s) => s.phoneme === sound.phoneme) ? "consonant" : "vowel"; - const formattedIndex = index + 1; - return `${type}${formattedIndex}`; + return `${type}${index + 1}`; }; const getBadgeColor = (sound, index) => { - const ispeakerData = JSON.parse(localStorage.getItem("ispeaker") || "{}"); - const accentReviews = ispeakerData.soundReview ? ispeakerData.soundReview[selectedAccent] || {} : {}; const reviewKey = getReviewKey(sound, index); - const review = accentReviews[reviewKey]; - - const badgeColors = { - good: "badge-success", - neutral: "badge-warning", - bad: "badge-error", - }; - - return badgeColors[review] || null; // Return the Tailwind class or null + const review = reviews[reviewKey]; + return ( + { + good: "badge-success", + neutral: "badge-warning", + bad: "badge-error", + }[review] || null + ); }; - const getReviewText = (review) => { - switch (review) { - case "good": - return t("sound_page.reviewGood"); - case "neutral": - return t("sound_page.reviewNeutral"); - case "bad": - return t("sound_page.reviewBad"); - default: - return ""; - } - }; + const getReviewText = (review) => t(`sound_page.review${review?.charAt(0).toUpperCase() + review?.slice(1)}`); useEffect(() => { const fetchData = async () => { try { setLoading(true); - - // If it's not an Electron environment, check IndexedDB first - if (!isElectron()) { - const cachedDataBlob = await getFileFromIndexedDB("sounds_data.json", "json"); - - if (cachedDataBlob) { - // Convert Blob to text, then parse the JSON - const cachedDataText = await cachedDataBlob.text(); - const cachedData = JSON.parse(cachedDataText); - - setSoundsData(cachedData); - setLoading(false); - - return; + const cachedDataBlob = !isElectron() && (await getFileFromIndexedDB("sounds_data.json", "json")); + + if (cachedDataBlob) { + const cachedData = JSON.parse(await cachedDataBlob.text()); + setSoundsData(cachedData); + } else { + const response = await fetch(`${import.meta.env.BASE_URL}json/sounds_data.json`); + const data = await response.json(); + setSoundsData(data); + + if (!isElectron()) { + const blob = new Blob([JSON.stringify(data)], { type: "application/json" }); + await saveFileToIndexedDB("sounds_data.json", blob, "json"); } } - - // If not in IndexedDB or running in Electron, fetch from the network - const response = await fetch(`${import.meta.env.BASE_URL}json/sounds_data.json`); - const data = await response.json(); - - setSoundsData(data); - setLoading(false); - - // Save the fetched data to IndexedDB (excluding Electron) - if (!isElectron()) { - const blob = new Blob([JSON.stringify(data)], { type: "application/json" }); - await saveFileToIndexedDB("sounds_data.json", blob, "json"); - } } catch (error) { console.error("Error fetching data:", error); - alert("Error while loading the data for this section. Please check your Internet connection."); + alert(t("sound_page.loadError")); + } finally { + setLoading(false); } }; + fetchData(); }, []); @@ -131,6 +117,15 @@ const SoundList = () => { document.title = `${t("navigation.sounds")} | iSpeakerReact v${__APP_VERSION__}`; }, [t]); + const filteredSounds = useMemo(() => { + const currentTabData = activeTab === "tab1" ? soundsData.consonants : soundsData.vowels_n_diphthongs; + return currentTabData.filter( + (sound) => + (selectedAccent === "british" && sound.b_s === "yes") || + (selectedAccent === "american" && sound.a_s === "yes") + ); + }, [activeTab, selectedAccent, soundsData]); + return ( <> @@ -139,11 +134,11 @@ const SoundList = () => { {selectedSound ? ( }> handleGoBack()} + onBack={handleGoBack} /> ) : ( @@ -156,7 +151,6 @@ const SoundList = () => { <>
- {/* Menu */}
- {/* Tab Content */} -
- {activeTab === "tab1" && ( - <> -
- {soundsData.consonants - .filter( - (sound) => - (selectedAccent === "british" && sound.b_s === "yes") || - (selectedAccent === "american" && sound.a_s === "yes") - ) - .map((sound, index) => ( -
- {getBadgeColor(sound, index) && ( - - {getReviewText( - reviews[`${getReviewKey(sound, index)}`] - )} - - )} -
-
-

- {he.decode(sound.phoneme)}{" "} -

-

{sound.example_word}

-
-
- -
-
-
- ))} -
- - )} - {activeTab === "tab2" && ( - <> -
- {soundsData.vowels_n_diphthongs - .filter( - (sound) => - (selectedAccent === "british" && sound.b_s === "yes") || - (selectedAccent === "american" && sound.a_s === "yes") - ) - .map((sound, index) => ( -
- {getBadgeColor(sound, index) && ( - - {getReviewText( - reviews[`${getReviewKey(sound, index)}`] - )} - - )} -
-
-

- {he.decode(sound.phoneme)} -

-

{sound.example_word}

-
-
- -
-
-
- ))} -
- - )} +
+ {filteredSounds.map((sound, index) => ( + + ))}
)} diff --git a/src/components/sound_page/WatchVideoCard.jsx b/src/components/sound_page/WatchVideoCard.jsx index 3a232ea32..45be58bf3 100644 --- a/src/components/sound_page/WatchVideoCard.jsx +++ b/src/components/sound_page/WatchVideoCard.jsx @@ -1,7 +1,7 @@ import { IoInformationCircleOutline } from "react-icons/io5"; import { isElectron } from "../../utils/isElectron"; -export const WatchVideoCard = ({ t, videoUrl, iframeLoadingStates, handleIframeLoad }) => { +const WatchVideoCard = ({ t, videoUrl, iframeLoadingStates, handleIframeLoad }) => { return (
@@ -46,3 +46,5 @@ export const WatchVideoCard = ({ t, videoUrl, iframeLoadingStates, handleIframeL
); }; + +export default WatchVideoCard \ No newline at end of file From 18f7d0defec23195c83bf528f817d0c2c9cab5ed Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Fri, 13 Dec 2024 16:55:17 +0700 Subject: [PATCH 28/95] Fix modal not closing --- .../exercise_page/ExerciseDetailPage.jsx | 19 +++++++++++++------ src/components/exercise_page/ExercisePage.jsx | 12 ++++++++++-- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/components/exercise_page/ExerciseDetailPage.jsx b/src/components/exercise_page/ExerciseDetailPage.jsx index 0eb5ce164..69262ab05 100644 --- a/src/components/exercise_page/ExerciseDetailPage.jsx +++ b/src/components/exercise_page/ExerciseDetailPage.jsx @@ -339,13 +339,19 @@ const ExerciseDetailPage = ({ heading, id, title, accent, file, onBack }) => {
- +
- @@ -430,9 +439,7 @@ const ExerciseDetailPage = ({ heading, id, title, accent, file, onBack }) => { ) : (
-
- {t("sound_page.reviewCard")} -
+
{t("sound_page.reviewCard")}
{score === 0 && totalAnswered === 0 ? ( "" diff --git a/src/components/exercise_page/ExercisePage.jsx b/src/components/exercise_page/ExercisePage.jsx index fc611bacc..9d6ea0b7b 100644 --- a/src/components/exercise_page/ExercisePage.jsx +++ b/src/components/exercise_page/ExercisePage.jsx @@ -57,7 +57,10 @@ const ExercisePage = () => {
{/* Modal for small screens */} - +
From ea6b42bca3f60e28732491c123757fba736ac75f Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Fri, 13 Dec 2024 18:23:40 +0700 Subject: [PATCH 29/95] Fix grid performance on iOS and fix badge not showing --- src/components/sound_page/SoundList.jsx | 57 +++++++++++++++++++------ 1 file changed, 44 insertions(+), 13 deletions(-) diff --git a/src/components/sound_page/SoundList.jsx b/src/components/sound_page/SoundList.jsx index e2cc831b5..600ca4835 100644 --- a/src/components/sound_page/SoundList.jsx +++ b/src/components/sound_page/SoundList.jsx @@ -11,9 +11,20 @@ import { getFileFromIndexedDB, saveFileToIndexedDB } from "../setting_page/offli const PracticeSound = lazy(() => import("./PracticeSound")); -const SoundCard = ({ sound, index, accent, handlePracticeClick, getBadgeColor, getReviewText, t }) => { +const SoundCard = ({ + sound, + index, + selectedAccent, + handlePracticeClick, + getBadgeColor, + getReviewText, + getReviewKey, + reviews, + t, +}) => { const badgeColor = getBadgeColor(sound, index); - const reviewText = badgeColor ? getReviewText(reviews[getReviewKey(sound, index)]) : null; + const reviewKey = getReviewKey(sound, index); + const reviewText = badgeColor ? getReviewText(reviews[reviewKey]) : null; return (
@@ -26,7 +37,7 @@ const SoundCard = ({ sound, index, accent, handlePracticeClick, getBadgeColor, g
@@ -52,13 +63,20 @@ const SoundList = () => { const [reviewsUpdateTrigger, setReviewsUpdateTrigger] = useState(0); useEffect(() => { - const ispeakerData = JSON.parse(localStorage.getItem("ispeaker") || "{}"); - const accentReviews = ispeakerData.soundReview?.[selectedAccent] || {}; - setReviews(accentReviews); + const fetchReviews = () => { + const ispeakerData = JSON.parse(localStorage.getItem("ispeaker") || "{}"); + const accentReviews = ispeakerData.soundReview?.[selectedAccent] || {}; + setReviews(accentReviews); + }; + fetchReviews(); }, [selectedAccent, reviewsUpdateTrigger]); const triggerReviewsUpdate = () => setReviewsUpdateTrigger((prev) => prev + 1); + const handlePracticeClick = (sound, accent, index) => { + setSelectedSound({ sound, accent, index }); + }; + const handleGoBack = () => { setSelectedSound(null); triggerReviewsUpdate(); @@ -81,7 +99,18 @@ const SoundList = () => { ); }; - const getReviewText = (review) => t(`sound_page.review${review?.charAt(0).toUpperCase() + review?.slice(1)}`); + const getReviewText = (review) => { + switch (review) { + case "good": + return t("sound_page.reviewGood"); + case "neutral": + return t("sound_page.reviewNeutral"); + case "bad": + return t("sound_page.reviewBad"); + default: + return ""; + } + }; useEffect(() => { const fetchData = async () => { @@ -111,7 +140,7 @@ const SoundList = () => { }; fetchData(); - }, []); + }, [t]); useEffect(() => { document.title = `${t("navigation.sounds")} | iSpeakerReact v${__APP_VERSION__}`; @@ -134,8 +163,8 @@ const SoundList = () => { {selectedSound ? ( }> {
-
+
{filteredSounds.map((sound, index) => ( ))} From 1e01eb9ae7269ef35eb5d85083a623e2de3bbf83 Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Fri, 13 Dec 2024 19:43:01 +0700 Subject: [PATCH 30/95] Update TopNavBar.jsx --- src/components/general/TopNavBar.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/general/TopNavBar.jsx b/src/components/general/TopNavBar.jsx index 21b8945d5..f609c8c6a 100644 --- a/src/components/general/TopNavBar.jsx +++ b/src/components/general/TopNavBar.jsx @@ -10,7 +10,7 @@ import { useTheme } from "../../utils/ThemeContext/useTheme"; const TopNavBar = () => { const { t } = useTranslation(); const { theme } = useTheme(); - const [currentTheme, setCurrentTheme] = useState(theme); + const [, setCurrentTheme] = useState(theme); const [isDarkMode, setIsDarkMode] = useState(false); useEffect(() => { @@ -42,7 +42,7 @@ const TopNavBar = () => { ? `${import.meta.env.BASE_URL}images/logos/ispeakerreact-no-background-darkmode.svg` : `${import.meta.env.BASE_URL}images/logos/ispeakerreact-no-background.svg`; - const navbarClass = isDarkMode ? "bg-slate-600/50" : "bg-lime-300/50"; + const navbarClass = isDarkMode ? "bg-slate-600 md:bg-slate-600/50" : "bg-lime-300 md:bg-lime-300/50"; const menuItems = [ { to: "/", icon: , label: t("navigation.home") }, @@ -55,7 +55,7 @@ const TopNavBar = () => { return (