Compare commits
74 Commits
v2026.2.0-
...
v2026.2.7-
| Author | SHA1 | Date | |
|---|---|---|---|
| 1d8efa4e61 | |||
| 69fce52bed | |||
| 13adf908bf | |||
| 3bbfb8740f | |||
| 0d04ec91e7 | |||
| 20a969705d | |||
| 4944d2aa21 | |||
| 2ec37a802b | |||
| 6a5ba84b5e | |||
| 6be0d52fb7 | |||
| f493f94368 | |||
| cd93a26c2c | |||
| 175581cdd5 | |||
| 5f856e4d70 | |||
| e0495d182e | |||
| 0b7c1406cf | |||
| 30edc5782b | |||
| d92a7f3aa0 | |||
| f72cd45afd | |||
| 51bd909879 | |||
| db37ab45b2 | |||
| 9256db7f8c | |||
| d9059acb78 | |||
| 5da8a04c18 | |||
| ba8a0befb0 | |||
| 5ac291de81 | |||
| 991b114a3c | |||
| c79584c7d2 | |||
| e77c8a8f7c | |||
| c2308ba408 | |||
| e5a22cdfe3 | |||
| 09b0d21621 | |||
| 9aef27a0eb | |||
| c00ed57240 | |||
| ef4cf6ef69 | |||
| dc9c5f6545 | |||
| 25f2aaab8c | |||
| 0cc9cdaf07 | |||
| 247f437445 | |||
| 0e94367223 | |||
| a9ee28b395 | |||
| bd074c5c9d | |||
| 42c552c528 | |||
| 3b376e5386 | |||
| 45a8e6b4a1 | |||
| 1f8e9c3c56 | |||
| a511b86db8 | |||
| 1c0ad054bb | |||
| 5a8799bb7f | |||
| 6c443d8e86 | |||
| 8795fedda9 | |||
| 588fb57299 | |||
| eb345e17ca | |||
| c2693c4648 | |||
| 43efc16562 | |||
| 80d6440ece | |||
| 5ee972f003 | |||
| 6f3edb41ea | |||
| c52939a7a3 | |||
| 573d409606 | |||
| 9a58bc9a5e | |||
| 8780800dff | |||
| f442942faf | |||
| a61d881a4e | |||
| 926b614136 | |||
| c0c84f4651 | |||
| 176e255037 | |||
| b134358e9e | |||
| 3525aaeeb7 | |||
| af67ec3931 | |||
| d515f42cfd | |||
| 5d6aff8d90 | |||
| 15ba26ccf2 | |||
| d3fa0cbbf9 |
@@ -6,6 +6,12 @@
|
|||||||
APP_ENV=dev
|
APP_ENV=dev
|
||||||
APP_SECRET=changethis
|
APP_SECRET=changethis
|
||||||
APP_NAME=mineseeker
|
APP_NAME=mineseeker
|
||||||
|
# APP_PUBLIC_HOSTNAME: The public hostname for your application (used for generating absolute URLs in emails)
|
||||||
|
# For production, set this to your domain (e.g., mineseeker.com)
|
||||||
|
APP_PUBLIC_HOSTNAME=localhost
|
||||||
|
# TRUSTED_PROXIES: Only needed for bare-metal dev behind a reverse proxy
|
||||||
|
# For Docker development, this is set in compose.override.yaml
|
||||||
|
# For production, set in PROD_ENV_FILE Gitea secret (use 172.18.0.0/16 initially)
|
||||||
#TRUSTED_PROXIES=127.0.0.1,127.0.0.2
|
#TRUSTED_PROXIES=127.0.0.1,127.0.0.2
|
||||||
#TRUSTED_HOSTS=localhost,example.com
|
#TRUSTED_HOSTS=localhost,example.com
|
||||||
###< symfony/framework-bundle ###
|
###< symfony/framework-bundle ###
|
||||||
|
|||||||
32
.gitchangelog.rc
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
ignore_regexps = [
|
||||||
|
r'@minor', r'!minor',
|
||||||
|
r'@cosmetic', r'!cosmetic',
|
||||||
|
r'@refactor', r'!refactor',
|
||||||
|
r'@wip', r'!wip',
|
||||||
|
r'^(.{3,3}\s*:)?\s*[fF]irst commit.?\s*$',
|
||||||
|
r'^$', ## ignore commits with empty messages
|
||||||
|
r'@skipChangelog', r'!skipChangelog', r'skipChangeLog', r'!skipChangeLog',
|
||||||
|
r'Merge branch', r'Merge remote-tracking branch', r'!deploy',
|
||||||
|
]
|
||||||
|
section_regexps = [
|
||||||
|
('New', [
|
||||||
|
r'^[nN]ew\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$',
|
||||||
|
]),
|
||||||
|
('Changes', [
|
||||||
|
r'^[cC]hg\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$',
|
||||||
|
]),
|
||||||
|
('Fix', [
|
||||||
|
r'^[fF]ix\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$',
|
||||||
|
]),
|
||||||
|
('Other', None ## Match all lines
|
||||||
|
),
|
||||||
|
]
|
||||||
|
body_process = ReSub(r'((^|\n)[A-Z]\w+(-\w+)*: .*(\n\s+.*)*)+$', r'') | strip
|
||||||
|
subject_process = (strip |
|
||||||
|
ReSub(r'^([cC]hg|[fF]ix|[nN]ew)\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n@]*)(@[a-z]+\s+)*$', r'\4') |
|
||||||
|
SetIfEmpty("No commit message.") | ucfirst | final_dot)
|
||||||
|
tag_filter_regexp = r'^(v)?[0-9]+\.[0-9]+(\.[0-9]+)?(\-[0-9]+)?$'
|
||||||
|
unreleased_version_label = "(unreleased)"
|
||||||
|
output_engine = mustache("markdown")
|
||||||
|
include_merge = True
|
||||||
|
revs = []
|
||||||
658
CHANGELOG.md
@@ -1,177 +1,505 @@
|
|||||||
Changelog
|
# Changelog
|
||||||
=========
|
|
||||||
|
|
||||||
|
|
||||||
v2026.01 (2026-04-14)
|
## v2026.2.7-0 (2026-04-21)
|
||||||
---------------------
|
|
||||||
|
|
||||||
New
|
### Changes
|
||||||
~~~
|
|
||||||
- Registered users have avatars next to the timer #4. [Lang]
|
|
||||||
- Add opportunity to use profile picture. #4. [Lang]
|
|
||||||
- Add more stats and a dialog for the recent battle that can be
|
|
||||||
shareable #4. [Lang]
|
|
||||||
- Implement the 2FA authentication (TOTP and backup codes) #4. [Lang]
|
|
||||||
- Add beta logo to the corner #3. [Lang]
|
|
||||||
- Add mineseeker game to the symfony 4 project #3. [Lang]
|
|
||||||
- Upgrade to the latest symfony v4 #3. [Lang]
|
|
||||||
|
|
||||||
Changes
|
* Small changes on docs - and improve text on homepage #8. [Lang]
|
||||||
~~~~~~~
|
|
||||||
- Fix missing favicon #4. [Lang]
|
|
||||||
- Add modern Webauthn authentication #4. [Lang]
|
|
||||||
- Refactor all forms to have Symfony Form Types & Validation
|
|
||||||
Constrainsts - & implement Google ReCapthca v3 #4. [Lang]
|
|
||||||
- Add forgot password functionality #4. [Lang]
|
|
||||||
- Increase the minimum PHP version to the latest major - and massive
|
|
||||||
refactor on back-end, like Controllers and Repositories #4. [Lang]
|
|
||||||
- Redesign the resign dialog #4. [Lang]
|
|
||||||
- Re-implement the waiting for opponent dialog - refactor its gfx - &
|
|
||||||
add online user selection dialog #4. [Lang]
|
|
||||||
- Improve the gfx on homepage - implement login/register and activation
|
|
||||||
for authentication - and add the first version of profile page #4.
|
|
||||||
[Lang]
|
|
||||||
- Refactor and redesign the gfx on front-end #4. [Lang]
|
|
||||||
- Add timers to each player - renew the whole migration #4. [Lang]
|
|
||||||
- Use namespaces for front-end #4. [Lang]
|
|
||||||
- Replace webpack w/ vite & remove old, legacy jQuery from the code #4.
|
|
||||||
[Lang]
|
|
||||||
- More, massive refactor for front-end #4. [Lang]
|
|
||||||
- Massive refactor on front-end - and remove unnecessary deps #4. [Lang]
|
|
||||||
- Change the code style to fit the current standard #4. [Lang]
|
|
||||||
- Refactor to use Attributes instead of yaml markdown #4. [Lang]
|
|
||||||
- Outsource the Grid generation and interactions to the backend #4.
|
|
||||||
[Lang]
|
|
||||||
- Remove unnecessary variables and prune the Facebook registration
|
|
||||||
method #4. [Lang]
|
|
||||||
- Replace the legacy gos/web-socket-bundle & replace it with Mercure
|
|
||||||
protocol #4. [Lang]
|
|
||||||
- Created the first working solution since 7 yrs #4. [Lang]
|
|
||||||
- Make the first working version - the stepping is broken due to the
|
|
||||||
algorythm structure #4. [Lang]
|
|
||||||
- Change the composer default php minimum environment #3. [Lang]
|
|
||||||
- Change the default url to wss on frontend #3. [Lang]
|
|
||||||
- Refactor Rpc and Topic classes #3. [Lang]
|
|
||||||
- Refactor classes and reformat some layout #3. [Lang]
|
|
||||||
- Remove deprecated files #3. [Lang]
|
|
||||||
- Doc in README.md #3. [Lang]
|
|
||||||
- Gitignore a js.map file #2. [Lang]
|
|
||||||
|
|
||||||
Other
|
* Massive refactor on front-end for unification and readiness #8. [Lang]
|
||||||
~~~~~
|
|
||||||
- Hg: pkg: new version release !skipChangelog. [Lang]
|
* Update all doc blocks on back-end #8. [Lang]
|
||||||
- Pkg: usr: solve the not-working mailing on dev env under docker #4.
|
|
||||||
[Lang]
|
* Small refactors on back-end #8. [Lang]
|
||||||
- Deploy version 1.1.0 !deploy #11. [Lang]
|
|
||||||
|
* Add RecentBattle entity that is a Materialized View to speed up the view - and further refactor on ProfileController #8. [Lang]
|
||||||
|
|
||||||
|
* Create the UserStats entity what is a Materialized View to store Profile stats for every user - & massive ProfileController refactor #8. [Lang]
|
||||||
|
|
||||||
|
* Refactor the SecurityController #7. [Lang]
|
||||||
|
|
||||||
|
* Refactor the code - there was unnecessary codes and wrongly formatted or designed code that are related to Repositories #7. [Lang]
|
||||||
|
|
||||||
|
* Upgrade the doctrine related back-end pkgs to the latest available version #7. [Lang]
|
||||||
|
|
||||||
|
* Add filter to the Profile page's recent plays and an infite list too #7. [Lang]
|
||||||
|
|
||||||
|
* Upgrade to the latest doctrine pkg on back-end #7. [Lang]
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
* Do not hide the end-game overlay ever #8. [Lang]
|
||||||
|
|
||||||
|
* The username was not recognized properly #7. [Lang]
|
||||||
|
|
||||||
|
|
||||||
1.1.0 (2019-10-26)
|
## v2026.2.6-1 (2026-04-19)
|
||||||
------------------
|
|
||||||
|
|
||||||
Changes
|
### Fix
|
||||||
~~~~~~~
|
|
||||||
- Reinit project - disable redis module and make the project compatible
|
* The PostgreSQL logo was horrible #7. [Lang]
|
||||||
w/ PHP7.3 #2. [Lang]
|
|
||||||
|
|
||||||
|
|
||||||
0.4.0 (2019-10-26)
|
## v2026.2.6-0 (2026-04-19)
|
||||||
------------------
|
|
||||||
- Change session driver to REDIS. [Lang]
|
### Changes
|
||||||
- Add created, updated field to db && improve graph design. [Lang]
|
|
||||||
- Cache setup && optimalize for google pagespeed && optimalize all
|
* Add ReCaptcha overlay again to protect the game #7. [Lang]
|
||||||
images. [Lang]
|
|
||||||
- Improve graph design on homepage && add footer and techs && add
|
* Upgrade all front-end packages to the latest available version - and fix all eslint warnings & errors #7. [Lang]
|
||||||
official pages. [Lang]
|
|
||||||
- Bugfix mine websocket periodic mysql calling. [Lang]
|
* Upgrade fe packages #7. [Lang]
|
||||||
- Bugfix hwioauth remember me && centralize hwioauth and facebook
|
|
||||||
settings. [Lang]
|
* Massive refactor on fetches - create centralized dataProvider #7. [Lang]
|
||||||
- Centralize jquery && bugfix mysql auto-termination problem w/ user
|
|
||||||
auth. [Lang]
|
|
||||||
- Release beta4. [Lang]
|
## v2026.2.5-0 (2026-04-19)
|
||||||
- Gitignore npm debug log. [Lang]
|
|
||||||
- Add english lang everywhere && add snowfall && add centralized version
|
### New
|
||||||
nbr && improve stylesheet && slack integration. [Lang]
|
|
||||||
- Bugfix #30 && random bg in game. [Lang]
|
* Add Firebase deps to back-end #7. [Lang]
|
||||||
- Add google analytics and facebook scripts && improve url share method
|
|
||||||
w/ fb && enforce https in prod. [Lang]
|
* Add missing buttons for overlays #7. [Lang]
|
||||||
- Reg and login buttons on index && remove list method && facebook
|
|
||||||
centralize. [Lang]
|
* A new feature came up - the abandoned plays can be restored, if both users are registered users #7. [Lang]
|
||||||
- Redesign user frontend. [Lang]
|
|
||||||
- Mods for performance; one js.min file on prod. [Lang]
|
### Changes
|
||||||
- Improve webpack config for prod compile #23. [Lang]
|
|
||||||
- Ssl handling #22 && reconnection issues #20, #21. [Lang]
|
* Fix the '0' in Battle reports #6. [Lang]
|
||||||
- Facebook prod settings w/ app; hwi/HWIOAuthBundle. [Lang]
|
|
||||||
- Refact && game reconnection and restore w/o refresh #3 && bugfix bomb
|
* Fix missing icons on "Battle report" #6. [Lang]
|
||||||
explosion on opponent mines #19. [Lang]
|
|
||||||
- Typo in rpc. [Lang]
|
### Fix
|
||||||
- Handle prod mysql timeout && graphics improve. [Lang]
|
|
||||||
- Gitignore webpacked index.js. [Lang]
|
* The bomb using was not recorded correctly - the old data will be corrupted #6. [Lang]
|
||||||
- Add production mods. [Lang]
|
|
||||||
- Bugfix points saving and exploded bombs to db && you can resign #6.
|
|
||||||
[Lang]
|
## v2026.2.4-0 (2026-04-18)
|
||||||
- Bugfix resign button existence #11. [Lang]
|
|
||||||
- Bugfix opponent bomb btn buzz on hover #10. [Lang]
|
### New
|
||||||
- Bugfix points problem in the end #16. [Lang]
|
|
||||||
- Add desc to every user #9. [Lang]
|
* Add new profile charts and stats - & add new logo to the tech stack #5. [Lang]
|
||||||
- Clipboard - not working #8. [Lang]
|
|
||||||
- Random player on start #5. [Lang]
|
### Changes
|
||||||
- Show left mines after end #2 && reduce network traffic && better
|
|
||||||
active field checking method. [Lang]
|
* Upgrade fe deps #5. [Lang]
|
||||||
- Some refactor #13. [Lang]
|
|
||||||
- Bugfix grid field render #12. [Lang]
|
* Improve the Battle reports to change unnecessary data with interesting data #5. [Lang]
|
||||||
- Game ends after x mines. [Lang]
|
|
||||||
- Add new sounds && refactor && new bg images && form redesigns. [Lang]
|
### Fix
|
||||||
- Bugfix entities gridrow, grid && improve graph design on homepage.
|
|
||||||
[Lang]
|
* The react is crashing on some cases #5. [Lang]
|
||||||
- Some refactor && prod settings. [Lang]
|
|
||||||
- Improve graphics design in game. [Lang]
|
|
||||||
- Bugfix grid row in entity. [Lang]
|
## v2026.2.3-0 (2026-04-18)
|
||||||
- Bugfix changePlayer after bomb explosion. [Lang]
|
|
||||||
- Improve game graph design. [Lang]
|
### New
|
||||||
- Login and register form more design. [Lang]
|
|
||||||
- Add basic design to userbundle && refactor. [Lang]
|
* Add initialization bonus points' system to the gameplay #5. [Lang]
|
||||||
- Add font-awesome. [Lang]
|
|
||||||
- Working user authentication w/ fb and plain login. [Lang]
|
### Changes
|
||||||
- Add facebook login module, hwi/HWIOAuthBundle. [Lang]
|
|
||||||
- Login && register form overrided. [Lang]
|
* Add extended data to battle reports and sharing image to make viewable bonus points #5. [Lang]
|
||||||
- Js and config refactor. [Lang]
|
|
||||||
- Replace gridcol object to json array in db. [Lang]
|
|
||||||
- Refactor. [Lang]
|
## v2026.2.2-9 (2026-04-18)
|
||||||
- Save steps and point info to db. [Lang]
|
|
||||||
- Save the step data to db. [Lang]
|
### New
|
||||||
- Renamed the acme to mineseeker && handle when the user connection has
|
|
||||||
been lost. [Lang]
|
* Add rules page #4. [Lang]
|
||||||
- Add player names to UI. [Lang]
|
|
||||||
- Add overlay && game do not start until the opponent came. [Lang]
|
### Fix
|
||||||
- Add base64 encryption to grid when it has been sended to server.
|
|
||||||
[Lang]
|
* The font-awesome simplifying to work on bare-metal - & fix all warnings at build time #4. [Lang]
|
||||||
- On click opponents bomb, you cannot target && refactor. [Lang]
|
|
||||||
- Warning when player has been found more than 20 mines. [Lang]
|
* The css problem had been solved on reponsive gfx on homepage #4. [Lang]
|
||||||
- Bugfix center mine counter animation. [Lang]
|
|
||||||
- The opponent is the next when bomb is exploded. [Lang]
|
|
||||||
- Current username checked && refactor && remove players in channel when
|
## v2026.2.1-8 (2026-04-18)
|
||||||
they are more than 2. [Lang]
|
|
||||||
- Send bomb info and use it on opponent. [Lang]
|
### Fix
|
||||||
- Add sounds w/ howler. [Lang]
|
|
||||||
- Bugfix multiple empty fields w/ one click on opponent view. [Lang]
|
* Quickfix for https-only login - & add user data when the user is not logged in #4. [Lang]
|
||||||
- Refact && remove sound and logging && bugfix BIGBUG - handleGridField
|
|
||||||
and showAppropriateFields sort order... [Lang]
|
|
||||||
- Create first working communication. [Lang]
|
## v2026.2.1-7 (2026-04-16)
|
||||||
- Create entities and repositories. [Lang]
|
|
||||||
- Changed websocket default port && debug RPC. [Lang]
|
### Changes
|
||||||
- Created working session and client handler w/ websocket. [Lang]
|
|
||||||
- Working websocket client and server w/o session handling and storage.
|
* Add consent checkbox to user's registration - and fix the sharing pics #4. [Lang]
|
||||||
[Lang]
|
|
||||||
- Composer update. [Lang]
|
* Add correct version numbering and CHANGELOG - and add the LICENSE #4. [Lang]
|
||||||
- Improve game && start sound creating. [Lang]
|
|
||||||
- Refactor grid control and grid field. [Lang]
|
|
||||||
- Created basic game w/ table and animations. [Lang]
|
## v2026.2.1-6 (2026-04-16)
|
||||||
- Websocket basic setup FE & BE && working basic game w/ react &&
|
|
||||||
webpack & babel config. [Lang]
|
### Changes
|
||||||
- Gitignore node_modules && add symlink to node_modules (just for
|
|
||||||
install) && basic react. [Lang]
|
* Update all texts on all pages - extend them with the game specific things #4. [Lang]
|
||||||
- Add react hello world. [Lang]
|
|
||||||
- Rename project in config. [Lang]
|
|
||||||
- Initial commit && create project in symfony3. [Lang]
|
## v2026.2.1-5 (2026-04-16)
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
* The meta tags does not have https scheme - nothing worked in configuration #4. [Lang]
|
||||||
|
|
||||||
|
|
||||||
|
## v2026.2.1-4 (2026-04-15)
|
||||||
|
|
||||||
|
### New
|
||||||
|
|
||||||
|
* Add notification email when a user is registered #4. [Lang]
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
* Add notification on activation too #4. [Lang]
|
||||||
|
|
||||||
|
|
||||||
|
## v2026.2.1-2 (2026-04-15)
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
* Another attempt to fix the email assets #4. [Lang]
|
||||||
|
|
||||||
|
|
||||||
|
## v2026.2.1-1 (2026-04-15)
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
* The images does not shows in emails #4. [Lang]
|
||||||
|
|
||||||
|
|
||||||
|
## v2026.2.1-0 (2026-04-15)
|
||||||
|
|
||||||
|
### New
|
||||||
|
|
||||||
|
* Add Contact page with email sending behaviour #4. [Lang]
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
* Add missing .env variable and increase the version number and add missing data from front-end and back-end deps descriptor #4. [Lang]
|
||||||
|
|
||||||
|
* Change the shareable battle - add avatars to it - even on the og tags #4. [Lang]
|
||||||
|
|
||||||
|
* Change text #4. [Lang]
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
* The mailhog is crashed on development env #4. [Lang]
|
||||||
|
|
||||||
|
* The og tags did not have proper http schema - they should have https #4. [Lang]
|
||||||
|
|
||||||
|
|
||||||
|
## v2026.2.0-5 (2026-04-14)
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
* Add donation button #4. [Lang]
|
||||||
|
|
||||||
|
|
||||||
|
## v2026.2.0-4 (2026-04-14)
|
||||||
|
|
||||||
|
### New
|
||||||
|
|
||||||
|
* Add timer for the acceptance of the challenge #4. [Lang]
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
* Protect the gameplay with recaptcha #4. [Lang]
|
||||||
|
|
||||||
|
* The waiting dialog is uncloseable until the time is up #4. [Lang]
|
||||||
|
|
||||||
|
* Add share button to the overlay when the game ends #4. [Lang]
|
||||||
|
|
||||||
|
* Make fancy og tags - and create a special one for battle sharing #4. [Lang]
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
* Missing font-awesome icons on bare-metal environment #4. [Lang]
|
||||||
|
|
||||||
|
|
||||||
|
## v2026.2.0-3 (2026-04-14)
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
* The user's avatar will be saved as a uuid.extension #4. [Lang]
|
||||||
|
|
||||||
|
|
||||||
|
## v2026.2.0-1 (2026-04-14)
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
* Quickfix for email sending #4. [Lang]
|
||||||
|
|
||||||
|
|
||||||
|
## v2026.2.0-0 (2026-04-14)
|
||||||
|
|
||||||
|
### New
|
||||||
|
|
||||||
|
* Registered users have avatars next to the timer #4. [Lang]
|
||||||
|
|
||||||
|
* Add opportunity to use profile picture. #4. [Lang]
|
||||||
|
|
||||||
|
* Add more stats and a dialog for the recent battle that can be shareable #4. [Lang]
|
||||||
|
|
||||||
|
* Implement the 2FA authentication (TOTP and backup codes) #4. [Lang]
|
||||||
|
|
||||||
|
* Add beta logo to the corner #3. [Lang]
|
||||||
|
|
||||||
|
* Add mineseeker game to the symfony 4 project #3. [Lang]
|
||||||
|
|
||||||
|
* Upgrade to the latest symfony v4 #3. [Lang]
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
* Implement CD script to Gitea and add docs to the process #4. [Lang]
|
||||||
|
|
||||||
|
* Remove unnecessary cdn based fonts #4. [Lang]
|
||||||
|
|
||||||
|
* Update docs #4. [Lang]
|
||||||
|
|
||||||
|
* Add JWT generation script to make Mercure safe #4. [Lang]
|
||||||
|
|
||||||
|
* Fix missing favicon #4. [Lang]
|
||||||
|
|
||||||
|
* Make compatible the whole project with bare metal AND with docker #4. [Lang]
|
||||||
|
|
||||||
|
* Add modern Webauthn authentication #4. [Lang]
|
||||||
|
|
||||||
|
* Refactor all forms to have Symfony Form Types & Validation Constrainsts - & implement Google ReCapthca v3 #4. [Lang]
|
||||||
|
|
||||||
|
* Add forgot password functionality #4. [Lang]
|
||||||
|
|
||||||
|
* Increase the minimum PHP version to the latest major - and massive refactor on back-end, like Controllers and Repositories #4. [Lang]
|
||||||
|
|
||||||
|
* Redesign the resign dialog #4. [Lang]
|
||||||
|
|
||||||
|
* Re-implement the waiting for opponent dialog - refactor its gfx - & add online user selection dialog #4. [Lang]
|
||||||
|
|
||||||
|
* Improve the gfx on homepage - implement login/register and activation for authentication - and add the first version of profile page #4. [Lang]
|
||||||
|
|
||||||
|
* Refactor and redesign the gfx on front-end #4. [Lang]
|
||||||
|
|
||||||
|
* Upgrade to the latest LTS Symfony package and backend #4. [Lang]
|
||||||
|
|
||||||
|
* Add timers to each player - renew the whole migration #4. [Lang]
|
||||||
|
|
||||||
|
* Update the vite related stuff because CORS and React errors - reinit the miration #4. [Lang]
|
||||||
|
|
||||||
|
* Use namespaces for front-end #4. [Lang]
|
||||||
|
|
||||||
|
* Replace webpack w/ vite & remove old, legacy jQuery from the code #4. [Lang]
|
||||||
|
|
||||||
|
* More, massive refactor for front-end #4. [Lang]
|
||||||
|
|
||||||
|
* Massive refactor on front-end - and remove unnecessary deps #4. [Lang]
|
||||||
|
|
||||||
|
* Change the code style to fit the current standard #4. [Lang]
|
||||||
|
|
||||||
|
* Refactor to use Attributes instead of yaml markdown #4. [Lang]
|
||||||
|
|
||||||
|
* Outsource the Grid generation and interactions to the backend #4. [Lang]
|
||||||
|
|
||||||
|
* Remove unnecessary variables and prune the Facebook registration method #4. [Lang]
|
||||||
|
|
||||||
|
* Replace the legacy gos/web-socket-bundle & replace it with Mercure protocol #4. [Lang]
|
||||||
|
|
||||||
|
* Make a massive refactor to the backend and remove all unnecessary deps - and make small refactors for the frontend too #4. [Lang]
|
||||||
|
|
||||||
|
* Created the first working solution since 7 yrs #4. [Lang]
|
||||||
|
|
||||||
|
* Add some changes on BE - add eslint and editorconfig - and add some deps #4. [Lang]
|
||||||
|
|
||||||
|
* Make the first working version - the stepping is broken due to the algorythm structure #4. [Lang]
|
||||||
|
|
||||||
|
* Change the composer default php minimum environment #3. [Lang]
|
||||||
|
|
||||||
|
* Change the default url to wss on frontend #3. [Lang]
|
||||||
|
|
||||||
|
* Refactor Rpc and Topic classes #3. [Lang]
|
||||||
|
|
||||||
|
* Refactor classes and reformat some layout #3. [Lang]
|
||||||
|
|
||||||
|
* Remove deprecated files #3. [Lang]
|
||||||
|
|
||||||
|
* Doc in README.md #3. [Lang]
|
||||||
|
|
||||||
|
* Gitignore a js.map file #2. [Lang]
|
||||||
|
|
||||||
|
### Other
|
||||||
|
|
||||||
|
* Pkg: usr: solve the not-working mailing on dev env under docker #4. [Lang]
|
||||||
|
|
||||||
|
|
||||||
|
## 1.1.0 (2019-10-26)
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
* Reinit project - disable redis module and make the project compatible w/ PHP7.3 #2. [Lang]
|
||||||
|
|
||||||
|
|
||||||
|
## 0.4.0 (2019-10-26)
|
||||||
|
|
||||||
|
### Other
|
||||||
|
|
||||||
|
* Change session driver to REDIS. [Lang]
|
||||||
|
|
||||||
|
* Add created, updated field to db && improve graph design. [Lang]
|
||||||
|
|
||||||
|
* Cache setup && optimalize for google pagespeed && optimalize all images. [Lang]
|
||||||
|
|
||||||
|
* Improve graph design on homepage && add footer and techs && add official pages. [Lang]
|
||||||
|
|
||||||
|
* Bugfix mine websocket periodic mysql calling. [Lang]
|
||||||
|
|
||||||
|
* Bugfix hwioauth remember me && centralize hwioauth and facebook settings. [Lang]
|
||||||
|
|
||||||
|
* Centralize jquery && bugfix mysql auto-termination problem w/ user auth. [Lang]
|
||||||
|
|
||||||
|
* Release beta4. [Lang]
|
||||||
|
|
||||||
|
* Gitignore npm debug log. [Lang]
|
||||||
|
|
||||||
|
* Add english lang everywhere && add snowfall && add centralized version nbr && improve stylesheet && slack integration. [Lang]
|
||||||
|
|
||||||
|
* Bugfix #30 && random bg in game. [Lang]
|
||||||
|
|
||||||
|
* Add google analytics and facebook scripts && improve url share method w/ fb && enforce https in prod. [Lang]
|
||||||
|
|
||||||
|
* Reg and login buttons on index && remove list method && facebook centralize. [Lang]
|
||||||
|
|
||||||
|
* Redesign user frontend. [Lang]
|
||||||
|
|
||||||
|
* Mods for performance; one js.min file on prod. [Lang]
|
||||||
|
|
||||||
|
* Improve webpack config for prod compile #23. [Lang]
|
||||||
|
|
||||||
|
* Ssl handling #22 && reconnection issues #20, #21. [Lang]
|
||||||
|
|
||||||
|
* Facebook prod settings w/ app; hwi/HWIOAuthBundle. [Lang]
|
||||||
|
|
||||||
|
* Refact && game reconnection and restore w/o refresh #3 && bugfix bomb explosion on opponent mines #19. [Lang]
|
||||||
|
|
||||||
|
* Typo in rpc. [Lang]
|
||||||
|
|
||||||
|
* Handle prod mysql timeout && graphics improve. [Lang]
|
||||||
|
|
||||||
|
* Gitignore webpacked index.js. [Lang]
|
||||||
|
|
||||||
|
* Add production mods. [Lang]
|
||||||
|
|
||||||
|
* Bugfix points saving and exploded bombs to db && you can resign #6. [Lang]
|
||||||
|
|
||||||
|
* Bugfix resign button existence #11. [Lang]
|
||||||
|
|
||||||
|
* Bugfix opponent bomb btn buzz on hover #10. [Lang]
|
||||||
|
|
||||||
|
* Bugfix points problem in the end #16. [Lang]
|
||||||
|
|
||||||
|
* Add desc to every user #9. [Lang]
|
||||||
|
|
||||||
|
* Clipboard - not working #8. [Lang]
|
||||||
|
|
||||||
|
* Random player on start #5. [Lang]
|
||||||
|
|
||||||
|
* Show left mines after end #2 && reduce network traffic && better active field checking method. [Lang]
|
||||||
|
|
||||||
|
* Some refactor #13. [Lang]
|
||||||
|
|
||||||
|
* Bugfix grid field render #12. [Lang]
|
||||||
|
|
||||||
|
* Game ends after x mines. [Lang]
|
||||||
|
|
||||||
|
* Add new sounds && refactor && new bg images && form redesigns. [Lang]
|
||||||
|
|
||||||
|
* Bugfix entities gridrow, grid && improve graph design on homepage. [Lang]
|
||||||
|
|
||||||
|
* Some refactor && prod settings. [Lang]
|
||||||
|
|
||||||
|
* Improve graphics design in game. [Lang]
|
||||||
|
|
||||||
|
* Bugfix grid row in entity. [Lang]
|
||||||
|
|
||||||
|
* Bugfix changePlayer after bomb explosion. [Lang]
|
||||||
|
|
||||||
|
* Improve game graph design. [Lang]
|
||||||
|
|
||||||
|
* Login and register form more design. [Lang]
|
||||||
|
|
||||||
|
* Add basic design to userbundle && refactor. [Lang]
|
||||||
|
|
||||||
|
* Add font-awesome. [Lang]
|
||||||
|
|
||||||
|
* Working user authentication w/ fb and plain login. [Lang]
|
||||||
|
|
||||||
|
* Add facebook login module, hwi/HWIOAuthBundle. [Lang]
|
||||||
|
|
||||||
|
* Login && register form overrided. [Lang]
|
||||||
|
|
||||||
|
* Js and config refactor. [Lang]
|
||||||
|
|
||||||
|
* Replace gridcol object to json array in db. [Lang]
|
||||||
|
|
||||||
|
* Refactor. [Lang]
|
||||||
|
|
||||||
|
* Save steps and point info to db. [Lang]
|
||||||
|
|
||||||
|
* Save the step data to db. [Lang]
|
||||||
|
|
||||||
|
* Renamed the acme to mineseeker && handle when the user connection has been lost. [Lang]
|
||||||
|
|
||||||
|
* Add player names to UI. [Lang]
|
||||||
|
|
||||||
|
* Add overlay && game do not start until the opponent came. [Lang]
|
||||||
|
|
||||||
|
* Add base64 encryption to grid when it has been sended to server. [Lang]
|
||||||
|
|
||||||
|
* On click opponents bomb, you cannot target && refactor. [Lang]
|
||||||
|
|
||||||
|
* Warning when player has been found more than 20 mines. [Lang]
|
||||||
|
|
||||||
|
* Bugfix center mine counter animation. [Lang]
|
||||||
|
|
||||||
|
* The opponent is the next when bomb is exploded. [Lang]
|
||||||
|
|
||||||
|
* Current username checked && refactor && remove players in channel when they are more than 2. [Lang]
|
||||||
|
|
||||||
|
* Send bomb info and use it on opponent. [Lang]
|
||||||
|
|
||||||
|
* Add sounds w/ howler. [Lang]
|
||||||
|
|
||||||
|
* Bugfix multiple empty fields w/ one click on opponent view. [Lang]
|
||||||
|
|
||||||
|
* Refact && remove sound and logging && bugfix BIGBUG - handleGridField and showAppropriateFields sort order... [Lang]
|
||||||
|
|
||||||
|
* Create first working communication. [Lang]
|
||||||
|
|
||||||
|
* Create entities and repositories. [Lang]
|
||||||
|
|
||||||
|
* Changed websocket default port && debug RPC. [Lang]
|
||||||
|
|
||||||
|
* Created working session and client handler w/ websocket. [Lang]
|
||||||
|
|
||||||
|
* Working websocket client and server w/o session handling and storage. [Lang]
|
||||||
|
|
||||||
|
* Composer update. [Lang]
|
||||||
|
|
||||||
|
* Improve game && start sound creating. [Lang]
|
||||||
|
|
||||||
|
* Refactor grid control and grid field. [Lang]
|
||||||
|
|
||||||
|
* Created basic game w/ table and animations. [Lang]
|
||||||
|
|
||||||
|
* Websocket basic setup FE & BE && working basic game w/ react && webpack & babel config. [Lang]
|
||||||
|
|
||||||
|
* Gitignore node_modules && add symlink to node_modules (just for install) && basic react. [Lang]
|
||||||
|
|
||||||
|
* Add react hello world. [Lang]
|
||||||
|
|
||||||
|
* Rename project in config. [Lang]
|
||||||
|
|
||||||
|
* Initial commit && create project in symfony3. [Lang]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,10 @@
|
|||||||
|
|
||||||
encode zstd br gzip
|
encode zstd br gzip
|
||||||
|
|
||||||
|
# Forward scheme information to the PHP application
|
||||||
|
header X-Forwarded-Proto {scheme}
|
||||||
|
header X-Forwarded-Host {host}
|
||||||
|
|
||||||
mercure {
|
mercure {
|
||||||
transport_url {$MERCURE_TRANSPORT_URL:bolt:///data/mercure.db}
|
transport_url {$MERCURE_TRANSPORT_URL:bolt:///data/mercure.db}
|
||||||
publisher_jwt {$MERCURE_JWT_SECRET} HS256
|
publisher_jwt {$MERCURE_JWT_SECRET} HS256
|
||||||
|
|||||||
@@ -22,6 +22,12 @@ RUN install-php-extensions \
|
|||||||
apcu \
|
apcu \
|
||||||
sodium
|
sodium
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
fonts-dejavu-core \
|
||||||
|
fontconfig \
|
||||||
|
&& fc-cache -f -v \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
|
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
|
||||||
RUN printf '[opcache]\nopcache.enable=1\nopcache.memory_consumption=256\nopcache.max_accelerated_files=20000\nopcache.validate_timestamps=0\n' \
|
RUN printf '[opcache]\nopcache.enable=1\nopcache.memory_consumption=256\nopcache.max_accelerated_files=20000\nopcache.validate_timestamps=0\n' \
|
||||||
> "$PHP_INI_DIR/conf.d/opcache.ini"
|
> "$PHP_INI_DIR/conf.d/opcache.ini"
|
||||||
|
|||||||
124
LICENSE
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
|
Copyright (C) 2026 SplendidBear (https://www.splendidbear.org)
|
||||||
|
|
||||||
|
MineSeeker is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
MineSeeker is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS:
|
||||||
|
|
||||||
|
0. Definitions.
|
||||||
|
|
||||||
|
"This License" refers to version 3 of the GNU General Public License.
|
||||||
|
|
||||||
|
"Copyright" also means copyright-like laws that apply to other kinds of works.
|
||||||
|
|
||||||
|
"The Program" refers to any copyrightable work licensed under this License.
|
||||||
|
|
||||||
|
"You" refers to each licensee.
|
||||||
|
|
||||||
|
"Legal entities" means the union of the acting entity and all other entities
|
||||||
|
that control, are controlled by, or are under common control with that entity.
|
||||||
|
|
||||||
|
"Modify" means to copy from or adapt all or part of the work in a fashion
|
||||||
|
requiring copyright permission, other than the making of an exact copy.
|
||||||
|
|
||||||
|
1. Source Code.
|
||||||
|
|
||||||
|
The "source code" for a work means the preferred form of the work for making
|
||||||
|
modifications to it. "Object code" means any non-source form of a work.
|
||||||
|
|
||||||
|
2. Basic Permissions.
|
||||||
|
|
||||||
|
All rights granted under this License are granted for the term of copyright
|
||||||
|
on the Program. You are granted all permissions necessary to run, modify and
|
||||||
|
propagate covered works by this License.
|
||||||
|
|
||||||
|
3. Copyleft - Derivative Works.
|
||||||
|
|
||||||
|
If you modify the Program, your modified version must:
|
||||||
|
|
||||||
|
- Carry prominent notices stating that you have modified it
|
||||||
|
- License the entire work under this License or a compatible license
|
||||||
|
- Make the source code available to recipients
|
||||||
|
- Preserve all notices of previous licensing
|
||||||
|
|
||||||
|
4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
|
You may convey verbatim copies of the Program's source code as you receive it,
|
||||||
|
provided that you:
|
||||||
|
|
||||||
|
- Keep intact all notices of authorship and licensing
|
||||||
|
- Give recipients access to the source code along with this License
|
||||||
|
- Do not modify anything except the License itself
|
||||||
|
|
||||||
|
5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
|
You may convey a work based on the Program under this License provided that:
|
||||||
|
|
||||||
|
- The work must be licensed as a whole under this License
|
||||||
|
- You must give prominent notice of any modifications
|
||||||
|
- You must provide access to the Corresponding Source code
|
||||||
|
- You preserve all licensing notices
|
||||||
|
|
||||||
|
6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
|
If you convey object code or compiled versions, you must also provide:
|
||||||
|
|
||||||
|
- The Corresponding Source code (in machine-readable form)
|
||||||
|
- A notice of the terms under which it is licensed
|
||||||
|
|
||||||
|
7. Additional Terms.
|
||||||
|
|
||||||
|
No additional restrictions may be placed on the exercise of the rights
|
||||||
|
granted or affirmed under this License.
|
||||||
|
|
||||||
|
8. Disclaimer of Warranty.
|
||||||
|
|
||||||
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||||
|
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||||
|
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||||
|
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
9. Limitation of Liability.
|
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
|
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING
|
||||||
|
ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF
|
||||||
|
THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS
|
||||||
|
OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR
|
||||||
|
THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||||
|
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||||
|
SUCH DAMAGES.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
For the complete GPL-3.0-or-later license text, visit:
|
||||||
|
https://www.gnu.org/licenses/gpl-3.0.html
|
||||||
|
|
||||||
|
For more information about GNU GPL, visit:
|
||||||
|
https://www.gnu.org/licenses/
|
||||||
|
|
||||||
|
MineSeeker is a multiplayer minesweeper game inspired by MSN Messenger's game.
|
||||||
|
Project: https://www.mineseeker.hu
|
||||||
|
Author: SplendidBear (https://www.splendidbear.org)
|
||||||
|
|
||||||
|
|
||||||
33
Makefile
@@ -1,4 +1,4 @@
|
|||||||
.PHONY: help start start-build stop build down ps logs prune-everything db-reset mercure-jwt
|
.PHONY: help start start-build stop build down ps logs prune-everything db-reset mercure-jwt cache-clear og-cache-clear
|
||||||
|
|
||||||
.DEFAULT_GOAL := help
|
.DEFAULT_GOAL := help
|
||||||
|
|
||||||
@@ -11,6 +11,9 @@ help:
|
|||||||
@echo " make down - Stop and remove containers/networks"
|
@echo " make down - Stop and remove containers/networks"
|
||||||
@echo " make prune-everything - Prune volumes, networks and images (DANGEROUS!)"
|
@echo " make prune-everything - Prune volumes, networks and images (DANGEROUS!)"
|
||||||
@echo " make db-reset - Reset the database (drop, create, migrate) (DANGEROUS!)"
|
@echo " make db-reset - Reset the database (drop, create, migrate) (DANGEROUS!)"
|
||||||
|
@echo " make ccp - Clear the production cache"
|
||||||
|
@echo " make cache-clear - Clear all caches (Vite, Symfony, node_modules)"
|
||||||
|
@echo " make og-cache-clear - Clear Open Graph cache only"
|
||||||
|
|
||||||
start:
|
start:
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
@@ -51,3 +54,31 @@ db-reset:
|
|||||||
bin/console doctrine:database:drop --force --if-exists --no-interaction
|
bin/console doctrine:database:drop --force --if-exists --no-interaction
|
||||||
bin/console doctrine:database:create --if-not-exists --no-interaction
|
bin/console doctrine:database:create --if-not-exists --no-interaction
|
||||||
bin/console doctrine:migrations:migrate --no-interaction
|
bin/console doctrine:migrations:migrate --no-interaction
|
||||||
|
|
||||||
|
ccp:
|
||||||
|
bin/console cache:clear --no-warmup --env=prod
|
||||||
|
|
||||||
|
cache-clear:
|
||||||
|
@echo "Clearing all caches..."
|
||||||
|
@rm -rf node_modules/.vite
|
||||||
|
@rm -rf .vite
|
||||||
|
@rm -rf var/og-cache
|
||||||
|
@php bin/console cache:clear --no-warmup
|
||||||
|
@echo "✓ Vite cache cleared"
|
||||||
|
@echo "✓ OG cache cleared"
|
||||||
|
@echo "✓ Symfony cache cleared"
|
||||||
|
@echo ""
|
||||||
|
@echo "Rebuilding assets..."
|
||||||
|
@bun run build
|
||||||
|
@echo ""
|
||||||
|
@echo "✓ All caches cleared and assets rebuilt!"
|
||||||
|
@echo " Next step: Refresh browser with Ctrl+Shift+R"
|
||||||
|
|
||||||
|
og-cache-clear:
|
||||||
|
@echo "Clearing Open Graph cache..."
|
||||||
|
@rm -rf var/og-cache
|
||||||
|
@echo "✓ OG cache cleared!"
|
||||||
|
@echo " Battle card images will be regenerated on next access"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
22
README.md
@@ -151,6 +151,7 @@ services:
|
|||||||
app:
|
app:
|
||||||
environment:
|
environment:
|
||||||
MAILER_DSN: smtp://mail:1025?verify_peer=0
|
MAILER_DSN: smtp://mail:1025?verify_peer=0
|
||||||
|
TRUSTED_PROXIES: "0.0.0.0/0"
|
||||||
mail:
|
mail:
|
||||||
image: mailhog/mailhog:latest
|
image: mailhog/mailhog:latest
|
||||||
ports:
|
ports:
|
||||||
@@ -216,7 +217,7 @@ POSTGRES_VERSION=18
|
|||||||
MINIO_ROOT_USER=mineseeker
|
MINIO_ROOT_USER=mineseeker
|
||||||
MINIO_ROOT_PASSWORD="<strong password>"
|
MINIO_ROOT_PASSWORD="<strong password>"
|
||||||
MINIO_ENDPOINT=http://minio:9000
|
MINIO_ENDPOINT=http://minio:9000
|
||||||
MINIO_PUBLIC_URL=https://minio.mineseeker.hu
|
MINIO_PUBLIC_URL=https://aws.mineseeker.hu
|
||||||
|
|
||||||
MAILER_DSN=smtp://mail:25?verify_peer=0
|
MAILER_DSN=smtp://mail:25?verify_peer=0
|
||||||
MAIL_DOMAIN=mineseeker.hu
|
MAIL_DOMAIN=mineseeker.hu
|
||||||
@@ -233,8 +234,13 @@ MERCURE_SUBSCRIBER_JWT="<generated by make mercure-jwt>"
|
|||||||
APP_PUBLIC_HOSTNAME=mineseeker.hu
|
APP_PUBLIC_HOSTNAME=mineseeker.hu
|
||||||
WEBAUTHN_RP_ID=mineseeker.hu
|
WEBAUTHN_RP_ID=mineseeker.hu
|
||||||
WEBAUTHN_ORIGIN=https://mineseeker.hu
|
WEBAUTHN_ORIGIN=https://mineseeker.hu
|
||||||
```
|
|
||||||
|
|
||||||
|
# OG Tags & Social Media Sharing (IMPORTANT for Docker deployments)
|
||||||
|
# TRUSTED_PROXIES: IP address (or range) of your reverse proxy (Caddy/Nginx)
|
||||||
|
# This ensures OG image tags use HTTPS URLs instead of HTTP
|
||||||
|
TRUSTED_PROXIES="172.18.0.0/16"
|
||||||
|
TRUSTED_HOSTS="mineseeker.hu,www.mineseeker.hu"
|
||||||
|
```
|
||||||
### Production server: one-time setup
|
### Production server: one-time setup
|
||||||
|
|
||||||
The server needs Docker, Git, and a self-hosted `act_runner` registered against the Gitea repository. Bun and Composer run inside the multi-stage Dockerfile, so they are not needed on the server.
|
The server needs Docker, Git, and a self-hosted `act_runner` registered against the Gitea repository. Bun and Composer run inside the multi-stage Dockerfile, so they are not needed on the server.
|
||||||
@@ -254,7 +260,7 @@ make mercure-jwt
|
|||||||
|
|
||||||
Copy the three printed values into the `PROD_ENV_FILE` secret.
|
Copy the three printed values into the `PROD_ENV_FILE` secret.
|
||||||
|
|
||||||
#### 5. First deploy
|
#### 3. First deploy
|
||||||
|
|
||||||
Trigger it by pushing the first tag:
|
Trigger it by pushing the first tag:
|
||||||
|
|
||||||
@@ -265,7 +271,7 @@ git push origin v2026.01
|
|||||||
|
|
||||||
This writes `.env`, builds the Docker image, starts all services, runs migrations, and initialises the MinIO buckets automatically via `minio_init`.
|
This writes `.env`, builds the Docker image, starts all services, runs migrations, and initialises the MinIO buckets automatically via `minio_init`.
|
||||||
|
|
||||||
#### 6. Verify
|
#### 4. Verify
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose ps # all services should be healthy/running
|
docker compose ps # all services should be healthy/running
|
||||||
@@ -281,6 +287,14 @@ git push origin v2026.01
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Game Documentation
|
||||||
|
|
||||||
|
For detailed information about game mechanics, bonus systems, and scoring rules, see the [docs](./docs/) directory:
|
||||||
|
|
||||||
|
- **[Bonus Points System](./docs/game-mechanics/BONUS_POINTS_SYSTEM.md)** — Complete reference for all bonus point types, calculation rules, and implementation details
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
LGPL-3.0 — see [LICENSE](LICENSE) for details.
|
LGPL-3.0 — see [LICENSE](LICENSE) for details.
|
||||||
|
|||||||
@@ -7,9 +7,4 @@
|
|||||||
* file that was distributed with this source code.
|
* file that was distributed with this source code.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
$font-path: "/webfonts";
|
@import '@fortawesome/fontawesome-free/css/all.min.css';
|
||||||
|
|
||||||
@import '@fortawesome/fontawesome-free/scss/fontawesome';
|
|
||||||
@import '@fortawesome/fontawesome-free/scss/brands';
|
|
||||||
@import '@fortawesome/fontawesome-free/scss/solid';
|
|
||||||
@import '@fortawesome/fontawesome-free/scss/regular';
|
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
@keyframes appear {
|
@keyframes appear {
|
||||||
from { opacity: 0; transform: scale(0.94); }
|
from { opacity: 0; transform: scale(0.94); }
|
||||||
to { opacity: 1; transform: scale(1); }
|
to { opacity: 1; transform: scale(1); }
|
||||||
|
|||||||
@@ -1,14 +1,24 @@
|
|||||||
.hero-auth {
|
/*!*
|
||||||
position: absolute;
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
top: 28px;
|
*
|
||||||
right: 36px;
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#hero-auth {
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
.hero-auth {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-auth-user {
|
.hero-auth-user {
|
||||||
font: 600 13px 'Rajdhani', sans-serif;
|
font: 600 13px 'Rajdhani', sans-serif;
|
||||||
color: rgba(149, 207, 245, 0.75);
|
color: rgba(149, 207, 245, 0.75);
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
@@ -17,6 +27,13 @@
|
|||||||
gap: 6px;
|
gap: 6px;
|
||||||
|
|
||||||
i { font-size: 15px; }
|
i { font-size: 15px; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 1100px) {
|
||||||
|
.hero-auth {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-auth-btn {
|
.hero-auth-btn {
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
.auth-page {
|
.auth-page {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -180,6 +189,41 @@
|
|||||||
input[type="checkbox"] { accent-color: #236f87; }
|
input[type="checkbox"] { accent-color: #236f87; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.auth-checkbox {
|
||||||
|
accent-color: #236f87;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-checkbox-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
font: 400 14px 'Rajdhani', sans-serif;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
line-height: 1.5;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #95cff5;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: color 180ms;
|
||||||
|
|
||||||
|
&:hover { color: #c5e8ff; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea.auth-input {
|
||||||
|
padding: 11px 14px;
|
||||||
|
min-height: 120px;
|
||||||
|
resize: vertical;
|
||||||
|
font-family: 'Rajdhani', sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
.auth-submit {
|
.auth-submit {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
200
assets/css/homepage/_battle-dialog.scss
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ── Avatar ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
.bd-avatar-wrap {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bd-avatar-ring-wrap {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bd-avatar-ring {
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--bd-avatar-gradient);
|
||||||
|
border: 2px solid var(--bd-avatar-border);
|
||||||
|
box-shadow: var(--bd-avatar-glow);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font: 800 24px 'Rajdhani', sans-serif;
|
||||||
|
color: var(--bd-avatar-color);
|
||||||
|
letter-spacing: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bd-avatar-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bd-avatar-bonus {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -6px;
|
||||||
|
right: -6px;
|
||||||
|
background: #ffd700;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 0 12px rgba(255, 215, 0, 0.6), 0 0 0 2px rgba(7, 9, 13, 1);
|
||||||
|
border: 2px solid rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 10;
|
||||||
|
|
||||||
|
i {
|
||||||
|
color: #000;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bd-avatar-name {
|
||||||
|
font: 700 15px 'Rajdhani', sans-serif;
|
||||||
|
color: var(--bd-avatar-color);
|
||||||
|
letter-spacing: 1px;
|
||||||
|
max-width: 120px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bd-avatar-side {
|
||||||
|
font: 600 10px 'Rajdhani', sans-serif;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
color: rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── StatRow ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
.bd-stat-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 9px 0;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
width: 16px;
|
||||||
|
color: rgba(149, 207, 245, 0.4);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__label {
|
||||||
|
font: 500 13px 'Rajdhani', sans-serif;
|
||||||
|
color: rgba(255, 255, 255, 0.45);
|
||||||
|
flex: 1;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__value {
|
||||||
|
font: 700 13px 'Rajdhani', sans-serif;
|
||||||
|
color: var(--bd-stat-value-color, rgba(255, 255, 255, 0.75));
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── BonusPoints ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
.bd-bonus {
|
||||||
|
padding: 16px 20px 0;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
margin: 16px 0;
|
||||||
|
|
||||||
|
&__grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__column {
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
|
||||||
|
&--red {
|
||||||
|
border: 1px solid rgba(173, 10, 5, 0.2);
|
||||||
|
background: rgba(173, 10, 5, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--blue {
|
||||||
|
border: 1px solid rgba(149, 207, 245, 0.2);
|
||||||
|
background: rgba(149, 207, 245, 0.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__heading {
|
||||||
|
font: 700 12px 'Rajdhani', sans-serif;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
color: #ffd700;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
i {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__rows {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── BattleDialog header actions & bonus score row ────────────────────────────
|
||||||
|
|
||||||
|
.bd-header-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bd-bonus-score {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
|
||||||
|
&__red {
|
||||||
|
font: 700 13px 'Rajdhani', sans-serif;
|
||||||
|
color: #f67d52;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__blue {
|
||||||
|
font: 700 13px 'Rajdhani', sans-serif;
|
||||||
|
color: #95cff5;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bd-result-badge {
|
||||||
|
background: var(--bd-result-bg);
|
||||||
|
border: 1px solid var(--bd-result-border);
|
||||||
|
color: var(--bd-result-color);
|
||||||
|
}
|
||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
main div.txt {
|
main div.txt {
|
||||||
color: rgba(255, 255, 255, 0.85);
|
color: rgba(255, 255, 255, 0.85);
|
||||||
max-width: 900px;
|
max-width: 900px;
|
||||||
@@ -33,3 +42,11 @@ main div.txt a {
|
|||||||
|
|
||||||
&:hover { color: #c5e8ff; }
|
&:hover { color: #c5e8ff; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
main div.txt img {
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
main div.txt .img-container {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
.hero-cta {
|
.hero-cta {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|||||||
72
assets/css/homepage/_donate.scss
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.hero-donate-text {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
font: 400 12px 'Rajdhani', sans-serif;
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin-top: 42px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
animation: rise 0.8s 0.5s cubic-bezier(0.22, 1, 0.36, 1) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-donate {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
font: 500 12px 'Rajdhani', sans-serif;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: rgba(255, 184, 82, 0.75);
|
||||||
|
padding: 6px 14px;
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 1px solid rgba(255, 184, 82, 0.25);
|
||||||
|
|
||||||
|
background: rgba(255, 140, 30, 0.05);
|
||||||
|
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 1px rgba(255, 140, 30, 0.1),
|
||||||
|
0 0 8px rgba(255, 140, 30, 0.08);
|
||||||
|
|
||||||
|
transition: transform 200ms ease, box-shadow 200ms ease, color 200ms ease, background 200ms ease;
|
||||||
|
animation: rise 0.8s 0.58s cubic-bezier(0.22, 1, 0.36, 1) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-donate::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: -2px;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: rgba(255, 140, 30, 0.15);
|
||||||
|
filter: blur(8px);
|
||||||
|
opacity: 0.1;
|
||||||
|
z-index: -1;
|
||||||
|
transition: opacity 200ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-donate:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
color: rgba(255, 200, 100, 0.9);
|
||||||
|
background: rgba(255, 140, 30, 0.1);
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 1px rgba(255, 140, 30, 0.2),
|
||||||
|
0 0 12px rgba(255, 140, 30, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-donate:hover::before {
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-donate:active {
|
||||||
|
transform: translateY(0px);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
.feature-block {
|
.feature-block {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 80px 40px;
|
padding: 80px 40px;
|
||||||
@@ -45,33 +54,33 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Bar chart — large, centre
|
// Bar chart — large, centre
|
||||||
i.fa-bar-chart {
|
i.fa-chart-bar {
|
||||||
font-size: 140px;
|
font-size: 160px;
|
||||||
color: rgba(35, 111, 135, 0.35);
|
color: rgba(35, 111, 135, 0.5);
|
||||||
filter: drop-shadow(0 0 30px rgba(35, 111, 135, 0.3));
|
filter: drop-shadow(0 0 40px rgba(35, 111, 135, 0.5));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trophy — top right
|
// Trophy — top right
|
||||||
i.fa-trophy {
|
i.fa-trophy {
|
||||||
font-size: 64px;
|
font-size: 80px;
|
||||||
top: 12px;
|
top: 0px;
|
||||||
right: 30px;
|
right: 20px;
|
||||||
color: rgba(246, 125, 82, 0.5);
|
color: rgba(246, 125, 82, 0.7);
|
||||||
filter: drop-shadow(0 0 16px rgba(246, 125, 82, 0.25));
|
filter: drop-shadow(0 0 25px rgba(246, 125, 82, 0.4));
|
||||||
}
|
}
|
||||||
|
|
||||||
// History — bottom left
|
// Clock history — bottom left
|
||||||
i.fa-history {
|
i.fa-clock-rotate-left {
|
||||||
font-size: 52px;
|
font-size: 68px;
|
||||||
bottom: 18px;
|
bottom: 0px;
|
||||||
left: 30px;
|
left: 20px;
|
||||||
color: rgba(149, 207, 245, 0.4);
|
color: rgba(149, 207, 245, 0.65);
|
||||||
filter: drop-shadow(0 0 12px rgba(149, 207, 245, 0.2));
|
filter: drop-shadow(0 0 20px rgba(149, 207, 245, 0.35));
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover i.fa-bar-chart { color: rgba(35, 111, 135, 0.6); }
|
&:hover i.fa-chart-bar { color: rgba(35, 111, 135, 0.8); filter: drop-shadow(0 0 50px rgba(35, 111, 135, 0.7)); }
|
||||||
&:hover i.fa-trophy { color: rgba(246, 125, 82, 0.75); }
|
&:hover i.fa-trophy { color: rgba(246, 125, 82, 0.9); filter: drop-shadow(0 0 35px rgba(246, 125, 82, 0.6)); }
|
||||||
&:hover i.fa-history { color: rgba(149, 207, 245, 0.65); }
|
&:hover i.fa-clock-rotate-left { color: rgba(149, 207, 245, 0.9); filter: drop-shadow(0 0 30px rgba(149, 207, 245, 0.5)); }
|
||||||
}
|
}
|
||||||
|
|
||||||
// MSN visual
|
// MSN visual
|
||||||
@@ -107,6 +116,98 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Privacy visual
|
||||||
|
.feature-block__visual--privacy {
|
||||||
|
height: 260px;
|
||||||
|
gap: 0;
|
||||||
|
|
||||||
|
i {
|
||||||
|
position: absolute;
|
||||||
|
color: rgba(35, 111, 135, 0.5);
|
||||||
|
transition: color 300ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shield — centre, large
|
||||||
|
i.fa-shield {
|
||||||
|
font-size: 140px;
|
||||||
|
color: rgba(34, 197, 94, 0.3);
|
||||||
|
filter: drop-shadow(0 0 30px rgba(34, 197, 94, 0.25));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lock — top right
|
||||||
|
i.fa-lock {
|
||||||
|
font-size: 56px;
|
||||||
|
top: 20px;
|
||||||
|
right: 35px;
|
||||||
|
color: rgba(168, 85, 247, 0.5);
|
||||||
|
filter: drop-shadow(0 0 16px rgba(168, 85, 247, 0.2));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eye slash — bottom left
|
||||||
|
i.fa-eye-slash {
|
||||||
|
font-size: 48px;
|
||||||
|
bottom: 28px;
|
||||||
|
left: 40px;
|
||||||
|
color: rgba(59, 130, 246, 0.5);
|
||||||
|
filter: drop-shadow(0 0 12px rgba(59, 130, 246, 0.15));
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover i.fa-shield { color: rgba(34, 197, 94, 0.6); }
|
||||||
|
&:hover i.fa-lock { color: rgba(168, 85, 247, 0.75); }
|
||||||
|
&:hover i.fa-eye-slash { color: rgba(59, 130, 246, 0.7); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Practice visual
|
||||||
|
.feature-block__visual--practice {
|
||||||
|
height: 260px;
|
||||||
|
gap: 0;
|
||||||
|
|
||||||
|
i {
|
||||||
|
position: absolute;
|
||||||
|
color: rgba(35, 111, 135, 0.5);
|
||||||
|
transition: color 300ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Laptop — centre, large
|
||||||
|
i.fa-laptop {
|
||||||
|
font-size: 140px;
|
||||||
|
color: rgba(251, 146, 60, 0.3);
|
||||||
|
filter: drop-shadow(0 0 30px rgba(251, 146, 60, 0.25));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Linux — top left
|
||||||
|
i.fa-linux {
|
||||||
|
font-size: 48px;
|
||||||
|
top: 20px;
|
||||||
|
left: 35px;
|
||||||
|
color: rgba(245, 158, 11, 0.5);
|
||||||
|
filter: drop-shadow(0 0 16px rgba(245, 158, 11, 0.2));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apple — top right
|
||||||
|
i.fa-apple {
|
||||||
|
font-size: 56px;
|
||||||
|
top: 20px;
|
||||||
|
right: 35px;
|
||||||
|
color: rgba(156, 163, 175, 0.5);
|
||||||
|
filter: drop-shadow(0 0 16px rgba(156, 163, 175, 0.2));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Windows — bottom left
|
||||||
|
i.fa-windows {
|
||||||
|
font-size: 48px;
|
||||||
|
bottom: 28px;
|
||||||
|
left: 40px;
|
||||||
|
color: rgba(59, 130, 246, 0.5);
|
||||||
|
filter: drop-shadow(0 0 12px rgba(59, 130, 246, 0.15));
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover i.fa-laptop { color: rgba(251, 146, 60, 0.6); }
|
||||||
|
&:hover i.fa-linux { color: rgba(245, 158, 11, 0.75); }
|
||||||
|
&:hover i.fa-apple { color: rgba(156, 163, 175, 0.75); }
|
||||||
|
&:hover i.fa-windows { color: rgba(59, 130, 246, 0.7); }
|
||||||
|
}
|
||||||
|
|
||||||
// Text side
|
// Text side
|
||||||
.feature-block__text {
|
.feature-block__text {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -161,6 +262,56 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.practice-links {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.practice-link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
font: 700 13px 'Rajdhani', sans-serif;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: rgba(149, 207, 245, 0.85);
|
||||||
|
border: 1px solid rgba(59, 130, 246, 0.3);
|
||||||
|
background: rgba(59, 130, 246, 0.08);
|
||||||
|
padding: 10px 18px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 200ms ease;
|
||||||
|
width: fit-content;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(59, 130, 246, 0.2);
|
||||||
|
border-color: rgba(59, 130, 246, 0.6);
|
||||||
|
color: #fff;
|
||||||
|
transform: translateX(4px);
|
||||||
|
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.practice-link-icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 8px;
|
||||||
|
object-fit: contain;
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
padding: 4px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: all 200ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.practice-link:hover .practice-link-icon {
|
||||||
|
background: rgba(59, 130, 246, 0.15);
|
||||||
|
border-color: rgba(59, 130, 246, 0.4);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 900px) {
|
@media screen and (max-width: 900px) {
|
||||||
.feature-block__inner,
|
.feature-block__inner,
|
||||||
.feature-block--reverse .feature-block__inner {
|
.feature-block--reverse .feature-block__inner {
|
||||||
@@ -199,4 +350,14 @@
|
|||||||
.feature-block {
|
.feature-block {
|
||||||
padding: 60px 24px;
|
padding: 60px 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.practice-links {
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.practice-link {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
background: #040608;
|
background: #040608;
|
||||||
border-top: 1px solid rgba(35, 111, 135, 0.12);
|
border-top: 1px solid rgba(35, 111, 135, 0.12);
|
||||||
@@ -14,7 +23,6 @@ footer {
|
|||||||
gap: 40px;
|
gap: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Left: brand block
|
|
||||||
.footer-brand {
|
.footer-brand {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -55,7 +63,6 @@ footer {
|
|||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Right: navigation
|
|
||||||
.footer-nav-label {
|
.footer-nav-label {
|
||||||
font: 700 11px 'Rajdhani', sans-serif;
|
font: 700 11px 'Rajdhani', sans-serif;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
@@ -91,7 +98,6 @@ footer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bottom copyright bar
|
|
||||||
.footer-copy {
|
.footer-copy {
|
||||||
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
padding: 16px 60px;
|
padding: 16px 60px;
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
header {
|
header {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
.hero--compact {
|
.hero--compact {
|
||||||
min-height: unset;
|
min-height: unset;
|
||||||
padding: 36px 60px 48px;
|
padding: 36px 60px 48px;
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
.hero {
|
.hero {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
|||||||
@@ -210,11 +210,43 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&--best {
|
&--bonus {
|
||||||
border-color: rgba(255, 215, 0, 0.15);
|
border-color: rgba(255, 215, 0, 0.18);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
border-color: rgba(255, 215, 0, 0.4);
|
border-color: rgba(255, 215, 0, 0.45);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--avg-bonus {
|
||||||
|
border-color: rgba(230, 184, 60, 0.18);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: rgba(230, 184, 60, 0.45);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--chain {
|
||||||
|
border-color: rgba(94, 232, 154, 0.15);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: rgba(94, 232, 154, 0.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--blind {
|
||||||
|
border-color: rgba(255, 140, 90, 0.15);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: rgba(255, 140, 90, 0.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--edge {
|
||||||
|
border-color: rgba(168, 210, 255, 0.15);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: rgba(168, 210, 255, 0.4);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -248,8 +280,24 @@
|
|||||||
color: rgba(80, 200, 220, 0.35);
|
color: rgba(80, 200, 220, 0.35);
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-stat--best & {
|
.profile-stat--bonus & {
|
||||||
color: rgba(255, 215, 0, 0.3);
|
color: rgba(255, 215, 0, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-stat--avg-bonus & {
|
||||||
|
color: rgba(230, 184, 60, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-stat--chain & {
|
||||||
|
color: rgba(94, 232, 154, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-stat--blind & {
|
||||||
|
color: rgba(255, 140, 90, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-stat--edge & {
|
||||||
|
color: rgba(168, 210, 255, 0.3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,9 +337,25 @@
|
|||||||
color: #50c8dc;
|
color: #50c8dc;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-stat--best & {
|
.profile-stat--bonus & {
|
||||||
color: #ffd700;
|
color: #ffd700;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.profile-stat--avg-bonus & {
|
||||||
|
color: #e6b83c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-stat--chain & {
|
||||||
|
color: #5ee89a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-stat--blind & {
|
||||||
|
color: #ff8c5a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-stat--edge & {
|
||||||
|
color: #a8d2ff;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-stat__label {
|
.profile-stat__label {
|
||||||
@@ -363,15 +427,93 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.profile-games__filter-wrap {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-games__filter-icon {
|
||||||
|
position: absolute;
|
||||||
|
left: 14px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(149, 207, 245, 0.4);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-games__filter {
|
||||||
|
width: 100%;
|
||||||
|
background: rgba(255, 255, 255, 0.025);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.07);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 9px 14px 9px 36px;
|
||||||
|
font: 500 13px 'Rajdhani', sans-serif;
|
||||||
|
color: #fff;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
transition: border-color 200ms ease, background 200ms ease;
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
background: rgba(255, 255, 255, 0.045);
|
||||||
|
border-color: rgba(35, 111, 135, 0.55);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.profile-games {
|
.profile-games {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
|
|
||||||
|
&.is-filtering + .profile-games__load-more {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-filtering .profile-game--hidden:not(.profile-game--filtered-out) {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-game--filtered-out {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__load-more {
|
||||||
|
align-self: center;
|
||||||
|
margin-top: 14px;
|
||||||
|
background: rgba(35, 111, 135, 0.12);
|
||||||
|
color: rgba(149, 207, 245, 0.75);
|
||||||
|
border: 1px solid rgba(35, 111, 135, 0.3);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 9px 20px;
|
||||||
|
font: 600 12px 'Rajdhani', sans-serif;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1.5px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
transition: background 200ms ease, border-color 200ms ease, color 200ms ease;
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 11px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(35, 111, 135, 0.22);
|
||||||
|
border-color: rgba(35, 111, 135, 0.55);
|
||||||
|
color: rgba(149, 207, 245, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-game {
|
.profile-game {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 26px 76px 22px 1fr 18px auto;
|
grid-template-columns: 60px 76px 22px 1fr 18px auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding: 11px 16px;
|
padding: 11px 16px;
|
||||||
@@ -400,17 +542,31 @@
|
|||||||
&--draw {
|
&--draw {
|
||||||
border-left-color: rgba(149, 207, 245, 0.25);
|
border-left-color: rgba(149, 207, 245, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--ongoing {
|
||||||
|
border-left-color: rgba(255, 193, 7, 0.4);
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-game__badge {
|
.profile-game__badge {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 20px;
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font: 800 10px 'Rajdhani', sans-serif;
|
font: 800 10px 'Rajdhani', sans-serif;
|
||||||
letter-spacing: 0;
|
letter-spacing: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
.profile-game--win & {
|
.profile-game--win & {
|
||||||
background: rgba(42, 158, 96, 0.18);
|
background: rgba(42, 158, 96, 0.18);
|
||||||
@@ -426,12 +582,49 @@
|
|||||||
background: rgba(149, 207, 245, 0.1);
|
background: rgba(149, 207, 245, 0.1);
|
||||||
color: rgba(149, 207, 245, 0.65);
|
color: rgba(149, 207, 245, 0.65);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.profile-game--ongoing & {
|
||||||
|
background: rgba(255, 193, 7, 0.12);
|
||||||
|
color: #ffc107;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
border-top-color: #ffc107;
|
||||||
|
border-right-color: #ffc107;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-game--abandoned & {
|
||||||
|
background: rgba(107, 114, 126, 0.18);
|
||||||
|
color: #6b727e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-game__score {
|
.profile-game__score {
|
||||||
font: 700 14px 'Rajdhani', sans-serif;
|
font: 700 14px 'Rajdhani', sans-serif;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
letter-spacing: 1px;
|
letter-spacing: 1px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-game__vs {
|
.profile-game__vs {
|
||||||
@@ -461,21 +654,23 @@
|
|||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-charts {
|
.profile-charts {
|
||||||
display: flex;
|
display: grid;
|
||||||
flex-wrap: wrap;
|
grid-template-columns: 1fr 1fr;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-chart-block {
|
.profile-chart-block {
|
||||||
flex: 1 1 300px;
|
min-width: 0;
|
||||||
background: rgba(255, 255, 255, 0.03);
|
background: rgba(255, 255, 255, 0.03);
|
||||||
border: 1px solid rgba(35, 111, 135, 0.2);
|
border: 1px solid rgba(35, 111, 135, 0.2);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
padding: 24px 20px 16px;
|
padding: 24px 20px 16px;
|
||||||
backdrop-filter: blur(4px);
|
|
||||||
box-shadow: 0 8px 48px rgba(0, 0, 0, 0.4);
|
box-shadow: 0 8px 48px rgba(0, 0, 0, 0.4);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -484,12 +679,25 @@
|
|||||||
.profile-section__title {
|
.profile-section__title {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--wide {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
|
||||||
|
.profile-chart-inner {
|
||||||
|
justify-content: stretch;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-chart-inner {
|
.profile-chart-inner {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
overflow: auto;
|
overflow: hidden;
|
||||||
|
|
||||||
svg text {
|
svg text {
|
||||||
font-family: 'Rajdhani', sans-serif !important;
|
font-family: 'Rajdhani', sans-serif !important;
|
||||||
@@ -564,6 +772,32 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bd-continue {
|
||||||
|
background: linear-gradient(135deg, rgba(42, 158, 96, 0.35) 0%, rgba(94, 232, 154, 0.35) 100%);
|
||||||
|
border: 1px solid rgba(94, 232, 154, 0.6);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #5ee89a;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font: 700 11px 'Rajdhani', sans-serif;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1.5px;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 180ms ease;
|
||||||
|
white-space: nowrap;
|
||||||
|
box-shadow: 0 0 14px rgba(94, 232, 154, 0.25);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: linear-gradient(135deg, rgba(42, 158, 96, 0.55) 0%, rgba(94, 232, 154, 0.55) 100%);
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 0 20px rgba(94, 232, 154, 0.45);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.bd-close {
|
.bd-close {
|
||||||
background: rgba(255, 255, 255, 0.05);
|
background: rgba(255, 255, 255, 0.05);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
@@ -747,6 +981,13 @@
|
|||||||
font: 800 24px 'Rajdhani', sans-serif;
|
font: 800 24px 'Rajdhani', sans-serif;
|
||||||
letter-spacing: 2px;
|
letter-spacing: 2px;
|
||||||
|
|
||||||
|
&__img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
&--red {
|
&--red {
|
||||||
background: linear-gradient(135deg, rgba(173, 10, 5, 0.6) 0%, rgba(246, 125, 82, 0.4) 100%);
|
background: linear-gradient(135deg, rgba(173, 10, 5, 0.6) 0%, rgba(246, 125, 82, 0.4) 100%);
|
||||||
border: 2px solid rgba(173, 10, 5, 0.5);
|
border: 2px solid rgba(173, 10, 5, 0.5);
|
||||||
@@ -852,6 +1093,104 @@
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bshare-bonus {
|
||||||
|
padding: 28px 28px 0;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bshare-bonus__title {
|
||||||
|
font: 700 13px 'Rajdhani', sans-serif;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
color: #ffd700;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
i { font-size: 14px; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.bshare-bonus__grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 24px;
|
||||||
|
margin-bottom: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bshare-bonus__player {
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
background: rgba(255, 255, 255, 0.02);
|
||||||
|
|
||||||
|
&--red {
|
||||||
|
border-color: rgba(246, 125, 82, 0.15);
|
||||||
|
background: rgba(246, 125, 82, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--blue {
|
||||||
|
border-color: rgba(149, 207, 245, 0.15);
|
||||||
|
background: rgba(149, 207, 245, 0.04);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bshare-bonus__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bshare-bonus__points {
|
||||||
|
font: 700 24px 'Rajdhani', sans-serif;
|
||||||
|
background: linear-gradient(135deg, #ffd700, #ffed4e);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bshare-bonus__label {
|
||||||
|
font: 700 11px 'Rajdhani', sans-serif;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
color: rgba(255, 215, 0, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bshare-bonus__stats {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bshare-bonus__stat {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 12px;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bshare-bonus__stat-label {
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
font: 500 11px 'Rajdhani', sans-serif;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bshare-bonus__stat-value {
|
||||||
|
font: 700 13px 'Rajdhani', sans-serif;
|
||||||
|
color: rgba(255, 215, 0, 0.9);
|
||||||
|
min-width: 24px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bshare-bonus__stat--empty {
|
||||||
|
color: rgba(255, 255, 255, 0.4);
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
.bshare-btn {
|
.bshare-btn {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
* {
|
* {
|
||||||
outline: none;
|
outline: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
@media screen and (max-width: 900px) {
|
@media screen and (max-width: 900px) {
|
||||||
.hero h1 {
|
.hero h1 {
|
||||||
font-size: 44px;
|
font-size: 44px;
|
||||||
@@ -24,6 +33,10 @@
|
|||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.profile-charts {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
.profile-header {
|
.profile-header {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
main {
|
main {
|
||||||
background: #07090d;
|
background: #07090d;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
.back-from-game {
|
.back-from-game {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
html {
|
html {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -17,7 +26,6 @@ main {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mine-container {
|
.mine-container {
|
||||||
background: url("/images/bg-mineseeker-0-outbg.jpg") no-repeat;
|
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
#mine-wrapper .game-wrapper .users .user-container .user-control {
|
#mine-wrapper .game-wrapper .users .user-container .user-control {
|
||||||
background: -moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.5) 0%, rgba(125, 185, 232, 0) 100%);
|
background: -moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.5) 0%, rgba(125, 185, 232, 0) 100%);
|
||||||
background: -webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.5) 0%, rgba(125, 185, 232, 0) 100%);
|
background: -webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.5) 0%, rgba(125, 185, 232, 0) 100%);
|
||||||
|
|||||||
250
assets/css/mineseeker/_bonus-box.scss
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#mine-wrapper .bonus-box {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
min-width: 58px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
background: #07090d;
|
||||||
|
font-family: 'Rajdhani', sans-serif;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.25s ease;
|
||||||
|
align-self: stretch;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
filter: brightness(1.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .bonus-box.red-bonus {
|
||||||
|
background: linear-gradient(to bottom, #2a0502 0%, #4a1510 100%);
|
||||||
|
border-color: rgba(246, 125, 82, 0.4);
|
||||||
|
color: rgba(246, 125, 82, 0.85);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: rgba(246, 125, 82, 0.85);
|
||||||
|
box-shadow: 0 0 12px rgba(173, 10, 5, 0.6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .bonus-box.blue-bonus {
|
||||||
|
background: linear-gradient(to bottom, #050f18 0%, #0f2838 100%);
|
||||||
|
border-color: rgba(149, 207, 245, 0.4);
|
||||||
|
color: rgba(149, 207, 245, 0.85);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: rgba(149, 207, 245, 0.85);
|
||||||
|
box-shadow: 0 0 12px rgba(35, 111, 135, 0.6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .bonus-box__icon {
|
||||||
|
font-size: 13px;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .bonus-box__value {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 16px;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bsd {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font-family: 'Rajdhani', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bsd-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 18px 22px 14px;
|
||||||
|
border-bottom: 1px solid rgba(35, 111, 135, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bsd-header-text {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bsd-label {
|
||||||
|
font-size: 11px;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
color: rgba(149, 207, 245, 0.7);
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bsd-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
color: #fff;
|
||||||
|
|
||||||
|
.fa {
|
||||||
|
color: #f6d572;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bsd-close {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid rgba(35, 111, 135, 0.4);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #fff;
|
||||||
|
border-color: rgba(149, 207, 245, 0.8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bsd-body {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 14px;
|
||||||
|
padding: 18px 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bsd-column {
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 14px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
background: rgba(255, 255, 255, 0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bsd-column--red {
|
||||||
|
border-color: rgba(246, 125, 82, 0.35);
|
||||||
|
background: linear-gradient(to bottom, rgba(74, 6, 3, 0.35), rgba(107, 37, 21, 0.15));
|
||||||
|
}
|
||||||
|
|
||||||
|
.bsd-column--blue {
|
||||||
|
border-color: rgba(149, 207, 245, 0.35);
|
||||||
|
background: linear-gradient(to bottom, rgba(11, 37, 48, 0.35), rgba(22, 61, 85, 0.15));
|
||||||
|
}
|
||||||
|
|
||||||
|
.bsd-column-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bsd-column-name {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bsd-column-total {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #f6d572;
|
||||||
|
|
||||||
|
.fa {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bsd-stats {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bsd-stat {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 6px 2px;
|
||||||
|
border-bottom: 1px dashed rgba(255, 255, 255, 0.06);
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bsd-stat-text {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bsd-stat-label {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: rgba(255, 255, 255, 0.92);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bsd-stat-desc {
|
||||||
|
font-size: 11px;
|
||||||
|
color: rgba(255, 255, 255, 0.48);
|
||||||
|
line-height: 1.25;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bsd-stat-value {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #fff;
|
||||||
|
min-width: 24px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bsd-note {
|
||||||
|
margin: 0;
|
||||||
|
padding: 12px 22px 18px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(255, 255, 255, 0.45);
|
||||||
|
text-align: center;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 520px) {
|
||||||
|
.bsd-body {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
#mine-wrapper .grid {
|
#mine-wrapper .grid {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@@ -78,8 +87,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#mine-wrapper .grid .field-wrapper .field .field-corner {
|
#mine-wrapper .grid .field-wrapper .field .field-corner {
|
||||||
background: url('/images/bg-corner-outbg.png') no-repeat top left;
|
|
||||||
background-size: 100%;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
#mine-wrapper .game-wrapper .users .active-mines-container {
|
#mine-wrapper .game-wrapper .users .active-mines-container {
|
||||||
background: -moz-radial-gradient(center, ellipse cover, rgba(255, 252, 252, 1) 0%, rgba(255, 252, 252, 0.99) 1%, rgba(106, 106, 106, 0.39) 61%, rgba(106, 106, 106, 0) 100%);
|
background: -moz-radial-gradient(center, ellipse cover, rgba(255, 252, 252, 1) 0%, rgba(255, 252, 252, 0.99) 1%, rgba(106, 106, 106, 0.39) 61%, rgba(106, 106, 106, 0) 100%);
|
||||||
background: -webkit-radial-gradient(center, ellipse cover, rgba(255, 252, 252, 1) 0%, rgba(255, 252, 252, 0.99) 1%, rgba(106, 106, 106, 0.39) 61%, rgba(106, 106, 106, 0) 100%);
|
background: -webkit-radial-gradient(center, ellipse cover, rgba(255, 252, 252, 1) 0%, rgba(255, 252, 252, 0.99) 1%, rgba(106, 106, 106, 0.39) 61%, rgba(106, 106, 106, 0) 100%);
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
#mine-wrapper .game-wrapper .game-overlay {
|
#mine-wrapper .game-wrapper .game-overlay {
|
||||||
background: rgba(255, 255, 255, 0.2);
|
background: rgba(255, 255, 255, 0.2);
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -21,21 +30,23 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#mine-wrapper .game-wrapper .game-overlay .game-overlay-window {
|
#mine-wrapper .game-wrapper .game-overlay .game-overlay-window {
|
||||||
background: linear-gradient(135deg, rgba(7, 9, 13, 0.98) 0%, rgba(10, 20, 35, 0.98) 100%);
|
background: linear-gradient(135deg, rgba(7, 9, 13, 0.98) 0%, rgba(10, 20, 35, 0.98) 100%);
|
||||||
border: 2px solid rgba(35, 111, 135, 0.4);
|
border: 2px solid rgba(35, 111, 135, 0.4);
|
||||||
backdrop-filter: blur(12px);
|
backdrop-filter: blur(12px);
|
||||||
font-family: 'Rajdhani', sans-serif;
|
font-family: 'Rajdhani', sans-serif;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 680px;
|
max-width: 680px;
|
||||||
padding: 40px;
|
padding: 40px;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.7), 0 0 40px rgba(35, 111, 135, 0.15);
|
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.7), 0 0 40px rgba(35, 111, 135, 0.15);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0;
|
gap: 0;
|
||||||
animation: slideUp 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
|
animation: slideUp 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
}
|
overflow: hidden;
|
||||||
|
max-height: 90vh;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes slideUp {
|
@keyframes slideUp {
|
||||||
from {
|
from {
|
||||||
@@ -49,12 +60,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#mine-wrapper .game-wrapper .game-overlay .game-overlay-window h1 {
|
#mine-wrapper .game-wrapper .game-overlay .game-overlay-window h1 {
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
font-size: 32px;
|
font-size: 32px;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
margin: 0 0 50px 0;
|
margin: 0 0 50px 0;
|
||||||
letter-spacing: 1px;
|
letter-spacing: 1px;
|
||||||
}
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
#mine-wrapper .game-wrapper .game-overlay .game-overlay-window h2 {
|
#mine-wrapper .game-wrapper .game-overlay .game-overlay-window h2 {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@@ -183,6 +199,10 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
animation: fadeInUp 0.6s ease-out 0.2s both;
|
animation: fadeInUp 0.6s ease-out 0.2s both;
|
||||||
|
|
||||||
|
&.waiting-options--invite-only {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
@@ -259,12 +279,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.waiting-option-desc {
|
.waiting-option-desc {
|
||||||
font: 600 12px 'Rajdhani', sans-serif;
|
font: 600 12px 'Rajdhani', sans-serif;
|
||||||
color: rgba(149, 207, 245, 0.75);
|
color: rgba(149, 207, 245, 0.75);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
letter-spacing: 0.4px;
|
letter-spacing: 0.4px;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
.waiting-divider {
|
.waiting-divider {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -471,58 +496,285 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#mine-wrapper .game-wrapper .game-overlay .game-overlay-window .share-copy-btn {
|
#mine-wrapper .game-wrapper .game-overlay .game-overlay-window .share-copy-btn {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 9px;
|
||||||
|
background: linear-gradient(to bottom, #236f87 0%, #1a5068 100%);
|
||||||
|
border: 2px solid #2e7a9a;
|
||||||
|
color: #e0f4ff;
|
||||||
|
font-family: 'Rajdhani', sans-serif;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 4px 12px rgba(35, 111, 135, 0.25);
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -100%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||||
|
transition: left 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: linear-gradient(to bottom, #2d8aa8 0%, #236f87 100%);
|
||||||
|
border-color: #5ba4d4;
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 8px 24px rgba(35, 111, 135, 0.4);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.copied {
|
||||||
|
background: linear-gradient(to bottom, #1a6844 0%, #135233 100%);
|
||||||
|
border-color: #2a9e60;
|
||||||
|
color: #a0f0c0;
|
||||||
|
box-shadow: 0 4px 12px rgba(26, 104, 68, 0.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .game-overlay .game-overlay-window .game-overlay-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 20px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
flex: 1 1 0;
|
||||||
|
margin-top: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .game-overlay .game-overlay-window .game-overlay-share {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 9px;
|
||||||
|
background: linear-gradient(to bottom, #236f87 0%, #1a5068 100%);
|
||||||
|
border: 2px solid #2e7a9a;
|
||||||
|
color: #e0f4ff;
|
||||||
|
font-family: 'Rajdhani', sans-serif;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 20px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 4px 12px rgba(35, 111, 135, 0.25);
|
||||||
|
z-index: 10;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -100%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||||
|
transition: left 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: linear-gradient(to bottom, #2d8aa8 0%, #236f87 100%);
|
||||||
|
border-color: #5ba4d4;
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 8px 24px rgba(35, 111, 135, 0.4);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.copied {
|
||||||
|
background: linear-gradient(to bottom, #1a6844 0%, #135233 100%);
|
||||||
|
border-color: #2a9e60;
|
||||||
|
color: #a0f0c0;
|
||||||
|
box-shadow: 0 4px 12px rgba(26, 104, 68, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .game-overlay .game-overlay-window .game-overlay-profile {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 9px;
|
||||||
|
background: linear-gradient(to bottom, #1a6844 0%, #135233 100%);
|
||||||
|
border: 2px solid #2a9e60;
|
||||||
|
color: #d0ffe0;
|
||||||
|
font-family: 'Rajdhani', sans-serif;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 20px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 4px 12px rgba(42, 158, 96, 0.25);
|
||||||
|
text-decoration: none;
|
||||||
|
z-index: 10;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -100%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||||
|
transition: left 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: linear-gradient(to bottom, #238f5c 0%, #1a6844 100%);
|
||||||
|
border-color: #5ee89a;
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 8px 24px rgba(42, 158, 96, 0.4);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CaptchaOverlay Styles
|
||||||
|
.captcha-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(7, 9, 13, 0.95);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 9px;
|
z-index: 1000;
|
||||||
background: linear-gradient(to bottom, #236f87 0%, #1a5068 100%);
|
}
|
||||||
border: 2px solid #2e7a9a;
|
|
||||||
color: #e0f4ff;
|
|
||||||
font-family: 'Rajdhani', sans-serif;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 800;
|
|
||||||
letter-spacing: 1px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
padding: 12px 24px;
|
|
||||||
border-radius: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
||||||
width: 100%;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
box-shadow: 0 4px 12px rgba(35, 111, 135, 0.25);
|
|
||||||
|
|
||||||
&::before {
|
.captcha-content {
|
||||||
content: '';
|
text-align: center;
|
||||||
position: absolute;
|
color: #fff;
|
||||||
top: 0;
|
max-width: 400px;
|
||||||
left: -100%;
|
padding: 40px;
|
||||||
width: 100%;
|
}
|
||||||
height: 100%;
|
|
||||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
.captcha-icon {
|
||||||
transition: left 0.4s ease;
|
font-size: 64px;
|
||||||
|
color: #236f87;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-title {
|
||||||
|
font: 800 32px 'Rajdhani', sans-serif;
|
||||||
|
margin: 0 0 16px;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-description {
|
||||||
|
color: rgba(149, 207, 245, 0.7);
|
||||||
|
font: 400 16px 'Rajdhani', sans-serif;
|
||||||
|
margin: 0 0 32px;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-button {
|
||||||
|
background: linear-gradient(#236f87 0%, #1a5068 100%);
|
||||||
|
border: 2px solid #2e7a9a;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #e0f4ff;
|
||||||
|
cursor: pointer;
|
||||||
|
font: 800 18px 'Rajdhani', sans-serif;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
padding: 16px 40px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
opacity: 1;
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:disabled {
|
||||||
background: linear-gradient(to bottom, #2d8aa8 0%, #236f87 100%);
|
opacity: 0.7;
|
||||||
|
cursor: wait;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background: linear-gradient(#2d8aa8 0%, #236f87 100%);
|
||||||
border-color: #5ba4d4;
|
border-color: #5ba4d4;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
box-shadow: 0 8px 24px rgba(35, 111, 135, 0.4);
|
box-shadow: 0 8px 24px rgba(35, 111, 135, 0.4);
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
|
|
||||||
&::before {
|
|
||||||
left: 100%;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
&:active:not(:disabled) {
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.copied {
|
&.captcha-button--error {
|
||||||
background: linear-gradient(to bottom, #1a6844 0%, #135233 100%);
|
background: linear-gradient(#8a2323 0%, #681a1a 100%);
|
||||||
border-color: #2a9e60;
|
border-color: #9a2e2e;
|
||||||
color: #a0f0c0;
|
|
||||||
box-shadow: 0 4px 12px rgba(26, 104, 68, 0.4);
|
&:hover {
|
||||||
|
background: linear-gradient(#a82d2d 0%, #872323 100%);
|
||||||
|
border-color: #d45b5b;
|
||||||
|
box-shadow: 0 8px 24px rgba(135, 35, 35, 0.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.captcha-button--loading {
|
||||||
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
@media screen and (max-width: 900px) {
|
@media screen and (max-width: 900px) {
|
||||||
#mine-wrapper .game-wrapper .users {
|
#mine-wrapper .game-wrapper .users {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
#mine-wrapper .game-timer-container {
|
#mine-wrapper .game-timer-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
#mine-wrapper .game-wrapper .users {
|
#mine-wrapper .game-wrapper .users {
|
||||||
width: 180px;
|
width: 180px;
|
||||||
padding: 0 10px 0 0;
|
padding: 0 10px 0 0;
|
||||||
@@ -100,16 +109,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#mine-wrapper .game-wrapper .users .user-container .user-name {
|
#mine-wrapper .game-wrapper .users .user-container .user-name {
|
||||||
min-height: 30px;
|
min-height: 30px;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
padding: 3px 0;
|
padding: 3px 5px;
|
||||||
margin: 0 5px;
|
margin: 0;
|
||||||
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
word-break: break-word;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
#mine-wrapper .game-wrapper .users .user-container.user-blue .user-name {
|
#mine-wrapper .game-wrapper .users .user-container.user-blue .user-name {
|
||||||
border-top: 1px dashed #0b3776;
|
border-top: 1px dashed #0b3776;
|
||||||
@@ -139,10 +150,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#mine-wrapper .game-wrapper .users .user-container .user-desc {
|
#mine-wrapper .game-wrapper .users .user-container .user-desc {
|
||||||
height: 65px;
|
height: 65px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
word-break: break-word;
|
||||||
|
padding: 0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
#mine-wrapper .game-wrapper .users .user-container.user-blue .user-desc {
|
#mine-wrapper .game-wrapper .users .user-container.user-blue .user-desc {
|
||||||
color: #0b3776;
|
color: #0b3776;
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
.opd-paper {
|
.opd-paper {
|
||||||
background: #07090d !important;
|
background: #07090d !important;
|
||||||
background-image: linear-gradient(rgba(35, 111, 135, 0.08) 1px, transparent 1px),
|
background-image: linear-gradient(rgba(35, 111, 135, 0.08) 1px, transparent 1px),
|
||||||
@@ -305,3 +314,43 @@
|
|||||||
padding-top: 14px;
|
padding-top: 14px;
|
||||||
border-top: 1px solid rgba(35, 111, 135, 0.14);
|
border-top: 1px solid rgba(35, 111, 135, 0.14);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.opd-header-actions {
|
||||||
|
.opd-refresh[disabled] {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opd-close[disabled] {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.opd-waiting {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
background: rgba(35, 111, 135, 0.07);
|
||||||
|
border: 1px solid rgba(35, 111, 135, 0.28);
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #95cff5;
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 16px;
|
||||||
|
animation: opd-hourglass 1s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
font: 600 14px 'Rajdhani', sans-serif;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes opd-hourglass {
|
||||||
|
0%, 100% { transform: rotate(0deg); }
|
||||||
|
50% { transform: rotate(180deg); }
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
@use "sass:color";
|
@use "sass:color";
|
||||||
|
|
||||||
.twofa-status {
|
.twofa-status {
|
||||||
|
|||||||
@@ -1,9 +1,19 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2019 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
@use 'homepage/reset';
|
@use 'homepage/reset';
|
||||||
@use 'homepage/animations';
|
@use 'homepage/animations';
|
||||||
@use 'homepage/header';
|
@use 'homepage/header';
|
||||||
@use 'homepage/hero';
|
@use 'homepage/hero';
|
||||||
@use 'homepage/hero-compact';
|
@use 'homepage/hero-compact';
|
||||||
@use 'homepage/cta';
|
@use 'homepage/cta';
|
||||||
|
@use 'homepage/donate';
|
||||||
@use 'homepage/auth-bar';
|
@use 'homepage/auth-bar';
|
||||||
@use 'homepage/auth';
|
@use 'homepage/auth';
|
||||||
@use 'homepage/content';
|
@use 'homepage/content';
|
||||||
@@ -11,4 +21,5 @@
|
|||||||
@use 'homepage/tech';
|
@use 'homepage/tech';
|
||||||
@use 'homepage/footer';
|
@use 'homepage/footer';
|
||||||
@use 'homepage/profile';
|
@use 'homepage/profile';
|
||||||
|
@use 'homepage/battle-dialog';
|
||||||
@use 'homepage/responsive';
|
@use 'homepage/responsive';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/*!*
|
/*!*
|
||||||
* This file is part of the SplendidBear Websites' projects.
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
*
|
*
|
||||||
* Copyright (c) 2026 @ www.splendidbear.org
|
* Copyright (c) 2019 @ www.splendidbear.org
|
||||||
*
|
*
|
||||||
* For the full copyright and license information, please view the LICENSE
|
* For the full copyright and license information, please view the LICENSE
|
||||||
* file that was distributed with this source code.
|
* file that was distributed with this source code.
|
||||||
@@ -320,7 +320,6 @@ footer nav ul li {
|
|||||||
}
|
}
|
||||||
|
|
||||||
footer nav ul li:nth-child(even) {
|
footer nav ul li:nth-child(even) {
|
||||||
width: 50px;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -401,8 +400,4 @@ footer nav ul li a:hover {
|
|||||||
footer nav ul li {
|
footer nav ul li {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
footer nav ul li:nth-child(even) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/*!*
|
/*!*
|
||||||
* This file is part of the SplendidBear Websites' projects.
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
*
|
*
|
||||||
* Copyright (c) 2026 @ www.splendidbear.org
|
* Copyright (c) 2019 @ www.splendidbear.org
|
||||||
*
|
*
|
||||||
* For the full copyright and license information, please view the LICENSE
|
* For the full copyright and license information, please view the LICENSE
|
||||||
* file that was distributed with this source code.
|
* file that was distributed with this source code.
|
||||||
@@ -19,5 +19,6 @@
|
|||||||
@import 'mineseeker/grid';
|
@import 'mineseeker/grid';
|
||||||
@import 'mineseeker/back-button';
|
@import 'mineseeker/back-button';
|
||||||
@import 'mineseeker/timer';
|
@import 'mineseeker/timer';
|
||||||
|
@import 'mineseeker/bonus-box';
|
||||||
@import 'mineseeker/responsive';
|
@import 'mineseeker/responsive';
|
||||||
@import 'mineseeker/waiting-dialog';
|
@import 'mineseeker/waiting-dialog';
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2019 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
.mine-beta {
|
.mine-beta {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|||||||
@@ -17,5 +17,6 @@ createRoot(wrapper).render(
|
|||||||
<MineSeeker
|
<MineSeeker
|
||||||
env={wrapper.dataset.env}
|
env={wrapper.dataset.env}
|
||||||
gameId={wrapper.dataset.gameId}
|
gameId={wrapper.dataset.gameId}
|
||||||
|
opponentName={wrapper.dataset.opponentName || ''}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,36 +1,32 @@
|
|||||||
import React, { useRef, useState } from 'react';
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
export default function AvatarUpload({ uploadUrl, initialThumbUrl, initials }) {
|
import React, { useMemo, useRef } from 'react';
|
||||||
const [thumbUrl, setThumbUrl] = useState(initialThumbUrl || null);
|
import { string } from 'prop-types';
|
||||||
const [loading, setLoading] = useState(false);
|
import { useProfileDataProvider } from '@mine-hooks/useGameDataProvider';
|
||||||
const [error, setError] = useState(null);
|
|
||||||
|
export const AvatarUpload = ({ uploadUrl, initialThumbUrl, initials }) => {
|
||||||
const inputRef = useRef(null);
|
const inputRef = useRef(null);
|
||||||
|
const [thumbUrl, setThumbUrl] = React.useState(initialThumbUrl || null);
|
||||||
|
const { uploadAvatarMutation: { isPending, error, mutate } } = useProfileDataProvider();
|
||||||
|
|
||||||
function handleClick() {
|
const handleChange = e => {
|
||||||
inputRef.current?.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleChange(e) {
|
|
||||||
const file = e.target.files?.[0];
|
const file = e.target.files?.[0];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
|
||||||
const fd = new FormData();
|
mutate({ uploadUrl, file }, {
|
||||||
fd.append('avatar', file);
|
onSuccess: data => {
|
||||||
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
fetch(uploadUrl, { method: 'POST', body: fd })
|
|
||||||
.then(r => r.json())
|
|
||||||
.then(data => {
|
|
||||||
if (data.error) {
|
|
||||||
setError(data.error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setThumbUrl(data.thumbUrl);
|
setThumbUrl(data.thumbUrl);
|
||||||
|
|
||||||
const navImg = document.querySelector('.hero-auth-avatar:not(.hero-auth-avatar--initials)');
|
const navImg = document.querySelector('.hero-auth-avatar:not(.hero-auth-avatar--initials)');
|
||||||
const navInitials = document.querySelector('.hero-auth-avatar.hero-auth-avatar--initials');
|
const navInitials = document.querySelector('.hero-auth-avatar.hero-auth-avatar--initials');
|
||||||
|
|
||||||
if (navImg) {
|
if (navImg) {
|
||||||
navImg.src = data.thumbUrl;
|
navImg.src = data.thumbUrl;
|
||||||
} else if (navInitials) {
|
} else if (navInitials) {
|
||||||
@@ -40,16 +36,17 @@ export default function AvatarUpload({ uploadUrl, initialThumbUrl, initials }) {
|
|||||||
img.className = 'hero-auth-avatar';
|
img.className = 'hero-auth-avatar';
|
||||||
navInitials.replaceWith(img);
|
navInitials.replaceWith(img);
|
||||||
}
|
}
|
||||||
})
|
},
|
||||||
.catch(() => setError('Upload failed. Please try again.'))
|
});
|
||||||
.finally(() => setLoading(false));
|
};
|
||||||
}
|
|
||||||
|
const errorMessage = useMemo(() => error?.message ?? null, [error]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`profile-avatar${loading ? ' profile-avatar--loading' : ''}`}
|
className={`profile-avatar${isPending ? ' profile-avatar--loading' : ''}`}
|
||||||
title="Click to change profile picture"
|
title="Click to change profile picture"
|
||||||
onClick={handleClick}
|
onClick={() => inputRef.current?.click()}
|
||||||
>
|
>
|
||||||
{thumbUrl
|
{thumbUrl
|
||||||
? <img src={thumbUrl} alt={initials} className="profile-avatar__img" />
|
? <img src={thumbUrl} alt={initials} className="profile-avatar__img" />
|
||||||
@@ -65,7 +62,13 @@ export default function AvatarUpload({ uploadUrl, initialThumbUrl, initials }) {
|
|||||||
style={{ display: 'none' }}
|
style={{ display: 'none' }}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
{error && <div className="profile-avatar__error">{error}</div>}
|
{errorMessage && <div className="profile-avatar__error">{errorMessage}</div>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AvatarUpload.propTypes = {
|
||||||
|
uploadUrl: string.isRequired,
|
||||||
|
initialThumbUrl: string,
|
||||||
|
initials: string.isRequired,
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,10 +1,204 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { array } from 'prop-types';
|
||||||
|
import { formatDuration } from '@global-utils/format';
|
||||||
import Dialog from '@mui/material/Dialog';
|
import Dialog from '@mui/material/Dialog';
|
||||||
import { createTheme, ThemeProvider } from '@mui/material/styles';
|
import { createTheme, styled, ThemeProvider } from '@mui/material/styles';
|
||||||
|
import { Avatar, BonusPoints, StatRow } from '@global-components';
|
||||||
|
|
||||||
const darkTheme = createTheme({ palette: { mode: 'dark' } });
|
const darkTheme = createTheme({ palette: { mode: 'dark' } });
|
||||||
|
|
||||||
const DIALOG_SX = {
|
const RESULT_META = {
|
||||||
|
win: {
|
||||||
|
label: 'Victory',
|
||||||
|
color: '#5ee89a',
|
||||||
|
bg: 'rgba(42,158,96,0.15)',
|
||||||
|
border: 'rgba(42,158,96,0.4)',
|
||||||
|
icon: 'fa-trophy',
|
||||||
|
},
|
||||||
|
loss: {
|
||||||
|
label: 'Defeated',
|
||||||
|
color: '#f67d52',
|
||||||
|
bg: 'rgba(173,10,5,0.15)',
|
||||||
|
border: 'rgba(173,10,5,0.4)',
|
||||||
|
icon: 'fa-flag',
|
||||||
|
},
|
||||||
|
draw: {
|
||||||
|
label: 'Draw',
|
||||||
|
color: '#95cff5',
|
||||||
|
bg: 'rgba(149,207,245,0.1)',
|
||||||
|
border: 'rgba(149,207,245,0.3)',
|
||||||
|
icon: 'fa-minus',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BattleDialog = ({ games }) => {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [game, setGame] = useState(null);
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = e => {
|
||||||
|
const row = e.target.closest('[data-game-index]');
|
||||||
|
if (!row) return;
|
||||||
|
const idx = parseInt(row.dataset.gameIndex, 10);
|
||||||
|
if (!isNaN(idx) && games[idx]) {
|
||||||
|
setGame(games[idx]);
|
||||||
|
setOpen(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener('click', handler);
|
||||||
|
return () => document.removeEventListener('click', handler);
|
||||||
|
}, [games]);
|
||||||
|
|
||||||
|
if (!game) {
|
||||||
|
return <ThemeProvider theme={darkTheme}><StyledDialog open={false} /></ThemeProvider>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const meta = RESULT_META[game.result] ?? RESULT_META.draw;
|
||||||
|
const resign = game.resign;
|
||||||
|
const maxPoints = Math.max(game.redPoints ?? 0, game.bluePoints ?? 0);
|
||||||
|
const endReason = resign
|
||||||
|
? `${resign.charAt(0).toUpperCase() + resign.slice(1)} resigned`
|
||||||
|
: 26 <= maxPoints ? 'Points' : 'Abandoned';
|
||||||
|
const canShare = !canContinue;
|
||||||
|
const bothRegistered = game.bothRegistered;
|
||||||
|
const canContinue = bothRegistered && !resign && 26 > maxPoints;
|
||||||
|
const playUrl = `${window.location.origin}/play/${game.uuid}`;
|
||||||
|
const shareUrl = `${window.location.origin}/battle/${game.uuid}`;
|
||||||
|
|
||||||
|
const duration = formatDuration(game.created, game.date);
|
||||||
|
const pointDiff = Math.abs((game.redPoints ?? 0) - (game.bluePoints ?? 0));
|
||||||
|
const winnerColor = (game.redPoints ?? 0) > (game.bluePoints ?? 0) ? '#f67d52'
|
||||||
|
: (game.bluePoints ?? 0) > (game.redPoints ?? 0) ? '#95cff5'
|
||||||
|
: 'rgba(255,255,255,0.45)';
|
||||||
|
|
||||||
|
const handleShare = () => {
|
||||||
|
navigator.clipboard.writeText(shareUrl).then(() => {
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2200);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeProvider theme={darkTheme}>
|
||||||
|
<StyledDialog open={open} onClose={() => setOpen(false)}>
|
||||||
|
<div className="bd">
|
||||||
|
<div className="bd-header">
|
||||||
|
<div className="bd-header-left">
|
||||||
|
<span className="bd-label">Battle Report</span>
|
||||||
|
<h2 className="bd-title">
|
||||||
|
<i className="fa fa-crosshairs" /> Match Details
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="bd-header-actions">
|
||||||
|
{canContinue ? (
|
||||||
|
<a
|
||||||
|
className="bd-continue"
|
||||||
|
href={playUrl}
|
||||||
|
aria-label="Continue the game"
|
||||||
|
title="Continue the game"
|
||||||
|
>
|
||||||
|
<i className="fa fa-play" />
|
||||||
|
Continue
|
||||||
|
</a>
|
||||||
|
) : canShare ? (
|
||||||
|
<button
|
||||||
|
className={`bd-share${copied ? ' bd-share--copied' : ''}`}
|
||||||
|
onClick={handleShare}
|
||||||
|
aria-label="Copy share link"
|
||||||
|
title="Copy share link"
|
||||||
|
>
|
||||||
|
<i className={`fa ${copied ? 'fa-check' : 'fa-share-alt'}`} />
|
||||||
|
{copied ? 'Copied!' : 'Share'}
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
<button className="bd-close" onClick={() => setOpen(false)} aria-label="Close">
|
||||||
|
<i className="fa fa-times" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bd-vs-panel">
|
||||||
|
<Avatar
|
||||||
|
name={game.redName} color="red" avatarUrl={game.redAvatar}
|
||||||
|
bonusPoints={game.redBonusPoints > game.blueBonusPoints ? game.redBonusPoints : 0}
|
||||||
|
/>
|
||||||
|
<div className="bd-vs-center">
|
||||||
|
<div className="bd-vs-score">
|
||||||
|
<span className="bd-vs-score__red">{game.redPoints ?? '—'}</span>
|
||||||
|
<span className="bd-vs-score__sep">:</span>
|
||||||
|
<span className="bd-vs-score__blue">{game.bluePoints ?? '—'}</span>
|
||||||
|
</div>
|
||||||
|
<div className="bd-vs-score bd-bonus-score">
|
||||||
|
<span className="bd-bonus-score__red">
|
||||||
|
<i className="fa fa-star" /> {(game.redBonusPoints ?? 0).toFixed(1)}
|
||||||
|
</span>
|
||||||
|
<span className="bd-vs-score__sep">:</span>
|
||||||
|
<span className="bd-bonus-score__blue">
|
||||||
|
{(game.blueBonusPoints ?? 0).toFixed(1)} <i className="fa fa-star" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="bd-vs-label">VS</div>
|
||||||
|
<div
|
||||||
|
className="bd-result-badge"
|
||||||
|
style={{ '--bd-result-bg': meta.bg, '--bd-result-border': meta.border, '--bd-result-color': meta.color }}
|
||||||
|
>
|
||||||
|
<i className={`fa ${meta.icon}`} /> {meta.label}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Avatar
|
||||||
|
name={game.blueName} color="blue" avatarUrl={game.blueAvatar}
|
||||||
|
bonusPoints={game.blueBonusPoints > game.redBonusPoints ? game.blueBonusPoints : 0}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="bd-stats">
|
||||||
|
<StatRow icon="fa-calendar" label="Date" value={game.date ?? '—'} />
|
||||||
|
{game.created && game.date && game.created !== game.date && (
|
||||||
|
<StatRow icon="fa-clock" label="Started" value={game.created} />
|
||||||
|
)}
|
||||||
|
{duration && (
|
||||||
|
<StatRow icon="fa-hourglass-half" label="Match duration" value={duration} />
|
||||||
|
)}
|
||||||
|
<StatRow icon="fa-flag-checkered" label="End reason" value={endReason} />
|
||||||
|
{0 < pointDiff && (
|
||||||
|
<StatRow
|
||||||
|
icon="fa-balance-scale" label="Winning margin"
|
||||||
|
value={`${pointDiff} mine${1 === pointDiff ? '' : 's'}`} valueColor={winnerColor}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<StatRow
|
||||||
|
icon="fa-bomb" label="Red used bomb"
|
||||||
|
value={game.redExplodedBomb ? 'Yes' : 'No'}
|
||||||
|
valueColor={game.redExplodedBomb ? '#f67d52' : 'rgba(255,255,255,0.45)'}
|
||||||
|
/>
|
||||||
|
<StatRow
|
||||||
|
icon="fa-bomb" label="Blue used bomb"
|
||||||
|
value={game.blueExplodedBomb ? 'Yes' : 'No'}
|
||||||
|
valueColor={game.blueExplodedBomb ? '#95cff5' : 'rgba(255,255,255,0.45)'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<BonusPoints
|
||||||
|
game={game}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</StyledDialog>
|
||||||
|
</ThemeProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
BattleDialog.propTypes = {
|
||||||
|
games: array.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledDialog = styled(Dialog)({
|
||||||
'& .MuiDialog-paper': {
|
'& .MuiDialog-paper': {
|
||||||
background: '#07090d',
|
background: '#07090d',
|
||||||
backgroundImage: `
|
backgroundImage: `
|
||||||
@@ -24,215 +218,4 @@ const DIALOG_SX = {
|
|||||||
background: 'rgba(2, 4, 8, 0.88)',
|
background: 'rgba(2, 4, 8, 0.88)',
|
||||||
backdropFilter: 'blur(4px)',
|
backdropFilter: 'blur(4px)',
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
|
|
||||||
const RESULT_META = {
|
|
||||||
win: {
|
|
||||||
label: 'Victory',
|
|
||||||
color: '#5ee89a',
|
|
||||||
bg: 'rgba(42,158,96,0.15)',
|
|
||||||
border: 'rgba(42,158,96,0.4)',
|
|
||||||
icon: 'fa-trophy',
|
|
||||||
},
|
|
||||||
loss: {
|
|
||||||
label: 'Defeat',
|
|
||||||
color: '#f67d52',
|
|
||||||
bg: 'rgba(173,10,5,0.15)',
|
|
||||||
border: 'rgba(173,10,5,0.4)',
|
|
||||||
icon: 'fa-flag',
|
|
||||||
},
|
|
||||||
draw: {
|
|
||||||
label: 'Draw',
|
|
||||||
color: '#95cff5',
|
|
||||||
bg: 'rgba(149,207,245,0.1)',
|
|
||||||
border: 'rgba(149,207,245,0.3)',
|
|
||||||
icon: 'fa-minus',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
function Avatar({ name, color }) {
|
|
||||||
const isRed = 'red' === color;
|
|
||||||
const initials = (name || '?').slice(0, 2).toUpperCase();
|
|
||||||
|
|
||||||
const gradient = isRed
|
|
||||||
? 'linear-gradient(135deg, rgba(173,10,5,0.6) 0%, rgba(246,125,82,0.4) 100%)'
|
|
||||||
: 'linear-gradient(135deg, rgba(35,111,135,0.6) 0%, rgba(41,128,185,0.4) 100%)';
|
|
||||||
const glow = isRed
|
|
||||||
? '0 0 0 3px rgba(173,10,5,0.2), 0 0 28px rgba(173,10,5,0.35)'
|
|
||||||
: '0 0 0 3px rgba(35,111,135,0.2), 0 0 28px rgba(35,111,135,0.35)';
|
|
||||||
const border = isRed
|
|
||||||
? 'rgba(173,10,5,0.5)'
|
|
||||||
: 'rgba(35,111,135,0.5)';
|
|
||||||
const textColor = isRed ? '#f67d52' : '#95cff5';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 10 }}>
|
|
||||||
<div style={{
|
|
||||||
width: 72, height: 72, borderRadius: '50%',
|
|
||||||
background: gradient,
|
|
||||||
border: `2px solid ${border}`,
|
|
||||||
boxShadow: glow,
|
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
||||||
font: '800 24px \'Rajdhani\', sans-serif',
|
|
||||||
color: textColor,
|
|
||||||
letterSpacing: 2,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{initials}
|
|
||||||
</div>
|
|
||||||
<span style={{
|
|
||||||
font: '700 15px \'Rajdhani\', sans-serif',
|
|
||||||
color: textColor,
|
|
||||||
letterSpacing: 1,
|
|
||||||
maxWidth: 120, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
|
|
||||||
textAlign: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{name}
|
|
||||||
</span>
|
|
||||||
<span style={{
|
|
||||||
font: '600 10px \'Rajdhani\', sans-serif',
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
letterSpacing: 2,
|
|
||||||
color: 'rgba(255,255,255,0.3)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{isRed ? 'Red' : 'Blue'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function StatRow({ icon, label, value, valueColor }) {
|
|
||||||
return (
|
|
||||||
<div style={{
|
|
||||||
display: 'flex', alignItems: 'center',
|
|
||||||
gap: 10, padding: '9px 0',
|
|
||||||
borderBottom: '1px solid rgba(255,255,255,0.05)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<i className={`fa ${icon}`} style={{ width: 16, color: 'rgba(149,207,245,0.4)', fontSize: 13 }} />
|
|
||||||
<span style={{
|
|
||||||
font: '500 13px \'Rajdhani\', sans-serif',
|
|
||||||
color: 'rgba(255,255,255,0.45)',
|
|
||||||
flex: 1,
|
|
||||||
letterSpacing: 0.5,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</span>
|
|
||||||
<span style={{
|
|
||||||
font: '700 13px \'Rajdhani\', sans-serif',
|
|
||||||
color: valueColor || 'rgba(255,255,255,0.75)',
|
|
||||||
letterSpacing: 0.5,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{value}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function BattleDialog({ games }) {
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const [game, setGame] = useState(null);
|
|
||||||
const [copied, setCopied] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handler = e => {
|
|
||||||
const row = e.target.closest('[data-game-index]');
|
|
||||||
if (!row) return;
|
|
||||||
const idx = parseInt(row.dataset.gameIndex, 10);
|
|
||||||
if (!isNaN(idx) && games[idx]) {
|
|
||||||
setGame(games[idx]);
|
|
||||||
setOpen(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
document.addEventListener('click', handler);
|
|
||||||
return () => document.removeEventListener('click', handler);
|
|
||||||
}, [games]);
|
|
||||||
|
|
||||||
if (!game) {
|
|
||||||
return <ThemeProvider theme={darkTheme}><Dialog open={false} sx={DIALOG_SX} /></ThemeProvider>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const meta = RESULT_META[game.result] ?? RESULT_META.draw;
|
|
||||||
const resign = game.resign;
|
|
||||||
const endReason = resign
|
|
||||||
? `${resign.charAt(0).toUpperCase() + resign.slice(1)} resigned`
|
|
||||||
: 'Points';
|
|
||||||
const shareUrl = `${window.location.origin}/battle/${game.id}`;
|
|
||||||
|
|
||||||
const handleShare = () => {
|
|
||||||
navigator.clipboard.writeText(shareUrl).then(() => {
|
|
||||||
setCopied(true);
|
|
||||||
setTimeout(() => setCopied(false), 2200);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ThemeProvider theme={darkTheme}>
|
|
||||||
<Dialog open={open} onClose={() => setOpen(false)} sx={DIALOG_SX}>
|
|
||||||
<div className="bd">
|
|
||||||
<div className="bd-header">
|
|
||||||
<div className="bd-header-left">
|
|
||||||
<span className="bd-label">Battle Report</span>
|
|
||||||
<h2 className="bd-title">
|
|
||||||
<i className="fa fa-crosshairs" /> Match Details
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<div style={{ display: 'flex', gap: 8 }}>
|
|
||||||
<button
|
|
||||||
className={`bd-share${copied ? ' bd-share--copied' : ''}`}
|
|
||||||
onClick={handleShare}
|
|
||||||
aria-label="Copy share link"
|
|
||||||
title="Copy share link"
|
|
||||||
>
|
|
||||||
<i className={`fa ${copied ? 'fa-check' : 'fa-share-alt'}`} />
|
|
||||||
{copied ? 'Copied!' : 'Share'}
|
|
||||||
</button>
|
|
||||||
<button className="bd-close" onClick={() => setOpen(false)} aria-label="Close">
|
|
||||||
<i className="fa fa-times" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="bd-vs-panel">
|
|
||||||
<Avatar name={game.redName} color="red" />
|
|
||||||
<div className="bd-vs-center">
|
|
||||||
<div className="bd-vs-score">
|
|
||||||
<span className="bd-vs-score__red">{game.redPoints ?? '—'}</span>
|
|
||||||
<span className="bd-vs-score__sep">:</span>
|
|
||||||
<span className="bd-vs-score__blue">{game.bluePoints ?? '—'}</span>
|
|
||||||
</div>
|
|
||||||
<div className="bd-vs-label">VS</div>
|
|
||||||
<div
|
|
||||||
className="bd-result-badge"
|
|
||||||
style={{ background: meta.bg, border: `1px solid ${meta.border}`, color: meta.color }}
|
|
||||||
>
|
|
||||||
<i className={`fa ${meta.icon}`} /> {meta.label}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Avatar name={game.blueName} color="blue" />
|
|
||||||
</div>
|
|
||||||
<div className="bd-stats">
|
|
||||||
<StatRow icon="fa-calendar" label="Date" value={game.date ?? '—'} />
|
|
||||||
<StatRow icon="fa-flag-checkered" label="End reason" value={endReason} />
|
|
||||||
<StatRow
|
|
||||||
icon="fa-bomb" label="Red hit a mine"
|
|
||||||
value={game.redExplodedBomb ? 'Yes' : 'No'}
|
|
||||||
valueColor={game.redExplodedBomb ? '#f67d52' : 'rgba(255,255,255,0.45)'}
|
|
||||||
/>
|
|
||||||
<StatRow
|
|
||||||
icon="fa-bomb" label="Blue hit a mine"
|
|
||||||
value={game.blueExplodedBomb ? 'Yes' : 'No'}
|
|
||||||
valueColor={game.blueExplodedBomb ? '#f67d52' : 'rgba(255,255,255,0.45)'}
|
|
||||||
/>
|
|
||||||
{game.created && game.date && game.created !== game.date && (
|
|
||||||
<StatRow icon="fa-clock-o" label="Started" value={game.created} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
</ThemeProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
89
assets/js/components/ContactForm.jsx
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
import { string } from 'prop-types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ContactForm Component
|
||||||
|
*
|
||||||
|
* Handles reCAPTCHA v3 integration for the contact form.
|
||||||
|
* Intercepts form submission, executes reCAPTCHA, and submits the form with the token.
|
||||||
|
*
|
||||||
|
* @param {string} siteKey - Google reCAPTCHA site key
|
||||||
|
* @param {string} recaptchaFieldId - ID of the hidden recaptcha input field
|
||||||
|
*/
|
||||||
|
const ContactForm = ({ siteKey, recaptchaFieldId }) => {
|
||||||
|
const formRef = useRef(null);
|
||||||
|
const isSubmittingRef = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const form = document.querySelector('.auth-form');
|
||||||
|
|
||||||
|
if (!form) {
|
||||||
|
console.warn('ContactForm: No .auth-form found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
formRef.current = form;
|
||||||
|
|
||||||
|
const handleSubmit = e => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (isSubmittingRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isSubmittingRef.current = true;
|
||||||
|
|
||||||
|
if ('undefined' !== typeof grecaptcha) {
|
||||||
|
grecaptcha.ready(() => {
|
||||||
|
grecaptcha
|
||||||
|
.execute(siteKey, { action: 'contact' })
|
||||||
|
.then(token => {
|
||||||
|
const recaptchaField = document.getElementById(recaptchaFieldId);
|
||||||
|
|
||||||
|
if (recaptchaField) {
|
||||||
|
recaptchaField.value = token;
|
||||||
|
} else {
|
||||||
|
console.error(`ContactForm: Recaptcha field with ID "${recaptchaFieldId}" not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
isSubmittingRef.current = false;
|
||||||
|
form.submit();
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('ContactForm: reCAPTCHA execution failed', error);
|
||||||
|
isSubmittingRef.current = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error('ContactForm: grecaptcha is not loaded');
|
||||||
|
isSubmittingRef.current = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
form.addEventListener('submit', handleSubmit);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (formRef.current) {
|
||||||
|
formRef.current.removeEventListener('submit', handleSubmit);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [siteKey, recaptchaFieldId]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
ContactForm.propTypes = {
|
||||||
|
siteKey: string.isRequired,
|
||||||
|
recaptchaFieldId: string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ContactForm;
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useCallback } from 'react';
|
import React, { useState, useCallback } from 'react';
|
||||||
|
import { shape, string } from 'prop-types';
|
||||||
|
|
||||||
const base64ToArrayBuffer = base64 => {
|
const base64ToArrayBuffer = base64 => {
|
||||||
const binary = atob(base64.replace(/([-_])/g, m => ('-' === m ? '+' : '/')));
|
const binary = atob(base64.replace(/([-_])/g, m => ('-' === m ? '+' : '/')));
|
||||||
@@ -108,3 +109,10 @@ const PasskeyLogin = ({ apiRoutes }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default PasskeyLogin;
|
export default PasskeyLogin;
|
||||||
|
|
||||||
|
PasskeyLogin.propTypes = {
|
||||||
|
apiRoutes: shape({
|
||||||
|
authenticationBegin: string.isRequired,
|
||||||
|
authenticationComplete: string.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
};
|
||||||
|
|||||||
@@ -9,13 +9,15 @@
|
|||||||
|
|
||||||
import React, { Fragment, useCallback, useEffect, useState } from 'react';
|
import React, { Fragment, useCallback, useEffect, useState } from 'react';
|
||||||
import Dialog from '@mui/material/Dialog';
|
import Dialog from '@mui/material/Dialog';
|
||||||
|
import { styled } from '@mui/material/styles';
|
||||||
import DialogTitle from '@mui/material/DialogTitle';
|
import DialogTitle from '@mui/material/DialogTitle';
|
||||||
import DialogContent from '@mui/material/DialogContent';
|
import DialogContent from '@mui/material/DialogContent';
|
||||||
import DialogActions from '@mui/material/DialogActions';
|
import DialogActions from '@mui/material/DialogActions';
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
import TextField from '@mui/material/TextField';
|
import TextField from '@mui/material/TextField';
|
||||||
|
import { arrayOf, shape, string, bool } from 'prop-types';
|
||||||
|
|
||||||
const DIALOG_SX = {
|
const StyledDialog = styled(Dialog)({
|
||||||
'& .MuiDialog-paper': {
|
'& .MuiDialog-paper': {
|
||||||
background: '#0a0e14',
|
background: '#0a0e14',
|
||||||
color: '#e0e0e0',
|
color: '#e0e0e0',
|
||||||
@@ -47,7 +49,7 @@ const DIALOG_SX = {
|
|||||||
background: 'rgba(2, 4, 8, 0.88)',
|
background: 'rgba(2, 4, 8, 0.88)',
|
||||||
backdropFilter: 'blur(4px)',
|
backdropFilter: 'blur(4px)',
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
|
|
||||||
const base64ToArrayBuffer = base64 => {
|
const base64ToArrayBuffer = base64 => {
|
||||||
const binary = atob(base64.replace(/([-_])/g, m => ('-' === m ? '+' : '/')));
|
const binary = atob(base64.replace(/([-_])/g, m => ('-' === m ? '+' : '/')));
|
||||||
@@ -314,7 +316,7 @@ const PasskeyManager = ({ credentials, apiRoutes }) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Dialog open={addModalOpen} onClose={closeAddModal} sx={DIALOG_SX}>
|
<StyledDialog open={addModalOpen} onClose={closeAddModal}>
|
||||||
<DialogTitle>Add New Passkey</DialogTitle>
|
<DialogTitle>Add New Passkey</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<TextField
|
<TextField
|
||||||
@@ -344,9 +346,9 @@ const PasskeyManager = ({ credentials, apiRoutes }) => {
|
|||||||
Continue
|
Continue
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</StyledDialog>
|
||||||
|
|
||||||
<Dialog open={renameModalOpen} onClose={closeRenameModal} sx={DIALOG_SX}>
|
<StyledDialog open={renameModalOpen} onClose={closeRenameModal}>
|
||||||
<DialogTitle>Rename Passkey</DialogTitle>
|
<DialogTitle>Rename Passkey</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<TextField
|
<TextField
|
||||||
@@ -374,9 +376,9 @@ const PasskeyManager = ({ credentials, apiRoutes }) => {
|
|||||||
Rename
|
Rename
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</StyledDialog>
|
||||||
|
|
||||||
<Dialog open={deleteModalOpen} onClose={closeDeleteModal} sx={DIALOG_SX}>
|
<StyledDialog open={deleteModalOpen} onClose={closeDeleteModal}>
|
||||||
<DialogTitle>Delete Passkey</DialogTitle>
|
<DialogTitle>Delete Passkey</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<p>
|
<p>
|
||||||
@@ -402,9 +404,25 @@ const PasskeyManager = ({ credentials, apiRoutes }) => {
|
|||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</StyledDialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PasskeyManager;
|
export default PasskeyManager;
|
||||||
|
|
||||||
|
PasskeyManager.propTypes = {
|
||||||
|
credentials: arrayOf(shape({
|
||||||
|
id: string.isRequired,
|
||||||
|
credentialName: string.isRequired,
|
||||||
|
createdAt: string,
|
||||||
|
lastUsedAt: string,
|
||||||
|
isBackupEligible: bool,
|
||||||
|
isBackupAuthenticated: bool,
|
||||||
|
})).isRequired,
|
||||||
|
apiRoutes: shape({
|
||||||
|
credentials: string.isRequired,
|
||||||
|
registrationBegin: string.isRequired,
|
||||||
|
registrationComplete: string.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,7 +1,18 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { BarChart } from '@mui/x-charts/BarChart';
|
import { BarChart } from '@mui/x-charts/BarChart';
|
||||||
|
import { LineChart } from '@mui/x-charts/LineChart';
|
||||||
import { PieChart } from '@mui/x-charts/PieChart';
|
import { PieChart } from '@mui/x-charts/PieChart';
|
||||||
import { createTheme, ThemeProvider } from '@mui/material/styles';
|
import { createTheme, ThemeProvider } from '@mui/material/styles';
|
||||||
|
import { shape, arrayOf, number, string } from 'prop-types';
|
||||||
|
|
||||||
const darkTheme = createTheme({
|
const darkTheme = createTheme({
|
||||||
palette: {
|
palette: {
|
||||||
@@ -16,6 +27,8 @@ const darkTheme = createTheme({
|
|||||||
const WIN_COLOR = '#5ee89a';
|
const WIN_COLOR = '#5ee89a';
|
||||||
const LOSS_COLOR = '#f67d52';
|
const LOSS_COLOR = '#f67d52';
|
||||||
const DRAW_COLOR = '#95cff5';
|
const DRAW_COLOR = '#95cff5';
|
||||||
|
const MINES_COLOR = '#f67d52';
|
||||||
|
const BONUS_COLOR = '#ffd700';
|
||||||
|
|
||||||
const axisStyle = {
|
const axisStyle = {
|
||||||
tickLabelStyle: { fill: 'rgba(255,255,255,0.4)', fontSize: 11, fontFamily: '\'Rajdhani\', sans-serif' },
|
tickLabelStyle: { fill: 'rgba(255,255,255,0.4)', fontSize: 11, fontFamily: '\'Rajdhani\', sans-serif' },
|
||||||
@@ -23,10 +36,12 @@ const axisStyle = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function ProfileCharts({ chartData }) {
|
export default function ProfileCharts({ chartData }) {
|
||||||
const { months, wins, losses, draws, pieWins, pieLosses, pieDraws } = chartData;
|
const { months, wins, losses, draws, pieWins, pieLosses, pieDraws, recentGames } = chartData;
|
||||||
const total = pieWins + pieLosses + pieDraws;
|
const total = pieWins + pieLosses + pieDraws;
|
||||||
|
|
||||||
const hasBars = wins.some(v => 0 < v) || losses.some(v => 0 < v) || draws.some(v => 0 < v);
|
const hasBars = wins.some(v => 0 < v) || losses.some(v => 0 < v) || draws.some(v => 0 < v);
|
||||||
|
const hasRecent = recentGames
|
||||||
|
&& (recentGames.mines?.some(v => 0 < v) || recentGames.bonus?.some(v => 0 < v));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={darkTheme}>
|
<ThemeProvider theme={darkTheme}>
|
||||||
@@ -97,7 +112,54 @@ export default function ProfileCharts({ chartData }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{hasRecent && (
|
||||||
|
<div className="profile-chart-block profile-chart-block--wide">
|
||||||
|
<h2 className="profile-section__title">
|
||||||
|
<i className="fa fa-line-chart" /> Last {recentGames.labels.length} games — mines & bonus
|
||||||
|
</h2>
|
||||||
|
<div className="profile-chart-inner">
|
||||||
|
<LineChart
|
||||||
|
xAxis={[{ scaleType: 'band', data: recentGames.labels, ...axisStyle }]}
|
||||||
|
yAxis={[{ ...axisStyle }]}
|
||||||
|
series={[
|
||||||
|
{ data: recentGames.mines, label: 'Mines hit', color: MINES_COLOR },
|
||||||
|
{ data: recentGames.bonus, label: 'Bonus points', color: BONUS_COLOR },
|
||||||
|
]}
|
||||||
|
slotProps={{
|
||||||
|
legend: {
|
||||||
|
labelStyle: {
|
||||||
|
fill: 'rgba(255,255,255,0.55)',
|
||||||
|
fontSize: 13,
|
||||||
|
fontFamily: '\'Rajdhani\', sans-serif',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
borderRadius={3}
|
||||||
|
height={220}
|
||||||
|
margin={{ top: 10, bottom: 30, left: 40, right: 140 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ProfileCharts.propTypes = {
|
||||||
|
chartData: shape({
|
||||||
|
months: arrayOf(string).isRequired,
|
||||||
|
wins: arrayOf(number).isRequired,
|
||||||
|
losses: arrayOf(number).isRequired,
|
||||||
|
draws: arrayOf(number).isRequired,
|
||||||
|
pieWins: number.isRequired,
|
||||||
|
pieLosses: number.isRequired,
|
||||||
|
pieDraws: number.isRequired,
|
||||||
|
recentGames: shape({
|
||||||
|
labels: arrayOf(string).isRequired,
|
||||||
|
mines: arrayOf(number).isRequired,
|
||||||
|
bonus: arrayOf(number).isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
};
|
||||||
|
|||||||
54
assets/js/components/battle-dialog/Avatar.jsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { string } from 'prop-types';
|
||||||
|
|
||||||
|
export const Avatar = ({ name, color, avatarUrl, bonusPoints = 0 }) => {
|
||||||
|
const isRed = 'red' === color;
|
||||||
|
const initials = useMemo(() => (name || '?').slice(0, 2).toUpperCase(), [name]);
|
||||||
|
|
||||||
|
const cssVars = isRed ? {
|
||||||
|
'--bd-avatar-gradient': 'linear-gradient(135deg, rgba(173,10,5,0.6) 0%, rgba(246,125,82,0.4) 100%)',
|
||||||
|
'--bd-avatar-glow': '0 0 0 3px rgba(173,10,5,0.2), 0 0 28px rgba(173,10,5,0.35)',
|
||||||
|
'--bd-avatar-border': 'rgba(173,10,5,0.5)',
|
||||||
|
'--bd-avatar-color': '#f67d52',
|
||||||
|
} : {
|
||||||
|
'--bd-avatar-gradient': 'linear-gradient(135deg, rgba(35,111,135,0.6) 0%, rgba(41,128,185,0.4) 100%)',
|
||||||
|
'--bd-avatar-glow': '0 0 0 3px rgba(35,111,135,0.2), 0 0 28px rgba(35,111,135,0.35)',
|
||||||
|
'--bd-avatar-border': 'rgba(35,111,135,0.5)',
|
||||||
|
'--bd-avatar-color': '#95cff5',
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bd-avatar-wrap" style={cssVars}>
|
||||||
|
<div className="bd-avatar-ring-wrap">
|
||||||
|
<div className="bd-avatar-ring">
|
||||||
|
{avatarUrl
|
||||||
|
? <img src={avatarUrl} alt={name} className="bd-avatar-img" />
|
||||||
|
: initials}
|
||||||
|
</div>
|
||||||
|
{0 < bonusPoints && (
|
||||||
|
<div className="bd-avatar-bonus">
|
||||||
|
<i className="fa fa-star" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span className="bd-avatar-name">{name}</span>
|
||||||
|
<span className="bd-avatar-side">{isRed ? 'Red' : 'Blue'}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Avatar.propTypes = {
|
||||||
|
name: string,
|
||||||
|
color: string,
|
||||||
|
avatarUrl: string,
|
||||||
|
bonusPoints: string,
|
||||||
|
};
|
||||||
122
assets/js/components/battle-dialog/BonusPoints.jsx
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { StatRow } from './StatRow';
|
||||||
|
import { object } from 'prop-types';
|
||||||
|
|
||||||
|
export const BonusPoints = ({ game }) => {
|
||||||
|
const hasBonuspoints = useMemo(
|
||||||
|
() => 0 < game?.redBonusPoints
|
||||||
|
|| 0 < game?.blueBonusPoints
|
||||||
|
|| game?.redBonusStats?.blindHits
|
||||||
|
|| game?.blueBonusStats?.blindHits,
|
||||||
|
[
|
||||||
|
game?.blueBonusPoints,
|
||||||
|
game?.blueBonusStats?.blindHits,
|
||||||
|
game?.redBonusPoints,
|
||||||
|
game?.redBonusStats?.blindHits,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasRedNoBonuses = useMemo(
|
||||||
|
() => !game.redBonusStats?.blindHits
|
||||||
|
&& !game.redBonusStats?.chainBest
|
||||||
|
&& !game.redBonusStats?.edgeMines
|
||||||
|
&& !game.redBonusStats?.lastMineHits
|
||||||
|
&& !game.redBonusStats?.biggestReveal,
|
||||||
|
[
|
||||||
|
game.redBonusStats?.biggestReveal,
|
||||||
|
game.redBonusStats?.blindHits,
|
||||||
|
game.redBonusStats?.chainBest,
|
||||||
|
game.redBonusStats?.edgeMines,
|
||||||
|
game.redBonusStats?.lastMineHits,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasBlueNoBonuses = useMemo(
|
||||||
|
() => !game.blueBonusStats?.blindHits
|
||||||
|
&& !game.blueBonusStats?.chainBest
|
||||||
|
&& !game.blueBonusStats?.edgeMines
|
||||||
|
&& !game.blueBonusStats?.lastMineHits
|
||||||
|
&& !game.blueBonusStats?.biggestReveal,
|
||||||
|
[
|
||||||
|
game.blueBonusStats?.biggestReveal,
|
||||||
|
game.blueBonusStats?.blindHits,
|
||||||
|
game.blueBonusStats?.chainBest,
|
||||||
|
game.blueBonusStats?.edgeMines,
|
||||||
|
game.blueBonusStats?.lastMineHits,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hasBonuspoints) return '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bd-bonus">
|
||||||
|
<div className="bd-bonus__grid">
|
||||||
|
<div className="bd-bonus__column bd-bonus__column--red">
|
||||||
|
<span className="bd-bonus__heading">
|
||||||
|
<i className="fa fa-star" /> Red Bonus Statistics
|
||||||
|
</span>
|
||||||
|
<div className="bd-bonus__rows">
|
||||||
|
<StatRow icon="fa-star" label="Total Bonus Points" value={(game.redBonusPoints ?? 0).toFixed(1)} valueColor="#ffd700" />
|
||||||
|
{0 < game.redBonusStats?.blindHits && (
|
||||||
|
<StatRow icon="fa-bullseye" label="Blind hits" value={game.redBonusStats.blindHits} />
|
||||||
|
)}
|
||||||
|
{0 < game.redBonusStats?.chainBest && (
|
||||||
|
<StatRow icon="fa-link" label="Best chain" value={game.redBonusStats.chainBest} />
|
||||||
|
)}
|
||||||
|
{0 < game.redBonusStats?.edgeMines && (
|
||||||
|
<StatRow icon="fa-border-all" label="Edge mines" value={game.redBonusStats.edgeMines} />
|
||||||
|
)}
|
||||||
|
{0 < game.redBonusStats?.lastMineHits && (
|
||||||
|
<StatRow icon="fa-hourglass-end" label="Endgame mines" value={game.redBonusStats.lastMineHits} />
|
||||||
|
)}
|
||||||
|
{0 < game.redBonusStats?.biggestReveal && (
|
||||||
|
<StatRow icon="fa-expand" label="Biggest reveal" value={game.redBonusStats.biggestReveal} />
|
||||||
|
)}
|
||||||
|
{hasRedNoBonuses && (
|
||||||
|
<StatRow icon="fa-minus-circle" label="Status" value="No bonuses" valueColor="rgba(255,255,255,0.3)" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bd-bonus__column bd-bonus__column--blue">
|
||||||
|
<span className="bd-bonus__heading">
|
||||||
|
<i className="fa fa-star" /> Blue Bonus Statistics
|
||||||
|
</span>
|
||||||
|
<div className="bd-bonus__rows">
|
||||||
|
<StatRow icon="fa-star" label="Total Bonus Points" value={(game.blueBonusPoints ?? 0).toFixed(1)} valueColor="#ffd700" />
|
||||||
|
{0 < game.blueBonusStats?.blindHits && (
|
||||||
|
<StatRow icon="fa-bullseye" label="Blind hits" value={game.blueBonusStats.blindHits} />
|
||||||
|
)}
|
||||||
|
{0 < game.blueBonusStats?.chainBest && (
|
||||||
|
<StatRow icon="fa-link" label="Best chain" value={game.blueBonusStats.chainBest} />
|
||||||
|
)}
|
||||||
|
{0 < game.blueBonusStats?.edgeMines && (
|
||||||
|
<StatRow icon="fa-border-all" label="Edge mines" value={game.blueBonusStats.edgeMines} />
|
||||||
|
)}
|
||||||
|
{0 < game.blueBonusStats?.lastMineHits && (
|
||||||
|
<StatRow icon="fa-hourglass-end" label="Endgame mines" value={game.blueBonusStats.lastMineHits} />
|
||||||
|
)}
|
||||||
|
{0 < game.blueBonusStats?.biggestReveal && (
|
||||||
|
<StatRow icon="fa-expand" label="Biggest reveal" value={game.blueBonusStats.biggestReveal} />
|
||||||
|
)}
|
||||||
|
{hasBlueNoBonuses && (
|
||||||
|
<StatRow icon="fa-minus-circle" label="Status" value="No bonuses" valueColor="rgba(255,255,255,0.3)" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
BonusPoints.propTypes = {
|
||||||
|
game: object.isRequired,
|
||||||
|
};
|
||||||
31
assets/js/components/battle-dialog/StatRow.jsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { node, string } from 'prop-types';
|
||||||
|
|
||||||
|
export const StatRow = ({ icon, label, value, valueColor }) => (
|
||||||
|
<div className="bd-stat-row">
|
||||||
|
<i className={`fa ${icon} bd-stat-row__icon`} />
|
||||||
|
<span className="bd-stat-row__label">{label}</span>
|
||||||
|
<span
|
||||||
|
className="bd-stat-row__value"
|
||||||
|
style={valueColor ? { '--bd-stat-value-color': valueColor } : undefined}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
StatRow.propTypes = {
|
||||||
|
icon: string.isRequired,
|
||||||
|
label: string.isRequired,
|
||||||
|
value: node.isRequired,
|
||||||
|
valueColor: string,
|
||||||
|
};
|
||||||
18
assets/js/components/index.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { AvatarUpload } from './AvatarUpload';
|
||||||
|
export { BattleDialog } from './BattleDialog';
|
||||||
|
export { default as ContactForm } from './ContactForm';
|
||||||
|
export { default as PasskeyLogin } from './PasskeyLogin';
|
||||||
|
export { default as PasskeyManager } from './PasskeyManager';
|
||||||
|
export { default as ProfileCharts } from './ProfileCharts';
|
||||||
|
export { BonusPoints } from './battle-dialog/BonusPoints';
|
||||||
|
export { Avatar } from './battle-dialog/Avatar';
|
||||||
|
export { StatRow } from './battle-dialog/StatRow';
|
||||||
30
assets/js/contact.jsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
import { ContactForm } from '@global-components';
|
||||||
|
|
||||||
|
const wrapper = document.getElementById('contact-form-wrapper');
|
||||||
|
|
||||||
|
if (wrapper) {
|
||||||
|
const siteKey = wrapper.dataset.siteKey;
|
||||||
|
const recaptchaFieldId = wrapper.dataset.recaptchaFieldId;
|
||||||
|
|
||||||
|
if (siteKey && recaptchaFieldId) {
|
||||||
|
createRoot(wrapper).render(
|
||||||
|
<ContactForm
|
||||||
|
siteKey={siteKey}
|
||||||
|
recaptchaFieldId={recaptchaFieldId}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.error('ContactForm: Missing siteKey or recaptchaFieldId in data attributes');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,10 +11,11 @@ import React, { useRef } from 'react';
|
|||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { GameProvider } from '@mine-contexts';
|
import { GameProvider } from '@mine-contexts';
|
||||||
import { GameBoard } from '@mine-components';
|
import { GameBoard } from '@mine-components';
|
||||||
|
import { string } from 'prop-types';
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
const MineSeeker = ({ env, gameId }) => {
|
const MineSeeker = ({ env, gameId, opponentName = '' }) => {
|
||||||
const isEnvDev = 'dev' === env;
|
const isEnvDev = 'dev' === env;
|
||||||
const gameAssoc = useRef('' !== gameId ? gameId : crypto.randomUUID()).current;
|
const gameAssoc = useRef('' !== gameId ? gameId : crypto.randomUUID()).current;
|
||||||
const gameInherited = '' !== gameId;
|
const gameInherited = '' !== gameId;
|
||||||
@@ -25,6 +26,7 @@ const MineSeeker = ({ env, gameId }) => {
|
|||||||
<GameBoard
|
<GameBoard
|
||||||
gameAssoc={gameAssoc}
|
gameAssoc={gameAssoc}
|
||||||
gameInherited={gameInherited}
|
gameInherited={gameInherited}
|
||||||
|
opponentName={opponentName}
|
||||||
isEnvDev={isEnvDev}
|
isEnvDev={isEnvDev}
|
||||||
/>
|
/>
|
||||||
</GameProvider>
|
</GameProvider>
|
||||||
@@ -33,3 +35,9 @@ const MineSeeker = ({ env, gameId }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default MineSeeker;
|
export default MineSeeker;
|
||||||
|
|
||||||
|
MineSeeker.propTypes = {
|
||||||
|
env: string.isRequired,
|
||||||
|
gameId: string,
|
||||||
|
opponentName: string,
|
||||||
|
};
|
||||||
|
|||||||
33
assets/js/mine-seeker/components/BonusBox.jsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { func, number, string } from 'prop-types';
|
||||||
|
|
||||||
|
const BonusBox = ({ color, points, onClick, title }) => (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`bonus-box ${color}-bonus`}
|
||||||
|
onClick={onClick}
|
||||||
|
title={title || 'View bonus statistics'}
|
||||||
|
aria-label={`${color} bonus points: ${points}`}
|
||||||
|
>
|
||||||
|
<i className="fa fa-star bonus-box__icon" />
|
||||||
|
<span className="bonus-box__value">{points}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default BonusBox;
|
||||||
|
|
||||||
|
BonusBox.propTypes = {
|
||||||
|
color: string.isRequired,
|
||||||
|
points: number.isRequired,
|
||||||
|
onClick: func.isRequired,
|
||||||
|
title: string,
|
||||||
|
};
|
||||||
79
assets/js/mine-seeker/components/BonusStatsDialog.jsx
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import Dialog from '@mui/material/Dialog';
|
||||||
|
import { styled } from '@mui/material/styles';
|
||||||
|
import { PlayerColumn } from '@mine-components';
|
||||||
|
import { bool, func, shape, string, number, object } from 'prop-types';
|
||||||
|
|
||||||
|
const BonusStatsDialog = ({ open, onClose, red, blue }) => (
|
||||||
|
<StyledDialog open={open} onClose={onClose}>
|
||||||
|
<div className="bsd">
|
||||||
|
<div className="bsd-header">
|
||||||
|
<div className="bsd-header-text">
|
||||||
|
<span className="bsd-label">Scoring</span>
|
||||||
|
<h2 className="bsd-title">
|
||||||
|
<i className="fa fa-star" />
|
||||||
|
Bonus Statistics
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<button className="bsd-close" onClick={onClose} aria-label="Close">
|
||||||
|
<i className="fa fa-times" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="bsd-body">
|
||||||
|
<PlayerColumn color="red" player={red} />
|
||||||
|
<PlayerColumn color="blue" player={blue} />
|
||||||
|
</div>
|
||||||
|
<p className="bsd-note">
|
||||||
|
Bonus points are awarded alongside the main score for skillful play.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</StyledDialog>
|
||||||
|
);
|
||||||
|
|
||||||
|
const StyledDialog = styled(Dialog)({
|
||||||
|
'& .MuiDialog-paper': {
|
||||||
|
background: '#07090d',
|
||||||
|
backgroundImage: `
|
||||||
|
linear-gradient(rgba(35, 111, 135, 0.08) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, rgba(35, 111, 135, 0.08) 1px, transparent 1px)
|
||||||
|
`,
|
||||||
|
backgroundSize: '46px 46px',
|
||||||
|
border: '1px solid rgba(35, 111, 135, 0.4)',
|
||||||
|
borderRadius: '12px',
|
||||||
|
boxShadow: '0 0 80px rgba(35, 111, 135, 0.15), 0 32px 80px rgba(0, 0, 0, 0.9)',
|
||||||
|
width: '560px',
|
||||||
|
maxWidth: '94vw',
|
||||||
|
overflow: 'hidden',
|
||||||
|
color: '#fff',
|
||||||
|
},
|
||||||
|
'& .MuiBackdrop-root': {
|
||||||
|
background: 'rgba(2, 4, 8, 0.88)',
|
||||||
|
backdropFilter: 'blur(4px)',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default BonusStatsDialog;
|
||||||
|
|
||||||
|
BonusStatsDialog.propTypes = {
|
||||||
|
open: bool.isRequired,
|
||||||
|
onClose: func.isRequired,
|
||||||
|
red: shape({
|
||||||
|
name: string,
|
||||||
|
bonusPoints: number,
|
||||||
|
bonusStats: object,
|
||||||
|
}).isRequired,
|
||||||
|
blue: shape({
|
||||||
|
name: string,
|
||||||
|
bonusPoints: number,
|
||||||
|
bonusStats: object,
|
||||||
|
}).isRequired,
|
||||||
|
};
|
||||||
123
assets/js/mine-seeker/components/CaptchaOverlay.jsx
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { Fragment, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { func, node, string } from 'prop-types';
|
||||||
|
|
||||||
|
const CAPTCHA_STORAGE_KEY = 'mineseeker_captcha_verified';
|
||||||
|
const CAPTCHA_TOKEN_KEY = 'mineseeker_captcha_token';
|
||||||
|
const RECAPTCHA_ACTION = 'mineseeker_play';
|
||||||
|
|
||||||
|
const CaptchaOverlay = ({ siteKey, onVerified, children }) => {
|
||||||
|
const [verified, setVerified] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState(false);
|
||||||
|
|
||||||
|
const handleToken = useCallback(token => {
|
||||||
|
const wrapper = document.getElementById('mine-wrapper');
|
||||||
|
if (wrapper) {
|
||||||
|
wrapper.dataset.captchaToken = token;
|
||||||
|
}
|
||||||
|
sessionStorage.setItem(CAPTCHA_TOKEN_KEY, token);
|
||||||
|
sessionStorage.setItem(CAPTCHA_STORAGE_KEY, Date.now().toString());
|
||||||
|
setVerified(true);
|
||||||
|
onVerified?.();
|
||||||
|
}, [onVerified]);
|
||||||
|
|
||||||
|
const buttonClasses = useMemo(() => [
|
||||||
|
'captcha-button',
|
||||||
|
error && 'captcha-button--error',
|
||||||
|
loading && 'captcha-button--loading',
|
||||||
|
].filter(Boolean).join(' '), [error, loading]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const storedToken = sessionStorage.getItem(CAPTCHA_TOKEN_KEY);
|
||||||
|
const storedTime = sessionStorage.getItem(CAPTCHA_STORAGE_KEY);
|
||||||
|
|
||||||
|
if (storedToken && storedTime) {
|
||||||
|
const elapsed = (Date.now() - parseInt(storedTime)) / 1000;
|
||||||
|
if (110 > elapsed) {
|
||||||
|
const wrapper = document.getElementById('mine-wrapper');
|
||||||
|
if (wrapper) {
|
||||||
|
wrapper.dataset.captchaToken = storedToken;
|
||||||
|
}
|
||||||
|
setVerified(true);
|
||||||
|
onVerified?.();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.grecaptcha) {
|
||||||
|
window.grecaptcha.ready(() => {
|
||||||
|
window.grecaptcha
|
||||||
|
.execute(siteKey, { action: RECAPTCHA_ACTION })
|
||||||
|
.then(token => {
|
||||||
|
handleToken(token);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setError(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [siteKey, onVerified, handleToken]);
|
||||||
|
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(false);
|
||||||
|
|
||||||
|
window.grecaptcha.ready(() => {
|
||||||
|
window.grecaptcha
|
||||||
|
.execute(siteKey, { action: RECAPTCHA_ACTION })
|
||||||
|
.then(token => {
|
||||||
|
handleToken(token);
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setLoading(false);
|
||||||
|
setError(true);
|
||||||
|
setTimeout(() => setError(false), 2000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (verified) {
|
||||||
|
return <Fragment>{children}</Fragment>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="captcha-overlay">
|
||||||
|
<div className="captcha-content">
|
||||||
|
<div className="captcha-icon">
|
||||||
|
<i className="fa fa-shield-halved" />
|
||||||
|
</div>
|
||||||
|
<h1 className="captcha-title">Ready to Play?</h1>
|
||||||
|
<p className="captcha-description">
|
||||||
|
Click below to verify you're human and start playing.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
className={buttonClasses}
|
||||||
|
onClick={handleClick}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
<i className={`fa ${loading ? 'fa-spinner fa-spin' : error ? 'fa-exclamation-circle' : 'fa-play'}`} />
|
||||||
|
{loading ? 'Verifying...' : error ? 'Try Again' : 'Start Playing'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CaptchaOverlay;
|
||||||
|
|
||||||
|
CaptchaOverlay.propTypes = {
|
||||||
|
siteKey: string.isRequired,
|
||||||
|
onVerified: func,
|
||||||
|
children: node,
|
||||||
|
};
|
||||||
48
assets/js/mine-seeker/components/ChallengeCountdown.jsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
import { Fragment, useEffect, useState } from 'react';
|
||||||
|
import { func, number } from 'prop-types';
|
||||||
|
|
||||||
|
const ChallengeCountdown = ({ onAccept, onDecline, seconds = 30 }) => {
|
||||||
|
const [countdown, setCountdown] = useState(seconds);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setCountdown(prev => {
|
||||||
|
if (1 >= prev) {
|
||||||
|
clearInterval(interval);
|
||||||
|
onDecline();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return prev - 1;
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [onDecline]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<p style={{ textAlign: 'center', marginBottom: 20, color: '#95cff5' }}>
|
||||||
|
You have {countdown} second{1 === countdown ? '' : 's'} to answer to the challenge!
|
||||||
|
</p>
|
||||||
|
<div className="resign">
|
||||||
|
<a onClick={onAccept}>Accept</a>
|
||||||
|
<a onClick={onDecline}>Decline</a>
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChallengeCountdown;
|
||||||
|
|
||||||
|
ChallengeCountdown.propTypes = {
|
||||||
|
onAccept: func.isRequired,
|
||||||
|
onDecline: func.isRequired,
|
||||||
|
seconds: number,
|
||||||
|
};
|
||||||
@@ -7,14 +7,19 @@
|
|||||||
* file that was distributed with this source code.
|
* file that was distributed with this source code.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useGame } from '@mine-contexts';
|
import { useGame } from '@mine-contexts';
|
||||||
import { useServerCommunication } from '@mine-hooks';
|
import { useServerCommunication } from '@mine-hooks';
|
||||||
|
import CaptchaOverlay from './CaptchaOverlay';
|
||||||
import GridControl from './grid/GridControl';
|
import GridControl from './grid/GridControl';
|
||||||
|
import { bool, string } from 'prop-types';
|
||||||
|
|
||||||
export const GameBoard = ({ gameAssoc, gameInherited, isEnvDev }) => {
|
export const GameBoard = ({ gameAssoc, gameInherited, opponentName = '', isEnvDev }) => {
|
||||||
const { gridReady } = useGame();
|
const { gridReady } = useGame();
|
||||||
const { onClick, resign } = useServerCommunication(gameAssoc, gameInherited, isEnvDev);
|
const { onClick, resign } = useServerCommunication(gameAssoc, gameInherited, opponentName, isEnvDev);
|
||||||
|
const [captchaVerified, setCaptchaVerified] = useState(false);
|
||||||
|
|
||||||
|
const siteKey = document.getElementById('mine-wrapper')?.dataset.recaptchaSiteKey;
|
||||||
|
|
||||||
if (!gridReady) {
|
if (!gridReady) {
|
||||||
return (
|
return (
|
||||||
@@ -24,10 +29,24 @@ export const GameBoard = ({ gameAssoc, gameInherited, isEnvDev }) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!captchaVerified && siteKey) {
|
||||||
|
return (
|
||||||
|
<CaptchaOverlay siteKey={siteKey} onVerified={() => setCaptchaVerified(true)} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GridControl
|
<GridControl
|
||||||
|
gameAssoc={gameAssoc}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
resign={resign}
|
resign={resign}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
GameBoard.propTypes = {
|
||||||
|
gameAssoc: string.isRequired,
|
||||||
|
gameInherited: bool.isRequired,
|
||||||
|
opponentName: string,
|
||||||
|
isEnvDev: bool,
|
||||||
|
};
|
||||||
|
|||||||
@@ -7,37 +7,26 @@
|
|||||||
* file that was distributed with this source code.
|
* file that was distributed with this source code.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import { BonusBox, BonusStatsDialog, Avatar } from '@mine-components';
|
||||||
|
import { formatTime } from '@global-utils/format';
|
||||||
import { useGame } from '@mine-contexts';
|
import { useGame } from '@mine-contexts';
|
||||||
|
|
||||||
const renderAvatar = player => {
|
|
||||||
if (!player.registered) return null;
|
|
||||||
return (
|
|
||||||
<div className="timer-avatar">
|
|
||||||
{player.avatar
|
|
||||||
? <img src={player.avatar} alt={player.name} className="timer-avatar__img" />
|
|
||||||
: <span className="timer-avatar__initials">{player.name.slice(0, 2).toUpperCase()}</span>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const GameTimer = () => {
|
const GameTimer = () => {
|
||||||
const { overlay, connectionLost, endRef, activePlayer, red, blue } = useGame();
|
const { overlay, connectionLost, endRef, activePlayer, red, blue } = useGame();
|
||||||
const [redTime, setRedTime] = useState(0);
|
const [redTime, setRedTime] = useState(0);
|
||||||
const [blueTime, setBlueTime] = useState(0);
|
const [blueTime, setBlueTime] = useState(0);
|
||||||
const [isRunning, setIsRunning] = useState(false);
|
const [isRunning, setIsRunning] = useState(false);
|
||||||
|
const [bonusDialogOpen, setBonusDialogOpen] = useState(false);
|
||||||
const timerIntervalRef = useRef(null);
|
const timerIntervalRef = useRef(null);
|
||||||
const gameStartedRef = useRef(false);
|
const gameStartedRef = useRef(false);
|
||||||
|
|
||||||
// Use timestamps instead of counters for more reliable background tracking
|
|
||||||
const redStartTimeRef = useRef(null);
|
const redStartTimeRef = useRef(null);
|
||||||
const blueStartTimeRef = useRef(null);
|
const blueStartTimeRef = useRef(null);
|
||||||
const lastActivePlayerRef = useRef(null);
|
const lastActivePlayerRef = useRef(null);
|
||||||
const pausedRedTimeRef = useRef(0);
|
const pausedRedTimeRef = useRef(0);
|
||||||
const pausedBlueTimeRef = useRef(0);
|
const pausedBlueTimeRef = useRef(0);
|
||||||
|
|
||||||
// Start timer when overlay is hidden (both players connected and game started)
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!overlay && !gameStartedRef.current) {
|
if (!overlay && !gameStartedRef.current) {
|
||||||
gameStartedRef.current = true;
|
gameStartedRef.current = true;
|
||||||
@@ -50,28 +39,20 @@ const GameTimer = () => {
|
|||||||
pausedBlueTimeRef.current = 0;
|
pausedBlueTimeRef.current = 0;
|
||||||
lastActivePlayerRef.current = activePlayer;
|
lastActivePlayerRef.current = activePlayer;
|
||||||
}
|
}
|
||||||
}, [overlay]);
|
}, [activePlayer, overlay]);
|
||||||
|
|
||||||
// Stop timer on game end (resign/win)
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (endRef.current) {
|
if (endRef.current) setIsRunning(false);
|
||||||
setIsRunning(false);
|
}, [endRef]);
|
||||||
}
|
|
||||||
}, [endRef.current]);
|
|
||||||
|
|
||||||
// Stop timer on connection loss
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (connectionLost) {
|
if (connectionLost) setIsRunning(false);
|
||||||
setIsRunning(false);
|
|
||||||
}
|
|
||||||
}, [connectionLost]);
|
}, [connectionLost]);
|
||||||
|
|
||||||
// Handle player switch - pause one timer, resume the other
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isRunning) return;
|
if (!isRunning) return;
|
||||||
|
|
||||||
if (lastActivePlayerRef.current !== activePlayer) {
|
if (lastActivePlayerRef.current !== activePlayer) {
|
||||||
// Player switched, save current accumulated time for whoever was active
|
|
||||||
const startRef = lastActivePlayerRef.current ? blueStartTimeRef.current : redStartTimeRef.current;
|
const startRef = lastActivePlayerRef.current ? blueStartTimeRef.current : redStartTimeRef.current;
|
||||||
if (startRef) {
|
if (startRef) {
|
||||||
const elapsed = Math.floor((Date.now() - startRef) / 1000);
|
const elapsed = Math.floor((Date.now() - startRef) / 1000);
|
||||||
@@ -82,7 +63,6 @@ const GameTimer = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start the new active player's timer
|
|
||||||
if (activePlayer) {
|
if (activePlayer) {
|
||||||
blueStartTimeRef.current = Date.now();
|
blueStartTimeRef.current = Date.now();
|
||||||
} else {
|
} else {
|
||||||
@@ -93,85 +73,61 @@ const GameTimer = () => {
|
|||||||
}
|
}
|
||||||
}, [activePlayer, isRunning]);
|
}, [activePlayer, isRunning]);
|
||||||
|
|
||||||
// Main timer effect - update display every 100ms
|
const syncTimes = useCallback(() => {
|
||||||
|
let currentRedTime = pausedRedTimeRef.current;
|
||||||
|
let currentBlueTime = pausedBlueTimeRef.current;
|
||||||
|
|
||||||
|
if (!activePlayer && redStartTimeRef.current) {
|
||||||
|
currentRedTime += Math.floor((Date.now() - redStartTimeRef.current) / 1000);
|
||||||
|
} else if (activePlayer && blueStartTimeRef.current) {
|
||||||
|
currentBlueTime += Math.floor((Date.now() - blueStartTimeRef.current) / 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
setRedTime(currentRedTime);
|
||||||
|
setBlueTime(currentBlueTime);
|
||||||
|
}, [activePlayer]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isRunning) {
|
if (!isRunning) {
|
||||||
if (timerIntervalRef.current) {
|
if (timerIntervalRef.current) clearInterval(timerIntervalRef.current);
|
||||||
clearInterval(timerIntervalRef.current);
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
timerIntervalRef.current = setInterval(() => {
|
timerIntervalRef.current = setInterval(syncTimes, 100);
|
||||||
let currentRedTime = pausedRedTimeRef.current;
|
|
||||||
let currentBlueTime = pausedBlueTimeRef.current;
|
|
||||||
|
|
||||||
// Add elapsed time for the active player
|
|
||||||
if (!activePlayer && redStartTimeRef.current) {
|
|
||||||
currentRedTime += Math.floor((Date.now() - redStartTimeRef.current) / 1000);
|
|
||||||
} else if (activePlayer && blueStartTimeRef.current) {
|
|
||||||
currentBlueTime += Math.floor((Date.now() - blueStartTimeRef.current) / 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
setRedTime(currentRedTime);
|
|
||||||
setBlueTime(currentBlueTime);
|
|
||||||
}, 100);
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (timerIntervalRef.current) {
|
if (timerIntervalRef.current) clearInterval(timerIntervalRef.current);
|
||||||
clearInterval(timerIntervalRef.current);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}, [isRunning, activePlayer]);
|
}, [isRunning, activePlayer, syncTimes]);
|
||||||
|
|
||||||
// Handle focus/blur to synchronize timer when tab regains focus
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleFocus = () => {
|
const handleFocus = () => {
|
||||||
// Force update when tab regains focus to sync any background drift
|
if (isRunning) syncTimes();
|
||||||
if (isRunning) {
|
|
||||||
let currentRedTime = pausedRedTimeRef.current;
|
|
||||||
let currentBlueTime = pausedBlueTimeRef.current;
|
|
||||||
|
|
||||||
if (!activePlayer && redStartTimeRef.current) {
|
|
||||||
currentRedTime += Math.floor((Date.now() - redStartTimeRef.current) / 1000);
|
|
||||||
} else if (activePlayer && blueStartTimeRef.current) {
|
|
||||||
currentBlueTime += Math.floor((Date.now() - blueStartTimeRef.current) / 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
setRedTime(currentRedTime);
|
|
||||||
setBlueTime(currentBlueTime);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('focus', handleFocus);
|
window.addEventListener('focus', handleFocus);
|
||||||
return () => window.removeEventListener('focus', handleFocus);
|
return () => window.removeEventListener('focus', handleFocus);
|
||||||
}, [isRunning, activePlayer]);
|
}, [isRunning, activePlayer, syncTimes]);
|
||||||
|
|
||||||
// Cleanup on unmount
|
|
||||||
useEffect(() => () => {
|
useEffect(() => () => {
|
||||||
if (timerIntervalRef.current) {
|
if (timerIntervalRef.current) clearInterval(timerIntervalRef.current);
|
||||||
clearInterval(timerIntervalRef.current);
|
|
||||||
}
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const formatTime = seconds => {
|
|
||||||
const mins = Math.floor(seconds / 60);
|
|
||||||
const secs = seconds % 60;
|
|
||||||
return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="game-timer-container">
|
<div className="game-timer-container">
|
||||||
|
<BonusBox color="red" points={red.bonusPoints ?? 0} onClick={() => setBonusDialogOpen(true)} />
|
||||||
<div className={`game-timer red-timer ${!activePlayer ? 'active' : ''}`}>
|
<div className={`game-timer red-timer ${!activePlayer ? 'active' : ''}`}>
|
||||||
{renderAvatar(red)}
|
<Avatar player={red} />
|
||||||
<i className={`fa ${!activePlayer ? 'fa-hourglass-half' : 'fa-hourglass-start'} timer-icon`} />
|
<i className={`fa ${!activePlayer ? 'fa-hourglass-half' : 'fa-hourglass-start'} timer-icon`} />
|
||||||
<span className="timer-display">{formatTime(redTime)}</span>
|
<span className="timer-display">{formatTime(redTime)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={`game-timer blue-timer ${activePlayer ? 'active' : ''}`}>
|
<div className={`game-timer blue-timer ${activePlayer ? 'active' : ''}`}>
|
||||||
{renderAvatar(blue)}
|
<Avatar player={blue} />
|
||||||
<i className={`fa ${activePlayer ? 'fa-hourglass-half' : 'fa-hourglass-start'} timer-icon`} />
|
<i className={`fa ${activePlayer ? 'fa-hourglass-half' : 'fa-hourglass-start'} timer-icon`} />
|
||||||
<span className="timer-display">{formatTime(blueTime)}</span>
|
<span className="timer-display">{formatTime(blueTime)}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<BonusBox color="blue" points={blue.bonusPoints ?? 0} onClick={() => setBonusDialogOpen(true)} />
|
||||||
|
<BonusStatsDialog open={bonusDialogOpen} onClose={() => setBonusDialogOpen(false)} red={red} blue={blue} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,38 +8,13 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import { formatSince } from '@global-utils/format';
|
||||||
import Dialog from '@mui/material/Dialog';
|
import Dialog from '@mui/material/Dialog';
|
||||||
|
import { styled } from '@mui/material/styles';
|
||||||
|
import { useLobbyDataProvider } from '@mine-hooks';
|
||||||
|
import { bool, func, string } from 'prop-types';
|
||||||
|
|
||||||
const DIALOG_SX = {
|
const OnlinePlayersDialog = ({ open, onClose, currentGameAssoc, isEnvDev = false }) => {
|
||||||
'& .MuiDialog-paper': {
|
|
||||||
background: '#07090d',
|
|
||||||
backgroundImage: `
|
|
||||||
linear-gradient(rgba(35, 111, 135, 0.08) 1px, transparent 1px),
|
|
||||||
linear-gradient(90deg, rgba(35, 111, 135, 0.08) 1px, transparent 1px)
|
|
||||||
`,
|
|
||||||
backgroundSize: '46px 46px',
|
|
||||||
border: '1px solid rgba(35, 111, 135, 0.4)',
|
|
||||||
borderRadius: '12px',
|
|
||||||
boxShadow: '0 0 80px rgba(35, 111, 135, 0.15), 0 32px 80px rgba(0, 0, 0, 0.9)',
|
|
||||||
width: '500px',
|
|
||||||
maxWidth: '94vw',
|
|
||||||
overflow: 'hidden',
|
|
||||||
color: '#fff',
|
|
||||||
},
|
|
||||||
'& .MuiBackdrop-root': {
|
|
||||||
background: 'rgba(2, 4, 8, 0.88)',
|
|
||||||
backdropFilter: 'blur(4px)',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatSince = isoStr => {
|
|
||||||
const diff = Math.floor((Date.now() - new Date(isoStr)) / 60000);
|
|
||||||
if (1 > diff) return 'just now';
|
|
||||||
if (1 === diff) return '1 min ago';
|
|
||||||
return `${diff} min ago`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const OnlinePlayersDialog = ({ open, onClose, currentGameAssoc }) => {
|
|
||||||
const [players, setPlayers] = useState([]);
|
const [players, setPlayers] = useState([]);
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -47,7 +22,9 @@ const OnlinePlayersDialog = ({ open, onClose, currentGameAssoc }) => {
|
|||||||
const [snapshotLoaded, setSnapshotLoaded] = useState(false);
|
const [snapshotLoaded, setSnapshotLoaded] = useState(false);
|
||||||
const [challengingGameAssoc, setChallengingGameAssoc] = useState(null);
|
const [challengingGameAssoc, setChallengingGameAssoc] = useState(null);
|
||||||
const [declinedMsg, setDeclinedMsg] = useState('');
|
const [declinedMsg, setDeclinedMsg] = useState('');
|
||||||
|
const [waitingCountdown, setWaitingCountdown] = useState(0);
|
||||||
const declinedTimerRef = useRef(null);
|
const declinedTimerRef = useRef(null);
|
||||||
|
const { waitingPlayersQuery, challengeMutation } = useLobbyDataProvider();
|
||||||
|
|
||||||
const addPlayer = useCallback(entry => {
|
const addPlayer = useCallback(entry => {
|
||||||
setPlayers(prev =>
|
setPlayers(prev =>
|
||||||
@@ -65,20 +42,21 @@ const OnlinePlayersDialog = ({ open, onClose, currentGameAssoc }) => {
|
|||||||
if (!open) return;
|
if (!open) return;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setSnapshotLoaded(false);
|
setSnapshotLoaded(false);
|
||||||
fetch('/api/game/waiting')
|
|
||||||
.then(r => r.json())
|
waitingPlayersQuery.refetch().then(result => {
|
||||||
.then(data => {
|
if (result.data) {
|
||||||
// Filter out current user's game from the snapshot
|
// Filter out current user's game from the snapshot
|
||||||
const filtered = data.filter(p => p.gameAssoc !== currentGameAssoc);
|
const filtered = result.data.filter(p => p.gameAssoc !== currentGameAssoc);
|
||||||
setPlayers(filtered);
|
setPlayers(filtered);
|
||||||
setSnapshotLoaded(true);
|
}
|
||||||
setLoading(false);
|
setSnapshotLoaded(true);
|
||||||
})
|
setLoading(false);
|
||||||
.catch(() => {
|
}).catch(() => {
|
||||||
setPlayers([]);
|
setPlayers([]);
|
||||||
setSnapshotLoaded(true);
|
setSnapshotLoaded(true);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
});
|
});
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [open, refreshKey, currentGameAssoc]);
|
}, [open, refreshKey, currentGameAssoc]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -106,11 +84,19 @@ const OnlinePlayersDialog = ({ open, onClose, currentGameAssoc }) => {
|
|||||||
return () => es.close();
|
return () => es.close();
|
||||||
}, [open, snapshotLoaded, addPlayer, removePlayer, currentGameAssoc]);
|
}, [open, snapshotLoaded, addPlayer, removePlayer, currentGameAssoc]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (challengeMutation.isError) {
|
||||||
|
setChallengingGameAssoc(null);
|
||||||
|
setWaitingCountdown(0);
|
||||||
|
}
|
||||||
|
}, [challengeMutation.isError]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = () => {
|
const handler = () => {
|
||||||
setChallengingGameAssoc(null);
|
setChallengingGameAssoc(null);
|
||||||
clearTimeout(declinedTimerRef.current);
|
clearTimeout(declinedTimerRef.current);
|
||||||
setDeclinedMsg('Challenge was not accepted.');
|
setDeclinedMsg('Challenge was not accepted.');
|
||||||
|
setWaitingCountdown(0);
|
||||||
declinedTimerRef.current = setTimeout(() => setDeclinedMsg(''), 3500);
|
declinedTimerRef.current = setTimeout(() => setDeclinedMsg(''), 3500);
|
||||||
};
|
};
|
||||||
window.addEventListener('challenge-declined', handler);
|
window.addEventListener('challenge-declined', handler);
|
||||||
@@ -120,15 +106,26 @@ const OnlinePlayersDialog = ({ open, onClose, currentGameAssoc }) => {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!waitingCountdown) return;
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setWaitingCountdown(prev => {
|
||||||
|
if (1 >= prev) return 0;
|
||||||
|
return prev - 1;
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [waitingCountdown]);
|
||||||
|
|
||||||
const handleChallenge = player => {
|
const handleChallenge = player => {
|
||||||
if (challengingGameAssoc) return;
|
if (challengingGameAssoc) return;
|
||||||
setChallengingGameAssoc(player.gameAssoc);
|
setChallengingGameAssoc(player.gameAssoc);
|
||||||
setDeclinedMsg('');
|
setDeclinedMsg('');
|
||||||
fetch('/api/game/challenge/' + player.gameAssoc, {
|
setWaitingCountdown(30);
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
challengeMutation.mutate(
|
||||||
body: JSON.stringify({ challengerGameAssoc: currentGameAssoc }),
|
{ targetGameAssoc: player.gameAssoc, challengerGameAssoc: currentGameAssoc }
|
||||||
}).catch(() => setChallengingGameAssoc(null));
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const visible = players
|
const visible = players
|
||||||
@@ -139,15 +136,19 @@ const OnlinePlayersDialog = ({ open, onClose, currentGameAssoc }) => {
|
|||||||
const hasMore = 5 < visible.length;
|
const hasMore = 5 < visible.length;
|
||||||
|
|
||||||
// Debug: log if currentGameAssoc is undefined or if current user appears
|
// Debug: log if currentGameAssoc is undefined or if current user appears
|
||||||
if ('development' === process.env.NODE_ENV && 0 < players.length) {
|
if (isEnvDev && 0 < players.length) {
|
||||||
const userInList = players.find(p => p.gameAssoc === currentGameAssoc);
|
const userInList = players.find(p => p.gameAssoc === currentGameAssoc);
|
||||||
|
|
||||||
if (userInList) {
|
if (userInList) {
|
||||||
console.warn('[OnlinePlayersDialog] Current user appears in players list:', { currentGameAssoc, userInList });
|
console.warn('[OnlinePlayersDialog] Current user appears in players list:', { currentGameAssoc, userInList });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onClose={onClose} sx={DIALOG_SX}>
|
<StyledDialog
|
||||||
|
open={open}
|
||||||
|
onClose={0 < waitingCountdown ? undefined : onClose}
|
||||||
|
>
|
||||||
<div className="opd">
|
<div className="opd">
|
||||||
<div className="opd-header">
|
<div className="opd-header">
|
||||||
<div className="opd-header-text">
|
<div className="opd-header-text">
|
||||||
@@ -160,32 +161,44 @@ const OnlinePlayersDialog = ({ open, onClose, currentGameAssoc }) => {
|
|||||||
<div className="opd-header-actions">
|
<div className="opd-header-actions">
|
||||||
<button
|
<button
|
||||||
className={`opd-refresh${loading ? ' opd-refresh--spin' : ''}`}
|
className={`opd-refresh${loading ? ' opd-refresh--spin' : ''}`}
|
||||||
onClick={() => setRefreshKey(k => k + 1)}
|
onClick={() => { if (0 === waitingCountdown) setRefreshKey(k => k + 1); }}
|
||||||
disabled={loading}
|
disabled={loading || 0 < waitingCountdown}
|
||||||
aria-label="Refresh"
|
aria-label="Refresh"
|
||||||
title="Refresh list"
|
title="Refresh list"
|
||||||
>
|
>
|
||||||
<i className="fa fa-refresh" />
|
<i className="fa fa-refresh" />
|
||||||
</button>
|
</button>
|
||||||
<button className="opd-close" onClick={onClose} aria-label="Close">
|
<button
|
||||||
|
className="opd-close"
|
||||||
|
onClick={() => { if (0 === waitingCountdown) onClose(); }}
|
||||||
|
disabled={0 < waitingCountdown}
|
||||||
|
aria-label="Close"
|
||||||
|
>
|
||||||
<i className="fa fa-times" />
|
<i className="fa fa-times" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="opd-search-wrap">
|
{0 < waitingCountdown ? (
|
||||||
<i className="fa fa-search opd-search-icon" />
|
<div className="opd-waiting">
|
||||||
<input
|
<i className="fa fa-hourglass-start" />
|
||||||
className="opd-search"
|
<p>Waiting {waitingCountdown} second{1 === waitingCountdown ? '' : 's'} for opponent's answer...</p>
|
||||||
placeholder="Search by username…"
|
</div>
|
||||||
value={search}
|
) : (
|
||||||
onChange={e => setSearch(e.target.value)}
|
<div className="opd-search-wrap">
|
||||||
/>
|
<i className="fa fa-search opd-search-icon" />
|
||||||
{search && (
|
<input
|
||||||
<button className="opd-search-clear" onClick={() => setSearch('')}>
|
className="opd-search"
|
||||||
<i className="fa fa-times" />
|
placeholder="Search by username…"
|
||||||
</button>
|
value={search}
|
||||||
)}
|
onChange={e => setSearch(e.target.value)}
|
||||||
</div>
|
/>
|
||||||
|
{search && (
|
||||||
|
<button className="opd-search-clear" onClick={() => setSearch('')}>
|
||||||
|
<i className="fa fa-times" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="opd-list">
|
<div className="opd-list">
|
||||||
{loading && (
|
{loading && (
|
||||||
<div className="opd-empty">
|
<div className="opd-empty">
|
||||||
@@ -222,7 +235,7 @@ const OnlinePlayersDialog = ({ open, onClose, currentGameAssoc }) => {
|
|||||||
<div className="opd-info">
|
<div className="opd-info">
|
||||||
<span className="opd-name">{player.name}</span>
|
<span className="opd-name">{player.name}</span>
|
||||||
<span className="opd-since">
|
<span className="opd-since">
|
||||||
<i className="fa fa-clock-o" />
|
<i className="fa fa-clock" />
|
||||||
{' '}Waiting {formatSince(player.since)}
|
{' '}Waiting {formatSince(player.since)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -245,8 +258,37 @@ const OnlinePlayersDialog = ({ open, onClose, currentGameAssoc }) => {
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Dialog>
|
</StyledDialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const StyledDialog = styled(Dialog)({
|
||||||
|
'& .MuiDialog-paper': {
|
||||||
|
background: '#07090d',
|
||||||
|
backgroundImage: `
|
||||||
|
linear-gradient(rgba(35, 111, 135, 0.08) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, rgba(35, 111, 135, 0.08) 1px, transparent 1px)
|
||||||
|
`,
|
||||||
|
backgroundSize: '46px 46px',
|
||||||
|
border: '1px solid rgba(35, 111, 135, 0.4)',
|
||||||
|
borderRadius: '12px',
|
||||||
|
boxShadow: '0 0 80px rgba(35, 111, 135, 0.15), 0 32px 80px rgba(0, 0, 0, 0.9)',
|
||||||
|
width: '500px',
|
||||||
|
maxWidth: '94vw',
|
||||||
|
overflow: 'hidden',
|
||||||
|
color: '#fff',
|
||||||
|
},
|
||||||
|
'& .MuiBackdrop-root': {
|
||||||
|
background: 'rgba(2, 4, 8, 0.88)',
|
||||||
|
backdropFilter: 'blur(4px)',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export default OnlinePlayersDialog;
|
export default OnlinePlayersDialog;
|
||||||
|
|
||||||
|
OnlinePlayersDialog.propTypes = {
|
||||||
|
open: bool.isRequired,
|
||||||
|
onClose: func.isRequired,
|
||||||
|
currentGameAssoc: string,
|
||||||
|
isEnvDev: bool,
|
||||||
|
};
|
||||||
|
|||||||
@@ -8,47 +8,57 @@
|
|||||||
*/
|
*/
|
||||||
import { Fragment, useState } from 'react';
|
import { Fragment, useState } from 'react';
|
||||||
import { OnlinePlayersDialog } from '@mine-components';
|
import { OnlinePlayersDialog } from '@mine-components';
|
||||||
|
import { bool, string } from 'prop-types';
|
||||||
|
|
||||||
const WaitingOverlayContent = ({ shareUrl, currentGameAssoc }) => {
|
const WaitingOverlayContent = ({ shareUrl, currentGameAssoc, opponentName = '', inviteOnly = false }) => {
|
||||||
const [dialogOpen, setDialogOpen] = useState(false);
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
|
const inviteHeader = inviteOnly && opponentName
|
||||||
|
? `Invite ${opponentName}`
|
||||||
|
: 'Invite a Friend';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<div className="waiting-options">
|
<div className={`waiting-options${inviteOnly ? ' waiting-options--invite-only' : ''}`}>
|
||||||
<div className="waiting-option">
|
<div className="waiting-option">
|
||||||
<div className="waiting-option-header">
|
<div className="waiting-option-header">
|
||||||
<i className="fa fa-link" />
|
<i className="fa fa-link" />
|
||||||
<span>Invite a Friend</span>
|
<span>{inviteHeader}</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="waiting-option-desc">Share this link with your opponent</p>
|
<p className="waiting-option-desc">Share this link with your opponent</p>
|
||||||
<ShareLinkBox
|
<ShareLinkBox
|
||||||
url={shareUrl}
|
url={shareUrl}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="waiting-divider">
|
{!inviteOnly && (
|
||||||
<span>OR</span>
|
<Fragment>
|
||||||
</div>
|
<div className="waiting-divider">
|
||||||
<div className="waiting-option">
|
<span>OR</span>
|
||||||
<div className="waiting-option-header">
|
</div>
|
||||||
<i className="fa fa-users" />
|
<div className="waiting-option">
|
||||||
<span>Challenge a Player</span>
|
<div className="waiting-option-header">
|
||||||
</div>
|
<i className="fa fa-users" />
|
||||||
<p className="waiting-option-desc">Browse online players and challenge them</p>
|
<span>Challenge a Player</span>
|
||||||
<button
|
</div>
|
||||||
className="browse-players-btn"
|
<p className="waiting-option-desc">Browse online players and challenge them</p>
|
||||||
onClick={() => setDialogOpen(true)}
|
<button
|
||||||
>
|
className="browse-players-btn"
|
||||||
<i className="fa fa-search" />
|
onClick={() => setDialogOpen(true)}
|
||||||
Browse Players
|
>
|
||||||
</button>
|
<i className="fa fa-search" />
|
||||||
</div>
|
Browse Players
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<OnlinePlayersDialog
|
{!inviteOnly && (
|
||||||
open={dialogOpen}
|
<OnlinePlayersDialog
|
||||||
onClose={() => setDialogOpen(false)}
|
open={dialogOpen}
|
||||||
currentGameAssoc={currentGameAssoc}
|
onClose={() => setDialogOpen(false)}
|
||||||
/>
|
currentGameAssoc={currentGameAssoc}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -57,10 +67,12 @@ const ShareLinkBox = ({ url }) => {
|
|||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
const handleCopy = () => {
|
const handleCopy = () => {
|
||||||
navigator.clipboard.writeText(url).then(() => {
|
navigator.clipboard.writeText(url)
|
||||||
setCopied(true);
|
.then(() => {
|
||||||
setTimeout(() => setCopied(false), 2500);
|
setCopied(true);
|
||||||
}).catch(() => {});
|
setTimeout(() => setCopied(false), 2500);
|
||||||
|
})
|
||||||
|
.catch(() => null);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -83,3 +95,10 @@ const ShareLinkBox = ({ url }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default WaitingOverlayContent;
|
export default WaitingOverlayContent;
|
||||||
|
|
||||||
|
WaitingOverlayContent.propTypes = {
|
||||||
|
shareUrl: string.isRequired,
|
||||||
|
currentGameAssoc: string,
|
||||||
|
opponentName: string,
|
||||||
|
inviteOnly: bool,
|
||||||
|
};
|
||||||
|
|||||||
@@ -7,20 +7,35 @@
|
|||||||
* file that was distributed with this source code.
|
* file that was distributed with this source code.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { Fragment } from 'react';
|
import React, { Fragment, useState } from 'react';
|
||||||
import { useGame } from '@mine-contexts';
|
import { useGame } from '@mine-contexts';
|
||||||
import GridField from './GridField';
|
import GridField from './GridField';
|
||||||
import UserControl from '../user/UserControl';
|
import UserControl from '../user/UserControl';
|
||||||
import GameTimer from '../GameTimer';
|
import GameTimer from '../GameTimer';
|
||||||
import { BOMB_SYMBOLS, bombRadius } from '@mine-utils';
|
import { BOMB_SYMBOLS, bombRadius } from '@mine-utils';
|
||||||
|
import { func, string } from 'prop-types';
|
||||||
|
|
||||||
const GridControl = ({ onClick, resign }) => {
|
const GridControl = ({ gameAssoc, onClick, resign }) => {
|
||||||
const {
|
const {
|
||||||
overlay, overlayTitle, overlaySubTitle,
|
overlay, overlayTitle, overlaySubTitle,
|
||||||
webPlayer, activePlayer, bombSelected,
|
webPlayer, activePlayer, bombSelected,
|
||||||
cells, setCells,
|
cells, setCells, endRef,
|
||||||
} = useGame();
|
} = useGame();
|
||||||
|
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
const shareUrl = gameAssoc ? `${window.location.origin}/play/${gameAssoc}` : null;
|
||||||
|
const endShareUrl = gameAssoc ? `${window.location.origin}/battle/${gameAssoc}` : null;
|
||||||
|
const isAuthenticated = '1' === document.getElementById('mine-wrapper')?.dataset.isAuthenticated;
|
||||||
|
|
||||||
|
const handleShare = () => {
|
||||||
|
const url = endRef.current ? endShareUrl : shareUrl;
|
||||||
|
if (!url) return;
|
||||||
|
navigator.clipboard.writeText(url).then(() => {
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2200);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const handleHover = (row, col) => {
|
const handleHover = (row, col) => {
|
||||||
if (!bombSelected) return;
|
if (!bombSelected) return;
|
||||||
const activeColor = activePlayer ? 'blue' : 'red';
|
const activeColor = activePlayer ? 'blue' : 'red';
|
||||||
@@ -47,7 +62,34 @@ const GridControl = ({ onClick, resign }) => {
|
|||||||
<div className={`game-overlay${overlay ? '' : ' hide'}`}>
|
<div className={`game-overlay${overlay ? '' : ' hide'}`}>
|
||||||
<div className="game-overlay-window">
|
<div className="game-overlay-window">
|
||||||
<h1>{overlayTitle}</h1>
|
<h1>{overlayTitle}</h1>
|
||||||
<h2>{overlaySubTitle}</h2>
|
{'string' === typeof overlaySubTitle && (
|
||||||
|
<h2>{overlaySubTitle}</h2>
|
||||||
|
)}
|
||||||
|
{'string' !== typeof overlaySubTitle && (
|
||||||
|
<Fragment>{overlaySubTitle}</Fragment>
|
||||||
|
)}
|
||||||
|
{gameAssoc && endRef.current && (
|
||||||
|
<div className="game-overlay-actions">
|
||||||
|
<button
|
||||||
|
className={`game-overlay-share${copied ? ' copied' : ''}`}
|
||||||
|
onClick={handleShare}
|
||||||
|
title="Copy share link"
|
||||||
|
aria-label="Copy share link"
|
||||||
|
>
|
||||||
|
<i className={`fa ${copied ? 'fa-check' : 'fa-share-alt'}`} />
|
||||||
|
{copied ? 'Copied!' : 'Share Battle'}
|
||||||
|
</button>
|
||||||
|
<a
|
||||||
|
className="game-overlay-profile"
|
||||||
|
href={isAuthenticated ? '/profile' : '/'}
|
||||||
|
title={isAuthenticated ? 'Go to your profile' : 'Go to homepage'}
|
||||||
|
aria-label={isAuthenticated ? 'Go to your profile' : 'Go to homepage'}
|
||||||
|
>
|
||||||
|
<i className={`fa ${isAuthenticated ? 'fa-user' : 'fa-house'}`} />
|
||||||
|
{isAuthenticated ? 'My Profile' : 'Homepage'}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<UserControl
|
<UserControl
|
||||||
@@ -73,3 +115,9 @@ const GridControl = ({ onClick, resign }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default GridControl;
|
export default GridControl;
|
||||||
|
|
||||||
|
GridControl.propTypes = {
|
||||||
|
gameAssoc: string,
|
||||||
|
onClick: func.isRequired,
|
||||||
|
resign: func.isRequired,
|
||||||
|
};
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
import React, { memo, useMemo } from 'react';
|
import React, { memo, useMemo } from 'react';
|
||||||
import { IMAGES } from '@mine-utils';
|
import { IMAGES } from '@mine-utils';
|
||||||
|
import { func, shape, bool, number, string } from 'prop-types';
|
||||||
|
|
||||||
const bombSrc = area => {
|
const bombSrc = area => {
|
||||||
if (null === area) return null;
|
if (null === area) return null;
|
||||||
@@ -53,7 +54,10 @@ const GridField = memo(function GridField({ cell, onClick, onMouseEnter }) {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className={fieldClass}>
|
<div className={fieldClass}>
|
||||||
<div className="field-corner">
|
<div
|
||||||
|
style={{ background: "url('/images/bg-corner-outbg.png') no-repeat top left / 100% 100%" }}
|
||||||
|
className="field-corner"
|
||||||
|
>
|
||||||
{isNaN(currentImage) && (
|
{isNaN(currentImage) && (
|
||||||
<div className="flag-mine">
|
<div className="flag-mine">
|
||||||
<img src={currentImage} alt="" />
|
<img src={currentImage} alt="" />
|
||||||
@@ -72,3 +76,16 @@ const GridField = memo(function GridField({ cell, onClick, onMouseEnter }) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export default GridField;
|
export default GridField;
|
||||||
|
|
||||||
|
GridField.propTypes = {
|
||||||
|
cell: shape({
|
||||||
|
currentImage: string,
|
||||||
|
currentObj: string,
|
||||||
|
active: bool,
|
||||||
|
lastClickedRed: bool,
|
||||||
|
lastClickedBlue: bool,
|
||||||
|
bombTargetArea: number,
|
||||||
|
}).isRequired,
|
||||||
|
onClick: func.isRequired,
|
||||||
|
onMouseEnter: func.isRequired,
|
||||||
|
};
|
||||||
|
|||||||
@@ -10,8 +10,13 @@
|
|||||||
export { GameBoard } from './GameBoard';
|
export { GameBoard } from './GameBoard';
|
||||||
export { default as OnlinePlayersDialog } from './OnlinePlayersDialog';
|
export { default as OnlinePlayersDialog } from './OnlinePlayersDialog';
|
||||||
export { default as WaitingOverlayContent } from './WaitingOverlayContent';
|
export { default as WaitingOverlayContent } from './WaitingOverlayContent';
|
||||||
|
export { default as ChallengeCountdown } from './ChallengeCountdown';
|
||||||
export { default as GameTimer } from './GameTimer';
|
export { default as GameTimer } from './GameTimer';
|
||||||
export { default as GridControl } from './grid/GridControl';
|
export { default as GridControl } from './grid/GridControl';
|
||||||
export { default as GridField } from './grid/GridField';
|
export { default as GridField } from './grid/GridField';
|
||||||
export { default as User } from './user/User';
|
export { default as User } from './user/User';
|
||||||
export { default as UserControl } from './user/UserControl';
|
export { default as UserControl } from './user/UserControl';
|
||||||
|
export { default as BonusBox } from './BonusBox';
|
||||||
|
export { default as BonusStatsDialog } from './BonusStatsDialog';
|
||||||
|
export { Avatar } from './timer/Avatar';
|
||||||
|
export { PlayerColumn } from './profile/PlayerColumn';
|
||||||
|
|||||||
52
assets/js/mine-seeker/components/profile/PlayerColumn.jsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { object, string } from 'prop-types';
|
||||||
|
import { BONUS_LABELS } from '@mine-utils';
|
||||||
|
|
||||||
|
const formatPlayerName = name => {
|
||||||
|
if (name && name.startsWith('anon_')) {
|
||||||
|
return 'Anonymous';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name && 10 < name.length) {
|
||||||
|
return name.substring(0, 7) + '...';
|
||||||
|
}
|
||||||
|
|
||||||
|
return name || 'Unknown';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PlayerColumn = ({ color, player }) => (
|
||||||
|
<div className={`bsd-column bsd-column--${color}`}>
|
||||||
|
<div className="bsd-column-header">
|
||||||
|
<span className="bsd-column-name">{formatPlayerName(player.name)}</span>
|
||||||
|
<span className="bsd-column-total">
|
||||||
|
<i className="fa fa-star" />
|
||||||
|
{player.bonusPoints}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<ul className="bsd-stats">
|
||||||
|
{Object.entries(BONUS_LABELS).map(([key, { label, desc }]) => (
|
||||||
|
<li key={key} className="bsd-stat">
|
||||||
|
<div className="bsd-stat-text">
|
||||||
|
<span className="bsd-stat-label">{label}</span>
|
||||||
|
<span className="bsd-stat-desc">{desc}</span>
|
||||||
|
</div>
|
||||||
|
<span className="bsd-stat-value">{player.bonusStats?.[key] ?? 0}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
PlayerColumn.propTypes = {
|
||||||
|
color: string.isRequired,
|
||||||
|
player: object.isRequired,
|
||||||
|
};
|
||||||
28
assets/js/mine-seeker/components/timer/Avatar.jsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { object } from 'prop-types';
|
||||||
|
|
||||||
|
export const Avatar = ({ player }) => {
|
||||||
|
if (!player.registered) return '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="timer-avatar">
|
||||||
|
{player.avatar
|
||||||
|
? <img src={player.avatar} alt={player.name} className="timer-avatar__img" />
|
||||||
|
: <span className="timer-avatar__initials">{player.name.slice(0, 2).toUpperCase()}</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Avatar.propTypes = {
|
||||||
|
player: object.isRequired,
|
||||||
|
};
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
import React, { memo } from 'react';
|
import React, { memo } from 'react';
|
||||||
import { IMAGES } from '@mine-utils';
|
import { IMAGES } from '@mine-utils';
|
||||||
|
import { bool, func, number, string } from 'prop-types';
|
||||||
|
|
||||||
const User = memo(function User(
|
const User = memo(function User(
|
||||||
{
|
{
|
||||||
@@ -52,3 +53,15 @@ const User = memo(function User(
|
|||||||
});
|
});
|
||||||
|
|
||||||
export default User;
|
export default User;
|
||||||
|
|
||||||
|
User.propTypes = {
|
||||||
|
color: string.isRequired,
|
||||||
|
webPlayer: string,
|
||||||
|
name: string,
|
||||||
|
desc: string,
|
||||||
|
active: bool,
|
||||||
|
mines: number,
|
||||||
|
haveBomb: bool,
|
||||||
|
enabledBomb: bool,
|
||||||
|
onClickBombSelector: func.isRequired,
|
||||||
|
};
|
||||||
|
|||||||
@@ -7,15 +7,19 @@
|
|||||||
* file that was distributed with this source code.
|
* file that was distributed with this source code.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, { Fragment, useState } from 'react';
|
||||||
import { useGame } from '@mine-contexts';
|
import { useGame } from '@mine-contexts';
|
||||||
import User from './User';
|
import User from './User';
|
||||||
|
import BonusStatsDialog from '../BonusStatsDialog';
|
||||||
|
import { func } from 'prop-types';
|
||||||
|
|
||||||
const UserControl = ({ resign }) => {
|
const UserControl = ({ resign }) => {
|
||||||
const { webPlayer, activePlayer, mines, foundMines, red, blue, onBombToggle } = useGame();
|
const { webPlayer, activePlayer, foundMines, red, blue, onBombToggle } = useGame();
|
||||||
|
const [bonusDialogOpen, setBonusDialogOpen] = useState(false);
|
||||||
const activeColor = activePlayer ? 'blue' : 'red';
|
const activeColor = activePlayer ? 'blue' : 'red';
|
||||||
const resignClass = 'resign' + (activeColor !== webPlayer ? ' disabled' : '');
|
const resignClass = 'resign' + (activeColor !== webPlayer ? ' disabled' : '');
|
||||||
const minesClass = 'active-mines' + (foundMines ? ' found-mine' : '');
|
const minesClass = 'active-mines' + (foundMines ? ' found-mine' : '');
|
||||||
|
const remainingMines = 51 - red.mines - blue.mines;
|
||||||
|
|
||||||
const handleBombClick = (color, player) => {
|
const handleBombClick = (color, player) => {
|
||||||
const p = 'red' === color ? red : blue;
|
const p = 'red' === color ? red : blue;
|
||||||
@@ -24,31 +28,49 @@ const UserControl = ({ resign }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleBonusClick = () => {
|
||||||
|
setBonusDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="users">
|
<Fragment>
|
||||||
<User
|
<div className="users">
|
||||||
color="blue" webPlayer={webPlayer} {...blue}
|
<User
|
||||||
onClickBombSelector={() => handleBombClick('blue', 1)}
|
color="blue" webPlayer={webPlayer} {...blue}
|
||||||
/>
|
onClickBombSelector={() => handleBombClick('blue', 1)}
|
||||||
<div className="active-mines-container">
|
onBonusClick={handleBonusClick}
|
||||||
<i className="fa fa-star" />
|
/>
|
||||||
<div className={minesClass}>
|
<div className="active-mines-container">
|
||||||
<div className="active-mines-nbr">{mines}</div>
|
<i className="fa fa-star" />
|
||||||
<div className="active-mines-shine" />
|
<div className={minesClass}>
|
||||||
|
<div className="active-mines-nbr">{remainingMines}</div>
|
||||||
|
<div className="active-mines-shine" />
|
||||||
|
</div>
|
||||||
|
<i className="fa fa-star" />
|
||||||
</div>
|
</div>
|
||||||
<i className="fa fa-star" />
|
<div className="clear" />
|
||||||
|
<User
|
||||||
|
color="red" webPlayer={webPlayer} {...red}
|
||||||
|
onClickBombSelector={() => handleBombClick('red', 0)}
|
||||||
|
onBonusClick={handleBonusClick}
|
||||||
|
/>
|
||||||
|
<button className={resignClass} onClick={resign}>
|
||||||
|
<div className="resign-shine" />
|
||||||
|
Resign
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="clear" />
|
<BonusStatsDialog
|
||||||
<User
|
open={bonusDialogOpen}
|
||||||
color="red" webPlayer={webPlayer} {...red}
|
onClose={() => setBonusDialogOpen(false)}
|
||||||
onClickBombSelector={() => handleBombClick('red', 0)}
|
red={red}
|
||||||
|
blue={blue}
|
||||||
/>
|
/>
|
||||||
<button className={resignClass} onClick={resign}>
|
</Fragment>
|
||||||
<div className="resign-shine" />
|
|
||||||
Resign
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default UserControl;
|
export default UserControl;
|
||||||
|
|
||||||
|
UserControl.propTypes = {
|
||||||
|
resign: func.isRequired,
|
||||||
|
};
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ export const GameProvider = ({ children }) => {
|
|||||||
connectionLost, setConnectionLost,
|
connectionLost, setConnectionLost,
|
||||||
} = useGameState();
|
} = useGameState();
|
||||||
|
|
||||||
|
const [gameUuid, setGameUuid] = React.useState(null);
|
||||||
|
|
||||||
const sounds = useRef({
|
const sounds = useRef({
|
||||||
click: new Howl({ src: ['/sound/click.mp3'] }),
|
click: new Howl({ src: ['/sound/click.mp3'] }),
|
||||||
bomb: new Howl({ src: ['/sound/bomb.mp3'] }),
|
bomb: new Howl({ src: ['/sound/bomb.mp3'] }),
|
||||||
@@ -130,7 +132,18 @@ export const GameProvider = ({ children }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const applyStep = stepData => {
|
const applyStep = stepData => {
|
||||||
const { player, bomb: isBomb, minesFound = 0, revealedCells = [], redPoints: rp, bluePoints: bp } = stepData;
|
const {
|
||||||
|
player,
|
||||||
|
bomb: isBomb,
|
||||||
|
minesFound = 0,
|
||||||
|
revealedCells = [],
|
||||||
|
redPoints: rp,
|
||||||
|
bluePoints: bp,
|
||||||
|
redBonusPoints = 0,
|
||||||
|
blueBonusPoints = 0,
|
||||||
|
redBonusStats = {},
|
||||||
|
blueBonusStats = {},
|
||||||
|
} = stepData;
|
||||||
|
|
||||||
if (isBomb) {
|
if (isBomb) {
|
||||||
sounds.current.bomb.play();
|
sounds.current.bomb.play();
|
||||||
@@ -174,6 +187,18 @@ export const GameProvider = ({ children }) => {
|
|||||||
syncBlue(p => ({ ...p, mines: 'blue' === player ? bp : p.mines }));
|
syncBlue(p => ({ ...p, mines: 'blue' === player ? bp : p.mines }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Update bonus points and stats */
|
||||||
|
syncRed(p => ({
|
||||||
|
...p,
|
||||||
|
bonusPoints: 'red' === player ? redBonusPoints : p.bonusPoints,
|
||||||
|
bonusStats: 'red' === player ? redBonusStats : p.bonusStats,
|
||||||
|
}));
|
||||||
|
syncBlue(p => ({
|
||||||
|
...p,
|
||||||
|
bonusPoints: 'blue' === player ? blueBonusPoints : p.bonusPoints,
|
||||||
|
bonusStats: 'blue' === player ? blueBonusStats : p.bonusStats,
|
||||||
|
}));
|
||||||
|
|
||||||
syncRed(p => ({ ...p, enabledBomb: rp <= bp }));
|
syncRed(p => ({ ...p, enabledBomb: rp <= bp }));
|
||||||
syncBlue(p => ({ ...p, enabledBomb: bp <= rp }));
|
syncBlue(p => ({ ...p, enabledBomb: bp <= rp }));
|
||||||
|
|
||||||
@@ -193,7 +218,10 @@ export const GameProvider = ({ children }) => {
|
|||||||
if (redWins || blueWins || resign) {
|
if (redWins || blueWins || resign) {
|
||||||
sounds.current.won.play();
|
sounds.current.won.play();
|
||||||
|
|
||||||
if (!resign) showOverlay((redWins ? 'Red' : 'Blue') + ' wins the game!', 'Play again!');
|
if (!resign) {
|
||||||
|
endRef.current = true;
|
||||||
|
showOverlay((redWins ? 'Red' : 'Blue') + ' wins the game!', null);
|
||||||
|
}
|
||||||
|
|
||||||
showLeftMines(leftMines);
|
showLeftMines(leftMines);
|
||||||
syncActivePlayer(false);
|
syncActivePlayer(false);
|
||||||
@@ -202,8 +230,11 @@ export const GameProvider = ({ children }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const resignProcess = color => {
|
const resignProcess = (color, uuid = null) => {
|
||||||
const wp = webPlayerRef.current;
|
const wp = webPlayerRef.current;
|
||||||
|
if (uuid) {
|
||||||
|
setGameUuid(uuid);
|
||||||
|
}
|
||||||
showOverlay(
|
showOverlay(
|
||||||
color === wp ? 'You have been give up' : 'Your opponent has been resigned',
|
color === wp ? 'You have been give up' : 'Your opponent has been resigned',
|
||||||
color === wp ? 'You LOSE!' : 'You WIN!',
|
color === wp ? 'You LOSE!' : 'You WIN!',
|
||||||
@@ -223,20 +254,20 @@ export const GameProvider = ({ children }) => {
|
|||||||
return (
|
return (
|
||||||
<GameContext.Provider
|
<GameContext.Provider
|
||||||
value={{
|
value={{
|
||||||
// State (for rendering)
|
/** State (for rendering) */
|
||||||
webPlayer, activePlayer, overlay, overlayTitle, overlaySubTitle,
|
webPlayer, activePlayer, overlay, overlayTitle, overlaySubTitle,
|
||||||
mines, bombSelected, foundMines, red, blue, cells, gridReady, connectionLost,
|
mines, bombSelected, foundMines, red, blue, cells, gridReady, connectionLost, gameUuid,
|
||||||
// Setters needed by useServerComm
|
/** Setters needed by useServerComm */
|
||||||
setCells, setGridReady,
|
setCells, setGridReady, setGameUuid,
|
||||||
// Refs (needed by useServerComm for async-safe reads)
|
/** Refs (needed by useServerComm for async-safe reads) */
|
||||||
webPlayerRef, activePlayerRef, bombSelectedRef, connectionLostRef, endRef,
|
webPlayerRef, activePlayerRef, bombSelectedRef, connectionLostRef, lastClickedRef, endRef,
|
||||||
// Sync helpers
|
/** Sync helpers */
|
||||||
syncWebPlayer, syncActivePlayer, syncBombSelected, syncConnLost, syncRed, syncBlue,
|
syncWebPlayer, syncActivePlayer, syncBombSelected, syncConnLost, syncRed, syncBlue,
|
||||||
// Game logic called by useServerComm
|
/** Game logic called by useServerComm */
|
||||||
showOverlay, hideOverlay,
|
showOverlay, hideOverlay,
|
||||||
applyRevealedCell, applyStep,
|
applyRevealedCell, applyStep,
|
||||||
makeGameEndIfItEnds, resignProcess,
|
makeGameEndIfItEnds, resignProcess,
|
||||||
// UI action
|
/** UI action */
|
||||||
onBombToggle,
|
onBombToggle,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -11,4 +11,4 @@ export { default as useGameRefs } from './useGameRefs';
|
|||||||
export { default as useGameState } from './useGameState';
|
export { default as useGameState } from './useGameState';
|
||||||
export { default as useServerCommunication } from './useServerCommunication';
|
export { default as useServerCommunication } from './useServerCommunication';
|
||||||
export { default as useStepTimer } from './useStepTimer';
|
export { default as useStepTimer } from './useStepTimer';
|
||||||
|
export { default as useGameDataProvider, useLobbyDataProvider } from './useGameDataProvider';
|
||||||
|
|||||||
132
assets/js/mine-seeker/hooks/useGameDataProvider.js
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useQuery, useMutation } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Game Data Provider Hook
|
||||||
|
* Centralized API communication layer for game-related queries and mutations
|
||||||
|
*/
|
||||||
|
const useGameDataProvider = gameAssoc => {
|
||||||
|
// Queries
|
||||||
|
const connectQuery = useQuery({
|
||||||
|
queryKey: ['game-connect', gameAssoc],
|
||||||
|
queryFn: () => fetch(`/api/game/connect/${gameAssoc}`)
|
||||||
|
.then(r => r.text())
|
||||||
|
.then(b64 => JSON.parse(window.atob(b64))),
|
||||||
|
enabled: false,
|
||||||
|
retry: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mutations
|
||||||
|
const startMutation = useMutation({
|
||||||
|
mutationFn: () => fetch('/api/game/start', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ gameAssoc }),
|
||||||
|
}).then(r => r.json()),
|
||||||
|
});
|
||||||
|
|
||||||
|
const joinMutation = useMutation({
|
||||||
|
mutationFn: () => fetch(`/api/game/join/${gameAssoc}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const stepMutation = useMutation({
|
||||||
|
mutationFn: dataPack => fetch(`/api/game/step/${gameAssoc}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(dataPack),
|
||||||
|
}).then(r => r.json()),
|
||||||
|
});
|
||||||
|
|
||||||
|
const heartbeatMutation = useMutation({
|
||||||
|
mutationFn: color => fetch(`/api/game/heartbeat/${gameAssoc}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ color }),
|
||||||
|
}).then(r => r.json()),
|
||||||
|
});
|
||||||
|
|
||||||
|
const challengeRespondMutation = useMutation({
|
||||||
|
mutationFn: ({ challengerGameAssoc, accepted, targetGameAssoc }) => fetch('/api/game/challenge/respond/' + challengerGameAssoc, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ accepted, targetGameAssoc }),
|
||||||
|
}).then(r => r.json()),
|
||||||
|
});
|
||||||
|
|
||||||
|
const leaveMutation = useMutation({
|
||||||
|
mutationFn: () => fetch(`/api/game/leave/${gameAssoc}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
}).then(r => r.json()),
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
// Queries
|
||||||
|
connectQuery,
|
||||||
|
// Mutations
|
||||||
|
startMutation,
|
||||||
|
joinMutation,
|
||||||
|
stepMutation,
|
||||||
|
heartbeatMutation,
|
||||||
|
challengeRespondMutation,
|
||||||
|
leaveMutation,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lobby Data Provider Hook
|
||||||
|
* Centralized API communication layer for lobby-related queries and mutations
|
||||||
|
*/
|
||||||
|
export const useLobbyDataProvider = () => {
|
||||||
|
const waitingPlayersQuery = useQuery({
|
||||||
|
queryKey: ['game-waiting'],
|
||||||
|
queryFn: () => fetch('/api/game/waiting')
|
||||||
|
.then(r => r.json()),
|
||||||
|
});
|
||||||
|
|
||||||
|
const challengeMutation = useMutation({
|
||||||
|
mutationFn: ({ targetGameAssoc, challengerGameAssoc }) => fetch(`/api/game/challenge/${targetGameAssoc}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ challengerGameAssoc }),
|
||||||
|
}).then(r => r.json()),
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
// Queries
|
||||||
|
waitingPlayersQuery,
|
||||||
|
// Mutations
|
||||||
|
challengeMutation,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useGameDataProvider;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Profile Data Provider Hook
|
||||||
|
* Centralized API communication layer for profile-related mutations
|
||||||
|
*/
|
||||||
|
export const useProfileDataProvider = () => {
|
||||||
|
const uploadAvatarMutation = useMutation({
|
||||||
|
mutationFn: ({ uploadUrl, file }) => {
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append('avatar', file);
|
||||||
|
return fetch(uploadUrl, { method: 'POST', body: fd })
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => { if (data.error) throw new Error(data.error); return data; });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return { uploadAvatarMutation };
|
||||||
|
};
|
||||||
@@ -8,104 +8,230 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect, useRef } from 'react';
|
||||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
|
||||||
import { useGame } from '@mine-contexts';
|
import { useGame } from '@mine-contexts';
|
||||||
import { DESC } from '@mine-utils';
|
import { DESC, IMAGES } from '@mine-utils';
|
||||||
import useStepTimer from './useStepTimer';
|
import { ChallengeCountdown, WaitingOverlayContent } from '@mine-components';
|
||||||
import { WaitingOverlayContent } from '@mine-components';
|
import { useGameDataProvider, useStepTimer } from '@mine-hooks';
|
||||||
|
|
||||||
/** Handles all server communication: SSE (Mercure), REST calls, and the initialization lifecycle. */
|
const useServerCommunication = (gameAssoc, gameInherited, opponentName, isEnvDev) => {
|
||||||
const useServerCommunication = (gameAssoc, gameInherited, isEnvDev) => {
|
|
||||||
const {
|
const {
|
||||||
/** Async-safe refs */
|
/** Async-safe refs */
|
||||||
webPlayerRef, activePlayerRef, bombSelectedRef, connectionLostRef, endRef,
|
webPlayerRef, activePlayerRef, bombSelectedRef, connectionLostRef, lastClickedRef, endRef,
|
||||||
/** State setters */
|
/** State setters */
|
||||||
setGridReady,
|
setCells, setGridReady, setGameUuid,
|
||||||
/** Sync helpers */
|
/** Sync helpers */
|
||||||
syncWebPlayer, syncActivePlayer, syncBombSelected, syncConnLost, syncRed, syncBlue,
|
syncWebPlayer, syncActivePlayer, syncBombSelected, syncConnLost, syncRed, syncBlue,
|
||||||
/** Game logic */
|
/** Game logic */
|
||||||
showOverlay, hideOverlay,
|
showOverlay, hideOverlay, applyStep, makeGameEndIfItEnds, resignProcess,
|
||||||
applyRevealedCell, applyStep,
|
|
||||||
makeGameEndIfItEnds, resignProcess,
|
|
||||||
/** Current cells snapshot (for active-check in onClick) */
|
/** Current cells snapshot (for active-check in onClick) */
|
||||||
cells,
|
cells,
|
||||||
} = useGame();
|
} = useGame();
|
||||||
|
|
||||||
|
/** Get all API queries and mutations from data provider */
|
||||||
|
const {
|
||||||
|
connectQuery,
|
||||||
|
startMutation,
|
||||||
|
joinMutation,
|
||||||
|
stepMutation,
|
||||||
|
heartbeatMutation,
|
||||||
|
challengeRespondMutation,
|
||||||
|
leaveMutation,
|
||||||
|
} = useGameDataProvider(gameAssoc);
|
||||||
|
|
||||||
const eventSourceRef = useRef(null);
|
const eventSourceRef = useRef(null);
|
||||||
const rpcUsersRef = useRef(null);
|
const rpcUsersRef = useRef(null);
|
||||||
const stepCacheRef = useRef([]);
|
const stepCacheRef = useRef([]);
|
||||||
|
const lastStepRef = useRef(null);
|
||||||
|
const isGameFinishedRef = useRef(false);
|
||||||
const { getStepElapsed, resetStepTimer, startNewTurn } = useStepTimer();
|
const { getStepElapsed, resetStepTimer, startNewTurn } = useStepTimer();
|
||||||
const isGameRunningRef = useRef(false);
|
const isGameRunningRef = useRef(false);
|
||||||
const lastActivePlayerRef = useRef(null);
|
const lastActivePlayerRef = useRef(null);
|
||||||
|
const heartbeatPubIntervalRef = useRef(null);
|
||||||
|
const opponentLastSeenRef = useRef(0);
|
||||||
|
const isTrueRestoredRef = useRef(false);
|
||||||
|
|
||||||
/** REST mutations / queries */
|
const HEARTBEAT_INTERVAL_MS = 1500;
|
||||||
|
|
||||||
const connectQuery = useQuery({
|
|
||||||
queryKey: ['game-connect', gameAssoc],
|
|
||||||
queryFn: () => fetch('/api/game/connect/' + gameAssoc)
|
|
||||||
.then(r => r.text())
|
|
||||||
.then(b64 => JSON.parse(window.atob(b64))),
|
|
||||||
enabled: false,
|
|
||||||
retry: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const startMutation = useMutation({
|
|
||||||
mutationFn: () => fetch('/api/game/start', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ gameAssoc }),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const joinMutation = useMutation({
|
|
||||||
mutationFn: () => fetch('/api/game/join/' + gameAssoc, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
}).catch(e => isEnvDev && console.error('Join error', e)),
|
|
||||||
});
|
|
||||||
|
|
||||||
const stepMutation = useMutation({
|
|
||||||
mutationFn: dataPack => fetch('/api/game/step/' + gameAssoc, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(dataPack),
|
|
||||||
}).then(r => r.json()),
|
|
||||||
});
|
|
||||||
|
|
||||||
/** Game-start helpers (triggered by server events) */
|
/** Game-start helpers (triggered by server events) */
|
||||||
|
|
||||||
const wInit = (revealedCells = []) => {
|
const wInit = (revealedCells = [], lastStep = {}, gameState = {}, isGameFinished = false) => {
|
||||||
setGridReady(true);
|
/** Detect if this is a restored game */
|
||||||
showOverlay('Choose an opponent!', gameAssoc ? (
|
const isRestoredGame = 0 < revealedCells.length;
|
||||||
<WaitingOverlayContent
|
isTrueRestoredRef.current = isRestoredGame;
|
||||||
shareUrl={`${window.location.href}/${gameAssoc}`}
|
|
||||||
currentGameAssoc={gameAssoc}
|
/** Store game finished status */
|
||||||
/>
|
isGameFinishedRef.current = isGameFinished;
|
||||||
) : '');
|
|
||||||
setTimeout(() => revealedCells.forEach(cell => applyRevealedCell(cell, cell.player)), 0);
|
/** Apply game state (points, bonus) immediately for restored games */
|
||||||
|
if (0 < Object.keys(gameState).length) {
|
||||||
|
const {
|
||||||
|
redPoints = 0,
|
||||||
|
bluePoints = 0,
|
||||||
|
redBonusPoints = 0,
|
||||||
|
blueBonusPoints = 0,
|
||||||
|
redBonusStats = {},
|
||||||
|
blueBonusStats = {},
|
||||||
|
} = gameState;
|
||||||
|
syncRed(p => ({
|
||||||
|
...p,
|
||||||
|
mines: redPoints,
|
||||||
|
bonusPoints: redBonusPoints,
|
||||||
|
bonusStats: redBonusStats,
|
||||||
|
}));
|
||||||
|
syncBlue(p => ({
|
||||||
|
...p,
|
||||||
|
mines: bluePoints,
|
||||||
|
bonusPoints: blueBonusPoints,
|
||||||
|
bonusStats: blueBonusStats,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Apply revealed cells immediately (not in setTimeout) */
|
||||||
|
if (0 < revealedCells.length) {
|
||||||
|
setCells(prev => {
|
||||||
|
let next = prev.map(r => [...r]);
|
||||||
|
revealedCells.forEach(({ row, col, value, player }) => {
|
||||||
|
if (next[row][col].active) return;
|
||||||
|
/** Check if this cell is the last step for either player */
|
||||||
|
const isRedLastStep = lastStep.red && lastStep.red.player === player && lastStep.red.row === row && lastStep.red.col === col;
|
||||||
|
const isBlueLastStep = lastStep.blue && lastStep.blue.player === player && lastStep.blue.row === row && lastStep.blue.col === col;
|
||||||
|
const patch = 'm' === value
|
||||||
|
? { currentImage: IMAGES.flag(player), currentObj: 'm', active: true }
|
||||||
|
: { currentImage: value, currentObj: value, active: true };
|
||||||
|
if (isRedLastStep || isBlueLastStep) {
|
||||||
|
patch.lastClickedRed = 'red' === player;
|
||||||
|
patch.lastClickedBlue = 'blue' === player;
|
||||||
|
}
|
||||||
|
next[row][col] = { ...next[row][col], ...patch };
|
||||||
|
});
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Update the lastClickedRef so applyStep knows about it */
|
||||||
|
if (lastStep.red) {
|
||||||
|
lastClickedRef.current = {
|
||||||
|
...lastClickedRef.current,
|
||||||
|
red: [lastStep.red.row, lastStep.red.col],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (lastStep.blue) {
|
||||||
|
lastClickedRef.current = {
|
||||||
|
...lastClickedRef.current,
|
||||||
|
blue: [lastStep.blue.row, lastStep.blue.col],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Determine overlay message */
|
||||||
|
let overlayTitle, overlaySubtitle;
|
||||||
|
|
||||||
|
if (isGameFinished) {
|
||||||
|
/** Game is finished - show game over message */
|
||||||
|
const redPoints = gameState.redPoints ?? 0;
|
||||||
|
const bluePoints = gameState.bluePoints ?? 0;
|
||||||
|
const winner = redPoints > bluePoints ? 'Red' : 'Blue';
|
||||||
|
overlayTitle = `${winner} wins the game!`;
|
||||||
|
overlaySubtitle = 'Play again!';
|
||||||
|
/** Mark the game as ended */
|
||||||
|
endRef.current = true;
|
||||||
|
} else if (isRestoredGame) {
|
||||||
|
overlayTitle = 'Waiting for opponent to reconnect...';
|
||||||
|
overlaySubtitle = gameAssoc ? (
|
||||||
|
<WaitingOverlayContent
|
||||||
|
shareUrl={`${window.location.origin}/play/${gameAssoc}`}
|
||||||
|
currentGameAssoc={gameAssoc}
|
||||||
|
opponentName={opponentName}
|
||||||
|
inviteOnly
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div style={{ textAlign: 'center', padding: '20px' }}>
|
||||||
|
<p>Waiting for opponent to join...</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
overlayTitle = 'Choose an opponent!';
|
||||||
|
overlaySubtitle = gameAssoc ? (
|
||||||
|
<WaitingOverlayContent
|
||||||
|
shareUrl={`${window.location.origin}/play/${gameAssoc}`}
|
||||||
|
currentGameAssoc={gameAssoc}
|
||||||
|
/>
|
||||||
|
) : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
showOverlay(overlayTitle, overlaySubtitle);
|
||||||
|
|
||||||
|
/** Use Promise.resolve to defer setGridReady slightly to ensure overlay is rendered first */
|
||||||
|
Promise.resolve().then(() => setGridReady(true));
|
||||||
};
|
};
|
||||||
|
|
||||||
const makeGameStart = payload => {
|
const makeGameStart = payload => {
|
||||||
syncActivePlayer(1);
|
/** Don't start a finished game */
|
||||||
|
if (isGameFinishedRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** If game is being restored and has a most recent step, determine starter based on that */
|
||||||
|
let starterIsBlue;
|
||||||
|
|
||||||
|
/** lastStepRef contains the single most recent step from the server */
|
||||||
|
if (lastStepRef.current && lastStepRef.current.player) {
|
||||||
|
/** The NEXT player is opposite of who made the last step */
|
||||||
|
starterIsBlue = 'red' === lastStepRef.current.player; // If red played last, blue plays next
|
||||||
|
} else {
|
||||||
|
/** New game: blue always starts */
|
||||||
|
starterIsBlue = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const starterColor = starterIsBlue ? 'blue' : 'red';
|
||||||
|
const starterVal = starterIsBlue ? 1 : 0;
|
||||||
|
const starterDesc = starterColor === webPlayerRef.current ? DESC.you : DESC.buddy;
|
||||||
|
syncActivePlayer(starterVal);
|
||||||
syncRed(p => ({
|
syncRed(p => ({
|
||||||
...p,
|
...p,
|
||||||
name: payload.users.red || payload.users.redAnon || p.name,
|
name: payload.users.red || payload.users.redAnon || p.name,
|
||||||
registered: !!payload.users.red,
|
registered: !!payload.users.red,
|
||||||
avatar: payload.users.redAvatar ?? null,
|
avatar: payload.users.redAvatar ?? null,
|
||||||
|
desc: 'red' === starterColor ? starterDesc : '',
|
||||||
|
active: 'red' === starterColor,
|
||||||
}));
|
}));
|
||||||
syncBlue(p => ({
|
syncBlue(p => ({
|
||||||
...p,
|
...p,
|
||||||
name: payload.users.blue || payload.users.blueAnon || p.name,
|
name: payload.users.blue || payload.users.blueAnon || p.name,
|
||||||
registered: !!payload.users.blue,
|
registered: !!payload.users.blue,
|
||||||
avatar: payload.users.blueAvatar ?? null,
|
avatar: payload.users.blueAvatar ?? null,
|
||||||
desc: 'blue' === webPlayerRef.current ? DESC.you : DESC.buddy,
|
desc: 'blue' === starterColor ? starterDesc : '',
|
||||||
active: true,
|
active: 'blue' === starterColor,
|
||||||
}));
|
}));
|
||||||
isGameRunningRef.current = true;
|
isGameRunningRef.current = true;
|
||||||
lastActivePlayerRef.current = 1; // Blue starts
|
lastActivePlayerRef.current = starterVal;
|
||||||
startNewTurn();
|
startNewTurn();
|
||||||
resetStepTimer();
|
resetStepTimer();
|
||||||
hideOverlay();
|
/**
|
||||||
|
* For a truly restored game, keep the "Waiting for opponent..." overlay
|
||||||
|
* up until we actually see a heartbeat from the other player.
|
||||||
|
*/
|
||||||
|
if (!endRef.current && (!isTrueRestoredRef.current || 0 !== opponentLastSeenRef.current)) {
|
||||||
|
hideOverlay();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const publishHeartbeat = () => {
|
||||||
|
const me = webPlayerRef.current;
|
||||||
|
if (!me || endRef.current) return;
|
||||||
|
heartbeatMutation.mutate(me);
|
||||||
|
};
|
||||||
|
|
||||||
|
const startHeartbeat = () => {
|
||||||
|
if (heartbeatPubIntervalRef.current) return;
|
||||||
|
publishHeartbeat();
|
||||||
|
heartbeatPubIntervalRef.current = setInterval(publishHeartbeat, HEARTBEAT_INTERVAL_MS);
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopHeartbeat = () => {
|
||||||
|
if (heartbeatPubIntervalRef.current) {
|
||||||
|
clearInterval(heartbeatPubIntervalRef.current);
|
||||||
|
heartbeatPubIntervalRef.current = null;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Mercure / SSE message handlers */
|
/** Mercure / SSE message handlers */
|
||||||
@@ -131,43 +257,68 @@ const useServerCommunication = (gameAssoc, gameInherited, isEnvDev) => {
|
|||||||
|
|
||||||
const wUnsubscribe = payload => {
|
const wUnsubscribe = payload => {
|
||||||
isEnvDev && console.info(payload.msg);
|
isEnvDev && console.info(payload.msg);
|
||||||
showOverlay('The connection has been lost w/ your friend...', 'Please, restart the game!');
|
const isAuthenticated = '1' === document.getElementById('mine-wrapper')?.dataset.isAuthenticated;
|
||||||
|
const redirectPath = isAuthenticated ? '/profile' : '/';
|
||||||
|
const buttonText = isAuthenticated ? 'My Profile' : 'Homepage';
|
||||||
|
const buttonIcon = isAuthenticated ? 'fa-user' : 'fa-house';
|
||||||
|
|
||||||
|
showOverlay(
|
||||||
|
'The connection has been lost w/ your friend...',
|
||||||
|
(
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '12px', width: '100%' }}>
|
||||||
|
<p style={{ margin: 0 }}>Please, restart the game!</p>
|
||||||
|
<a
|
||||||
|
className="game-overlay-profile"
|
||||||
|
href={redirectPath}
|
||||||
|
title={isAuthenticated ? 'Go to your profile' : 'Go to homepage'}
|
||||||
|
aria-label={isAuthenticated ? 'Go to your profile' : 'Go to homepage'}
|
||||||
|
>
|
||||||
|
<i className={`fa ${buttonIcon}`} />
|
||||||
|
{buttonText}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const wChallenge = payload => {
|
const wChallenge = payload => {
|
||||||
const { challengerName, challengerGameAssoc } = payload;
|
const { challengerName, challengerGameAssoc } = payload;
|
||||||
|
let declineTimeout = null;
|
||||||
|
|
||||||
const handleAccept = () => {
|
const handleAccept = () => {
|
||||||
fetch('/api/game/challenge/respond/' + challengerGameAssoc, {
|
clearTimeout(declineTimeout);
|
||||||
method: 'POST',
|
challengeRespondMutation.mutate(
|
||||||
headers: { 'Content-Type': 'application/json' },
|
{ challengerGameAssoc, accepted: true, targetGameAssoc: gameAssoc },
|
||||||
body: JSON.stringify({ accepted: true, targetGameAssoc: gameAssoc }),
|
{
|
||||||
}).then(() => {
|
onSuccess: () => {
|
||||||
showOverlay('Challenge accepted!', 'Waiting for the challenger to join...');
|
showOverlay('Challenge accepted!', 'Waiting for the challenger to join...');
|
||||||
}).catch(() => {});
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDecline = () => {
|
const handleDecline = () => {
|
||||||
fetch('/api/game/challenge/respond/' + challengerGameAssoc, {
|
clearTimeout(declineTimeout);
|
||||||
method: 'POST',
|
challengeRespondMutation.mutate(
|
||||||
headers: { 'Content-Type': 'application/json' },
|
{ challengerGameAssoc, accepted: false, targetGameAssoc: gameAssoc },
|
||||||
body: JSON.stringify({ accepted: false, targetGameAssoc: gameAssoc }),
|
{
|
||||||
}).then(() => {
|
onSuccess: () => {
|
||||||
showOverlay('We are waiting for your opponent...', gameAssoc ? (
|
showOverlay('We are waiting for your opponent...', gameAssoc ? (
|
||||||
<WaitingOverlayContent
|
<WaitingOverlayContent
|
||||||
shareUrl={window.location.origin + '/play/' + gameAssoc}
|
shareUrl={`${window.location.origin}/play/${gameAssoc}`}
|
||||||
currentGameAssoc={gameAssoc}
|
currentGameAssoc={gameAssoc}
|
||||||
/>
|
/>
|
||||||
) : '');
|
) : '');
|
||||||
}).catch(() => {});
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
declineTimeout = setTimeout(handleDecline, 30000);
|
||||||
|
|
||||||
showOverlay(
|
showOverlay(
|
||||||
challengerName + ' wants to challenge you!',
|
challengerName + ' wants to challenge you!',
|
||||||
<div className="resign">
|
<ChallengeCountdown onAccept={handleAccept} onDecline={handleDecline} />,
|
||||||
<a onClick={handleAccept}>Accept</a>
|
|
||||||
<a onClick={handleDecline}>Decline</a>
|
|
||||||
</div>,
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -185,17 +336,22 @@ const useServerCommunication = (gameAssoc, gameInherited, isEnvDev) => {
|
|||||||
isEnvDev && console.warn(payload.user + ' stepped to ' + payload.data.coords.join(','));
|
isEnvDev && console.warn(payload.user + ' stepped to ' + payload.data.coords.join(','));
|
||||||
syncBombSelected(payload.data.bomb);
|
syncBombSelected(payload.data.bomb);
|
||||||
|
|
||||||
// Detect if turn switched (other player made a move)
|
/**
|
||||||
// After their move, it's now our turn (or the opposite player's turn)
|
* Detect if turn switched (other player made a move)
|
||||||
|
* After their move, it's now our turn (or the opposite player's turn)
|
||||||
|
*/
|
||||||
if (lastActivePlayerRef.current !== activePlayerRef.current) {
|
if (lastActivePlayerRef.current !== activePlayerRef.current) {
|
||||||
startNewTurn();
|
startNewTurn();
|
||||||
lastActivePlayerRef.current = activePlayerRef.current;
|
lastActivePlayerRef.current = activePlayerRef.current;
|
||||||
}
|
}
|
||||||
|
|
||||||
applyStep(payload.data);
|
applyStep(payload.data);
|
||||||
|
if (payload.data.uuid && !endRef.current) {
|
||||||
|
setGameUuid(payload.data.uuid);
|
||||||
|
}
|
||||||
makeGameEndIfItEnds(payload.data.bluePoints, payload.data.redPoints, false, payload.data.leftMines);
|
makeGameEndIfItEnds(payload.data.bluePoints, payload.data.redPoints, false, payload.data.leftMines);
|
||||||
} else {
|
} else {
|
||||||
resignProcess(payload.data.resign);
|
resignProcess(payload.data.resign, payload.data.uuid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -204,6 +360,16 @@ const useServerCommunication = (gameAssoc, gameInherited, isEnvDev) => {
|
|||||||
if (undefined !== payload.type) {
|
if (undefined !== payload.type) {
|
||||||
if ('challenge' === payload.type) wChallenge(payload);
|
if ('challenge' === payload.type) wChallenge(payload);
|
||||||
else if ('challenge-response' === payload.type) wChallengeResponse(payload);
|
else if ('challenge-response' === payload.type) wChallengeResponse(payload);
|
||||||
|
else if ('heartbeat' === payload.type) {
|
||||||
|
const me = webPlayerRef.current;
|
||||||
|
if (me && payload.color && payload.color !== me) {
|
||||||
|
const wasFirst = 0 === opponentLastSeenRef.current;
|
||||||
|
opponentLastSeenRef.current = Date.now();
|
||||||
|
if (wasFirst && isTrueRestoredRef.current && !endRef.current) {
|
||||||
|
hideOverlay();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,9 +395,9 @@ const useServerCommunication = (gameAssoc, gameInherited, isEnvDev) => {
|
|||||||
const subscriberJwt = wrapper.dataset.mercureSubscriberJwt;
|
const subscriberJwt = wrapper.dataset.mercureSubscriberJwt;
|
||||||
const url = new URL(hubUrl, window.location.origin);
|
const url = new URL(hubUrl, window.location.origin);
|
||||||
|
|
||||||
url.searchParams.append('topic', 'mineseeker/channel/' + gameAssoc);
|
url.searchParams.append('topic', `mineseeker/channel/${gameAssoc}`);
|
||||||
if (subscriberJwt) url.searchParams.append('authorization', subscriberJwt);
|
|
||||||
|
|
||||||
|
if (subscriberJwt) url.searchParams.append('authorization', subscriberJwt);
|
||||||
if (eventSourceRef.current) eventSourceRef.current.close();
|
if (eventSourceRef.current) eventSourceRef.current.close();
|
||||||
|
|
||||||
const es = new EventSource(url.toString());
|
const es = new EventSource(url.toString());
|
||||||
@@ -258,6 +424,7 @@ const useServerCommunication = (gameAssoc, gameInherited, isEnvDev) => {
|
|||||||
openEventSource();
|
openEventSource();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (gameInherited) {
|
if (gameInherited) {
|
||||||
const serverData = await connectQuery.refetch().then(r => {
|
const serverData = await connectQuery.refetch().then(r => {
|
||||||
@@ -272,23 +439,50 @@ const useServerCommunication = (gameAssoc, gameInherited, isEnvDev) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
rpcUsersRef.current = serverData.users;
|
rpcUsersRef.current = serverData.users;
|
||||||
|
lastStepRef.current = serverData.mostRecentStep || null;
|
||||||
|
|
||||||
|
/** Pass game state (points, bonus) to wInit */
|
||||||
|
const gameState = {
|
||||||
|
redPoints: serverData.redPoints ?? 0,
|
||||||
|
bluePoints: serverData.bluePoints ?? 0,
|
||||||
|
redBonusPoints: serverData.redBonusPoints ?? 0,
|
||||||
|
blueBonusPoints: serverData.blueBonusPoints ?? 0,
|
||||||
|
redBonusStats: serverData.redBonusStats ?? {},
|
||||||
|
blueBonusStats: serverData.blueBonusStats ?? {},
|
||||||
|
};
|
||||||
|
const isGameFinished = serverData.gameFinished ?? false;
|
||||||
|
wInit(serverData.revealedCells || [], serverData.lastStep || {}, gameState, isGameFinished);
|
||||||
|
|
||||||
|
/** Open event source after showing overlay */
|
||||||
openEventSource();
|
openEventSource();
|
||||||
wInit(serverData.revealedCells || []);
|
|
||||||
} else {
|
} else {
|
||||||
await startMutation.mutateAsync();
|
const startResponse = await startMutation.mutateAsync();
|
||||||
|
if (!startResponse?.success) {
|
||||||
|
showOverlay('Error', 'Failed to start game. Please try again.');
|
||||||
|
isEnvDev && console.error('Start game failed:', startResponse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
openEventSource();
|
openEventSource();
|
||||||
wInit();
|
wInit();
|
||||||
}
|
}
|
||||||
|
|
||||||
isEnvDev && console.info('Connection initialised — joining channel');
|
isEnvDev && console.info('Connection initialised — joining channel');
|
||||||
await joinMutation.mutateAsync();
|
await joinMutation.mutateAsync();
|
||||||
|
startHeartbeat();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
isEnvDev && console.error('Connection error', e);
|
isEnvDev && console.error('Connection error', e);
|
||||||
|
showOverlay('Error', 'Connection failed. Please try again.');
|
||||||
setTimeout(() => window.location.reload(), 500);
|
setTimeout(() => window.location.reload(), 500);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
window.addEventListener('pagehide', () => navigator.sendBeacon('/api/game/leave/' + gameAssoc));
|
window.addEventListener('pagehide', () => {
|
||||||
|
leaveMutation.mutate();
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
stopHeartbeat();
|
||||||
|
};
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -312,6 +506,11 @@ const useServerCommunication = (gameAssoc, gameInherited, isEnvDev) => {
|
|||||||
try {
|
try {
|
||||||
const result = await stepMutation.mutateAsync(dataPack);
|
const result = await stepMutation.mutateAsync(dataPack);
|
||||||
applyStep(result);
|
applyStep(result);
|
||||||
|
|
||||||
|
if (result.uuid && !endRef.current) {
|
||||||
|
setGameUuid(result.uuid);
|
||||||
|
}
|
||||||
|
|
||||||
makeGameEndIfItEnds(result.bluePoints, result.redPoints, false, result.leftMines);
|
makeGameEndIfItEnds(result.bluePoints, result.redPoints, false, result.leftMines);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
isEnvDev && console.error('Step error', e);
|
isEnvDev && console.error('Step error', e);
|
||||||
@@ -321,13 +520,24 @@ const useServerCommunication = (gameAssoc, gameInherited, isEnvDev) => {
|
|||||||
const clickResign = () => {
|
const clickResign = () => {
|
||||||
const color = activePlayerRef.current ? 'blue' : 'red';
|
const color = activePlayerRef.current ? 'blue' : 'red';
|
||||||
const stepElapsed = getStepElapsed(activePlayerRef.current, isGameRunningRef.current);
|
const stepElapsed = getStepElapsed(activePlayerRef.current, isGameRunningRef.current);
|
||||||
stepMutation.mutate({ resign: color, stepElapsed });
|
|
||||||
resignProcess(webPlayerRef.current);
|
stepMutation.mutate(
|
||||||
|
{ resign: color, stepElapsed },
|
||||||
|
{
|
||||||
|
onSuccess: result => {
|
||||||
|
if (result?.uuid && !endRef.current) {
|
||||||
|
resignProcess(webPlayerRef.current, result.uuid);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const resign = () => {
|
const resign = () => {
|
||||||
const activeColor = activePlayerRef.current ? 'blue' : 'red';
|
const activeColor = activePlayerRef.current ? 'blue' : 'red';
|
||||||
|
|
||||||
if (webPlayerRef.current !== activeColor) return;
|
if (webPlayerRef.current !== activeColor) return;
|
||||||
|
|
||||||
showOverlay('Are u sure u want to resign?!', (
|
showOverlay('Are u sure u want to resign?!', (
|
||||||
<div className="resign">
|
<div className="resign">
|
||||||
<a onClick={clickResign}>Yes</a>
|
<a onClick={clickResign}>Yes</a>
|
||||||
|
|||||||
@@ -10,24 +10,18 @@
|
|||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
|
|
||||||
const useStepTimer = () => {
|
const useStepTimer = () => {
|
||||||
// Record when the current turn started (timestamp)
|
|
||||||
const turnStartTimeRef = useRef(null);
|
const turnStartTimeRef = useRef(null);
|
||||||
// Flag to track if we've already recorded a turn start
|
|
||||||
const turnStartedRef = useRef(false);
|
const turnStartedRef = useRef(false);
|
||||||
|
|
||||||
const getStepElapsed = (currentActivePlayer, isGameRunning) => {
|
const getStepElapsed = (currentActivePlayer, isGameRunning) => {
|
||||||
// If game not running, return 0
|
|
||||||
if (!isGameRunning) return 0;
|
if (!isGameRunning) return 0;
|
||||||
|
|
||||||
// Only initialize the turn timer ONCE per call to getStepElapsed
|
|
||||||
// This prevents resetting on multiple calls
|
|
||||||
if (!turnStartedRef.current) {
|
if (!turnStartedRef.current) {
|
||||||
turnStartTimeRef.current = Date.now();
|
turnStartTimeRef.current = Date.now();
|
||||||
turnStartedRef.current = true;
|
turnStartedRef.current = true;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// After initialization, just calculate elapsed time
|
|
||||||
if (turnStartTimeRef.current) {
|
if (turnStartTimeRef.current) {
|
||||||
return Math.floor((Date.now() - turnStartTimeRef.current) / 1000);
|
return Math.floor((Date.now() - turnStartTimeRef.current) / 1000);
|
||||||
}
|
}
|
||||||
@@ -40,7 +34,6 @@ const useStepTimer = () => {
|
|||||||
turnStartedRef.current = false;
|
turnStartedRef.current = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Call this when we know a turn has actually changed (from server response)
|
|
||||||
const startNewTurn = () => {
|
const startNewTurn = () => {
|
||||||
turnStartTimeRef.current = Date.now();
|
turnStartTimeRef.current = Date.now();
|
||||||
turnStartedRef.current = true;
|
turnStartedRef.current = true;
|
||||||
|
|||||||
@@ -34,9 +34,23 @@ export const IMAGES = {
|
|||||||
bombPos: (hor, vert) => `${IMG}bg-bomb-${hor}-${vert}-outbg.png`,
|
bombPos: (hor, vert) => `${IMG}bg-bomb-${hor}-${vert}-outbg.png`,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const BONUS_STATS_DEF = {
|
||||||
|
blindHits: 0, chainBest: 0, chainCurrent: 0, lastMineHits: 0, edgeMines: 0, biggestReveal: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BONUS_LABELS = {
|
||||||
|
blindHits: { label: 'Blind hits', desc: 'Mines clicked with no revealed number nearby' },
|
||||||
|
chainBest: { label: 'Best chain', desc: 'Longest streak of consecutive mine-clicks' },
|
||||||
|
chainCurrent: { label: 'Current chain', desc: 'Active consecutive mine-click streak' },
|
||||||
|
lastMineHits: { label: 'Endgame mines', desc: 'Mines clicked while few remain on the board' },
|
||||||
|
edgeMines: { label: 'Edge mines', desc: 'Mines clicked on the board boundary' },
|
||||||
|
biggestReveal: { label: 'Biggest reveal', desc: 'Largest number of safe cells revealed in one click' },
|
||||||
|
};
|
||||||
|
|
||||||
export const PLAYER_DEF = {
|
export const PLAYER_DEF = {
|
||||||
name: '...', desc: '', active: false, mines: 0, haveBomb: true, enabledBomb: true,
|
name: '...', desc: '', active: false, mines: 0, haveBomb: true, enabledBomb: true,
|
||||||
registered: false, avatar: null,
|
registered: false, avatar: null,
|
||||||
|
bonusPoints: 0, bonusStats: { ...BONUS_STATS_DEF },
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DESC = {
|
export const DESC = {
|
||||||
|
|||||||
@@ -7,4 +7,4 @@
|
|||||||
* file that was distributed with this source code.
|
* file that was distributed with this source code.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export { DESC, IMAGES, BOMB_SYMBOLS, PLAYER_DEF, bombRadius, initCells, patchCells } from './constants';
|
export { DESC, IMAGES, BOMB_SYMBOLS, PLAYER_DEF, BONUS_STATS_DEF, BONUS_LABELS, bombRadius, initCells, patchCells } from './constants';
|
||||||
|
|||||||
@@ -9,8 +9,7 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import PasskeyManager from './components/PasskeyManager';
|
import { PasskeyLogin, PasskeyManager } from '@global-components';
|
||||||
import PasskeyLogin from './components/PasskeyLogin';
|
|
||||||
|
|
||||||
const passkeyManagerRoot = document.getElementById('passkey-manager-root');
|
const passkeyManagerRoot = document.getElementById('passkey-manager-root');
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import ProfileCharts from './components/ProfileCharts';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import BattleDialog from './components/BattleDialog';
|
import { AvatarUpload, BattleDialog, ProfileCharts } from '@global-components';
|
||||||
import AvatarUpload from './components/AvatarUpload';
|
|
||||||
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
const avatarRoot = document.getElementById('profile-avatar-root');
|
const avatarRoot = document.getElementById('profile-avatar-root');
|
||||||
if (avatarRoot) {
|
if (avatarRoot) {
|
||||||
const { uploadUrl, thumbUrl, initials } = avatarRoot.dataset;
|
const { uploadUrl, thumbUrl, initials } = avatarRoot.dataset;
|
||||||
createRoot(avatarRoot).render(
|
createRoot(avatarRoot).render(
|
||||||
<AvatarUpload
|
<QueryClientProvider client={queryClient}>
|
||||||
uploadUrl={uploadUrl}
|
<AvatarUpload
|
||||||
initialThumbUrl={thumbUrl || null}
|
uploadUrl={uploadUrl}
|
||||||
initials={initials}
|
initialThumbUrl={thumbUrl || null}
|
||||||
/>,
|
initials={initials}
|
||||||
|
/>
|
||||||
|
</QueryClientProvider>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,3 +41,28 @@ if (battleRoot) {
|
|||||||
<BattleDialog games={JSON.parse(battleRoot.dataset.games)} />,
|
<BattleDialog games={JSON.parse(battleRoot.dataset.games)} />,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const list = document.querySelector('.profile-games');
|
||||||
|
const loadMoreBtn = document.querySelector('[data-load-more]');
|
||||||
|
if (list && loadMoreBtn) {
|
||||||
|
const batchSize = parseInt(list.dataset.batchSize, 10) || 5;
|
||||||
|
loadMoreBtn.addEventListener('click', () => {
|
||||||
|
const hidden = list.querySelectorAll('.profile-game--hidden');
|
||||||
|
Array.from(hidden).slice(0, batchSize).forEach(el => el.classList.remove('profile-game--hidden'));
|
||||||
|
if (0 === list.querySelectorAll('.profile-game--hidden').length) {
|
||||||
|
loadMoreBtn.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const filterInput = document.querySelector('[data-filter]');
|
||||||
|
if (list && filterInput) {
|
||||||
|
filterInput.addEventListener('input', () => {
|
||||||
|
const term = filterInput.value.trim().toLowerCase();
|
||||||
|
list.classList.toggle('is-filtering', 0 < term.length);
|
||||||
|
list.querySelectorAll('.profile-game').forEach(card => {
|
||||||
|
const opp = card.querySelector('.profile-game__opponent')?.textContent.trim().toLowerCase() ?? '';
|
||||||
|
card.classList.toggle('profile-game--filtered-out', 0 < term.length && !opp.includes(term));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
44
assets/js/utils/format.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Formats a duration in seconds as MM:SS. */
|
||||||
|
export const formatTime = seconds => {
|
||||||
|
const mins = Math.floor(seconds / 60);
|
||||||
|
const secs = seconds % 60;
|
||||||
|
return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats the difference between two 'YYYY-MM-DD HH:mm' strings as a
|
||||||
|
* human-readable duration (e.g. "1h 4m 23s", "4m 23s", "23s").
|
||||||
|
* Returns null when the inputs are missing or the diff is not positive.
|
||||||
|
*/
|
||||||
|
export const formatDuration = (from, to) => {
|
||||||
|
if (!from || !to) return null;
|
||||||
|
const diffMs = new Date(to.replace(' ', 'T')) - new Date(from.replace(' ', 'T'));
|
||||||
|
if (isNaN(diffMs) || 0 >= diffMs) return null;
|
||||||
|
const totalSec = Math.floor(diffMs / 1000);
|
||||||
|
const h = Math.floor(totalSec / 3600);
|
||||||
|
const m = Math.floor((totalSec % 3600) / 60);
|
||||||
|
const s = totalSec % 60;
|
||||||
|
if (0 < h) return `${h}h ${m}m ${s}s`;
|
||||||
|
if (0 < m) return `${m}m ${s}s`;
|
||||||
|
return `${s}s`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats an ISO timestamp as a "X min ago" string (minute resolution).
|
||||||
|
* Returns 'just now' for differences under one minute.
|
||||||
|
*/
|
||||||
|
export const formatSince = isoStr => {
|
||||||
|
const diff = Math.floor((Date.now() - new Date(isoStr)) / 60000);
|
||||||
|
if (1 > diff) return 'just now';
|
||||||
|
if (1 === diff) return '1 min ago';
|
||||||
|
return `${diff} min ago`;
|
||||||
|
};
|
||||||
190
bun.lock
@@ -12,24 +12,24 @@
|
|||||||
"@fontsource/rajdhani": "^5.2.7",
|
"@fontsource/rajdhani": "^5.2.7",
|
||||||
"@fortawesome/fontawesome-free": "^7.2.0",
|
"@fortawesome/fontawesome-free": "^7.2.0",
|
||||||
"@mui/material": "^9.0.0",
|
"@mui/material": "^9.0.0",
|
||||||
"@mui/x-charts": "^9.0.1",
|
"@mui/x-charts": "^9.0.2",
|
||||||
"@tanstack/react-query": "^5.0.0",
|
"@tanstack/react-query": "^5.99.2",
|
||||||
"howler": "^2.1.2",
|
"howler": "^2.2.4",
|
||||||
"lodash": "^4.18.1",
|
"lodash": "^4.18.1",
|
||||||
"prop-types": "^15.7.2",
|
"prop-types": "^15.8.1",
|
||||||
"react": "^19.0.0",
|
"react": "^19.2.5",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.2.5",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3.3.5",
|
"@eslint/eslintrc": "^3.3.5",
|
||||||
"@eslint/js": "^9.0.0",
|
"@eslint/js": "10.0.1",
|
||||||
"@stylistic/eslint-plugin": "^4.0.0",
|
"@stylistic/eslint-plugin": "5.10.0",
|
||||||
"@vitejs/plugin-react": "^6.0.1",
|
"@vitejs/plugin-react": "^6.0.1",
|
||||||
"eslint": "^9.0.0",
|
"eslint": "10.2.1",
|
||||||
"eslint-plugin-react": "^7.0.0",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"eslint-plugin-react-hooks": "^5.0.0",
|
"eslint-plugin-react-hooks": "7.1.1",
|
||||||
"globals": "^15.0.0",
|
"globals": "17.5.0",
|
||||||
"sass": "^1.77.0",
|
"sass": "^1.99.0",
|
||||||
"vite": "^8.0.8",
|
"vite": "^8.0.8",
|
||||||
"vite-plugin-symfony": "^8.2.4",
|
"vite-plugin-symfony": "^8.2.4",
|
||||||
},
|
},
|
||||||
@@ -38,16 +38,28 @@
|
|||||||
"packages": {
|
"packages": {
|
||||||
"@babel/code-frame": ["@babel/code-frame@7.29.0", "http://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
|
"@babel/code-frame": ["@babel/code-frame@7.29.0", "http://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
|
||||||
|
|
||||||
|
"@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="],
|
||||||
|
|
||||||
|
"@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="],
|
||||||
|
|
||||||
"@babel/generator": ["@babel/generator@7.29.1", "http://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
|
"@babel/generator": ["@babel/generator@7.29.1", "http://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
|
||||||
|
|
||||||
|
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="],
|
||||||
|
|
||||||
"@babel/helper-globals": ["@babel/helper-globals@7.28.0", "http://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
|
"@babel/helper-globals": ["@babel/helper-globals@7.28.0", "http://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
|
||||||
|
|
||||||
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "http://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="],
|
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "http://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="],
|
||||||
|
|
||||||
|
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="],
|
||||||
|
|
||||||
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "http://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
|
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "http://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
|
||||||
|
|
||||||
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "http://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
|
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "http://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
|
||||||
|
|
||||||
|
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
|
||||||
|
|
||||||
|
"@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="],
|
||||||
|
|
||||||
"@babel/parser": ["@babel/parser@7.29.2", "http://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="],
|
"@babel/parser": ["@babel/parser@7.29.2", "http://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="],
|
||||||
|
|
||||||
"@babel/runtime": ["@babel/runtime@7.29.2", "http://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="],
|
"@babel/runtime": ["@babel/runtime@7.29.2", "http://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="],
|
||||||
@@ -94,23 +106,23 @@
|
|||||||
|
|
||||||
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "http://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
|
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "http://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
|
||||||
|
|
||||||
"@eslint/config-array": ["@eslint/config-array@0.21.2", "http://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.5" } }, "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw=="],
|
"@eslint/config-array": ["@eslint/config-array@0.23.5", "", { "dependencies": { "@eslint/object-schema": "^3.0.5", "debug": "^4.3.1", "minimatch": "^10.2.4" } }, "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA=="],
|
||||||
|
|
||||||
"@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "http://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="],
|
"@eslint/config-helpers": ["@eslint/config-helpers@0.5.5", "", { "dependencies": { "@eslint/core": "^1.2.1" } }, "sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w=="],
|
||||||
|
|
||||||
"@eslint/core": ["@eslint/core@0.17.0", "http://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="],
|
"@eslint/core": ["@eslint/core@1.2.1", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ=="],
|
||||||
|
|
||||||
"@eslint/eslintrc": ["@eslint/eslintrc@3.3.5", "http://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", { "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" } }, "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg=="],
|
"@eslint/eslintrc": ["@eslint/eslintrc@3.3.5", "http://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", { "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" } }, "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg=="],
|
||||||
|
|
||||||
"@eslint/js": ["@eslint/js@9.39.4", "http://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", {}, "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw=="],
|
"@eslint/js": ["@eslint/js@10.0.1", "", { "peerDependencies": { "eslint": "^10.0.0" }, "optionalPeers": ["eslint"] }, "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA=="],
|
||||||
|
|
||||||
"@eslint/object-schema": ["@eslint/object-schema@2.1.7", "http://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="],
|
"@eslint/object-schema": ["@eslint/object-schema@3.0.5", "", {}, "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw=="],
|
||||||
|
|
||||||
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "http://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="],
|
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.7.1", "", { "dependencies": { "@eslint/core": "^1.2.1", "levn": "^0.4.1" } }, "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ=="],
|
||||||
|
|
||||||
"@fontsource/changa-one": ["@fontsource/changa-one@5.2.8", "http://registry.npmjs.org/@fontsource/changa-one/-/changa-one-5.2.8.tgz", {}, "sha512-gagiU8sMWLs9ejh41NmrYlGdgasSYWFegz5/+22WqYdJVS9HZbaUEXj/6jj3ZKgi+dQZT0kYI+Nha2UQGbO/mA=="],
|
"@fontsource/changa-one": ["@fontsource/changa-one@5.2.8", "http://registry.npmjs.org/@fontsource/changa-one/-/changa-one-5.2.8.tgz", {}, "sha512-gagiU8sMWLs9ejh41NmrYlGdgasSYWFegz5/+22WqYdJVS9HZbaUEXj/6jj3ZKgi+dQZT0kYI+Nha2UQGbO/mA=="],
|
||||||
|
|
||||||
"@fontsource/open-sans": ["@fontsource/open-sans@5.2.7", "", {}, "sha512-8yfgDYjE5O0vmTPdrcjV35y4yMnctsokmi9gN49Gcsr0sjzkMkR97AnKDe6OqZh2SFkYlR28fxOvi21bYEgMSw=="],
|
"@fontsource/open-sans": ["@fontsource/open-sans@5.2.7", "http://registry.npmjs.org/@fontsource/open-sans/-/open-sans-5.2.7.tgz", {}, "sha512-8yfgDYjE5O0vmTPdrcjV35y4yMnctsokmi9gN49Gcsr0sjzkMkR97AnKDe6OqZh2SFkYlR28fxOvi21bYEgMSw=="],
|
||||||
|
|
||||||
"@fontsource/rajdhani": ["@fontsource/rajdhani@5.2.7", "http://registry.npmjs.org/@fontsource/rajdhani/-/rajdhani-5.2.7.tgz", {}, "sha512-7Gy10U688fCdeFfYKebUF2TZotdgH/ghKyMsseXPmB60lpaUHC8aoCSJl5/OpZ+KHKSU2TqBfKfteVkcIXxTAQ=="],
|
"@fontsource/rajdhani": ["@fontsource/rajdhani@5.2.7", "http://registry.npmjs.org/@fontsource/rajdhani/-/rajdhani-5.2.7.tgz", {}, "sha512-7Gy10U688fCdeFfYKebUF2TZotdgH/ghKyMsseXPmB60lpaUHC8aoCSJl5/OpZ+KHKSU2TqBfKfteVkcIXxTAQ=="],
|
||||||
|
|
||||||
@@ -126,6 +138,8 @@
|
|||||||
|
|
||||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "http://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "http://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||||
|
|
||||||
|
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
|
||||||
|
|
||||||
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "http://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "http://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||||
|
|
||||||
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "http://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "http://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||||
@@ -146,11 +160,11 @@
|
|||||||
|
|
||||||
"@mui/utils": ["@mui/utils@9.0.0", "http://registry.npmjs.org/@mui/utils/-/utils-9.0.0.tgz", { "dependencies": { "@babel/runtime": "^7.29.2", "@mui/types": "^9.0.0", "@types/prop-types": "^15.7.15", "clsx": "^2.1.1", "prop-types": "^15.8.1", "react-is": "^19.2.4" }, "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-bQcqyg/gjULUqTuyUjSAFr6LQGLvtkNtDbJerAtoUn9kGZ0hg5QJiN1PLHMLbeFpe3te1831uq7GFl2ITokGdg=="],
|
"@mui/utils": ["@mui/utils@9.0.0", "http://registry.npmjs.org/@mui/utils/-/utils-9.0.0.tgz", { "dependencies": { "@babel/runtime": "^7.29.2", "@mui/types": "^9.0.0", "@types/prop-types": "^15.7.15", "clsx": "^2.1.1", "prop-types": "^15.8.1", "react-is": "^19.2.4" }, "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-bQcqyg/gjULUqTuyUjSAFr6LQGLvtkNtDbJerAtoUn9kGZ0hg5QJiN1PLHMLbeFpe3te1831uq7GFl2ITokGdg=="],
|
||||||
|
|
||||||
"@mui/x-charts": ["@mui/x-charts@9.0.1", "http://registry.npmjs.org/@mui/x-charts/-/x-charts-9.0.1.tgz", { "dependencies": { "@babel/runtime": "^7.28.6", "@mui/utils": "9.0.0", "@mui/x-charts-vendor": "^9.0.0", "@mui/x-internal-gestures": "^9.0.0", "@mui/x-internals": "^9.0.0", "bezier-easing": "^2.1.0", "clsx": "^2.1.1", "prop-types": "^15.8.1", "reselect": "^5.1.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@emotion/react": "^11.9.0", "@emotion/styled": "^11.8.1", "@mui/material": "^7.3.0 || ^9.0.0", "@mui/system": "^7.3.0 || ^9.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/react", "@emotion/styled"] }, "sha512-0LyhlGhUm07wGJY0d0U+hSljGS1EHKWgPBsTJ/lBNGDrNc4DI9zSbp4h802LN/eLwMUVXJSI7DH2W3Ef3WsqnQ=="],
|
"@mui/x-charts": ["@mui/x-charts@9.0.2", "http://registry.npmjs.org/@mui/x-charts/-/x-charts-9.0.2.tgz", { "dependencies": { "@babel/runtime": "^7.28.6", "@mui/utils": "9.0.0", "@mui/x-charts-vendor": "^9.0.0", "@mui/x-internal-gestures": "^9.0.2", "@mui/x-internals": "^9.0.0", "bezier-easing": "^2.1.0", "clsx": "^2.1.1", "prop-types": "^15.8.1", "reselect": "^5.1.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@emotion/react": "^11.9.0", "@emotion/styled": "^11.8.1", "@mui/material": "^7.3.0 || ^9.0.0", "@mui/system": "^7.3.0 || ^9.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/react", "@emotion/styled"] }, "sha512-bKgjGD+uJbDN/g7tMjVmlNdm+iM4UkCJoYruQmgpQ0l+cip8Kn4kmn1iD//rZ35an+LdWaUZ4MHvMzV76D6EJw=="],
|
||||||
|
|
||||||
"@mui/x-charts-vendor": ["@mui/x-charts-vendor@9.0.0", "http://registry.npmjs.org/@mui/x-charts-vendor/-/x-charts-vendor-9.0.0.tgz", { "dependencies": { "@babel/runtime": "^7.28.6", "@types/d3-array": "^3.2.2", "@types/d3-color": "^3.1.3", "@types/d3-format": "^3.0.4", "@types/d3-interpolate": "^3.0.4", "@types/d3-path": "^3.1.1", "@types/d3-scale": "^4.0.9", "@types/d3-shape": "^3.1.8", "@types/d3-time": "^3.0.4", "@types/d3-time-format": "^4.0.3", "@types/d3-timer": "^3.0.2", "d3-array": "^3.2.4", "d3-color": "^3.1.0", "d3-format": "^3.1.2", "d3-interpolate": "^3.0.1", "d3-path": "^3.1.0", "d3-scale": "^4.0.2", "d3-shape": "^3.2.0", "d3-time": "^3.1.0", "d3-time-format": "^4.1.0", "d3-timer": "^3.0.1", "flatqueue": "^3.0.0", "internmap": "^2.0.3" } }, "sha512-Do91i+fZiNj/4LN5oaGpJoutolzDBDwdfw6tHrx2LKXDMCRlaImCfreLbdbkk7dFsi9fuIP7hWiMV4vDJKPJTA=="],
|
"@mui/x-charts-vendor": ["@mui/x-charts-vendor@9.0.0", "http://registry.npmjs.org/@mui/x-charts-vendor/-/x-charts-vendor-9.0.0.tgz", { "dependencies": { "@babel/runtime": "^7.28.6", "@types/d3-array": "^3.2.2", "@types/d3-color": "^3.1.3", "@types/d3-format": "^3.0.4", "@types/d3-interpolate": "^3.0.4", "@types/d3-path": "^3.1.1", "@types/d3-scale": "^4.0.9", "@types/d3-shape": "^3.1.8", "@types/d3-time": "^3.0.4", "@types/d3-time-format": "^4.0.3", "@types/d3-timer": "^3.0.2", "d3-array": "^3.2.4", "d3-color": "^3.1.0", "d3-format": "^3.1.2", "d3-interpolate": "^3.0.1", "d3-path": "^3.1.0", "d3-scale": "^4.0.2", "d3-shape": "^3.2.0", "d3-time": "^3.1.0", "d3-time-format": "^4.1.0", "d3-timer": "^3.0.1", "flatqueue": "^3.0.0", "internmap": "^2.0.3" } }, "sha512-Do91i+fZiNj/4LN5oaGpJoutolzDBDwdfw6tHrx2LKXDMCRlaImCfreLbdbkk7dFsi9fuIP7hWiMV4vDJKPJTA=="],
|
||||||
|
|
||||||
"@mui/x-internal-gestures": ["@mui/x-internal-gestures@9.0.0", "http://registry.npmjs.org/@mui/x-internal-gestures/-/x-internal-gestures-9.0.0.tgz", { "dependencies": { "@babel/runtime": "^7.28.6" } }, "sha512-+fW1EUai25GJbivGRsi3GX4GYsSvzFPvUEjmMgB4POkRBDjrEZNaLdVWfapT6DlWv/Vfbi08bYSuyvhPXGMZjw=="],
|
"@mui/x-internal-gestures": ["@mui/x-internal-gestures@9.0.2", "http://registry.npmjs.org/@mui/x-internal-gestures/-/x-internal-gestures-9.0.2.tgz", { "dependencies": { "@babel/runtime": "^7.28.6" } }, "sha512-xCp99a7cSb7iH1bj4G524ooMOFe92H8m/rONCUiKyj7LvV1YUGzTfHgJysQgDCZJqHYaW7YAGLvwMUyEMZVzqQ=="],
|
||||||
|
|
||||||
"@mui/x-internals": ["@mui/x-internals@9.0.0", "http://registry.npmjs.org/@mui/x-internals/-/x-internals-9.0.0.tgz", { "dependencies": { "@babel/runtime": "^7.28.6", "@mui/utils": "9.0.0", "reselect": "^5.1.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-E/4rdg69JjhyybpPGypCjAKSKLLnSdCFM+O6P/nkUg47+qt3uftxQEhjQO53rcn6ahHl6du/uNZ9BLgeY6kYxQ=="],
|
"@mui/x-internals": ["@mui/x-internals@9.0.0", "http://registry.npmjs.org/@mui/x-internals/-/x-internals-9.0.0.tgz", { "dependencies": { "@babel/runtime": "^7.28.6", "@mui/utils": "9.0.0", "reselect": "^5.1.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-E/4rdg69JjhyybpPGypCjAKSKLLnSdCFM+O6P/nkUg47+qt3uftxQEhjQO53rcn6ahHl6du/uNZ9BLgeY6kYxQ=="],
|
||||||
|
|
||||||
@@ -228,11 +242,11 @@
|
|||||||
|
|
||||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.7", "http://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", {}, "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA=="],
|
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.7", "http://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", {}, "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA=="],
|
||||||
|
|
||||||
"@stylistic/eslint-plugin": ["@stylistic/eslint-plugin@4.4.1", "http://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-4.4.1.tgz", { "dependencies": { "@typescript-eslint/utils": "^8.32.1", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "estraverse": "^5.3.0", "picomatch": "^4.0.2" }, "peerDependencies": { "eslint": ">=9.0.0" } }, "sha512-CEigAk7eOLyHvdgmpZsKFwtiqS2wFwI1fn4j09IU9GmD4euFM4jEBAViWeCqaNLlbX2k2+A/Fq9cje4HQBXuJQ=="],
|
"@stylistic/eslint-plugin": ["@stylistic/eslint-plugin@5.10.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/types": "^8.56.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "estraverse": "^5.3.0", "picomatch": "^4.0.3" }, "peerDependencies": { "eslint": "^9.0.0 || ^10.0.0" } }, "sha512-nPK52ZHvot8Ju/0A4ucSX1dcPV2/1clx0kLcH5wDmrE4naKso7TUC/voUyU1O9OTKTrR6MYip6LP0ogEMQ9jPQ=="],
|
||||||
|
|
||||||
"@tanstack/query-core": ["@tanstack/query-core@5.97.0", "http://registry.npmjs.org/@tanstack/query-core/-/query-core-5.97.0.tgz", {}, "sha512-QdpLP5VzVMgo4VtaPppRA2W04UFjIqX+bxke/ZJhE5cfd5UPkRzqIAJQt9uXkQJjqE8LBOMbKv7f8HCsZltXlg=="],
|
"@tanstack/query-core": ["@tanstack/query-core@5.99.2", "http://registry.npmjs.org/@tanstack/query-core/-/query-core-5.99.2.tgz", {}, "sha512-1HunU0bXVsR1ZJMZbcOPE6VtaBJxsW809RE9xPe4Gz7MlB0GWwQvuTPhMoEmQ/hIzFKJ/DWAuttIe7BOaWx0tA=="],
|
||||||
|
|
||||||
"@tanstack/react-query": ["@tanstack/react-query@5.97.0", "http://registry.npmjs.org/@tanstack/react-query/-/react-query-5.97.0.tgz", { "dependencies": { "@tanstack/query-core": "5.97.0" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-y4So4eGcQoK2WVMAcDNZE9ofB/p5v1OlKvtc1F3uqHwrtifobT7q+ZnXk2mRkc8E84HKYSlAE9z6HXl2V0+ySQ=="],
|
"@tanstack/react-query": ["@tanstack/react-query@5.99.2", "http://registry.npmjs.org/@tanstack/react-query/-/react-query-5.99.2.tgz", { "dependencies": { "@tanstack/query-core": "5.99.2" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-vM91UEe45QUS9ED6OklsVL15i8qKcRqNwpWzPTVWvRPRSEgDudDgHpvyTjcdlwHcrKNa80T+xXYcchT2noPnZA=="],
|
||||||
|
|
||||||
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "http://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "http://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
||||||
|
|
||||||
@@ -256,6 +270,8 @@
|
|||||||
|
|
||||||
"@types/d3-timer": ["@types/d3-timer@3.0.2", "http://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="],
|
"@types/d3-timer": ["@types/d3-timer@3.0.2", "http://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="],
|
||||||
|
|
||||||
|
"@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="],
|
||||||
|
|
||||||
"@types/estree": ["@types/estree@1.0.8", "http://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
"@types/estree": ["@types/estree@1.0.8", "http://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||||
|
|
||||||
"@types/json-schema": ["@types/json-schema@7.0.15", "http://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
"@types/json-schema": ["@types/json-schema@7.0.15", "http://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||||
@@ -268,20 +284,8 @@
|
|||||||
|
|
||||||
"@types/react-transition-group": ["@types/react-transition-group@4.4.12", "http://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", { "peerDependencies": { "@types/react": "*" } }, "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w=="],
|
"@types/react-transition-group": ["@types/react-transition-group@4.4.12", "http://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", { "peerDependencies": { "@types/react": "*" } }, "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w=="],
|
||||||
|
|
||||||
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.58.1", "http://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.1.tgz", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.58.1", "@typescript-eslint/types": "^8.58.1", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-gfQ8fk6cxhtptek+/8ZIqw8YrRW5048Gug8Ts5IYcMLCw18iUgrZAEY/D7s4hkI0FxEfGakKuPK/XUMPzPxi5g=="],
|
|
||||||
|
|
||||||
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.58.1", "http://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.1.tgz", { "dependencies": { "@typescript-eslint/types": "8.58.1", "@typescript-eslint/visitor-keys": "8.58.1" } }, "sha512-TPYUEqJK6avLcEjumWsIuTpuYODTTDAtoMdt8ZZa93uWMTX13Nb8L5leSje1NluammvU+oI3QRr5lLXPgihX3w=="],
|
|
||||||
|
|
||||||
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.58.1", "http://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.1.tgz", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-JAr2hOIct2Q+qk3G+8YFfqkqi7sC86uNryT+2i5HzMa2MPjw4qNFvtjnw1IiA1rP7QhNKVe21mSSLaSjwA1Olw=="],
|
|
||||||
|
|
||||||
"@typescript-eslint/types": ["@typescript-eslint/types@8.58.1", "http://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.1.tgz", {}, "sha512-io/dV5Aw5ezwzfPBBWLoT+5QfVtP8O7q4Kftjn5azJ88bYyp/ZMCsyW1lpKK46EXJcaYMZ1JtYj+s/7TdzmQMw=="],
|
"@typescript-eslint/types": ["@typescript-eslint/types@8.58.1", "http://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.1.tgz", {}, "sha512-io/dV5Aw5ezwzfPBBWLoT+5QfVtP8O7q4Kftjn5azJ88bYyp/ZMCsyW1lpKK46EXJcaYMZ1JtYj+s/7TdzmQMw=="],
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.58.1", "http://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.1.tgz", { "dependencies": { "@typescript-eslint/project-service": "8.58.1", "@typescript-eslint/tsconfig-utils": "8.58.1", "@typescript-eslint/types": "8.58.1", "@typescript-eslint/visitor-keys": "8.58.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-w4w7WR7GHOjqqPnvAYbazq+Y5oS68b9CzasGtnd6jIeOIeKUzYzupGTB2T4LTPSv4d+WPeccbxuneTFHYgAAWg=="],
|
|
||||||
|
|
||||||
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.58.1", "http://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.1.tgz", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.58.1", "@typescript-eslint/types": "8.58.1", "@typescript-eslint/typescript-estree": "8.58.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-Ln8R0tmWC7pTtLOzgJzYTXSCjJ9rDNHAqTaVONF4FEi2qwce8mD9iSOxOpLFFvWp/wBFlew0mjM1L1ihYWfBdQ=="],
|
|
||||||
|
|
||||||
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.58.1", "http://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.1.tgz", { "dependencies": { "@typescript-eslint/types": "8.58.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-y+vH7QE8ycjoa0bWciFg7OpFcipUuem1ujhrdLtq1gByKwfbC7bPeKsiny9e0urg93DqwGcHey+bGRKCnF1nZQ=="],
|
|
||||||
|
|
||||||
"@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.1", "http://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.7" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ=="],
|
"@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.1", "http://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.7" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ=="],
|
||||||
|
|
||||||
"acorn": ["acorn@8.16.0", "http://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
|
"acorn": ["acorn@8.16.0", "http://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
|
||||||
@@ -290,8 +294,6 @@
|
|||||||
|
|
||||||
"ajv": ["ajv@6.14.0", "http://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="],
|
"ajv": ["ajv@6.14.0", "http://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="],
|
||||||
|
|
||||||
"ansi-styles": ["ansi-styles@4.3.0", "http://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
|
||||||
|
|
||||||
"argparse": ["argparse@2.0.1", "http://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
"argparse": ["argparse@2.0.1", "http://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||||
|
|
||||||
"array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "http://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="],
|
"array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "http://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="],
|
||||||
@@ -316,12 +318,16 @@
|
|||||||
|
|
||||||
"balanced-match": ["balanced-match@1.0.0", "http://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", {}, "sha512-9Y0g0Q8rmSt+H33DfKv7FOc3v+iRI+o1lbzt8jGcIosYW37IIW/2XVYq5NPdmaD5NQ59Nk26Kl/vZbwW9Fr8vg=="],
|
"balanced-match": ["balanced-match@1.0.0", "http://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", {}, "sha512-9Y0g0Q8rmSt+H33DfKv7FOc3v+iRI+o1lbzt8jGcIosYW37IIW/2XVYq5NPdmaD5NQ59Nk26Kl/vZbwW9Fr8vg=="],
|
||||||
|
|
||||||
|
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.20", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-1AaXxEPfXT+GvTBJFuy4yXVHWJBXa4OdbIebGN/wX5DlsIkU0+wzGnd2lOzokSk51d5LUmqjgBLRLlypLUqInQ=="],
|
||||||
|
|
||||||
"bezier-easing": ["bezier-easing@2.1.0", "http://registry.npmjs.org/bezier-easing/-/bezier-easing-2.1.0.tgz", {}, "sha512-gbIqZ/eslnUFC1tjEvtz0sgx+xTK20wDnYMIA27VA04R7w6xxXQPZDbibjA9DTWZRA2CXtwHykkVzlCaAJAZig=="],
|
"bezier-easing": ["bezier-easing@2.1.0", "http://registry.npmjs.org/bezier-easing/-/bezier-easing-2.1.0.tgz", {}, "sha512-gbIqZ/eslnUFC1tjEvtz0sgx+xTK20wDnYMIA27VA04R7w6xxXQPZDbibjA9DTWZRA2CXtwHykkVzlCaAJAZig=="],
|
||||||
|
|
||||||
"brace-expansion": ["brace-expansion@1.1.11", "http://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
|
"brace-expansion": ["brace-expansion@1.1.11", "http://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
|
||||||
|
|
||||||
"braces": ["braces@3.0.3", "http://registry.npmjs.org/braces/-/braces-3.0.3.tgz", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
"braces": ["braces@3.0.3", "http://registry.npmjs.org/braces/-/braces-3.0.3.tgz", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
||||||
|
|
||||||
|
"browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="],
|
||||||
|
|
||||||
"call-bind": ["call-bind@1.0.9", "http://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" } }, "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ=="],
|
"call-bind": ["call-bind@1.0.9", "http://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" } }, "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ=="],
|
||||||
|
|
||||||
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "http://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "http://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||||
@@ -330,19 +336,15 @@
|
|||||||
|
|
||||||
"callsites": ["callsites@3.1.0", "http://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
|
"callsites": ["callsites@3.1.0", "http://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
|
||||||
|
|
||||||
"chalk": ["chalk@4.1.2", "http://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
"caniuse-lite": ["caniuse-lite@1.0.30001788", "", {}, "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ=="],
|
||||||
|
|
||||||
"chokidar": ["chokidar@4.0.3", "http://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
"chokidar": ["chokidar@4.0.3", "http://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
||||||
|
|
||||||
"clsx": ["clsx@2.1.1", "http://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
"clsx": ["clsx@2.1.1", "http://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||||
|
|
||||||
"color-convert": ["color-convert@2.0.1", "http://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
|
||||||
|
|
||||||
"color-name": ["color-name@1.1.4", "http://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
|
||||||
|
|
||||||
"concat-map": ["concat-map@0.0.1", "http://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
|
"concat-map": ["concat-map@0.0.1", "http://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
|
||||||
|
|
||||||
"convert-source-map": ["convert-source-map@1.9.0", "http://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="],
|
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||||
|
|
||||||
"cosmiconfig": ["cosmiconfig@7.1.0", "http://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", { "dependencies": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.2.1", "parse-json": "^5.0.0", "path-type": "^4.0.0", "yaml": "^1.10.0" } }, "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA=="],
|
"cosmiconfig": ["cosmiconfig@7.1.0", "http://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", { "dependencies": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.2.1", "parse-json": "^5.0.0", "path-type": "^4.0.0", "yaml": "^1.10.0" } }, "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA=="],
|
||||||
|
|
||||||
@@ -392,6 +394,8 @@
|
|||||||
|
|
||||||
"dunder-proto": ["dunder-proto@1.0.1", "http://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
"dunder-proto": ["dunder-proto@1.0.1", "http://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||||
|
|
||||||
|
"electron-to-chromium": ["electron-to-chromium@1.5.340", "", {}, "sha512-908qahOGocRMinT2nM3ajCEM99H4iPdv84eagPP3FfZy/1ZGeOy2CZYzjhms81ckOPCXPlW7LkY4XpxD8r1DrA=="],
|
||||||
|
|
||||||
"error-ex": ["error-ex@1.3.4", "http://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="],
|
"error-ex": ["error-ex@1.3.4", "http://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="],
|
||||||
|
|
||||||
"es-abstract": ["es-abstract@1.24.2", "http://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg=="],
|
"es-abstract": ["es-abstract@1.24.2", "http://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg=="],
|
||||||
@@ -410,15 +414,17 @@
|
|||||||
|
|
||||||
"es-to-primitive": ["es-to-primitive@1.3.0", "http://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="],
|
"es-to-primitive": ["es-to-primitive@1.3.0", "http://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="],
|
||||||
|
|
||||||
|
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||||
|
|
||||||
"escape-string-regexp": ["escape-string-regexp@4.0.0", "http://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
"escape-string-regexp": ["escape-string-regexp@4.0.0", "http://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
||||||
|
|
||||||
"eslint": ["eslint@9.39.4", "http://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.5", "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ=="],
|
"eslint": ["eslint@10.2.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.5.5", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-wiyGaKsDgqXvF40P8mDwiUp/KQjE1FdrIEJsM8PZ3XCiniTMXS3OHWWUe5FI5agoCnr8x4xPrTDZuxsBlNHl+Q=="],
|
||||||
|
|
||||||
"eslint-plugin-react": ["eslint-plugin-react@7.37.5", "http://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", { "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.3", "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", "es-iterator-helpers": "^1.2.1", "estraverse": "^5.3.0", "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", "object.entries": "^1.1.9", "object.fromentries": "^2.0.8", "object.values": "^1.2.1", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", "string.prototype.matchall": "^4.0.12", "string.prototype.repeat": "^1.0.0" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA=="],
|
"eslint-plugin-react": ["eslint-plugin-react@7.37.5", "http://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", { "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.3", "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", "es-iterator-helpers": "^1.2.1", "estraverse": "^5.3.0", "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", "object.entries": "^1.1.9", "object.fromentries": "^2.0.8", "object.values": "^1.2.1", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", "string.prototype.matchall": "^4.0.12", "string.prototype.repeat": "^1.0.0" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA=="],
|
||||||
|
|
||||||
"eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@5.2.0", "http://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", { "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg=="],
|
"eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.1.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" } }, "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g=="],
|
||||||
|
|
||||||
"eslint-scope": ["eslint-scope@8.4.0", "http://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
|
"eslint-scope": ["eslint-scope@9.1.2", "", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ=="],
|
||||||
|
|
||||||
"eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "http://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
|
"eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "http://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
|
||||||
|
|
||||||
@@ -470,6 +476,8 @@
|
|||||||
|
|
||||||
"generator-function": ["generator-function@2.0.1", "http://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="],
|
"generator-function": ["generator-function@2.0.1", "http://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="],
|
||||||
|
|
||||||
|
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
|
||||||
|
|
||||||
"get-intrinsic": ["get-intrinsic@1.3.0", "http://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
"get-intrinsic": ["get-intrinsic@1.3.0", "http://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||||
|
|
||||||
"get-proto": ["get-proto@1.0.1", "http://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
"get-proto": ["get-proto@1.0.1", "http://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||||
@@ -478,7 +486,7 @@
|
|||||||
|
|
||||||
"glob-parent": ["glob-parent@6.0.2", "http://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
"glob-parent": ["glob-parent@6.0.2", "http://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
||||||
|
|
||||||
"globals": ["globals@15.15.0", "http://registry.npmjs.org/globals/-/globals-15.15.0.tgz", {}, "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg=="],
|
"globals": ["globals@17.5.0", "", {}, "sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g=="],
|
||||||
|
|
||||||
"globalthis": ["globalthis@1.0.4", "http://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="],
|
"globalthis": ["globalthis@1.0.4", "http://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="],
|
||||||
|
|
||||||
@@ -486,8 +494,6 @@
|
|||||||
|
|
||||||
"has-bigints": ["has-bigints@1.1.0", "http://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="],
|
"has-bigints": ["has-bigints@1.1.0", "http://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="],
|
||||||
|
|
||||||
"has-flag": ["has-flag@4.0.0", "http://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
|
||||||
|
|
||||||
"has-property-descriptors": ["has-property-descriptors@1.0.2", "http://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="],
|
"has-property-descriptors": ["has-property-descriptors@1.0.2", "http://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="],
|
||||||
|
|
||||||
"has-proto": ["has-proto@1.2.0", "http://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", { "dependencies": { "dunder-proto": "^1.0.0" } }, "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ=="],
|
"has-proto": ["has-proto@1.2.0", "http://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", { "dependencies": { "dunder-proto": "^1.0.0" } }, "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ=="],
|
||||||
@@ -498,9 +504,13 @@
|
|||||||
|
|
||||||
"hasown": ["hasown@2.0.2", "http://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
"hasown": ["hasown@2.0.2", "http://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||||
|
|
||||||
|
"hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="],
|
||||||
|
|
||||||
|
"hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="],
|
||||||
|
|
||||||
"hoist-non-react-statics": ["hoist-non-react-statics@3.3.2", "http://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="],
|
"hoist-non-react-statics": ["hoist-non-react-statics@3.3.2", "http://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="],
|
||||||
|
|
||||||
"howler": ["howler@2.1.2", "http://registry.npmjs.org/howler/-/howler-2.1.2.tgz", {}, "sha512-oKrTFaVXsDRoB/jik7cEpWKTj7VieoiuzMYJ7E/EU5ayvmpRhumCv3YQ3823zi9VTJkSWAhbryHnlZAionGAJg=="],
|
"howler": ["howler@2.2.4", "http://registry.npmjs.org/howler/-/howler-2.2.4.tgz", {}, "sha512-iARIBPgcQrwtEr+tALF+rapJ8qSc+Set2GJQl7xT1MQzWaVkFebdJhR3alVlSiUf5U7nAANKuj3aWpwerocD5w=="],
|
||||||
|
|
||||||
"ignore": ["ignore@5.3.2", "http://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
"ignore": ["ignore@5.3.2", "http://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||||
|
|
||||||
@@ -586,6 +596,8 @@
|
|||||||
|
|
||||||
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "http://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
|
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "http://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
|
||||||
|
|
||||||
|
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
||||||
|
|
||||||
"jsx-ast-utils": ["jsx-ast-utils@3.3.5", "http://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="],
|
"jsx-ast-utils": ["jsx-ast-utils@3.3.5", "http://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="],
|
||||||
|
|
||||||
"keyv": ["keyv@4.5.4", "http://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
|
"keyv": ["keyv@4.5.4", "http://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
|
||||||
@@ -622,10 +634,10 @@
|
|||||||
|
|
||||||
"lodash": ["lodash@4.18.1", "http://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="],
|
"lodash": ["lodash@4.18.1", "http://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="],
|
||||||
|
|
||||||
"lodash.merge": ["lodash.merge@4.6.2", "http://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
|
|
||||||
|
|
||||||
"loose-envify": ["loose-envify@1.4.0", "http://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": "cli.js" }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
|
"loose-envify": ["loose-envify@1.4.0", "http://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": "cli.js" }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
|
||||||
|
|
||||||
|
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||||
|
|
||||||
"math-intrinsics": ["math-intrinsics@1.1.0", "http://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
"math-intrinsics": ["math-intrinsics@1.1.0", "http://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||||
|
|
||||||
"merge2": ["merge2@1.4.1", "http://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
|
"merge2": ["merge2@1.4.1", "http://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
|
||||||
@@ -646,6 +658,8 @@
|
|||||||
|
|
||||||
"node-exports-info": ["node-exports-info@1.6.0", "http://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", { "dependencies": { "array.prototype.flatmap": "^1.3.3", "es-errors": "^1.3.0", "object.entries": "^1.1.9", "semver": "^6.3.1" } }, "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw=="],
|
"node-exports-info": ["node-exports-info@1.6.0", "http://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", { "dependencies": { "array.prototype.flatmap": "^1.3.3", "es-errors": "^1.3.0", "object.entries": "^1.1.9", "semver": "^6.3.1" } }, "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw=="],
|
||||||
|
|
||||||
|
"node-releases": ["node-releases@2.0.37", "", {}, "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg=="],
|
||||||
|
|
||||||
"object-assign": ["object-assign@4.1.1", "http://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
"object-assign": ["object-assign@4.1.1", "http://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||||
|
|
||||||
"object-inspect": ["object-inspect@1.13.4", "http://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
"object-inspect": ["object-inspect@1.13.4", "http://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
||||||
@@ -690,7 +704,7 @@
|
|||||||
|
|
||||||
"prelude-ls": ["prelude-ls@1.2.1", "http://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
"prelude-ls": ["prelude-ls@1.2.1", "http://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
||||||
|
|
||||||
"prop-types": ["prop-types@15.7.2", "http://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.8.1" } }, "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ=="],
|
"prop-types": ["prop-types@15.8.1", "http://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
|
||||||
|
|
||||||
"punycode": ["punycode@2.3.1", "http://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
"punycode": ["punycode@2.3.1", "http://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||||
|
|
||||||
@@ -774,8 +788,6 @@
|
|||||||
|
|
||||||
"stylis": ["stylis@4.2.0", "http://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", {}, "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw=="],
|
"stylis": ["stylis@4.2.0", "http://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", {}, "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw=="],
|
||||||
|
|
||||||
"supports-color": ["supports-color@7.2.0", "http://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
|
||||||
|
|
||||||
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "http://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
|
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "http://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
|
||||||
|
|
||||||
"tinyglobby": ["tinyglobby@0.2.16", "http://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="],
|
"tinyglobby": ["tinyglobby@0.2.16", "http://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="],
|
||||||
@@ -784,8 +796,6 @@
|
|||||||
|
|
||||||
"totalist": ["totalist@3.0.1", "http://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="],
|
"totalist": ["totalist@3.0.1", "http://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="],
|
||||||
|
|
||||||
"ts-api-utils": ["ts-api-utils@2.5.0", "http://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="],
|
|
||||||
|
|
||||||
"tslib": ["tslib@2.8.1", "http://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
"tslib": ["tslib@2.8.1", "http://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
"type-check": ["type-check@0.4.0", "http://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
|
"type-check": ["type-check@0.4.0", "http://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
|
||||||
@@ -798,10 +808,10 @@
|
|||||||
|
|
||||||
"typed-array-length": ["typed-array-length@1.0.7", "http://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="],
|
"typed-array-length": ["typed-array-length@1.0.7", "http://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="],
|
||||||
|
|
||||||
"typescript": ["typescript@6.0.2", "http://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ=="],
|
|
||||||
|
|
||||||
"unbox-primitive": ["unbox-primitive@1.1.0", "http://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="],
|
"unbox-primitive": ["unbox-primitive@1.1.0", "http://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="],
|
||||||
|
|
||||||
|
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
|
||||||
|
|
||||||
"uri-js": ["uri-js@4.4.1", "http://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
"uri-js": ["uri-js@4.4.1", "http://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
||||||
|
|
||||||
"use-sync-external-store": ["use-sync-external-store@1.6.0", "http://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
|
"use-sync-external-store": ["use-sync-external-store@1.6.0", "http://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
|
||||||
@@ -822,35 +832,31 @@
|
|||||||
|
|
||||||
"word-wrap": ["word-wrap@1.2.5", "http://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
|
"word-wrap": ["word-wrap@1.2.5", "http://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
|
||||||
|
|
||||||
|
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||||
|
|
||||||
"yaml": ["yaml@1.10.3", "http://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", {}, "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA=="],
|
"yaml": ["yaml@1.10.3", "http://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", {}, "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA=="],
|
||||||
|
|
||||||
"yocto-queue": ["yocto-queue@0.1.0", "http://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
"yocto-queue": ["yocto-queue@0.1.0", "http://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||||
|
|
||||||
|
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
|
||||||
|
|
||||||
|
"zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="],
|
||||||
|
|
||||||
|
"@emotion/babel-plugin/convert-source-map": ["convert-source-map@1.9.0", "http://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="],
|
||||||
|
|
||||||
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "http://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "http://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||||
|
|
||||||
|
"@eslint/config-array/minimatch": ["minimatch@10.2.5", "http://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
|
||||||
|
|
||||||
"@eslint/eslintrc/globals": ["globals@14.0.0", "http://registry.npmjs.org/globals/-/globals-14.0.0.tgz", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
|
"@eslint/eslintrc/globals": ["globals@14.0.0", "http://registry.npmjs.org/globals/-/globals-14.0.0.tgz", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
|
||||||
|
|
||||||
"@mui/material/prop-types": ["prop-types@15.8.1", "http://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
|
|
||||||
|
|
||||||
"@mui/private-theming/prop-types": ["prop-types@15.8.1", "http://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
|
|
||||||
|
|
||||||
"@mui/styled-engine/prop-types": ["prop-types@15.8.1", "http://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
|
|
||||||
|
|
||||||
"@mui/system/prop-types": ["prop-types@15.8.1", "http://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
|
|
||||||
|
|
||||||
"@mui/utils/prop-types": ["prop-types@15.8.1", "http://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
|
|
||||||
|
|
||||||
"@mui/x-charts/prop-types": ["prop-types@15.8.1", "http://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
|
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.5", "http://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
|
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree/semver": ["semver@7.7.4", "http://registry.npmjs.org/semver/-/semver-7.7.4.tgz", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
|
||||||
|
|
||||||
"@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "http://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
|
|
||||||
|
|
||||||
"babel-plugin-macros/resolve": ["resolve@1.22.12", "http://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="],
|
"babel-plugin-macros/resolve": ["resolve@1.22.12", "http://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="],
|
||||||
|
|
||||||
"eslint-plugin-react/prop-types": ["prop-types@15.8.1", "http://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
|
"eslint/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "http://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
|
||||||
|
|
||||||
|
"eslint/espree": ["espree@11.2.0", "", { "dependencies": { "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^5.0.1" } }, "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw=="],
|
||||||
|
|
||||||
|
"eslint/minimatch": ["minimatch@10.2.5", "http://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
|
||||||
|
|
||||||
"fast-glob/glob-parent": ["glob-parent@5.1.2", "http://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
"fast-glob/glob-parent": ["glob-parent@5.1.2", "http://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||||
|
|
||||||
@@ -858,30 +864,16 @@
|
|||||||
|
|
||||||
"micromatch/picomatch": ["picomatch@2.3.2", "http://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
|
"micromatch/picomatch": ["picomatch@2.3.2", "http://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
|
||||||
|
|
||||||
"prop-types/react-is": ["react-is@16.11.0", "http://registry.npmjs.org/react-is/-/react-is-16.11.0.tgz", {}, "sha512-gbBVYR2p8mnriqAwWx9LbuUrShnAuSCNnuPGyc7GJrMVQtPDAh8iLpv7FRuMPFb56KkaVZIYSz1PrjI9q0QPCw=="],
|
"prop-types/react-is": ["react-is@16.13.1", "http://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
||||||
|
|
||||||
"react-transition-group/prop-types": ["prop-types@15.8.1", "http://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
|
|
||||||
|
|
||||||
"rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.15", "http://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz", {}, "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g=="],
|
"rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.15", "http://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz", {}, "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g=="],
|
||||||
|
|
||||||
"@mui/material/prop-types/react-is": ["react-is@16.13.1", "http://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
"@eslint/config-array/minimatch/brace-expansion": ["brace-expansion@5.0.5", "http://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="],
|
||||||
|
|
||||||
"@mui/private-theming/prop-types/react-is": ["react-is@16.13.1", "http://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
"eslint/minimatch/brace-expansion": ["brace-expansion@5.0.5", "http://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="],
|
||||||
|
|
||||||
"@mui/styled-engine/prop-types/react-is": ["react-is@16.13.1", "http://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
"@eslint/config-array/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "http://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
||||||
|
|
||||||
"@mui/system/prop-types/react-is": ["react-is@16.13.1", "http://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
"eslint/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "http://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
||||||
|
|
||||||
"@mui/utils/prop-types/react-is": ["react-is@16.13.1", "http://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
|
||||||
|
|
||||||
"@mui/x-charts/prop-types/react-is": ["react-is@16.13.1", "http://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.5", "http://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="],
|
|
||||||
|
|
||||||
"eslint-plugin-react/prop-types/react-is": ["react-is@16.13.1", "http://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
|
||||||
|
|
||||||
"react-transition-group/prop-types/react-is": ["react-is@16.13.1", "http://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "http://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
15
compose.yaml
@@ -11,6 +11,8 @@ services:
|
|||||||
SERVER_NAME: ${SERVER_NAME:-:80}
|
SERVER_NAME: ${SERVER_NAME:-:80}
|
||||||
APP_ENV: prod
|
APP_ENV: prod
|
||||||
APP_SECRET: ${APP_SECRET}
|
APP_SECRET: ${APP_SECRET}
|
||||||
|
APP_PUBLIC_HOSTNAME: ${APP_PUBLIC_HOSTNAME:-localhost}
|
||||||
|
APP_CONTACT_MAIL_ADDRESS: ${APP_CONTACT_MAIL_ADDRESS:-7system7@gmail.com}
|
||||||
DATABASE_URL: >-
|
DATABASE_URL: >-
|
||||||
postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}?serverVersion=${POSTGRES_VERSION}&charset=utf8
|
postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}?serverVersion=${POSTGRES_VERSION}&charset=utf8
|
||||||
POSTGRES_URL: db
|
POSTGRES_URL: db
|
||||||
@@ -31,6 +33,11 @@ services:
|
|||||||
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
|
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
|
||||||
MINIO_ENDPOINT: http://minio:9000
|
MINIO_ENDPOINT: http://minio:9000
|
||||||
MINIO_PUBLIC_URL: ${MINIO_PUBLIC_URL:-http://localhost:9000}
|
MINIO_PUBLIC_URL: ${MINIO_PUBLIC_URL:-http://localhost:9000}
|
||||||
|
# IMPORTANT: Set TRUSTED_PROXIES to your reverse proxy IP in production.
|
||||||
|
# For Docker on same host, use: 172.17.0.1 (default bridge) or 172.16.0.0/12 (overlay network)
|
||||||
|
# For Kubernetes or external proxy, use the proxy's IP address.
|
||||||
|
# WARNING: Using 0.0.0.0/0 is insecure in production environments!
|
||||||
|
TRUSTED_PROXIES: ${TRUSTED_PROXIES:-127.0.0.1}
|
||||||
volumes:
|
volumes:
|
||||||
- app_var:/app/var
|
- app_var:/app/var
|
||||||
- caddy_data:/data
|
- caddy_data:/data
|
||||||
@@ -76,13 +83,19 @@ services:
|
|||||||
image: boky/postfix:latest
|
image: boky/postfix:latest
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
container_name: '${APP_NAME}-mail'
|
container_name: '${APP_NAME}-mail'
|
||||||
|
hostname: postfix.mail.${MAIL_DOMAIN:-localhost}
|
||||||
environment:
|
environment:
|
||||||
ALLOWED_SENDER_DOMAINS: ${MAIL_DOMAIN:-localhost}
|
ALLOWED_SENDER_DOMAINS: ${MAIL_DOMAIN:-localhost}
|
||||||
|
HOSTNAME: postfix.mail.${MAIL_DOMAIN:-localhost}
|
||||||
|
POSTFIX_myhostname: postfix.mail.${MAIL_DOMAIN:-localhost}
|
||||||
|
POSTFIX_mynetworks: "127.0.0.0/8 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16"
|
||||||
|
POSTFIX_smtpd_tls_security_level: none
|
||||||
RELAYHOST: ${MAIL_RELAYHOST:-}
|
RELAYHOST: ${MAIL_RELAYHOST:-}
|
||||||
RELAYHOST_AUTH: ${MAIL_RELAYHOST_AUTH:-}
|
RELAYHOST_AUTH: ${MAIL_RELAYHOST_AUTH:-}
|
||||||
RELAYHOST_PASSWORD: ${MAIL_RELAYHOST_PASSWORD:-}
|
RELAYHOST_PASSWORD: ${MAIL_RELAYHOST_PASSWORD:-}
|
||||||
volumes:
|
volumes:
|
||||||
- postfix_spool:/var/spool/postfix
|
- postfix_spool:/var/spool/postfix
|
||||||
|
- ./docker/aliases:/tmp/aliases:ro
|
||||||
db:
|
db:
|
||||||
image: postgres:${POSTGRES_VERSION:-18}-alpine
|
image: postgres:${POSTGRES_VERSION:-18}-alpine
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
@@ -108,3 +121,5 @@ volumes:
|
|||||||
caddy_config:
|
caddy_config:
|
||||||
postfix_spool:
|
postfix_spool:
|
||||||
minio_data:
|
minio_data:
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
189
composer.json
@@ -1,94 +1,103 @@
|
|||||||
{
|
{
|
||||||
"type": "project",
|
"name": "splendid-bear/mineseeker",
|
||||||
"license": "proprietary",
|
"version": "2026.2.1",
|
||||||
"require": {
|
"license": "GPL-3.0-or-later",
|
||||||
"php": ">=8.5",
|
"author": "https://www.splendidbear.org",
|
||||||
"ext-iconv": "*",
|
"bugs": "https://source.splendidbear.org",
|
||||||
"ext-json": "*",
|
"description": "This is a minesweeper game that is inspired from MSN Messenger's game.",
|
||||||
"doctrine/dbal": "^3.7",
|
"minimum-stability": "dev",
|
||||||
"doctrine/doctrine-bundle": ">=2.11 <2.14",
|
"type": "project",
|
||||||
"doctrine/doctrine-migrations-bundle": "^3.0",
|
"prefer-stable": true,
|
||||||
"doctrine/orm": "^2.6",
|
"private": true,
|
||||||
"endroid/qr-code": "^6.1",
|
"require": {
|
||||||
"league/flysystem-aws-s3-v3": "^3.0",
|
"php": ">=8.5",
|
||||||
"league/flysystem-bundle": "^3.6",
|
"ext-gd": "*",
|
||||||
"liip/imagine-bundle": "^2.13",
|
"ext-iconv": "*",
|
||||||
"pentatrion/vite-bundle": "^8.2",
|
"ext-json": "*",
|
||||||
"scheb/2fa-backup-code": "^8.5",
|
"doctrine/dbal": "^4.3",
|
||||||
"scheb/2fa-bundle": "^8.5",
|
"doctrine/doctrine-bundle": "^3.2",
|
||||||
"scheb/2fa-totp": "^8.5",
|
"doctrine/doctrine-migrations-bundle": "^4.0",
|
||||||
"symfony/console": "7.4.*",
|
"doctrine/orm": "^3.5",
|
||||||
"symfony/flex": "^2.10.0",
|
"endroid/qr-code": "^6.1",
|
||||||
"symfony/form": "7.4.*",
|
"firebase/php-jwt": "^7.0",
|
||||||
"symfony/framework-bundle": "7.4.*",
|
"league/flysystem-aws-s3-v3": "^3.0",
|
||||||
"symfony/http-client": "7.4.*",
|
"league/flysystem-bundle": "^3.6",
|
||||||
"symfony/mailer": "7.4.*",
|
"liip/imagine-bundle": "^2.13",
|
||||||
"symfony/mercure": "^0.6",
|
"pentatrion/vite-bundle": "^8.2",
|
||||||
"symfony/mercure-bundle": "*",
|
"scheb/2fa-backup-code": "^8.5",
|
||||||
"symfony/monolog-bundle": "^3.8",
|
"scheb/2fa-bundle": "^8.5",
|
||||||
"symfony/security-bundle": "7.4.*",
|
"scheb/2fa-totp": "^8.5",
|
||||||
"symfony/translation": "7.4.*",
|
"symfony/console": "7.4.*",
|
||||||
"symfony/twig-bundle": "7.4.*",
|
"symfony/flex": "^2.10.0",
|
||||||
"symfony/validator": "7.4.*",
|
"symfony/form": "7.4.*",
|
||||||
"symfony/yaml": "7.4.*",
|
"symfony/framework-bundle": "7.4.*",
|
||||||
"web-auth/webauthn-framework": "^5.2"
|
"symfony/http-client": "7.4.*",
|
||||||
|
"symfony/mailer": "7.4.*",
|
||||||
|
"symfony/mercure": "^0.6",
|
||||||
|
"symfony/mercure-bundle": "*",
|
||||||
|
"symfony/monolog-bundle": "^3.8",
|
||||||
|
"symfony/security-bundle": "7.4.*",
|
||||||
|
"symfony/translation": "7.4.*",
|
||||||
|
"symfony/twig-bundle": "7.4.*",
|
||||||
|
"symfony/validator": "7.4.*",
|
||||||
|
"symfony/yaml": "7.4.*",
|
||||||
|
"web-auth/webauthn-framework": "^5.2"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"roave/security-advisories": "dev-master",
|
||||||
|
"symfony/dotenv": "7.4.*",
|
||||||
|
"symfony/maker-bundle": "^1.5",
|
||||||
|
"symfony/stopwatch": "7.4.*",
|
||||||
|
"symfony/web-profiler-bundle": "7.4.*"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"preferred-install": {
|
||||||
|
"*": "dist"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"sort-packages": true,
|
||||||
"firebase/php-jwt": "^7.0",
|
"allow-plugins": {
|
||||||
"roave/security-advisories": "dev-master",
|
"symfony/flex": true
|
||||||
"symfony/dotenv": "7.4.*",
|
|
||||||
"symfony/maker-bundle": "^1.5",
|
|
||||||
"symfony/stopwatch": "7.4.*",
|
|
||||||
"symfony/web-profiler-bundle": "7.4.*"
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"preferred-install": {
|
|
||||||
"*": "dist"
|
|
||||||
},
|
|
||||||
"sort-packages": true,
|
|
||||||
"allow-plugins": {
|
|
||||||
"symfony/flex": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"autoload": {
|
|
||||||
"psr-4": {
|
|
||||||
"App\\": "src/"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"autoload-dev": {
|
|
||||||
"psr-4": {
|
|
||||||
"App\\Tests\\": "tests/"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"replace": {
|
|
||||||
"symfony/polyfill-iconv": "*",
|
|
||||||
"symfony/polyfill-ctype": "*",
|
|
||||||
"symfony/polyfill-php73": "*",
|
|
||||||
"symfony/polyfill-php72": "*",
|
|
||||||
"symfony/polyfill-php71": "*",
|
|
||||||
"symfony/polyfill-php70": "*",
|
|
||||||
"symfony/polyfill-php56": "*"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"auto-scripts": {
|
|
||||||
"cache:clear": "symfony-cmd",
|
|
||||||
"assets:install --symlink --relative %PUBLIC_DIR%": "symfony-cmd"
|
|
||||||
},
|
|
||||||
"post-install-cmd": [
|
|
||||||
"@auto-scripts"
|
|
||||||
],
|
|
||||||
"post-update-cmd": [
|
|
||||||
"@auto-scripts"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"conflict": {
|
|
||||||
"symfony/symfony": "*",
|
|
||||||
"doctrine/persistence": "<1.3"
|
|
||||||
},
|
|
||||||
"extra": {
|
|
||||||
"symfony": {
|
|
||||||
"allow-contrib": false,
|
|
||||||
"require": "7.4.*"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"App\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"App\\Tests\\": "tests/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"replace": {
|
||||||
|
"symfony/polyfill-iconv": "*",
|
||||||
|
"symfony/polyfill-ctype": "*",
|
||||||
|
"symfony/polyfill-php73": "*",
|
||||||
|
"symfony/polyfill-php72": "*",
|
||||||
|
"symfony/polyfill-php71": "*",
|
||||||
|
"symfony/polyfill-php70": "*",
|
||||||
|
"symfony/polyfill-php56": "*"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"auto-scripts": {
|
||||||
|
"cache:clear": "symfony-cmd",
|
||||||
|
"assets:install --symlink --relative %PUBLIC_DIR%": "symfony-cmd"
|
||||||
|
},
|
||||||
|
"post-install-cmd": [
|
||||||
|
"@auto-scripts"
|
||||||
|
],
|
||||||
|
"post-update-cmd": [
|
||||||
|
"@auto-scripts"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"conflict": {
|
||||||
|
"symfony/symfony": "*",
|
||||||
|
"doctrine/persistence": "<1.3"
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"symfony": {
|
||||||
|
"allow-contrib": false,
|
||||||
|
"require": "7.4.*"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
673
composer.lock
generated
@@ -11,9 +11,12 @@ doctrine:
|
|||||||
url: '%env(resolve:DATABASE_URL)%'
|
url: '%env(resolve:DATABASE_URL)%'
|
||||||
|
|
||||||
orm:
|
orm:
|
||||||
auto_generate_proxy_classes: '%kernel.debug%'
|
enable_native_lazy_objects: true
|
||||||
naming_strategy: doctrine.orm.naming_strategy.underscore
|
naming_strategy: doctrine.orm.naming_strategy.underscore
|
||||||
auto_mapping: true
|
auto_mapping: true
|
||||||
|
schema_ignore_classes:
|
||||||
|
- App\Entity\UserStats
|
||||||
|
- App\Entity\RecentBattle
|
||||||
mappings:
|
mappings:
|
||||||
App:
|
App:
|
||||||
is_bundle: false
|
is_bundle: false
|
||||||
|
|||||||
@@ -8,6 +8,13 @@ framework:
|
|||||||
session:
|
session:
|
||||||
handler_id: ~
|
handler_id: ~
|
||||||
|
|
||||||
|
# Trust headers from reverse proxy (Caddy)
|
||||||
|
# This ensures absolute_url() uses HTTPS scheme when behind a reverse proxy
|
||||||
|
# Production: TRUSTED_PROXIES from .env (Gitea secret)
|
||||||
|
# Development: TRUSTED_PROXIES from compose.override.yaml
|
||||||
|
trusted_proxies: '%env(TRUSTED_PROXIES)%'
|
||||||
|
trusted_headers: ['x-forwarded-for', 'x-forwarded-proto', 'x-forwarded-host', 'x-forwarded-port']
|
||||||
|
|
||||||
#esi: true
|
#esi: true
|
||||||
#fragments: true
|
#fragments: true
|
||||||
php_errors:
|
php_errors:
|
||||||
|
|||||||
8
config/packages/prod/framework.yaml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
framework:
|
||||||
|
# In production with FrankenPHP, the reverse proxy (Caddy) is in the same container
|
||||||
|
# Requests come from 127.0.0.1, so we must trust that IP to process X-Forwarded-Proto headers
|
||||||
|
# TRUSTED_PROXIES is set in the .env file (stored in Gitea secrets)
|
||||||
|
# Typical value for Docker: 172.18.0.0/16 (or the specific Docker network CIDR)
|
||||||
|
# This must be provided by the PROD_ENV_FILE secret in Gitea
|
||||||
|
trusted_proxies: '%env(TRUSTED_PROXIES)%'
|
||||||
|
trusted_headers: ['x-forwarded-for', 'x-forwarded-proto', 'x-forwarded-host', 'x-forwarded-port']
|
||||||
@@ -23,12 +23,14 @@ security:
|
|||||||
auth_code_parameter_name: _auth_code
|
auth_code_parameter_name: _auth_code
|
||||||
post_only: true
|
post_only: true
|
||||||
default_target_path: MineSeekerBundle_homepage
|
default_target_path: MineSeekerBundle_homepage
|
||||||
|
always_use_default_target_path: false
|
||||||
prepare_on_login: true
|
prepare_on_login: true
|
||||||
prepare_on_access_denied: true
|
prepare_on_access_denied: true
|
||||||
form_login:
|
form_login:
|
||||||
login_path: MineSeekerBundle_login
|
login_path: MineSeekerBundle_login
|
||||||
check_path: MineSeekerBundle_login
|
check_path: MineSeekerBundle_login
|
||||||
default_target_path: MineSeekerBundle_homepage
|
default_target_path: MineSeekerBundle_homepage
|
||||||
|
always_use_default_target_path: false
|
||||||
username_parameter: _username
|
username_parameter: _username
|
||||||
password_parameter: _password
|
password_parameter: _password
|
||||||
enable_csrf: true
|
enable_csrf: true
|
||||||
|
|||||||
@@ -25,6 +25,11 @@ services:
|
|||||||
resource: '../src/Controller'
|
resource: '../src/Controller'
|
||||||
tags: [ 'controller.service_arguments' ]
|
tags: [ 'controller.service_arguments' ]
|
||||||
|
|
||||||
|
App\Service\BattleCardGenerator:
|
||||||
|
arguments:
|
||||||
|
$cacheDir: '%kernel.project_dir%/var/og-cache'
|
||||||
|
$minioMediaStorage: '@mineseeker.media.storage'
|
||||||
|
|
||||||
Aws\S3\S3Client:
|
Aws\S3\S3Client:
|
||||||
arguments:
|
arguments:
|
||||||
- version: 'latest'
|
- version: 'latest'
|
||||||
|
|||||||
5
docker/aliases
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Postfix aliases file
|
||||||
|
# Mail addressed to system users are redirected to this address
|
||||||
|
postmaster: root
|
||||||
|
root: root
|
||||||
|
|
||||||
47
docs/README.md
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# Mine-Seeker Game Documentation
|
||||||
|
|
||||||
|
This directory contains comprehensive documentation about the Mine-Seeker game mechanics and implementation.
|
||||||
|
|
||||||
|
## Game Mechanics
|
||||||
|
|
||||||
|
### [Bonus Points System](./game-mechanics/BONUS_POINTS_SYSTEM.md)
|
||||||
|
Complete reference for the bonus points system including:
|
||||||
|
- All 6 bonus point types (Blind Hit, Chain Combo, Edge Mine, Endgame Mine, Safe Cell Bonus, Biggest Reveal)
|
||||||
|
- Calculation rules and examples
|
||||||
|
- Bonus statistics tracking
|
||||||
|
- Player name formatting in dialogs
|
||||||
|
- Database schema
|
||||||
|
- Implementation notes
|
||||||
|
- Testing checklist
|
||||||
|
|
||||||
|
**Recommended for**: Developers working on bonus system, AI assistants implementing or debugging bonus features, understanding game scoring mechanics.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
### Bonus Points at a Glance
|
||||||
|
| Bonus Type | Points | Condition |
|
||||||
|
|-----------|--------|-----------|
|
||||||
|
| Blind Hit | +2 | Mine with no revealed numbered neighbors |
|
||||||
|
| Edge Mine | +1 | Mine on board boundary (row/col 0 or 15) |
|
||||||
|
| Endgame Mine | +3 | Mine clicked when ≤10 mines remain |
|
||||||
|
| Safe Cell | +0.5 each | ≥2 safe cells revealed (min requirement) |
|
||||||
|
| Chain Combo | Tracked | Consecutive mine clicks (no safe clicks) |
|
||||||
|
| Biggest Reveal | Tracked | Largest number of safe cells revealed |
|
||||||
|
|
||||||
|
### Key Rules
|
||||||
|
- Safe cell bonus only awarded for ≥2 cells minimum
|
||||||
|
- Chain counter resets on any safe cell click
|
||||||
|
- Endgame threshold: 51 - (redPoints + bluePoints) ≤ 10
|
||||||
|
- Bonus stats are per-player and persist in database
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Files Using This Information
|
||||||
|
- Backend: `/src/Util/TopicManager.php`
|
||||||
|
- Frontend: `/assets/js/mine-seeker/contexts/GameProvider.jsx`
|
||||||
|
- UI: `/assets/js/mine-seeker/components/BonusStatsDialog.jsx`
|
||||||
|
- Constants: `/assets/js/mine-seeker/utils/constants.jsx`
|
||||||
|
|
||||||
|
|
||||||
168
docs/game-mechanics/BONUS_POINTS_SYSTEM.md
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
# Mine-Seeker Bonus Points System
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
The Mine-Seeker game includes a bonus points system that rewards skilled play. Bonus points are tracked separately from the main score and displayed in the "Bonus Statistics" dialog.
|
||||||
|
|
||||||
|
## Bonus Point Types
|
||||||
|
|
||||||
|
### 1. Blind Hit (+2 points)
|
||||||
|
**When**: Click a mine with no revealed numbered neighbors around it.
|
||||||
|
|
||||||
|
**Example**: Mine surrounded by unrevealed cells = +2 points
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Chain Combo
|
||||||
|
**When**: Click consecutive mines without clicking any safe cell in between.
|
||||||
|
|
||||||
|
**Tracked as**:
|
||||||
|
- `chainCurrent`: Current streak (resets when you click a safe cell)
|
||||||
|
- `chainBest`: Longest streak achieved
|
||||||
|
|
||||||
|
**Example**: Mine → Mine → Mine = chainBest becomes 3
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Edge Mine (+1 point)
|
||||||
|
**When**: Click a mine on the board boundary (row 0, row 15, col 0, or col 15).
|
||||||
|
|
||||||
|
**Example**: Click a mine on the edge = +1 point
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Endgame Mine (+3 points)
|
||||||
|
**When**: Click a mine when 10 or fewer mines remain on the board.
|
||||||
|
|
||||||
|
**Calculation**: `51 total mines - (red_points + blue_points) = mines_remaining`
|
||||||
|
|
||||||
|
**Example**: When 8 mines remain, click one = +3 points
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Safe Cell Bonus (+0.5 points per cell)
|
||||||
|
**When**: Click a safe cell and reveal 2 or more cells.
|
||||||
|
|
||||||
|
**Important**: Minimum 2 cells required. Single cell reveals = 0 points.
|
||||||
|
|
||||||
|
**Examples**:
|
||||||
|
- Reveal 1 safe cell = 0 points
|
||||||
|
- Reveal 2 safe cells = 1.0 points
|
||||||
|
- Reveal 11 safe cells = 5.5 points
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. Biggest Reveal (Tracking stat)
|
||||||
|
**What**: Tracks the largest number of safe cells revealed in one click.
|
||||||
|
|
||||||
|
**Example**: Largest reveal in a game = 15 cells shown in stats
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bonus Statistics Display
|
||||||
|
|
||||||
|
### Dialog Shows
|
||||||
|
- Both players' bonus statistics side-by-side
|
||||||
|
- Each stat with label, description, and value
|
||||||
|
- Total bonus points per player
|
||||||
|
|
||||||
|
### Player Name Rules
|
||||||
|
- `anon_*` usernames → displays as "Anonymous"
|
||||||
|
- Names longer than 10 chars → truncated to 7 chars + "..." (example: `VeryLongName` → `VeryLon...`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tracked Statistics
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
blindHits: 0, // Blind hit mines clicked
|
||||||
|
chainBest: 0, // Longest mine streak
|
||||||
|
chainCurrent: 0, // Current active streak
|
||||||
|
lastMineHits: 0, // Endgame mines clicked
|
||||||
|
edgeMines: 0, // Edge mine clicks
|
||||||
|
biggestReveal: 0 // Largest safe cell reveal
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Database
|
||||||
|
- `red_bonus_points` (FLOAT) — Red player's total bonus points
|
||||||
|
- `blue_bonus_points` (FLOAT) — Blue player's total bonus points
|
||||||
|
- `red_bonus_stats` (JSON) — Red player's tracked stats
|
||||||
|
- `blue_bonus_stats` (JSON) — Blue player's tracked stats
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Documentation Maintenance
|
||||||
|
|
||||||
|
**IMPORTANT**: This documentation must be updated whenever:
|
||||||
|
- New bonus types are added
|
||||||
|
- Point values change
|
||||||
|
- Bonus calculation logic changes
|
||||||
|
- New stats are tracked
|
||||||
|
- Bonus display rules change
|
||||||
|
|
||||||
|
**Update these files**:
|
||||||
|
1. This file (`BONUS_POINTS_SYSTEM.md`) — Update descriptions and examples
|
||||||
|
2. Code comments in `/src/Util/TopicManager.php` — Explain calculation logic
|
||||||
|
3. `/docs/README.md` — Update Quick Reference table if values change
|
||||||
|
|
||||||
|
**Keep documentation**:
|
||||||
|
- ✅ Simple and clear
|
||||||
|
- ✅ With real code examples
|
||||||
|
- ✅ Synchronized with actual code behavior
|
||||||
|
- ✅ Updated before/after feature changes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Files
|
||||||
|
- Backend: `/src/Util/TopicManager.php` — Bonus calculation logic
|
||||||
|
- Frontend: `/assets/js/mine-seeker/contexts/GameProvider.jsx` — State sync
|
||||||
|
- UI: `/assets/js/mine-seeker/components/BonusStatsDialog.jsx` — Display dialog
|
||||||
|
- Constants: `/assets/js/mine-seeker/utils/constants.jsx` — Labels and defaults
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Battle Report Display Components
|
||||||
|
|
||||||
|
**IMPORTANT**: The Bonus Statistics display appears in **two places** that must be kept in sync:
|
||||||
|
|
||||||
|
### 1. Public Battle Share Page
|
||||||
|
**File**: `/templates/Game/battle_share.html.twig`
|
||||||
|
- Displays via `bshare-bonus` CSS classes
|
||||||
|
- Backend data passed from `ProfileController::battleShare()`
|
||||||
|
- Shows bonus stats as HTML table format
|
||||||
|
|
||||||
|
### 2. Profile Dialog (BattleDialog component)
|
||||||
|
**File**: `/assets/js/components/BattleDialog.jsx`
|
||||||
|
- React component using Material-UI Dialog
|
||||||
|
- Displays inside the match details modal on profile page
|
||||||
|
- Shows bonus stats using `StatRow` components in side-by-side boxes
|
||||||
|
|
||||||
|
### Synchronization Requirements
|
||||||
|
|
||||||
|
When making changes to the bonus statistics display, update **BOTH** files:
|
||||||
|
|
||||||
|
1. **Update logic/data** → Edit both template and component
|
||||||
|
2. **Change stat names** → Update both BONUS_LABELS and both display files
|
||||||
|
3. **Modify formatting** → Keep visual consistency between both displays
|
||||||
|
4. **Add new stats** → Add to both the `.twig` template AND the `.jsx` component
|
||||||
|
|
||||||
|
**Checklist for changes**:
|
||||||
|
- [ ] Update `/src/Util/TopicManager.php` if bonus calculation changes
|
||||||
|
- [ ] Update `/templates/Game/battle_share.html.twig` for public display
|
||||||
|
- [ ] Update `/assets/js/components/BattleDialog.jsx` for profile dialog
|
||||||
|
- [ ] Update `/assets/js/mine-seeker/utils/constants.jsx` if adding new stats
|
||||||
|
- [ ] Test both displays show identical data
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Checklist for Changes
|
||||||
|
- [ ] Code changes implemented
|
||||||
|
- [ ] This documentation updated
|
||||||
|
- [ ] `/docs/README.md` Quick Reference table updated
|
||||||
|
- [ ] Code comments added/updated
|
||||||
|
- [ ] Examples updated to match new behavior
|
||||||
|
- [ ] Both battle report displays tested
|
||||||
|
|
||||||
97
package.json
@@ -1,50 +1,51 @@
|
|||||||
{
|
{
|
||||||
"name": "mine-seeker",
|
"name": "mine-seeker",
|
||||||
"version": "1.0.0",
|
"version": "2026.2.1",
|
||||||
"description": "Mine Seeker Game by system7",
|
"author": "https://www.splendidbear.org",
|
||||||
"keywords": [
|
"license": "GPL-3.0-or-later",
|
||||||
"mine",
|
"bugs": "https://source.splendidbear.org",
|
||||||
"seeker",
|
"description": "Mine Seeker Game by system7",
|
||||||
"game",
|
"private": true,
|
||||||
"multiplayer",
|
"keywords": [
|
||||||
"websocket"
|
"mine",
|
||||||
],
|
"seeker",
|
||||||
"author": "Laszlo Lang <system7>",
|
"game",
|
||||||
"license": "UNLICENSED",
|
"multiplayer",
|
||||||
"private": true,
|
"websocket"
|
||||||
"dependencies": {
|
],
|
||||||
"@emotion/react": "^11.14.0",
|
"dependencies": {
|
||||||
"@emotion/styled": "^11.14.1",
|
"@emotion/react": "^11.14.0",
|
||||||
"@fontsource/changa-one": "^5.2.8",
|
"@emotion/styled": "^11.14.1",
|
||||||
"@fontsource/open-sans": "^5.2.7",
|
"@fontsource/changa-one": "^5.2.8",
|
||||||
"@fontsource/rajdhani": "^5.2.7",
|
"@fontsource/open-sans": "^5.2.7",
|
||||||
"@fortawesome/fontawesome-free": "^7.2.0",
|
"@fontsource/rajdhani": "^5.2.7",
|
||||||
"@mui/material": "^9.0.0",
|
"@fortawesome/fontawesome-free": "^7.2.0",
|
||||||
"@mui/x-charts": "^9.0.1",
|
"@mui/material": "^9.0.0",
|
||||||
"@tanstack/react-query": "^5.0.0",
|
"@mui/x-charts": "^9.0.2",
|
||||||
"howler": "^2.1.2",
|
"@tanstack/react-query": "^5.99.2",
|
||||||
"lodash": "^4.18.1",
|
"howler": "^2.2.4",
|
||||||
"prop-types": "^15.7.2",
|
"lodash": "^4.18.1",
|
||||||
"react": "^19.0.0",
|
"prop-types": "^15.8.1",
|
||||||
"react-dom": "^19.0.0"
|
"react": "^19.2.5",
|
||||||
},
|
"react-dom": "^19.2.5"
|
||||||
"devDependencies": {
|
},
|
||||||
"@eslint/eslintrc": "^3.3.5",
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.0.0",
|
"@eslint/eslintrc": "^3.3.5",
|
||||||
"@stylistic/eslint-plugin": "^4.0.0",
|
"@eslint/js": "10.0.1",
|
||||||
"@vitejs/plugin-react": "^6.0.1",
|
"@stylistic/eslint-plugin": "5.10.0",
|
||||||
"eslint": "^9.0.0",
|
"@vitejs/plugin-react": "^6.0.1",
|
||||||
"eslint-plugin-react": "^7.0.0",
|
"eslint": "10.2.1",
|
||||||
"eslint-plugin-react-hooks": "^5.0.0",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"globals": "^15.0.0",
|
"eslint-plugin-react-hooks": "7.1.1",
|
||||||
"sass": "^1.77.0",
|
"globals": "17.5.0",
|
||||||
"vite": "^8.0.8",
|
"sass": "^1.99.0",
|
||||||
"vite-plugin-symfony": "^8.2.4"
|
"vite": "^8.0.8",
|
||||||
},
|
"vite-plugin-symfony": "^8.2.4"
|
||||||
"scripts": {
|
},
|
||||||
"dev": "vite",
|
"scripts": {
|
||||||
"watch": "vite build --watch",
|
"dev": "vite",
|
||||||
"build": "vite build && cp -r node_modules/@fortawesome/fontawesome-free/webfonts public/build/webfonts",
|
"watch": "vite build --watch",
|
||||||
"lint": "eslint assets/js/"
|
"build": "vite build",
|
||||||
}
|
"lint": "eslint assets/js/"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
public/images/another-games/apple-minesweeper.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
public/images/another-games/gnome-mines.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
public/images/another-games/microsoft-minesweeper.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
public/images/privileges/battle.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
public/images/privileges/history.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
public/images/privileges/security.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
public/images/privileges/shared-battle.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
public/images/privileges/stat.png
Normal file
|
After Width: | Height: | Size: 27 KiB |