feat: add tg api
All checks were successful
Deploy to VPS (dist) / deploy (push) Successful in 1m35s

This commit is contained in:
Hewston Fox
2026-03-16 00:50:53 +02:00
parent 67c2721cff
commit 9f0ff8c4e5
29 changed files with 544 additions and 40 deletions

0
.env
View File

3
.env.example Normal file
View File

@@ -0,0 +1,3 @@
VITE_API_BASE_URL=http://localhost:5004/api/v1/
VITE_APP_URL=http://localhost:5173
VITE_APP_NAME=Honey

View File

View File

@@ -0,0 +1,3 @@
VITE_API_BASE_URL=http://localhost:5004/api/v1/
VITE_APP_URL=http://localhost:5173
VITE_APP_NAME=Honey

2
.gitignore vendored
View File

@@ -25,3 +25,5 @@ dist-ssr
.vite
.tanstack
.env

View File

@@ -1 +1,82 @@
# TODO: update readme
# Honey Frontend
Telegram mini-app game frontend with a bee/honey theme. Built as a single-page application targeting mobile-first layout.
## Tech Stack
- **React 19** + **TypeScript 5.9** (strict mode)
- **Vite 7** with SWC (`@vitejs/plugin-react-swc`)
- **TanStack Router** (file-based routing, auto code-splitting) + **React Query**
- **Tailwind CSS v4** (`@tailwindcss/vite` plugin)
- **i18next** + react-i18next (EN, RU — loaded via HTTP backend from `public/locales/`)
- **axios** for HTTP requests
- **arktype** for runtime validation
- **motion** (Framer Motion) for animations
- **oxlint** / **oxfmt** for linting and formatting
- **pnpm** package manager
## Getting Started
### Prerequisites
- Node.js (see `.nvmrc` for a version) / NVM
- pnpm
### Setup
```bash
nvm use
pnpm install
cp .env.example .env # fill in required variables
pnpm dev
```
### Environment Variables
| Variable | Description |
| ------------------- | -------------------- |
| `VITE_APP_NAME` | Application name |
| `VITE_APP_URL` | Application URL |
| `VITE_API_BASE_URL` | Backend API base URL |
## Scripts
| Command | Description |
| -------------------- | -------------------------------- |
| `pnpm dev` | Start dev server |
| `pnpm build` | Typecheck + production build |
| `pnpm build:staging` | Typecheck + staging build |
| `pnpm lint` | Run oxlint |
| `pnpm lint:fix` | Run oxlint with auto-fix |
| `pnpm fmt` | Format code with oxfmt |
| `pnpm fmt:check` | Check formatting without writing |
## Project Structure
```
src/
api/ — Axios instance and API utilities
components/
atoms/ — Generic reusable components
form/ — Components for user inputs
icons/ — SVG icon components
modals/ — Application modals with shared interface
surface/ — Themed containers
i18n/ — i18n runtime setup
routes/ — TanStack file-based routes
styles/ — Global CSS, fonts
main.tsx — App entry point
public/
locales/ — Translation JSON files
plugins/ — Custom Vite plugins
dynamic-json.ts — JSON templating with env var interpolation
```
### Path Aliases
- `@/*``src/*`
- `@components/*``src/components/*`
## Pre-commit Hooks
Husky + lint-staged run `oxfmt` and `oxlint` on staged files before each commit.

View File

@@ -21,7 +21,9 @@
"@tanstack/react-query-devtools": "^5.91.3",
"@tanstack/react-router": "^1.166.3",
"@tanstack/react-router-devtools": "^1.166.3",
"@telegram-apps/sdk-react": "^3.3.9",
"arktype": "^2.2.0",
"axios": "^1.13.6",
"clsx": "^2.1.1",
"i18next": "^25.8.17",
"i18next-http-backend": "^3.0.2",
@@ -43,6 +45,7 @@
"lint-staged": "^16.3.2",
"oxfmt": "^0.36.0",
"oxlint": "^1.51.0",
"rollup": "^4.59.0",
"typescript": "~5.9.3",
"vite": "^7.3.1"
},

322
pnpm-lock.yaml generated
View File

