Compare commits

..

14 Commits

Author SHA1 Message Date
Hewston Fox
5747e3611d feat: add profile counters
All checks were successful
Deploy to VPS (dist) / deploy (push) Successful in 1m37s
2026-03-23 02:36:07 +02:00
Hewston Fox
8a11ee4c29 fix: add input scroll into view
All checks were successful
Deploy to VPS (dist) / deploy (push) Successful in 1m31s
2026-03-22 17:48:35 +02:00
Hewston Fox
a9ed3dea5f fix: svg props
All checks were successful
Deploy to VPS (dist) / deploy (push) Successful in 1m35s
2026-03-22 17:38:46 +02:00
Hewston Fox
5c08238dbd feat: use howler
Some checks failed
Deploy to VPS (dist) / deploy (push) Has been cancelled
2026-03-22 17:37:19 +02:00
Hewston Fox
e5b3b97680 fix: hide navigation on keyboard visible
All checks were successful
Deploy to VPS (dist) / deploy (push) Successful in 1m32s
2026-03-22 17:21:14 +02:00
Hewston Fox
31d7eaf6ac fix: hide navigation on keyboard visible
All checks were successful
Deploy to VPS (dist) / deploy (push) Successful in 1m32s
2026-03-22 16:50:09 +02:00
Hewston Fox
9bb32c1756 fix: hide navigation on keyboard visible
Some checks failed
Deploy to VPS (dist) / deploy (push) Has been cancelled
2026-03-22 16:49:48 +02:00
Hewston Fox
de711a407f fix: hide navigation on keyboard visible
All checks were successful
Deploy to VPS (dist) / deploy (push) Successful in 1m35s
2026-03-22 16:42:22 +02:00
Hewston Fox
082b9bb714 fix: remove switch animation
All checks were successful
Deploy to VPS (dist) / deploy (push) Successful in 1m41s
2026-03-22 16:39:48 +02:00
Hewston Fox
3c646bc5ac fix: rizna huyeta
All checks were successful
Deploy to VPS (dist) / deploy (push) Successful in 1m40s
2026-03-22 16:26:38 +02:00
Hewston Fox
2c072f1474 fix: lang back button styles
All checks were successful
Deploy to VPS (dist) / deploy (push) Successful in 1m37s
2026-03-22 13:36:22 +02:00
Hewston Fox
9e035d5007 feat: add header padding
All checks were successful
Deploy to VPS (dist) / deploy (push) Successful in 1m37s
2026-03-22 13:32:04 +02:00
Hewston Fox
2cc51e62f4 fix: overflow
All checks were successful
Deploy to VPS (dist) / deploy (push) Successful in 1m40s
2026-03-22 13:29:01 +02:00
Hewston Fox
e3088b7c47 fix: profile lifting
All checks were successful
Deploy to VPS (dist) / deploy (push) Successful in 1m39s
2026-03-22 13:25:49 +02:00
45 changed files with 703 additions and 326 deletions

View File

@@ -27,6 +27,7 @@
"axios": "^1.13.6", "axios": "^1.13.6",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"eruda": "^3.4.3", "eruda": "^3.4.3",
"howler": "^2.2.4",
"i18next": "^25.8.17", "i18next": "^25.8.17",
"i18next-http-backend": "^3.0.2", "i18next-http-backend": "^3.0.2",
"motion": "^12.35.1", "motion": "^12.35.1",
@@ -34,10 +35,12 @@
"react-dom": "^19.2.4", "react-dom": "^19.2.4",
"react-i18next": "^16.5.6", "react-i18next": "^16.5.6",
"tailwindcss": "^4.2.1", "tailwindcss": "^4.2.1",
"xstate": "^5.28.0" "xstate": "^5.28.0",
"zustand": "^5.0.12"
}, },
"devDependencies": { "devDependencies": {
"@tanstack/router-plugin": "^1.166.3", "@tanstack/router-plugin": "^1.166.3",
"@types/howler": "^2.2.12",
"@types/node": "^24.10.1", "@types/node": "^24.10.1",
"@types/react": "^19.2.7", "@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",

115
pnpm-lock.yaml generated
View File

@@ -10,10 +10,10 @@ importers:
dependencies: dependencies:
'@tailwindcss/vite': '@tailwindcss/vite':
specifier: ^4.2.1 specifier: ^4.2.1
version: 4.2.1(vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(tsx@4.21.0)(yaml@2.8.2)) version: 4.2.1(vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2))
'@tanstack/devtools-vite': '@tanstack/devtools-vite':
specifier: ^0.5.3 specifier: ^0.5.3
version: 0.5.3(vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(tsx@4.21.0)(yaml@2.8.2)) version: 0.5.3(vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2))
'@tanstack/react-devtools': '@tanstack/react-devtools':
specifier: ^0.9.10 specifier: ^0.9.10
version: 0.9.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(solid-js@1.9.11) version: 0.9.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(solid-js@1.9.11)
@@ -47,6 +47,9 @@ importers:
eruda: eruda:
specifier: ^3.4.3 specifier: ^3.4.3
version: 3.4.3 version: 3.4.3
howler:
specifier: ^2.2.4
version: 2.2.4
i18next: i18next:
specifier: ^25.8.17 specifier: ^25.8.17
version: 25.8.17(typescript@5.9.3) version: 25.8.17(typescript@5.9.3)
@@ -71,10 +74,16 @@ importers:
xstate: xstate:
specifier: ^5.28.0 specifier: ^5.28.0
version: 5.28.0 version: 5.28.0
zustand:
specifier: ^5.0.12
version: 5.0.12(@types/react@19.2.14)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))
devDependencies: devDependencies:
'@tanstack/router-plugin': '@tanstack/router-plugin':
specifier: ^1.166.3 specifier: ^1.166.3
version: 1.166.3(@tanstack/react-router@1.166.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(tsx@4.21.0)(yaml@2.8.2)) version: 1.166.3(@tanstack/react-router@1.166.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2))
'@types/howler':
specifier: ^2.2.12
version: 2.2.12
'@types/node': '@types/node':
specifier: ^24.10.1 specifier: ^24.10.1
version: 24.12.0 version: 24.12.0
@@ -86,7 +95,7 @@ importers:
version: 19.2.3(@types/react@19.2.14) version: 19.2.3(@types/react@19.2.14)
'@vitejs/plugin-react-swc': '@vitejs/plugin-react-swc':
specifier: ^4.2.3 specifier: ^4.2.3
version: 4.2.3(vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(tsx@4.21.0)(yaml@2.8.2)) version: 4.2.3(vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2))
globals: globals:
specifier: ^17.4.0 specifier: ^17.4.0
version: 17.4.0 version: 17.4.0
@@ -110,7 +119,7 @@ importers:
version: 5.9.3 version: 5.9.3
vite: vite:
specifier: ^7.3.1 specifier: ^7.3.1
version: 7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(tsx@4.21.0)(yaml@2.8.2) version: 7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2)
packages: packages:
@@ -376,6 +385,9 @@ packages:
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
engines: {node: '>=6.0.0'} engines: {node: '>=6.0.0'}
'@jridgewell/source-map@0.3.11':
resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==}
'@jridgewell/sourcemap-codec@1.5.5': '@jridgewell/sourcemap-codec@1.5.5':
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
@@ -1223,6 +1235,9 @@ packages:
'@types/estree@1.0.8': '@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
'@types/howler@2.2.12':
resolution: {integrity: sha512-hy769UICzOSdK0Kn1FBk4gN+lswcj1EKRkmiDtMkUGvFfYJzgaDXmVXkSShS2m89ERAatGIPnTUlp2HhfkVo5g==}
'@types/node@24.12.0': '@types/node@24.12.0':
resolution: {integrity: sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==} resolution: {integrity: sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==}
@@ -1314,6 +1329,9 @@ packages:
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true hasBin: true
buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
call-bind-apply-helpers@1.0.2: call-bind-apply-helpers@1.0.2:
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -1359,6 +1377,9 @@ packages:
resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==}
engines: {node: '>=20'} engines: {node: '>=20'}
commander@2.20.3:
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
convert-source-map@2.0.0: convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
@@ -1558,6 +1579,9 @@ packages:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
howler@2.2.4:
resolution: {integrity: sha512-iARIBPgcQrwtEr+tALF+rapJ8qSc+Set2GJQl7xT1MQzWaVkFebdJhR3alVlSiUf5U7nAANKuj3aWpwerocD5w==}
html-parse-stringify@3.0.1: html-parse-stringify@3.0.1:
resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==}
@@ -2049,6 +2073,9 @@ packages:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
source-map-support@0.5.21:
resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
source-map@0.6.1: source-map@0.6.1:
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -2092,6 +2119,11 @@ packages:
resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==}
engines: {node: '>=6'} engines: {node: '>=6'}
terser@5.46.1:
resolution: {integrity: sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==}
engines: {node: '>=10'}
hasBin: true
tiny-invariant@1.3.3: tiny-invariant@1.3.3:
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
@@ -2251,6 +2283,24 @@ packages:
zod@3.25.76: zod@3.25.76:
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
zustand@5.0.12:
resolution: {integrity: sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==}
engines: {node: '>=12.20.0'}
peerDependencies:
'@types/react': '>=18.0.0'
immer: '>=9.0.6'
react: '>=18.0.0'
use-sync-external-store: '>=1.2.0'
peerDependenciesMeta:
'@types/react':
optional: true
immer:
optional: true
react:
optional: true
use-sync-external-store:
optional: true
snapshots: snapshots:
'@ark/schema@0.56.0': '@ark/schema@0.56.0':
@@ -2466,6 +2516,12 @@ snapshots:
'@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/resolve-uri@3.1.2': {}
'@jridgewell/source-map@0.3.11':
dependencies:
'@jridgewell/gen-mapping': 0.3.13
'@jridgewell/trace-mapping': 0.3.31
optional: true
'@jridgewell/sourcemap-codec@1.5.5': {} '@jridgewell/sourcemap-codec@1.5.5': {}
'@jridgewell/trace-mapping@0.3.31': '@jridgewell/trace-mapping@0.3.31':
@@ -2872,12 +2928,12 @@ snapshots:
'@tailwindcss/oxide-win32-arm64-msvc': 4.2.1 '@tailwindcss/oxide-win32-arm64-msvc': 4.2.1
'@tailwindcss/oxide-win32-x64-msvc': 4.2.1 '@tailwindcss/oxide-win32-x64-msvc': 4.2.1
'@tailwindcss/vite@4.2.1(vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(tsx@4.21.0)(yaml@2.8.2))': '@tailwindcss/vite@4.2.1(vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2))':
dependencies: dependencies:
'@tailwindcss/node': 4.2.1 '@tailwindcss/node': 4.2.1
'@tailwindcss/oxide': 4.2.1 '@tailwindcss/oxide': 4.2.1
tailwindcss: 4.2.1 tailwindcss: 4.2.1
vite: 7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(tsx@4.21.0)(yaml@2.8.2) vite: 7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2)
'@tanstack/devtools-client@0.0.6': '@tanstack/devtools-client@0.0.6':
dependencies: dependencies:
@@ -2901,7 +2957,7 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- csstype - csstype
'@tanstack/devtools-vite@0.5.3(vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(tsx@4.21.0)(yaml@2.8.2))': '@tanstack/devtools-vite@0.5.3(vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2))':
dependencies: dependencies:
'@babel/core': 7.29.0 '@babel/core': 7.29.0
'@babel/generator': 7.29.1 '@babel/generator': 7.29.1
@@ -2913,7 +2969,7 @@ snapshots:
chalk: 5.6.2 chalk: 5.6.2
launch-editor: 2.13.1 launch-editor: 2.13.1
picomatch: 4.0.3 picomatch: 4.0.3
vite: 7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(tsx@4.21.0)(yaml@2.8.2) vite: 7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2)
transitivePeerDependencies: transitivePeerDependencies:
- bufferutil - bufferutil
- supports-color - supports-color
@@ -3026,7 +3082,7 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@tanstack/router-plugin@1.166.3(@tanstack/react-router@1.166.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(tsx@4.21.0)(yaml@2.8.2))': '@tanstack/router-plugin@1.166.3(@tanstack/react-router@1.166.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2))':
dependencies: dependencies:
'@babel/core': 7.29.0 '@babel/core': 7.29.0
'@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0)
@@ -3043,7 +3099,7 @@ snapshots:
zod: 3.25.76 zod: 3.25.76
optionalDependencies: optionalDependencies:
'@tanstack/react-router': 1.166.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@tanstack/react-router': 1.166.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
vite: 7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(tsx@4.21.0)(yaml@2.8.2) vite: 7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -3122,6 +3178,8 @@ snapshots:
'@types/estree@1.0.8': {} '@types/estree@1.0.8': {}
'@types/howler@2.2.12': {}
'@types/node@24.12.0': '@types/node@24.12.0':
dependencies: dependencies:
undici-types: 7.16.0 undici-types: 7.16.0
@@ -3134,11 +3192,11 @@ snapshots:
dependencies: dependencies:
csstype: 3.2.3 csstype: 3.2.3
'@vitejs/plugin-react-swc@4.2.3(vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(tsx@4.21.0)(yaml@2.8.2))': '@vitejs/plugin-react-swc@4.2.3(vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2))':
dependencies: dependencies:
'@rolldown/pluginutils': 1.0.0-rc.2 '@rolldown/pluginutils': 1.0.0-rc.2
'@swc/core': 1.15.18 '@swc/core': 1.15.18
vite: 7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(tsx@4.21.0)(yaml@2.8.2) vite: 7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2)
transitivePeerDependencies: transitivePeerDependencies:
- '@swc/helpers' - '@swc/helpers'
@@ -3222,6 +3280,9 @@ snapshots:
node-releases: 2.0.36 node-releases: 2.0.36
update-browserslist-db: 1.2.3(browserslist@4.28.1) update-browserslist-db: 1.2.3(browserslist@4.28.1)
buffer-from@1.1.2:
optional: true
call-bind-apply-helpers@1.0.2: call-bind-apply-helpers@1.0.2:
dependencies: dependencies:
es-errors: 1.3.0 es-errors: 1.3.0
@@ -3270,6 +3331,9 @@ snapshots:
commander@14.0.3: {} commander@14.0.3: {}
commander@2.20.3:
optional: true
convert-source-map@2.0.0: {} convert-source-map@2.0.0: {}
cookie-es@2.0.0: {} cookie-es@2.0.0: {}
@@ -3452,6 +3516,8 @@ snapshots:
dependencies: dependencies:
function-bind: 1.1.2 function-bind: 1.1.2
howler@2.2.4: {}
html-parse-stringify@3.0.1: html-parse-stringify@3.0.1:
dependencies: dependencies:
void-elements: 3.1.0 void-elements: 3.1.0
@@ -3908,6 +3974,12 @@ snapshots:
source-map-js@1.2.1: {} source-map-js@1.2.1: {}
source-map-support@0.5.21:
dependencies:
buffer-from: 1.1.2
source-map: 0.6.1
optional: true
source-map@0.6.1: {} source-map@0.6.1: {}
source-map@0.7.6: {} source-map@0.7.6: {}
@@ -3946,6 +4018,14 @@ snapshots:
tapable@2.3.0: {} tapable@2.3.0: {}
terser@5.46.1:
dependencies:
'@jridgewell/source-map': 0.3.11
acorn: 8.16.0
commander: 2.20.3
source-map-support: 0.5.21
optional: true
tiny-invariant@1.3.3: {} tiny-invariant@1.3.3: {}
tiny-warning@1.0.3: {} tiny-warning@1.0.3: {}
@@ -4008,7 +4088,7 @@ snapshots:
varint@6.0.0: varint@6.0.0:
optional: true optional: true
vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(tsx@4.21.0)(yaml@2.8.2): vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2):
dependencies: dependencies:
esbuild: 0.27.3 esbuild: 0.27.3
fdir: 6.5.0(picomatch@4.0.3) fdir: 6.5.0(picomatch@4.0.3)
@@ -4023,6 +4103,7 @@ snapshots:
lightningcss: 1.31.1 lightningcss: 1.31.1
sass: 1.97.3 sass: 1.97.3
sass-embedded: 1.97.3 sass-embedded: 1.97.3
terser: 5.46.1
tsx: 4.21.0 tsx: 4.21.0
yaml: 2.8.2 yaml: 2.8.2
@@ -4052,3 +4133,9 @@ snapshots:
yaml@2.8.2: {} yaml@2.8.2: {}
zod@3.25.76: {} zod@3.25.76: {}
zustand@5.0.12(@types/react@19.2.14)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)):
optionalDependencies:
'@types/react': 19.2.14
react: 19.2.4
use-sync-external-store: 1.6.0(react@19.2.4)

