Compare commits
16 Commits
4aa0291176
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5747e3611d | ||
|
|
8a11ee4c29 | ||
|
|
a9ed3dea5f | ||
|
|
5c08238dbd | ||
|
|
e5b3b97680 | ||
|
|
31d7eaf6ac | ||
|
|
9bb32c1756 | ||
|
|
de711a407f | ||
|
|
082b9bb714 | ||
|
|
3c646bc5ac | ||
|
|
2c072f1474 | ||
|
|
9e035d5007 | ||
|
|
2cc51e62f4 | ||
|
|
e3088b7c47 | ||
|
|
5e9acffa09 | ||
|
|
2a1115b66f |
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxfmt/configuration_schema.json",
|
||||
"ignorePatterns": ["**/routeTree.gen.ts"]
|
||||
"ignorePatterns": ["**/*.gen.ts", "src/i18n/resources.d.ts", "public"]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"ignorePatterns": ["**/routeTree.gen.ts"],
|
||||
"ignorePatterns": ["**/*.gen.ts", "src/i18n/resources.d.ts"],
|
||||
"plugins": ["react", "react-perf", "import", "jsx-a11y", "promise"],
|
||||
"rules": {
|
||||
"eqeqeq": ["error", "smart"],
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<link rel="icon" type="image/svg+xml" href="/honey.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
<title>honey-fe</title>
|
||||
<title>Honey</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -22,21 +22,25 @@
|
||||
"@tanstack/react-router": "^1.166.3",
|
||||
"@tanstack/react-router-devtools": "^1.166.3",
|
||||
"@tma.js/sdk-react": "^3.0.16",
|
||||
"@xstate/react": "^6.1.0",
|
||||
"arktype": "^2.2.0",
|
||||
"axios": "^1.13.6",
|
||||
"clsx": "^2.1.1",
|
||||
"eruda": "^3.4.3",
|
||||
"howler": "^2.2.4",
|
||||
"i18next": "^25.8.17",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"motion": "^12.35.1",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-i18next": "^16.5.6",
|
||||
"tailwindcss": "^4.2.1"
|
||||
"tailwindcss": "^4.2.1",
|
||||
"xstate": "^5.28.0",
|
||||
"zustand": "^5.0.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@i18next-selector/vite-plugin": "^0.0.18",
|
||||
"@tanstack/router-plugin": "^1.166.3",
|
||||
"@types/howler": "^2.2.12",
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
|
||||
68
plugins/i18nextSortPlugin.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { readFileSync, readdirSync, writeFileSync } from "node:fs";
|
||||
import { join, resolve } from "node:path";
|
||||
import { normalizePath } from "vite";
|
||||
import type { Plugin, ResolvedConfig } from "vite";
|
||||
|
||||
interface Options {
|
||||
sourceDir: string;
|
||||
}
|
||||
|
||||
type JsonValue = string | number | boolean | null | JsonObject | JsonValue[];
|
||||
type JsonObject = { [key: string]: JsonValue };
|
||||
|
||||
function deepSortKeys(obj: JsonObject): JsonObject {
|
||||
const sorted: JsonObject = {};
|
||||
for (const key of Object.keys(obj).sort()) {
|
||||
const value = obj[key];
|
||||
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
||||
sorted[key] = deepSortKeys(value as JsonObject);
|
||||
} else {
|
||||
sorted[key] = value;
|
||||
}
|
||||
}
|
||||
return sorted;
|
||||
}
|
||||
|
||||
function sortFile(filePath: string): void {
|
||||
const raw = readFileSync(filePath, "utf-8");
|
||||
const parsed = JSON.parse(raw) as JsonObject;
|
||||
const sorted = deepSortKeys(parsed);
|
||||
const output = JSON.stringify(sorted, null, "\t") + "\n";
|
||||
|
||||
if (raw !== output) {
|
||||
writeFileSync(filePath, output, "utf-8");
|
||||
}
|
||||
}
|
||||
|
||||
function sortAllFiles(sourceDir: string): void {
|
||||
const files = readdirSync(sourceDir).filter((f) => f.endsWith(".json"));
|
||||
for (const file of files) {
|
||||
sortFile(join(sourceDir, file));
|
||||
}
|
||||
}
|
||||
|
||||
export function i18nextSortPlugin(options: Options): Plugin {
|
||||
let resolvedSourceDir: string;
|
||||
|
||||
return {
|
||||
name: "i18next-sort",
|
||||
|
||||
configResolved(config: ResolvedConfig) {
|
||||
resolvedSourceDir = normalizePath(resolve(config.root, options.sourceDir));
|
||||
},
|
||||
|
||||
buildStart() {
|
||||
sortAllFiles(resolvedSourceDir);
|
||||
},
|
||||
|
||||
configureServer(server) {
|
||||
server.watcher.add(resolvedSourceDir);
|
||||
|
||||
server.watcher.on("change", (file: string) => {
|
||||
if (file.startsWith(resolvedSourceDir) && file.endsWith(".json")) {
|
||||
sortFile(file);
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
97
plugins/i18nextTypesPlugin.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { readFileSync, readdirSync, writeFileSync } from "node:fs";
|
||||
import { join, resolve } from "node:path";
|
||||
import { normalizePath } from "vite";
|
||||
import type { Plugin, ResolvedConfig } from "vite";
|
||||
|
||||
interface Options {
|
||||
sourceDir: string;
|
||||
destination: string;
|
||||
}
|
||||
|
||||
type JsonValue = string | number | boolean | null | JsonObject | JsonValue[];
|
||||
type JsonObject = { [key: string]: JsonValue };
|
||||
|
||||
function flattenKeys(obj: JsonObject, prefix: string, result: Map<string, Set<string>>): void {
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
const fullKey = prefix ? `${prefix}.${key}` : key;
|
||||
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
||||
flattenKeys(value as JsonObject, fullKey, result);
|
||||
} else if (typeof value === "string") {
|
||||
if (!result.has(fullKey)) {
|
||||
result.set(fullKey, new Set());
|
||||
}
|
||||
result.get(fullKey)!.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function generateTypes(sourceDir: string, destination: string): void {
|
||||
const files = readdirSync(sourceDir).filter((f) => f.endsWith(".json"));
|
||||
|
||||
const keyValues = new Map<string, Set<string>>();
|
||||
|
||||
for (const file of files) {
|
||||
const content = JSON.parse(readFileSync(join(sourceDir, file), "utf-8")) as JsonObject;
|
||||
|
||||
flattenKeys(content, "", keyValues);
|
||||
}
|
||||
|
||||
const sortedKeys = Array.from(keyValues.keys()).sort();
|
||||
|
||||
const lines: string[] = [
|
||||
"// Auto-generated by i18nextTypesPlugin — do not edit manually",
|
||||
"declare const resources: {",
|
||||
];
|
||||
|
||||
for (const key of sortedKeys) {
|
||||
const values = Array.from(keyValues.get(key)!);
|
||||
const union = values.map((v) => JSON.stringify(v)).join(" | ");
|
||||
lines.push(` ${JSON.stringify(key)}: ${union};`);
|
||||
}
|
||||
|
||||
lines.push("};");
|
||||
lines.push("export default resources;");
|
||||
lines.push("");
|
||||
|
||||
writeFileSync(destination, lines.join("\n"), "utf-8");
|
||||
}
|
||||
|
||||
export function i18nextTypesPlugin(options: Options): Plugin {
|
||||
let resolvedSourceDir: string;
|
||||
let resolvedDestination: string;
|
||||
|
||||
return {
|
||||
name: "i18next-types",
|
||||
|
||||
configResolved(config: ResolvedConfig) {
|
||||
resolvedSourceDir = normalizePath(resolve(config.root, options.sourceDir));
|
||||
resolvedDestination = normalizePath(resolve(config.root, options.destination));
|
||||
},
|
||||
|
||||
buildStart() {
|
||||
generateTypes(resolvedSourceDir, resolvedDestination);
|
||||
},
|
||||
|
||||
configureServer(server) {
|
||||
server.watcher.add(resolvedSourceDir);
|
||||
|
||||
server.watcher.on("change", (file: string) => {
|
||||
if (file.startsWith(resolvedSourceDir) && file.endsWith(".json")) {
|
||||
generateTypes(resolvedSourceDir, resolvedDestination);
|
||||
}
|
||||
});
|
||||
|
||||
server.watcher.on("add", (file: string) => {
|
||||
if (file.startsWith(resolvedSourceDir) && file.endsWith(".json")) {
|
||||
generateTypes(resolvedSourceDir, resolvedDestination);
|
||||
}
|
||||
});
|
||||
|
||||
server.watcher.on("unlink", (file: string) => {
|
||||
if (file.startsWith(resolvedSourceDir) && file.endsWith(".json")) {
|
||||
generateTypes(resolvedSourceDir, resolvedDestination);
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
208
pnpm-lock.yaml
generated
@@ -10,10 +10,10 @@ importers:
|
||||
dependencies:
|
||||
'@tailwindcss/vite':
|
||||
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':
|
||||
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':
|
||||
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)
|
||||
@@ -32,6 +32,9 @@ importers:
|
||||
'@tma.js/sdk-react':
|
||||
specifier: ^3.0.16
|
||||
version: 3.0.16(@types/react@19.2.14)(react@19.2.4)(typescript@5.9.3)
|
||||
'@xstate/react':
|
||||
specifier: ^6.1.0
|
||||
version: 6.1.0(@types/react@19.2.14)(react@19.2.4)(xstate@5.28.0)
|
||||
arktype:
|
||||
specifier: ^2.2.0
|
||||
version: 2.2.0
|
||||
@@ -44,6 +47,9 @@ importers:
|
||||
eruda:
|
||||
specifier: ^3.4.3
|
||||
version: 3.4.3
|
||||
howler:
|
||||
specifier: ^2.2.4
|
||||
version: 2.2.4
|
||||
i18next:
|
||||
specifier: ^25.8.17
|
||||
version: 25.8.17(typescript@5.9.3)
|
||||
@@ -65,13 +71,19 @@ importers:
|
||||
tailwindcss:
|
||||
specifier: ^4.2.1
|
||||
version: 4.2.1
|
||||
xstate:
|
||||
specifier: ^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:
|
||||
'@i18next-selector/vite-plugin':
|
||||
specifier: ^0.0.18
|
||||
version: 0.0.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))
|
||||
'@tanstack/router-plugin':
|
||||
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':
|
||||
specifier: ^24.10.1
|
||||
version: 24.12.0
|
||||
@@ -83,7 +95,7 @@ importers:
|
||||
version: 19.2.3(@types/react@19.2.14)
|
||||
'@vitejs/plugin-react-swc':
|
||||
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:
|
||||
specifier: ^17.4.0
|
||||
version: 17.4.0
|
||||
@@ -107,7 +119,7 @@ importers:
|
||||
version: 5.9.3
|
||||
vite:
|
||||
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:
|
||||
|
||||
@@ -363,11 +375,6 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@i18next-selector/vite-plugin@0.0.18':
|
||||
resolution: {integrity: sha512-jCdVJdaYDqa3dE8LCscCa7OzCN7UvUP+FDgSuc3L3mkPRlmhiXwNpCSWNUpSgGfubGBcpIE9vexHaLcS2KU0Bg==}
|
||||
peerDependencies:
|
||||
vite: 6 - 7
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.13':
|
||||
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
|
||||
|
||||
@@ -378,6 +385,9 @@ packages:
|
||||
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
'@jridgewell/source-map@0.3.11':
|
||||
resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==}
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.5.5':
|
||||
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
|
||||
|
||||
@@ -1222,21 +1232,12 @@ packages:
|
||||
'@tma.js/types@1.0.2':
|
||||
resolution: {integrity: sha512-qs4mi+U1xZmMQBdMhWAo1X4YqUJ/ae0y28s+GNCpQq58bsJo0h8rvyVOB1RwPvXogIY9+yribbZe6z3AIJmsAQ==}
|
||||
|
||||
'@traversable/json@0.0.26':
|
||||
resolution: {integrity: sha512-oXKX0eNxbbHGLjLV27nTuV0uyR6uSoWi0BF+FYJu4jXRcSsWqCHOqNjIb2x/0usKd70rnKLGyHxurlTBTpQVOw==}
|
||||
peerDependencies:
|
||||
'@traversable/registry': ^0.0.25
|
||||
fast-check: ^3
|
||||
peerDependenciesMeta:
|
||||
fast-check:
|
||||
optional: true
|
||||
|
||||
'@traversable/registry@0.0.25':
|
||||
resolution: {integrity: sha512-idu2DhzoHOeqO+FZSpnDTgrFQWZL+poyxO9KozHeW7KdVqecrtYwR10vCVB/dItVdMBVZbavbNWO6PgUYN1KLg==}
|
||||
|
||||
'@types/estree@1.0.8':
|
||||
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
||||
|
||||
'@types/howler@2.2.12':
|
||||
resolution: {integrity: sha512-hy769UICzOSdK0Kn1FBk4gN+lswcj1EKRkmiDtMkUGvFfYJzgaDXmVXkSShS2m89ERAatGIPnTUlp2HhfkVo5g==}
|
||||
|
||||
'@types/node@24.12.0':
|
||||
resolution: {integrity: sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==}
|
||||
|
||||
@@ -1254,6 +1255,15 @@ packages:
|
||||
peerDependencies:
|
||||
vite: ^4 || ^5 || ^6 || ^7
|
||||
|
||||
'@xstate/react@6.1.0':
|
||||
resolution: {integrity: sha512-ep9F0jGTI63B/jE8GHdMpUqtuz7yRebNaKv8EMUaiSi29NOglywc2X2YSOV/ygbIK+LtmgZ0q9anoEA2iBSEOw==}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
xstate: ^5.28.0
|
||||
peerDependenciesMeta:
|
||||
xstate:
|
||||
optional: true
|
||||
|
||||
acorn@8.16.0:
|
||||
resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
@@ -1279,9 +1289,6 @@ packages:
|
||||
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
argparse@2.0.1:
|
||||
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
||||
|
||||
arkregex@0.0.5:
|
||||
resolution: {integrity: sha512-ncYjBdLlh5/QnVsAA8De16Tc9EqmYM7y/WU9j+236KcyYNUXogpz3sC4ATIZYzzLxwI+0sEOaQLEmLmRleaEXw==}
|
||||
|
||||
@@ -1322,6 +1329,9 @@ packages:
|
||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||
hasBin: true
|
||||
|
||||
buffer-from@1.1.2:
|
||||
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
||||
|
||||
call-bind-apply-helpers@1.0.2:
|
||||
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -1367,6 +1377,9 @@ packages:
|
||||
resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
commander@2.20.3:
|
||||
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
|
||||
|
||||
convert-source-map@2.0.0:
|
||||
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
||||
|
||||
@@ -1566,6 +1579,9 @@ packages:
|
||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
howler@2.2.4:
|
||||
resolution: {integrity: sha512-iARIBPgcQrwtEr+tALF+rapJ8qSc+Set2GJQl7xT1MQzWaVkFebdJhR3alVlSiUf5U7nAANKuj3aWpwerocD5w==}
|
||||
|
||||
html-parse-stringify@3.0.1:
|
||||
resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==}
|
||||
|
||||
@@ -1619,10 +1635,6 @@ packages:
|
||||
js-tokens@4.0.0:
|
||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||
|
||||
js-yaml@4.1.1:
|
||||
resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
|
||||
hasBin: true
|
||||
|
||||
jsesc@3.1.0:
|
||||
resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -2061,6 +2073,9 @@ packages:
|
||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
source-map-support@0.5.21:
|
||||
resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
|
||||
|
||||
source-map@0.6.1:
|
||||
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -2104,6 +2119,11 @@ packages:
|
||||
resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
terser@5.46.1:
|
||||
resolution: {integrity: sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
tiny-invariant@1.3.3:
|
||||
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
|
||||
|
||||
@@ -2155,6 +2175,15 @@ packages:
|
||||
peerDependencies:
|
||||
browserslist: '>= 4.21.0'
|
||||
|
||||
use-isomorphic-layout-effect@1.2.1:
|
||||
resolution: {integrity: sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
use-sync-external-store@1.6.0:
|
||||
resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==}
|
||||
peerDependencies:
|
||||
@@ -2240,6 +2269,9 @@ packages:
|
||||
utf-8-validate:
|
||||
optional: true
|
||||
|
||||
xstate@5.28.0:
|
||||
resolution: {integrity: sha512-Iaqq6ZrUzqeUtA3hC5LQKZfR8ZLzEFTImMHJM3jWEdVvXWdKvvVLXZEiNQWm3SCA9ZbEou/n5rcsna1wb9t28A==}
|
||||
|
||||
yallist@3.1.1:
|
||||
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
||||
|
||||
@@ -2251,6 +2283,24 @@ packages:
|
||||
zod@3.25.76:
|
||||
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:
|
||||
|
||||
'@ark/schema@0.56.0':
|
||||
@@ -2454,15 +2504,6 @@ snapshots:
|
||||
'@esbuild/win32-x64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@i18next-selector/vite-plugin@0.0.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))':
|
||||
dependencies:
|
||||
'@traversable/json': 0.0.26(@traversable/registry@0.0.25)
|
||||
'@traversable/registry': 0.0.25
|
||||
js-yaml: 4.1.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)
|
||||
transitivePeerDependencies:
|
||||
- fast-check
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.13':
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
@@ -2475,6 +2516,12 @@ snapshots:
|
||||
|
||||
'@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/trace-mapping@0.3.31':
|
||||
@@ -2881,12 +2928,12 @@ snapshots:
|
||||
'@tailwindcss/oxide-win32-arm64-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:
|
||||
'@tailwindcss/node': 4.2.1
|
||||
'@tailwindcss/oxide': 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':
|
||||
dependencies:
|
||||
@@ -2910,7 +2957,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- 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:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/generator': 7.29.1
|
||||
@@ -2922,7 +2969,7 @@ snapshots:
|
||||
chalk: 5.6.2
|
||||
launch-editor: 2.13.1
|
||||
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:
|
||||
- bufferutil
|
||||
- supports-color
|
||||
@@ -3035,7 +3082,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- 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:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0)
|
||||
@@ -3052,7 +3099,7 @@ snapshots:
|
||||
zod: 3.25.76
|
||||
optionalDependencies:
|
||||
'@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:
|
||||
- supports-color
|
||||
|
||||
@@ -3129,14 +3176,10 @@ snapshots:
|
||||
|
||||
'@tma.js/types@1.0.2': {}
|
||||
|
||||
'@traversable/json@0.0.26(@traversable/registry@0.0.25)':
|
||||
dependencies:
|
||||
'@traversable/registry': 0.0.25
|
||||
|
||||
'@traversable/registry@0.0.25': {}
|
||||
|
||||
'@types/estree@1.0.8': {}
|
||||
|
||||
'@types/howler@2.2.12': {}
|
||||
|
||||
'@types/node@24.12.0':
|
||||
dependencies:
|
||||
undici-types: 7.16.0
|
||||
@@ -3149,14 +3192,24 @@ snapshots:
|
||||
dependencies:
|
||||
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:
|
||||
'@rolldown/pluginutils': 1.0.0-rc.2
|
||||
'@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:
|
||||
- '@swc/helpers'
|
||||
|
||||
'@xstate/react@6.1.0(@types/react@19.2.14)(react@19.2.4)(xstate@5.28.0)':
|
||||
dependencies:
|
||||
react: 19.2.4
|
||||
use-isomorphic-layout-effect: 1.2.1(@types/react@19.2.14)(react@19.2.4)
|
||||
use-sync-external-store: 1.6.0(react@19.2.4)
|
||||
optionalDependencies:
|
||||
xstate: 5.28.0
|
||||
transitivePeerDependencies:
|
||||
- '@types/react'
|
||||
|
||||
acorn@8.16.0: {}
|
||||
|
||||
ansi-escapes@7.3.0:
|
||||
@@ -3174,8 +3227,6 @@ snapshots:
|
||||
normalize-path: 3.0.0
|
||||
picomatch: 2.3.1
|
||||
|
||||
argparse@2.0.1: {}
|
||||
|
||||
arkregex@0.0.5:
|
||||
dependencies:
|
||||
'@ark/util': 0.56.0
|
||||
@@ -3229,6 +3280,9 @@ snapshots:
|
||||
node-releases: 2.0.36
|
||||
update-browserslist-db: 1.2.3(browserslist@4.28.1)
|
||||
|
||||
buffer-from@1.1.2:
|
||||
optional: true
|
||||
|
||||
call-bind-apply-helpers@1.0.2:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
@@ -3277,6 +3331,9 @@ snapshots:
|
||||
|
||||
commander@14.0.3: {}
|
||||
|
||||
commander@2.20.3:
|
||||
optional: true
|
||||
|
||||
convert-source-map@2.0.0: {}
|
||||
|
||||
cookie-es@2.0.0: {}
|
||||
@@ -3459,6 +3516,8 @@ snapshots:
|
||||
dependencies:
|
||||
function-bind: 1.1.2
|
||||
|
||||
howler@2.2.4: {}
|
||||
|
||||
html-parse-stringify@3.0.1:
|
||||
dependencies:
|
||||
void-elements: 3.1.0
|
||||
@@ -3502,10 +3561,6 @@ snapshots:
|
||||
|
||||
js-tokens@4.0.0: {}
|
||||
|
||||
js-yaml@4.1.1:
|
||||
dependencies:
|
||||
argparse: 2.0.1
|
||||
|
||||
jsesc@3.1.0: {}
|
||||
|
||||
json5@2.2.3: {}
|
||||
@@ -3919,6 +3974,12 @@ snapshots:
|
||||
|
||||
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.7.6: {}
|
||||
@@ -3957,6 +4018,14 @@ snapshots:
|
||||
|
||||
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-warning@1.0.3: {}
|
||||
@@ -4002,6 +4071,12 @@ snapshots:
|
||||
escalade: 3.2.0
|
||||
picocolors: 1.1.1
|
||||
|
||||
use-isomorphic-layout-effect@1.2.1(@types/react@19.2.14)(react@19.2.4):
|
||||
dependencies:
|
||||
react: 19.2.4
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.14
|
||||
|
||||
use-sync-external-store@1.6.0(react@19.2.4):
|
||||
dependencies:
|
||||
react: 19.2.4
|
||||
@@ -4013,7 +4088,7 @@ snapshots:
|
||||
varint@6.0.0:
|
||||
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:
|
||||
esbuild: 0.27.3
|
||||
fdir: 6.5.0(picomatch@4.0.3)
|
||||
@@ -4028,6 +4103,7 @@ snapshots:
|
||||
lightningcss: 1.31.1
|
||||
sass: 1.97.3
|
||||
sass-embedded: 1.97.3
|
||||
terser: 5.46.1
|
||||
tsx: 4.21.0
|
||||
yaml: 2.8.2
|
||||
|
||||
@@ -4050,8 +4126,16 @@ snapshots:
|
||||
|
||||
ws@8.19.0: {}
|
||||
|
||||
xstate@5.28.0: {}
|
||||
|
||||
yallist@3.1.1: {}
|
||||
|
||||
yaml@2.8.2: {}
|
||||
|
||||
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)
|
||||
|
||||
551
public/honey.svg
Normal file
|
After Width: | Height: | Size: 77 KiB |
60
public/locales/de.json
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"accountInfo": {
|
||||
"paymentBalance": "Einzahlungsguthaben",
|
||||
"registrationDate": "Registrierungsdatum",
|
||||
"withdrawalBalance": "Auszahlungsguthaben",
|
||||
"yourId": "Ihre ID"
|
||||
},
|
||||
"actionModal": {
|
||||
"close": "Schließen"
|
||||
},
|
||||
"common": {
|
||||
"off": "aus",
|
||||
"on": "an"
|
||||
},
|
||||
"faq": {
|
||||
"1a": "Antwort",
|
||||
"1q": "Frage",
|
||||
"title": "FAQ"
|
||||
},
|
||||
"nav": {
|
||||
"apiary": "Bienenhaus",
|
||||
"cashdesk": "Kasse",
|
||||
"earnings": "Einnahmen",
|
||||
"game": "Spiel",
|
||||
"menu": "Menü",
|
||||
"roulette": "Roulette",
|
||||
"shop": "Shop",
|
||||
"tasks": "Aufgaben"
|
||||
},
|
||||
"operationType": {
|
||||
"deposit": "Einzahlung",
|
||||
"greeting": "Willkommensbonus",
|
||||
"referral": "Empfehlungsbonus",
|
||||
"withdrawal": "Auszahlung"
|
||||
},
|
||||
"pagination": {
|
||||
"of": "von"
|
||||
},
|
||||
"settings": {
|
||||
"accountInfo": "Kontoinformationen",
|
||||
"back": "Zurück",
|
||||
"faq": "FAQ",
|
||||
"language": "Sprache",
|
||||
"sound": "Ton",
|
||||
"support": "Support",
|
||||
"transactionHistory": "Transaktionsverlauf"
|
||||
},
|
||||
"support": {
|
||||
"action": "Support kontaktieren",
|
||||
"text": "Wenn Sie Fragen zum Spiel haben, wenden Sie sich bitte an unseren Support.",
|
||||
"title": "Support"
|
||||
},
|
||||
"transactionHistory": {
|
||||
"date": "Datum",
|
||||
"operationType": "Vorgangsart",
|
||||
"sum": "Summe",
|
||||
"title": "Transaktionsverlauf",
|
||||
"yourTransactions": "Ihre Transaktionen"
|
||||
}
|
||||
}
|
||||
12
public/locales/en.d.ts
vendored
@@ -1,12 +0,0 @@
|
||||
export declare const resources: {
|
||||
hello: "Hello World!";
|
||||
"actionModal.close": "Close";
|
||||
"nav.shop": "Shop";
|
||||
"nav.apiary": "Apiary";
|
||||
"nav.game": "Game";
|
||||
"nav.cashdesk": "Cashdesk";
|
||||
"nav.menu": "Menu";
|
||||
"nav.roulette": "Roulette";
|
||||
"nav.tasks": "Tasks";
|
||||
"nav.earnings": "Earnings";
|
||||
};
|
||||
@@ -1,12 +1,60 @@
|
||||
{
|
||||
"hello": "Hello World!",
|
||||
"actionModal.close": "Close",
|
||||
"nav.shop": "Shop",
|
||||
"nav.apiary": "Apiary",
|
||||
"nav.game": "Game",
|
||||
"nav.cashdesk": "Cashdesk",
|
||||
"nav.menu": "Menu",
|
||||
"nav.roulette": "Roulette",
|
||||
"nav.tasks": "Tasks",
|
||||
"nav.earnings": "Earnings"
|
||||
"accountInfo": {
|
||||
"paymentBalance": "Payment balance",
|
||||
"registrationDate": "Registration date",
|
||||
"withdrawalBalance": "Withdrawal balance",
|
||||
"yourId": "Your ID"
|
||||
},
|
||||
"actionModal": {
|
||||
"close": "Close"
|
||||
},
|
||||
"common": {
|
||||
"off": "off",
|
||||
"on": "on"
|
||||
},
|
||||
"faq": {
|
||||
"1a": "Answer",
|
||||
"1q": "Question",
|
||||
"title": "FAQ"
|
||||
},
|
||||
"nav": {
|
||||
"apiary": "Apiary",
|
||||
"cashdesk": "Cashdesk",
|
||||
"earnings": "Earnings",
|
||||
"game": "Game",
|
||||
"menu": "Menu",
|
||||
"roulette": "Roulette",
|
||||
"shop": "Shop",
|
||||
"tasks": "Tasks"
|
||||
},
|
||||
"operationType": {
|
||||
"deposit": "Deposit",
|
||||
"greeting": "Greeting bonus",
|
||||
"referral": "Referral bonus",
|
||||
"withdrawal": "Withdrawal"
|
||||
},
|
||||
"pagination": {
|
||||
"of": "of"
|
||||
},
|
||||
"settings": {
|
||||
"accountInfo": "Account information",
|
||||
"back": "Back",
|
||||
"faq": "FAQ",
|
||||
"language": "Language",
|
||||
"sound": "Sound",
|
||||
"support": "Support",
|
||||
"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": {
|
||||
"date": "Date",
|
||||
"operationType": "Operation type",
|
||||
"sum": "Sum",
|
||||
"title": "Transaction History",
|
||||
"yourTransactions": "Your Transactions"
|
||||
}
|
||||
}
|
||||
|
||||
60
public/locales/es.json
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"accountInfo": {
|
||||
"paymentBalance": "Saldo de pagos",
|
||||
"registrationDate": "Fecha de registro",
|
||||
"withdrawalBalance": "Saldo de retiros",
|
||||
"yourId": "Tu ID"
|
||||
},
|
||||
"actionModal": {
|
||||
"close": "Cerrar"
|
||||
},
|
||||
"common": {
|
||||
"off": "no",
|
||||
"on": "sí"
|
||||
},
|
||||
"faq": {
|
||||
"1a": "Respuesta",
|
||||
"1q": "Pregunta",
|
||||
"title": "FAQ"
|
||||
},
|
||||
"nav": {
|
||||
"apiary": "Apiario",
|
||||
"cashdesk": "Caja",
|
||||
"earnings": "Ganancias",
|
||||
"game": "Juego",
|
||||
"menu": "Menú",
|
||||
"roulette": "Ruleta",
|
||||
"shop": "Tienda",
|
||||
"tasks": "Tareas"
|
||||
},
|
||||
"operationType": {
|
||||
"deposit": "Depósito",
|
||||
"greeting": "Bono de bienvenida",
|
||||
"referral": "Bono de referido",
|
||||
"withdrawal": "Retiro"
|
||||
},
|
||||
"pagination": {
|
||||
"of": "de"
|
||||
},
|
||||
"settings": {
|
||||
"accountInfo": "Información de la cuenta",
|
||||
"back": "Volver",
|
||||
"faq": "FAQ",
|
||||
"language": "Idioma",
|
||||
"sound": "Sonido",
|
||||
"support": "Soporte",
|
||||
"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": {
|
||||
"date": "Fecha",
|
||||
"operationType": "Tipo de operación",
|
||||
"sum": "Suma",
|
||||
"title": "Historial de transacciones",
|
||||
"yourTransactions": "Tus transacciones"
|
||||
}
|
||||
}
|
||||
60
public/locales/fr.json
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"accountInfo": {
|
||||
"paymentBalance": "Solde des paiements",
|
||||
"registrationDate": "Date d'inscription",
|
||||
"withdrawalBalance": "Solde des retraits",
|
||||
"yourId": "Votre ID"
|
||||
},
|
||||
"actionModal": {
|
||||
"close": "Fermer"
|
||||
},
|
||||
"common": {
|
||||
"off": "non",
|
||||
"on": "oui"
|
||||
},
|
||||
"faq": {
|
||||
"1a": "Réponse",
|
||||
"1q": "Question",
|
||||
"title": "FAQ"
|
||||
},
|
||||
"nav": {
|
||||
"apiary": "Rucher",
|
||||
"cashdesk": "Caisse",
|
||||
"earnings": "Gains",
|
||||
"game": "Jeu",
|
||||
"menu": "Menu",
|
||||
"roulette": "Roulette",
|
||||
"shop": "Boutique",
|
||||
"tasks": "Tâches"
|
||||
},
|
||||
"operationType": {
|
||||
"deposit": "Dépôt",
|
||||
"greeting": "Bonus de bienvenue",
|
||||
"referral": "Bonus de parrainage",
|
||||
"withdrawal": "Retrait"
|
||||
},
|
||||
"pagination": {
|
||||
"of": "sur"
|
||||
},
|
||||
"settings": {
|
||||
"accountInfo": "Informations du compte",
|
||||
"back": "Retour",
|
||||
"faq": "FAQ",
|
||||
"language": "Langue",
|
||||
"sound": "Son",
|
||||
"support": "Support",
|
||||
"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": {
|
||||
"date": "Date",
|
||||
"operationType": "Type d'opération",
|
||||
"sum": "Somme",
|
||||
"title": "Historique des transactions",
|
||||
"yourTransactions": "Vos transactions"
|
||||
}
|
||||
}
|
||||
60
public/locales/id.json
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"accountInfo": {
|
||||
"paymentBalance": "Saldo pembayaran",
|
||||
"registrationDate": "Tanggal pendaftaran",
|
||||
"withdrawalBalance": "Saldo penarikan",
|
||||
"yourId": "ID Anda"
|
||||
},
|
||||
"actionModal": {
|
||||
"close": "Tutup"
|
||||
},
|
||||
"common": {
|
||||
"off": "mati",
|
||||
"on": "nyala"
|
||||
},
|
||||
"faq": {
|
||||
"1a": "Jawaban",
|
||||
"1q": "Pertanyaan",
|
||||
"title": "FAQ"
|
||||
},
|
||||
"nav": {
|
||||
"apiary": "Peternakan Lebah",
|
||||
"cashdesk": "Kasir",
|
||||
"earnings": "Penghasilan",
|
||||
"game": "Permainan",
|
||||
"menu": "Menu",
|
||||
"roulette": "Roulette",
|
||||
"shop": "Toko",
|
||||
"tasks": "Tugas"
|
||||
},
|
||||
"operationType": {
|
||||
"deposit": "Setoran",
|
||||
"greeting": "Bonus sambutan",
|
||||
"referral": "Bonus referral",
|
||||
"withdrawal": "Penarikan"
|
||||
},
|
||||
"pagination": {
|
||||
"of": "dari"
|
||||
},
|
||||
"settings": {
|
||||
"accountInfo": "Informasi akun",
|
||||
"back": "Kembali",
|
||||
"faq": "FAQ",
|
||||
"language": "Bahasa",
|
||||
"sound": "Suara",
|
||||
"support": "Dukungan",
|
||||
"transactionHistory": "Riwayat transaksi"
|
||||
},
|
||||
"support": {
|
||||
"action": "Hubungi dukungan",
|
||||
"text": "Jika Anda memiliki pertanyaan terkait permainan, silakan hubungi tim dukungan kami.",
|
||||
"title": "Dukungan"
|
||||
},
|
||||
"transactionHistory": {
|
||||
"date": "Tanggal",
|
||||
"operationType": "Jenis operasi",
|
||||
"sum": "Jumlah",
|
||||
"title": "Riwayat transaksi",
|
||||
"yourTransactions": "Transaksi Anda"
|
||||
}
|
||||
}
|
||||
60
public/locales/it.json
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"accountInfo": {
|
||||
"paymentBalance": "Saldo pagamenti",
|
||||
"registrationDate": "Data di registrazione",
|
||||
"withdrawalBalance": "Saldo prelievi",
|
||||
"yourId": "Il tuo ID"
|
||||
},
|
||||
"actionModal": {
|
||||
"close": "Chiudi"
|
||||
},
|
||||
"common": {
|
||||
"off": "no",
|
||||
"on": "sì"
|
||||
},
|
||||
"faq": {
|
||||
"1a": "Risposta",
|
||||
"1q": "Domanda",
|
||||
"title": "FAQ"
|
||||
},
|
||||
"nav": {
|
||||
"apiary": "Apiario",
|
||||
"cashdesk": "Cassa",
|
||||
"earnings": "Guadagni",
|
||||
"game": "Gioco",
|
||||
"menu": "Menu",
|
||||
"roulette": "Roulette",
|
||||
"shop": "Negozio",
|
||||
"tasks": "Compiti"
|
||||
},
|
||||
"operationType": {
|
||||
"deposit": "Deposito",
|
||||
"greeting": "Bonus di benvenuto",
|
||||
"referral": "Bonus referral",
|
||||
"withdrawal": "Prelievo"
|
||||
},
|
||||
"pagination": {
|
||||
"of": "di"
|
||||
},
|
||||
"settings": {
|
||||
"accountInfo": "Informazioni account",
|
||||
"back": "Indietro",
|
||||
"faq": "FAQ",
|
||||
"language": "Lingua",
|
||||
"sound": "Suono",
|
||||
"support": "Supporto",
|
||||
"transactionHistory": "Cronologia transazioni"
|
||||
},
|
||||
"support": {
|
||||
"action": "Contatta il supporto",
|
||||
"text": "Se hai domande relative al gioco, contatta il nostro team di supporto.",
|
||||
"title": "Supporto"
|
||||
},
|
||||
"transactionHistory": {
|
||||
"date": "Data",
|
||||
"operationType": "Tipo di operazione",
|
||||
"sum": "Somma",
|
||||
"title": "Cronologia transazioni",
|
||||
"yourTransactions": "Le tue transazioni"
|
||||
}
|
||||
}
|
||||
60
public/locales/nl.json
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"accountInfo": {
|
||||
"paymentBalance": "Stortingssaldo",
|
||||
"registrationDate": "Registratiedatum",
|
||||
"withdrawalBalance": "Opnamesaldo",
|
||||
"yourId": "Uw ID"
|
||||
},
|
||||
"actionModal": {
|
||||
"close": "Sluiten"
|
||||
},
|
||||
"common": {
|
||||
"off": "uit",
|
||||
"on": "aan"
|
||||
},
|
||||
"faq": {
|
||||
"1a": "Antwoord",
|
||||
"1q": "Vraag",
|
||||
"title": "FAQ"
|
||||
},
|
||||
"nav": {
|
||||
"apiary": "Bijenstal",
|
||||
"cashdesk": "Kassa",
|
||||
"earnings": "Inkomsten",
|
||||
"game": "Spel",
|
||||
"menu": "Menu",
|
||||
"roulette": "Roulette",
|
||||
"shop": "Winkel",
|
||||
"tasks": "Taken"
|
||||
},
|
||||
"operationType": {
|
||||
"deposit": "Storting",
|
||||
"greeting": "Welkomstbonus",
|
||||
"referral": "Verwijzingsbonus",
|
||||
"withdrawal": "Opname"
|
||||
},
|
||||
"pagination": {
|
||||
"of": "van"
|
||||
},
|
||||
"settings": {
|
||||
"accountInfo": "Accountinformatie",
|
||||
"back": "Terug",
|
||||
"faq": "FAQ",
|
||||
"language": "Taal",
|
||||
"sound": "Geluid",
|
||||
"support": "Ondersteuning",
|
||||
"transactionHistory": "Transactiegeschiedenis"
|
||||
},
|
||||
"support": {
|
||||
"action": "Contact opnemen",
|
||||
"text": "Als u vragen heeft over het spel, neem dan contact op met ons ondersteuningsteam.",
|
||||
"title": "Ondersteuning"
|
||||
},
|
||||
"transactionHistory": {
|
||||
"date": "Datum",
|
||||
"operationType": "Bewerkingstype",
|
||||
"sum": "Som",
|
||||
"title": "Transactiegeschiedenis",
|
||||
"yourTransactions": "Uw transacties"
|
||||
}
|
||||
}
|
||||
60
public/locales/pl.json
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"accountInfo": {
|
||||
"paymentBalance": "Saldo wpłat",
|
||||
"registrationDate": "Data rejestracji",
|
||||
"withdrawalBalance": "Saldo wypłat",
|
||||
"yourId": "Twoje ID"
|
||||
},
|
||||
"actionModal": {
|
||||
"close": "Zamknij"
|
||||
},
|
||||
"common": {
|
||||
"off": "wył",
|
||||
"on": "wł"
|
||||
},
|
||||
"faq": {
|
||||
"1a": "Odpowiedź",
|
||||
"1q": "Pytanie",
|
||||
"title": "FAQ"
|
||||
},
|
||||
"nav": {
|
||||
"apiary": "Pasieka",
|
||||
"cashdesk": "Kasa",
|
||||
"earnings": "Zarobki",
|
||||
"game": "Gra",
|
||||
"menu": "Menu",
|
||||
"roulette": "Ruletka",
|
||||
"shop": "Sklep",
|
||||
"tasks": "Zadania"
|
||||
},
|
||||
"operationType": {
|
||||
"deposit": "Wpłata",
|
||||
"greeting": "Bonus powitalny",
|
||||
"referral": "Bonus polecający",
|
||||
"withdrawal": "Wypłata"
|
||||
},
|
||||
"pagination": {
|
||||
"of": "z"
|
||||
},
|
||||
"settings": {
|
||||
"accountInfo": "Informacje o koncie",
|
||||
"back": "Wstecz",
|
||||
"faq": "FAQ",
|
||||
"language": "Język",
|
||||
"sound": "Dźwięk",
|
||||
"support": "Wsparcie",
|
||||
"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": {
|
||||
"date": "Data",
|
||||
"operationType": "Typ operacji",
|
||||
"sum": "Suma",
|
||||
"title": "Historia transakcji",
|
||||
"yourTransactions": "Twoje transakcje"
|
||||
}
|
||||
}
|
||||
60
public/locales/pt.json
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"accountInfo": {
|
||||
"paymentBalance": "Saldo de pagamentos",
|
||||
"registrationDate": "Data de registro",
|
||||
"withdrawalBalance": "Saldo de saques",
|
||||
"yourId": "Seu ID"
|
||||
},
|
||||
"actionModal": {
|
||||
"close": "Fechar"
|
||||
},
|
||||
"common": {
|
||||
"off": "não",
|
||||
"on": "sim"
|
||||
},
|
||||
"faq": {
|
||||
"1a": "Resposta",
|
||||
"1q": "Pergunta",
|
||||
"title": "FAQ"
|
||||
},
|
||||
"nav": {
|
||||
"apiary": "Apiário",
|
||||
"cashdesk": "Caixa",
|
||||
"earnings": "Ganhos",
|
||||
"game": "Jogo",
|
||||
"menu": "Menu",
|
||||
"roulette": "Roleta",
|
||||
"shop": "Loja",
|
||||
"tasks": "Tarefas"
|
||||
},
|
||||
"operationType": {
|
||||
"deposit": "Depósito",
|
||||
"greeting": "Bônus de boas-vindas",
|
||||
"referral": "Bônus de indicação",
|
||||
"withdrawal": "Saque"
|
||||
},
|
||||
"pagination": {
|
||||
"of": "de"
|
||||
},
|
||||
"settings": {
|
||||
"accountInfo": "Informações da conta",
|
||||
"back": "Voltar",
|
||||
"faq": "FAQ",
|
||||
"language": "Idioma",
|
||||
"sound": "Som",
|
||||
"support": "Suporte",
|
||||
"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": {
|
||||
"date": "Data",
|
||||
"operationType": "Tipo de operação",
|
||||
"sum": "Soma",
|
||||
"title": "Histórico de transações",
|
||||
"yourTransactions": "Suas transações"
|
||||
}
|
||||
}
|
||||
12
public/locales/ru.d.ts
vendored
@@ -1,12 +0,0 @@
|
||||
export declare const resources: {
|
||||
hello: "Привет мир";
|
||||
"actionModal.close": "Закрыть";
|
||||
"nav.shop": "Магазин";
|
||||
"nav.apiary": "Пасека";
|
||||
"nav.game": "Игра";
|
||||
"nav.cashdesk": "Касса";
|
||||
"nav.menu": "Меню";
|
||||
"nav.roulette": "Рулетка";
|
||||
"nav.tasks": "Задания";
|
||||
"nav.earnings": "Заработок";
|
||||
};
|
||||
@@ -1,12 +1,60 @@
|
||||
{
|
||||
"hello": "Привет мир",
|
||||
"actionModal.close": "Закрыть",
|
||||
"nav.shop": "Магазин",
|
||||
"nav.apiary": "Пасека",
|
||||
"nav.game": "Игра",
|
||||
"nav.cashdesk": "Касса",
|
||||
"nav.menu": "Меню",
|
||||
"nav.roulette": "Рулетка",
|
||||
"nav.tasks": "Задания",
|
||||
"nav.earnings": "Заработок"
|
||||
"accountInfo": {
|
||||
"paymentBalance": "Баланс пополнений",
|
||||
"registrationDate": "Дата регистрации",
|
||||
"withdrawalBalance": "Баланс выводов",
|
||||
"yourId": "Ваш ID"
|
||||
},
|
||||
"actionModal": {
|
||||
"close": "Закрыть"
|
||||
},
|
||||
"common": {
|
||||
"off": "выкл",
|
||||
"on": "вкл"
|
||||
},
|
||||
"faq": {
|
||||
"1a": "Ответ",
|
||||
"1q": "Вопрос",
|
||||
"title": "ЧаВо"
|
||||
},
|
||||
"nav": {
|
||||
"apiary": "Пасека",
|
||||
"cashdesk": "Касса",
|
||||
"earnings": "Заработок",
|
||||
"game": "Игра",
|
||||
"menu": "Меню",
|
||||
"roulette": "Рулетка",
|
||||
"shop": "Магазин",
|
||||
"tasks": "Задания"
|
||||
},
|
||||
"operationType": {
|
||||
"deposit": "Пополнение",
|
||||
"greeting": "Приветственный бонус",
|
||||
"referral": "Реферальный бонус",
|
||||
"withdrawal": "Вывод"
|
||||
},
|
||||
"pagination": {
|
||||
"of": "из"
|
||||
},
|
||||
"settings": {
|
||||
"accountInfo": "Информация об аккаунте",
|
||||
"back": "Назад",
|
||||
"faq": "ЧаВо",
|
||||
"language": "Язык",
|
||||
"sound": "Звук",
|
||||
"support": "Поддержка",
|
||||
"transactionHistory": "История транзакций"
|
||||
},
|
||||
"support": {
|
||||
"action": "Связаться с поддержкой",
|
||||
"text": "Если у вас возникли вопросы, связанные с игрой — обратитесь в нашу службу поддержки.",
|
||||
"title": "Поддержка"
|
||||
},
|
||||
"transactionHistory": {
|
||||
"date": "Дата",
|
||||
"operationType": "Тип операции",
|
||||
"sum": "Сумма",
|
||||
"title": "История транзакций",
|
||||
"yourTransactions": "Ваши транзакции"
|
||||
}
|
||||
}
|
||||
|
||||
60
public/locales/tr.json
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"accountInfo": {
|
||||
"paymentBalance": "Ödeme bakiyesi",
|
||||
"registrationDate": "Kayıt tarihi",
|
||||
"withdrawalBalance": "Çekim bakiyesi",
|
||||
"yourId": "Kimliğiniz"
|
||||
},
|
||||
"actionModal": {
|
||||
"close": "Kapat"
|
||||
},
|
||||
"common": {
|
||||
"off": "kapalı",
|
||||
"on": "açık"
|
||||
},
|
||||
"faq": {
|
||||
"1a": "Cevap",
|
||||
"1q": "Soru",
|
||||
"title": "SSS"
|
||||
},
|
||||
"nav": {
|
||||
"apiary": "Arılık",
|
||||
"cashdesk": "Kasa",
|
||||
"earnings": "Kazançlar",
|
||||
"game": "Oyun",
|
||||
"menu": "Menü",
|
||||
"roulette": "Rulet",
|
||||
"shop": "Mağaza",
|
||||
"tasks": "Görevler"
|
||||
},
|
||||
"operationType": {
|
||||
"deposit": "Yatırım",
|
||||
"greeting": "Hoş geldin bonusu",
|
||||
"referral": "Referans bonusu",
|
||||
"withdrawal": "Çekim"
|
||||
},
|
||||
"pagination": {
|
||||
"of": "/"
|
||||
},
|
||||
"settings": {
|
||||
"accountInfo": "Hesap bilgileri",
|
||||
"back": "Geri",
|
||||
"faq": "SSS",
|
||||
"language": "Dil",
|
||||
"sound": "Ses",
|
||||
"support": "Destek",
|
||||
"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": {
|
||||
"date": "Tarih",
|
||||
"operationType": "İşlem türü",
|
||||
"sum": "Toplam",
|
||||
"title": "İşlem Geçmişi",
|
||||
"yourTransactions": "İşlemleriniz"
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
BIN
src/audio/assets/click.mp3
Normal file
3
src/audio/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { useAudioStore } from "./store";
|
||||
export { SOUNDS } from "./sounds";
|
||||
export type { SoundKey } from "./sounds";
|
||||
17
src/audio/sounds.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Howl } from "howler";
|
||||
|
||||
import click from "./assets/click.mp3";
|
||||
|
||||
const SOUND_SOURCES = {
|
||||
click,
|
||||
} as const;
|
||||
|
||||
export type SoundKey = keyof typeof SOUND_SOURCES;
|
||||
|
||||
export const SOUNDS: Record<SoundKey, Howl> = {} as Record<SoundKey, Howl>;
|
||||
|
||||
export function preloadSounds() {
|
||||
for (const [key, src] of Object.entries(SOUND_SOURCES)) {
|
||||
SOUNDS[key as SoundKey] = new Howl({ src: [src], preload: true });
|
||||
}
|
||||
}
|
||||
76
src/audio/store.ts
Normal 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();
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -24,6 +24,8 @@
|
||||
|
||||
&:disabled {
|
||||
pointer-events: none;
|
||||
opacity: 0.9;
|
||||
filter: grayscale(0.6);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { motion, type HTMLMotionProps } from "motion/react";
|
||||
import clsx, { type ClassValue } from "clsx";
|
||||
|
||||
import { useAudioStore } from "@/audio";
|
||||
import tg from "@/tg";
|
||||
|
||||
import classes from "./Button.module.css";
|
||||
@@ -18,10 +19,13 @@ const VARIANTS_MAP = {
|
||||
} satisfies Record<Exclude<Props["variant"], undefined>, string>;
|
||||
|
||||
export default function Button({ className, variant = "blue", onClick, ...props }: Props) {
|
||||
const play = useAudioStore((s) => s.play);
|
||||
|
||||
return (
|
||||
<motion.button
|
||||
{...props}
|
||||
onClick={(e) => {
|
||||
play("click");
|
||||
tg.hapticFeedback.click();
|
||||
onClick?.(e);
|
||||
}}
|
||||
|
||||
74
src/components/atoms/Pagination/Pagination.module.css
Normal file
@@ -0,0 +1,74 @@
|
||||
@layer base {
|
||||
.wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
flex-shrink: 0;
|
||||
gap: 1px;
|
||||
border-radius: 9999px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.startItem {
|
||||
border-radius: 9999px 5px 5px 9999px;
|
||||
}
|
||||
|
||||
.endItem {
|
||||
border-radius: 5px 9999px 9999px 5px;
|
||||
}
|
||||
|
||||
.button {
|
||||
cursor: pointer;
|
||||
flex: 1;
|
||||
flex-shrink: 0;
|
||||
user-select: none;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.state {
|
||||
flex: 1;
|
||||
padding: 8px;
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.mirrored {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
.blueWrapper {
|
||||
padding: 3px;
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
.blueItem {
|
||||
background: #bef9fb;
|
||||
box-shadow:
|
||||
0px 1px 0px 0px #ffffffbf inset,
|
||||
-1px 0px 0px 0px #00000059 inset,
|
||||
1px 0px 0px 0px #00000059 inset,
|
||||
0px -1px 0px 0px #00000059 inset;
|
||||
color: #0c2836;
|
||||
--icon-fill: #227873;
|
||||
--icon-stroke: #0c2836;
|
||||
}
|
||||
}
|
||||
107
src/components/atoms/Pagination/Pagination.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import { motion } from "motion/react";
|
||||
import clsx from "clsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import tg from "@/tg";
|
||||
import ContentSurface from "@components/surface/ContentSurface";
|
||||
import { BlueSurface } from "@components/surface/BlueSectionSurface";
|
||||
import LightSurface from "@components/surface/LightSurface";
|
||||
|
||||
import BackIcon from "./icons/BackIcon";
|
||||
import StartIcon from "./icons/StartIcon";
|
||||
import classes from "./Pagination.module.css";
|
||||
import { useAudioStore } from "@/audio";
|
||||
|
||||
type Props = {
|
||||
value: number;
|
||||
total: number;
|
||||
onChange?: (page: number) => unknown;
|
||||
variant?: "default" | "blue";
|
||||
};
|
||||
|
||||
export default function Pagination({ value, total, onChange, variant = "default" }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const play = useAudioStore((s) => s.play);
|
||||
|
||||
const isAtStart = value <= 1 || total <= 1;
|
||||
const isAtEnd = value >= total || total <= 1;
|
||||
const isBlue = variant === "blue";
|
||||
|
||||
const Wrapper = isBlue ? BlueSurface : ContentSurface;
|
||||
const ItemSurface = isBlue ? motion.div : LightSurface;
|
||||
|
||||
const itemClass = (...extra: Parameters<typeof clsx>) =>
|
||||
clsx(classes.item, isBlue ? classes.blueItem : undefined, ...extra);
|
||||
|
||||
return (
|
||||
<Wrapper className={clsx(classes.wrapper, isBlue ? classes.blueWrapper : "rounded-full")}>
|
||||
<ItemSurface className={itemClass(classes.startItem)}>
|
||||
<motion.button
|
||||
type="button"
|
||||
whileTap={{ scale: 0.95 }}
|
||||
className={classes.button}
|
||||
onClick={() => {
|
||||
play("click");
|
||||
tg.hapticFeedback.click();
|
||||
onChange?.(1);
|
||||
}}
|
||||
disabled={isAtStart}
|
||||
>
|
||||
<StartIcon className="w-5 h-4.5" />
|
||||
</motion.button>
|
||||
</ItemSurface>
|
||||
|
||||
<ItemSurface className={itemClass()}>
|
||||
<motion.button
|
||||
type="button"
|
||||
whileTap={{ scale: 0.95 }}
|
||||
className={classes.button}
|
||||
onClick={() => {
|
||||
play("click");
|
||||
tg.hapticFeedback.click();
|
||||
onChange?.(value - 1);
|
||||
}}
|
||||
disabled={isAtStart}
|
||||
>
|
||||
<BackIcon className="w-4 h-5" />
|
||||
</motion.button>
|
||||
</ItemSurface>
|
||||
|
||||
<ItemSurface className={itemClass(classes.state)}>
|
||||
{value} {t("pagination.of")} {total}
|
||||
</ItemSurface>
|
||||
|
||||
<ItemSurface className={itemClass()}>
|
||||
<motion.button
|
||||
type="button"
|
||||
whileTap={{ scale: 0.95 }}
|
||||
className={classes.button}
|
||||
onClick={() => {
|
||||
play("click");
|
||||
tg.hapticFeedback.click();
|
||||
onChange?.(value + 1);
|
||||
}}
|
||||
disabled={isAtEnd}
|
||||
>
|
||||
<BackIcon className={clsx("w-4 h-5", classes.mirrored)} />
|
||||
</motion.button>
|
||||
</ItemSurface>
|
||||
|
||||
<ItemSurface className={itemClass(classes.endItem)}>
|
||||
<motion.button
|
||||
type="button"
|
||||
whileTap={{ scale: 0.95 }}
|
||||
className={classes.button}
|
||||
onClick={() => {
|
||||
play("click");
|
||||
tg.hapticFeedback.click();
|
||||
onChange?.(total);
|
||||
}}
|
||||
disabled={isAtEnd}
|
||||
>
|
||||
<StartIcon className={clsx("w-5 h-4.5", classes.mirrored)} />
|
||||
</motion.button>
|
||||
</ItemSurface>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
3
src/components/atoms/Pagination/assets/back.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="20" viewBox="0 0 16 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.4341 0.835938C13.8355 0.426426 13.086 0.362755 12.4019 0.804688V0.805664L1.61182 7.77344C0.889589 8.23986 0.50054 9.09777 0.500488 10C0.500488 10.9023 0.889512 11.7611 1.61182 12.2275L12.4019 19.1953C13.0867 19.6375 13.8357 19.5743 14.4341 19.165C15.0406 18.7501 15.5005 17.9663 15.5005 16.9668V3.0332C15.5005 2.03451 15.0407 1.2511 14.4341 0.835938Z" fill="#774923" stroke="#3F2814"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 502 B |
3
src/components/atoms/Pagination/assets/start.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="20" height="18" viewBox="0 0 20 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.36816 0.5H1.89453C1.71624 0.500029 1.53882 0.537515 1.37207 0.611328C1.20516 0.685217 1.05109 0.794386 0.919922 0.93457C0.788697 1.07482 0.683015 1.2432 0.610352 1.43066C0.537677 1.61818 0.5 1.82068 0.5 2.02539V15.9746C0.5 16.1793 0.537677 16.3818 0.610352 16.5693C0.683014 16.7568 0.788695 16.9252 0.919922 17.0654C1.05109 17.2056 1.20517 17.3148 1.37207 17.3887C1.53882 17.4625 1.71624 17.5 1.89453 17.5H3.36816C3.72683 17.5 4.07828 17.348 4.34277 17.0654C4.60835 16.7816 4.7627 16.3894 4.7627 15.9746V2.02539C4.7627 1.6106 4.60835 1.2184 4.34277 0.93457C4.07828 0.65202 3.72683 0.5 3.36816 0.5ZM18.9141 0.500977C18.8201 0.507575 18.7262 0.541698 18.6436 0.603516L8.11719 8.47852C8.04438 8.53306 7.98186 8.6082 7.9375 8.69922C7.89307 8.79047 7.86914 8.89398 7.86914 9C7.86914 9.10602 7.89307 9.20953 7.9375 9.30078C7.98186 9.3918 8.04438 9.46694 8.11719 9.52148L18.6436 17.3955C18.7262 17.4573 18.8201 17.4915 18.9141 17.498C19.0078 17.5045 19.1032 17.4841 19.1904 17.4355C19.2782 17.3867 19.3558 17.31 19.4121 17.2109C19.4684 17.1118 19.4999 16.9956 19.5 16.875V1.125L19.4941 1.03516C19.4827 0.946822 19.4544 0.862538 19.4121 0.788086C19.3559 0.689138 19.278 0.613272 19.1904 0.564453C19.1031 0.515936 19.0079 0.494413 18.9141 0.500977Z" fill="#774923" stroke="#3F2814"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
23
src/components/atoms/Pagination/icons/BackIcon.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { motion, type SVGMotionProps } from "motion/react";
|
||||
import clsx, { type ClassValue } from "clsx";
|
||||
|
||||
type Props = Omit<SVGMotionProps<SVGElement>, "className"> & {
|
||||
className?: ClassValue;
|
||||
};
|
||||
|
||||
export default function BackIcon(props: Props) {
|
||||
return (
|
||||
<motion.svg
|
||||
viewBox="0 0 16 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
className={clsx(props.className)}
|
||||
>
|
||||
<path
|
||||
d="M14.4341 0.835938C13.8355 0.426426 13.086 0.362755 12.4019 0.804688V0.805664L1.61182 7.77344C0.889589 8.23986 0.50054 9.09777 0.500488 10C0.500488 10.9023 0.889512 11.7611 1.61182 12.2275L12.4019 19.1953C13.0867 19.6375 13.8357 19.5743 14.4341 19.165C15.0406 18.7501 15.5005 17.9663 15.5005 16.9668V3.0332C15.5005 2.03451 15.0407 1.2511 14.4341 0.835938Z"
|
||||
style={{ fill: "var(--icon-fill, #774923)", stroke: "var(--icon-stroke, #3F2814)" }}
|
||||
/>
|
||||
</motion.svg>
|
||||
);
|
||||
}
|
||||
23
src/components/atoms/Pagination/icons/StartIcon.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { motion, type SVGMotionProps } from "motion/react";
|
||||
import clsx, { type ClassValue } from "clsx";
|
||||
|
||||
type Props = Omit<SVGMotionProps<SVGElement>, "className"> & {
|
||||
className?: ClassValue;
|
||||
};
|
||||
|
||||
export default function StartIcon(props: Props) {
|
||||
return (
|
||||
<motion.svg
|
||||
viewBox="0 0 20 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
className={clsx(props.className)}
|
||||
>
|
||||
<path
|
||||
d="M3.36816 0.5H1.89453C1.71624 0.500029 1.53882 0.537515 1.37207 0.611328C1.20516 0.685217 1.05109 0.794386 0.919922 0.93457C0.788697 1.07482 0.683015 1.2432 0.610352 1.43066C0.537677 1.61818 0.5 1.82068 0.5 2.02539V15.9746C0.5 16.1793 0.537677 16.3818 0.610352 16.5693C0.683014 16.7568 0.788695 16.9252 0.919922 17.0654C1.05109 17.2056 1.20517 17.3148 1.37207 17.3887C1.53882 17.4625 1.71624 17.5 1.89453 17.5H3.36816C3.72683 17.5 4.07828 17.348 4.34277 17.0654C4.60835 16.7816 4.7627 16.3894 4.7627 15.9746V2.02539C4.7627 1.6106 4.60835 1.2184 4.34277 0.93457C4.07828 0.65202 3.72683 0.5 3.36816 0.5ZM18.9141 0.500977C18.8201 0.507575 18.7262 0.541698 18.6436 0.603516L8.11719 8.47852C8.04438 8.53306 7.98186 8.6082 7.9375 8.69922C7.89307 8.79047 7.86914 8.89398 7.86914 9C7.86914 9.10602 7.89307 9.20953 7.9375 9.30078C7.98186 9.3918 8.04438 9.46694 8.11719 9.52148L18.6436 17.3955C18.7262 17.4573 18.8201 17.4915 18.9141 17.498C19.0078 17.5045 19.1032 17.4841 19.1904 17.4355C19.2782 17.3867 19.3558 17.31 19.4121 17.2109C19.4684 17.1118 19.4999 16.9956 19.5 16.875V1.125L19.4941 1.03516C19.4827 0.946822 19.4544 0.862538 19.4121 0.788086C19.3559 0.689138 19.278 0.613272 19.1904 0.564453C19.1031 0.515936 19.0079 0.494413 18.9141 0.500977Z"
|
||||
style={{ fill: "var(--icon-fill, #774923)", stroke: "var(--icon-stroke, #3F2814)" }}
|
||||
/>
|
||||
</motion.svg>
|
||||
);
|
||||
}
|
||||
1
src/components/atoms/Pagination/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from "./Pagination";
|
||||
@@ -4,6 +4,8 @@ import DarkSurface from "@components/surface/DarkSurface";
|
||||
import { motion, type HTMLMotionProps } from "motion/react";
|
||||
|
||||
import classes from "./TabSelector.module.css";
|
||||
import { useAudioStore } from "@/audio";
|
||||
import tg from "@/tg";
|
||||
|
||||
type Tab = {
|
||||
key: string;
|
||||
@@ -18,6 +20,8 @@ type Props = Omit<HTMLMotionProps<"div">, "className" | "onChange"> & {
|
||||
};
|
||||
|
||||
export default function TabSelector({ tabs, value, onChange, className, ...props }: Props) {
|
||||
const play = useAudioStore((s) => s.play);
|
||||
|
||||
const selectedIndex = value != null ? tabs.findIndex((tab) => tab.key === value) : -1;
|
||||
|
||||
return (
|
||||
@@ -29,7 +33,11 @@ export default function TabSelector({ tabs, value, onChange, className, ...props
|
||||
type="button"
|
||||
key={tab.key}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={() => onChange?.(tab.key)}
|
||||
onClick={() => {
|
||||
play("click");
|
||||
tg.hapticFeedback.click();
|
||||
onChange?.(tab.key);
|
||||
}}
|
||||
className={classes.tab}
|
||||
>
|
||||
{tab.title}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { motion, type HTMLMotionProps } from "motion/react";
|
||||
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 classes from "./NumberInput.module.css";
|
||||
import { useAudioStore } from "@/audio";
|
||||
import tg from "@/tg";
|
||||
|
||||
type Props = Omit<HTMLMotionProps<"input">, "className" | "type" | "onChange"> & {
|
||||
className?: ClassValue;
|
||||
@@ -27,8 +29,11 @@ export default function NumberInput({
|
||||
prefix,
|
||||
value,
|
||||
onChange,
|
||||
onFocus,
|
||||
...props
|
||||
}: Props) {
|
||||
const play = useAudioStore((s) => s.play);
|
||||
|
||||
const stableId = useId();
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const id = props.id ?? stableId;
|
||||
@@ -40,6 +45,16 @@ export default function NumberInput({
|
||||
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 normalized = filterNumericInput(e.target.value);
|
||||
setStrValue(normalized);
|
||||
@@ -61,12 +76,17 @@ export default function NumberInput({
|
||||
className={classes.input}
|
||||
value={strValue}
|
||||
onChange={handleChange}
|
||||
onFocus={handleFocus}
|
||||
/>
|
||||
</label>
|
||||
<motion.button
|
||||
whileTap={{ scale: 0.95 }}
|
||||
className={classes.iconButton}
|
||||
onClick={() => inputRef.current?.focus()}
|
||||
onClick={() => {
|
||||
play("click");
|
||||
tg.hapticFeedback.click();
|
||||
inputRef.current?.focus();
|
||||
}}
|
||||
>
|
||||
<KeyboardIcon className={classes.icon} />
|
||||
</motion.button>
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 50%;
|
||||
border-radius: 9999px;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import clsx, { type ClassValue } from "clsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import ContentSurface from "@components/surface/ContentSurface";
|
||||
import LightSurface from "@components/surface/LightSurface";
|
||||
import { motion, type HTMLMotionProps } from "motion/react";
|
||||
|
||||
import classes from "./SwitchInput.module.css";
|
||||
import tg from "@/tg";
|
||||
import { useRef } from "react";
|
||||
|
||||
type Props = Omit<HTMLMotionProps<"div">, "className" | "onChange"> & {
|
||||
value?: boolean | null;
|
||||
@@ -12,42 +15,40 @@ type Props = Omit<HTMLMotionProps<"div">, "className" | "onChange"> & {
|
||||
};
|
||||
|
||||
export default function SwitchInput({ value, onChange, className, ...props }: Props) {
|
||||
const selectedIndex = value != null ? (value ? 0 : 1) : -1;
|
||||
const { t } = useTranslation();
|
||||
const selectedIndex = value != null ? (value ? 0 : 1) : 0;
|
||||
const selectedIndexRef = useRef(selectedIndex);
|
||||
|
||||
return (
|
||||
<ContentSurface
|
||||
{...props}
|
||||
className={clsx(classes.container, className)}
|
||||
whileTap={{ scale: 1.1 }}
|
||||
onClick={() => {
|
||||
tg.hapticFeedback.click();
|
||||
onChange?.(!value);
|
||||
}}
|
||||
>
|
||||
<div className={classes.optionsContainer}>
|
||||
<div className={classes.options}>
|
||||
<motion.button
|
||||
type="button"
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={() => onChange?.(true)}
|
||||
className={clsx(classes.option, value === true && classes.selected)}
|
||||
>
|
||||
on
|
||||
{t("common.on")}
|
||||
</motion.button>
|
||||
<motion.button
|
||||
type="button"
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={() => onChange?.(false)}
|
||||
className={clsx(classes.option, value === false && classes.selected)}
|
||||
>
|
||||
off
|
||||
{t("common.off")}
|
||||
</motion.button>
|
||||
</div>
|
||||
{selectedIndex >= 0 && (
|
||||
<LightSurface
|
||||
className={classes.thumb}
|
||||
initial={{ scale: 0.5 }}
|
||||
animate={{
|
||||
left: `${selectedIndex * 50}%`,
|
||||
width: "50%",
|
||||
scale: 1,
|
||||
}}
|
||||
initial={{ left: `${selectedIndexRef.current * 50}%` }}
|
||||
animate={{ left: `${selectedIndex * 50}%` }}
|
||||
transition={{ type: "spring", stiffness: 500, damping: 35 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { motion, type HTMLMotionProps } from "motion/react";
|
||||
import clsx, { type ClassValue } from "clsx";
|
||||
import type { FocusEvent } from "react";
|
||||
|
||||
import classes from "./TextAreaInput.module.css";
|
||||
|
||||
@@ -8,10 +9,21 @@ type Props = Omit<HTMLMotionProps<"textarea">, "className"> & {
|
||||
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 (
|
||||
<motion.textarea
|
||||
{...props}
|
||||
onFocus={handleFocus}
|
||||
className={clsx(classes.input, error && classes.error, className)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { motion, type HTMLMotionProps } from "motion/react";
|
||||
import clsx, { type ClassValue } from "clsx";
|
||||
import type { FocusEvent } from "react";
|
||||
|
||||
import classes from "./TextInput.module.css";
|
||||
|
||||
@@ -8,11 +9,22 @@ type Props = Omit<HTMLMotionProps<"input">, "className"> & {
|
||||
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 (
|
||||
<motion.input
|
||||
{...props}
|
||||
type="text"
|
||||
onFocus={handleFocus}
|
||||
className={clsx(classes.input, error && classes.error, className)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -324,8 +324,8 @@ export default function HoneyIcon(props: Props) {
|
||||
y2="127.651"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#FFD844" />
|
||||
<stop offset="1" stop-color="#FDB922" />
|
||||
<stop stopColor="#FFD844" />
|
||||
<stop offset="1" stopColor="#FDB922" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint1_linear_227_12316"
|
||||
@@ -335,8 +335,8 @@ export default function HoneyIcon(props: Props) {
|
||||
y2="110.77"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#FFD844" />
|
||||
<stop offset="1" stop-color="#FDB922" />
|
||||
<stop stopColor="#FFD844" />
|
||||
<stop offset="1" stopColor="#FDB922" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint2_linear_227_12316"
|
||||
@@ -346,8 +346,8 @@ export default function HoneyIcon(props: Props) {
|
||||
y2="64.1507"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#FFD844" />
|
||||
<stop offset="1" stop-color="#FDB922" />
|
||||
<stop stopColor="#FFD844" />
|
||||
<stop offset="1" stopColor="#FDB922" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint3_linear_227_12316"
|
||||
@@ -357,8 +357,8 @@ export default function HoneyIcon(props: Props) {
|
||||
y2="154.721"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#CB5402" />
|
||||
<stop offset="1" stop-color="#F07D02" />
|
||||
<stop stopColor="#CB5402" />
|
||||
<stop offset="1" stopColor="#F07D02" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint4_linear_227_12316"
|
||||
@@ -368,8 +368,8 @@ export default function HoneyIcon(props: Props) {
|
||||
y2="100.601"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#CB5402" />
|
||||
<stop offset="1" stop-color="#F07D02" />
|
||||
<stop stopColor="#CB5402" />
|
||||
<stop offset="1" stopColor="#F07D02" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint5_linear_227_12316"
|
||||
@@ -379,8 +379,8 @@ export default function HoneyIcon(props: Props) {
|
||||
y2="63.6805"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#CB5402" />
|
||||
<stop offset="1" stop-color="#F07D02" />
|
||||
<stop stopColor="#CB5402" />
|
||||
<stop offset="1" stopColor="#F07D02" />
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
id="paint6_radial_227_12316"
|
||||
@@ -390,8 +390,8 @@ export default function HoneyIcon(props: Props) {
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(167.7 150.321) scale(59.48)"
|
||||
>
|
||||
<stop stop-color="#F37900" />
|
||||
<stop offset="1" stop-color="#FFCA13" />
|
||||
<stop stopColor="#F37900" />
|
||||
<stop offset="1" stopColor="#FFCA13" />
|
||||
</radialGradient>
|
||||
<radialGradient
|
||||
id="paint7_radial_227_12316"
|
||||
@@ -401,8 +401,8 @@ export default function HoneyIcon(props: Props) {
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(89.3202 89.8705) scale(69.51 69.51)"
|
||||
>
|
||||
<stop stop-color="#F37900" />
|
||||
<stop offset="1" stop-color="#FFCA13" />
|
||||
<stop stopColor="#F37900" />
|
||||
<stop offset="1" stopColor="#FFCA13" />
|
||||
</radialGradient>
|
||||
<radialGradient
|
||||
id="paint8_radial_227_12316"
|
||||
@@ -412,8 +412,8 @@ export default function HoneyIcon(props: Props) {
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(207.66 68.3504) scale(75.09 75.0899)"
|
||||
>
|
||||
<stop stop-color="#F37900" />
|
||||
<stop offset="1" stop-color="#FFCA13" />
|
||||
<stop stopColor="#F37900" />
|
||||
<stop offset="1" stopColor="#FFCA13" />
|
||||
</radialGradient>
|
||||
<linearGradient
|
||||
id="paint9_linear_227_12316"
|
||||
@@ -423,8 +423,8 @@ export default function HoneyIcon(props: Props) {
|
||||
y2="114.091"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#F5AD11" />
|
||||
<stop offset="1" stop-color="#CD7015" />
|
||||
<stop stopColor="#F5AD11" />
|
||||
<stop offset="1" stopColor="#CD7015" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint10_linear_227_12316"
|
||||
@@ -434,8 +434,8 @@ export default function HoneyIcon(props: Props) {
|
||||
y2="165.14"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#F5AD11" />
|
||||
<stop offset="1" stop-color="#CD7015" />
|
||||
<stop stopColor="#F5AD11" />
|
||||
<stop offset="1" stopColor="#CD7015" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint11_linear_227_12316"
|
||||
@@ -445,8 +445,8 @@ export default function HoneyIcon(props: Props) {
|
||||
y2="77.0305"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#F5AD11" />
|
||||
<stop offset="1" stop-color="#CD7015" />
|
||||
<stop stopColor="#F5AD11" />
|
||||
<stop offset="1" stopColor="#CD7015" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint12_linear_227_12316"
|
||||
@@ -456,8 +456,8 @@ export default function HoneyIcon(props: Props) {
|
||||
y2="153.36"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#CA6617" stop-opacity="0.4" />
|
||||
<stop offset="1" stop-color="#C36216" stop-opacity="0.6" />
|
||||
<stop stopColor="#CA6617" stopOpacity="0.4" />
|
||||
<stop offset="1" stopColor="#C36216" stopOpacity="0.6" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint13_linear_227_12316"
|
||||
@@ -467,8 +467,8 @@ export default function HoneyIcon(props: Props) {
|
||||
y2="116.26"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#CA6617" stop-opacity="0.4" />
|
||||
<stop offset="1" stop-color="#C36216" stop-opacity="0.6" />
|
||||
<stop stopColor="#CA6617" stopOpacity="0.4" />
|
||||
<stop offset="1" stopColor="#C36216" stopOpacity="0.6" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint14_linear_227_12316"
|
||||
@@ -478,8 +478,8 @@ export default function HoneyIcon(props: Props) {
|
||||
y2="79.4803"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0.08" stop-color="#FFFBF3" />
|
||||
<stop offset="1" stop-color="#FFFBF3" stop-opacity="0.4" />
|
||||
<stop offset="0.08" stopColor="#FFFBF3" />
|
||||
<stop offset="1" stopColor="#FFFBF3" stopOpacity="0.4" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint15_linear_227_12316"
|
||||
@@ -489,8 +489,8 @@ export default function HoneyIcon(props: Props) {
|
||||
y2="110.991"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0.08" stop-color="#FFFBF3" />
|
||||
<stop offset="1" stop-color="#FFFBF3" stop-opacity="0.4" />
|
||||
<stop offset="0.08" stopColor="#FFFBF3" />
|
||||
<stop offset="1" stopColor="#FFFBF3" stopOpacity="0.4" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint16_linear_227_12316"
|
||||
@@ -500,8 +500,8 @@ export default function HoneyIcon(props: Props) {
|
||||
y2="52.5508"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0.08" stop-color="#FFFBF3" />
|
||||
<stop offset="1" stop-color="#FFFBF3" stop-opacity="0.4" />
|
||||
<stop offset="0.08" stopColor="#FFFBF3" />
|
||||
<stop offset="1" stopColor="#FFFBF3" stopOpacity="0.4" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint17_linear_227_12316"
|
||||
@@ -511,8 +511,8 @@ export default function HoneyIcon(props: Props) {
|
||||
y2="23.7907"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0.08" stop-color="#FFFBF3" />
|
||||
<stop offset="1" stop-color="#FFFBF3" stop-opacity="0.4" />
|
||||
<stop offset="0.08" stopColor="#FFFBF3" />
|
||||
<stop offset="1" stopColor="#FFFBF3" stopOpacity="0.4" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint18_linear_227_12316"
|
||||
@@ -522,8 +522,8 @@ export default function HoneyIcon(props: Props) {
|
||||
y2="33.1406"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0.08" stop-color="#FFFBF3" />
|
||||
<stop offset="1" stop-color="#FFFBF3" stop-opacity="0.4" />
|
||||
<stop offset="0.08" stopColor="#FFFBF3" />
|
||||
<stop offset="1" stopColor="#FFFBF3" stopOpacity="0.4" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint19_linear_227_12316"
|
||||
@@ -533,8 +533,8 @@ export default function HoneyIcon(props: Props) {
|
||||
y2="70.1908"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0.08" stop-color="#FFFBF3" />
|
||||
<stop offset="1" stop-color="#FFFBF3" stop-opacity="0.4" />
|
||||
<stop offset="0.08" stopColor="#FFFBF3" />
|
||||
<stop offset="1" stopColor="#FFFBF3" stopOpacity="0.4" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint20_linear_227_12316"
|
||||
@@ -544,8 +544,8 @@ export default function HoneyIcon(props: Props) {
|
||||
y2="75.8007"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0.08" stop-color="#FFFBF3" />
|
||||
<stop offset="1" stop-color="#FFFBF3" stop-opacity="0.4" />
|
||||
<stop offset="0.08" stopColor="#FFFBF3" />
|
||||
<stop offset="1" stopColor="#FFFBF3" stopOpacity="0.4" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint21_linear_227_12316"
|
||||
@@ -555,8 +555,8 @@ export default function HoneyIcon(props: Props) {
|
||||
y2="70.1004"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0.08" stop-color="#FFFBF3" />
|
||||
<stop offset="1" stop-color="#FFFBF3" stop-opacity="0.4" />
|
||||
<stop offset="0.08" stopColor="#FFFBF3" />
|
||||
<stop offset="1" stopColor="#FFFBF3" stopOpacity="0.4" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</motion.svg>
|
||||
|
||||
96
src/components/lift/LiftContext.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import { createContext, useCallback, useContext, useRef, useState } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
import { cloneWithoutAnimations } from "./Liftable";
|
||||
|
||||
type LiftContextValue = {
|
||||
liftedIds: Set<string>;
|
||||
alwaysLiftedIds: Set<string>;
|
||||
modalOpen: boolean;
|
||||
setLiftedIds: (ids: string[]) => void;
|
||||
registerAlways: (id: string) => void;
|
||||
unregisterAlways: (id: string) => void;
|
||||
registerModal: () => void;
|
||||
beginModalClose: () => void;
|
||||
endModalClose: () => void;
|
||||
portalContainer: HTMLElement | null;
|
||||
setPortalContainer: (el: HTMLElement | null) => void;
|
||||
setCloneContainer: (el: HTMLElement | null) => void;
|
||||
};
|
||||
|
||||
const LiftContext = createContext<LiftContextValue | null>(null);
|
||||
|
||||
export function useLift(): LiftContextValue {
|
||||
const ctx = useContext(LiftContext);
|
||||
if (!ctx) throw new Error("useLift must be used within LiftProvider");
|
||||
return ctx;
|
||||
}
|
||||
|
||||
export function LiftProvider({ children }: { children: ReactNode }) {
|
||||
const [liftedIds, setLiftedIdsRaw] = 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 alwaysRef = useRef<Set<string>>(new Set());
|
||||
const modalCountRef = useRef(0);
|
||||
const cloneContainerRef = useRef<HTMLElement | null>(null);
|
||||
|
||||
const setLiftedIds = useCallback((ids: string[]) => {
|
||||
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) => {
|
||||
alwaysRef.current.add(id);
|
||||
setAlwaysLiftedIds(new Set(alwaysRef.current));
|
||||
}, []);
|
||||
|
||||
const unregisterAlways = useCallback((id: string) => {
|
||||
alwaysRef.current.delete(id);
|
||||
setAlwaysLiftedIds(new Set(alwaysRef.current));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<LiftContext
|
||||
value={{
|
||||
liftedIds,
|
||||
alwaysLiftedIds,
|
||||
modalOpen,
|
||||
setLiftedIds,
|
||||
registerAlways,
|
||||
unregisterAlways,
|
||||
registerModal,
|
||||
beginModalClose,
|
||||
endModalClose,
|
||||
portalContainer,
|
||||
setPortalContainer,
|
||||
setCloneContainer,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</LiftContext>
|
||||
);
|
||||
}
|
||||
8
src/components/lift/LiftLayer.module.css
Normal file
@@ -0,0 +1,8 @@
|
||||
@layer base {
|
||||
.liftLayer {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 103;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
28
src/components/lift/LiftLayer.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { useCallback } from "react";
|
||||
import { useLift } from "./LiftContext";
|
||||
import classes from "./LiftLayer.module.css";
|
||||
|
||||
export function LiftLayer() {
|
||||
const { setPortalContainer, setCloneContainer } = useLift();
|
||||
|
||||
const portalRef = useCallback(
|
||||
(node: HTMLDivElement | null) => {
|
||||
setPortalContainer(node);
|
||||
},
|
||||
[setPortalContainer],
|
||||
);
|
||||
|
||||
const cloneRef = useCallback(
|
||||
(node: HTMLDivElement | null) => {
|
||||
setCloneContainer(node);
|
||||
},
|
||||
[setCloneContainer],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div ref={portalRef} className={classes.liftLayer} />
|
||||
<div ref={cloneRef} className={classes.liftLayer} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
145
src/components/lift/Liftable.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
import { useEffect, useId, useLayoutEffect, useRef } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
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 = {
|
||||
id?: string;
|
||||
always?: boolean;
|
||||
};
|
||||
|
||||
type Props = LiftableProps & {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export function useLiftable<T extends Record<string, unknown> = Record<string, never>>(
|
||||
render: (props: { isLifted: boolean } & T) => ReactNode,
|
||||
options?: LiftableProps & T,
|
||||
): { id: string; element: ReactNode } {
|
||||
const autoId = useId();
|
||||
const {
|
||||
id: idProp,
|
||||
always,
|
||||
...extraProps
|
||||
} = (options ?? {}) as LiftableProps & Record<string, unknown>;
|
||||
const id = idProp ?? autoId;
|
||||
const { liftedIds, alwaysLiftedIds, modalOpen, registerAlways, unregisterAlways } = useLift();
|
||||
|
||||
useEffect(() => {
|
||||
if (always) {
|
||||
registerAlways(id);
|
||||
return () => unregisterAlways(id);
|
||||
}
|
||||
}, [always, id, registerAlways, unregisterAlways]);
|
||||
|
||||
const isLifted = liftedIds.has(id) || (alwaysLiftedIds.has(id) && modalOpen);
|
||||
|
||||
const element = (
|
||||
<Liftable id={id}>{render({ isLifted, ...extraProps } as { isLifted: boolean } & T)}</Liftable>
|
||||
);
|
||||
|
||||
return { id, element };
|
||||
}
|
||||
|
||||
export { cloneWithoutAnimations };
|
||||
|
||||
export function Liftable({ id: idProp, always, children }: Props) {
|
||||
const autoId = useId();
|
||||
const id = idProp ?? autoId;
|
||||
const {
|
||||
liftedIds,
|
||||
alwaysLiftedIds,
|
||||
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(() => {
|
||||
if (always) {
|
||||
registerAlways(id);
|
||||
return () => unregisterAlways(id);
|
||||
}
|
||||
}, [always, id, registerAlways, unregisterAlways]);
|
||||
|
||||
const isLifted = liftedIds.has(id) || (alwaysLiftedIds.has(id) && modalOpen);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const host = childrenHostRef.current;
|
||||
const inlineSlot = inlineSlotRef.current;
|
||||
const portalWrapper = portalWrapperRef.current;
|
||||
if (!host || !inlineSlot || !portalWrapper) return;
|
||||
|
||||
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]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLifted || !inlineSlotRef.current || !portalWrapperRef.current) return;
|
||||
const portalWrapper = portalWrapperRef.current;
|
||||
const inlineSlot = inlineSlotRef.current;
|
||||
|
||||
const measure = () => {
|
||||
const rect = inlineSlot.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);
|
||||
observer.observe(inlineSlot);
|
||||
window.addEventListener("resize", measure);
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
window.removeEventListener("resize", measure);
|
||||
};
|
||||
}, [isLifted]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div ref={inlineSlotRef}>
|
||||
<div ref={childrenHostRef}>{children}</div>
|
||||
</div>
|
||||
{portalContainer &&
|
||||
createPortal(<div ref={portalWrapperRef} style={{ display: "none" }} />, portalContainer)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
3
src/components/lift/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { LiftProvider, useLift } from "./LiftContext";
|
||||
export { LiftLayer } from "./LiftLayer";
|
||||
export { Liftable, useLiftable } from "./Liftable";
|
||||
@@ -1,29 +1,11 @@
|
||||
@layer base {
|
||||
.overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 100;
|
||||
}
|
||||
.content {
|
||||
border-radius: 22px;
|
||||
|
||||
.modal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
max-width: 320px;
|
||||
padding: 13px;
|
||||
|
||||
.content {
|
||||
border-radius: 22px;
|
||||
|
||||
.description {
|
||||
padding: 12px;
|
||||
border-radius: 18px;
|
||||
text-align: center;
|
||||
}
|
||||
.description {
|
||||
padding: 12px;
|
||||
border-radius: 18px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import SectionSurface from "../../surface/SectionSurface/SectionSurface";
|
||||
import Modal from "../Modal/Modal";
|
||||
import ContentSurface from "../../surface/ContentSurface/ContentSurface";
|
||||
import LightSurface from "../../surface/LightSurface/LightSurface";
|
||||
import Button from "../../atoms/Button/Button";
|
||||
@@ -27,36 +26,20 @@ export default function ActionModal({ open, description, onClose, onConfirm, con
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{open && (
|
||||
<motion.div
|
||||
className={classes.overlay}
|
||||
initial={{ backdropFilter: "blur(0px)" }}
|
||||
animate={{ backdropFilter: "blur(8px)" }}
|
||||
exit={{ backdropFilter: "blur(0px)" }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<SectionSurface
|
||||
className={classes.modal}
|
||||
exit={{ scale: 0 }}
|
||||
transition={{ duration: 0.2, type: "spring" }}
|
||||
>
|
||||
<ContentSurface className={classes.content}>
|
||||
<LightSurface className={classes.description}>{description}</LightSurface>
|
||||
</ContentSurface>
|
||||
<div className={classes.buttons}>
|
||||
<Button variant="blue" onClick={onClose} className={classes.button}>
|
||||
{t("actionModal.close")}
|
||||
</Button>
|
||||
{onConfirm != null && (
|
||||
<Button variant="green" onClick={onConfirm} className={classes.button}>
|
||||
{confirmText}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</SectionSurface>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<ContentSurface className={classes.content}>
|
||||
<LightSurface className={classes.description}>{description}</LightSurface>
|
||||
</ContentSurface>
|
||||
<div className={classes.buttons}>
|
||||
<Button variant="blue" onClick={onClose} className={classes.button}>
|
||||
{t("actionModal.close")}
|
||||
</Button>
|
||||
{onConfirm != null && (
|
||||
<Button variant="green" onClick={onConfirm} className={classes.button}>
|
||||
{confirmText}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
47
src/components/modals/Modal/Modal.module.css
Normal file
@@ -0,0 +1,47 @@
|
||||
@layer base {
|
||||
.overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.modalContainer {
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 102;
|
||||
}
|
||||
|
||||
.modalInner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-width: 320px;
|
||||
max-height: calc(
|
||||
var(--safe-area-height) - var(--header-total) - var(--navigation-total) - 70px
|
||||
);
|
||||
}
|
||||
|
||||
.modal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
padding: 13px;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: #ffffff;
|
||||
-webkit-text-stroke: 0.7px #331b01;
|
||||
font-weight: 700;
|
||||
font-size: 24px;
|
||||
margin-bottom: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
95
src/components/modals/Modal/Modal.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
|
||||
import SectionSurface from "@components/surface/SectionSurface";
|
||||
import { useLift } from "@components/lift";
|
||||
import classes from "./Modal.module.css";
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
children: ReactNode;
|
||||
onClose: () => void;
|
||||
liftIds?: string[];
|
||||
title?: ReactNode;
|
||||
className?: ClassValue;
|
||||
};
|
||||
|
||||
export default function Modal({ open, children, onClose, liftIds, title, className }: Props) {
|
||||
const { setLiftedIds, registerModal, beginModalClose, endModalClose } = useLift();
|
||||
const prevLiftIdsRef = useRef<string>("");
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
registerModal();
|
||||
return () => {
|
||||
beginModalClose();
|
||||
};
|
||||
}
|
||||
}, [open, registerModal, beginModalClose]);
|
||||
|
||||
useEffect(() => {
|
||||
const key = liftIds?.join(",") ?? "";
|
||||
|
||||
if (open && key) {
|
||||
if (key !== prevLiftIdsRef.current) {
|
||||
prevLiftIdsRef.current = key;
|
||||
setLiftedIds(liftIds!);
|
||||
}
|
||||
}
|
||||
|
||||
if (!open && prevLiftIdsRef.current) {
|
||||
prevLiftIdsRef.current = "";
|
||||
setLiftedIds([]);
|
||||
}
|
||||
}, [open, liftIds, setLiftedIds]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (prevLiftIdsRef.current) {
|
||||
setLiftedIds([]);
|
||||
}
|
||||
};
|
||||
}, [setLiftedIds]);
|
||||
|
||||
return (
|
||||
<AnimatePresence onExitComplete={endModalClose}>
|
||||
{open && (
|
||||
<>
|
||||
<motion.div
|
||||
className={classes.overlay}
|
||||
initial={{ backdropFilter: "blur(0px)" }}
|
||||
animate={{ backdropFilter: "blur(8px)" }}
|
||||
exit={{ backdropFilter: "blur(0px)" }}
|
||||
transition={{ duration: 0.2 }}
|
||||
onClick={onClose}
|
||||
/>
|
||||
<motion.div
|
||||
className={classes.modalContainer}
|
||||
initial={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
onClick={onClose}
|
||||
>
|
||||
{/* oxlint-disable-next-line jsx_a11y/no-static-element-interactions*/}
|
||||
<div
|
||||
className={classes.modalInner}
|
||||
// oxlint-disable-next-line jsx_a11y/click-events-have-key-events
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{title && <div className={classes.title}>{title}</div>}
|
||||
<SectionSurface
|
||||
className={clsx(classes.modal, className)}
|
||||
exit={{ scale: 0 }}
|
||||
transition={{ duration: 0.2, type: "spring" }}
|
||||
>
|
||||
{children}
|
||||
</SectionSurface>
|
||||
</div>
|
||||
</motion.div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
1
src/components/modals/Modal/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from "./Modal";
|
||||
@@ -1,5 +1,3 @@
|
||||
@reference "@/index.css";
|
||||
|
||||
@layer base {
|
||||
.blueSectionSurface {
|
||||
background: linear-gradient(180deg, #278789 0%, #206f66 100%);
|
||||
|
||||
8
src/helpers/dom.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export const prefetch = (url: string | null | undefined) => {
|
||||
if (!url) return;
|
||||
const link = document.createElement("link");
|
||||
link.rel = "prefetch";
|
||||
link.href = url;
|
||||
link.onload = () => link.remove();
|
||||
document.head.appendChild(link);
|
||||
};
|
||||
42
src/i18n/assets/br.svg
Normal file
|
After Width: | Height: | Size: 15 KiB |
13
src/i18n/assets/de.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<svg width="31" height="31" viewBox="0 0 31 31" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_228_3393)">
|
||||
<path d="M0.0117188 26.1301C0.0117188 28.6701 2.06172 30.7202 4.60172 30.7202H26.4217C28.9517 30.7202 31.0117 28.6701 31.0117 26.1301V20.3901H0.0117188V26.1301Z" fill="#FFCE00"/>
|
||||
<path d="M31.0117 4.31021C31.0117 1.78021 28.9617 -0.279785 26.4217 -0.279785H4.60172C2.07172 -0.279785 0.0117188 1.77021 0.0117188 4.31021V10.0502H31.0117V4.31021Z" fill="black"/>
|
||||
<path d="M31.0117 10.0503H0.0117188V20.3803H31.0117V10.0503Z" fill="#DD0000"/>
|
||||
</g>
|
||||
<rect x="0.5" y="0.5" width="30" height="30" rx="15" stroke="#432710"/>
|
||||
<defs>
|
||||
<clipPath id="clip0_228_3393">
|
||||
<rect width="31" height="31" rx="15.5" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 768 B |
288
src/i18n/assets/es.svg
Normal file
|
After Width: | Height: | Size: 168 KiB |
13
src/i18n/assets/fr.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<svg width="31" height="31" viewBox="0 0 31 31" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_228_3424)">
|
||||
<path d="M4.60172 -0.200195C2.07172 -0.200195 0.0117188 1.84983 0.0117188 4.38983V26.2098C0.0117188 28.7398 2.06172 30.7998 4.60172 30.7998H8.68172V-0.200195H4.60172Z" fill="#00337E"/>
|
||||
<path d="M26.4213 -0.200195H22.3413V30.7998H26.4213C28.9513 30.7998 31.0113 28.7498 31.0113 26.2098V4.38983C31.0113 1.85983 28.9613 -0.200195 26.4213 -0.200195Z" fill="#E3001B"/>
|
||||
<path d="M22.3441 -0.200195H8.68408V30.7998H22.3441V-0.200195Z" fill="white"/>
|
||||
</g>
|
||||
<rect x="0.5" y="0.5" width="30" height="30" rx="15" stroke="#432710"/>
|
||||
<defs>
|
||||
<clipPath id="clip0_228_3424">
|
||||
<rect width="31" height="31" rx="15.5" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 772 B |
27
src/i18n/assets/gb.svg
Normal file
@@ -0,0 +1,27 @@
|
||||
<svg width="31" height="31" viewBox="0 0 31 31" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_228_3029)">
|
||||
<path d="M30.991 12.2061V9.94313H29.1222C29.2122 9.76289 29.3421 9.72284 29.472 9.63273L29.8318 9.36239L30.2015 9.09202L30.6013 8.81166L30.991 8.5313V4.54618L25.4046 8.6014L23.5458 9.95315H20.4378C20.4179 9.83299 20.4079 9.65276 20.4478 9.57265C20.4878 9.50256 20.6177 9.4425 20.7077 9.37241L21.0674 9.10204L21.4372 8.83171L21.8869 8.51129L22.3366 8.18087L30.4514 2.28325C30.2815 1.96283 30.0816 1.66245 29.8418 1.38209L20.4079 8.24095V-0.209961H18.5391V12.2161H30.981L30.991 12.2061Z" fill="#F5F5F5"/>
|
||||
<path d="M12.9012 -0.22998H11.0224V8.22093L6.13558 4.67638L1.31869 1.17185C0.549189 1.96287 0.0695011 3.04426 0.0295269 4.22578L5.83578 8.44121L6.37543 8.82171L7.88446 9.93316H4.26679L2.40799 8.5914L0.0195312 6.85919V8.20091L0.0595064 8.23095L0.409279 8.4913L0.898965 8.84172C1.39864 9.20219 1.86834 9.52261 2.388 9.94315H0.0195312V12.2161H12.9012V-0.209935V-0.22998Z" fill="#F5F5F5"/>
|
||||
<path d="M27.1813 21.2976L30.9889 24.0712V22.7294L29.0401 21.3176H30.9889V19.0347H18.5469V30.7598H20.4157V23.0699C20.5956 23.14 20.6855 23.2201 20.7954 23.3102L21.1652 23.5805L21.5549 23.8609L21.9747 24.1613L29.24 29.4381L29.4299 29.5783C30.2493 28.8473 30.799 27.826 30.9289 26.6745L23.5536 21.3076H27.1613L27.1813 21.2976Z" fill="#F5F5F5"/>
|
||||
<path d="M0.00999569 19.0347V21.3076H2.37847C2.1786 21.4978 2.03869 21.588 1.84881 21.7281L0.00999569 23.0599V26.1639C0.00999569 26.4442 0.0399744 26.7246 0.0899421 26.9949L7.47518 21.618L7.93488 21.2976H10.9929C11.0029 21.4377 11.0029 21.5779 10.9929 21.7081L5.66635 25.5831L0.979368 28.9875C1.20922 29.2779 1.46905 29.5382 1.75887 29.7685L2.17859 29.4681L10.1934 23.6406L10.6531 23.3102C10.7631 23.2201 10.873 23.14 11.0129 23.0899V30.7698H12.8917V19.0447H0L0.00999569 19.0347Z" fill="#F5F5F5"/>
|
||||
<path d="M30.9917 19.0349V12.2061L18.5498 12.1961V-0.22998H12.9034V12.1961L0.0117188 12.2061V19.0349H12.8934L12.9034 30.76H18.5498V19.0349H30.9917Z" fill="#E6283F"/>
|
||||
<path d="M21.8884 8.49122L21.4387 8.81161L21.0689 9.08198L20.7092 9.35232C20.6192 9.42241 20.4893 9.4825 20.4493 9.55259C20.3994 9.63269 20.4194 9.81293 20.4393 9.93309H23.5473L25.4061 8.58134L30.9925 4.52608V4.3659C30.9925 3.60492 30.7927 2.894 30.4629 2.26318L22.3481 8.16078L21.8984 8.49122H21.8884Z" fill="#E6283F"/>
|
||||
<path d="M30.9894 24.0812L27.1819 21.3076H23.5742L30.9495 26.6846C30.9694 26.5143 30.9994 26.3441 30.9994 26.1639V24.0812H30.9894Z" fill="#E6283F"/>
|
||||
<path d="M30.9878 22.7297V21.3179H29.0391L30.9878 22.7297Z" fill="#21318A"/>
|
||||
<path d="M30.2004 9.08221L29.8306 9.35254L29.4709 9.62291C29.341 9.71303 29.211 9.75308 29.1211 9.93331H30.9899V8.52148L30.6001 8.80184L30.2004 9.08221Z" fill="#21318A"/>
|
||||
<path d="M11.0045 21.7081C11.0145 21.578 11.0145 21.4378 11.0045 21.3076H7.9465L7.4868 21.628L0.101562 27.005C0.241472 27.7459 0.561264 28.4168 1.00098 28.9875L5.68796 25.5831L11.0145 21.7081H11.0045Z" fill="#E6283F"/>
|
||||
<path d="M1.85053 21.7282C2.04041 21.588 2.18032 21.4979 2.38019 21.3076H0.0117188V23.0699L1.85053 21.7382V21.7282Z" fill="#21318A"/>
|
||||
<path d="M29.8519 1.3621C29.0124 0.390848 27.7832 -0.22998 26.4041 -0.22998H20.418V8.22093L29.8519 1.3621Z" fill="#21318A"/>
|
||||
<path d="M11.0241 8.22093V-0.22998H4.5982C3.30903 -0.22998 2.14978 0.310732 1.32031 1.16183L6.1372 4.66635L11.0241 8.21094V8.22093Z" fill="#21318A"/>
|
||||
<path d="M21.9887 24.1617L21.569 23.8613L21.1792 23.581L20.8094 23.3106C20.6995 23.2205 20.5996 23.1404 20.4297 23.0703V30.7602H26.4158C27.5851 30.7602 28.6444 30.3096 29.4539 29.5887L29.264 29.4485L21.9987 24.1717L21.9887 24.1617Z" fill="#21318A"/>
|
||||
<path d="M10.6638 23.3004L10.2041 23.6308L2.18926 29.4583L1.76953 29.7587C2.54903 30.3795 3.5284 30.76 4.59771 30.76H11.0236V23.0801C10.8737 23.1301 10.7737 23.2102 10.6638 23.3004Z" fill="#21318A"/>
|
||||
<path d="M4.25898 9.93296H7.87664L6.36762 8.82151L5.82796 8.44102L0.0217106 4.22559C0.0217106 4.22559 0.0117188 4.32571 0.0117188 4.37577V6.85899L2.40018 8.59121L4.25898 9.94296V9.93296Z" fill="#E6283F"/>
|
||||
<path d="M0.891149 8.82148L0.401467 8.47102L0.0516901 8.2107L0.0117188 8.18066V9.9229H2.38019C1.85053 9.49235 1.39083 9.17193 0.891149 8.82148Z" fill="#21318A"/>
|
||||
</g>
|
||||
<rect x="0.5" y="0.5" width="30" height="30" rx="15" stroke="#432710"/>
|
||||
<defs>
|
||||
<clipPath id="clip0_228_3029">
|
||||
<rect width="31" height="31" rx="15.5" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.3 KiB |
12
src/i18n/assets/id.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<svg width="31" height="31" viewBox="0 0 31 31" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_228_3432)">
|
||||
<path d="M0.0117188 26.2401C0.0117188 28.7701 2.06172 30.8301 4.60172 30.8301H26.4217C28.9517 30.8301 31.0117 28.7701 31.0117 26.2401V15.3301H0.0117188V26.2401Z" fill="white"/>
|
||||
<path d="M31.0117 15.3301V4.4201C31.0117 1.89011 28.9617 -0.169922 26.4217 -0.169922H4.60172C2.07172 -0.169922 0.0117188 1.88011 0.0117188 4.4201V15.3301H31.0117Z" fill="#DA0630"/>
|
||||
</g>
|
||||
<rect x="0.5" y="0.5" width="30" height="30" rx="15" stroke="#432710"/>
|
||||
<defs>
|
||||
<clipPath id="clip0_228_3432">
|
||||
<rect width="31" height="31" rx="15.5" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 687 B |
13
src/i18n/assets/it.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<svg width="31" height="31" viewBox="0 0 31 31" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_228_3408)">
|
||||
<path d="M4.60172 -0.109863C2.07172 -0.109863 0.0117188 1.94013 0.0117188 4.48013V26.3001C0.0117188 28.8301 2.06172 30.8901 4.60172 30.8901H8.68172V-0.109863H4.60172Z" fill="#009246"/>
|
||||
<path d="M26.4213 -0.109863H22.3413V30.8901H26.4213C28.9513 30.8901 31.0113 28.8401 31.0113 26.3001V4.48013C31.0113 1.95013 28.9613 -0.109863 26.4213 -0.109863Z" fill="#CE2B37"/>
|
||||
<path d="M22.3441 -0.109863H8.68408V30.8901H22.3441V-0.109863Z" fill="white"/>
|
||||
</g>
|
||||
<rect x="0.5" y="0.5" width="30" height="30" rx="15" stroke="#432710"/>
|
||||
<defs>
|
||||
<clipPath id="clip0_228_3408">
|
||||
<rect width="31" height="31" rx="15.5" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 772 B |
13
src/i18n/assets/nl.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<svg width="31" height="31" viewBox="0 0 31 31" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_228_3416)">
|
||||
<path d="M0.0117188 26.3201C0.0117188 28.8601 2.06172 30.9101 4.60172 30.9101H26.4217C28.9517 30.9101 31.0117 28.8601 31.0117 26.3201V20.5801H0.0117188V26.3201Z" fill="#21468B"/>
|
||||
<path d="M31.0117 4.50015C31.0117 1.97015 28.9617 -0.0898438 26.4217 -0.0898438H4.60172C2.07172 -0.0898438 0.0117188 1.96015 0.0117188 4.50015V10.2402H31.0117V4.50015Z" fill="#AE1C28"/>
|
||||
<path d="M31.0117 10.25H0.0117188V20.58H31.0117V10.25Z" fill="white"/>
|
||||
</g>
|
||||
<rect x="0.5" y="0.5" width="30" height="30" rx="15" stroke="#432710"/>
|
||||
<defs>
|
||||
<clipPath id="clip0_228_3416">
|
||||
<rect width="31" height="31" rx="15.5" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 765 B |
12
src/i18n/assets/pl.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<svg width="31" height="31" viewBox="0 0 31 31" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_228_3401)">
|
||||
<path d="M31.0117 4.52994C31.0117 1.99994 28.9617 -0.0600586 26.4217 -0.0600586H4.60172C2.07172 -0.0600586 0.0117188 1.99994 0.0117188 4.52994V15.4399H31.0117V4.52994Z" fill="white"/>
|
||||
<path d="M0.0117188 15.4399V26.3499C0.0117188 28.8799 2.06172 30.9399 4.60172 30.9399H26.4217C28.9517 30.9399 31.0117 28.8899 31.0117 26.3499V15.4399H0.0117188Z" fill="#DA0630"/>
|
||||
</g>
|
||||
<rect x="0.5" y="0.5" width="30" height="30" rx="15" stroke="#432710"/>
|
||||
<defs>
|
||||
<clipPath id="clip0_228_3401">
|
||||
<rect width="31" height="31" rx="15.5" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 692 B |
13
src/i18n/assets/ru.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<svg width="31" height="31" viewBox="0 0 31 31" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_228_3051)">
|
||||
<path d="M0.0117188 26.3797C0.0117188 28.9197 2.06172 30.9697 4.60172 30.9697H26.4217C28.9517 30.9697 31.0117 28.9197 31.0117 26.3797V20.6396H0.0117188V26.3797Z" fill="#D52B1E"/>
|
||||
<path d="M31.0117 4.55972C31.0117 2.02972 28.9617 -0.0302734 26.4217 -0.0302734H4.60172C2.07172 -0.0302734 0.0117188 2.01972 0.0117188 4.55972V10.2997H31.0117V4.55972Z" fill="white"/>
|
||||
<path d="M31.0117 10.2998H0.0117188V20.6298H31.0117V10.2998Z" fill="#0039A6"/>
|
||||
</g>
|
||||
<rect x="0.5" y="0.5" width="30" height="30" rx="15" stroke="#432710"/>
|
||||
<defs>
|
||||
<clipPath id="clip0_228_3051">
|
||||
<rect width="31" height="31" rx="15.5" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 771 B |
13
src/i18n/assets/tr.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<svg width="31" height="31" viewBox="0 0 31 31" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_228_3384)">
|
||||
<path d="M26.4217 0H4.60172C2.06673 0 0.0117188 2.05501 0.0117188 4.59V26.41C0.0117188 28.945 2.06673 31 4.60172 31H26.4217C28.9567 31 31.0117 28.945 31.0117 26.41V4.59C31.0117 2.05501 28.9567 0 26.4217 0Z" fill="#E30A17"/>
|
||||
<path d="M17.1402 21.2898C17.8002 20.9298 18.2102 20.4098 18.8002 20.0398C18.4602 20.4798 18.1902 20.9198 17.8102 21.3098C14.5002 24.6598 8.7402 24.5698 5.6202 21.0598C4.8102 20.1498 4.19019 19.1298 3.85019 17.9898C3.09019 15.4398 3.47019 13.1798 4.85019 10.9398C5.61019 9.71982 6.74019 8.90982 7.99019 8.15982C10.0302 6.93982 13.6002 6.91982 15.6402 8.15982C16.8802 8.91982 18.0602 9.69982 18.7902 10.9898C16.0202 8.22982 11.3202 8.27982 8.83019 11.2798C7.22019 13.2098 6.86019 15.6598 7.74019 17.9798C8.24019 19.2898 9.19019 20.3198 10.4502 21.1398C12.3602 22.3898 15.0702 22.4398 17.1402 21.2898Z" fill="white"/>
|
||||
<path d="M24.8894 16.8599L24.1394 17.9199C23.8594 18.3199 23.5994 18.6599 23.2494 19.0999L23.2194 16.3499L20.6094 15.4899L22.9394 14.7299C23.0494 14.6999 23.2094 14.4899 23.2294 14.4099L23.2594 11.8999L24.8794 14.0299L27.4394 13.2499C27.2494 13.6699 26.9794 13.9699 26.7194 14.3499L25.9194 15.5099C26.1094 15.7599 26.2994 16.0099 26.4894 16.2699C26.7294 16.5899 26.9694 16.9099 27.2094 17.2299C27.3194 17.3699 27.4694 17.4799 27.5394 17.6499C27.3894 17.6799 27.2494 17.6299 27.1094 17.5899C26.9494 17.5499 26.7994 17.4999 26.6494 17.4499C26.0594 17.2599 25.4694 17.0699 24.8694 16.8799L24.8894 16.8599Z" fill="white"/>
|
||||
</g>
|
||||
<rect x="0.5" y="0.5" width="30" height="30" rx="15" stroke="#432710"/>
|
||||
<defs>
|
||||
<clipPath id="clip0_228_3384">
|
||||
<rect width="31" height="31" rx="15.5" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
4
src/i18n/index.d.ts
vendored
@@ -1,9 +1,11 @@
|
||||
import "i18next";
|
||||
|
||||
import type { resources } from "../../public/locales/en.d.ts";
|
||||
import resources from "./resources";
|
||||
|
||||
declare module "i18next" {
|
||||
interface CustomTypeOptions {
|
||||
resources: { translation: typeof resources };
|
||||
strictKeyChecks: true;
|
||||
enableSelector: true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,10 +6,9 @@ declare const __LANGS__: string[];
|
||||
declare const __LOCALES_PATH__: string;
|
||||
declare const __DEFAULT_LANG__: string;
|
||||
|
||||
export const languages = __LANGS__.map((key) => ({
|
||||
key,
|
||||
label: key.toUpperCase(),
|
||||
}));
|
||||
export type { Language } from "./languages";
|
||||
export { languages } from "./languages";
|
||||
export { useLanguages } from "./useLanguages";
|
||||
|
||||
export default {
|
||||
init() {
|
||||
|
||||
31
src/i18n/languages.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import gbSvg from "./assets/gb.svg";
|
||||
import ruSvg from "./assets/ru.svg";
|
||||
import brSvg from "./assets/br.svg";
|
||||
import esSvg from "./assets/es.svg";
|
||||
import trSvg from "./assets/tr.svg";
|
||||
import deSvg from "./assets/de.svg";
|
||||
import plSvg from "./assets/pl.svg";
|
||||
import itSvg from "./assets/it.svg";
|
||||
import nlSvg from "./assets/nl.svg";
|
||||
import frSvg from "./assets/fr.svg";
|
||||
import idSvg from "./assets/id.svg";
|
||||
|
||||
export type Language = {
|
||||
key: string;
|
||||
label: string;
|
||||
image: string;
|
||||
};
|
||||
|
||||
export const languages: Language[] = [
|
||||
{ key: "en", label: "English", image: gbSvg },
|
||||
{ key: "ru", label: "Русский", image: ruSvg },
|
||||
{ key: "pt", label: "Português", image: brSvg },
|
||||
{ key: "es", label: "Español", image: esSvg },
|
||||
{ key: "tr", label: "Türkçe", image: trSvg },
|
||||
{ key: "de", label: "Deutsch", image: deSvg },
|
||||
{ key: "pl", label: "Polski", image: plSvg },
|
||||
{ key: "it", label: "Italiano", image: itSvg },
|
||||
{ key: "nl", label: "Nederlands", image: nlSvg },
|
||||
{ key: "fr", label: "Français", image: frSvg },
|
||||
{ key: "id", label: "Indonesia", image: idSvg },
|
||||
];
|
||||
42
src/i18n/resources.d.ts
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
// Auto-generated by i18nextTypesPlugin — do not edit manually
|
||||
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";
|
||||
"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";
|
||||
"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.cashdesk": "Kasse" | "Cashdesk" | "Caja" | "Caisse" | "Kasir" | "Cassa" | "Kassa" | "Kasa" | "Caixa" | "Касса";
|
||||
"nav.earnings": "Einnahmen" | "Earnings" | "Ganancias" | "Gains" | "Penghasilan" | "Guadagni" | "Inkomsten" | "Zarobki" | "Ganhos" | "Заработок" | "Kazançlar";
|
||||
"nav.game": "Spiel" | "Game" | "Juego" | "Jeu" | "Permainan" | "Gioco" | "Spel" | "Gra" | "Jogo" | "Игра" | "Oyun";
|
||||
"nav.menu": "Menü" | "Menu" | "Menú" | "Меню";
|
||||
"nav.roulette": "Roulette" | "Ruleta" | "Ruletka" | "Roleta" | "Рулетка" | "Rulet";
|
||||
"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";
|
||||
"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" | "из" | "/";
|
||||
"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.faq": "FAQ" | "ЧаВо" | "SSS";
|
||||
"settings.language": "Sprache" | "Language" | "Idioma" | "Langue" | "Bahasa" | "Lingua" | "Taal" | "Język" | "Язык" | "Dil";
|
||||
"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.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;
|
||||
20
src/i18n/useLanguages.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { languages } from "./languages";
|
||||
import type { Language } from "./languages";
|
||||
|
||||
export function useLanguages(): {
|
||||
languages: Language[];
|
||||
current: Language;
|
||||
setLanguage: (key: string) => void;
|
||||
} {
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
const current = languages.find((lang) => lang.key === i18n.language)!;
|
||||
|
||||
const setLanguage = (key: string): void => {
|
||||
i18n.changeLanguage(key);
|
||||
};
|
||||
|
||||
return { languages, current, setLanguage };
|
||||
}
|
||||
@@ -14,6 +14,8 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import i18n from "@/i18n";
|
||||
import tg from "@/tg";
|
||||
import { routeTree } from "./routeTree.gen";
|
||||
import { useAudioStore } from "@/audio";
|
||||
import { LiftProvider } from "@components/lift";
|
||||
|
||||
import "./styles/index.css";
|
||||
|
||||
@@ -28,11 +30,14 @@ declare module "@tanstack/react-router" {
|
||||
|
||||
tg.init();
|
||||
i18n.init();
|
||||
useAudioStore.getState().hydrate();
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
<RouterProvider router={router} />
|
||||
<LiftProvider>
|
||||
<RouterProvider router={router} />
|
||||
</LiftProvider>
|
||||
</QueryClientProvider>
|
||||
</StrictMode>,
|
||||
);
|
||||
|
||||
@@ -7,5 +7,16 @@
|
||||
background-image: url("./assets/main-bg.svg");
|
||||
background-size: auto 101%;
|
||||
background-position: center;
|
||||
overflow: hidden;
|
||||
|
||||
.main {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
|
||||
--padding-x: 16px;
|
||||
padding: calc(var(--header-total) + var(--padding-x)) var(--safe-right)
|
||||
calc(var(--navigation-total) + var(--padding-x)) var(--safe-left);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { ReactNode } from "react";
|
||||
import Header from "./components/Header";
|
||||
import Navigation from "./components/Navigation";
|
||||
import { LiftLayer } from "@components/lift";
|
||||
import classes from "./RootLayout.module.css";
|
||||
|
||||
type Props = {
|
||||
@@ -13,11 +14,10 @@ export default function RootLayout({ children, hideControls }: Props) {
|
||||
<div className={classes.rootLayout}>
|
||||
{!hideControls && <Header />}
|
||||
|
||||
<main className="h-full overflow-y-auto overflow-x-hidden pt-[calc(var(--header-total)+16px)] pb-[calc(var(--navigation-total)+16px)]">
|
||||
{children}
|
||||
</main>
|
||||
<main className={classes.main}>{children}</main>
|
||||
|
||||
{!hideControls && <Navigation />}
|
||||
<LiftLayer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
BIN
src/routes/-/RootLayout/assets/main-bg.png
Normal file
|
After Width: | Height: | Size: 720 KiB |
@@ -1,9 +1,6 @@
|
||||
:root {
|
||||
--header-height: 90px;
|
||||
--header-padding: calc(
|
||||
var(--tg-viewport-safe-area-inset-top, 0px) +
|
||||
var(--tg-viewport-content-safe-area-inset-top, 0px)
|
||||
);
|
||||
--header-padding: var(--safe-top);
|
||||
--header-total: calc(var(--header-height) + var(--header-padding));
|
||||
}
|
||||
|
||||
@@ -13,9 +10,31 @@
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
height: var(--header-total);
|
||||
z-index: 20;
|
||||
|
||||
padding-top: var(--header-padding);
|
||||
background: rgb(59 130 246 / 0.3);
|
||||
--padding-x: 10px;
|
||||
padding-left: calc(var(--safe-left) + var(--padding-x));
|
||||
padding-right: calc(var(--safe-right) + var(--padding-x));
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
bottom: -20px;
|
||||
backdrop-filter: blur(10px);
|
||||
mask-image: linear-gradient(to bottom, black 0%, black calc(100% - 50px), transparent 100%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
height: var(--header-height);
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
import classes from "./Header.module.css";
|
||||
import Profile from "./components/Profile";
|
||||
import Settings from "./components/Settings";
|
||||
|
||||
export default function Header() {
|
||||
return <header className={classes.header} />;
|
||||
return (
|
||||
<header className={classes.header}>
|
||||
<div className={classes.content}>
|
||||
<Profile />
|
||||
<Settings />
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
@layer base {
|
||||
.profile {
|
||||
pointer-events: auto;
|
||||
width: 200px;
|
||||
aspect-ratio: 2.18;
|
||||
background-image: url("./assets/profile-bg.svg");
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.avatarBorder {
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(180deg, #eed074 0%, #a5602c 100%);
|
||||
border: 1px solid #401e08;
|
||||
box-shadow:
|
||||
4px 0px 1px -2px #00000040,
|
||||
0px 2px 0px 0px #ffefd1 inset,
|
||||
0px -2px 0px 0px #7a451c inset;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.avatarInner {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(180deg, #9e6025 0%, #feeea1 100%);
|
||||
box-shadow:
|
||||
0px -1px 0px 0px #ffffffa6 inset,
|
||||
0px 1px 0px 0px #00000040 inset;
|
||||
padding: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.avatarImage {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.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;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import classes from "./Profile.module.css";
|
||||
import { motion } from "motion/react";
|
||||
import { Liftable } from "@components/lift";
|
||||
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() {
|
||||
const user = useTelegramUser();
|
||||
|
||||
return (
|
||||
<Liftable always>
|
||||
<motion.div
|
||||
initial={{ scale: 0, x: "-100vw" }}
|
||||
animate={{ scale: 1, x: 0 }}
|
||||
transition={{ duration: 0.3, type: "spring", bounce: 0.2 }}
|
||||
className={classes.profile}
|
||||
>
|
||||
<div className={classes.avatar}>
|
||||
<div className={classes.avatarBorder}>
|
||||
<div className={classes.avatarInner}>
|
||||
{user?.photoUrl && <img className={classes.avatarImage} src={user.photoUrl} alt="" />}
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes.level}>
|
||||
<div className={classes.levelInner}>1</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes.balances}>
|
||||
<div className={classes.balanceRow}>
|
||||
<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>
|
||||
</motion.div>
|
||||
</Liftable>
|
||||
);
|
||||
}
|
||||
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 77 KiB |
@@ -0,0 +1 @@
|
||||
export { default } from "./Profile";
|
||||
@@ -0,0 +1,34 @@
|
||||
@layer base {
|
||||
.settings {
|
||||
width: 80px;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: url("./assets/settings-bg.svg") center / contain no-repeat;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.bars {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.bar {
|
||||
display: block;
|
||||
width: 32px;
|
||||
height: 7px;
|
||||
border-radius: 5px;
|
||||
background-color: #563417;
|
||||
background-image: linear-gradient(180deg, #ebcb71 0%, #aa6831 100%);
|
||||
box-shadow:
|
||||
0px 1px 1px 0px #00000073,
|
||||
0px 1px 0px 0px #ffefd1 inset,
|
||||
0px -1px 0px 0px #0000008c inset;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import { lazy, Suspense } from "react";
|
||||
import { useMachine } from "@xstate/react";
|
||||
import { motion } from "motion/react";
|
||||
|
||||
import tg from "@/tg";
|
||||
import { settingsMachine } from "./settingsMachine";
|
||||
import SettingsModal, { type SettingsModalId } from "./components/SettingsModal";
|
||||
|
||||
const FAQModal = lazy(() => import("./components/FAQModal"));
|
||||
const AccountModal = lazy(() => import("./components/AccountModal"));
|
||||
const SupportModal = lazy(() => import("./components/SupportModal"));
|
||||
const TransactionsHistoryModal = lazy(() => import("./components/TransactionsHistoryModal"));
|
||||
const LanguageModal = lazy(() => import("./components/LanguageModal"));
|
||||
|
||||
import classes from "./Settings.module.css";
|
||||
import { useAudioStore } from "@/audio";
|
||||
|
||||
export default function Settings() {
|
||||
const play = useAudioStore((s) => s.play);
|
||||
|
||||
const [state, send] = useMachine(settingsMachine);
|
||||
|
||||
const isOpen = state.value !== "closed";
|
||||
|
||||
const toggle = () => {
|
||||
play("click");
|
||||
tg.hapticFeedback.click();
|
||||
if (isOpen) {
|
||||
send({ type: "CLOSE" });
|
||||
} else {
|
||||
send({ type: "OPEN_SETTINGS" });
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
send({ type: "CLOSE" });
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
send({ type: "BACK" });
|
||||
};
|
||||
|
||||
const handleNavigate = (modal: SettingsModalId) => {
|
||||
const eventMap = {
|
||||
faq: "OPEN_FAQ",
|
||||
account: "OPEN_ACCOUNT",
|
||||
support: "OPEN_SUPPORT",
|
||||
transactionsHistory: "OPEN_TRANSACTIONS_HISTORY",
|
||||
language: "OPEN_LANGUAGE",
|
||||
} satisfies Record<SettingsModalId, string>;
|
||||
send({ type: eventMap[modal] as Parameters<typeof send>[0]["type"] });
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<motion.button
|
||||
initial={{ scale: 0, x: "50%", y: "-50%" }}
|
||||
animate={{ scale: 1, x: 0, y: 0 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
transition={{ duration: 0.1, type: "spring" }}
|
||||
className={classes.settings}
|
||||
onClick={toggle}
|
||||
>
|
||||
<div className={classes.bars}>
|
||||
{[0, 1, 2].map((i) => (
|
||||
<motion.span
|
||||
key={i}
|
||||
className={classes.bar}
|
||||
initial={{ scale: 0 }}
|
||||
animate={
|
||||
isOpen
|
||||
? i === 0
|
||||
? { scale: 1, rotate: 45, y: 12 }
|
||||
: i === 1
|
||||
? { scale: 1, opacity: 0, scaleX: 0 }
|
||||
: { scale: 1, rotate: -45, y: -12 }
|
||||
: { scale: 1, rotate: 0, y: 0, opacity: 1, scaleX: 1 }
|
||||
}
|
||||
transition={{
|
||||
scale: { delay: 0.05 * i + 0.1, duration: 0.15, type: "spring" },
|
||||
default: { duration: 0.1 },
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</motion.button>
|
||||
|
||||
<SettingsModal
|
||||
open={state.value === "settings"}
|
||||
onClose={handleClose}
|
||||
onNavigate={handleNavigate}
|
||||
/>
|
||||
|
||||
<Suspense>
|
||||
<FAQModal open={state.value === "faq"} onClose={handleClose} />
|
||||
|
||||
<AccountModal open={state.value === "account"} onClose={handleClose} />
|
||||
|
||||
<SupportModal open={state.value === "support"} onClose={handleClose} />
|
||||
|
||||
<TransactionsHistoryModal
|
||||
open={state.value === "transactionsHistory"}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
|
||||
<LanguageModal
|
||||
open={state.value === "language"}
|
||||
onClose={handleClose}
|
||||
onBack={handleBack}
|
||||
/>
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
@@ -0,0 +1,46 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Modal from "@components/modals/Modal";
|
||||
import ContentSurface from "@components/surface/ContentSurface";
|
||||
import LightSurface from "@components/surface/LightSurface";
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const USER_DATA_MOCK = {
|
||||
id: 123456789,
|
||||
registrationDate: "2023-01-01",
|
||||
paymentBalance: 1000,
|
||||
withdrawalBalance: 500,
|
||||
};
|
||||
|
||||
export default function AccountModal({ open, onClose }: Props) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Modal className="w-full max-w-67.5" open={open} onClose={onClose}>
|
||||
<ContentSurface className="w-full rounded-full">
|
||||
<LightSurface className="w-full rounded-full p-2.5 font-bold text-lg">
|
||||
{t("accountInfo.yourId")} - {USER_DATA_MOCK.id}
|
||||
</LightSurface>
|
||||
</ContentSurface>
|
||||
<ContentSurface className="w-full rounded-full">
|
||||
<LightSurface className="w-full rounded-full p-2.5 font-bold text-lg">
|
||||
{t("accountInfo.registrationDate")} - {USER_DATA_MOCK.registrationDate}
|
||||
</LightSurface>
|
||||
</ContentSurface>
|
||||
<ContentSurface className="w-full rounded-full">
|
||||
<LightSurface className="w-full rounded-full p-2.5 font-bold text-lg">
|
||||
{t("accountInfo.paymentBalance")} - {USER_DATA_MOCK.paymentBalance}
|
||||
</LightSurface>
|
||||
</ContentSurface>
|
||||
<ContentSurface className="w-full rounded-full">
|
||||
<LightSurface className="w-full rounded-full p-2.5 font-bold text-lg">
|
||||
{t("accountInfo.withdrawalBalance")} - {USER_DATA_MOCK.withdrawalBalance}
|
||||
</LightSurface>
|
||||
</ContentSurface>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./AccountModal";
|
||||
@@ -0,0 +1,43 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Modal from "@components/modals/Modal";
|
||||
import ContentSurface from "@components/surface/ContentSurface";
|
||||
import LightSurface from "@components/surface/LightSurface";
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export default function FAQModal({ open, onClose }: Props) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Modal className="w-full max-w-67.5" open={open} onClose={onClose} title={t("settings.faq")}>
|
||||
<ContentSurface className="w-full rounded-4xl">
|
||||
<LightSurface className="w-full rounded-4xl p-2.5 font-bold text-lg text-center">
|
||||
<p>{t("faq.1q")}</p>
|
||||
<p className="text-[#83552E]">{t("faq.1a")}</p>
|
||||
</LightSurface>
|
||||
</ContentSurface>
|
||||
<ContentSurface className="w-full rounded-4xl">
|
||||
<LightSurface className="w-full rounded-4xl p-2.5 font-bold text-lg text-center">
|
||||
<p>{t("faq.1q")}</p>
|
||||
<p className="text-[#83552E]">{t("faq.1a")}</p>
|
||||
</LightSurface>
|
||||
</ContentSurface>
|
||||
<ContentSurface className="w-full rounded-4xl">
|
||||
<LightSurface className="w-full rounded-4xl p-2.5 font-bold text-lg text-center">
|
||||
<p>{t("faq.1q")}</p>
|
||||
<p className="text-[#83552E]">{t("faq.1a")}</p>
|
||||
</LightSurface>
|
||||
</ContentSurface>
|
||||
<ContentSurface className="w-full rounded-4xl">
|
||||
<LightSurface className="w-full rounded-4xl p-2.5 font-bold text-lg text-center">
|
||||
<p>{t("faq.1q")}</p>
|
||||
<p className="text-[#83552E]">{t("faq.1a")}</p>
|
||||
</LightSurface>
|
||||
</ContentSurface>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./FAQModal";
|
||||
@@ -0,0 +1,48 @@
|
||||
@layer base {
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.contentSurface {
|
||||
border-radius: 9999px;
|
||||
width: 100%;
|
||||
|
||||
&.active > * {
|
||||
background-color: #f8eb86;
|
||||
}
|
||||
}
|
||||
|
||||
.item {
|
||||
all: unset;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
padding: 8px 12px;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.langIcon {
|
||||
width: 27px;
|
||||
height: 27px;
|
||||
border-radius: 50%;
|
||||
background: #ccc;
|
||||
flex-shrink: 0;
|
||||
|
||||
image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.itemActive {
|
||||
color: #4b2c13;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { motion } from "motion/react";
|
||||
|
||||
import Modal from "@components/modals/Modal";
|
||||
import ContentSurface from "@components/surface/ContentSurface";
|
||||
import DarkSurface from "@components/surface/DarkSurface";
|
||||
import { useLanguages } from "@/i18n/useLanguages";
|
||||
import { useAudioStore } from "@/audio";
|
||||
import tg from "@/tg";
|
||||
|
||||
import classes from "./LanguageModal.module.css";
|
||||
import LightSurface from "@components/surface/LightSurface";
|
||||
import clsx from "clsx";
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onBack: () => void;
|
||||
};
|
||||
|
||||
export default function LanguageModal({ open, onClose, onBack }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const { languages, current, setLanguage } = useLanguages();
|
||||
const play = useAudioStore((s) => s.play);
|
||||
|
||||
const handleSelect = (key: string) => {
|
||||
play("click");
|
||||
tg.hapticFeedback.click();
|
||||
setLanguage(key);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
play("click");
|
||||
tg.hapticFeedback.click();
|
||||
onBack();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal className="w-full" open={open} onClose={onClose}>
|
||||
<div className={classes.grid}>
|
||||
{languages.map((lang) => (
|
||||
<ContentSurface
|
||||
key={lang.key}
|
||||
className={clsx(classes.contentSurface, lang.key === current.key && classes.active)}
|
||||
>
|
||||
<LightSurface className="rounded-full">
|
||||
<motion.button
|
||||
className={classes.item}
|
||||
onClick={() => handleSelect(lang.key)}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
<div className={classes.langIcon}>
|
||||
<img src={lang.image} alt={lang.label} />
|
||||
</div>
|
||||
<span>{lang.label}</span>
|
||||
</motion.button>
|
||||
</LightSurface>
|
||||
</ContentSurface>
|
||||
))}
|
||||
|
||||
<ContentSurface className={classes.contentSurface}>
|
||||
<DarkSurface className="rounded-full h-full">
|
||||
<motion.button
|
||||
className={clsx(classes.item, "justify-center text-white text-lg")}
|
||||
onClick={handleBack}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
<span>{t("settings.back")}</span>
|
||||
</motion.button>
|
||||
</DarkSurface>
|
||||
</ContentSurface>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./LanguageModal";
|
||||
@@ -0,0 +1,51 @@
|
||||
@layer base {
|
||||
.items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.contentSurface {
|
||||
border-radius: 9999px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.lightSurface {
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
.item {
|
||||
all: unset;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
padding: 10px 12px;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.itemRow {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.langIcon {
|
||||
width: 27px;
|
||||
height: 27px;
|
||||
border-radius: 50%;
|
||||
background: #ccc;
|
||||
flex-shrink: 0;
|
||||
image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { motion } from "motion/react";
|
||||
|
||||
import Modal from "@components/modals/Modal";
|
||||
import ContentSurface from "@components/surface/ContentSurface";
|
||||
import LightSurface from "@components/surface/LightSurface";
|
||||
import SwitchInput from "@components/form/SwitchInput";
|
||||
import { useAudioStore } from "@/audio";
|
||||
import tg from "@/tg";
|
||||
|
||||
import classes from "./SettingsModal.module.css";
|
||||
import { useLanguages } from "@/i18n/useLanguages";
|
||||
|
||||
export type SettingsModalId = "faq" | "account" | "support" | "transactionsHistory" | "language";
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onNavigate: (modal: SettingsModalId) => void;
|
||||
liftIds?: string[];
|
||||
};
|
||||
|
||||
export default function SettingsModal({ open, onClose, onNavigate, liftIds }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const { current } = useLanguages();
|
||||
const { isEnabled, setIsEnabled, play } = useAudioStore();
|
||||
|
||||
const handleNavigate = (modal: SettingsModalId) => {
|
||||
play("click");
|
||||
tg.hapticFeedback.click();
|
||||
onNavigate(modal);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal className="w-full max-w-67.5" open={open} onClose={onClose} liftIds={liftIds}>
|
||||
<div className={classes.items}>
|
||||
<ContentSurface className={classes.contentSurface}>
|
||||
<LightSurface className={classes.lightSurface}>
|
||||
<motion.button
|
||||
className={classes.item}
|
||||
onClick={() => handleNavigate("faq")}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
{t("settings.faq")}
|
||||
</motion.button>
|
||||
</LightSurface>
|
||||
</ContentSurface>
|
||||
|
||||
<ContentSurface className={classes.contentSurface}>
|
||||
<LightSurface className={classes.lightSurface}>
|
||||
<motion.button
|
||||
className={classes.item}
|
||||
onClick={() => handleNavigate("account")}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
{t("settings.accountInfo")}
|
||||
</motion.button>
|
||||
</LightSurface>
|
||||
</ContentSurface>
|
||||
|
||||
<ContentSurface className={classes.contentSurface}>
|
||||
<LightSurface className={classes.lightSurface}>
|
||||
<motion.button
|
||||
className={classes.item}
|
||||
onClick={() => handleNavigate("support")}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
{t("settings.support")}
|
||||
</motion.button>
|
||||
</LightSurface>
|
||||
</ContentSurface>
|
||||
|
||||
<ContentSurface className={classes.contentSurface}>
|
||||
<LightSurface className={classes.lightSurface}>
|
||||
<motion.button
|
||||
className={classes.item}
|
||||
onClick={() => handleNavigate("transactionsHistory")}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
{t("settings.transactionHistory")}
|
||||
</motion.button>
|
||||
</LightSurface>
|
||||
</ContentSurface>
|
||||
|
||||
<ContentSurface className={classes.contentSurface}>
|
||||
<LightSurface className={classes.lightSurface}>
|
||||
<div className={classes.itemRow}>
|
||||
<span>{t("settings.sound")}</span>
|
||||
<SwitchInput
|
||||
value={isEnabled}
|
||||
onChange={(value) => {
|
||||
setIsEnabled(value);
|
||||
if (value) play("click", { force: true });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</LightSurface>
|
||||
</ContentSurface>
|
||||
|
||||
<ContentSurface className={classes.contentSurface}>
|
||||
<LightSurface className={classes.lightSurface}>
|
||||
<motion.button
|
||||
className={classes.itemRow}
|
||||
onClick={() => handleNavigate("language")}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
<span>{t("settings.language")}</span>
|
||||
<div className={classes.langIcon}>
|
||||
<img src={current.image} alt="lang" />
|
||||
</div>
|
||||
</motion.button>
|
||||
</LightSurface>
|
||||
</ContentSurface>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default } from "./SettingsModal";
|
||||
export type { SettingsModalId } from "./SettingsModal";
|
||||
@@ -0,0 +1,33 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Modal from "@components/modals/Modal";
|
||||
import ContentSurface from "@components/surface/ContentSurface";
|
||||
import LightSurface from "@components/surface/LightSurface";
|
||||
import Button from "@components/atoms/Button";
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export default function SupportModal({ open, onClose }: Props) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className="w-full max-w-67.5 flex flex-col items-center gap-2"
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
title={t("support.title")}
|
||||
>
|
||||
<ContentSurface className="w-full rounded-4xl">
|
||||
<LightSurface className="rounded-4xl py-1 px-2 font-bold text-lg text-center">
|
||||
{t("support.text")}
|
||||
</LightSurface>
|
||||
</ContentSurface>
|
||||
<Button variant="blue" className="w-11/12">
|
||||
{t("support.action")}
|
||||
</Button>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./SupportModal";
|
||||
@@ -0,0 +1,6 @@
|
||||
@layer base {
|
||||
.placeholder {
|
||||
padding: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Modal from "@components/modals/Modal";
|
||||
import ContentSurface from "@components/surface/ContentSurface";
|
||||
import LightSurface from "@components/surface/LightSurface";
|
||||
|
||||
import DarkSurface from "@components/surface/DarkSurface";
|
||||
import HoneyIcon from "@components/icons/HoneyIcon";
|
||||
import MoneyIcon from "@components/icons/MoneyIcon";
|
||||
import Pagination from "@components/atoms/Pagination";
|
||||
import { useState } from "react";
|
||||
|
||||
const MOCK_TRANSACTIONS: {
|
||||
id: number;
|
||||
amount: number;
|
||||
date: string;
|
||||
currency: "honey" | "money";
|
||||
kind: "withdrawal" | "deposit" | "greeting" | "referral";
|
||||
}[] = [
|
||||
{ id: 1, amount: 500, date: "2026-03-20", currency: "honey", kind: "deposit" },
|
||||
{ id: 2, amount: -150, date: "2026-03-19", currency: "money", kind: "withdrawal" },
|
||||
{ id: 3, amount: 100, date: "2026-03-18", currency: "honey", kind: "greeting" },
|
||||
{ id: 4, amount: 250, date: "2026-03-17", currency: "honey", kind: "referral" },
|
||||
{ id: 7, amount: 75, date: "2026-03-14", currency: "money", kind: "referral" },
|
||||
{ id: 8, amount: -200, date: "2026-03-13", currency: "honey", kind: "withdrawal" },
|
||||
];
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const numberFormat = new Intl.NumberFormat("en-US", { signDisplay: "exceptZero" });
|
||||
const dateFormat = new Intl.DateTimeFormat("fi-FI");
|
||||
|
||||
export default function TransactionsHistoryModal({ open, onClose }: Props) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const totalPages = 6;
|
||||
const [page, setPage] = useState(1);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className="w-full flex flex-col items-center gap-2"
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
title={t("transactionHistory.title")}
|
||||
>
|
||||
<span className="text-center font-bold text-xl text-[#FBE6BE]">
|
||||
{t("transactionHistory.yourTransactions")}
|
||||
</span>
|
||||
<div className="w-full flex justify-between items-center text-center font-bold text-lg">
|
||||
<ContentSurface className="rounded-full w-25">
|
||||
<DarkSurface className="rounded-full p-1.5 text-white">
|
||||
{t("transactionHistory.sum")}
|
||||
</DarkSurface>
|
||||
</ContentSurface>
|
||||
<ContentSurface className="rounded-full w-25">
|
||||
<DarkSurface className="rounded-full p-1.5 text-white">
|
||||
{t("transactionHistory.date")}
|
||||
</DarkSurface>
|
||||
</ContentSurface>
|
||||
</div>
|
||||
|
||||
{MOCK_TRANSACTIONS.map(({ id, kind, date, currency, amount }) => (
|
||||
<ContentSurface key={id} className="rounded-2xl w-full">
|
||||
<LightSurface className="rounded-t-2xl rounded-b-sm flex justify-between p-2">
|
||||
<div className="flex gap-1">
|
||||
<span>{numberFormat.format(amount)}</span>
|
||||
{currency === "honey" ? <HoneyIcon /> : <MoneyIcon />}
|
||||
</div>
|
||||
<div>{dateFormat.format(new Date(date))}</div>
|
||||
</LightSurface>
|
||||
<DarkSurface className="rounded-b-2xl rounded-t-sm p-2">
|
||||
{t("transactionHistory.operationType")}: {t(`operationType.${kind}`)}
|
||||
</DarkSurface>
|
||||
</ContentSurface>
|
||||
))}
|
||||
|
||||
<Pagination value={page} total={totalPages} onChange={setPage} />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./TransactionsHistoryModal";
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./Settings";
|
||||
@@ -0,0 +1,65 @@
|
||||
import { setup } from "xstate";
|
||||
|
||||
export const settingsMachine = setup({
|
||||
types: {
|
||||
events: {} as
|
||||
| { type: "OPEN_SETTINGS" }
|
||||
| { type: "OPEN_FAQ" }
|
||||
| { type: "OPEN_ACCOUNT" }
|
||||
| { type: "OPEN_SUPPORT" }
|
||||
| { type: "OPEN_TRANSACTIONS_HISTORY" }
|
||||
| { type: "OPEN_LANGUAGE" }
|
||||
| { type: "CLOSE" }
|
||||
| { type: "BACK" },
|
||||
},
|
||||
}).createMachine({
|
||||
id: "settings",
|
||||
initial: "closed",
|
||||
states: {
|
||||
closed: {
|
||||
on: {
|
||||
OPEN_SETTINGS: { target: "settings" },
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
on: {
|
||||
CLOSE: { target: "closed" },
|
||||
OPEN_FAQ: { target: "faq" },
|
||||
OPEN_ACCOUNT: { target: "account" },
|
||||
OPEN_SUPPORT: { target: "support" },
|
||||
OPEN_TRANSACTIONS_HISTORY: { target: "transactionsHistory" },
|
||||
OPEN_LANGUAGE: { target: "language" },
|
||||
},
|
||||
},
|
||||
faq: {
|
||||
on: {
|
||||
CLOSE: { target: "closed" },
|
||||
BACK: { target: "settings" },
|
||||
},
|
||||
},
|
||||
account: {
|
||||
on: {
|
||||
CLOSE: { target: "closed" },
|
||||
BACK: { target: "settings" },
|
||||
},
|
||||
},
|
||||
support: {
|
||||
on: {
|
||||
CLOSE: { target: "closed" },
|
||||
BACK: { target: "settings" },
|
||||
},
|
||||
},
|
||||
transactionsHistory: {
|
||||
on: {
|
||||
CLOSE: { target: "closed" },
|
||||
BACK: { target: "settings" },
|
||||
},
|
||||
},
|
||||
language: {
|
||||
on: {
|
||||
CLOSE: { target: "closed" },
|
||||
BACK: { target: "settings" },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||