feat: add settings menu
All checks were successful
Deploy to VPS (dist) / deploy (push) Successful in 1m40s
@@ -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,6 +22,7 @@
|
||||
"@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",
|
||||
@@ -32,10 +33,10 @@
|
||||
"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"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@i18next-selector/vite-plugin": "^0.0.18",
|
||||
"@tanstack/router-plugin": "^1.166.3",
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/react": "^19.2.7",
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
93
pnpm-lock.yaml
generated
@@ -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
|
||||
@@ -65,10 +68,10 @@ importers:
|
||||
tailwindcss:
|
||||
specifier: ^4.2.1
|
||||
version: 4.2.1
|
||||
xstate:
|
||||
specifier: ^5.28.0
|
||||
version: 5.28.0
|
||||
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))
|
||||
@@ -363,11 +366,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==}
|
||||
|
||||
@@ -1222,18 +1220,6 @@ 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==}
|
||||
|
||||
@@ -1254,6 +1240,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 +1274,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==}
|
||||
|
||||
@@ -1619,10 +1611,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'}
|
||||
@@ -2155,6 +2143,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 +2237,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==}
|
||||
|
||||
@@ -2454,15 +2454,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
|
||||
@@ -3129,12 +3120,6 @@ 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/node@24.12.0':
|
||||
@@ -3157,6 +3142,16 @@ snapshots:
|
||||
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 +3169,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
|
||||
@@ -3502,10 +3495,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: {}
|
||||
@@ -4002,6 +3991,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
|
||||
@@ -4050,6 +4045,8 @@ snapshots:
|
||||
|
||||
ws@8.19.0: {}
|
||||
|
||||
xstate@5.28.0: {}
|
||||
|
||||
yallist@3.1.1: {}
|
||||
|
||||
yaml@2.8.2: {}
|
||||
|
||||
551
public/honey.svg
Normal file
|
After Width: | Height: | Size: 77 KiB |
54
public/locales/de.json
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"accountInfo": {
|
||||
"paymentBalance": "Einzahlungsguthaben",
|
||||
"registrationDate": "Registrierungsdatum",
|
||||
"withdrawalBalance": "Auszahlungsguthaben",
|
||||
"yourId": "Ihre ID"
|
||||
},
|
||||
"actionModal": {
|
||||
"close": "Schließen"
|
||||
},
|
||||
"common": {
|
||||
"off": "aus",
|
||||
"on": "an"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"support": {
|
||||
"action": "Support kontaktieren",
|
||||
"text": "Wenn Sie Fragen zum Spiel haben, wenden Sie sich bitte an unseren Support."
|
||||
},
|
||||
"settings": {
|
||||
"accountInfo": "Kontoinformationen",
|
||||
"back": "Zurück",
|
||||
"faq": "FAQ",
|
||||
"language": "Sprache",
|
||||
"sound": "Ton",
|
||||
"support": "Support",
|
||||
"transactionHistory": "Transaktionsverlauf"
|
||||
},
|
||||
"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,54 @@
|
||||
{
|
||||
"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"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"support": {
|
||||
"action": "Contact support",
|
||||
"text": "If you have any questions related to the game, please contact our support team."
|
||||
},
|
||||
"settings": {
|
||||
"accountInfo": "Account information",
|
||||
"back": "Back",
|
||||
"faq": "FAQ",
|
||||
"language": "Language",
|
||||
"sound": "Sound",
|
||||
"support": "Support",
|
||||
"transactionHistory": "Transaction History"
|
||||
},
|
||||
"transactionHistory": {
|
||||
"date": "Date",
|
||||
"operationType": "Operation type",
|
||||
"sum": "Sum",
|
||||
"title": "Transaction History",
|
||||
"yourTransactions": "Your Transactions"
|
||||
}
|
||||
}
|
||||
|
||||
54
public/locales/es.json
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"accountInfo": {
|
||||
"paymentBalance": "Saldo de pagos",
|
||||
"registrationDate": "Fecha de registro",
|
||||
"withdrawalBalance": "Saldo de retiros",
|
||||
"yourId": "Tu ID"
|
||||
},
|
||||
"actionModal": {
|
||||
"close": "Cerrar"
|
||||
},
|
||||
"common": {
|
||||
"off": "no",
|
||||
"on": "sí"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"support": {
|
||||
"action": "Contactar soporte",
|
||||
"text": "Si tienes alguna pregunta relacionada con el juego, contacta con nuestro equipo de soporte."
|
||||
},
|
||||
"settings": {
|
||||
"accountInfo": "Información de la cuenta",
|
||||
"back": "Volver",
|
||||
"faq": "FAQ",
|
||||
"language": "Idioma",
|
||||
"sound": "Sonido",
|
||||
"support": "Soporte",
|
||||
"transactionHistory": "Historial de transacciones"
|
||||
},
|
||||
"transactionHistory": {
|
||||
"date": "Fecha",
|
||||
"operationType": "Tipo de operación",
|
||||
"sum": "Suma",
|
||||
"title": "Historial de transacciones",
|
||||
"yourTransactions": "Tus transacciones"
|
||||
}
|
||||
}
|
||||
54
public/locales/fr.json
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"accountInfo": {
|
||||
"paymentBalance": "Solde des paiements",
|
||||
"registrationDate": "Date d'inscription",
|
||||
"withdrawalBalance": "Solde des retraits",
|
||||
"yourId": "Votre ID"
|
||||
},
|
||||
"actionModal": {
|
||||
"close": "Fermer"
|
||||
},
|
||||
"common": {
|
||||
"off": "non",
|
||||
"on": "oui"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"support": {
|
||||
"action": "Contacter le support",
|
||||
"text": "Si vous avez des questions liées au jeu, veuillez contacter notre équipe de support."
|
||||
},
|
||||
"settings": {
|
||||
"accountInfo": "Informations du compte",
|
||||
"back": "Retour",
|
||||
"faq": "FAQ",
|
||||
"language": "Langue",
|
||||
"sound": "Son",
|
||||
"support": "Support",
|
||||
"transactionHistory": "Historique des transactions"
|
||||
},
|
||||
"transactionHistory": {
|
||||
"date": "Date",
|
||||
"operationType": "Type d'opération",
|
||||
"sum": "Somme",
|
||||
"title": "Historique des transactions",
|
||||
"yourTransactions": "Vos transactions"
|
||||
}
|
||||
}
|
||||
54
public/locales/id.json
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"accountInfo": {
|
||||
"paymentBalance": "Saldo pembayaran",
|
||||
"registrationDate": "Tanggal pendaftaran",
|
||||
"withdrawalBalance": "Saldo penarikan",
|
||||
"yourId": "ID Anda"
|
||||
},
|
||||
"actionModal": {
|
||||
"close": "Tutup"
|
||||
},
|
||||
"common": {
|
||||
"off": "mati",
|
||||
"on": "nyala"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"support": {
|
||||
"action": "Hubungi dukungan",
|
||||
"text": "Jika Anda memiliki pertanyaan terkait permainan, silakan hubungi tim dukungan kami."
|
||||
},
|
||||
"settings": {
|
||||
"accountInfo": "Informasi akun",
|
||||
"back": "Kembali",
|
||||
"faq": "FAQ",
|
||||
"language": "Bahasa",
|
||||
"sound": "Suara",
|
||||
"support": "Dukungan",
|
||||
"transactionHistory": "Riwayat transaksi"
|
||||
},
|
||||
"transactionHistory": {
|
||||
"date": "Tanggal",
|
||||
"operationType": "Jenis operasi",
|
||||
"sum": "Jumlah",
|
||||
"title": "Riwayat transaksi",
|
||||
"yourTransactions": "Transaksi Anda"
|
||||
}
|
||||
}
|
||||
54
public/locales/it.json
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"accountInfo": {
|
||||
"paymentBalance": "Saldo pagamenti",
|
||||
"registrationDate": "Data di registrazione",
|
||||
"withdrawalBalance": "Saldo prelievi",
|
||||
"yourId": "Il tuo ID"
|
||||
},
|
||||
"actionModal": {
|
||||
"close": "Chiudi"
|
||||
},
|
||||
"common": {
|
||||
"off": "no",
|
||||
"on": "sì"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"support": {
|
||||
"action": "Contatta il supporto",
|
||||
"text": "Se hai domande relative al gioco, contatta il nostro team di supporto."
|
||||
},
|
||||
"settings": {
|
||||
"accountInfo": "Informazioni account",
|
||||
"back": "Indietro",
|
||||
"faq": "FAQ",
|
||||
"language": "Lingua",
|
||||
"sound": "Suono",
|
||||
"support": "Supporto",
|
||||
"transactionHistory": "Cronologia transazioni"
|
||||
},
|
||||
"transactionHistory": {
|
||||
"date": "Data",
|
||||
"operationType": "Tipo di operazione",
|
||||
"sum": "Somma",
|
||||
"title": "Cronologia transazioni",
|
||||
"yourTransactions": "Le tue transazioni"
|
||||
}
|
||||
}
|
||||
54
public/locales/nl.json
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"accountInfo": {
|
||||
"paymentBalance": "Stortingssaldo",
|
||||
"registrationDate": "Registratiedatum",
|
||||
"withdrawalBalance": "Opnamesaldo",
|
||||
"yourId": "Uw ID"
|
||||
},
|
||||
"actionModal": {
|
||||
"close": "Sluiten"
|
||||
},
|
||||
"common": {
|
||||
"off": "uit",
|
||||
"on": "aan"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"support": {
|
||||
"action": "Contact opnemen",
|
||||
"text": "Als u vragen heeft over het spel, neem dan contact op met ons ondersteuningsteam."
|
||||
},
|
||||
"settings": {
|
||||
"accountInfo": "Accountinformatie",
|
||||
"back": "Terug",
|
||||
"faq": "FAQ",
|
||||
"language": "Taal",
|
||||
"sound": "Geluid",
|
||||
"support": "Ondersteuning",
|
||||
"transactionHistory": "Transactiegeschiedenis"
|
||||
},
|
||||
"transactionHistory": {
|
||||
"date": "Datum",
|
||||
"operationType": "Bewerkingstype",
|
||||
"sum": "Som",
|
||||
"title": "Transactiegeschiedenis",
|
||||
"yourTransactions": "Uw transacties"
|
||||
}
|
||||
}
|
||||
54
public/locales/pl.json
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"accountInfo": {
|
||||
"paymentBalance": "Saldo wpłat",
|
||||
"registrationDate": "Data rejestracji",
|
||||
"withdrawalBalance": "Saldo wypłat",
|
||||
"yourId": "Twoje ID"
|
||||
},
|
||||
"actionModal": {
|
||||
"close": "Zamknij"
|
||||
},
|
||||
"common": {
|
||||
"off": "wył",
|
||||
"on": "wł"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"support": {
|
||||
"action": "Skontaktuj się z pomocą",
|
||||
"text": "Jeśli masz pytania dotyczące gry, skontaktuj się z naszym zespołem wsparcia."
|
||||
},
|
||||
"settings": {
|
||||
"accountInfo": "Informacje o koncie",
|
||||
"back": "Wstecz",
|
||||
"faq": "FAQ",
|
||||
"language": "Język",
|
||||
"sound": "Dźwięk",
|
||||
"support": "Wsparcie",
|
||||
"transactionHistory": "Historia transakcji"
|
||||
},
|
||||
"transactionHistory": {
|
||||
"date": "Data",
|
||||
"operationType": "Typ operacji",
|
||||
"sum": "Suma",
|
||||
"title": "Historia transakcji",
|
||||
"yourTransactions": "Twoje transakcje"
|
||||
}
|
||||
}
|
||||
54
public/locales/pt.json
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"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"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"support": {
|
||||
"action": "Contactar suporte",
|
||||
"text": "Se tiver dúvidas relacionadas ao jogo, entre em contato com nossa equipe de suporte."
|
||||
},
|
||||
"settings": {
|
||||
"accountInfo": "Informações da conta",
|
||||
"back": "Voltar",
|
||||
"faq": "FAQ",
|
||||
"language": "Idioma",
|
||||
"sound": "Som",
|
||||
"support": "Suporte",
|
||||
"transactionHistory": "Histórico de transações"
|
||||
},
|
||||
"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,54 @@
|
||||
{
|
||||
"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": "вкл"
|
||||
},
|
||||
"nav": {
|
||||
"apiary": "Пасека",
|
||||
"cashdesk": "Касса",
|
||||
"earnings": "Заработок",
|
||||
"game": "Игра",
|
||||
"menu": "Меню",
|
||||
"roulette": "Рулетка",
|
||||
"shop": "Магазин",
|
||||
"tasks": "Задания"
|
||||
},
|
||||
"operationType": {
|
||||
"deposit": "Пополнение",
|
||||
"greeting": "Приветственный бонус",
|
||||
"referral": "Реферальный бонус",
|
||||
"withdrawal": "Вывод"
|
||||
},
|
||||
"pagination": {
|
||||
"of": "из"
|
||||
},
|
||||
"support": {
|
||||
"action": "Связаться с поддержкой",
|
||||
"text": "Если у вас возникли вопросы, связанные с игрой — обратитесь в нашу службу поддержки."
|
||||
},
|
||||
"settings": {
|
||||
"accountInfo": "Информация об аккаунте",
|
||||
"back": "Назад",
|
||||
"faq": "ЧаВо",
|
||||
"language": "Язык",
|
||||
"sound": "Звук",
|
||||
"support": "Поддержка",
|
||||
"transactionHistory": "История транзакций"
|
||||
},
|
||||
"transactionHistory": {
|
||||
"date": "Дата",
|
||||
"operationType": "Тип операции",
|
||||
"sum": "Сумма",
|
||||
"title": "История транзакций",
|
||||
"yourTransactions": "Ваши транзакции"
|
||||
}
|
||||
}
|
||||
|
||||
54
public/locales/tr.json
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"accountInfo": {
|
||||
"paymentBalance": "Ödeme bakiyesi",
|
||||
"registrationDate": "Kayıt tarihi",
|
||||
"withdrawalBalance": "Çekim bakiyesi",
|
||||
"yourId": "Kimliğiniz"
|
||||
},
|
||||
"actionModal": {
|
||||
"close": "Kapat"
|
||||
},
|
||||
"common": {
|
||||
"off": "kapalı",
|
||||
"on": "açık"
|
||||
},
|
||||
"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": "/"
|
||||
},
|
||||
"support": {
|
||||
"action": "Destek ile iletişime geç",
|
||||
"text": "Oyunla ilgili sorularınız varsa lütfen destek ekibimizle iletişime geçin."
|
||||
},
|
||||
"settings": {
|
||||
"accountInfo": "Hesap bilgileri",
|
||||
"back": "Geri",
|
||||
"faq": "SSS",
|
||||
"language": "Dil",
|
||||
"sound": "Ses",
|
||||
"support": "Destek",
|
||||
"transactionHistory": "İşlem Geçmişi"
|
||||
},
|
||||
"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 |
110
src/audio/AudioContext.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import { type ReactNode, type RefObject, useMemo } from "react";
|
||||
import { createContext, useCallback, useContext, useEffect, useRef, useState } from "react";
|
||||
import tg, { STORAGE_KEYS } from "@/tg";
|
||||
import type { SoundKey } from "./sounds";
|
||||
import { SOUNDS, preloadSounds } from "./sounds";
|
||||
|
||||
type AudioCtxValue = {
|
||||
isEnabled: boolean;
|
||||
setIsEnabled: (enabled: boolean) => void;
|
||||
loopingRef: RefObject<Map<SoundKey, HTMLAudioElement>>;
|
||||
};
|
||||
|
||||
const AudioCtx = createContext<AudioCtxValue | null>(null);
|
||||
|
||||
export function useAudioSettings(): {
|
||||
isEnabled: boolean;
|
||||
setIsEnabled: (enabled: boolean) => void;
|
||||
} {
|
||||
const ctx = useContext(AudioCtx);
|
||||
if (!ctx) throw new Error("useAudioSettings must be used within AudioProvider");
|
||||
return { isEnabled: ctx.isEnabled, setIsEnabled: ctx.setIsEnabled };
|
||||
}
|
||||
|
||||
export function usePlaySound(): (
|
||||
key: SoundKey,
|
||||
{ mode, force }?: { mode?: "once" | "loop"; force: boolean },
|
||||
) => void {
|
||||
const ctx = useContext(AudioCtx);
|
||||
if (!ctx) throw new Error("usePlaySound must be used within AudioProvider");
|
||||
|
||||
const { isEnabled, loopingRef } = ctx;
|
||||
|
||||
return useCallback(
|
||||
(key: SoundKey, { mode, force }: { mode?: "once" | "loop"; force?: boolean } = {}) => {
|
||||
if (!isEnabled && !force) return;
|
||||
|
||||
if (mode === "loop" && loopingRef.current.has(key)) return;
|
||||
|
||||
const audio = new Audio(SOUNDS[key]);
|
||||
|
||||
if (mode === "loop") {
|
||||
audio.loop = true;
|
||||
loopingRef.current.set(key, audio);
|
||||
} else {
|
||||
audio.addEventListener(
|
||||
"ended",
|
||||
() => {
|
||||
audio.src = "";
|
||||
},
|
||||
{ once: true },
|
||||
);
|
||||
}
|
||||
|
||||
audio.play().catch(() => {
|
||||
if (mode === "loop") loopingRef.current.delete(key);
|
||||
});
|
||||
},
|
||||
[isEnabled, loopingRef],
|
||||
);
|
||||
}
|
||||
|
||||
export function AudioProvider({ children }: { children: ReactNode }) {
|
||||
const [isEnabled, setIsEnabled] = useState(true);
|
||||
const hydratedRef = useRef(false);
|
||||
const loopingRef = useRef<Map<SoundKey, HTMLAudioElement>>(null);
|
||||
if (!loopingRef.current) {
|
||||
loopingRef.current = new Map();
|
||||
preloadSounds();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
tg.storage.getItem(STORAGE_KEYS.soundEnabled).then((value) => {
|
||||
if (value !== "") {
|
||||
setIsEnabled(value !== "false");
|
||||
}
|
||||
hydratedRef.current = true;
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hydratedRef.current) return;
|
||||
tg.storage.setItem(STORAGE_KEYS.soundEnabled, String(isEnabled));
|
||||
}, [isEnabled]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEnabled) {
|
||||
for (const audio of loopingRef.current!.values()) {
|
||||
audio.pause();
|
||||
audio.src = "";
|
||||
}
|
||||
loopingRef.current!.clear();
|
||||
}
|
||||
}, [isEnabled]);
|
||||
|
||||
return (
|
||||
<AudioCtx
|
||||
value={useMemo(
|
||||
() =>
|
||||
({
|
||||
isEnabled,
|
||||
setIsEnabled,
|
||||
loopingRef,
|
||||
}) as AudioCtxValue,
|
||||
[isEnabled],
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</AudioCtx>
|
||||
);
|
||||
}
|
||||
BIN
src/audio/assets/click.mp3
Normal file
3
src/audio/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { AudioProvider, useAudioSettings, usePlaySound } from "./AudioContext";
|
||||
export { SOUNDS } from "./sounds";
|
||||
export type { SoundKey } from "./sounds";
|
||||
15
src/audio/sounds.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { prefetch } from "@/helpers/dom";
|
||||
|
||||
import click from "./assets/click.mp3";
|
||||
|
||||
export const SOUNDS = {
|
||||
click,
|
||||
} as const;
|
||||
|
||||
export type SoundKey = keyof typeof SOUNDS;
|
||||
|
||||
export function preloadSounds() {
|
||||
for (const src of Object.values(SOUNDS)) {
|
||||
prefetch(src);
|
||||
}
|
||||
}
|
||||
@@ -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 { usePlaySound } 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 = usePlaySound();
|
||||
|
||||
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 { usePlaySound } 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 = usePlaySound();
|
||||
|
||||
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 { usePlaySound } 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 = usePlaySound();
|
||||
|
||||
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}
|
||||
|
||||
@@ -4,6 +4,8 @@ import { type ReactNode, useRef, useState, type ChangeEvent, useId } from "react
|
||||
import KeyboardIcon from "@components/icons/KeyboardIcon";
|
||||
|
||||
import classes from "./NumberInput.module.css";
|
||||
import { usePlaySound } from "@/audio";
|
||||
import tg from "@/tg";
|
||||
|
||||
type Props = Omit<HTMLMotionProps<"input">, "className" | "type" | "onChange"> & {
|
||||
className?: ClassValue;
|
||||
@@ -29,6 +31,8 @@ export default function NumberInput({
|
||||
onChange,
|
||||
...props
|
||||
}: Props) {
|
||||
const play = usePlaySound();
|
||||
|
||||
const stableId = useId();
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const id = props.id ?? stableId;
|
||||
@@ -66,7 +70,11 @@ export default function NumberInput({
|
||||
<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>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
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";
|
||||
|
||||
type Props = Omit<HTMLMotionProps<"div">, "className" | "onChange"> & {
|
||||
value?: boolean | null;
|
||||
@@ -12,6 +14,7 @@ type Props = Omit<HTMLMotionProps<"div">, "className" | "onChange"> & {
|
||||
};
|
||||
|
||||
export default function SwitchInput({ value, onChange, className, ...props }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const selectedIndex = value != null ? (value ? 0 : 1) : -1;
|
||||
|
||||
return (
|
||||
@@ -19,24 +22,24 @@ export default function SwitchInput({ value, onChange, className, ...props }: Pr
|
||||
{...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 && (
|
||||
|
||||
57
src/components/lift/LiftContext.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import { createContext, useCallback, useContext, useRef, useState } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
type LiftContextValue = {
|
||||
liftedIds: Set<string>;
|
||||
alwaysLiftedIds: Set<string>;
|
||||
setLiftedIds: (ids: string[]) => void;
|
||||
registerAlways: (id: string) => void;
|
||||
unregisterAlways: (id: string) => void;
|
||||
portalContainer: HTMLElement | null;
|
||||
setPortalContainer: (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 [portalContainer, setPortalContainer] = useState<HTMLElement | null>(null);
|
||||
const alwaysRef = useRef<Set<string>>(new Set());
|
||||
|
||||
const setLiftedIds = useCallback((ids: string[]) => {
|
||||
setLiftedIdsRaw(new Set(ids));
|
||||
}, []);
|
||||
|
||||
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,
|
||||
setLiftedIds,
|
||||
registerAlways,
|
||||
unregisterAlways,
|
||||
portalContainer,
|
||||
setPortalContainer,
|
||||
}}
|
||||
>
|
||||
{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;
|
||||
}
|
||||
}
|
||||
16
src/components/lift/LiftLayer.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { useCallback } from "react";
|
||||
import { useLift } from "./LiftContext";
|
||||
import classes from "./LiftLayer.module.css";
|
||||
|
||||
export function LiftLayer() {
|
||||
const { setPortalContainer } = useLift();
|
||||
|
||||
const refCallback = useCallback(
|
||||
(node: HTMLDivElement | null) => {
|
||||
setPortalContainer(node);
|
||||
},
|
||||
[setPortalContainer],
|
||||
);
|
||||
|
||||
return <div ref={refCallback} className={classes.liftLayer} />;
|
||||
}
|
||||
121
src/components/lift/Liftable.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import { useEffect, useId, useLayoutEffect, useRef, useState } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { useLift } from "./LiftContext";
|
||||
|
||||
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, registerAlways, unregisterAlways } = useLift();
|
||||
|
||||
useEffect(() => {
|
||||
if (always) {
|
||||
registerAlways(id);
|
||||
return () => unregisterAlways(id);
|
||||
}
|
||||
}, [always, id, registerAlways, unregisterAlways]);
|
||||
|
||||
const isLifted = liftedIds.has(id) || alwaysLiftedIds.has(id);
|
||||
|
||||
const element = (
|
||||
<Liftable id={id}>{render({ isLifted, ...extraProps } as { isLifted: boolean } & T)}</Liftable>
|
||||
);
|
||||
|
||||
return { id, element };
|
||||
}
|
||||
|
||||
export function Liftable({ id: idProp, always, children }: Props) {
|
||||
const autoId = useId();
|
||||
const id = idProp ?? autoId;
|
||||
const { liftedIds, alwaysLiftedIds, registerAlways, unregisterAlways, portalContainer } =
|
||||
useLift();
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
const [rect, setRect] = useState<DOMRect | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (always) {
|
||||
registerAlways(id);
|
||||
return () => unregisterAlways(id);
|
||||
}
|
||||
}, [always, id, registerAlways, unregisterAlways]);
|
||||
|
||||
const isLifted = liftedIds.has(id) || alwaysLiftedIds.has(id);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (isLifted && wrapperRef.current) {
|
||||
setRect(wrapperRef.current.getBoundingClientRect());
|
||||
}
|
||||
if (!isLifted) {
|
||||
setRect(null);
|
||||
}
|
||||
}, [isLifted]);
|
||||
|
||||
// Re-measure on resize while lifted
|
||||
useEffect(() => {
|
||||
if (!isLifted || !wrapperRef.current) return;
|
||||
|
||||
const measure = () => {
|
||||
if (wrapperRef.current) {
|
||||
setRect(wrapperRef.current.getBoundingClientRect());
|
||||
}
|
||||
};
|
||||
|
||||
const observer = new ResizeObserver(measure);
|
||||
observer.observe(wrapperRef.current);
|
||||
window.addEventListener("resize", measure);
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
window.removeEventListener("resize", measure);
|
||||
};
|
||||
}, [isLifted]);
|
||||
|
||||
// When lifted and we have measurements + portal target, render in portal
|
||||
if (isLifted && rect && portalContainer) {
|
||||
return (
|
||||
<>
|
||||
{/* Placeholder preserves layout space */}
|
||||
<div
|
||||
ref={wrapperRef}
|
||||
style={{ width: rect.width, height: rect.height, visibility: "hidden" }}
|
||||
/>
|
||||
{/* Portal children above blur */}
|
||||
{createPortal(
|
||||
<div
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: rect.top,
|
||||
left: rect.left,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
pointerEvents: "auto",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>,
|
||||
portalContainer,
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Normal inline rendering
|
||||
return <div ref={wrapperRef}>{children}</div>;
|
||||
}
|
||||
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,11 +1,27 @@
|
||||
@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: 100;
|
||||
z-index: 102;
|
||||
}
|
||||
|
||||
.modalInner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-width: 320px;
|
||||
}
|
||||
|
||||
.modal {
|
||||
@@ -13,7 +29,19 @@
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
max-width: 320px;
|
||||
max-height: calc(
|
||||
var(--safe-area-height) - var(--header-total) - var(--navigation-total) - 70px
|
||||
);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +1,85 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
|
||||
import SectionSurface from "../../surface/SectionSurface/SectionSurface";
|
||||
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 }: Props) {
|
||||
export default function Modal({ open, children, onClose, liftIds, title, className }: Props) {
|
||||
const { setLiftedIds } = useLift();
|
||||
const prevLiftIdsRef = useRef<string>("");
|
||||
|
||||
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>
|
||||
{open && (
|
||||
<motion.div
|
||||
className={classes.overlay}
|
||||
initial={{ backdropFilter: "blur(0px)" }}
|
||||
animate={{ backdropFilter: "blur(8px)" }}
|
||||
exit={{ backdropFilter: "blur(0px)" }}
|
||||
transition={{ duration: 0.2 }}
|
||||
onClick={onClose}
|
||||
>
|
||||
<SectionSurface
|
||||
className={classes.modal}
|
||||
exit={{ scale: 0 }}
|
||||
transition={{ duration: 0.2, type: "spring" }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
<>
|
||||
<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}
|
||||
>
|
||||
{children}
|
||||
</SectionSurface>
|
||||
</motion.div>
|
||||
{/* 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,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 },
|
||||
];
|
||||
23
src/i18n/resources.d.ts
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
// Auto-generated by i18nextTypesPlugin — do not edit manually
|
||||
declare const resources: {
|
||||
"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";
|
||||
"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";
|
||||
"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";
|
||||
};
|
||||
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 { AudioProvider } from "@/audio";
|
||||
import { LiftProvider } from "@components/lift";
|
||||
|
||||
import "./styles/index.css";
|
||||
|
||||
@@ -32,7 +34,11 @@ i18n.init();
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
<RouterProvider router={router} />
|
||||
<AudioProvider>
|
||||
<LiftProvider>
|
||||
<RouterProvider router={router} />
|
||||
</LiftProvider>
|
||||
</AudioProvider>
|
||||
</QueryClientProvider>
|
||||
</StrictMode>,
|
||||
);
|
||||
|
||||
@@ -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 = {
|
||||
@@ -16,6 +17,7 @@ export default function RootLayout({ children, hideControls }: Props) {
|
||||
<main className={classes.main}>{children}</main>
|
||||
|
||||
{!hideControls && <Navigation />}
|
||||
<LiftLayer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
z-index: 20;
|
||||
|
||||
padding-top: var(--header-padding);
|
||||
padding-left: var(--safe-left);
|
||||
|
||||
@@ -8,12 +8,51 @@
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.left {
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -1,19 +1,30 @@
|
||||
import classes from "./Profile.module.css";
|
||||
import { motion } from "motion/react";
|
||||
import { Liftable } from "@components/lift";
|
||||
import { useTelegramUser } from "@/tg";
|
||||
|
||||
export default function Profile() {
|
||||
const user = useTelegramUser();
|
||||
return (
|
||||
<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.left} />
|
||||
<div className={classes.right}>
|
||||
<div className={classes.rightTop} />
|
||||
<div className={classes.rightBottom} />
|
||||
</div>
|
||||
</motion.div>
|
||||
<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>
|
||||
<div className={classes.right}>
|
||||
<div className={classes.rightTop} />
|
||||
<div className={classes.rightBottom} />
|
||||
</div>
|
||||
</motion.div>
|
||||
</Liftable>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,49 +1,114 @@
|
||||
import { useState } from "react";
|
||||
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 { usePlaySound } from "@/audio";
|
||||
|
||||
export default function Settings() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const play = usePlaySound();
|
||||
|
||||
const [state, send] = useMachine(settingsMachine);
|
||||
|
||||
const isOpen = state.value !== "closed";
|
||||
|
||||
const toggle = () => {
|
||||
play("click");
|
||||
tg.hapticFeedback.click();
|
||||
setIsOpen((prev) => !prev);
|
||||
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>
|
||||
<>
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,72 @@
|
||||
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 { usePlaySound } 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 = usePlaySound();
|
||||
|
||||
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={classes.item} 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,118 @@
|
||||
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 { useAudioSettings, usePlaySound } 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 } = useAudioSettings();
|
||||
const play = usePlaySound();
|
||||
|
||||
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,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" },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -23,6 +23,7 @@ import rouletteIcon from "./assets/roulette.svg";
|
||||
import tasksIcon from "./assets/tasks.svg";
|
||||
import earningsIcon from "./assets/earnings.svg";
|
||||
import tg, { useTelegramViewportValue } from "@/tg";
|
||||
import { usePlaySound } from "@/audio";
|
||||
|
||||
const ANIMATION_DURATION = 0.2;
|
||||
const SPRING_ANIMATION = {
|
||||
@@ -34,7 +35,6 @@ const SPRING_ANIMATION = {
|
||||
|
||||
const BAR_HEIGHT = 64;
|
||||
const ACTIVE_BAR_HEIGHT = 74;
|
||||
const OFFSCREEN_BAR_OFFSET = 20;
|
||||
|
||||
const NAV_ITEMS = [
|
||||
{ key: "nav.shop", route: ShopRoute, icon: shopIcon },
|
||||
@@ -162,6 +162,7 @@ function MenuBar({ labelKey, icon, delay, active, onClick }: MenuBarProps) {
|
||||
}
|
||||
|
||||
export default function Navigation() {
|
||||
const play = usePlaySound();
|
||||
const matchRoute = useMatchRoute();
|
||||
const navigate = useNavigate();
|
||||
const [menuOpen, setMenuOpen] = useState<number>(0);
|
||||
@@ -181,6 +182,7 @@ export default function Navigation() {
|
||||
}, [menuOpen, handleOutsideClick]);
|
||||
|
||||
const navigateRoute = async (route: AnyRoute, wait = false) => {
|
||||
play("click");
|
||||
tg.hapticFeedback.click();
|
||||
const redirection = navigate({ to: route.to });
|
||||
if (wait) {
|
||||
@@ -220,6 +222,7 @@ export default function Navigation() {
|
||||
onClick={
|
||||
item.isMenu
|
||||
? () => {
|
||||
play("click");
|
||||
tg.hapticFeedback.click();
|
||||
setMenuOpen((v) => (v ? 0 : Math.random()));
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import TextInput from "@components/form/TextInput";
|
||||
import NumberInput from "@components/form/NumberInput";
|
||||
import TextAreaInput from "@components/form/TextAreaInput";
|
||||
import ActionModal from "@components/modals/ActionModal";
|
||||
import Pagination from "@components/atoms/Pagination";
|
||||
|
||||
const TABS = [
|
||||
{ key: "tab1", title: "Tab 1" },
|
||||
@@ -24,9 +25,11 @@ const TABS = [
|
||||
|
||||
export default function GameRoute() {
|
||||
const [activeTab, setActiveTab] = useState<string | null>(TABS[0].key);
|
||||
const [page, setPage] = useState(1);
|
||||
const [progressValue, setProgressValue] = useState(0);
|
||||
const [switchValue, setSwitchValue] = useState<boolean | null>(false);
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [buttonsDisabled, setButtonsDisabled] = useState(false);
|
||||
|
||||
return (
|
||||
<SectionSurface className="relative flex flex-col gap-4 w-full">
|
||||
@@ -42,12 +45,23 @@ export default function GameRoute() {
|
||||
<DataSurface>100$</DataSurface>
|
||||
</DarkSurface>
|
||||
</ContentSurface>
|
||||
<Button onClick={() => setModalOpen(true)}>Open modal</Button>
|
||||
<Button>Click me</Button>
|
||||
<Button variant="green">Click me</Button>
|
||||
<Button variant="red">Click me</Button>
|
||||
<Button variant="yellow">Click me</Button>
|
||||
<Button variant="blue">Click me</Button>
|
||||
<Button onClick={() => setButtonsDisabled((v) => !v)}>Toggle disabled</Button>
|
||||
<Button disabled={buttonsDisabled} onClick={() => setModalOpen(true)}>
|
||||
Open modal
|
||||
</Button>
|
||||
<Button disabled={buttonsDisabled}>Click me</Button>
|
||||
<Button disabled={buttonsDisabled} variant="green">
|
||||
Click me
|
||||
</Button>
|
||||
<Button disabled={buttonsDisabled} variant="red">
|
||||
Click me
|
||||
</Button>
|
||||
<Button disabled={buttonsDisabled} variant="yellow">
|
||||
Click me
|
||||
</Button>
|
||||
<Button disabled={buttonsDisabled} variant="blue">
|
||||
Click me
|
||||
</Button>
|
||||
<ContentSurface className="rounded-full">
|
||||
<Progress value={progressValue} max={10000} variant="green" />
|
||||
</ContentSurface>
|
||||
@@ -66,6 +80,8 @@ export default function GameRoute() {
|
||||
<NumberInput error prefix="$" value={progressValue} onChange={setProgressValue} />
|
||||
<TextAreaInput placeholder="Text Area Input" rows={3} />
|
||||
<TextAreaInput placeholder="Text Area Error" error rows={3} />
|
||||
<Pagination value={page} total={10} onChange={setPage} />
|
||||
<Pagination value={page} total={10} onChange={setPage} variant="blue" />
|
||||
<ActionModal
|
||||
open={modalOpen}
|
||||
description="Are you sure you want to proceed with this action?"
|
||||
|
||||
@@ -1,8 +1,35 @@
|
||||
import * as tg from "@tma.js/sdk-react";
|
||||
import { useSignal } from "@tma.js/sdk-react";
|
||||
import { useLaunchParams, useSignal } from "@tma.js/sdk-react";
|
||||
import { prefetch } from "@/helpers/dom";
|
||||
|
||||
const USER_MOCK = {
|
||||
id: 1,
|
||||
first_name: "Pavel",
|
||||
is_bot: false,
|
||||
last_name: "Durov",
|
||||
username: "durov",
|
||||
language_code: "en",
|
||||
is_premium: true,
|
||||
photo_url:
|
||||
"https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcR8QcGzgQKSnte-VdI1k1QUbKwVEAdYhFIzEw&s",
|
||||
added_to_attachment_menu: false,
|
||||
allows_write_to_pm: true,
|
||||
} satisfies tg.User;
|
||||
|
||||
const CSS_VARS_MOCK = {
|
||||
"--tg-viewport-content-safe-area-inset-top": 10,
|
||||
"--tg-viewport-safe-area-inset-top": 10,
|
||||
"--tg-viewport-content-safe-area-inset-bottom": 10,
|
||||
"--tg-viewport-safe-area-inset-bottom": 20,
|
||||
"--tg-viewport-content-safe-area-inset-left": 10,
|
||||
"--tg-viewport-safe-area-inset-left": 10,
|
||||
"--tg-viewport-content-safe-area-inset-right": 10,
|
||||
"--tg-viewport-safe-area-inset-right": 10,
|
||||
};
|
||||
|
||||
export const STORAGE_KEYS = {
|
||||
authToken: "authToken",
|
||||
soundEnabled: "soundEnabled",
|
||||
} as const;
|
||||
export type StorageKey = (typeof STORAGE_KEYS)[keyof typeof STORAGE_KEYS];
|
||||
|
||||
@@ -49,19 +76,7 @@ const externalTgApi = {
|
||||
tg.mockTelegramEnv({
|
||||
launchParams: {
|
||||
tgWebAppData: new URLSearchParams({
|
||||
user: JSON.stringify({
|
||||
id: 1,
|
||||
first_name: "Pavel",
|
||||
is_bot: false,
|
||||
last_name: "Durov",
|
||||
username: "durov",
|
||||
language_code: "en",
|
||||
is_premium: true,
|
||||
photo_url:
|
||||
"https://media4.giphy.com/media/v1.Y2lkPTZjMDliOTUyeXF1MzYyY2pwMjR2YWFhNDhqdXBsc216MWo2aW9pczNnNXM2ZmZmbCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/xUPGcHc4I3wICqp8bu/giphy.gif",
|
||||
added_to_attachment_menu: false,
|
||||
allows_write_to_pm: true,
|
||||
} satisfies tg.User),
|
||||
user: JSON.stringify(USER_MOCK),
|
||||
hash: "",
|
||||
signature: "",
|
||||
auth_date: Date.now().toString(),
|
||||
@@ -129,22 +144,14 @@ const externalTgApi = {
|
||||
}
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
mockCssVar("--tg-viewport-content-safe-area-inset-top", 10);
|
||||
mockCssVar("--tg-viewport-safe-area-inset-top", 10);
|
||||
|
||||
mockCssVar("--tg-viewport-content-safe-area-inset-bottom", 10);
|
||||
mockCssVar("--tg-viewport-safe-area-inset-bottom", 20);
|
||||
|
||||
mockCssVar("--tg-viewport-content-safe-area-inset-left", 10);
|
||||
mockCssVar("--tg-viewport-safe-area-inset-left", 10);
|
||||
|
||||
mockCssVar("--tg-viewport-content-safe-area-inset-right", 10);
|
||||
mockCssVar("--tg-viewport-safe-area-inset-right", 10);
|
||||
Object.entries(CSS_VARS_MOCK).forEach(([key, value]) => mockCssVar(key, value));
|
||||
}
|
||||
|
||||
console.log(
|
||||
import.meta.env.DEV ? "TMA Debug mode in Web initialized" : "Telegram Mini App initialized",
|
||||
);
|
||||
|
||||
prefetch(launchParams.tgWebAppData?.user?.photo_url);
|
||||
},
|
||||
openLink(url: string | URL, options?: tg.OpenLinkOptions) {
|
||||
tg.openLink.ifAvailable(url, options);
|
||||
@@ -258,3 +265,5 @@ export const useTelegramViewportValue = (key: keyof typeof externalTgApi.viewpor
|
||||
useSignal(externalTgApi.viewport[key]);
|
||||
return externalTgApi.viewportCss[key]();
|
||||
};
|
||||
|
||||
export const useTelegramUser = () => useLaunchParams(true).tgWebAppData?.user;
|
||||
|
||||
@@ -5,7 +5,8 @@ import react from "@vitejs/plugin-react-swc";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import tanstackRouter from "@tanstack/router-plugin/vite";
|
||||
import { devtools as tanstackDevtools } from "@tanstack/devtools-vite";
|
||||
import { i18nextVitePlugin } from "@i18next-selector/vite-plugin";
|
||||
import { i18nextSortPlugin } from "./plugins/i18nextSortPlugin";
|
||||
import { i18nextTypesPlugin } from "./plugins/i18nextTypesPlugin";
|
||||
import i18nextConfig, {
|
||||
DEFAULT_LANGUAGE,
|
||||
LOCALES_PATH,
|
||||
@@ -14,9 +15,13 @@ import i18nextConfig, {
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
i18nextVitePlugin({
|
||||
i18nextSortPlugin({
|
||||
sourceDir: LOCAL_LOCALES_PATH,
|
||||
}),
|
||||
i18nextTypesPlugin({
|
||||
sourceDir: LOCAL_LOCALES_PATH,
|
||||
destination: "./src/i18n/resources.d.ts",
|
||||
}),
|
||||
tanstackDevtools({
|
||||
removeDevtoolsOnBuild: true,
|
||||
}),
|
||||
|
||||