View File

@@ -12,6 +12,11 @@
"off": "aus", "off": "aus",
"on": "an" "on": "an"
}, },
"faq": {
"1a": "Antwort",
"1q": "Frage",
"title": "FAQ"
},
"nav": { "nav": {
"apiary": "Bienenhaus", "apiary": "Bienenhaus",
"cashdesk": "Kasse", "cashdesk": "Kasse",
@@ -31,10 +36,6 @@
"pagination": { "pagination": {
"of": "von" "of": "von"
}, },
"support": {
"action": "Support kontaktieren",
"text": "Wenn Sie Fragen zum Spiel haben, wenden Sie sich bitte an unseren Support."
},
"settings": { "settings": {
"accountInfo": "Kontoinformationen", "accountInfo": "Kontoinformationen",
"back": "Zurück", "back": "Zurück",
@@ -44,6 +45,11 @@
"support": "Support", "support": "Support",
"transactionHistory": "Transaktionsverlauf" "transactionHistory": "Transaktionsverlauf"
}, },
"support": {
"action": "Support kontaktieren",
"text": "Wenn Sie Fragen zum Spiel haben, wenden Sie sich bitte an unseren Support.",
"title": "Support"
},
"transactionHistory": { "transactionHistory": {
"date": "Datum", "date": "Datum",
"operationType": "Vorgangsart", "operationType": "Vorgangsart",

View File

@@ -12,6 +12,11 @@
"off": "off", "off": "off",
"on": "on" "on": "on"
}, },
"faq": {
"1a": "Answer",
"1q": "Question",
"title": "FAQ"
},
"nav": { "nav": {
"apiary": "Apiary", "apiary": "Apiary",
"cashdesk": "Cashdesk", "cashdesk": "Cashdesk",
@@ -31,10 +36,6 @@
"pagination": { "pagination": {
"of": "of" "of": "of"
}, },
"support": {
"action": "Contact support",
"text": "If you have any questions related to the game, please contact our support team."
},
"settings": { "settings": {
"accountInfo": "Account information", "accountInfo": "Account information",
"back": "Back", "back": "Back",
@@ -44,6 +45,11 @@
"support": "Support", "support": "Support",
"transactionHistory": "Transaction History" "transactionHistory": "Transaction History"
}, },
"support": {
"action": "Contact support",
"text": "If you have any questions related to the game, please contact our support team.",
"title": "Support"
},
"transactionHistory": { "transactionHistory": {
"date": "Date", "date": "Date",
"operationType": "Operation type", "operationType": "Operation type",

View File

@@ -12,6 +12,11 @@
"off": "no", "off": "no",
"on": "sí" "on": "sí"
}, },
"faq": {
"1a": "Respuesta",
"1q": "Pregunta",
"title": "FAQ"
},
"nav": { "nav": {
"apiary": "Apiario", "apiary": "Apiario",
"cashdesk": "Caja", "cashdesk": "Caja",
@@ -31,10 +36,6 @@
"pagination": { "pagination": {
"of": "de" "of": "de"
}, },
"support": {
"action": "Contactar soporte",
"text": "Si tienes alguna pregunta relacionada con el juego, contacta con nuestro equipo de soporte."
},
"settings": { "settings": {
"accountInfo": "Información de la cuenta", "accountInfo": "Información de la cuenta",
"back": "Volver", "back": "Volver",
@@ -44,6 +45,11 @@
"support": "Soporte", "support": "Soporte",
"transactionHistory": "Historial de transacciones" "transactionHistory": "Historial de transacciones"
}, },
"support": {
"action": "Contactar soporte",
"text": "Si tienes alguna pregunta relacionada con el juego, contacta con nuestro equipo de soporte.",
"title": "Soporte"
},
"transactionHistory": { "transactionHistory": {
"date": "Fecha", "date": "Fecha",
"operationType": "Tipo de operación", "operationType": "Tipo de operación",

View File

@@ -12,6 +12,11 @@
"off": "non", "off": "non",
"on": "oui" "on": "oui"
}, },
"faq": {
"1a": "Réponse",
"1q": "Question",
"title": "FAQ"
},
"nav": { "nav": {
"apiary": "Rucher", "apiary": "Rucher",
"cashdesk": "Caisse", "cashdesk": "Caisse",
@@ -31,10 +36,6 @@
"pagination": { "pagination": {
"of": "sur" "of": "sur"
}, },
"support": {
"action": "Contacter le support",
"text": "Si vous avez des questions liées au jeu, veuillez contacter notre équipe de support."
},
"settings": { "settings": {
"accountInfo": "Informations du compte", "accountInfo": "Informations du compte",
"back": "Retour", "back": "Retour",
@@ -44,6 +45,11 @@
"support": "Support", "support": "Support",
"transactionHistory": "Historique des transactions" "transactionHistory": "Historique des transactions"
}, },
"support": {
"action": "Contacter le support",
"text": "Si vous avez des questions liées au jeu, veuillez contacter notre équipe de support.",
"title": "Support"
},
"transactionHistory": { "transactionHistory": {
"date": "Date", "date": "Date",
"operationType": "Type d'opération", "operationType": "Type d'opération",

View File

@@ -12,6 +12,11 @@
"off": "mati", "off": "mati",
"on": "nyala" "on": "nyala"
}, },
"faq": {
"1a": "Jawaban",
"1q": "Pertanyaan",
"title": "FAQ"
},
"nav": { "nav": {
"apiary": "Peternakan Lebah", "apiary": "Peternakan Lebah",
"cashdesk": "Kasir", "cashdesk": "Kasir",
@@ -31,10 +36,6 @@
"pagination": { "pagination": {
"of": "dari" "of": "dari"
}, },
"support": {
"action": "Hubungi dukungan",
"text": "Jika Anda memiliki pertanyaan terkait permainan, silakan hubungi tim dukungan kami."
},
"settings": { "settings": {
"accountInfo": "Informasi akun", "accountInfo": "Informasi akun",
"back": "Kembali", "back": "Kembali",
@@ -44,6 +45,11 @@
"support": "Dukungan", "support": "Dukungan",
"transactionHistory": "Riwayat transaksi" "transactionHistory": "Riwayat transaksi"
}, },
"support": {
"action": "Hubungi dukungan",
"text": "Jika Anda memiliki pertanyaan terkait permainan, silakan hubungi tim dukungan kami.",
"title": "Dukungan"
},
"transactionHistory": { "transactionHistory": {
"date": "Tanggal", "date": "Tanggal",
"operationType": "Jenis operasi", "operationType": "Jenis operasi",

View File

@@ -12,6 +12,11 @@
"off": "no", "off": "no",
"on": "sì" "on": "sì"
}, },
"faq": {
"1a": "Risposta",
"1q": "Domanda",
"title": "FAQ"
},
"nav": { "nav": {
"apiary": "Apiario", "apiary": "Apiario",
"cashdesk": "Cassa", "cashdesk": "Cassa",
@@ -31,10 +36,6 @@
"pagination": { "pagination": {
"of": "di" "of": "di"
}, },
"support": {
"action": "Contatta il supporto",
"text": "Se hai domande relative al gioco, contatta il nostro team di supporto."
},
"settings": { "settings": {
"accountInfo": "Informazioni account", "accountInfo": "Informazioni account",
"back": "Indietro", "back": "Indietro",
@@ -44,6 +45,11 @@
"support": "Supporto", "support": "Supporto",
"transactionHistory": "Cronologia transazioni" "transactionHistory": "Cronologia transazioni"
}, },
"support": {
"action": "Contatta il supporto",
"text": "Se hai domande relative al gioco, contatta il nostro team di supporto.",
"title": "Supporto"
},
"transactionHistory": { "transactionHistory": {
"date": "Data", "date": "Data",
"operationType": "Tipo di operazione", "operationType": "Tipo di operazione",

View File

@@ -12,6 +12,11 @@
"off": "uit", "off": "uit",
"on": "aan" "on": "aan"
}, },
"faq": {
"1a": "Antwoord",
"1q": "Vraag",
"title": "FAQ"
},
"nav": { "nav": {
"apiary": "Bijenstal", "apiary": "Bijenstal",
"cashdesk": "Kassa", "cashdesk": "Kassa",
@@ -31,10 +36,6 @@
"pagination": { "pagination": {
"of": "van" "of": "van"
}, },
"support": {
"action": "Contact opnemen",
"text": "Als u vragen heeft over het spel, neem dan contact op met ons ondersteuningsteam."
},
"settings": { "settings": {
"accountInfo": "Accountinformatie", "accountInfo": "Accountinformatie",
"back": "Terug", "back": "Terug",
@@ -44,6 +45,11 @@
"support": "Ondersteuning", "support": "Ondersteuning",
"transactionHistory": "Transactiegeschiedenis" "transactionHistory": "Transactiegeschiedenis"
}, },
"support": {
"action": "Contact opnemen",
"text": "Als u vragen heeft over het spel, neem dan contact op met ons ondersteuningsteam.",
"title": "Ondersteuning"
},
"transactionHistory": { "transactionHistory": {
"date": "Datum", "date": "Datum",
"operationType": "Bewerkingstype", "operationType": "Bewerkingstype",

View File

@@ -12,6 +12,11 @@
"off": "wył", "off": "wył",
"on": "wł" "on": "wł"
}, },
"faq": {
"1a": "Odpowiedź",
"1q": "Pytanie",
"title": "FAQ"
},
"nav": { "nav": {
"apiary": "Pasieka", "apiary": "Pasieka",
"cashdesk": "Kasa", "cashdesk": "Kasa",
@@ -31,10 +36,6 @@
"pagination": { "pagination": {
"of": "z" "of": "z"
}, },
"support": {
"action": "Skontaktuj się z pomocą",
"text": "Jeśli masz pytania dotyczące gry, skontaktuj się z naszym zespołem wsparcia."
},
"settings": { "settings": {
"accountInfo": "Informacje o koncie", "accountInfo": "Informacje o koncie",
"back": "Wstecz", "back": "Wstecz",
@@ -44,6 +45,11 @@
"support": "Wsparcie", "support": "Wsparcie",
"transactionHistory": "Historia transakcji" "transactionHistory": "Historia transakcji"
}, },
"support": {
"action": "Skontaktuj się z pomocą",
"text": "Jeśli masz pytania dotyczące gry, skontaktuj się z naszym zespołem wsparcia.",
"title": "Wsparcie"
},
"transactionHistory": { "transactionHistory": {
"date": "Data", "date": "Data",
"operationType": "Typ operacji", "operationType": "Typ operacji",

View File

@@ -12,6 +12,11 @@
"off": "não", "off": "não",
"on": "sim" "on": "sim"
}, },
"faq": {
"1a": "Resposta",
"1q": "Pergunta",
"title": "FAQ"
},
"nav": { "nav": {
"apiary": "Apiário", "apiary": "Apiário",
"cashdesk": "Caixa", "cashdesk": "Caixa",
@@ -31,10 +36,6 @@
"pagination": { "pagination": {
"of": "de" "of": "de"
}, },
"support": {
"action": "Contactar suporte",
"text": "Se tiver dúvidas relacionadas ao jogo, entre em contato com nossa equipe de suporte."
},
"settings": { "settings": {
"accountInfo": "Informações da conta", "accountInfo": "Informações da conta",
"back": "Voltar", "back": "Voltar",
@@ -44,6 +45,11 @@
"support": "Suporte", "support": "Suporte",
"transactionHistory": "Histórico de transações" "transactionHistory": "Histórico de transações"
}, },
"support": {
"action": "Contactar suporte",
"text": "Se tiver dúvidas relacionadas ao jogo, entre em contato com nossa equipe de suporte.",
"title": "Suporte"
},
"transactionHistory": { "transactionHistory": {
"date": "Data", "date": "Data",
"operationType": "Tipo de operação", "operationType": "Tipo de operação",

View File

@@ -12,6 +12,11 @@
"off": "выкл", "off": "выкл",
"on": "вкл" "on": "вкл"
}, },
"faq": {
"1a": "Ответ",
"1q": "Вопрос",
"title": "ЧаВо"
},
"nav": { "nav": {
"apiary": "Пасека", "apiary": "Пасека",
"cashdesk": "Касса", "cashdesk": "Касса",
@@ -31,10 +36,6 @@
"pagination": { "pagination": {
"of": "из" "of": "из"
}, },
"support": {
"action": "Связаться с поддержкой",
"text": "Если у вас возникли вопросы, связанные с игрой — обратитесь в нашу службу поддержки."
},
"settings": { "settings": {
"accountInfo": "Информация об аккаунте", "accountInfo": "Информация об аккаунте",
"back": "Назад", "back": "Назад",
@@ -44,6 +45,11 @@
"support": "Поддержка", "support": "Поддержка",
"transactionHistory": "История транзакций" "transactionHistory": "История транзакций"
}, },
"support": {
"action": "Связаться с поддержкой",
"text": "Если у вас возникли вопросы, связанные с игрой — обратитесь в нашу службу поддержки.",
"title": "Поддержка"
},
"transactionHistory": { "transactionHistory": {
"date": "Дата", "date": "Дата",
"operationType": "Тип операции", "operationType": "Тип операции",

View File

@@ -12,6 +12,11 @@
"off": "kapalı", "off": "kapalı",
"on": "açık" "on": "açık"
}, },
"faq": {
"1a": "Cevap",
"1q": "Soru",
"title": "SSS"
},
"nav": { "nav": {
"apiary": "Arılık", "apiary": "Arılık",
"cashdesk": "Kasa", "cashdesk": "Kasa",
@@ -31,10 +36,6 @@
"pagination": { "pagination": {
"of": "/" "of": "/"
}, },
"support": {
"action": "Destek ile iletişime geç",
"text": "Oyunla ilgili sorularınız varsa lütfen destek ekibimizle iletişime geçin."
},
"settings": { "settings": {
"accountInfo": "Hesap bilgileri", "accountInfo": "Hesap bilgileri",
"back": "Geri", "back": "Geri",
@@ -44,6 +45,11 @@
"support": "Destek", "support": "Destek",
"transactionHistory": "İşlem Geçmişi" "transactionHistory": "İşlem Geçmişi"
}, },
"support": {
"action": "Destek ile iletişime geç",
"text": "Oyunla ilgili sorularınız varsa lütfen destek ekibimizle iletişime geçin.",
"title": "Destek"
},
"transactionHistory": { "transactionHistory": {
"date": "Tarih", "date": "Tarih",
"operationType": "İşlem türü", "operationType": "İşlem türü",

View File

@@ -1,110 +0,0 @@
import { type ReactNode, type RefObject, useMemo } from "react";
import { createContext, useCallback, useContext, useEffect, useRef, useState } from "react";
import tg, { STORAGE_KEYS } from "@/tg";
import type { SoundKey } from "./sounds";
import { SOUNDS, preloadSounds } from "./sounds";
type AudioCtxValue = {
isEnabled: boolean;
setIsEnabled: (enabled: boolean) => void;
loopingRef: RefObject<Map<SoundKey, HTMLAudioElement>>;
};
const AudioCtx = createContext<AudioCtxValue | null>(null);
export function useAudioSettings(): {
isEnabled: boolean;
setIsEnabled: (enabled: boolean) => void;
} {
const ctx = useContext(AudioCtx);
if (!ctx) throw new Error("useAudioSettings must be used within AudioProvider");
return { isEnabled: ctx.isEnabled, setIsEnabled: ctx.setIsEnabled };
}
export function usePlaySound(): (
key: SoundKey,
{ mode, force }?: { mode?: "once" | "loop"; force: boolean },
) => void {
const ctx = useContext(AudioCtx);
if (!ctx) throw new Error("usePlaySound must be used within AudioProvider");
const { isEnabled, loopingRef } = ctx;
return useCallback(
(key: SoundKey, { mode, force }: { mode?: "once" | "loop"; force?: boolean } = {}) => {
if (!isEnabled && !force) return;
if (mode === "loop" && loopingRef.current.has(key)) return;
const audio = new Audio(SOUNDS[key]);
if (mode === "loop") {
audio.loop = true;
loopingRef.current.set(key, audio);
} else {
audio.addEventListener(
"ended",
() => {
audio.src = "";
},
{ once: true },
);
}
audio.play().catch(() => {
if (mode === "loop") loopingRef.current.delete(key);
});
},
[isEnabled, loopingRef],
);
}
export function AudioProvider({ children }: { children: ReactNode }) {
const [isEnabled, setIsEnabled] = useState(true);
const hydratedRef = useRef(false);
const loopingRef = useRef<Map<SoundKey, HTMLAudioElement>>(null);
if (!loopingRef.current) {
loopingRef.current = new Map();
preloadSounds();
}
useEffect(() => {
tg.storage.getItem(STORAGE_KEYS.soundEnabled).then((value) => {
if (value !== "") {
setIsEnabled(value !== "false");
}
hydratedRef.current = true;
});
}, []);
useEffect(() => {
if (!hydratedRef.current) return;
tg.storage.setItem(STORAGE_KEYS.soundEnabled, String(isEnabled));
}, [isEnabled]);
useEffect(() => {
if (!isEnabled) {
for (const audio of loopingRef.current!.values()) {
audio.pause();
audio.src = "";
}
loopingRef.current!.clear();
}
}, [isEnabled]);
return (
<AudioCtx
value={useMemo(
() =>
({
isEnabled,
setIsEnabled,
loopingRef,
}) as AudioCtxValue,
[isEnabled],
)}
>
{children}
</AudioCtx>
);
}

View File

@@ -1,3 +1,3 @@
export { AudioProvider, useAudioSettings, usePlaySound } from "./AudioContext"; export { useAudioStore } from "./store";
export { SOUNDS } from "./sounds"; export { SOUNDS } from "./sounds";
export type { SoundKey } from "./sounds"; export type { SoundKey } from "./sounds";

View File

@@ -1,15 +1,17 @@
import { prefetch } from "@/helpers/dom"; import { Howl } from "howler";
import click from "./assets/click.mp3"; import click from "./assets/click.mp3";
export const SOUNDS = { const SOUND_SOURCES = {
click, click,
} as const; } as const;
export type SoundKey = keyof typeof SOUNDS; export type SoundKey = keyof typeof SOUND_SOURCES;
export const SOUNDS: Record<SoundKey, Howl> = {} as Record<SoundKey, Howl>;
export function preloadSounds() { export function preloadSounds() {
for (const src of Object.values(SOUNDS)) { for (const [key, src] of Object.entries(SOUND_SOURCES)) {
prefetch(src); SOUNDS[key as SoundKey] = new Howl({ src: [src], preload: true });
} }
} }

76
src/audio/store.ts Normal file
View File

@@ -0,0 +1,76 @@
import { create } from "zustand";
import tg, { STORAGE_KEYS } from "@/tg";
import type { SoundKey } from "./sounds";
import { SOUNDS, preloadSounds } from "./sounds";
type AudioState = {
isEnabled: boolean;
_hydrated: boolean;
setIsEnabled: (enabled: boolean) => void;
hydrate: () => Promise<void>;
play: (key: SoundKey, opts?: { mode?: "once" | "loop"; force?: boolean }) => void;
stopLoop: (key: SoundKey) => void;
stopAllLoops: () => void;
};
const looping = new Map<SoundKey, number>();
export const useAudioStore = create<AudioState>((set, get) => {
preloadSounds();
return {
isEnabled: true,
_hydrated: false,
setIsEnabled(enabled: boolean) {
set({ isEnabled: enabled });
if (get()._hydrated) {
tg.storage.setItem(STORAGE_KEYS.soundEnabled, String(enabled));
}
if (!enabled) {
get().stopAllLoops();
}
},
async hydrate() {
const value = await tg.storage.getItem(STORAGE_KEYS.soundEnabled);
if (value !== "") {
set({ isEnabled: value !== "false" });
}
set({ _hydrated: true });
},
play(key: SoundKey, opts?: { mode?: "once" | "loop"; force?: boolean }) {
const { isEnabled } = get();
if (!isEnabled && !opts?.force) return;
const howl = SOUNDS[key];
if (!howl) return;
if (opts?.mode === "loop") {
if (looping.has(key)) return;
howl.loop(true);
const id = howl.play();
looping.set(key, id);
} else {
howl.loop(false);
howl.play();
}
},
stopLoop(key: SoundKey) {
const id = looping.get(key);
if (id != null) {
SOUNDS[key]?.stop(id);
looping.delete(key);
}
},
stopAllLoops() {
for (const [key, id] of looping) {
SOUNDS[key]?.stop(id);
}
looping.clear();
},
};
});

View File

@@ -1,7 +1,7 @@
import { motion, type HTMLMotionProps } from "motion/react"; import { motion, type HTMLMotionProps } from "motion/react";
import clsx, { type ClassValue } from "clsx"; import clsx, { type ClassValue } from "clsx";
import { usePlaySound } from "@/audio"; import { useAudioStore } from "@/audio";
import tg from "@/tg"; import tg from "@/tg";
import classes from "./Button.module.css"; import classes from "./Button.module.css";
@@ -19,7 +19,7 @@ const VARIANTS_MAP = {
} satisfies Record<Exclude<Props["variant"], undefined>, string>; } satisfies Record<Exclude<Props["variant"], undefined>, string>;
export default function Button({ className, variant = "blue", onClick, ...props }: Props) { export default function Button({ className, variant = "blue", onClick, ...props }: Props) {
const play = usePlaySound(); const play = useAudioStore((s) => s.play);
return ( return (
<motion.button <motion.button

View File

@@ -10,7 +10,7 @@ import LightSurface from "@components/surface/LightSurface";
import BackIcon from "./icons/BackIcon"; import BackIcon from "./icons/BackIcon";
import StartIcon from "./icons/StartIcon"; import StartIcon from "./icons/StartIcon";
import classes from "./Pagination.module.css"; import classes from "./Pagination.module.css";
import { usePlaySound } from "@/audio"; import { useAudioStore } from "@/audio";
type Props = { type Props = {
value: number; value: number;
@@ -21,7 +21,7 @@ type Props = {
export default function Pagination({ value, total, onChange, variant = "default" }: Props) { export default function Pagination({ value, total, onChange, variant = "default" }: Props) {
const { t } = useTranslation(); const { t } = useTranslation();
const play = usePlaySound(); const play = useAudioStore((s) => s.play);
const isAtStart = value <= 1 || total <= 1; const isAtStart = value <= 1 || total <= 1;
const isAtEnd = value >= total || total <= 1; const isAtEnd = value >= total || total <= 1;

View File

@@ -4,7 +4,7 @@ import DarkSurface from "@components/surface/DarkSurface";
import { motion, type HTMLMotionProps } from "motion/react"; import { motion, type HTMLMotionProps } from "motion/react";
import classes from "./TabSelector.module.css"; import classes from "./TabSelector.module.css";
import { usePlaySound } from "@/audio"; import { useAudioStore } from "@/audio";
import tg from "@/tg"; import tg from "@/tg";
type Tab = { type Tab = {
@@ -20,7 +20,7 @@ type Props = Omit<HTMLMotionProps<"div">, "className" | "onChange"> & {
}; };
export default function TabSelector({ tabs, value, onChange, className, ...props }: Props) { export default function TabSelector({ tabs, value, onChange, className, ...props }: Props) {
const play = usePlaySound(); const play = useAudioStore((s) => s.play);
const selectedIndex = value != null ? tabs.findIndex((tab) => tab.key === value) : -1; const selectedIndex = value != null ? tabs.findIndex((tab) => tab.key === value) : -1;

View File

@@ -1,10 +1,10 @@
import { motion, type HTMLMotionProps } from "motion/react"; import { motion, type HTMLMotionProps } from "motion/react";
import clsx, { type ClassValue } from "clsx"; import clsx, { type ClassValue } from "clsx";
import { type ReactNode, useRef, useState, type ChangeEvent, useId } from "react"; import { type ReactNode, useRef, useState, type ChangeEvent, type FocusEvent, useId } from "react";
import KeyboardIcon from "@components/icons/KeyboardIcon"; import KeyboardIcon from "@components/icons/KeyboardIcon";
import classes from "./NumberInput.module.css"; import classes from "./NumberInput.module.css";
import { usePlaySound } from "@/audio"; import { useAudioStore } from "@/audio";
import tg from "@/tg"; import tg from "@/tg";
type Props = Omit<HTMLMotionProps<"input">, "className" | "type" | "onChange"> & { type Props = Omit<HTMLMotionProps<"input">, "className" | "type" | "onChange"> & {
@@ -29,9 +29,10 @@ export default function NumberInput({
prefix, prefix,
value, value,
onChange, onChange,
onFocus,
...props ...props
}: Props) { }: Props) {
const play = usePlaySound(); const play = useAudioStore((s) => s.play);
const stableId = useId(); const stableId = useId();
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
@@ -44,6 +45,16 @@ export default function NumberInput({
setStrValue(value?.toString() ?? ""); setStrValue(value?.toString() ?? "");
} }
const handleFocus = (e: FocusEvent<HTMLInputElement>) => {
onFocus?.(e);
const target = e.target;
setTimeout(() => {
if (target.isConnected) {
target.scrollIntoView({ block: "center", behavior: "smooth" });
}
}, 100);
};
const handleChange = (e: ChangeEvent<HTMLInputElement>) => { const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const normalized = filterNumericInput(e.target.value); const normalized = filterNumericInput(e.target.value);
setStrValue(normalized); setStrValue(normalized);
@@ -65,6 +76,7 @@ export default function NumberInput({
className={classes.input} className={classes.input}
value={strValue} value={strValue}
onChange={handleChange} onChange={handleChange}
onFocus={handleFocus}
/> />
</label> </label>
<motion.button <motion.button

View File

@@ -45,6 +45,7 @@
position: absolute; position: absolute;
top: 0; top: 0;
height: 100%; height: 100%;
width: 50%;
border-radius: 9999px; border-radius: 9999px;
z-index: 0; z-index: 0;
} }

View File

@@ -6,6 +6,7 @@ import { motion, type HTMLMotionProps } from "motion/react";
import classes from "./SwitchInput.module.css"; import classes from "./SwitchInput.module.css";
import tg from "@/tg"; import tg from "@/tg";
import { useRef } from "react";
type Props = Omit<HTMLMotionProps<"div">, "className" | "onChange"> & { type Props = Omit<HTMLMotionProps<"div">, "className" | "onChange"> & {
value?: boolean | null; value?: boolean | null;
@@ -15,7 +16,8 @@ type Props = Omit<HTMLMotionProps<"div">, "className" | "onChange"> & {
export default function SwitchInput({ value, onChange, className, ...props }: Props) { export default function SwitchInput({ value, onChange, className, ...props }: Props) {
const { t } = useTranslation(); const { t } = useTranslation();
const selectedIndex = value != null ? (value ? 0 : 1) : -1; const selectedIndex = value != null ? (value ? 0 : 1) : 0;
const selectedIndexRef = useRef(selectedIndex);
return ( return (
<ContentSurface <ContentSurface
@@ -45,12 +47,8 @@ export default function SwitchInput({ value, onChange, className, ...props }: Pr
{selectedIndex >= 0 && ( {selectedIndex >= 0 && (
<LightSurface <LightSurface
className={classes.thumb} className={classes.thumb}
initial={{ scale: 0.5 }} initial={{ left: `${selectedIndexRef.current * 50}%` }}
animate={{ animate={{ left: `${selectedIndex * 50}%` }}
left: `${selectedIndex * 50}%`,
width: "50%",
scale: 1,
}}
transition={{ type: "spring", stiffness: 500, damping: 35 }} transition={{ type: "spring", stiffness: 500, damping: 35 }}
/> />
)} )}

View File

@@ -1,5 +1,6 @@
import { motion, type HTMLMotionProps } from "motion/react"; import { motion, type HTMLMotionProps } from "motion/react";
import clsx, { type ClassValue } from "clsx"; import clsx, { type ClassValue } from "clsx";
import type { FocusEvent } from "react";
import classes from "./TextAreaInput.module.css"; import classes from "./TextAreaInput.module.css";
@@ -8,10 +9,21 @@ type Props = Omit<HTMLMotionProps<"textarea">, "className"> & {
error?: boolean; error?: boolean;
}; };
export default function TextAreaInput({ className, error, ...props }: Props) { export default function TextAreaInput({ className, error, onFocus, ...props }: Props) {
const handleFocus = (e: FocusEvent<HTMLTextAreaElement>) => {
onFocus?.(e);
const target = e.target;
setTimeout(() => {
if (target.isConnected) {
target.scrollIntoView({ block: "center", behavior: "smooth" });
}
}, 100);
};
return ( return (
<motion.textarea <motion.textarea
{...props} {...props}
onFocus={handleFocus}
className={clsx(classes.input, error && classes.error, className)} className={clsx(classes.input, error && classes.error, className)}
/> />
); );

View File

@@ -1,5 +1,6 @@
import { motion, type HTMLMotionProps } from "motion/react"; import { motion, type HTMLMotionProps } from "motion/react";
import clsx, { type ClassValue } from "clsx"; import clsx, { type ClassValue } from "clsx";
import type { FocusEvent } from "react";
import classes from "./TextInput.module.css"; import classes from "./TextInput.module.css";
@@ -8,11 +9,22 @@ type Props = Omit<HTMLMotionProps<"input">, "className"> & {
error?: boolean; error?: boolean;
}; };
export default function TextInput({ className, error, ...props }: Props) { export default function TextInput({ className, error, onFocus, ...props }: Props) {
const handleFocus = (e: FocusEvent<HTMLInputElement>) => {
onFocus?.(e);
const target = e.target;
setTimeout(() => {
if (target.isConnected) {
target.scrollIntoView({ block: "center", behavior: "smooth" });
}
}, 100);
};
return ( return (
<motion.input <motion.input
{...props} {...props}
type="text" type="text"
onFocus={handleFocus}
className={clsx(classes.input, error && classes.error, className)} className={clsx(classes.input, error && classes.error, className)}
/> />
); );

View File

@@ -324,8 +324,8 @@ export default function HoneyIcon(props: Props) {
y2="127.651" y2="127.651"
gradientUnits="userSpaceOnUse" gradientUnits="userSpaceOnUse"
> >
<stop stop-color="#FFD844" /> <stop stopColor="#FFD844" />
<stop offset="1" stop-color="#FDB922" /> <stop offset="1" stopColor="#FDB922" />
</linearGradient> </linearGradient>
<linearGradient <linearGradient
id="paint1_linear_227_12316" id="paint1_linear_227_12316"
@@ -335,8 +335,8 @@ export default function HoneyIcon(props: Props) {
y2="110.77" y2="110.77"
gradientUnits="userSpaceOnUse" gradientUnits="userSpaceOnUse"
> >
<stop stop-color="#FFD844" /> <stop stopColor="#FFD844" />
<stop offset="1" stop-color="#FDB922" /> <stop offset="1" stopColor="#FDB922" />
</linearGradient> </linearGradient>
<linearGradient <linearGradient
id="paint2_linear_227_12316" id="paint2_linear_227_12316"
@@ -346,8 +346,8 @@ export default function HoneyIcon(props: Props) {
y2="64.1507" y2="64.1507"
gradientUnits="userSpaceOnUse" gradientUnits="userSpaceOnUse"
> >
<stop stop-color="#FFD844" /> <stop stopColor="#FFD844" />
<stop offset="1" stop-color="#FDB922" /> <stop offset="1" stopColor="#FDB922" />
</linearGradient> </linearGradient>
<linearGradient <linearGradient
id="paint3_linear_227_12316" id="paint3_linear_227_12316"
@@ -357,8 +357,8 @@ export default function HoneyIcon(props: Props) {
y2="154.721" y2="154.721"
gradientUnits="userSpaceOnUse" gradientUnits="userSpaceOnUse"
> >
<stop stop-color="#CB5402" /> <stop stopColor="#CB5402" />
<stop offset="1" stop-color="#F07D02" /> <stop offset="1" stopColor="#F07D02" />
</linearGradient> </linearGradient>
<linearGradient <linearGradient
id="paint4_linear_227_12316" id="paint4_linear_227_12316"
@@ -368,8 +368,8 @@ export default function HoneyIcon(props: Props) {
y2="100.601" y2="100.601"
gradientUnits="userSpaceOnUse" gradientUnits="userSpaceOnUse"
> >
<stop stop-color="#CB5402" /> <stop stopColor="#CB5402" />
<stop offset="1" stop-color="#F07D02" /> <stop offset="1" stopColor="#F07D02" />
</linearGradient> </linearGradient>
<linearGradient <linearGradient
id="paint5_linear_227_12316" id="paint5_linear_227_12316"
@@ -379,8 +379,8 @@ export default function HoneyIcon(props: Props) {
y2="63.6805" y2="63.6805"
gradientUnits="userSpaceOnUse" gradientUnits="userSpaceOnUse"
> >
<stop stop-color="#CB5402" /> <stop stopColor="#CB5402" />
<stop offset="1" stop-color="#F07D02" /> <stop offset="1" stopColor="#F07D02" />
</linearGradient> </linearGradient>
<radialGradient <radialGradient
id="paint6_radial_227_12316" id="paint6_radial_227_12316"
@@ -390,8 +390,8 @@ export default function HoneyIcon(props: Props) {
gradientUnits="userSpaceOnUse" gradientUnits="userSpaceOnUse"
gradientTransform="translate(167.7 150.321) scale(59.48)" gradientTransform="translate(167.7 150.321) scale(59.48)"
> >
<stop stop-color="#F37900" /> <stop stopColor="#F37900" />
<stop offset="1" stop-color="#FFCA13" /> <stop offset="1" stopColor="#FFCA13" />
</radialGradient> </radialGradient>
<radialGradient <radialGradient
id="paint7_radial_227_12316" id="paint7_radial_227_12316"
@@ -401,8 +401,8 @@ export default function HoneyIcon(props: Props) {
gradientUnits="userSpaceOnUse" gradientUnits="userSpaceOnUse"
gradientTransform="translate(89.3202 89.8705) scale(69.51 69.51)" gradientTransform="translate(89.3202 89.8705) scale(69.51 69.51)"
> >
<stop stop-color="#F37900" /> <stop stopColor="#F37900" />
<stop offset="1" stop-color="#FFCA13" /> <stop offset="1" stopColor="#FFCA13" />
</radialGradient> </radialGradient>
<radialGradient <radialGradient
id="paint8_radial_227_12316" id="paint8_radial_227_12316"
@@ -412,8 +412,8 @@ export default function HoneyIcon(props: Props) {
gradientUnits="userSpaceOnUse" gradientUnits="userSpaceOnUse"
gradientTransform="translate(207.66 68.3504) scale(75.09 75.0899)" gradientTransform="translate(207.66 68.3504) scale(75.09 75.0899)"
> >
<stop stop-color="#F37900" /> <stop stopColor="#F37900" />
<stop offset="1" stop-color="#FFCA13" /> <stop offset="1" stopColor="#FFCA13" />
</radialGradient> </radialGradient>
<linearGradient <linearGradient
id="paint9_linear_227_12316" id="paint9_linear_227_12316"
@@ -423,8 +423,8 @@ export default function HoneyIcon(props: Props) {
y2="114.091" y2="114.091"
gradientUnits="userSpaceOnUse" gradientUnits="userSpaceOnUse"
> >
<stop stop-color="#F5AD11" /> <stop stopColor="#F5AD11" />
<stop offset="1" stop-color="#CD7015" /> <stop offset="1" stopColor="#CD7015" />
</linearGradient> </linearGradient>
<linearGradient <linearGradient
id="paint10_linear_227_12316" id="paint10_linear_227_12316"
@@ -434,8 +434,8 @@ export default function HoneyIcon(props: Props) {
y2="165.14" y2="165.14"
gradientUnits="userSpaceOnUse" gradientUnits="userSpaceOnUse"
> >
<stop stop-color="#F5AD11" /> <stop stopColor="#F5AD11" />
<stop offset="1" stop-color="#CD7015" /> <stop offset="1" stopColor="#CD7015" />
</linearGradient> </linearGradient>
<linearGradient <linearGradient
id="paint11_linear_227_12316" id="paint11_linear_227_12316"
@@ -445,8 +445,8 @@ export default function HoneyIcon(props: Props) {
y2="77.0305" y2="77.0305"
gradientUnits="userSpaceOnUse" gradientUnits="userSpaceOnUse"
> >
<stop stop-color="#F5AD11" /> <stop stopColor="#F5AD11" />
<stop offset="1" stop-color="#CD7015" /> <stop offset="1" stopColor="#CD7015" />
</linearGradient> </linearGradient>
<linearGradient <linearGradient
id="paint12_linear_227_12316" id="paint12_linear_227_12316"
@@ -456,8 +456,8 @@ export default function HoneyIcon(props: Props) {
y2="153.36" y2="153.36"
gradientUnits="userSpaceOnUse" gradientUnits="userSpaceOnUse"
> >
<stop stop-color="#CA6617" stop-opacity="0.4" /> <stop stopColor="#CA6617" stopOpacity="0.4" />
<stop offset="1" stop-color="#C36216" stop-opacity="0.6" /> <stop offset="1" stopColor="#C36216" stopOpacity="0.6" />
</linearGradient> </linearGradient>
<linearGradient <linearGradient
id="paint13_linear_227_12316" id="paint13_linear_227_12316"
@@ -467,8 +467,8 @@ export default function HoneyIcon(props: Props) {
y2="116.26" y2="116.26"
gradientUnits="userSpaceOnUse" gradientUnits="userSpaceOnUse"
> >
<stop stop-color="#CA6617" stop-opacity="0.4" /> <stop stopColor="#CA6617" stopOpacity="0.4" />
<stop offset="1" stop-color="#C36216" stop-opacity="0.6" /> <stop offset="1" stopColor="#C36216" stopOpacity="0.6" />
</linearGradient> </linearGradient>
<linearGradient <linearGradient
id="paint14_linear_227_12316" id="paint14_linear_227_12316"
@@ -478,8 +478,8 @@ export default function HoneyIcon(props: Props) {
y2="79.4803" y2="79.4803"
gradientUnits="userSpaceOnUse" gradientUnits="userSpaceOnUse"
> >
<stop offset="0.08" stop-color="#FFFBF3" /> <stop offset="0.08" stopColor="#FFFBF3" />
<stop offset="1" stop-color="#FFFBF3" stop-opacity="0.4" /> <stop offset="1" stopColor="#FFFBF3" stopOpacity="0.4" />
</linearGradient> </linearGradient>
<linearGradient <linearGradient
id="paint15_linear_227_12316" id="paint15_linear_227_12316"
@@ -489,8 +489,8 @@ export default function HoneyIcon(props: Props) {
y2="110.991" y2="110.991"
gradientUnits="userSpaceOnUse" gradientUnits="userSpaceOnUse"
> >
<stop offset="0.08" stop-color="#FFFBF3" /> <stop offset="0.08" stopColor="#FFFBF3" />
<stop offset="1" stop-color="#FFFBF3" stop-opacity="0.4" /> <stop offset="1" stopColor="#FFFBF3" stopOpacity="0.4" />
</linearGradient> </linearGradient>
<linearGradient <linearGradient
id="paint16_linear_227_12316" id="paint16_linear_227_12316"
@@ -500,8 +500,8 @@ export default function HoneyIcon(props: Props) {
y2="52.5508" y2="52.5508"
gradientUnits="userSpaceOnUse" gradientUnits="userSpaceOnUse"
> >
<stop offset="0.08" stop-color="#FFFBF3" /> <stop offset="0.08" stopColor="#FFFBF3" />
<stop offset="1" stop-color="#FFFBF3" stop-opacity="0.4" /> <stop offset="1" stopColor="#FFFBF3" stopOpacity="0.4" />
</linearGradient> </linearGradient>
<linearGradient <linearGradient
id="paint17_linear_227_12316" id="paint17_linear_227_12316"
@@ -511,8 +511,8 @@ export default function HoneyIcon(props: Props) {
y2="23.7907" y2="23.7907"
gradientUnits="userSpaceOnUse" gradientUnits="userSpaceOnUse"
> >
<stop offset="0.08" stop-color="#FFFBF3" /> <stop offset="0.08" stopColor="#FFFBF3" />
<stop offset="1" stop-color="#FFFBF3" stop-opacity="0.4" /> <stop offset="1" stopColor="#FFFBF3" stopOpacity="0.4" />
</linearGradient> </linearGradient>
<linearGradient <linearGradient
id="paint18_linear_227_12316" id="paint18_linear_227_12316"
@@ -522,8 +522,8 @@ export default function HoneyIcon(props: Props) {
y2="33.1406" y2="33.1406"
gradientUnits="userSpaceOnUse" gradientUnits="userSpaceOnUse"
> >
<stop offset="0.08" stop-color="#FFFBF3" /> <stop offset="0.08" stopColor="#FFFBF3" />
<stop offset="1" stop-color="#FFFBF3" stop-opacity="0.4" /> <stop offset="1" stopColor="#FFFBF3" stopOpacity="0.4" />
</linearGradient> </linearGradient>
<linearGradient <linearGradient
id="paint19_linear_227_12316" id="paint19_linear_227_12316"
@@ -533,8 +533,8 @@ export default function HoneyIcon(props: Props) {
y2="70.1908" y2="70.1908"
gradientUnits="userSpaceOnUse" gradientUnits="userSpaceOnUse"
> >
<stop offset="0.08" stop-color="#FFFBF3" /> <stop offset="0.08" stopColor="#FFFBF3" />
<stop offset="1" stop-color="#FFFBF3" stop-opacity="0.4" /> <stop offset="1" stopColor="#FFFBF3" stopOpacity="0.4" />
</linearGradient> </linearGradient>
<linearGradient <linearGradient
id="paint20_linear_227_12316" id="paint20_linear_227_12316"
@@ -544,8 +544,8 @@ export default function HoneyIcon(props: Props) {
y2="75.8007" y2="75.8007"
gradientUnits="userSpaceOnUse" gradientUnits="userSpaceOnUse"
> >
<stop offset="0.08" stop-color="#FFFBF3" /> <stop offset="0.08" stopColor="#FFFBF3" />
<stop offset="1" stop-color="#FFFBF3" stop-opacity="0.4" /> <stop offset="1" stopColor="#FFFBF3" stopOpacity="0.4" />
</linearGradient> </linearGradient>
<linearGradient <linearGradient
id="paint21_linear_227_12316" id="paint21_linear_227_12316"
@@ -555,8 +555,8 @@ export default function HoneyIcon(props: Props) {
y2="70.1004" y2="70.1004"
gradientUnits="userSpaceOnUse" gradientUnits="userSpaceOnUse"
> >
<stop offset="0.08" stop-color="#FFFBF3" /> <stop offset="0.08" stopColor="#FFFBF3" />
<stop offset="1" stop-color="#FFFBF3" stop-opacity="0.4" /> <stop offset="1" stopColor="#FFFBF3" stopOpacity="0.4" />
</linearGradient> </linearGradient>
</defs> </defs>
</motion.svg> </motion.svg>

View File

@@ -1,14 +1,20 @@
import { createContext, useCallback, useContext, useRef, useState } from "react"; import { createContext, useCallback, useContext, useRef, useState } from "react";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { cloneWithoutAnimations } from "./Liftable";
type LiftContextValue = { type LiftContextValue = {
liftedIds: Set<string>; liftedIds: Set<string>;
alwaysLiftedIds: Set<string>; alwaysLiftedIds: Set<string>;
modalOpen: boolean;
setLiftedIds: (ids: string[]) => void; setLiftedIds: (ids: string[]) => void;
registerAlways: (id: string) => void; registerAlways: (id: string) => void;
unregisterAlways: (id: string) => void; unregisterAlways: (id: string) => void;
registerModal: () => void;
beginModalClose: () => void;
endModalClose: () => void;
portalContainer: HTMLElement | null; portalContainer: HTMLElement | null;
setPortalContainer: (el: HTMLElement | null) => void; setPortalContainer: (el: HTMLElement | null) => void;
setCloneContainer: (el: HTMLElement | null) => void;
}; };
const LiftContext = createContext<LiftContextValue | null>(null); const LiftContext = createContext<LiftContextValue | null>(null);
@@ -22,13 +28,41 @@ export function useLift(): LiftContextValue {
export function LiftProvider({ children }: { children: ReactNode }) { export function LiftProvider({ children }: { children: ReactNode }) {
const [liftedIds, setLiftedIdsRaw] = useState<Set<string>>(new Set()); const [liftedIds, setLiftedIdsRaw] = useState<Set<string>>(new Set());
const [alwaysLiftedIds, setAlwaysLiftedIds] = useState<Set<string>>(new Set()); const [alwaysLiftedIds, setAlwaysLiftedIds] = useState<Set<string>>(new Set());
const [modalOpen, setModalOpen] = useState(false);
const [portalContainer, setPortalContainer] = useState<HTMLElement | null>(null); const [portalContainer, setPortalContainer] = useState<HTMLElement | null>(null);
const alwaysRef = useRef<Set<string>>(new Set()); const alwaysRef = useRef<Set<string>>(new Set());
const modalCountRef = useRef(0);
const cloneContainerRef = useRef<HTMLElement | null>(null);
const setLiftedIds = useCallback((ids: string[]) => { const setLiftedIds = useCallback((ids: string[]) => {
setLiftedIdsRaw(new Set(ids)); setLiftedIdsRaw(new Set(ids));
}, []); }, []);
const registerModal = useCallback(() => {
modalCountRef.current++;
setModalOpen(true);
}, []);
const beginModalClose = useCallback(() => {
if (portalContainer && cloneContainerRef.current) {
cloneWithoutAnimations(portalContainer, cloneContainerRef.current);
}
modalCountRef.current--;
if (modalCountRef.current === 0) {
setModalOpen(false);
}
}, [portalContainer]);
const endModalClose = useCallback(() => {
if (cloneContainerRef.current) {
cloneContainerRef.current.innerHTML = "";
}
}, []);
const setCloneContainer = useCallback((el: HTMLElement | null) => {
cloneContainerRef.current = el;
}, []);
const registerAlways = useCallback((id: string) => { const registerAlways = useCallback((id: string) => {
alwaysRef.current.add(id); alwaysRef.current.add(id);
setAlwaysLiftedIds(new Set(alwaysRef.current)); setAlwaysLiftedIds(new Set(alwaysRef.current));
@@ -44,11 +78,16 @@ export function LiftProvider({ children }: { children: ReactNode }) {
value={{ value={{
liftedIds, liftedIds,
alwaysLiftedIds, alwaysLiftedIds,
modalOpen,
setLiftedIds, setLiftedIds,
registerAlways, registerAlways,
unregisterAlways, unregisterAlways,
registerModal,
beginModalClose,
endModalClose,
portalContainer, portalContainer,
setPortalContainer, setPortalContainer,
setCloneContainer,
}} }}
> >
{children} {children}

View File

@@ -3,14 +3,26 @@ import { useLift } from "./LiftContext";
import classes from "./LiftLayer.module.css"; import classes from "./LiftLayer.module.css";
export function LiftLayer() { export function LiftLayer() {
const { setPortalContainer } = useLift(); const { setPortalContainer, setCloneContainer } = useLift();
const refCallback = useCallback( const portalRef = useCallback(
(node: HTMLDivElement | null) => { (node: HTMLDivElement | null) => {
setPortalContainer(node); setPortalContainer(node);
}, },
[setPortalContainer], [setPortalContainer],
); );
return <div ref={refCallback} className={classes.liftLayer} />; const cloneRef = useCallback(
(node: HTMLDivElement | null) => {
setCloneContainer(node);
},
[setCloneContainer],
);
return (
<>
<div ref={portalRef} className={classes.liftLayer} />
<div ref={cloneRef} className={classes.liftLayer} />
</>
);
} }

View File

@@ -1,8 +1,29 @@
import { useEffect, useId, useLayoutEffect, useRef, useState } from "react"; import { useEffect, useId, useLayoutEffect, useRef } from "react";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
import { useLift } from "./LiftContext"; import { useLift } from "./LiftContext";
/** Recursively strip CSS animations and transitions from a cloned DOM tree */
function stripAnimations(node: Node) {
if (node instanceof HTMLElement) {
node.style.animation = "none";
node.style.transition = "none";
node.getAnimations().forEach((a) => a.cancel());
}
for (const child of Array.from(node.childNodes)) {
stripAnimations(child);
}
}
function cloneWithoutAnimations(source: HTMLElement, target: HTMLElement) {
target.innerHTML = "";
for (const child of Array.from(source.children)) {
const clone = child.cloneNode(true) as HTMLElement;
stripAnimations(clone);
target.appendChild(clone);
}
}
type LiftableProps = { type LiftableProps = {
id?: string; id?: string;
always?: boolean; always?: boolean;
@@ -23,7 +44,7 @@ export function useLiftable<T extends Record<string, unknown> = Record<string, n
...extraProps ...extraProps
} = (options ?? {}) as LiftableProps & Record<string, unknown>; } = (options ?? {}) as LiftableProps & Record<string, unknown>;
const id = idProp ?? autoId; const id = idProp ?? autoId;
const { liftedIds, alwaysLiftedIds, registerAlways, unregisterAlways } = useLift(); const { liftedIds, alwaysLiftedIds, modalOpen, registerAlways, unregisterAlways } = useLift();
useEffect(() => { useEffect(() => {
if (always) { if (always) {
@@ -32,7 +53,7 @@ export function useLiftable<T extends Record<string, unknown> = Record<string, n
} }
}, [always, id, registerAlways, unregisterAlways]); }, [always, id, registerAlways, unregisterAlways]);
const isLifted = liftedIds.has(id) || alwaysLiftedIds.has(id); const isLifted = liftedIds.has(id) || (alwaysLiftedIds.has(id) && modalOpen);
const element = ( const element = (
<Liftable id={id}>{render({ isLifted, ...extraProps } as { isLifted: boolean } & T)}</Liftable> <Liftable id={id}>{render({ isLifted, ...extraProps } as { isLifted: boolean } & T)}</Liftable>
@@ -41,13 +62,24 @@ export function useLiftable<T extends Record<string, unknown> = Record<string, n
return { id, element }; return { id, element };
} }
export { cloneWithoutAnimations };
export function Liftable({ id: idProp, always, children }: Props) { export function Liftable({ id: idProp, always, children }: Props) {
const autoId = useId(); const autoId = useId();
const id = idProp ?? autoId; const id = idProp ?? autoId;
const { liftedIds, alwaysLiftedIds, registerAlways, unregisterAlways, portalContainer } = const {
useLift(); liftedIds,
const wrapperRef = useRef<HTMLDivElement>(null); alwaysLiftedIds,
const [rect, setRect] = useState<DOMRect | null>(null); modalOpen,
registerAlways,
unregisterAlways,
portalContainer,
} = useLift();
const childrenHostRef = useRef<HTMLDivElement>(null);
const inlineSlotRef = useRef<HTMLDivElement>(null);
const portalWrapperRef = useRef<HTMLDivElement>(null);
const wasLiftedRef = useRef(false);
useEffect(() => { useEffect(() => {
if (always) { if (always) {
@@ -56,29 +88,43 @@ export function Liftable({ id: idProp, always, children }: Props) {
} }
}, [always, id, registerAlways, unregisterAlways]); }, [always, id, registerAlways, unregisterAlways]);
const isLifted = liftedIds.has(id) || alwaysLiftedIds.has(id); const isLifted = liftedIds.has(id) || (alwaysLiftedIds.has(id) && modalOpen);
useLayoutEffect(() => { useLayoutEffect(() => {
if (isLifted && wrapperRef.current) { const host = childrenHostRef.current;
setRect(wrapperRef.current.getBoundingClientRect()); const inlineSlot = inlineSlotRef.current;
} const portalWrapper = portalWrapperRef.current;
if (!isLifted) { if (!host || !inlineSlot || !portalWrapper) return;
setRect(null);
if (isLifted && !wasLiftedRef.current) {
const rect = inlineSlot.getBoundingClientRect();
cloneWithoutAnimations(host, inlineSlot);
portalWrapper.appendChild(host);
portalWrapper.style.cssText = `position:fixed;top:${rect.top}px;left:${rect.left}px;width:${rect.width}px;height:${rect.height}px;pointer-events:auto`;
wasLiftedRef.current = true;
} else if (!isLifted && wasLiftedRef.current) {
inlineSlot.innerHTML = "";
inlineSlot.appendChild(host);
portalWrapper.style.cssText = "display:none";
wasLiftedRef.current = false;
} }
}, [isLifted]); }, [isLifted]);
// Re-measure on resize while lifted
useEffect(() => { useEffect(() => {
if (!isLifted || !wrapperRef.current) return; if (!isLifted || !inlineSlotRef.current || !portalWrapperRef.current) return;
const portalWrapper = portalWrapperRef.current;
const inlineSlot = inlineSlotRef.current;
const measure = () => { const measure = () => {
if (wrapperRef.current) { const rect = inlineSlot.getBoundingClientRect();
setRect(wrapperRef.current.getBoundingClientRect()); portalWrapper.style.top = `${rect.top}px`;
} portalWrapper.style.left = `${rect.left}px`;
portalWrapper.style.width = `${rect.width}px`;
portalWrapper.style.height = `${rect.height}px`;
}; };
const observer = new ResizeObserver(measure); const observer = new ResizeObserver(measure);
observer.observe(wrapperRef.current); observer.observe(inlineSlot);
window.addEventListener("resize", measure); window.addEventListener("resize", measure);
return () => { return () => {
@@ -87,35 +133,13 @@ export function Liftable({ id: idProp, always, children }: Props) {
}; };
}, [isLifted]); }, [isLifted]);
// When lifted and we have measurements + portal target, render in portal return (
if (isLifted && rect && portalContainer) { <>
return ( <div ref={inlineSlotRef}>
<> <div ref={childrenHostRef}>{children}</div>
{/* Placeholder preserves layout space */} </div>
<div {portalContainer &&
ref={wrapperRef} createPortal(<div ref={portalWrapperRef} style={{ display: "none" }} />, portalContainer)}
style={{ width: rect.width, height: rect.height, visibility: "hidden" }} </>
/> );
{/* Portal children above blur */}
{createPortal(
<div
style={{
position: "fixed",
top: rect.top,
left: rect.left,
width: rect.width,
height: rect.height,
pointerEvents: "auto",
}}
>
{children}
</div>,
portalContainer,
)}
</>
);
}
// Normal inline rendering
return <div ref={wrapperRef}>{children}</div>;
} }

View File

@@ -22,6 +22,9 @@
align-items: center; align-items: center;
width: 100%; width: 100%;
max-width: 320px; max-width: 320px;
max-height: calc(
var(--safe-area-height) - var(--header-total) - var(--navigation-total) - 70px
);
} }
.modal { .modal {
@@ -29,9 +32,6 @@
flex-direction: column; flex-direction: column;
gap: 6px; gap: 6px;
width: 100%; width: 100%;
max-height: calc(
var(--safe-area-height) - var(--header-total) - var(--navigation-total) - 70px
);
overflow: auto; overflow: auto;
padding: 13px; padding: 13px;
} }

View File

@@ -17,9 +17,18 @@ type Props = {
}; };
export default function Modal({ open, children, onClose, liftIds, title, className }: Props) { export default function Modal({ open, children, onClose, liftIds, title, className }: Props) {
const { setLiftedIds } = useLift(); const { setLiftedIds, registerModal, beginModalClose, endModalClose } = useLift();
const prevLiftIdsRef = useRef<string>(""); const prevLiftIdsRef = useRef<string>("");
useEffect(() => {
if (open) {
registerModal();
return () => {
beginModalClose();
};
}
}, [open, registerModal, beginModalClose]);
useEffect(() => { useEffect(() => {
const key = liftIds?.join(",") ?? ""; const key = liftIds?.join(",") ?? "";
@@ -45,7 +54,7 @@ export default function Modal({ open, children, onClose, liftIds, title, classNa
}, [setLiftedIds]); }, [setLiftedIds]);
return ( return (
<AnimatePresence> <AnimatePresence onExitComplete={endModalClose}>
{open && ( {open && (
<> <>
<motion.div <motion.div

View File

@@ -1,8 +1,15 @@
// Auto-generated by i18nextTypesPlugin — do not edit manually // Auto-generated by i18nextTypesPlugin — do not edit manually
declare const resources: { declare const resources: {
"accountInfo.paymentBalance": "Einzahlungsguthaben" | "Payment balance" | "Saldo de pagos" | "Solde des paiements" | "Saldo pembayaran" | "Saldo pagamenti" | "Stortingssaldo" | "Saldo wpłat" | "Saldo de pagamentos" | "Баланс пополнений" | "Ödeme bakiyesi";
"accountInfo.registrationDate": "Registrierungsdatum" | "Registration date" | "Fecha de registro" | "Date d'inscription" | "Tanggal pendaftaran" | "Data di registrazione" | "Registratiedatum" | "Data rejestracji" | "Data de registro" | "Дата регистрации" | "Kayıt tarihi";
"accountInfo.withdrawalBalance": "Auszahlungsguthaben" | "Withdrawal balance" | "Saldo de retiros" | "Solde des retraits" | "Saldo penarikan" | "Saldo prelievi" | "Opnamesaldo" | "Saldo wypłat" | "Saldo de saques" | "Баланс выводов" | "Çekim bakiyesi";
"accountInfo.yourId": "Ihre ID" | "Your ID" | "Tu ID" | "Votre ID" | "ID Anda" | "Il tuo ID" | "Uw ID" | "Twoje ID" | "Seu ID" | "Ваш ID" | "Kimliğiniz";
"actionModal.close": "Schließen" | "Close" | "Cerrar" | "Fermer" | "Tutup" | "Chiudi" | "Sluiten" | "Zamknij" | "Fechar" | "Закрыть" | "Kapat"; "actionModal.close": "Schließen" | "Close" | "Cerrar" | "Fermer" | "Tutup" | "Chiudi" | "Sluiten" | "Zamknij" | "Fechar" | "Закрыть" | "Kapat";
"common.off": "aus" | "off" | "no" | "non" | "mati" | "uit" | "wył" | "não" | "выкл" | "kapalı"; "common.off": "aus" | "off" | "no" | "non" | "mati" | "uit" | "wył" | "não" | "выкл" | "kapalı";
"common.on": "an" | "on" | "sí" | "oui" | "nyala" | "sì" | "aan" | "wł" | "sim" | "вкл" | "açık"; "common.on": "an" | "on" | "sí" | "oui" | "nyala" | "sì" | "aan" | "wł" | "sim" | "вкл" | "açık";
"faq.1a": "Antwort" | "Answer" | "Respuesta" | "Réponse" | "Jawaban" | "Risposta" | "Antwoord" | "Odpowiedź" | "Resposta" | "Ответ" | "Cevap";
"faq.1q": "Frage" | "Question" | "Pregunta" | "Pertanyaan" | "Domanda" | "Vraag" | "Pytanie" | "Pergunta" | "Вопрос" | "Soru";
"faq.title": "FAQ" | "ЧаВо" | "SSS";
"nav.apiary": "Bienenhaus" | "Apiary" | "Apiario" | "Rucher" | "Peternakan Lebah" | "Bijenstal" | "Pasieka" | "Apiário" | "Пасека" | "Arılık"; "nav.apiary": "Bienenhaus" | "Apiary" | "Apiario" | "Rucher" | "Peternakan Lebah" | "Bijenstal" | "Pasieka" | "Apiário" | "Пасека" | "Arılık";
"nav.cashdesk": "Kasse" | "Cashdesk" | "Caja" | "Caisse" | "Kasir" | "Cassa" | "Kassa" | "Kasa" | "Caixa" | "Касса"; "nav.cashdesk": "Kasse" | "Cashdesk" | "Caja" | "Caisse" | "Kasir" | "Cassa" | "Kassa" | "Kasa" | "Caixa" | "Касса";
"nav.earnings": "Einnahmen" | "Earnings" | "Ganancias" | "Gains" | "Penghasilan" | "Guadagni" | "Inkomsten" | "Zarobki" | "Ganhos" | "Заработок" | "Kazançlar"; "nav.earnings": "Einnahmen" | "Earnings" | "Ganancias" | "Gains" | "Penghasilan" | "Guadagni" | "Inkomsten" | "Zarobki" | "Ganhos" | "Заработок" | "Kazançlar";
@@ -11,6 +18,10 @@ declare const resources: {
"nav.roulette": "Roulette" | "Ruleta" | "Ruletka" | "Roleta" | "Рулетка" | "Rulet"; "nav.roulette": "Roulette" | "Ruleta" | "Ruletka" | "Roleta" | "Рулетка" | "Rulet";
"nav.shop": "Shop" | "Tienda" | "Boutique" | "Toko" | "Negozio" | "Winkel" | "Sklep" | "Loja" | "Магазин" | "Mağaza"; "nav.shop": "Shop" | "Tienda" | "Boutique" | "Toko" | "Negozio" | "Winkel" | "Sklep" | "Loja" | "Магазин" | "Mağaza";
"nav.tasks": "Aufgaben" | "Tasks" | "Tareas" | "Tâches" | "Tugas" | "Compiti" | "Taken" | "Zadania" | "Tarefas" | "Задания" | "Görevler"; "nav.tasks": "Aufgaben" | "Tasks" | "Tareas" | "Tâches" | "Tugas" | "Compiti" | "Taken" | "Zadania" | "Tarefas" | "Задания" | "Görevler";
"operationType.deposit": "Einzahlung" | "Deposit" | "Depósito" | "Dépôt" | "Setoran" | "Deposito" | "Storting" | "Wpłata" | "Пополнение" | "Yatırım";
"operationType.greeting": "Willkommensbonus" | "Greeting bonus" | "Bono de bienvenida" | "Bonus de bienvenue" | "Bonus sambutan" | "Bonus di benvenuto" | "Welkomstbonus" | "Bonus powitalny" | "Bônus de boas-vindas" | "Приветственный бонус" | "Hoş geldin bonusu";
"operationType.referral": "Empfehlungsbonus" | "Referral bonus" | "Bono de referido" | "Bonus de parrainage" | "Bonus referral" | "Verwijzingsbonus" | "Bonus polecający" | "Bônus de indicação" | "Реферальный бонус" | "Referans bonusu";
"operationType.withdrawal": "Auszahlung" | "Withdrawal" | "Retiro" | "Retrait" | "Penarikan" | "Prelievo" | "Opname" | "Wypłata" | "Saque" | "Вывод" | "Çekim";
"pagination.of": "von" | "of" | "de" | "sur" | "dari" | "di" | "van" | "z" | "из" | "/"; "pagination.of": "von" | "of" | "de" | "sur" | "dari" | "di" | "van" | "z" | "из" | "/";
"settings.accountInfo": "Kontoinformationen" | "Account information" | "Información de la cuenta" | "Informations du compte" | "Informasi akun" | "Informazioni account" | "Accountinformatie" | "Informacje o koncie" | "Informações da conta" | "Информация об аккаунте" | "Hesap bilgileri"; "settings.accountInfo": "Kontoinformationen" | "Account information" | "Información de la cuenta" | "Informations du compte" | "Informasi akun" | "Informazioni account" | "Accountinformatie" | "Informacje o koncie" | "Informações da conta" | "Информация об аккаунте" | "Hesap bilgileri";
"settings.back": "Zurück" | "Back" | "Volver" | "Retour" | "Kembali" | "Indietro" | "Terug" | "Wstecz" | "Voltar" | "Назад" | "Geri"; "settings.back": "Zurück" | "Back" | "Volver" | "Retour" | "Kembali" | "Indietro" | "Terug" | "Wstecz" | "Voltar" | "Назад" | "Geri";
@@ -19,5 +30,13 @@ declare const resources: {
"settings.sound": "Ton" | "Sound" | "Sonido" | "Son" | "Suara" | "Suono" | "Geluid" | "Dźwięk" | "Som" | "Звук" | "Ses"; "settings.sound": "Ton" | "Sound" | "Sonido" | "Son" | "Suara" | "Suono" | "Geluid" | "Dźwięk" | "Som" | "Звук" | "Ses";
"settings.support": "Support" | "Soporte" | "Dukungan" | "Supporto" | "Ondersteuning" | "Wsparcie" | "Suporte" | "Поддержка" | "Destek"; "settings.support": "Support" | "Soporte" | "Dukungan" | "Supporto" | "Ondersteuning" | "Wsparcie" | "Suporte" | "Поддержка" | "Destek";
"settings.transactionHistory": "Transaktionsverlauf" | "Transaction History" | "Historial de transacciones" | "Historique des transactions" | "Riwayat transaksi" | "Cronologia transazioni" | "Transactiegeschiedenis" | "Historia transakcji" | "Histórico de transações" | "История транзакций" | "İşlem Geçmişi"; "settings.transactionHistory": "Transaktionsverlauf" | "Transaction History" | "Historial de transacciones" | "Historique des transactions" | "Riwayat transaksi" | "Cronologia transazioni" | "Transactiegeschiedenis" | "Historia transakcji" | "Histórico de transações" | "История транзакций" | "İşlem Geçmişi";
"support.action": "Support kontaktieren" | "Contact support" | "Contactar soporte" | "Contacter le support" | "Hubungi dukungan" | "Contatta il supporto" | "Contact opnemen" | "Skontaktuj się z pomocą" | "Contactar suporte" | "Связаться с поддержкой" | "Destek ile iletişime geç";
"support.text": "Wenn Sie Fragen zum Spiel haben, wenden Sie sich bitte an unseren Support." | "If you have any questions related to the game, please contact our support team." | "Si tienes alguna pregunta relacionada con el juego, contacta con nuestro equipo de soporte." | "Si vous avez des questions liées au jeu, veuillez contacter notre équipe de support." | "Jika Anda memiliki pertanyaan terkait permainan, silakan hubungi tim dukungan kami." | "Se hai domande relative al gioco, contatta il nostro team di supporto." | "Als u vragen heeft over het spel, neem dan contact op met ons ondersteuningsteam." | "Jeśli masz pytania dotyczące gry, skontaktuj się z naszym zespołem wsparcia." | "Se tiver dúvidas relacionadas ao jogo, entre em contato com nossa equipe de suporte." | "Если у вас возникли вопросы, связанные с игрой — обратитесь в нашу службу поддержки." | "Oyunla ilgili sorularınız varsa lütfen destek ekibimizle iletişime geçin.";
"support.title": "Support" | "Soporte" | "Dukungan" | "Supporto" | "Ondersteuning" | "Wsparcie" | "Suporte" | "Поддержка" | "Destek";
"transactionHistory.date": "Datum" | "Date" | "Fecha" | "Tanggal" | "Data" | "Дата" | "Tarih";
"transactionHistory.operationType": "Vorgangsart" | "Operation type" | "Tipo de operación" | "Type d'opération" | "Jenis operasi" | "Tipo di operazione" | "Bewerkingstype" | "Typ operacji" | "Tipo de operação" | "Тип операции" | "İşlem türü";
"transactionHistory.sum": "Summe" | "Sum" | "Suma" | "Somme" | "Jumlah" | "Somma" | "Som" | "Soma" | "Сумма" | "Toplam";
"transactionHistory.title": "Transaktionsverlauf" | "Transaction History" | "Historial de transacciones" | "Historique des transactions" | "Riwayat transaksi" | "Cronologia transazioni" | "Transactiegeschiedenis" | "Historia transakcji" | "Histórico de transações" | "История транзакций" | "İşlem Geçmişi";
"transactionHistory.yourTransactions": "Ihre Transaktionen" | "Your Transactions" | "Tus transacciones" | "Vos transactions" | "Transaksi Anda" | "Le tue transazioni" | "Uw transacties" | "Twoje transakcje" | "Suas transações" | "Ваши транзакции" | "İşlemleriniz";
}; };
export default resources; export default resources;

View File

@@ -14,7 +14,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import i18n from "@/i18n"; import i18n from "@/i18n";
import tg from "@/tg"; import tg from "@/tg";
import { routeTree } from "./routeTree.gen"; import { routeTree } from "./routeTree.gen";
import { AudioProvider } from "@/audio"; import { useAudioStore } from "@/audio";
import { LiftProvider } from "@components/lift"; import { LiftProvider } from "@components/lift";
import "./styles/index.css"; import "./styles/index.css";
@@ -30,15 +30,14 @@ declare module "@tanstack/react-router" {
tg.init(); tg.init();
i18n.init(); i18n.init();
useAudioStore.getState().hydrate();
ReactDOM.createRoot(document.getElementById("root")!).render( ReactDOM.createRoot(document.getElementById("root")!).render(
<StrictMode> <StrictMode>
<QueryClientProvider client={new QueryClient()}> <QueryClientProvider client={new QueryClient()}>
<AudioProvider> <LiftProvider>
<LiftProvider> <RouterProvider router={router} />
<RouterProvider router={router} /> </LiftProvider>
</LiftProvider>
</AudioProvider>
</QueryClientProvider> </QueryClientProvider>
</StrictMode>, </StrictMode>,
); );

View File

@@ -7,6 +7,7 @@
background-image: url("./assets/main-bg.svg"); background-image: url("./assets/main-bg.svg");
background-size: auto 101%; background-size: auto 101%;
background-position: center; background-position: center;
overflow: hidden;
.main { .main {
height: 100%; height: 100%;

Binary file not shown.

After

Width:  |  Height:  |  Size: 720 KiB

View File

@@ -13,8 +13,9 @@
z-index: 20; z-index: 20;
padding-top: var(--header-padding); padding-top: var(--header-padding);
padding-left: var(--safe-left); --padding-x: 10px;
padding-right: var(--safe-right); padding-left: calc(var(--safe-left) + var(--padding-x));
padding-right: calc(var(--safe-right) + var(--padding-x));
&::before { &::before {
content: ""; content: "";

View File

@@ -12,9 +12,6 @@
gap: 2px; gap: 2px;
} }
.left {
}
.avatar { .avatar {
position: relative; position: relative;
display: flex; display: flex;
@@ -53,14 +50,79 @@
object-fit: cover; object-fit: cover;
} }
.right { .level {
align-self: center;
width: 37px;
height: 21px;
border-radius: 14px;
background: #ac6b33;
box-shadow:
0px 0.5px 0px 0px #ffefd1 inset,
0px -0.5px 0px 0px #0000008c inset;
display: flex;
align-items: center;
justify-content: center;
margin-top: -12px;
padding: 3px;
}
.levelInner {
width: 100%;
height: 100%;
border-radius: 40px;
background: #000000b3;
border-top: 1px solid transparent;
box-shadow:
0px 4px 2px 0px #00000040 inset,
0px -1px 0px 0px #fff3b5 inset;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
font-size: 12px;
}
.balances {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 4px;
} }
.rightTop { .balanceRow {
width: 124px;
height: 22px;
border-radius: 17px;
background: #00000099;
box-shadow:
0px -2px 0px 0px #b0703d inset,
2px 0px 0px 0px #b0703d inset,
-2px 0px 0px 0px #b0703d inset,
0px 2px 0px 0px #593313 inset,
0px 2px 2px 2px #00000059;
display: flex;
align-items: center;
gap: 4px;
padding-inline: 4px;
} }
.rightBottom { .balanceIcon {
width: 18px;
height: 18px;
flex-shrink: 0;
}
.honeyValue {
font-family: "BalsamiqSans", sans-serif;
font-weight: 700;
font-size: 15px;
color: #f7d048;
}
.moneyValue {
font-family: "BalsamiqSans", sans-serif;
font-weight: 700;
font-size: 15px;
color: #6cc872;
} }
} }

View File

@@ -2,9 +2,13 @@ import classes from "./Profile.module.css";
import { motion } from "motion/react"; import { motion } from "motion/react";
import { Liftable } from "@components/lift"; import { Liftable } from "@components/lift";
import { useTelegramUser } from "@/tg"; import { useTelegramUser } from "@/tg";
import HoneyIcon from "@components/icons/HoneyIcon";
import MoneyIcon from "@components/icons/MoneyIcon";
import Counter from "@components/atoms/Counter";
export default function Profile() { export default function Profile() {
const user = useTelegramUser(); const user = useTelegramUser();
return ( return (
<Liftable always> <Liftable always>
<motion.div <motion.div
@@ -19,10 +23,19 @@ export default function Profile() {
{user?.photoUrl && <img className={classes.avatarImage} src={user.photoUrl} alt="" />} {user?.photoUrl && <img className={classes.avatarImage} src={user.photoUrl} alt="" />}
</div> </div>
</div> </div>
<div className={classes.level}>
<div className={classes.levelInner}>1</div>
</div>
</div> </div>
<div className={classes.right}> <div className={classes.balances}>
<div className={classes.rightTop} /> <div className={classes.balanceRow}>
<div className={classes.rightBottom} /> <HoneyIcon className={classes.balanceIcon} />
<Counter className={classes.honeyValue} value={129891} />
</div>
<div className={classes.balanceRow}>
<MoneyIcon className={classes.balanceIcon} />
<Counter className={classes.moneyValue} value={1781} />
</div>
</div> </div>
</motion.div> </motion.div>
</Liftable> </Liftable>

View File

@@ -1,6 +1,5 @@
@layer base { @layer base {
.settings { .settings {
margin-left: auto;
width: 80px; width: 80px;
height: 50px; height: 50px;
display: flex; display: flex;

View File

@@ -13,10 +13,10 @@ const TransactionsHistoryModal = lazy(() => import("./components/TransactionsHis
const LanguageModal = lazy(() => import("./components/LanguageModal")); const LanguageModal = lazy(() => import("./components/LanguageModal"));
import classes from "./Settings.module.css"; import classes from "./Settings.module.css";
import { usePlaySound } from "@/audio"; import { useAudioStore } from "@/audio";
export default function Settings() { export default function Settings() {
const play = usePlaySound(); const play = useAudioStore((s) => s.play);
const [state, send] = useMachine(settingsMachine); const [state, send] = useMachine(settingsMachine);

View File

@@ -5,7 +5,7 @@ import Modal from "@components/modals/Modal";
import ContentSurface from "@components/surface/ContentSurface"; import ContentSurface from "@components/surface/ContentSurface";
import DarkSurface from "@components/surface/DarkSurface"; import DarkSurface from "@components/surface/DarkSurface";
import { useLanguages } from "@/i18n/useLanguages"; import { useLanguages } from "@/i18n/useLanguages";
import { usePlaySound } from "@/audio"; import { useAudioStore } from "@/audio";
import tg from "@/tg"; import tg from "@/tg";
import classes from "./LanguageModal.module.css"; import classes from "./LanguageModal.module.css";
@@ -21,7 +21,7 @@ type Props = {
export default function LanguageModal({ open, onClose, onBack }: Props) { export default function LanguageModal({ open, onClose, onBack }: Props) {
const { t } = useTranslation(); const { t } = useTranslation();
const { languages, current, setLanguage } = useLanguages(); const { languages, current, setLanguage } = useLanguages();
const play = usePlaySound(); const play = useAudioStore((s) => s.play);
const handleSelect = (key: string) => { const handleSelect = (key: string) => {
play("click"); play("click");
@@ -61,7 +61,11 @@ export default function LanguageModal({ open, onClose, onBack }: Props) {
<ContentSurface className={classes.contentSurface}> <ContentSurface className={classes.contentSurface}>
<DarkSurface className="rounded-full h-full"> <DarkSurface className="rounded-full h-full">
<motion.button className={classes.item} onClick={handleBack} whileTap={{ scale: 0.95 }}> <motion.button
className={clsx(classes.item, "justify-center text-white text-lg")}
onClick={handleBack}
whileTap={{ scale: 0.95 }}
>
<span>{t("settings.back")}</span> <span>{t("settings.back")}</span>
</motion.button> </motion.button>
</DarkSurface> </DarkSurface>

View File

@@ -5,7 +5,7 @@ import Modal from "@components/modals/Modal";
import ContentSurface from "@components/surface/ContentSurface"; import ContentSurface from "@components/surface/ContentSurface";
import LightSurface from "@components/surface/LightSurface"; import LightSurface from "@components/surface/LightSurface";
import SwitchInput from "@components/form/SwitchInput"; import SwitchInput from "@components/form/SwitchInput";
import { useAudioSettings, usePlaySound } from "@/audio"; import { useAudioStore } from "@/audio";
import tg from "@/tg"; import tg from "@/tg";
import classes from "./SettingsModal.module.css"; import classes from "./SettingsModal.module.css";
@@ -23,8 +23,7 @@ type Props = {
export default function SettingsModal({ open, onClose, onNavigate, liftIds }: Props) { export default function SettingsModal({ open, onClose, onNavigate, liftIds }: Props) {
const { t } = useTranslation(); const { t } = useTranslation();
const { current } = useLanguages(); const { current } = useLanguages();
const { isEnabled, setIsEnabled } = useAudioSettings(); const { isEnabled, setIsEnabled, play } = useAudioStore();
const play = usePlaySound();
const handleNavigate = (modal: SettingsModalId) => { const handleNavigate = (modal: SettingsModalId) => {
play("click"); play("click");

View File

@@ -76,6 +76,10 @@
pointer-events: none; pointer-events: none;
} }
.hidden {
display: none;
}
.menuOverlay > * { .menuOverlay > * {
pointer-events: auto; pointer-events: auto;
} }

View File

@@ -1,6 +1,7 @@
import { type AnyRoute, useMatchRoute, useNavigate } from "@tanstack/react-router"; import { type AnyRoute, useMatchRoute, useNavigate } from "@tanstack/react-router";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { AnimatePresence } from "motion/react"; import { AnimatePresence } from "motion/react";
import { useCallback, useEffect, useRef, useState } from "react";
import GlassSurface from "@components/surface/GlassSurface"; import GlassSurface from "@components/surface/GlassSurface";
@@ -12,7 +13,6 @@ import CashdeskRoute from "@/routes/cashdesk";
import RouletteRoute from "@/routes/roulette"; import RouletteRoute from "@/routes/roulette";
import TasksRoute from "@/routes/tasks"; import TasksRoute from "@/routes/tasks";
import EarningsRoute from "@/routes/earnings"; import EarningsRoute from "@/routes/earnings";
import { useCallback, useEffect, useRef, useState } from "react";
import shopIcon from "./assets/shop.svg"; import shopIcon from "./assets/shop.svg";
import apiaryIcon from "./assets/apiary.svg"; import apiaryIcon from "./assets/apiary.svg";
@@ -23,7 +23,7 @@ import rouletteIcon from "./assets/roulette.svg";
import tasksIcon from "./assets/tasks.svg"; import tasksIcon from "./assets/tasks.svg";
import earningsIcon from "./assets/earnings.svg"; import earningsIcon from "./assets/earnings.svg";
import tg, { useTelegramViewportValue } from "@/tg"; import tg, { useTelegramViewportValue } from "@/tg";
import { usePlaySound } from "@/audio"; import { useAudioStore } from "@/audio";
const ANIMATION_DURATION = 0.2; const ANIMATION_DURATION = 0.2;
const SPRING_ANIMATION = { const SPRING_ANIMATION = {
@@ -51,6 +51,37 @@ const MENU_ITEMS = [
{ key: "nav.earnings", route: EarningsRoute, icon: earningsIcon, delay: 0 }, { key: "nav.earnings", route: EarningsRoute, icon: earningsIcon, delay: 0 },
]; ];
// 200px is larger than any OS chrome resize but smaller than the smallest software keyboard
const KEYBOARD_THRESHOLD = 200;
function useKeyboardVisible(): boolean {
const [keyboardVisible, setKeyboardVisible] = useState(false);
const baselineHeight = useRef<number>(0);
useEffect(() => {
const vv = window.visualViewport;
if (!vv) return;
baselineHeight.current = vv.height;
const handleResize = () => {
const newHeight = vv.height;
if (baselineHeight.current - newHeight > KEYBOARD_THRESHOLD) {
setKeyboardVisible(true);
} else {
setKeyboardVisible(false);
baselineHeight.current = newHeight;
}
};
vv.addEventListener("resize", handleResize);
return () => vv.removeEventListener("resize", handleResize);
}, []);
return keyboardVisible;
}
type BarProps = { type BarProps = {
labelKey: string; labelKey: string;
icon: string; icon: string;
@@ -162,11 +193,12 @@ function MenuBar({ labelKey, icon, delay, active, onClick }: MenuBarProps) {
} }
export default function Navigation() { export default function Navigation() {
const play = usePlaySound(); const play = useAudioStore((s) => s.play);
const matchRoute = useMatchRoute(); const matchRoute = useMatchRoute();
const navigate = useNavigate(); const navigate = useNavigate();
const [menuOpen, setMenuOpen] = useState<number>(0); const [menuOpen, setMenuOpen] = useState<number>(0);
const navRef = useRef<HTMLDivElement>(null); const navRef = useRef<HTMLDivElement>(null);
const keyboardVisible = useKeyboardVisible();
const handleOutsideClick = useCallback((e: MouseEvent) => { const handleOutsideClick = useCallback((e: MouseEvent) => {
if (navRef.current && !navRef.current.contains(e.target as Node)) { if (navRef.current && !navRef.current.contains(e.target as Node)) {
@@ -193,7 +225,7 @@ export default function Navigation() {
}; };
return ( return (
<div ref={navRef}> <div ref={navRef} className={keyboardVisible ? classes.hidden : undefined}>
<div className={classes.menuOverlay}> <div className={classes.menuOverlay}>
<AnimatePresence> <AnimatePresence>
{menuOpen && {menuOpen &&

View File

@@ -29,9 +29,10 @@
html, html,
body { body {
background: #9ebf3e; background: #9ebf3e;
height: var(--safe-area-height); height: 100dvh;
width: var(--safe-area-width); width: var(--safe-area-width);
font-family: "BalsamiqSans", sans-serif; font-family: "BalsamiqSans", sans-serif;
overflow: hidden;
} }
#root { #root {