diff --git a/.env b/.env deleted file mode 100644 index e69de29..0000000 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..a628cc1 --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +VITE_API_BASE_URL=http://localhost:5004/api/v1/ +VITE_APP_URL=http://localhost:5173 +VITE_APP_NAME=Honey diff --git a/.env.production b/.env.production deleted file mode 100644 index e69de29..0000000 diff --git a/.env.staging b/.env.staging index e69de29..a628cc1 100644 --- a/.env.staging +++ b/.env.staging @@ -0,0 +1,3 @@ +VITE_API_BASE_URL=http://localhost:5004/api/v1/ +VITE_APP_URL=http://localhost:5173 +VITE_APP_NAME=Honey diff --git a/.gitignore b/.gitignore index f04de22..e6ce752 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,5 @@ dist-ssr .vite .tanstack + +.env diff --git a/README.md b/README.md index cd7adf6..4369b76 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/package.json b/package.json index dbd4da9..3b148c8 100644 --- a/package.json +++ b/package.json @@ -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" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cb38b78..e8b4c7b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/src/api/api.ts b/src/api/api.ts new file mode 100644 index 0000000..261ebe3 --- /dev/null +++ b/src/api/api.ts @@ -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; diff --git a/src/api/index.ts b/src/api/index.ts new file mode 100644 index 0000000..78703ca --- /dev/null +++ b/src/api/index.ts @@ -0,0 +1 @@ +export { default as api } from "./api"; diff --git a/src/components/atoms/Button/Button.tsx b/src/components/atoms/Button/Button.tsx index fa5cfa7..5aa4cfc 100644 --- a/src/components/atoms/Button/Button.tsx +++ b/src/components/atoms/Button/Button.tsx @@ -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, "className"> & { @@ -15,10 +17,14 @@ const VARIANTS_MAP = { yellow: classes.yellowButton, } satisfies Record, string>; -export default function Button({ className, variant = "blue", ...props }: Props) { +export default function Button({ className, variant = "blue", onClick, ...props }: Props) { return ( { + tg.hapticFeedback.click(); + onClick?.(e); + }} initial={{ scale: 0.5 }} animate={{ scale: 1 }} whileTap={{ scale: 0.95 }} diff --git a/src/components/modal/ActionModal/ActionModal.module.css b/src/components/modals/ActionModal/ActionModal.module.css similarity index 100% rename from src/components/modal/ActionModal/ActionModal.module.css rename to src/components/modals/ActionModal/ActionModal.module.css diff --git a/src/components/modal/ActionModal/ActionModal.tsx b/src/components/modals/ActionModal/ActionModal.tsx similarity index 100% rename from src/components/modal/ActionModal/ActionModal.tsx rename to src/components/modals/ActionModal/ActionModal.tsx diff --git a/src/components/modal/ActionModal/index.ts b/src/components/modals/ActionModal/index.ts similarity index 100% rename from src/components/modal/ActionModal/index.ts rename to src/components/modals/ActionModal/index.ts diff --git a/src/i18next.d.ts b/src/i18n/index.d.ts similarity index 68% rename from src/i18next.d.ts rename to src/i18n/index.d.ts index 69e850c..0d43767 100644 --- a/src/i18next.d.ts +++ b/src/i18n/index.d.ts @@ -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 { diff --git a/src/i18n/index.ts b/src/i18n/index.ts new file mode 100644 index 0000000..8360e30 --- /dev/null +++ b/src/i18n/index.ts @@ -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({ + backend: { + loadPath: `/${__LOCALES_PATH__}/{{lng}}.json`, + }, + fallbackLng: __DEFAULT_LANG__, + supportedLngs: __LANGS__, + debug: import.meta.env.DEV, + interpolation: { + escapeValue: false, + }, + }); + }, +}; diff --git a/src/i18next.ts b/src/i18next.ts deleted file mode 100644 index 7a74619..0000000 --- a/src/i18next.ts +++ /dev/null @@ -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({ - backend: { - loadPath: `/${__LOCALES_PATH__}/{{lng}}.json`, - }, - fallbackLng: __DEFAULT_LANG__, - supportedLngs: __LANGS__, - debug: import.meta.env.DEV, - interpolation: { - escapeValue: false, - }, - }); diff --git a/src/main.tsx b/src/main.tsx index 9b4a37d..fd3ec5b 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -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( diff --git a/src/routes/game/-/GameRoute.tsx b/src/routes/game/-/GameRoute.tsx index 14a5f73..03608aa 100644 --- a/src/routes/game/-/GameRoute.tsx +++ b/src/routes/game/-/GameRoute.tsx @@ -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" }, diff --git a/public/fonts/BalsamicSans/BalsamiqSans-Bold.ttf b/src/styles/fonts/BalsamiqSans/BalsamiqSans-Bold.ttf similarity index 100% rename from public/fonts/BalsamicSans/BalsamiqSans-Bold.ttf rename to src/styles/fonts/BalsamiqSans/BalsamiqSans-Bold.ttf diff --git a/public/fonts/BalsamicSans/BalsamiqSans-BoldItalic.ttf b/src/styles/fonts/BalsamiqSans/BalsamiqSans-BoldItalic.ttf similarity index 100% rename from public/fonts/BalsamicSans/BalsamiqSans-BoldItalic.ttf rename to src/styles/fonts/BalsamiqSans/BalsamiqSans-BoldItalic.ttf diff --git a/public/fonts/BalsamicSans/BalsamiqSans-Italic.ttf b/src/styles/fonts/BalsamiqSans/BalsamiqSans-Italic.ttf similarity index 100% rename from public/fonts/BalsamicSans/BalsamiqSans-Italic.ttf rename to src/styles/fonts/BalsamiqSans/BalsamiqSans-Italic.ttf diff --git a/public/fonts/BalsamicSans/BalsamiqSans-Regular.ttf b/src/styles/fonts/BalsamiqSans/BalsamiqSans-Regular.ttf similarity index 100% rename from public/fonts/BalsamicSans/BalsamiqSans-Regular.ttf rename to src/styles/fonts/BalsamiqSans/BalsamiqSans-Regular.ttf diff --git a/src/styles/fonts/BalsamiqSans.css b/src/styles/fonts/BalsamiqSans/index.css similarity index 58% rename from src/styles/fonts/BalsamiqSans.css rename to src/styles/fonts/BalsamiqSans/index.css index b016752..33a23b5 100644 --- a/src/styles/fonts/BalsamiqSans.css +++ b/src/styles/fonts/BalsamiqSans/index.css @@ -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; diff --git a/src/styles/index.css b/src/styles/index.css index a1e22fb..dd52991 100644 --- a/src/styles/index.css +++ b/src/styles/index.css @@ -1,6 +1,6 @@ @import "tailwindcss"; -@import "./fonts/BalsamiqSans.css"; +@import "./fonts/BalsamiqSans"; @theme { } diff --git a/src/tg/index.ts b/src/tg/index.ts new file mode 100644 index 0000000..8c1b790 --- /dev/null +++ b/src/tg/index.ts @@ -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); + }, + }, +}; diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 6e551a2..476a6ab 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -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 { diff --git a/tsconfig.node.json b/tsconfig.node.json index 5033235..d0aad51 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -21,5 +21,5 @@ "erasableSyntaxOnly": true, "noUncheckedSideEffectImports": true }, - "include": ["vite.config.ts"] + "include": ["vite.config.ts", "plugins/**/*.ts"] } diff --git a/vite.config.ts b/vite.config.ts index 06ba4c3..6f32be0 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -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: [