@@ -29,9 +29,15 @@ importers:
'@tanstack/react-router-devtools':
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))(@tanstack/router-core@1.166.2)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@telegram-apps/sdk-react':
specifier: ^3.3.9
version: 3.3.9(@types/react@19.2.14)(react@19.2.4)(typescript@5.9.3)
arktype:
specifier: ^2.2.0
version: 2.2.0
axios:
specifier: ^1.13.6
version: 1.13.6
clsx:
specifier: ^2.1.1
version: 2.1.1
@@ -90,6 +96,9 @@ importers:
oxlint:
specifier: ^1.51.0
version: 1.51.0
rollup:
specifier: ^4.59.0
version: 4.59.0
typescript:
specifier: ~5.9.3
version: 5.9.3
@@ -1183,6 +1192,39 @@ packages:
resolution: {integrity: sha512-42WoRePf8v690qG8yGRe/YOh+oHni9vUaUUfoqlS91U2scd3a5rkLtVsc6b7z60w3RogH0I00vdrC5AaeiZ18w==}
engines: {node: '>=20.19'}
'@telegram-apps/bridge@2.11.0':
resolution: {integrity: sha512-kBZjWRRp/lxKeQ8r8cDWUY9EjxUtyeh/9xTQjsjuGRsCR5XTO1cyVbvcvqzHn53csGx3aBs+fOR3Pk3b6w2JHg==}
deprecated: This package is not supported anymore. Use @tma.js/bridge instead
'@telegram-apps/navigation@1.0.14':
resolution: {integrity: sha512-bqNgF/J8Po7ZtsELm3E1a6aPr7awwxO3sIqD8J6u12urOlGoW5+1KxKKbkPa58mgXuQdsltd8apI+OVy0IYiUA==}
'@telegram-apps/sdk-react@3.3.9':
resolution: {integrity: sha512-85jF1ICT8sYCNzXu19SqJrfrS8XslsvV14KUcToiyL7H5ZMXHt0JQKM+QJgULjnLjEswweQ4/7Bd7mNULf/BIg==}
peerDependencies:
'@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0
react: ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@types/react':
optional: true
'@telegram-apps/sdk@3.11.8':
resolution: {integrity: sha512-vlpkYzMJCV9Cadsn9Q+/hbHNR39j5o96N9z4CjZ6AFHkkxOeOqVRrq9zRBw0gmffROkF2bU0WQxhRj8KZ/bPhw==}
'@telegram-apps/signals@1.1.2':
resolution: {integrity: sha512-1P1kdCLX7MfETGPxH7f3UZKIsdE7Tz5S7QmN4Km1sbYQMakD5Bi1NecSMR7/wnHp50gWMI1JzENcMtCEmouhSg==}
'@telegram-apps/toolkit@2.1.3':
resolution: {integrity: sha512-LPUBL7hxQTOr+Dowyk9a1O82nQS4ja4+OYiYWtvqq5nNUHC6Gbbus0zGWCbFcj9CLnIzaeb5HWOg5iSnhoRcyg==}
'@telegram-apps/transformers@2.2.6':
resolution: {integrity: sha512-MMBRs3demeBT9Cd614KKZmak7eEyNdEbfu99a0SwEEJe2oIODjJLrUxrhUcAOc5wvTRfrKka27VXVgruauLhdg==}
deprecated: This package is not supported anymore. Use @tma.js/transfomers instead
'@telegram-apps/types@2.0.3':
resolution: {integrity: sha512-pXh9BdnLZF3e2BGc4WL+RTRMAUpqKpaSP3Rs8rB4WyRwIoRSGWFKE4gtT/9m42LGixB8YVwdo/pJ+9XO765XEA==}
deprecated: This package is not supported anymore. Use @tma.js/types instead
'@traversable/json@0.0.26':
resolution: {integrity: sha512-oXKX0eNxbbHGLjLV27nTuV0uyR6uSoWi0BF+FYJu4jXRcSsWqCHOqNjIb2x/0usKd70rnKLGyHxurlTBTpQVOw==}
peerDependencies:
@@ -1253,6 +1295,12 @@ packages:
resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==}
engines: {node: '>=4'}
asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
axios@1.13.6:
resolution: {integrity: sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==}
babel-dead-code-elimination@1.0.12:
resolution: {integrity: sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig==}
@@ -1261,6 +1309,9 @@ packages:
engines: {node: '>=6.0.0'}
hasBin: true
better-promises@0.4.1:
resolution: {integrity: sha512-cDW7eMvhvCoWzSih5o/OxsAgTUfP05yGMq77xNZUTmcZoNU9vEeFZJ+yJJi4lnocvxFrVFKsG0Yxt7ZnuYJEig==}
binary-extensions@2.3.0:
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
engines: {node: '>=8'}
@@ -1274,6 +1325,10 @@ packages:
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
call-bind-apply-helpers@1.0.2:
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
engines: {node: '>= 0.4'}
caniuse-lite@1.0.30001777:
resolution: {integrity: sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==}
@@ -1307,6 +1362,10 @@ packages:
colorjs.io@0.5.2:
resolution: {integrity: sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==}
combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
commander@14.0.3:
resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==}
engines: {node: '>=20'}
@@ -1335,6 +1394,10 @@ packages:
supports-color:
optional: true
delayed-stream@1.0.0:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
@@ -1343,6 +1406,10 @@ packages:
resolution: {integrity: sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==}
engines: {node: '>=0.3.1'}
dunder-proto@1.0.1:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
electron-to-chromium@1.5.307:
resolution: {integrity: sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==}
@@ -1357,6 +1424,25 @@ packages:
resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==}
engines: {node: '>=18'}
error-kid@0.0.7:
resolution: {integrity: sha512-8mFs7ZaDWlBFgjOy4lHLMCe2+KZfZXGx5GOgh2VQ/M1CCIvSWzbl2OMl+5fdZgrgoUfhTeF+NPhmqfEvUhN8yw==}
es-define-property@1.0.1:
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
engines: {node: '>= 0.4'}
es-errors@1.3.0:
resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
engines: {node: '>= 0.4'}
es-object-atoms@1.1.1:
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
engines: {node: '>= 0.4'}
es-set-tostringtag@2.1.0:
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
engines: {node: '>= 0.4'}
esbuild@0.27.3:
resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==}
engines: {node: '>=18'}
@@ -1387,6 +1473,19 @@ packages:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'}
follow-redirects@1.15.11:
resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==}
engines: {node: '>=4.0'}
peerDependencies:
debug: '*'
peerDependenciesMeta:
debug:
optional: true
form-data@4.0.5:
resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
engines: {node: '>= 6'}
framer-motion@12.35.1:
resolution: {integrity: sha512-rL8cLrjYZNShZqKV3U0Qj6Y5WDiZXYEM5giiTLfEqsIZxtspzMDCkKmrO5po76jWfvOg04+Vk+sfBvTD0iMmLw==}
peerDependencies:
@@ -1406,6 +1505,9 @@ packages:
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
gensync@1.0.0-beta.2:
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
engines: {node: '>=6.9.0'}
@@ -1414,6 +1516,14 @@ packages:
resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==}
engines: {node: '>=18'}
get-intrinsic@1.3.0:
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
engines: {node: '>= 0.4'}
get-proto@1.0.1:
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
engines: {node: '>= 0.4'}
get-tsconfig@4.13.6:
resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==}
@@ -1430,6 +1540,10 @@ packages:
peerDependencies:
csstype: ^3.0.10
gopd@1.2.0:
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
engines: {node: '>= 0.4'}
graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
@@ -1437,6 +1551,18 @@ packages:
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
engines: {node: '>=8'}
has-symbols@1.1.0:
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
engines: {node: '>= 0.4'}
has-tostringtag@1.0.2:
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
engines: {node: '>= 0.4'}
hasown@2.0.2:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
html-parse-stringify@3.0.1:
resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==}
@@ -1600,14 +1726,29 @@ packages:
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
math-intrinsics@1.1.0:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
micromatch@4.0.8:
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
engines: {node: '>=8.6'}
mime-db@1.52.0:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'}
mime-types@2.1.35:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'}
mimic-function@5.0.1:
resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==}
engines: {node: '>=18'}
mitt@3.0.1:
resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
motion-dom@12.35.1:
resolution: {integrity: sha512-7n6r7TtNOsH2UFSAXzTkfzOeO5616v9B178qBIjmu/WgEyJK0uqwytCEhwKBTuM/HJA40ptAw7hLFpxtPAMRZQ==}
@@ -1697,6 +1838,9 @@ packages:
engines: {node: '>=14'}
hasBin: true
proxy-from-env@1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
react-dom@19.2.4:
resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==}
peerDependencies:
@@ -2013,6 +2157,22 @@ packages:
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
valibot@1.0.0:
resolution: {integrity: sha512-1Hc0ihzWxBar6NGeZv7fPLY0QuxFMyxwYR2sF1Blu7Wq7EnremwY2W02tit2ij2VJT8HcSkHAQqmFfl77f73Yw==}
peerDependencies:
typescript: '>=5'
peerDependenciesMeta:
typescript:
optional: true
valibot@1.0.0-beta.14:
resolution: {integrity: sha512-tLyV2rE5QL6U29MFy3xt4AqMrn+/HErcp2ZThASnQvPMwfSozjV1uBGKIGiegtZIGjinJqn0SlBdannf18wENA==}
peerDependencies:
typescript: '>=5'
peerDependenciesMeta:
typescript:
optional: true
varint@6.0.0:
resolution: {integrity: sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==}
@@ -2919,6 +3079,58 @@ snapshots:
'@tanstack/virtual-file-routes@1.161.4': {}
'@telegram-apps/bridge@2.11.0(typescript@5.9.3)':
dependencies:
'@telegram-apps/signals': 1.1.2
'@telegram-apps/toolkit': 2.1.3
'@telegram-apps/transformers': 2.2.6(typescript@5.9.3)
'@telegram-apps/types': 2.0.3
better-promises: 0.4.1
error-kid: 0.0.7
mitt: 3.0.1
valibot: 1.0.0(typescript@5.9.3)
transitivePeerDependencies:
- typescript
'@telegram-apps/navigation@1.0.14': {}
'@telegram-apps/sdk-react@3.3.9(@types/react@19.2.14)(react@19.2.4)(typescript@5.9.3)':
dependencies:
'@telegram-apps/sdk': 3.11.8(typescript@5.9.3)
react: 19.2.4
optionalDependencies:
'@types/react': 19.2.14
transitivePeerDependencies:
- typescript
'@telegram-apps/sdk@3.11.8(typescript@5.9.3)':
dependencies:
'@telegram-apps/bridge': 2.11.0(typescript@5.9.3)
'@telegram-apps/navigation': 1.0.14
'@telegram-apps/signals': 1.1.2
'@telegram-apps/toolkit': 2.1.3
'@telegram-apps/transformers': 2.2.6(typescript@5.9.3)
'@telegram-apps/types': 2.0.3
better-promises: 0.4.1
error-kid: 0.0.7
valibot: 1.0.0(typescript@5.9.3)
transitivePeerDependencies:
- typescript
'@telegram-apps/signals@1.1.2': {}
'@telegram-apps/toolkit@2.1.3': {}
'@telegram-apps/transformers@2.2.6(typescript@5.9.3)':
dependencies:
'@telegram-apps/toolkit': 2.1.3
'@telegram-apps/types': 2.0.3
valibot: 1.0.0-beta.14(typescript@5.9.3)
transitivePeerDependencies:
- typescript
'@telegram-apps/types@2.0.3': {}
'@traversable/json@0.0.26(@traversable/registry@0.0.25)':
dependencies:
'@traversable/registry': 0.0.25
@@ -2980,6 +3192,16 @@ snapshots:
dependencies:
tslib: 2.8.1
asynckit@0.4.0: {}
axios@1.13.6:
dependencies:
follow-redirects: 1.15.11
form-data: 4.0.5
proxy-from-env: 1.1.0
transitivePeerDependencies:
- debug
babel-dead-code-elimination@1.0.12:
dependencies:
'@babel/core': 7.29.0
@@ -2991,6 +3213,10 @@ snapshots:
baseline-browser-mapping@2.10.0: {}
better-promises@0.4.1:
dependencies:
error-kid: 0.0.7
binary-extensions@2.3.0: {}
braces@3.0.3:
@@ -3005,6 +3231,11 @@ snapshots:
node-releases: 2.0.36
update-browserslist-db: 1.2.3(browserslist@4.28.1)
call-bind-apply-helpers@1.0.2:
dependencies:
es-errors: 1.3.0
function-bind: 1.1.2
caniuse-lite@1.0.30001777: {}
chalk@5.6.2: {}
@@ -3042,6 +3273,10 @@ snapshots:
colorjs.io@0.5.2:
optional: true
combined-stream@1.0.8:
dependencies:
delayed-stream: 1.0.0
commander@14.0.3: {}
convert-source-map@2.0.0: {}
@@ -3062,10 +3297,18 @@ snapshots:
dependencies:
ms: 2.1.3
delayed-stream@1.0.0: {}
detect-libc@2.1.2: {}
diff@8.0.3: {}
dunder-proto@1.0.1:
dependencies:
call-bind-apply-helpers: 1.0.2
es-errors: 1.3.0
gopd: 1.2.0
electron-to-chromium@1.5.307: {}
emoji-regex@10.6.0: {}
@@ -3077,6 +3320,23 @@ snapshots:
environment@1.1.0: {}
error-kid@0.0.7: {}
es-define-property@1.0.1: {}
es-errors@1.3.0: {}
es-object-atoms@1.1.1:
dependencies:
es-errors: 1.3.0
es-set-tostringtag@2.1.0:
dependencies:
es-errors: 1.3.0
get-intrinsic: 1.3.0
has-tostringtag: 1.0.2
hasown: 2.0.2
esbuild@0.27.3:
optionalDependencies:
'@esbuild/aix-ppc64': 0.27.3
@@ -3120,6 +3380,16 @@ snapshots:
dependencies:
to-regex-range: 5.0.1
follow-redirects@1.15.11: {}
form-data@4.0.5:
dependencies:
asynckit: 0.4.0
combined-stream: 1.0.8
es-set-tostringtag: 2.1.0
hasown: 2.0.2
mime-types: 2.1.35
framer-motion@12.35.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
dependencies:
motion-dom: 12.35.1
@@ -3132,10 +3402,30 @@ snapshots:
fsevents@2.3.3:
optional: true
function-bind@1.1.2: {}
gensync@1.0.0-beta.2: {}
get-east-asian-width@1.5.0: {}
get-intrinsic@1.3.0:
dependencies:
call-bind-apply-helpers: 1.0.2
es-define-property: 1.0.1
es-errors: 1.3.0
es-object-atoms: 1.1.1
function-bind: 1.1.2
get-proto: 1.0.1
gopd: 1.2.0
has-symbols: 1.1.0
hasown: 2.0.2
math-intrinsics: 1.1.0
get-proto@1.0.1:
dependencies:
dunder-proto: 1.0.1
es-object-atoms: 1.1.1
get-tsconfig@4.13.6:
dependencies:
resolve-pkg-maps: 1.0.0
@@ -3150,11 +3440,23 @@ snapshots:
dependencies:
csstype: 3.2.3
gopd@1.2.0: {}
graceful-fs@4.2.11: {}
has-flag@4.0.0:
optional: true
has-symbols@1.1.0: {}
has-tostringtag@1.0.2:
dependencies:
has-symbols: 1.1.0
hasown@2.0.2:
dependencies:
function-bind: 1.1.2
html-parse-stringify@3.0.1:
dependencies:
void-elements: 3.1.0
@@ -3294,13 +3596,23 @@ snapshots:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
math-intrinsics@1.1.0: {}
micromatch@4.0.8:
dependencies:
braces: 3.0.3
picomatch: 2.3.1
mime-db@1.52.0: {}
mime-types@2.1.35:
dependencies:
mime-db: 1.52.0
mimic-function@5.0.1: {}
mitt@3.0.1: {}
motion-dom@12.35.1:
dependencies:
motion-utils: 12.29.2
@@ -3396,6 +3708,8 @@ snapshots:
prettier@3.8.1: {}
proxy-from-env@1.1.0: {}
react-dom@19.2.4(react@19.2.4):
dependencies:
react: 19.2.4
@@ -3690,6 +4004,14 @@ snapshots:
dependencies:
react: 19.2.4
valibot@1.0.0(typescript@5.9.3):
optionalDependencies:
typescript: 5.9.3
valibot@1.0.0-beta.14(typescript@5.9.3):
optionalDependencies:
typescript: 5.9.3
varint@6.0.0:
optional: true

18
src/api/api.ts Normal file
View File

@@ -0,0 +1,18 @@
import axios from "axios";
import tg, { STORAGE_KEYS } from "@/tg";
const api = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
headers: {
...(import.meta.env.DEV && {
"ngrok-skip-browser-warning": "true",
}),
},
});
api.interceptors.request.use(async (config) => {
config.headers.Authorization = `Bearer ${await tg.storage.getItem(STORAGE_KEYS.authToken)}`;
return config;
});
export default api;

1
src/api/index.ts Normal file
View File

@@ -0,0 +1 @@
export { default as api } from "./api";

View File

@@ -1,6 +1,8 @@
import { motion, type HTMLMotionProps } from "motion/react";
import clsx, { type ClassValue } from "clsx";
import tg from "@/tg";
import classes from "./Button.module.css";
type Props = Omit<HTMLMotionProps<"button">, "className"> & {
@@ -15,10 +17,14 @@ const VARIANTS_MAP = {
yellow: classes.yellowButton,
} satisfies Record<Exclude<Props["variant"], undefined>, string>;
export default function Button({ className, variant = "blue", ...props }: Props) {
export default function Button({ className, variant = "blue", onClick, ...props }: Props) {
return (
<motion.button
{...props}
onClick={(e) => {
tg.hapticFeedback.click();
onClick?.(e);
}}
initial={{ scale: 0.5 }}
animate={{ scale: 1 }}
whileTap={{ scale: 0.95 }}

View File

@@ -1,6 +1,6 @@
import "i18next";
import type { resources } from "../public/locales/en.d.ts";
import type { resources } from "../../public/locales/en.d.ts";
declare module "i18next" {
interface CustomTypeOptions {

31
src/i18n/index.ts Normal file
View File

@@ -0,0 +1,31 @@
import i18next from "i18next";
import Backend, { type HttpBackendOptions } from "i18next-http-backend";
import { initReactI18next } from "react-i18next";
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 default {
init() {
i18next
.use(Backend)
.use(initReactI18next)
.init<HttpBackendOptions>({
backend: {
loadPath: `/${__LOCALES_PATH__}/{{lng}}.json`,
},
fallbackLng: __DEFAULT_LANG__,
supportedLngs: __LANGS__,
debug: import.meta.env.DEV,
interpolation: {
escapeValue: false,
},
});
},
};

View File

@@ -1,27 +0,0 @@
import i18next from "i18next";
import Backend, { type HttpBackendOptions } from "i18next-http-backend";
import { initReactI18next } from "react-i18next";
declare const __LANGS__: string[];
declare const __LOCALES_PATH__: string;
declare const __DEFAULT_LANG__: string;
export const languages = __LANGS__.map((key) => ({
key,
label: key.toUpperCase(),
}));
i18next
.use(Backend)
.use(initReactI18next)
.init<HttpBackendOptions>({
backend: {
loadPath: `/${__LOCALES_PATH__}/{{lng}}.json`,
},
fallbackLng: __DEFAULT_LANG__,
supportedLngs: __LANGS__,
debug: import.meta.env.DEV,
interpolation: {
escapeValue: false,
},
});

View File

@@ -3,9 +3,11 @@ import ReactDOM from "react-dom/client";
import { RouterProvider, createRouter } from "@tanstack/react-router";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import "./styles/index.css";
import i18n from "@/i18n";
import tg from "@/tg";
import { routeTree } from "./routeTree.gen";
import "./i18next";
import "./styles/index.css";
const router = createRouter({ routeTree });
@@ -16,6 +18,9 @@ declare module "@tanstack/react-router" {
}
}
tg.init();
i18n.init();
ReactDOM.createRoot(document.getElementById("root")!).render(
<StrictMode>
<QueryClientProvider client={new QueryClient()}>

View File

@@ -15,7 +15,7 @@ import SwitchInput from "@components/form/SwitchInput";
import TextInput from "@components/form/TextInput";
import NumberInput from "@components/form/NumberInput";
import TextAreaInput from "@components/form/TextAreaInput";
import ActionModal from "@components/modal/ActionModal";
import ActionModal from "@components/modals/ActionModal";
const TABS = [
{ key: "tab1", title: "Tab 1" },

View File

@@ -1,6 +1,6 @@
@font-face {
font-family: "BalsamiqSans";
src: url("/fonts/BalsamicSans/BalsamiqSans-Regular.ttf") format("truetype");
src: url("./BalsamiqSans-Regular.ttf") format("truetype");
font-weight: 400;
font-style: normal;
font-display: swap;
@@ -8,7 +8,7 @@
@font-face {
font-family: "BalsamiqSans";
src: url("/fonts/BalsamicSans/BalsamiqSans-Italic.ttf") format("truetype");
src: url("./BalsamiqSans-Italic.ttf") format("truetype");
font-weight: 400;
font-style: italic;
font-display: swap;
@@ -16,7 +16,7 @@
@font-face {
font-family: "BalsamiqSans";
src: url("/fonts/BalsamicSans/BalsamiqSans-Bold.ttf") format("truetype");
src: url("./BalsamiqSans-Bold.ttf") format("truetype");
font-weight: 700;
font-style: normal;
font-display: swap;
@@ -24,7 +24,7 @@
@font-face {
font-family: "BalsamiqSans";
src: url("/fonts/BalsamicSans/BalsamiqSans-BoldItalic.ttf") format("truetype");
src: url("./BalsamiqSans-BoldItalic.ttf") format("truetype");
font-weight: 700;
font-style: italic;
font-display: swap;

View File

@@ -1,6 +1,6 @@
@import "tailwindcss";
@import "./fonts/BalsamiqSans.css";
@import "./fonts/BalsamiqSans";
@theme {
}

52
src/tg/index.ts Normal file
View File

@@ -0,0 +1,52 @@
import * as tg from "@telegram-apps/sdk-react";
export const STORAGE_KEYS = {
authToken: "authToken",
} as const;
export type StorageKey = (typeof STORAGE_KEYS)[keyof typeof STORAGE_KEYS];
export default {
init: () => {
try {
tg.setDebug(import.meta.env.DEV);
tg.init({ acceptCustomStyles: true });
tg.requestFullscreen();
tg.disableVerticalSwipes();
tg.expandViewport();
tg.setMiniAppHeaderColor("#000000");
} catch {
console.warn("Telegram SDK not available in browser.");
}
},
openLink(url: string | URL, options?: tg.OpenLinkOptions) {
tg.openLink.ifAvailable(url, options);
},
hapticFeedback: {
click() {
tg.hapticFeedback.impactOccurred.ifAvailable("light");
},
},
initData: tg.initData,
storage: {
clear() {
localStorage.clear();
tg.cloudStorage.clear.ifAvailable();
},
getItem(key: StorageKey, options?: tg.InvokeCustomMethodOptions) {
return tg.cloudStorage
.getItem(key, options)
.catch(
() =>
localStorage.getItem(key) ?? tg.AbortablePromise.reject(new Error("Item not found")),
);
},
setItem(key: StorageKey, value: string, options?: tg.InvokeCustomMethodOptions) {
localStorage.setItem(key, value);
tg.cloudStorage.setItem.ifAvailable(key, value, options);
},
deleteItem(key: StorageKey, options?: tg.InvokeCustomMethodOptions) {
tg.cloudStorage.deleteItem.ifAvailable(key, options);
localStorage.removeItem(key);
},
},
};

3
src/vite-env.d.ts vendored
View File

@@ -4,6 +4,9 @@ interface ViteTypeOptions {
interface ImportMetaEnv {
MODE: "development" | "production" | "staging";
VITE_APP_NAME: string;
VITE_APP_URL: string;
VITE_API_BASE_URL: string;
}
interface ImportMeta {

View File

@@ -21,5 +21,5 @@
"erasableSyntaxOnly": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
"include": ["vite.config.ts", "plugins/**/*.ts"]
}

View File

@@ -1,3 +1,5 @@
import path from "node:path";
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
import tailwindcss from "@tailwindcss/vite";
@@ -9,7 +11,6 @@ import i18nextConfig, {
LOCALES_PATH,
LOCAL_LOCALES_PATH,
} from "./i18next.config";
import path from "node:path";
export default defineConfig({
plugins: [