No files matched your search
@@ -0,0 +1,30 @@
|
||||
name: Deploy frontend
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
|
||||
jobs:
|
||||
goida:
|
||||
runs-on: ubuntu-latest
|
||||
name: Build and deploy
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
- name: Deploy site
|
||||
uses: appleboy/ssh-action@v1.2.1
|
||||
with:
|
||||
host: ${{ secrets.SSH_HOST }}
|
||||
username: ${{ secrets.SSH_USER }}
|
||||
password: ${{ secrets.SSH_PASSWORD }}
|
||||
key: ${{ secrets.SSH_KEY }}
|
||||
port: 22
|
||||
script: |
|
||||
cd what_is_bot
|
||||
git pull origin master
|
||||
cd frontend
|
||||
npm i
|
||||
npm run build
|
||||
chown -R :www-data dist
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
DEBUG=true
|
||||
VITE_API_BASE_URL=https://whatisbot-api.ruka.me/api/v1
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
.env
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
# Vue 3 + TypeScript + Vite
|
||||
|
||||
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
|
||||
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).
|
||||
|
||||
## Setup for development
|
||||
|
||||
Clone this repository, fill `.env.local` with your values and run with `npm run dev`!
|
||||
@@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<script src="https://telegram.org/js/telegram-web-app.js"></script>
|
||||
<title>Vite + Vue + TS</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev-host": "vite --host",
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc -b && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.12.1",
|
||||
"pinia": "^3.0.3",
|
||||
"vue": "^3.5.18",
|
||||
"vue-tg": "^0.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.3.1",
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"@vue/tsconfig": "^0.7.0",
|
||||
"typescript": "~5.8.3",
|
||||
"vite": "^7.1.2",
|
||||
"vite-plugin-vue-devtools": "^8.0.2",
|
||||
"vue-tsc": "^3.0.5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg version="1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" enable-background="new 0 0 48 48">
|
||||
<polygon fill="#FFC107" points="33,22 23.6,22 30,5 19,5 13,26 21.6,26 17,45"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 196 B |
|
After Width: | Height: | Size: 10 KiB |
@@ -0,0 +1,10 @@
|
||||
<svg width="385" height="116" viewBox="0 0 385 116" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<path d="M8.25 94V31.1797H46.5781V39.9023H18.5625V56.5742H26.082C32.0404 56.5742 36.8242 57.362 40.4336 58.9375C44.043 60.4844 46.6497 62.6471 48.2539 65.4258C49.8581 68.2044 50.6602 71.3984 50.6602 75.0078C50.6602 80.9948 48.7552 85.6641 44.9453 89.0156C41.1641 92.3385 35.0195 94 26.5117 94H8.25ZM18.5625 85.4062H25.8672C30.7083 85.4062 34.3034 84.5898 36.6523 82.957C39.0013 81.2956 40.1758 78.6458 40.1758 75.0078C40.1758 72.4583 39.6029 70.4818 38.457 69.0781C37.3112 67.6458 35.5924 66.6432 33.3008 66.0703C31.0378 65.4974 28.1732 65.2109 24.707 65.2109H18.5625V85.4062ZM55.043 46.4336H66.043L75.7969 73.5898C76.2266 74.8216 76.6133 76.0391 76.957 77.2422C77.3294 78.4167 77.6445 79.5768 77.9023 80.7227C78.1888 81.8685 78.418 83.0143 78.5898 84.1602H78.8477C79.1341 82.6706 79.5352 81.0091 80.0508 79.1758C80.5951 77.3138 81.1966 75.4518 81.8555 73.5898L91.2227 46.4336H102.094L81.6836 100.531C80.5091 103.625 79.0625 106.26 77.3438 108.438C75.6536 110.643 73.6484 112.305 71.3281 113.422C69.0078 114.568 66.3581 115.141 63.3789 115.141C61.9466 115.141 60.7005 115.055 59.6406 114.883C58.5807 114.74 57.6784 114.582 56.9336 114.41V106.332C57.5352 106.475 58.2943 106.604 59.2109 106.719C60.1276 106.833 61.0729 106.891 62.0469 106.891C63.8516 106.891 65.4128 106.533 66.7305 105.816C68.0482 105.1 69.1654 104.083 70.082 102.766C70.9987 101.477 71.7578 100.001 72.3594 98.3398L74.0352 93.8711L55.043 46.4336ZM142.312 46.4336V65.5547H164.57V46.4336H174.711V94H164.57V73.4609H142.312V94H132.172V46.4336H142.312ZM204.66 45.5312C209.329 45.5312 213.426 46.4479 216.949 48.2812C220.501 50.0859 223.266 52.793 225.242 56.4023C227.219 60.0117 228.207 64.4948 228.207 69.8516C228.207 75.1797 227.305 79.7057 225.5 83.4297C223.695 87.1536 221.16 89.9896 217.895 91.9375C214.658 93.8854 210.862 94.8594 206.508 94.8594C202.297 94.8594 198.659 94 195.594 92.2812C192.529 90.5339 190.18 88.0417 188.547 84.8047C186.914 81.5677 186.098 77.7292 186.098 73.2891V67.918H217.895C217.78 63.306 216.534 59.7539 214.156 57.2617C211.779 54.7409 208.456 53.4805 204.188 53.4805C201.18 53.4805 198.458 53.7669 196.023 54.3398C193.617 54.9128 191.125 55.8008 188.547 57.0039V48.7539C190.924 47.6654 193.345 46.8633 195.809 46.3477C198.272 45.8034 201.223 45.5312 204.66 45.5312ZM217.68 75.1367H196.023C196.052 77.4857 196.439 79.5768 197.184 81.4102C197.928 83.2435 199.074 84.6758 200.621 85.707C202.168 86.7383 204.13 87.2539 206.508 87.2539C209.688 87.2539 212.266 86.2227 214.242 84.1602C216.219 82.0977 217.365 79.0898 217.68 75.1367ZM264.258 45.5312C269.901 45.5312 274.427 47.5938 277.836 51.7188C281.273 55.8438 282.992 61.974 282.992 70.1094C282.992 75.4948 282.19 80.0352 280.586 83.7305C279.01 87.3971 276.79 90.1758 273.926 92.0664C271.09 93.9284 267.781 94.8594 264 94.8594C261.594 94.8594 259.503 94.5443 257.727 93.9141C255.951 93.2839 254.432 92.4674 253.172 91.4648C251.911 90.4336 250.852 89.3164 249.992 88.1133H249.391C249.534 89.2591 249.663 90.5339 249.777 91.9375C249.921 93.3125 249.992 94.5729 249.992 95.7188V115.098H239.852V46.4336H248.102L249.52 53.0078H249.992C250.88 51.6615 251.954 50.4154 253.215 49.2695C254.504 48.1237 256.051 47.2214 257.855 46.5625C259.689 45.875 261.823 45.5312 264.258 45.5312ZM261.551 53.7812C258.772 53.7812 256.538 54.3398 254.848 55.457C253.186 56.5456 251.969 58.1927 251.195 60.3984C250.451 62.6042 250.049 65.3685 249.992 68.6914V70.1094C249.992 73.6328 250.35 76.6263 251.066 79.0898C251.811 81.5247 253.029 83.3867 254.719 84.6758C256.438 85.9362 258.758 86.5664 261.68 86.5664C264.143 86.5664 266.177 85.8932 267.781 84.5469C269.414 83.2005 270.632 81.2812 271.434 78.7891C272.236 76.2969 272.637 73.3607 272.637 69.9805C272.637 64.8529 271.72 60.8711 269.887 58.0352C268.082 55.1992 265.303 53.7812 261.551 53.7812ZM313.844 94.8594C309.346 94.8594 305.451 93.9857 302.156 92.2383C298.862 90.4909 296.327 87.8125 294.551 84.2031C292.775 80.5938 291.887 76.0104 291.887 70.4531C291.887 64.6667 292.861 59.9401 294.809 56.2734C296.757 52.6068 299.449 49.8997 302.887 48.1523C306.353 46.4049 310.32 45.5312 314.789 45.5312C317.625 45.5312 320.189 45.8177 322.48 46.3906C324.801 46.9349 326.763 47.6081 328.367 48.4102L325.359 56.4883C323.612 55.7721 321.822 55.1706 319.988 54.6836C318.155 54.1966 316.393 53.9531 314.703 53.9531C311.924 53.9531 309.604 54.569 307.742 55.8008C305.909 57.0326 304.534 58.8659 303.617 61.3008C302.729 63.7357 302.285 66.7578 302.285 70.3672C302.285 73.862 302.743 76.8125 303.66 79.2188C304.577 81.5964 305.938 83.401 307.742 84.6328C309.547 85.8359 311.767 86.4375 314.402 86.4375C317.009 86.4375 319.344 86.1224 321.406 85.4922C323.469 84.862 325.417 84.0456 327.25 83.043V91.8086C325.445 92.8398 323.512 93.599 321.449 94.0859C319.387 94.6016 316.852 94.8594 313.844 94.8594ZM353.934 45.5312C358.603 45.5312 362.699 46.4479 366.223 48.2812C369.775 50.0859 372.539 52.793 374.516 56.4023C376.492 60.0117 377.48 64.4948 377.48 69.8516C377.48 75.1797 376.578 79.7057 374.773 83.4297C372.969 87.1536 370.434 89.9896 367.168 Line truncated
|
||||
<rect x="261.974" y="13.9781" width="54.1079" height="39.0779" transform="rotate(40.0585 261.974 13.9781)" fill="url(#pattern0_13214_856)"/>
|
||||
<defs>
|
||||
<pattern id="pattern0_13214_856" patternContentUnits="objectBoundingBox" width="1" height="1">
|
||||
<use xlink:href="#image0_13214_856" transform="matrix(0.00252207 0 0 0.00348797 -0.176923 -0.245117)"/>
|
||||
</pattern>
|
||||
<image id="image0_13214_856" width="610" height="412" preserveAspectRatio="none" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAmIAAAGcCAYAAAB6JswSAAAAAXNSR0IArs4c6QAAIABJREFUeF7s3QeQZdd5H/j/zfe9+3IOnbune7onImdAoCjbCrRIyippZUsOqtXWrkwvRVm2SIfFlleUtCutrEBRlESbS0paligRDAAIBiGnwWAGGEyenunpnN7rl9/NYeuc4aq8VVs2y4LZA87X4FQXZnr6vvd796H//M53viOAPkiABEiABEiABEiABPZFQNiXq9JFSYAESIAESIAESIAEQEGMbgISIAESIAESIAES2CcBCmL7BE+XJQESIAESIAESIAEKYnQPkAAJkAAJkAAJkMA+CVAQ2yd4uiwJkAAJkAAJkAAJUBCje4AESIAESIAESIAE9kmAgtg+wdNlSYAESIAESIAESICCGN0DJEACJEACJEACJLBPAhTE9gmeLksCJEACJEACJEACFMToHiABEiABEiABEiCBfRKgILZP8HRZEiABEiABEiABEqAgRvcACZAACZAACZAACeyTAAWxfYKny5IACZAACZAACZAABTG6B0iABEiABEiABEhgnwQoiO0TPF2WBEiABEiABEiABCiI0T1AAiRAAiRAAiRAAvskQEFsn+DpsiRAAiRAAiRAAiRAQYzuARIgARIgARIgARLYJwEKYvsET5clARIgARIgARIgAQpidA+QAAmQAAmQAAmQwD4JUBDbJ3i6LAmQAAmQAAmQAAlQEKN7gARIgARIgARIgAT2SYCC2D7B02VJgARIgARIgARIgIIY3QMkQAIkQAIkQAIksE8CFMT2CZ4uSwIkQAIkQAIkQAIUxOgeIAESIAESIAESIIF9EqAgtk/wdFkSIAESIAESIAESoCBG9wAJkAAJkAAJkAAJ7JMABbF9gqfLkgAJkAAJkAAJkAAFMboHSIAESIAESIAESGCfBCiI7RM8XZYESIAESIAESIAEKIjRPUACJEACJEACJEAC+yRAQWyf4OmyJEACJEACJEACJEBBjO4BEiABEiABEiABEtgnAQpi+wRPlyUBEiABEiABEiABCmJ0D5AACZAACZAACZDAPglQENsneLosCZAACZAACZAACVAQo3uABEiABEiABEiABPZJgILYPsHTZUmABEiABEiABEiAghjdAyRAAiRAAiRAAiSwTwIUxPYJni5LAiRAAiRAAiRAAhTE6B4gARIgARIgARIggX0SoCC2T/B0WRIgARIgARIgARKgIEb3AAmQAAmQAAmQAAnskwAFsX2Cp8uSAAmQAAmQAAmQAAUxugdIgARIgARIgARIYJ8EKIjtEzxdlgRIgARIgARIgAQoiNE9QAIkQAIkQAIkQAL7JEBBbJ/g6bIkQAIkQAIkQAIkQEGM7gESIAESIAESIAES2CcBCmL7BE+XJQESIAESIAESIAEKYnQPkAAJkAAJkAAJkMA+CVAQ2yd4uiwJkAAJkAAJkAAJUBCje4AESIAESIAESIAE9kmAgtg+wdNlSYAESIAESIAESICCGN0DJEACJEACJEACJLBPAhTE9gmeLksCJEACJEACJEACFMToHiABEiABEiABEiCBfRKgILZP8HRZEiABEiABEiABEqAgRvcACZAACZAACZAACeyTAAWxfYKny5IACZAACZAACZAABTG6B0iABEiABEiABEhgnwQoiO0TPF2WBEiABEiABEiABCiI0T1AAiRAAiRAAiRAAvskQEFsn+DpsiRAAiRAAiRAAiRAQYzuARIgARIgARIgARLYJwEKYvsET5clARIgARIgARIgAQpidA+QAAmQAAmQAAmQwD4JUBDbJ3i6LAmQAAmQAAmQAAlQEKN7gARIgARIgARIgAT2SYCC2D7B02VJgARIgARIgARIgIIY3QMkQAIkQAIkQAIksE8CFMT2CZ4uSwIkQAIkQAIkQAIUxOgeIAESIAESIAESIIF9EqAgtk/wdFkSIAESIAESIAESoCBG9wAJkAAJkAAJkAAJ7JMABbF9gqfLkgAJkAAJkAAJkAAFMboHSIAESIAESIAESGCfBCiI7RM8XZYESIAESIAESIAEKIjRPUACJEACJEACJEAC+yRAQWyf4OmyJEACt7bAT/3yL2clX1c80ZTjiEOWlchQY85vPfaR1q0tQ8+eBG4tAQpit9brTc+WBEjgbyjw8x/9aD7yfcUJQy1wwlTouQnH85K+6yf9MDAkUY55QaDajqc6rquEiijFYwkhl80gk85FghQJtm2JjmfJduBIru3JYiQJ6WQmyGcynhqXPajwRQmh4/qhY7m+Z4V+EAS2GGHgw2qHTr8lQ2jGdLH7iV/9xN7f8CnRXycBEthHAQpi+4hPlyYBErh5BD782G9lAtVSIElaGARxx3X1yIPuR25SiiJDlrWEGAlGFPlJVZTSkiCkBdfNmL1e2jTtVCgiEQZRvD+0lKE1lFw/lAfmUNA0XUin08gX80IilhB8IZRs0xIszxI63Zagx2KCoScR15NRPKZFiiJGgooQ8INIEEPPCSLPEwIg8uH7tuMMu5bV2O0PBmuVcvlyvVw7W8hnrv7S//hLuzePJj0SEiCB71SAgth3KkVfRwIk8D0j8OGP/Uo1FK2ELwgZQZAzkihkRUXJSJKUiyQh47pu2vOcpBcEcRGIC5EYj+tqDJGky0JoKJKi+46jOL2B5jhOstvtw7RtWJaFgWkhCALIigY1piNmxJFKpaDqOgLPw8A0EYYhJEWEIIfI5ArIpQsQBAGB5yOChxA+gtCDF0Tw/AAIJYgRYFtD9Ht78NweBr32ztTE9Or8ofm3ZybHXw0inNZUd/WD7/2HVCH7nrlT6YncCgIUxG6FV5meIwncggK/+BuPFUIPKVUQY0KkxAEYPpB1Ta9oW3ZVFsSq7TolLaZnM8lkQk3EjTCM4l7kG67j6F4YJPr9PnRdByAi9Fy4tgfP8+B7Hjp7LTj9IRRJhu06YF8bBBFUTUMikUA8mYAejwGiCFEU4bou/xrX93gwyxWyUHQF8YQBRdJhmTeCHFjxS/ARRAEEScJwaMJ1QsQ0DbqqIApt2MM2XHuAWqGC8fHx7ZnJmaV8KfmWHlNPiZFw1nKx9N573kuB7Ba87+kpv/sEKIi9+14zesQkQALfFvj5j380LzmqIUlhMhDDdGCHGUkOU7Igpp3AyflulI3psZQRS6ZUSU9ZnpPtt4YZy3KSQRjGbdfOi7LAg1MsYUCUJPhCBD8M4Ps+HMdBFEVwHR+uY4FXvvoDIIzg2g4iz4ehx3iFi1XBDCOBZCZ94/vF4zx8uYHPP1sOC1omBElELpdDvpQHu3YkCghdAaZpYjgcwnUdHsQggX9t4EcIQxGKoiBt6NA1EWHQhz0cQPQCjNbqmJ6YRrlSWJ8YH1suldIXfMd/q28Ozw5t6+rdhx7dphuGBEjg5hWgIHbzvjb0yEiABP4TgY/91q9URU9J+KKd8r0wGwZh3jKdwmBoltLxRK4yWkw7lpNu7m6nXMdKiqKUVPVYTFEUTVPiuiQqqU6vj25nCM8LILBu+DBEu9eB6/tIpZJI5/NIpBIQBBGmYyEeN7Czs4293SYPSN12F4PBAJlMBsVcHnFNB4KQV8lYUGK/L0kS/3e+1IgIvs+WGgFBwI3AFoVQVRW6oSMSQv51kqgjDIFep4299h4su49IjHhFrVAsI2FkYJkmBp09+J4JRENEvoOYqGJ+dg5HFg5hdLSOSqGIZNLYTCbia7GYerE/6Jzq9Yanh13v6rFjD1APGb2jSOAmFKAgdhO+KPSQSOBWF/joxz9ehIasLClZSYjyMSNe7LR65Uw6VdI0tWhZVsEa2rnNrZ2k67qJwPMNWYbSHfRlxzaNRDIO1iAPQUKr1cJeu8/DVz5XgGm68NwAiqbxQOR6Hvr9IfzIRy5XQCaXhiQpAItPooxGc4dXxDRdge/4sG2bvzyZVBqyAF45CwOfB6qkkYAsy+j1eryaxv5MlICYbiCVSfLrxWIx9IcDXmlj12SfPRewbZdX2wbWAF7o8CDGqmrsMbHHw67b3dsFIhcj9SwQuMjGE7jv7nv4r5HROlRBgiBE0DUNCIMdWRRWXGt4rtfvndhsNd5o7fSWHn30A51b/f6i508CN5MABbGb6dWgx0ICt5AA7+GyPc2OvETghhlBEFOaKGclWSogRNl33XK+WCjoqpbXNS3d7XYTsqbGbcs2LNPMNBp7GAwtHnwsa8jDT3fQhR5TMT8/j6mZSRjxJE6ePIUzZ88hCgVMTU1DgIQgECCrGmRV4X9fllXYtolejwWkAJqi80DT7d/4d12Ps0oT7xdj4Ypdy3RsqJIMRZH50qMoRJAgwHNcXvlybPvG74uApmkolUooFErQWRDr99HptuC6Nu8rc+wQtuXy8BYigKxLUHUFoqzCZEugLBMKgCYB6ZSOQiGOhK6gmM5iZmoK83NzqFXKkAURkvjtkMjCZMT6zaTNwLdXWnt7py4sXnplu9E69ZMf/Lkrt9CtRk+VBG5qAQpiN/XLQw+OBN79Aj/32C8WFA9GoAhJRVSyQiTmRFnMh4Ff8Hwv59pBTpWljCQIGcuy065tp2RVjVdKZSOVTSvxWDw1tEy02200Gg1sbW3x6pDjBZBkFdVqFelMkvdl6XENmUwKM3OzmJqa4st9S9ev47nnXuCBLJ5IYmZmFiMjI9D0OAYDkwexAqs6Abh8+QquXV2EPbAQIYQoSOh02/DDCJVqGcVSiYeozZ1t9AZdqLqG6elpJOIGotDH3m4Dw/4A+XQGnmPzwCYCiMfjSGUySKezvJeM9YKx5yNJAm/Qt8wAQRDyyhcLYFoixj9bjoe9vT2IgoK4rqKQz6BcSkGITMRVCbosI6bp6Lbb2N7cQCGXwd/+gffi4Ycegq6okAQRLine truncated
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 269 KiB |
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 32 32" version="1.1">
|
||||
<g id="surface1">
|
||||
<path style=" " d="M 16 4 C 10.886719 4 6.617188 7.160156 4.875 11.625 L 6.71875 12.375 C 8.175781 8.640625 11.710938 6 16 6 C 19.242188 6 22.132813 7.589844 23.9375 10 L 20 10 L 20 12 L 27 12 L 27 5 L 25 5 L 25 8.09375 C 22.808594 5.582031 19.570313 4 16 4 Z M 25.28125 19.625 C 23.824219 23.359375 20.289063 26 16 26 C 12.722656 26 9.84375 24.386719 8.03125 22 L 12 22 L 12 20 L 5 20 L 5 27 L 7 27 L 7 23.90625 C 9.1875 26.386719 12.394531 28 16 28 C 21.113281 28 25.382813 24.839844 27.125 20.375 Z "/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 693 B |
|
After Width: | Height: | Size: 99 KiB |
@@ -0,0 +1,16 @@
|
||||
<svg width="334" height="334" viewBox="0 0 334 334" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<path d="M108.914 5.448H3V108.912H0V2.448H108.914V5.448Z" fill="#FEFEFE"/>
|
||||
<path d="M328.555 108.912L328.555 2.99927H225.09L225.09 -0.000732422L331.555 -0.000732422L331.555 108.912H328.555Z" fill="#FEFEFE"/>
|
||||
<path d="M225.086 328.551H331V225.087H334V331.551H225.086V328.551Z" fill="#FEFEFE"/>
|
||||
<path d="M5.44525 225.087L5.44525 331H108.91V334H2.44525L2.44525 225.087H5.44525Z" fill="#FEFEFE"/>
|
||||
<rect x="51" y="72" width="232" height="237" fill="url(#pattern0_13218_1621)"/>
|
||||
<path d="M196.47 69.5735L177 77.0002L183.08 55.1526L196.47 69.5735Z" fill="#4A4A4A"/>
|
||||
<rect x="180.105" y="25" width="133.895" height="48.5062" rx="24.2531" fill="#4A4A4A"/>
|
||||
<path d="M233.521 50.9094C233.521 51.8625 233.354 52.675 233.021 53.3469C232.693 54.0188 232.222 54.5318 231.607 54.886C230.992 55.2349 230.261 55.4094 229.412 55.4094C228.625 55.4094 227.925 55.2349 227.31 54.886C226.701 54.5318 226.222 54.0188 225.873 53.3469C225.529 52.675 225.357 51.8625 225.357 50.9094C225.357 49.4823 225.724 48.3808 226.459 47.6047C227.193 46.8287 228.196 46.4407 229.466 46.4407C230.263 46.4407 230.966 46.6152 231.576 46.9641C232.185 47.3131 232.662 47.8209 233.005 48.4875C233.349 49.1542 233.521 49.9615 233.521 50.9094ZM229.443 53.9094C229.901 53.9094 230.282 53.8209 230.584 53.6438C230.886 53.4615 231.12 53.1933 231.287 52.8391C231.459 52.4849 231.563 52.0527 231.599 51.5422H227.263C227.305 52.0527 227.412 52.4849 227.584 52.8391C227.755 53.1933 227.995 53.4615 228.302 53.6438C228.61 53.8209 228.99 53.9094 229.443 53.9094ZM229.435 47.9485C228.763 47.9485 228.253 48.1386 227.904 48.5188C227.555 48.8938 227.344 49.4459 227.271 50.175H231.599C231.552 49.6907 231.443 49.2844 231.271 48.9563C231.099 48.623 230.862 48.373 230.56 48.2063C230.258 48.0344 229.883 47.9485 229.435 47.9485ZM239.115 55.4094C238.297 55.4094 237.589 55.2506 236.99 54.9329C236.391 54.6152 235.93 54.1282 235.607 53.4719C235.284 52.8157 235.123 51.9823 235.123 50.9719C235.123 49.9198 235.3 49.0605 235.654 48.3938C236.008 47.7271 236.498 47.2349 237.123 46.9172C237.753 46.5995 238.474 46.4407 239.287 46.4407C239.802 46.4407 240.269 46.4928 240.685 46.5969C241.107 46.6959 241.464 46.8183 241.755 46.9641L241.209 48.4329C240.891 48.3027 240.565 48.1933 240.232 48.1047C239.899 48.0162 239.578 47.9719 239.271 47.9719C238.766 47.9719 238.344 48.0839 238.005 48.3079C237.672 48.5318 237.422 48.8652 237.255 49.3079C237.094 49.7506 237.013 50.3 237.013 50.9563C237.013 51.5917 237.097 52.1282 237.263 52.5657C237.43 52.998 237.677 53.3261 238.005 53.55C238.334 53.7688 238.737 53.8782 239.216 53.8782C239.69 53.8782 240.115 53.8209 240.49 53.7063C240.865 53.5917 241.219 53.4433 241.552 53.261V54.8547C241.224 55.0422 240.873 55.1803 240.498 55.2688C240.123 55.3625 239.662 55.4094 239.115 55.4094ZM250.115 48.05H247.271V55.2532H245.443V48.05H242.615V46.6047H250.115V48.05ZM254.607 46.4407C255.456 46.4407 256.201 46.6073 256.841 46.9407C257.487 47.2688 257.99 47.761 258.349 48.4172C258.709 49.0735 258.888 49.8886 258.888 50.8625C258.888 51.8313 258.724 52.6542 258.396 53.3313C258.068 54.0084 257.607 54.524 257.013 54.8782C256.425 55.2323 255.735 55.4094 254.943 55.4094C254.177 55.4094 253.516 55.2532 252.959 54.9407C252.401 54.623 251.974 54.1698 251.677 53.5813C251.38 52.9928 251.232 52.2948 251.232 51.4875V50.511H257.013C256.992 49.6724 256.766 49.0266 256.334 48.5735C255.901 48.1152 255.297 47.886 254.521 47.886C253.974 47.886 253.479 47.9381 253.037 48.0422C252.599 48.1464 252.146 48.3079 251.677 48.5266V47.0266C252.11 46.8287 252.55 46.6829 252.998 46.5891C253.446 46.4902 253.982 46.4407 254.607 46.4407ZM256.974 51.8235H253.037C253.042 52.2506 253.112 52.6308 253.248 52.9641C253.383 53.2974 253.591 53.5579 253.873 53.7454C254.154 53.9329 254.511 54.0266 254.943 54.0266C255.521 54.0266 255.99 53.8391 256.349 53.4641C256.709 53.0891 256.917 52.5422 256.974 51.8235ZM268.123 55.2532H266.271V48.0422H263.873C263.769 49.3547 263.638 50.4771 263.482 51.4094C263.326 52.3417 263.13 53.1021 262.896 53.6907C262.662 54.274 262.373 54.7011 262.029 54.9719C261.685 55.2428 261.271 55.3782 260.787 55.3782C260.573 55.3782 260.38 55.3625 260.209 55.3313C260.037 55.3053 259.883 55.2636 259.748 55.2063V53.761C259.836 53.7922 259.933 53.8183 260.037 53.8391C260.141 53.8599 260.248 53.8704 260.357 53.8704C260.55 53.8704 260.727 53.7922 260.888 53.636C261.055 53.4745 261.209 53.2219 261.349 52.8782C261.49 52.5344 261.617 52.0813 261.732 51.5188C261.847 50.9511 261.951 50.2636 262.045 49.4563C262.138 48.6438 262.222 47.6933 262.295 46.6047H268.123V55.2532Z" fill="#FEFEFE"/>
|
||||
<defs>
|
||||
<pattern id="pattern0_13218_1621" patternContentUnits="objectBoundingBox" width="1" height="1">
|
||||
<use xlink:href="#image0_13218_1621" transform="matrix(0.00118694 0 0 0.00116302 -0.0697301 -0.337531)"/>
|
||||
</pattern>
|
||||
<image id="image0_13218_1621" width="960" height="1280" preserveAspectRatio="none" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA8AAAAUACAYAAACI7jsVAAAQAElEQVR4Aey9CbhkyVXf+T9xM/O9qu5Wb1pYJFaB2TeJHRsMHjyMF2zG1tgwDHj3GPhg8HgGjMENxuDxGMMHHmOzYzYhBBKSultisZCQelNJ3bVXd63d8mDm8wxG3V1Vb8m8Mb8TN/PVq+q9a3vL/757bkScOHHixC9uZt7z8nV1kQ8TMAETMAETMAETMAETMAETMAET2AUEdnkCvAt22Es0ARMwARMwARMwARMwARMwARNoBJwANwy79OJlm4AJmIAJmIAJmIAJmIAJmMAuIuAEeBdttpd6MQG3TMAETMAETMAETMAETMAEdhcBJ8C7a7+9WhNYEHBpAiZgAiZgAiZgAiZgAruOgBPgXbflXrAJmIBkBiZgAiZgAiZgAiZgAruRgBPg3bjrXrMJmMDuJuDVm4AJmIAJmIAJmMAuJeAEeJduvJdtAiZgAruVgNdtAiZgAiZgAiawewk4Ad69e++Vm4AJmIAJ7D4CXrEJmIAJmIAJ7GoCToB39fZ78SZgAiZgAiawmwh4rSZgAiZgArudgBPg3X4HeP0mYAImYAImYAK7g4BXaQImYAImICfAvglMwARMwARMwARMwAR2PAEv0ARMwASSgBPgpGAxARMwARMwARMwARMwgZ1LwCszAROYE3ACPAfhwgRMwARMwARMwARMwARMYCcS8JpM4AIBJ8AXWLhmAiZgAiZgAiZgAiZgAiZgAjuLgFdzEQEnwBfhcMMETMAETMAETMAETMAETMAETGCnELh0HU6ALyXitgmYgAmYgAmYgAmYgAmYgAmYwI4ksMsS4B25h16UCZiACZiACZiACZiACZiACZjA8yDgBPh5QNoxJl6ICZiACZiACZiACZiACZiACexiAk6Ad/Hm77ale70mYAImYAImYAImYAImYAK7m4AT4N29/1797iHglZqACZiACZiACZiACZjArifgBHjX3wIGYAK7gYDXaAImYAImYAImYAImYAKSE2DfBSZgAiaw0wl4fSZgAiZgAiZgAiZgAo2AE+CGwRcTMAETMIGdSsDrMgETMAETMAETMIEFASfACxIuTcAETMAETGDnEfCKTMAETMAETMAENhFwArwJhqsmYAImYAImYAI7iYDXYgImYAImYAIXE3ACfDEPt0zABEzABEzABExgZxDwKkzABEzABJ5CwAnwU5BYYQImYAImYAImYAImsN0JOH4TMAETeDoCToCfjop1JmACJmACJmACJmACJrB9CThyEzCBZyDgBPgZwFhtAiZgAiZgAiZgAiZgAiawHQk4ZhN4ZgJOgJ+ZjXtMwARMwARMwARMwARMwARMYHsRcLTPSsAJ8LPicacJmIAJmIAJmIAJmIAJmIAJmMB2IfBccToBfi5C7jcBEzABEzABEzABEzABEzABE9gRBHZ4Arwj9siLMAETMAETMAETMAETMAETMAETuAIEnABfAYhb1oUDMwETMAETMAETMAETMAETMAET2CDgBHgDhSs7jYDXYwImYAImYAImYAImYAImYAKbCTgB3kzDdRPYOQS8EhMwARMwARMwARMwARMwgUsIOAG+BIibJmACO4GA12ACJmACJmACJmACJmACTyXgBPipTKwxARMwge1NwNGbgAmYgAmYgAmYgAk8LQEnwE+LxUoTMAETMIHtSsBxm4AJmIAJmIAJmMAzEXAC/ExkrDcBEzABEzCB7UfAEZuACZiACZiACTwLASfAzwLHXSZgAiZgAiZgAtuJgGM1ARMwARMwgWcn4AT42fm41wRMwARMwARMwAS2BwFHaQImYAIm8JwEnAA/JyIbmIAJmIAJmIAJmIAJbHUCjs8ETMAEng8BJ8DPh5JtTMAETMAETMAETMAETGDrEnBkJmACz5OAE+DnCcpmJmACJmACJmACJmACJmACW5GAYzKB50/ACfDzZ2VLEzABEzABEzABEzABEzABE9haBBzNCyLgBPgF4bKxCZiACZiACZiACZiACZiACZjAViHwQuNwAvxCidneBEzABEzABEzABEzABEzABExgWxLYYQnwttwDB20CJmACJmACJmACJmACJmACJnANCDgBvgaQr9kUnsgETMAETMAETMAETMAETMAETOAZCTgBfkY07thuBByvCZiACZiACZiACZiACZiACTwbASfAz0bHfSawfQg4UhMwARMwARMwARMwARMwgecg4AT4OQC52wRMYDsQcIwmYAImYAImYAImYAIm8NwEnAA/NyNbmIAJmMDWJuDoTMAETMAETMAETMAEnhcBJ8DPC5ONTMAETMAEtioBx2UCJmACJmACJmACz5eAE+DnS8p2JmACJmACJrD1CDgiEzABEzABEzCBF0DACfALgGVTEzABEzABEzCBrUTAsZiACZiACZjACyPgBPiF8bK1CZiACZiACZiACWwNAo7CBEzABEzgBRNwAvyCkXmACZiACZiACZiACZjA9Sbg+U3ABEzgxRBwAvxiqHmMCZiACZiACZiACZiACVw/Ap7ZBEzgRRJwAvwiwXmYCZiACZiACZiACZiACZjA9SDgOU3gxRNwAvzi2XmkCZiACZiACZiACZiACZiACVxbAp7tsgg4Ab4sfB5sAiZgAiZgAiZgAiZgAiZgAiZwrQhc7jxOgC+XoMebgAmYgAmYgAmYgAmYgAmYgAlsCwLbPAHeFowdpAmYgAmYgAmYgAmYgAmYgAmYwBYg4AR4C2zCiw7BA03ABEzABEzABEzABEzABEzABJ43ASfAzxuVDbcaAcdjAiZgAiZgAiZgAiZgAiZgAi+EgBPgF0LLtiawdQg4EhMwARMwARMwARMwARMwgRdIwAnwCwRmcxMwga1AwDGYgAmYgAmYgAmYgAmYwAsn4AT4hTPzCBMwARO4vgQ8uwmYgAmYgAmYgAmYwIsi4AT4RWHzIBMwARMwgetFwPOagAmYgAmYgAmYwIsl4AT4xZLzOBMwARMwARO49gQ8owmYgAmYgAmYwGUQcAJ8GfA81ARMwARMwARM4FoS8FwmYAImYAImcHkEnABfHj+PNgETMAETMAETMIFrQ8CzmIAJmIAJXDYBJ8CXjdAOTMAETMAETMAETMAErjYB+zcBEzCBK0HACfCVoGgfJmACJmACJmACJmACJnD1CNizCZjAFSLgBPgKgbQbEzABEzABEzABEzABEzCBq0HAPk3gyhFwAnzlWNqTCZiACZiACZiACZiACZiACVxZAvZ2RQk4Ab6iOO3MBEzABEzABEzABEzABEzABEzgShG40n6cAF9povZnAiZgAiZgAiZgAiZgAiZgAiawJQlsswR4SzJ0UCZgAiZgAiZgAiZgAiZgAiZgAtuAgBPgbbBJGyG6YgImYAImYAImYAImYAImYAIm8KIJOAF+0eg88FoT8HwmYAImYAImYAImYAImYAImcDkEnABfDj2PNYFrR8AzmYAJmIAJmIAJmIAJmIAJXCYBJ8CXCdDDTcAErgUBz2ECJmACJmACJmACJmACl0/ACfDlM7QHEzABE7i6BOzdBEzABEzABEzABEzgihBwAnxFMNqJCZiACZjA1SJgvyZgAiZgAiZgAiZwpQg4Ab5SJO3HBEzABEzABK48AXs0ARMwARMwARO4ggScAF9BmHZlAiZgAiZgAiZwJQnYlwmYgAmYgAlcWQJOgK8sT3szARMwARMwARMwgStDwF5MwARMwASuOAEnwFccqR2agAmYgAmYgAmYgAlcLgGPNwETMIGrQcAJ8NWgap8mYAImYAImYAImYAIm8OIJeKQJmMBVIuAE+CqBtVsTMAETMAETMAETMAETMIEXQ8BjTODqEXACfPXY2rMJmIAJmIAJmIAJmIAJmIAJvDACtr6qBJwAX1W8dm4CJmACJmACJmACJmACJmACJvB8CVxtOyfAV5uw/ZuACZiACZiACZiACZiACZiACWwJAls8Ad4SjByECZiACZiACZiACZiACZiACZjADiDgBHgrb6JjMwETMAETMAETMAETMAETMAETuGIEnABfMZR2dKUJ2J8JmIAJmIAJmIAJmIAJmIAJXEkCToCvJE37MoErR8CeTMAETMAETMAETMAETMAErjABJ8BXGKjdmYAJXAkC9mECJmACJmACJmACJmACV56AE+Arz9QeTcAETODyCHi0CZiACZiACZiACZjAVSHgBPiqYLVTEzABEzCBF0vA40zABEzABEzABEzgahFwAny1yNqvCZiACZiACbxwAh5hAiZgAiZgAiZwFQk4Ab6KcO3aBEzABEzABEzghRCwrQmYgAmYgAlcXQJOgK8uX3s3ARMwARMwARMwgedHwFYmYAImYAJXnYAT4KuO2BOYgAmYgAmYgAmYgAk8FwH3m4AJmMC1IOAE+FpQ9hwmYAImYAImYAImYAIm8MwE3GMCJnCNCDgBvkagPY0JmIAJmIAJmIAJmIAJmMDTEbDOBK4dASfA1461ZzIBEzABEzABEzABEzABEzCBiwm4dU0JOAG+prg9mQmYgAmYgAmYgAmYgAmYgAmYwILAtS6dAF9r4p7PBEzABEzABEzABEzABEzABEzguhDYYgnwdWHgSU3ABEzABEzABEzABEzABEzABHYBASfAW2mTHYsJmIAJmIAJmIAJmIAJmIAJmMBVI+AE+KqhteMXSsD2JmACJmACJmACJmACJmACJnA1CTgBvpp07dsEnj8BW5qACZiACZiACZiACZiACVxlAk6ArzJguzcBE3g+BGxjAiZgAiZgAiZgAiZgAlefgBPgq8/YM5iACZjAsxNwrwmYgAmYgAmYgAmYwDUh4AT4mmD2JCZgAiZgAs9EwHoTMAETMAETMAETuFYEnABfK9KexwLine truncated
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 583 KiB |
@@ -0,0 +1 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,160 @@
|
||||
<script setup lang="ts">
|
||||
import './style.css';
|
||||
import { useBackButton, useWebApp } from 'vue-tg';
|
||||
import { useAppStateStore } from './store/core';
|
||||
import { Debug } from "./utils/debug"
|
||||
|
||||
import CameraScreen from './screens/CameraScreen.vue';
|
||||
import LandingScreen from './screens/LandingScreen.vue';
|
||||
import ResultScreen from './screens/ResultScreen.vue';
|
||||
import QuizScreen from './screens/QuizScreen.vue';
|
||||
|
||||
import Room from './screens/game/Room.vue';
|
||||
import GameWelcomeScreen from './screens/game/GameWelcomeScreen.vue';
|
||||
import GameSettingsScreen from './screens/game/GameSettingsScreen.vue';
|
||||
|
||||
import { onMounted } from 'vue';
|
||||
import { auth, initializeAuth, isAuthenticated, joinRoom } from './api/request';
|
||||
import type { AuthRequest } from './api/models';
|
||||
|
||||
const store = useAppStateStore()
|
||||
const backButton = useBackButton()
|
||||
const webapp = useWebApp()
|
||||
|
||||
|
||||
if (backButton.onClick !== undefined) {
|
||||
backButton.onClick(returnToLanding)
|
||||
}
|
||||
|
||||
function returnToLanding() {
|
||||
store.$patch({appState: "landing"})
|
||||
if (backButton.hide !== undefined) {
|
||||
backButton.hide()
|
||||
}
|
||||
}
|
||||
|
||||
async function authenticateUser() {
|
||||
try {
|
||||
const initData = webapp.initDataUnsafe
|
||||
|
||||
if (!initData?.user) {
|
||||
throw new Error('No user data available from Telegram')
|
||||
}
|
||||
|
||||
const authRequest: AuthRequest = {
|
||||
username: initData.user.username || `${initData.user.first_name}_${initData.user.id}`,
|
||||
tg_id: initData.user.id,
|
||||
last_name: initData.user.last_name ? initData.user.last_name : "",
|
||||
first_name: initData.user.first_name,
|
||||
photo_url: initData.user.photo_url ? initData.user.photo_url : ""
|
||||
}
|
||||
|
||||
Debug.log('Authenticating user:', authRequest.username)
|
||||
|
||||
const authResponse = await auth(authRequest)
|
||||
|
||||
if (authResponse.token !== undefined || authResponse.token !== null) {
|
||||
Debug.log('Authentication successful')
|
||||
await handleStartParameter()
|
||||
} else {
|
||||
throw new Error('Authentication failed')
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Authentication error:', error)
|
||||
|
||||
if (Debug.enabled) {
|
||||
Debug.log('Auth failed in debug mode, allowing bypass')
|
||||
await handleStartParameter()
|
||||
} else {
|
||||
store.$patch({appState: "error_auth"})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleStartParameter() {
|
||||
try {
|
||||
const startParam = webapp.initDataUnsafe.start_param
|
||||
|
||||
Debug.log('Start parameter:', startParam)
|
||||
|
||||
if (startParam && startParam.startsWith('room_')) {
|
||||
const roomId = startParam.substring(5)
|
||||
|
||||
Debug.log('Attempting to join room:', roomId)
|
||||
|
||||
try {
|
||||
const joinResponse = await joinRoom(roomId)
|
||||
|
||||
if (joinResponse.success) {
|
||||
Debug.log('Successfully joined room:', roomId)
|
||||
store.$patch({
|
||||
room_id: roomId,
|
||||
appState: 'game_room'
|
||||
})
|
||||
return
|
||||
} else {
|
||||
console.error('Failed to join room:', joinResponse.message)
|
||||
store.$patch({appState: "landing"})
|
||||
}
|
||||
} catch (joinError) {
|
||||
console.error('Error joining room:', joinError)
|
||||
store.$patch({appState: "landing"})
|
||||
}
|
||||
} else {
|
||||
store.$patch({appState: "landing"})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error handling start parameter:', error)
|
||||
store.$patch({appState: "landing"})
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const existingToken = initializeAuth()
|
||||
|
||||
if (webapp.platform != "android" && webapp.platform != "android_x" && webapp.platform !== "ios") {
|
||||
if (!(Debug.enabled && webapp.platform == "unknown")) {
|
||||
console.log(webapp.platform)
|
||||
store.$patch({appState: "error_not_mobile"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (existingToken && isAuthenticated()) {
|
||||
Debug.log('Found existing auth token')
|
||||
store.setAuthenticationStatus(true)
|
||||
// Проверяем параметр start для уже аутентифицированных пользователей
|
||||
await handleStartParameter()
|
||||
} else {
|
||||
Debug.log('No valid auth token, starting authentication')
|
||||
await authenticateUser()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LandingScreen v-if="store.appState == 'landing'"/>
|
||||
<CameraScreen v-if="store.appState == 'camera'" />
|
||||
<ResultScreen v-if="store.appState == 'results'" />
|
||||
<QuizScreen v-if="store.appState == 'quiz'" />
|
||||
<GameWelcomeScreen v-if="store.appState == 'game'" />
|
||||
<GameSettingsScreen v-if="store.appState == 'game_settings'" />
|
||||
<Room v-if="store.appState == 'game_room'" />
|
||||
|
||||
<div class="error" v-if="store.appState == 'error_not_mobile'">
|
||||
<div class="error-icon">❌</div>
|
||||
<div class="error-title">Требуется телефон</div>
|
||||
<div class="error-message">
|
||||
Вам потребуется камера для использования нашего приложения, поэтому используйте его из мобильной версии.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="error" v-if="store.appState == 'error_auth'">
|
||||
<div class="error-icon">❌</div>
|
||||
<div class="error-title">Хорошая попытка</div>
|
||||
<div class="error-message">
|
||||
Но приложение доступно только из Telegram
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,96 @@
|
||||
export type ApiError = {
|
||||
error: string
|
||||
};
|
||||
|
||||
export interface ImageDetectionResponse {
|
||||
example_russian: string;
|
||||
example_tatar: string;
|
||||
object_russian: string;
|
||||
object_tatar: string;
|
||||
pronunciation: string;
|
||||
transcription_ipa: string;
|
||||
description_russian: string;
|
||||
description_tatar: string;
|
||||
}
|
||||
|
||||
export interface ImageDetectionRequest {
|
||||
file: File;
|
||||
}
|
||||
|
||||
|
||||
export interface TextToSpeechRequest {
|
||||
speaker: string;
|
||||
text: string
|
||||
}
|
||||
|
||||
export interface TextToSpeechResponse {
|
||||
audio_base64: string
|
||||
}
|
||||
|
||||
|
||||
export interface AuthRequest {
|
||||
first_name: string,
|
||||
last_name: string,
|
||||
photo_url: string,
|
||||
tg_id: number,
|
||||
username: string
|
||||
}
|
||||
|
||||
export interface AuthResponse {
|
||||
first_name: string,
|
||||
last_name: string,
|
||||
photo_url: string,
|
||||
tg_id: number,
|
||||
username: string
|
||||
token: string
|
||||
}
|
||||
|
||||
|
||||
export interface Room {
|
||||
created_at: string;
|
||||
created_by: string;
|
||||
id: string;
|
||||
name: string;
|
||||
updated_at: string;
|
||||
error?: string | null
|
||||
}
|
||||
|
||||
export interface RoomMember {
|
||||
first_name: string;
|
||||
id: string;
|
||||
joined_at: string;
|
||||
photo_url: string;
|
||||
room_id: string;
|
||||
user_id: string;
|
||||
}
|
||||
|
||||
export interface RoomMembersResponse {
|
||||
members: RoomMember[];
|
||||
error?: string | null;
|
||||
}
|
||||
|
||||
export interface UserRoomsRespomse {
|
||||
rooms: Room[];
|
||||
error?: string | null;
|
||||
}
|
||||
|
||||
export interface JoinRoomResponse {
|
||||
message: string;
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export interface CreateRoomRequest {
|
||||
rounds_count: number;
|
||||
word_category: string;
|
||||
}
|
||||
|
||||
export interface StartGameRequest {
|
||||
room_id: string;
|
||||
}
|
||||
|
||||
export interface StartGameResponse {
|
||||
success: boolean;
|
||||
message?: string;
|
||||
game_id?: string;
|
||||
error?: string;
|
||||
}
|
||||
@@ -0,0 +1,381 @@
|
||||
import axios, { type AxiosResponse } from 'axios';
|
||||
import { type ImageDetectionResponse, type ImageDetectionRequest, type TextToSpeechResponse, type AuthRequest, type AuthResponse, type Room, type JoinRoomResponse, type RoomMember, type RoomMembersResponse, type UserRoomsRespomse, type CreateRoomRequest, type StartGameResponse } from './models';
|
||||
|
||||
// Configure base URL - adjust this to match your backend URL
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000';
|
||||
|
||||
|
||||
export async function authenticateUser(authData: AuthRequest): Promise<AuthResponse> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/auth`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(authData),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Authentication failed: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data: AuthResponse = await response.json();
|
||||
|
||||
if (data.token) {
|
||||
localStorage.setItem('authToken', data.token);
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Auth request failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export function getAuthToken(): string | null {
|
||||
return localStorage.getItem('authToken');
|
||||
}
|
||||
|
||||
export function clearAuthToken(): void {
|
||||
localStorage.removeItem('authToken');
|
||||
}
|
||||
|
||||
export function isAuthenticated(): boolean {
|
||||
return !!getAuthToken();
|
||||
}
|
||||
|
||||
const apiClient = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
/**
|
||||
* Send image file to the image detection endpoint
|
||||
* @param file - The image file to be analyzed
|
||||
* @returns Promise with the detection results
|
||||
*/
|
||||
export const detectImageObjects = async(file: File) => {
|
||||
try {
|
||||
// Create FormData to send the file
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const response: AxiosResponse<ImageDetectionResponse> = await apiClient.post(
|
||||
'/image-detection',
|
||||
formData,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
// Handle API errors
|
||||
if (error.response?.data) {
|
||||
throw new Error(error.response.data.error || 'Image detection failed');
|
||||
}
|
||||
throw new Error(error.message || 'Network error occurred');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const textToSpeech = async(text: string): Promise<TextToSpeechResponse> => {
|
||||
try {
|
||||
const data = {speaker: "almaz", text: text}
|
||||
|
||||
const response: AxiosResponse<TextToSpeechResponse> = await apiClient.post(
|
||||
"/text-to-speech", JSON.stringify(data), {headers: {'Content-Type': 'application/json'}}
|
||||
);
|
||||
|
||||
return response.data
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
if (error.response?.data) {
|
||||
throw new Error(error.response.data.error || 'Text-to-speech failed');
|
||||
}
|
||||
throw new Error(error.message || 'Network error occurred');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Alternative function that accepts ImageDetectionRequest object
|
||||
* @param request - Object containing the file to be analyzed
|
||||
* @returns Promise with the detection results
|
||||
*/
|
||||
export const detectImage = async (request: ImageDetectionRequest): Promise<ImageDetectionResponse> => {
|
||||
return detectImageObjects(request.file);
|
||||
};
|
||||
|
||||
/**
|
||||
* Upload image file with progress tracking
|
||||
* @param file - The image file to be analyzed
|
||||
* @param onProgress - Optional callback to track upload progress
|
||||
* @returns Promise with the detection results
|
||||
*/
|
||||
export const detectImageWithProgress = async (
|
||||
file: File,
|
||||
onProgress?: (progress: number) => void
|
||||
): Promise<ImageDetectionResponse> => {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const response: AxiosResponse<ImageDetectionResponse> = await apiClient.post(
|
||||
'/image-detection',
|
||||
formData,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
onUploadProgress: (progressEvent) => {
|
||||
if (onProgress && progressEvent.total) {
|
||||
const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total);
|
||||
onProgress(progress);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
if (error.response?.data) {
|
||||
throw new Error(error.response.data.error || 'Image detection failed');
|
||||
}
|
||||
throw new Error(error.message || 'Network error occurred');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate file before sending to API
|
||||
* @param file - The file to validate
|
||||
* @returns true if valid, throws error if invalid
|
||||
*/
|
||||
export const validateImageFile = (file: File): boolean => {
|
||||
// Check file type
|
||||
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
|
||||
if (!allowedTypes.includes(file.type)) {
|
||||
throw new Error('Invalid file type. Please upload a JPEG, PNG, GIF, or WebP image.');
|
||||
}
|
||||
|
||||
// Check file size (10MB limit)
|
||||
const maxSize = 10 * 1024 * 1024; // 10MB in bytes
|
||||
if (file.size > maxSize) {
|
||||
throw new Error('File size too large. Please upload an image smaller than 10MB.');
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Authenticate user with the backend
|
||||
* @param authData - User authentication data from Telegram
|
||||
* @returns Promise with authentication response including token
|
||||
*/
|
||||
export const auth = async (authData: AuthRequest): Promise<AuthResponse> => {
|
||||
try {
|
||||
const response: AxiosResponse<AuthResponse> = await apiClient.post(
|
||||
'/auth',
|
||||
authData,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (response.data.token) {
|
||||
localStorage.setItem('auth_token', response.data.token);
|
||||
|
||||
apiClient.defaults.headers.common['Authorization'] = `Bearer ${response.data.token}`;
|
||||
}
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
if (error.response?.data) {
|
||||
throw new Error(error.response.data.error || 'Authentication failed');
|
||||
}
|
||||
throw new Error(error.message || 'Network error occurred during authentication');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const createRoom = async (roomParams: CreateRoomRequest): Promise<Room> => {
|
||||
try {
|
||||
const response: AxiosResponse<Room> = await apiClient.post("/rooms", roomParams, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
if (response.data.id) {
|
||||
return response.data
|
||||
} else {
|
||||
throw new Error(response.data.error ? response.data.error : "Unknown error while creating room")
|
||||
}
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
if (error.response?.data) {
|
||||
throw new Error(error.response.data.error || 'Unknown error while creating room');
|
||||
}
|
||||
throw new Error(error.message || 'Network error occurred during creating room');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export const joinRoom = async (room_id: string): Promise<JoinRoomResponse> => {
|
||||
try {
|
||||
const response: AxiosResponse<JoinRoomResponse> = await apiClient.post("/rooms/join", {room_id: room_id}, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
if (response.data.success) {
|
||||
return response.data
|
||||
} else {
|
||||
throw new Error(response.data.message ? response.data.message : "Unknown error while joining room")
|
||||
}
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
if (error.response?.data) {
|
||||
throw new Error(error.response.data.error || 'Unknown error while joining room');
|
||||
}
|
||||
throw new Error(error.message || 'Network error occurred during joining room');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export const getRoom = async (room_id: string): Promise<Room> => {
|
||||
try {
|
||||
const response: AxiosResponse<Room> = await apiClient.get(`/rooms/${room_id}`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
if (response.data.id) {
|
||||
return response.data
|
||||
} else {
|
||||
throw new Error(response.data.error ? response.data.error : "Unknown error while fetching room information")
|
||||
}
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
if (error.response?.data) {
|
||||
throw new Error(error.response.data.error || 'Unknown error while fetching room information');
|
||||
}
|
||||
throw new Error(error.message || 'Network error occurred during joining room');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export const getRoomMembers = async (room_id: string): Promise<RoomMember[]> => {
|
||||
try {
|
||||
const response: AxiosResponse<RoomMembersResponse> = await apiClient.get(`/rooms/${room_id}/members`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
if (response.data.members) {
|
||||
return response.data.members
|
||||
} else {
|
||||
throw new Error(response.data.error ? response.data.error : "Unknown error while fetching room members")
|
||||
}
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
if (error.response?.data) {
|
||||
throw new Error(error.response.data.error || 'Unknown error while fetching room members');
|
||||
}
|
||||
throw new Error(error.message || 'Network error occurred during fethcing room members');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export const getUserRooms = async (room_id: string): Promise<Room[]> => {
|
||||
try {
|
||||
const response: AxiosResponse<UserRoomsRespomse> = await apiClient.get(`/users/${room_id}/rooms`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
if (response.data.rooms) {
|
||||
return response.data.rooms
|
||||
} else {
|
||||
throw new Error(response.data.error ? response.data.error : "Unknown error while fetching user rooms")
|
||||
}
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
if (error.response?.data) {
|
||||
throw new Error(error.response.data.error || 'Unknown error while fetching user rooms');
|
||||
}
|
||||
throw new Error(error.message || 'Network error occurred during fethcing user rooms');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export const startGame = async (roomId: string): Promise<StartGameResponse> => {
|
||||
try {
|
||||
const token = getAuthToken();
|
||||
|
||||
const response: AxiosResponse<StartGameResponse> = await apiClient.post('/games/start',
|
||||
{ room_id: roomId },
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(token && { 'Authorization': `Bearer ${token}` })
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (response.data.success) {
|
||||
return response.data;
|
||||
} else {
|
||||
throw new Error(response.data.message || response.data.error || "Unknown error while starting game");
|
||||
}
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
if (error.response?.data) {
|
||||
throw new Error(error.response.data.error || error.response.data.message || 'Unknown error while starting game');
|
||||
}
|
||||
throw new Error(error.message || 'Network error occurred during starting game');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const initializeAuth = (): string | null => {
|
||||
const token = localStorage.getItem('authToken');
|
||||
if (token) {
|
||||
apiClient.defaults.headers.common['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
return token;
|
||||
};
|
||||
|
||||
export default {
|
||||
detectImageObjects,
|
||||
detectImage,
|
||||
detectImageWithProgress,
|
||||
validateImageFile,
|
||||
textToSpeech,
|
||||
auth,
|
||||
initializeAuth,
|
||||
startGame,
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
<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="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 496 B |
@@ -0,0 +1,90 @@
|
||||
<script setup lang="ts">
|
||||
import { type RoomMember } from '../api/models';
|
||||
|
||||
defineProps<{
|
||||
member: RoomMember;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="player-card">
|
||||
<div class="player-avatar">
|
||||
{{ member.first_name.charAt(0).toUpperCase() }}
|
||||
</div>
|
||||
<div class="player-info">
|
||||
<div class="player-name">{{ member.first_name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.player-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 12px;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.player-avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #6a11cb 0%, #2575fc 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: 'OpenSans', sans-serif;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.player-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.player-name {
|
||||
font-family: 'OpenSans', sans-serif;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: rgba(254, 254, 254, 1);
|
||||
}
|
||||
|
||||
.player-status {
|
||||
font-family: 'OpenSans', sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.player-status.online {
|
||||
color: #90EE90;
|
||||
}
|
||||
|
||||
.player-status.offline {
|
||||
color: rgba(254, 254, 254, 0.5);
|
||||
}
|
||||
|
||||
.player-status.ready {
|
||||
color: #FFD700;
|
||||
}
|
||||
|
||||
.player-status.playing {
|
||||
color: #FF6B6B;
|
||||
}
|
||||
|
||||
.player-score {
|
||||
font-family: 'OpenSans', sans-serif;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #90EE90;
|
||||
min-width: 40px;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,11 @@
|
||||
import { createApp } from 'vue'
|
||||
import './style.css'
|
||||
import App from './App.vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import './utils/debug' // Initialize debug mode
|
||||
|
||||
const pinia = createPinia()
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(pinia)
|
||||
app.mount('#app')
|
||||
@@ -0,0 +1,507 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onUnmounted, onMounted } from 'vue';
|
||||
import { useAppStateStore } from '../store/core';
|
||||
import { useBackButton } from 'vue-tg';
|
||||
import { detectImageObjects, validateImageFile } from '../api/request';
|
||||
|
||||
declare global {
|
||||
interface MediaTrackCapabilities {
|
||||
torch?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface MediaTrackConstraintSet {
|
||||
torch?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
// брат ты буквально вызываешь init дважды, один раз в onMounted,
|
||||
// второй раз сразу при объявлении функции
|
||||
// типо из-за этого у тя камера багается
|
||||
|
||||
const video = ref<HTMLVideoElement | null>(null);
|
||||
const store = useAppStateStore();
|
||||
const backButton = useBackButton();
|
||||
|
||||
let frontStream: MediaStream | null = null;
|
||||
let backStream: MediaStream | null = null;
|
||||
let currentStream: MediaStream | null = null;
|
||||
let inactivityTimer: number | null = null;
|
||||
|
||||
const currentCamera = ref<'user' | 'environment'>('environment');
|
||||
const isLoading = ref(false);
|
||||
const error = ref<string | null>(null);
|
||||
const isSleeping = ref(true);
|
||||
|
||||
const flashSupported = ref(false);
|
||||
const flashOn = ref(false);
|
||||
|
||||
|
||||
async function startCamera() {
|
||||
if (currentStream) return;
|
||||
isSleeping.value = false;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
||||
throw new Error('Камера недоступна. Убедитесь, что сайт открыт по HTTPS.');
|
||||
}
|
||||
|
||||
let streamToUse: MediaStream | null = null;
|
||||
if (currentCamera.value === 'user' && frontStream) {
|
||||
streamToUse = frontStream;
|
||||
} else if (currentCamera.value === 'environment' && backStream) {
|
||||
streamToUse = backStream;
|
||||
}
|
||||
|
||||
if (streamToUse) {
|
||||
streamToUse.getTracks().forEach(track => track.enabled = true);
|
||||
} else {
|
||||
streamToUse = await navigator.mediaDevices.getUserMedia({
|
||||
video: {
|
||||
facingMode: { ideal: currentCamera.value },
|
||||
width: { ideal: 1280 },
|
||||
height: { ideal: 1280 }
|
||||
}
|
||||
});
|
||||
if (currentCamera.value === 'user') {
|
||||
frontStream = streamToUse;
|
||||
} else {
|
||||
backStream = streamToUse;
|
||||
}
|
||||
}
|
||||
|
||||
currentStream = streamToUse;
|
||||
if (video.value) {
|
||||
video.value.srcObject = currentStream;
|
||||
video.value.play();
|
||||
}
|
||||
|
||||
await checkFlashSupport();
|
||||
resetInactivityTimer();
|
||||
|
||||
} catch (err) {
|
||||
console.error('Ошибка при инициализации камеры:', err);
|
||||
if (err instanceof Error) {
|
||||
if (err.name === 'NotAllowedError') {
|
||||
error.value = 'Вы не разрешили доступ к камере. Проверьте настройки браузера.';
|
||||
} else if (err.name === 'NotFoundError') {
|
||||
error.value = 'Камера не найдена. Убедитесь, что она подключена и работает.';
|
||||
} else {
|
||||
error.value = 'Не удалось получить доступ к камере. Попробуйте перезагрузить страницу.';
|
||||
}
|
||||
} else {
|
||||
error.value = 'Произошла неизвестная ошибка с камерой.';
|
||||
}
|
||||
isSleeping.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
function stopAllStreams() {
|
||||
[frontStream, backStream].forEach(s => {
|
||||
if (s) s.getTracks().forEach(track => track.enabled = false);
|
||||
});
|
||||
if (video.value) video.value.srcObject = null;
|
||||
currentStream = null;
|
||||
flashOn.value = false;
|
||||
flashSupported.value = false;
|
||||
}
|
||||
|
||||
function goToSleep() {
|
||||
console.log("Камера уходит в сон...");
|
||||
stopAllStreams();
|
||||
isSleeping.value = true;
|
||||
if (inactivityTimer) clearTimeout(inactivityTimer);
|
||||
}
|
||||
|
||||
function resetInactivityTimer() {
|
||||
if (inactivityTimer) clearTimeout(inactivityTimer);
|
||||
inactivityTimer = setTimeout(goToSleep, 30000); // 30 секунд бездействия
|
||||
}
|
||||
|
||||
async function wakeUpCamera() {
|
||||
if(isSleeping.value) {
|
||||
await startCamera();
|
||||
}
|
||||
}
|
||||
|
||||
// initCamera('environment'); // вот этот нах не нуж
|
||||
|
||||
onMounted(() => {
|
||||
// показываем кнопку для перехода на лендинг. работает только в миниаппке
|
||||
if (backButton.show !== undefined && backButton.onClick !== undefined) {
|
||||
backButton.onClick(() => {
|
||||
store.$patch({appState: "landing"})
|
||||
})
|
||||
backButton.show()
|
||||
}
|
||||
// Автоматически запускаем камеру при открытии экрана
|
||||
// таймер бездействия будет инициализирован внутри startCamera()
|
||||
startCamera();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
// Останавливаем и очищаем все потоки при уходе со страницы
|
||||
[frontStream, backStream].forEach(s => {
|
||||
if (s) {
|
||||
try {
|
||||
s.getTracks().forEach(track => track.stop());
|
||||
} catch (e) {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
});
|
||||
frontStream = null;
|
||||
backStream = null;
|
||||
currentStream = null;
|
||||
if (inactivityTimer) {
|
||||
clearTimeout(inactivityTimer);
|
||||
inactivityTimer = null;
|
||||
}
|
||||
});
|
||||
|
||||
async function switchCamera() {
|
||||
if (isLoading.value) return;
|
||||
currentCamera.value = currentCamera.value === 'user' ? 'environment' : 'user';
|
||||
stopAllStreams();
|
||||
await startCamera();
|
||||
resetInactivityTimer();
|
||||
}
|
||||
|
||||
async function checkFlashSupport() {
|
||||
if (!currentStream) {
|
||||
flashSupported.value = false;
|
||||
return;
|
||||
}
|
||||
const track = currentStream.getVideoTracks()[0];
|
||||
const capabilities = track.getCapabilities();
|
||||
|
||||
flashSupported.value = !!capabilities.torch;
|
||||
if (!flashSupported.value) {
|
||||
flashOn.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleFlash() {
|
||||
if (!flashSupported.value || !currentStream) return;
|
||||
const track = currentStream.getVideoTracks()[0];
|
||||
try {
|
||||
await track.applyConstraints({
|
||||
|
||||
advanced: [{ torch: !flashOn.value }]
|
||||
});
|
||||
flashOn.value = !flashOn.value;
|
||||
resetInactivityTimer();
|
||||
} catch(err) {
|
||||
console.error("Вспышка не сработала:", err);
|
||||
error.value = "Не удалось управлять вспышкой";
|
||||
}
|
||||
}
|
||||
|
||||
async function takePhoto() {
|
||||
if (!video.value || isLoading.value || !currentStream) return;
|
||||
resetInactivityTimer();
|
||||
error.value = null;
|
||||
isLoading.value = true;
|
||||
|
||||
try {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = video.value.videoWidth;
|
||||
canvas.height = video.value.videoHeight;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) throw new Error('Не удалось создать контекст для обработки изображения');
|
||||
|
||||
ctx.drawImage(video.value, 0, 0, canvas.width, canvas.height);
|
||||
const imageDataUrl = canvas.toDataURL('image/jpeg', 0.8);
|
||||
|
||||
const blob = await new Promise<Blob | null>((resolve) => canvas.toBlob(resolve, 'image/jpeg', 0.8));
|
||||
if (!blob) throw new Error('Не удалось создать изображение');
|
||||
|
||||
const file = new File([blob], 'camera-capture.jpg', { type: 'image/jpeg' });
|
||||
validateImageFile(file);
|
||||
|
||||
store.$patch({ capturedImage: imageDataUrl });
|
||||
|
||||
// после того как фотка сделана, можн успыть
|
||||
goToSleep();
|
||||
|
||||
const result = await detectImageObjects(file);
|
||||
|
||||
store.$patch({
|
||||
appState: "results",
|
||||
title: result.object_russian,
|
||||
description: result.description_russian,
|
||||
transcription: result.transcription_ipa,
|
||||
exampleRussian: result.example_russian,
|
||||
exampleTatar: result.example_tatar,
|
||||
objectRussian: result.object_russian,
|
||||
objectTatar: result.object_tatar,
|
||||
pronunciation: result.pronunciation,
|
||||
transcriptionIPA: result.transcription_ipa,
|
||||
capturedImage: imageDataUrl,
|
||||
descriptionTatar: result.description_tatar,
|
||||
});
|
||||
|
||||
if (backButton.onClick !== undefined) {
|
||||
backButton.onClick(backToCamera);
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error('Error processing photo:', err);
|
||||
error.value = err instanceof Error ? err.message : 'Произошла ошибка при обработке фото';
|
||||
await wakeUpCamera();
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function backToCamera() {
|
||||
store.$patch({ appState: "camera", capturedImage: null });
|
||||
// камера по клику проснется
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="camera-screen" @click="wakeUpCamera">
|
||||
|
||||
<div v-if="isSleeping && !isLoading" class="sleep-overlay">
|
||||
<div class="sleep-icon">📷</div>
|
||||
<div class="sleep-text">Нажмите, чтобы включить камеру</div>
|
||||
</div>
|
||||
|
||||
<video ref="video" autoplay playsinline class="camera-video" :class="{ 'hidden': isSleeping }"></video>
|
||||
|
||||
<div v-if="error" class="error-message">
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<div v-if="isLoading" class="loading-overlay">
|
||||
<img v-if="store.capturedImage" :src="store.capturedImage" class="loading-bg-image" />
|
||||
<div class="scan-line"></div>
|
||||
<div class="loading-text">Анализ...</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!isSleeping && !isLoading" class="controls-wrapper">
|
||||
<div class="side-controls left">
|
||||
<button
|
||||
v-if="flashSupported"
|
||||
class="control-btn"
|
||||
@click.stop="toggleFlash"
|
||||
:class="{ 'active': flashOn }"
|
||||
title="Вспышка"
|
||||
>
|
||||
<img src="/flash.svg">
|
||||
</button>
|
||||
</div>
|
||||
<div class="center-control">
|
||||
<button
|
||||
class="camera-btn"
|
||||
@click.stop="takePhoto"
|
||||
></button>
|
||||
</div>
|
||||
<div class="side-controls right">
|
||||
<button
|
||||
class="control-btn"
|
||||
@click.stop="switchCamera"
|
||||
title="Сменить камеру"
|
||||
>
|
||||
<img src="/refresh.svg">
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.camera-screen {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
position: relative;
|
||||
background: #000;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.camera-video {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.camera-video.hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.sleep-overlay {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
z-index: 10;
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
.sleep-icon {
|
||||
font-size: 60px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.sleep-text {
|
||||
margin-top: 16px;
|
||||
font-size: 16px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
backdrop-filter: blur(5px);
|
||||
overflow: hidden;
|
||||
}
|
||||
.loading-bg-image {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
opacity: 0.3;
|
||||
filter: blur(8px);
|
||||
z-index: -1;
|
||||
}
|
||||
.loading-text {
|
||||
color: white;
|
||||
font-size: 20px;
|
||||
text-align: center;
|
||||
z-index: 2;
|
||||
text-shadow: 0 0 10px rgba(0,0,0,0.5);
|
||||
letter-spacing: 2px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.scan-line {
|
||||
position: absolute;
|
||||
top: -10%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
background: linear-gradient(90deg, transparent, rgba(0, 255, 255, 0.8), transparent);
|
||||
box-shadow: 0 0 15px 2px rgba(0, 255, 255, 0.7);
|
||||
animation: scan 2.5s linear infinite;
|
||||
z-index: 1;
|
||||
}
|
||||
@keyframes scan {
|
||||
0% { top: -10%; }
|
||||
100% { top: 110%; }
|
||||
}
|
||||
|
||||
/* --- Новые контролы --- */
|
||||
.controls-wrapper {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
bottom: 50px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 30px;
|
||||
z-index: 20;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.center-control {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.side-controls {
|
||||
width: 60px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.side-controls.left { justify-content: flex-start; }
|
||||
.side-controls.right { justify-content: flex-end; }
|
||||
|
||||
.control-btn {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255,255,255,0.2);
|
||||
border: none;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
font-size: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background 0.2s ease, transform 0.2s ease;
|
||||
color: white;
|
||||
}
|
||||
.control-btn:active {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
.control-btn.active {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.camera-btn {
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
border-radius: 50%;
|
||||
background: transparent;
|
||||
border: 5px solid #fff;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease;
|
||||
position: relative;
|
||||
}
|
||||
.camera-btn::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.camera-btn:active::before {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(255, 59, 48, 0.9);
|
||||
color: white;
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
max-width: 90%;
|
||||
font-size: 14px;
|
||||
z-index: 50;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
import { useAppStateStore } from '../store/core';
|
||||
import { useBackButton } from 'vue-tg';
|
||||
import { defineEmits } from 'vue';
|
||||
|
||||
const store = useAppStateStore()
|
||||
const backButton = useBackButton()
|
||||
const emit = defineEmits(['logout'])
|
||||
|
||||
function openCamera() {
|
||||
store.$patch({appState: "camera"})
|
||||
if (backButton.show !== undefined) {
|
||||
backButton.show()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="landing" v-if="store.appState == 'landing'">
|
||||
<img src="/logo.svg" class="landing-logo" alt="Logo">
|
||||
<div class="bubbles">
|
||||
<div class="bubble">Сканируй мир</div>
|
||||
<div class="bubble">Изучай татарский</div>
|
||||
</div>
|
||||
<img src="/scan_example.png" alt="scan example" class="landing-image" />
|
||||
|
||||
<div class="bottom-app-button">
|
||||
<button class="start-btn" @click="openCamera">Сканировать</button>
|
||||
<button class="game-btn" @click="store.$patch({appState: 'game'})">Игра</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,195 @@
|
||||
<script setup lang="ts">
|
||||
import { MainButton, useBackButton } from 'vue-tg';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useAppStateStore } from '../store/core';
|
||||
import { Debug } from '../utils/debug';
|
||||
|
||||
const selectedVariant = ref("");
|
||||
const correctVariant = ref("Яблоко");
|
||||
const buttonTitle = ref("Подтвердить")
|
||||
const isAnswered = ref(false);
|
||||
|
||||
const store = useAppStateStore()
|
||||
const backButton = useBackButton()
|
||||
|
||||
onMounted(() => {
|
||||
if (backButton.show !== undefined && backButton.onClick !== undefined) {
|
||||
backButton.onClick(() => {
|
||||
store.$patch({appState: "landing"})
|
||||
})
|
||||
backButton.show()
|
||||
}
|
||||
})
|
||||
|
||||
function quizAnswerSubmit() {
|
||||
if (isAnswered.value) {
|
||||
selectedVariant.value = "";
|
||||
isAnswered.value = false;
|
||||
buttonTitle.value = "Подтвердить";
|
||||
return;
|
||||
}
|
||||
|
||||
// тут будет отправка ответа в апи, но нужно сделать контракт. короче todo: add backend integration
|
||||
isAnswered.value = true;
|
||||
buttonTitle.value = "Следующий вопрос";
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="quiz">
|
||||
<h1 class="quiz-title">Квиз</h1>
|
||||
<h2 class="quiz-question">Выберите правильный перевод</h2>
|
||||
|
||||
<div class="chat-bubble">
|
||||
өчпочмак
|
||||
</div>
|
||||
|
||||
<div class="quiz-variants">
|
||||
<div
|
||||
class="quiz-variant"
|
||||
:class="{
|
||||
success: isAnswered && 'Яблоко' === correctVariant,
|
||||
fail: isAnswered && 'Яблоко' !== correctVariant,
|
||||
selected: selectedVariant === 'Яблоко',
|
||||
disabled: isAnswered
|
||||
}"
|
||||
@click="!isAnswered && (selectedVariant = 'Яблоко')"
|
||||
>
|
||||
Яблоко
|
||||
</div>
|
||||
<div
|
||||
class="quiz-variant"
|
||||
:class="{
|
||||
success: isAnswered && 'Пирог' === correctVariant,
|
||||
fail: isAnswered && selectedVariant == 'Пирог' && 'Пирог' !== correctVariant,
|
||||
selected: selectedVariant === 'Пирог',
|
||||
disabled: isAnswered
|
||||
}"
|
||||
@click="!isAnswered && (selectedVariant = 'Пирог')"
|
||||
>
|
||||
Пирог
|
||||
</div>
|
||||
<div
|
||||
class="quiz-variant"
|
||||
:class="{
|
||||
success: isAnswered && 'Булочка' === correctVariant,
|
||||
fail: isAnswered && selectedVariant == 'Булочка' && 'Булочка' !== correctVariant,
|
||||
selected: selectedVariant === 'Булочка',
|
||||
disabled: isAnswered
|
||||
}"
|
||||
@click="!isAnswered && (selectedVariant = 'Булочка')"
|
||||
>
|
||||
Булочка
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MainButton v-if="selectedVariant !== '' && !Debug.enabled" :text="buttonTitle" @click="quizAnswerSubmit" />
|
||||
<div class="bottom-app-button" v-if="Debug.enabled">
|
||||
<button class="start-btn" v-if="selectedVariant !== ''" @click="quizAnswerSubmit">{{ buttonTitle }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.quiz {
|
||||
padding-top: 80px;
|
||||
height: 100vh;
|
||||
color: #FEFEFE;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 40px;
|
||||
text-align: center;
|
||||
font-family: 'OpenSans', sans-serif;
|
||||
font-weight: 400;
|
||||
font-style: 'SemiBold'
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 32px;
|
||||
text-align: left;
|
||||
font-family: 'OpenSans', sans-serif;
|
||||
font-weight: 400;
|
||||
font-style: 'SemiBold';
|
||||
padding-bottom: 60px;
|
||||
}
|
||||
|
||||
.chat-bubble {
|
||||
position: relative;
|
||||
background-color: #248BDA;
|
||||
color: white;
|
||||
padding: 12px 28px;
|
||||
border-radius: 40px;
|
||||
font-size: 40px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
|
||||
font-family: 'OpenSans', sans-serif;
|
||||
font-style: 'SemiBold';
|
||||
|
||||
max-width: 261px;
|
||||
max-height: 95px;
|
||||
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.chat-bubble::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
|
||||
left: 25px;
|
||||
bottom: -9px;
|
||||
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 15px solid transparent;
|
||||
border-right: 0px solid transparent;
|
||||
border-top: 18px solid #248BDA;
|
||||
}
|
||||
|
||||
.quiz-variants {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.quiz-variant {
|
||||
background-color: #D9D9D91A;
|
||||
padding: 16px 20px;
|
||||
border-radius: 12px;
|
||||
font-family: 'OpenSans', sans-serif;
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
font-size: 18px;
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
.quiz-variant.disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.success {
|
||||
background-color: #355a35;
|
||||
}
|
||||
|
||||
.fail {
|
||||
background-color: #630209;
|
||||
}
|
||||
|
||||
.selected {
|
||||
/* background-color: #2929291a; */
|
||||
border: 2px solid #FFFFFF;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,303 @@
|
||||
<script setup lang="ts">
|
||||
import { useBackButton } from 'vue-tg';
|
||||
import { useAppStateStore } from '../store/core';
|
||||
import { onMounted } from 'vue';
|
||||
import { textToSpeech } from '../api/request';
|
||||
|
||||
const store = useAppStateStore()
|
||||
const backButton = useBackButton()
|
||||
|
||||
function backToCamera() {
|
||||
store.$patch({
|
||||
appState: "camera",
|
||||
capturedImage: null
|
||||
})
|
||||
}
|
||||
|
||||
function playAudio() {
|
||||
if (store.tts_audio) {
|
||||
try {
|
||||
// Создаем аудио объект из base64 строки
|
||||
const audio = new Audio(`data:audio/mp3;base64,${store.tts_audio}`)
|
||||
audio.play()
|
||||
} catch (error) {
|
||||
console.error('Ошибка воспроизведения аудио:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (backButton.onClick !== undefined) {
|
||||
backButton.onClick(backToCamera)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (store.objectTatar !== "") {
|
||||
const data = await textToSpeech(store.objectTatar)
|
||||
store.$patch({tts_audio: data.audio_base64})
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="results-screen">
|
||||
<img
|
||||
:src="store.capturedImage || '/scan_example.png'"
|
||||
class="result-image"
|
||||
alt="Сфотографированное изображение"
|
||||
>
|
||||
|
||||
<div class="content">
|
||||
<div class="title-section">
|
||||
<div class="title-with-audio">
|
||||
<h1>{{ store.objectTatar }}</h1>
|
||||
<button
|
||||
class="audio-button"
|
||||
:class="{ 'loading': !store.tts_audio }"
|
||||
@click="playAudio"
|
||||
:disabled="!store.tts_audio"
|
||||
aria-label="Воспроизвести произношение"
|
||||
>
|
||||
<!-- Спиннер -->
|
||||
<div v-if="!store.tts_audio" class="spinner">
|
||||
<svg width="33" height="33" viewBox="0 0 33 33" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="16.5" cy="16.5" r="12" stroke="rgba(255, 255, 255, 0.3)" stroke-width="2"/>
|
||||
<circle cx="16.5" cy="16.5" r="12" stroke="white" stroke-width="2"
|
||||
stroke-linecap="round" stroke-dasharray="60" stroke-dashoffset="15"/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- Иконка воспроизведения -->
|
||||
<svg v-else width="33" height="33" viewBox="0 0 33 33" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_13087_382)">
|
||||
<mask id="mask0_13087_382" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="33" height="33">
|
||||
<path d="M0 0H33V33H0V0Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_13087_382)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.4344 23.3064H10.0031L14.8034 28.1067C15.1705 28.4739 15.6382 28.7239 16.1474 28.8253C16.6567 28.9266 17.1845 28.8746 17.6642 28.6759C18.1439 28.4772 18.5538 28.1407 18.8422 27.7089C19.1306 27.2772 19.2845 26.7696 19.2844 26.2504V9.22491C19.2845 8.7057 19.1306 8.19813 18.8422 7.76639C18.5538 7.33465 18.1439 6.99814 17.6642 6.79944C17.1845 6.60073 16.6567 6.54875 16.1474 6.65007C15.6382 6.7514 15.1705 7.00147 14.8034 7.36866L10.0031 12.1689H4.4344C3.69594 12.1689 2.98772 12.4623 2.46555 12.9844C1.94338 13.5066 1.65002 14.2148 1.65002 14.9533L1.65002 20.522C1.65002 21.2605 1.94338 21.9687 2.46555 22.4909C2.98772 23.0131 3.69594 23.3064 4.4344 23.3064ZM27.9382 27.2101C27.4463 27.8022 26.559 27.7985 26.0152 27.2547C25.4731 26.7108 25.4806 25.8328 25.9576 25.2313C27.6508 23.1011 28.5704 20.4589 28.5656 17.7377C28.5699 15.0171 27.6504 12.3756 25.9576 10.2458C25.4806 9.64256 25.4731 8.76641 26.017 8.22253C26.559 7.67865 27.4463 7.67494 27.9382 8.26708C30.1475 10.9281 31.3547 14.2791 31.35 17.7377C31.35 21.3388 30.0692 24.6392 27.9382 27.2101ZM23.9696 23.247C23.5129 23.8651 22.6219 23.8596 22.078 23.3157C21.5342 22.7737 21.5509 21.8975 21.9667 21.2515C22.6399 20.2033 22.9975 18.9835 22.9969 17.7377C22.9969 16.4439 22.6182 15.2392 21.9667 14.2256C21.5509 13.5797 21.5342 12.7035 22.078 12.1596C22.6219 11.6158 23.5129 11.6102 23.9714 12.2302C25.1093 13.7709 25.7813 15.6754 25.7813 17.7377C25.784 19.7209 25.1488 21.6525 23.9696 23.247Z" fill="white"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_13087_382">
|
||||
<rect width="33" height="33" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="translation-section">
|
||||
<h2>{{ store.objectRussian }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="translation-section">
|
||||
<div class="transcription">
|
||||
{{ store.transcriptionIPA }}
|
||||
</div>
|
||||
<div class="pronunciation" v-if="store.pronunciation">
|
||||
Произношение: {{ store.pronunciation }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="description-section" v-if="store.description || store.descriptionTatar">
|
||||
<div class="language-switcher">
|
||||
<button
|
||||
class="language-button"
|
||||
:class="{ active: store.selectedDescriptionLanguage === 'ru' }"
|
||||
@click="store.selectedDescriptionLanguage = 'ru'"
|
||||
>
|
||||
RU
|
||||
</button>
|
||||
<button
|
||||
class="language-button"
|
||||
:class="{ active: store.selectedDescriptionLanguage === 'tat' }"
|
||||
@click="store.selectedDescriptionLanguage = 'tat'"
|
||||
>
|
||||
ТАТ
|
||||
</button>
|
||||
</div>
|
||||
<p class="description">
|
||||
{{ store.selectedDescriptionLanguage === 'ru' ? store.description : store.descriptionTatar }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="examples-section" v-if="store.exampleRussian || store.exampleTatar">
|
||||
<h3>Примеры использования</h3>
|
||||
<div class="example" v-if="store.exampleRussian">
|
||||
<strong>Русский:</strong> {{ store.exampleRussian }}
|
||||
</div>
|
||||
<div class="example" v-if="store.exampleTatar">
|
||||
<strong>Татарский:</strong> {{ store.exampleTatar }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.result-image {
|
||||
width: 250px;
|
||||
height: 250px;
|
||||
border-radius: 16px;
|
||||
margin-bottom: 24px;
|
||||
object-fit: cover;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.results-screen {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px;
|
||||
min-height: 100vh;
|
||||
padding-top: 60px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.content {
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.title-section h1 {
|
||||
font-size: 28px;
|
||||
margin: 0 0 16px 0;
|
||||
color: #90EE90;
|
||||
}
|
||||
|
||||
.title-with-audio {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.title-with-audio h1 {
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.audio-button {
|
||||
background: none;
|
||||
border: none;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
outline: none;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.audio-button:active {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.audio-button:disabled {
|
||||
cursor: default;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.audio-button.loading {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.translation-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.translation-section h2 {
|
||||
font-size: 24px;
|
||||
margin: 0 0 12px 0;
|
||||
color: rgba(254, 254, 254, 1);
|
||||
font-weight: 400;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.transcription {
|
||||
font-size: 18px;
|
||||
color: rgba(255, 255, 255, 1);
|
||||
font-style: italic;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.pronunciation {
|
||||
font-size: 16px;
|
||||
color: rgba(255, 255, 255, 1);
|
||||
}
|
||||
|
||||
.description-section {
|
||||
margin: 24px 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.language-switcher {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.language-button {
|
||||
padding: 8px 16px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 20px;
|
||||
background: transparent;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
min-width: 50px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.language-button.active {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
color: rgba(0, 0, 0, 0.9);
|
||||
border-color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 16px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.examples-section {
|
||||
text-align: left;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.examples-section h3 {
|
||||
font-size: 18px;
|
||||
margin: 0 0 12px 0;
|
||||
color: rgba(254, 254, 254, 1);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.example {
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.example strong {
|
||||
color: rgba(254, 254, 254, 1);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,201 @@
|
||||
<script setup lang="ts">
|
||||
import { useAppStateStore } from '../../store/core';
|
||||
import { useBackButton } from 'vue-tg';
|
||||
import { ref, defineEmits, onMounted } from 'vue';
|
||||
|
||||
import { createRoom } from '../../api/request';
|
||||
|
||||
import "../../utils/debug"
|
||||
|
||||
const store = useAppStateStore()
|
||||
const backButton = useBackButton()
|
||||
const emit = defineEmits(['logout'])
|
||||
|
||||
interface Category {
|
||||
display_name: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const roundsCount = ref(3);
|
||||
const selectedCategory = ref("")
|
||||
const categories = ref<Category[]>([{display_name: "Дом", name: "home"},{display_name: "Улица", name: "street"},{display_name: "Путеводитель по городу", name: "city"}])
|
||||
|
||||
onMounted(() => {
|
||||
if (backButton.show !== undefined && backButton.onClick !== undefined) {
|
||||
backButton.onClick(() => {
|
||||
store.$patch({appState: "game"})
|
||||
})
|
||||
backButton.show()
|
||||
}
|
||||
})
|
||||
|
||||
async function createRoomClick () {
|
||||
if (!selectedCategory.value) {
|
||||
console.error('Категория не выбрана');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const roomParams = {
|
||||
rounds_count: roundsCount.value,
|
||||
word_category: selectedCategory.value
|
||||
};
|
||||
const result = await createRoom(roomParams);
|
||||
console.log('Room created:', result);
|
||||
store.$patch({
|
||||
room_id: result.id,
|
||||
appState: 'game_room'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error creating room:', error);
|
||||
// return // на проде вернуть
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="game-settings" v-if="store.appState == 'game_settings'">
|
||||
<h1 class="game-title">Настройки игры</h1>
|
||||
|
||||
<div class="settings-section">
|
||||
<h2 class="section-title">Количество раундов</h2>
|
||||
<div class="rounds-selector">
|
||||
<button
|
||||
v-for="rounds in [3, 5, 7, 10]"
|
||||
:key="rounds"
|
||||
class="rounds-option"
|
||||
:class="{ selected: roundsCount === rounds }"
|
||||
@click="roundsCount = rounds"
|
||||
>
|
||||
{{ rounds }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<h2 class="section-title">Выберите категорию</h2>
|
||||
<div class="categories-list">
|
||||
<div
|
||||
v-for="category in categories"
|
||||
:key="category.name"
|
||||
class="category-variant"
|
||||
:class="{ selected: selectedCategory === category.name }"
|
||||
@click="selectedCategory = category.name"
|
||||
>
|
||||
{{ category.display_name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bottom-app-button">
|
||||
<button
|
||||
class="start-btn"
|
||||
:disabled="!selectedCategory"
|
||||
@click="createRoomClick"
|
||||
>
|
||||
Создать комнату
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.game-settings {
|
||||
padding-top: 60px;
|
||||
height: 100vh;
|
||||
color: #FEFEFE;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
padding-bottom: 100px;
|
||||
gap: 32px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.game-title {
|
||||
font-size: 32px;
|
||||
text-align: center;
|
||||
font-family: 'OpenSans', sans-serif;
|
||||
font-weight: 600;
|
||||
color: #90EE90;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.settings-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 20px;
|
||||
font-family: 'OpenSans', sans-serif;
|
||||
font-weight: 600;
|
||||
color: rgba(254, 254, 254, 1);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.rounds-selector {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.rounds-option {
|
||||
background-color: #D9D9D91A;
|
||||
border: none;
|
||||
padding: 12px 20px;
|
||||
border-radius: 12px;
|
||||
font-family: 'OpenSans', sans-serif;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #FEFEFE;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.rounds-option.selected {
|
||||
border: 2px solid #FFFFFF;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.rounds-option:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.categories-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.category-variant {
|
||||
background-color: #D9D9D91A;
|
||||
padding: 16px 20px;
|
||||
border-radius: 12px;
|
||||
font-family: 'OpenSans', sans-serif;
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
color: #FEFEFE;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
.category-variant.selected {
|
||||
border: 2px solid #FFFFFF;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.category-variant:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.start-btn:disabled {
|
||||
background: rgba(254, 254, 254, 0.3);
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,54 @@
|
||||
<script setup lang="ts">
|
||||
import { useAppStateStore } from '../../store/core';
|
||||
import { useBackButton } from 'vue-tg';
|
||||
import { defineEmits, onMounted } from 'vue';
|
||||
|
||||
import "../../utils/debug"
|
||||
|
||||
const store = useAppStateStore()
|
||||
const backButton = useBackButton()
|
||||
const emit = defineEmits(['logout'])
|
||||
|
||||
onMounted(() => {
|
||||
if (backButton.show !== undefined && backButton.onClick !== undefined) {
|
||||
backButton.onClick(() => {
|
||||
store.$patch({appState: "landing"})
|
||||
})
|
||||
backButton.show()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="landing" v-if="store.appState == 'game'">
|
||||
<h1 class="game-title">Игра</h1>
|
||||
<div class="description-section">
|
||||
<p>Это дуэль друзей в реальном мире. Вы получаете случайное слово, ваша задача — быстрее соперника найти
|
||||
этот предмет у себя дома, навести на него камеру и отсканировать. Скорость, смекалка и азарт!
|
||||
</p>
|
||||
</div>
|
||||
<img src="/stol.svg" alt="scan example" class="landing-image" />
|
||||
|
||||
<div class="bottom-app-button">
|
||||
<button class="start-btn" @click="store.$patch({appState: 'game_settings'})">Создать комнату</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.description-section {
|
||||
font-family: 'OpenSans', sans-serif;
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
text-align: left;
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
.game-title {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.landing {
|
||||
gap: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,131 @@
|
||||
<script setup lang="ts">
|
||||
import PlayerCard from '../../components/PlayerCard.vue';
|
||||
import { onMounted, ref, computed } from 'vue';
|
||||
import { getRoomMembers, startGame as apiStartGame } from '../../api/request';
|
||||
import type { RoomMember } from '../../api/models';
|
||||
import { useAppStateStore } from '../../store/core';
|
||||
|
||||
const store = useAppStateStore()
|
||||
const players = ref<RoomMember[]>([])
|
||||
const isStartingGame = ref(false)
|
||||
const errorMessage = ref('')
|
||||
|
||||
const inviteLink = computed(() => {
|
||||
return `https://t.me/what_is_tat_bot?startapp=room_${store.room_id}`
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
players.value = await getRoomMembers(store.room_id)
|
||||
} catch (error) {
|
||||
console.error('Ошибка при получении участников комнаты:', error)
|
||||
errorMessage.value = 'Не удалось загрузить участников комнаты'
|
||||
}
|
||||
})
|
||||
|
||||
function copyInviteLink() {
|
||||
navigator.clipboard.writeText(inviteLink.value).then(() => {
|
||||
console.log('Ссылка скопирована!');
|
||||
}).catch(err => {
|
||||
console.error('Ошибка при копировании: ', err);
|
||||
});
|
||||
}
|
||||
|
||||
async function startGame() {
|
||||
if (players.value.length < 2) {
|
||||
errorMessage.value = 'Для начала игры необходимо минимум 2 игрока'
|
||||
return;
|
||||
}
|
||||
|
||||
isStartingGame.value = true;
|
||||
errorMessage.value = '';
|
||||
|
||||
try {
|
||||
const result = await apiStartGame(store.room_id);
|
||||
|
||||
if (result.success) {
|
||||
console.log('Игра успешно начата!', result);
|
||||
|
||||
if (result.game_id) {
|
||||
store.game_id = result.game_id;
|
||||
console.log('Game ID:', result.game_id);
|
||||
}
|
||||
|
||||
store.setAppState('quiz');
|
||||
} else {
|
||||
errorMessage.value = result.message || 'Ошибка при начале игры';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка при начале игры:', error);
|
||||
errorMessage.value = error instanceof Error ? error.message : 'Неизвестная ошибка при начале игры';
|
||||
} finally {
|
||||
isStartingGame.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="room-screen">
|
||||
<div class="room-header">
|
||||
<h1 class="room-title">Комната</h1>
|
||||
<p class="room-description">Отправьте ссылку-приглашение своим друзьям, чтобы сыграть вместе!</p>
|
||||
</div>
|
||||
|
||||
<div class="invite-section">
|
||||
<div class="invite-link">
|
||||
<span class="link-text">{{ inviteLink }}</span>
|
||||
<button class="copy-btn" @click="copyInviteLink">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16 1H4C2.9 1 2 1.9 2 3V17H4V3H16V1ZM19 5H8C6.9 5 6 5.9 6 7V21C6 22.1 6.9 23 8 23H19C20.1 23 21 22.1 21 21V7C21 5.9 20.1 5 19 5ZM19 21H8V7H19V21Z" fill="currentColor"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="players-section">
|
||||
<div class="players-header">
|
||||
<h2 class="players-title">Игроки</h2>
|
||||
</div>
|
||||
<div class="players-list">
|
||||
<PlayerCard
|
||||
v-for="player in players"
|
||||
:key="player.id"
|
||||
:member="player"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="errorMessage" class="error-message">
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
|
||||
<div class="bottom-app-button">
|
||||
<button
|
||||
class="start-btn"
|
||||
:disabled="players.length < 2 || isStartingGame"
|
||||
@click="startGame"
|
||||
>
|
||||
<span v-if="isStartingGame">Начинаем игру...</span>
|
||||
<span v-else>Начать игру</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.error-message {
|
||||
background-color: #fee;
|
||||
color: #d00;
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #fcc;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.start-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,80 @@
|
||||
import { ref, watch } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import { Debug } from '../utils/debug'
|
||||
|
||||
export const useAppStateStore = defineStore('appStates', () => {
|
||||
const appState = ref("auth") // Start with auth state instead of landing
|
||||
const title = ref("")
|
||||
const transcription = ref("")
|
||||
const description = ref("")
|
||||
|
||||
// Authentication state
|
||||
const isAuthenticated = ref(false)
|
||||
const currentUser = ref<{
|
||||
id: string
|
||||
username: string
|
||||
email: string
|
||||
} | null>(null)
|
||||
|
||||
// Additional fields from API response
|
||||
const exampleRussian = ref("")
|
||||
const exampleTatar = ref("")
|
||||
const objectRussian = ref("")
|
||||
const objectTatar = ref("")
|
||||
const pronunciation = ref("")
|
||||
const transcriptionIPA = ref("")
|
||||
const descriptionTatar = ref("")
|
||||
const tts_audio = ref("")
|
||||
|
||||
// game
|
||||
const room_id = ref("")
|
||||
const game_id = ref("")
|
||||
|
||||
// UI state
|
||||
const selectedDescriptionLanguage = ref<'ru' | 'tat'>('ru')
|
||||
|
||||
// Photo data
|
||||
const capturedImage = ref<string | null>(null)
|
||||
|
||||
if (Debug.enabled) {
|
||||
watch(appState, (newState, oldState) => {
|
||||
Debug.log('App state changed:', { from: oldState, to: newState })
|
||||
})
|
||||
}
|
||||
|
||||
const setAppState = (state: string) => {
|
||||
Debug.log('Setting app state to:', state)
|
||||
appState.value = state
|
||||
}
|
||||
|
||||
const setAuthenticationStatus = (authenticated: boolean, user?: { id: string, username: string, email: string }) => {
|
||||
isAuthenticated.value = authenticated
|
||||
currentUser.value = user || null
|
||||
Debug.log('Authentication status changed:', { authenticated, user })
|
||||
}
|
||||
|
||||
Debug.log('AppState store initialized with state:', appState.value)
|
||||
|
||||
return {
|
||||
appState,
|
||||
setAppState,
|
||||
isAuthenticated,
|
||||
currentUser,
|
||||
setAuthenticationStatus,
|
||||
title,
|
||||
transcription,
|
||||
description,
|
||||
exampleRussian,
|
||||
exampleTatar,
|
||||
objectRussian,
|
||||
objectTatar,
|
||||
pronunciation,
|
||||
transcriptionIPA,
|
||||
capturedImage,
|
||||
descriptionTatar,
|
||||
selectedDescriptionLanguage,
|
||||
tts_audio,
|
||||
room_id,
|
||||
game_id
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,511 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300..800;1,300..800&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Old+Standard+TT:ital,wght@0,400;0,700;1,400&display=swap');
|
||||
|
||||
body {
|
||||
background: #303030;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.landing {
|
||||
padding-top: 60px;
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 60px;
|
||||
position: relative;
|
||||
|
||||
.user-menu-container {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.landing-title {
|
||||
font-weight: 600;
|
||||
font-size: 88px;
|
||||
color: #90EE90;
|
||||
font-family: 'OpenSans', sans-serif;
|
||||
}
|
||||
|
||||
.user-menu-button {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
color: rgba(254, 254, 254, 1);
|
||||
font-family: OpenSans, sans-serif;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.user-menu-button:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.user-menu-dropdown {
|
||||
position: absolute;
|
||||
top: 50px;
|
||||
right: 0;
|
||||
background: rgba(20, 30, 40, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 12px;
|
||||
min-width: 200px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.username {
|
||||
font-family: OpenSans, sans-serif;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: rgba(254, 254, 254, 1);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.email {
|
||||
font-family: OpenSans, sans-serif;
|
||||
font-size: 14px;
|
||||
color: rgba(254, 254, 254, 0.7);
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
background: none;
|
||||
border: none;
|
||||
text-align: left;
|
||||
font-family: OpenSans, sans-serif;
|
||||
font-size: 14px;
|
||||
color: rgba(254, 254, 254, 0.9);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.menu-item:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.logout-item {
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
.bubbles {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.bubble {
|
||||
border: 2px solid rgba(255, 255, 255, 1);
|
||||
border-radius: 100px;
|
||||
padding: 3px 33px 3px 33px;
|
||||
|
||||
font-weight: 400;
|
||||
font-family: OpenSans, sans-serif;
|
||||
font-size: 24px;
|
||||
color: rgba(254, 254, 254, 1);
|
||||
white-space: nowrap;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.bubble:first-child {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.bubble:last-child {
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.game-title {
|
||||
font-weight: 600;
|
||||
font-size: 48px;
|
||||
color: #90EE90;
|
||||
font-family: 'OpenSans', sans-serif;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.description-section {
|
||||
max-width: 600px;
|
||||
text-align: center;
|
||||
|
||||
p {
|
||||
font-family: 'OpenSans', sans-serif;
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
color: rgba(254, 254, 254, 0.9);
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Адаптивные стили для изображений на лендинге */
|
||||
.landing-logo {
|
||||
width: min(300px, 80vw);
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.landing-image {
|
||||
width: min(400px, 90vw);
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
/* Дополнительная адаптивность для изображений */
|
||||
@media screen and (max-width: 480px) {
|
||||
.landing-logo {
|
||||
width: min(200px, 70vw);
|
||||
}
|
||||
|
||||
.landing-image {
|
||||
width: min(300px, 85vw);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 320px) {
|
||||
.landing-logo {
|
||||
width: min(150px, 60vw);
|
||||
}
|
||||
|
||||
.landing-image {
|
||||
width: min(250px, 80vw);
|
||||
}
|
||||
}
|
||||
|
||||
.error {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px 20px;
|
||||
text-align: center;
|
||||
gap: 24px;
|
||||
|
||||
.error-icon {
|
||||
font-size: 64px;
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
.error-title {
|
||||
font-family: OpenSans, sans-serif;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: rgba(254, 254, 254, 1);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
font-family: OpenSans, sans-serif;
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
color: rgba(254, 254, 254, 0.8);
|
||||
line-height: 1.5;
|
||||
max-width: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
.results-screen {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
padding-top: 60px;
|
||||
color: rgba(254, 254, 254, 1);
|
||||
font-family: OpenSans, sans-serif;
|
||||
gap: 8px;
|
||||
|
||||
.title-section {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 48px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.transcription {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-app-button {
|
||||
position: fixed;
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
bottom: 20px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
pointer-events: none;
|
||||
z-index: 1000;
|
||||
}
|
||||
.start-btn {
|
||||
pointer-events: auto;
|
||||
background: #FEFEFE;
|
||||
color: black;
|
||||
border: none;
|
||||
padding: 14px 36px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
border-radius: 8px;
|
||||
flex: 2;
|
||||
font-family: 'OpenSans', sans-serif;
|
||||
font-weight: 500;
|
||||
}
|
||||
.start-btn:active { transform: translateY(1px); }
|
||||
|
||||
.game-btn {
|
||||
pointer-events: auto;
|
||||
background: transparent;
|
||||
color: #FEFEFE;
|
||||
border: 2px solid #FEFEFE;
|
||||
padding: 14px 36px;
|
||||
border-radius: 28px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
flex: 1;
|
||||
font-family: 'OpenSans', sans-serif;
|
||||
font-weight: 500;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.game-btn:active { transform: translateY(1px); }
|
||||
|
||||
/* Authentication Styles */
|
||||
.auth-screen {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.auth-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 40px;
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.auth-logo {
|
||||
width: 120px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.auth-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.auth-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||
border-top: 3px solid rgba(255, 255, 255, 1);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.auth-title {
|
||||
font-family: OpenSans, sans-serif;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: rgba(254, 254, 254, 1);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.auth-message {
|
||||
font-family: OpenSans, sans-serif;
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
color: rgba(254, 254, 254, 0.8);
|
||||
text-align: center;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.auth-error {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.retry-button {
|
||||
background: linear-gradient(90deg, #6a11cb 0%, #2575fc 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
border-radius: 24px;
|
||||
font-family: OpenSans, sans-serif;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 6px 16px rgba(37, 117, 252, 0.25);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.retry-button:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.retry-button:hover {
|
||||
box-shadow: 0 8px 20px rgba(37, 117, 252, 0.35);
|
||||
}
|
||||
|
||||
/* Room Screen Styles */
|
||||
.room-screen {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px;
|
||||
padding-bottom: 100px;
|
||||
gap: 24px;
|
||||
color: rgba(254, 254, 254, 1);
|
||||
font-family: 'OpenSans', sans-serif;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.room-title {
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
color: #90EE90;
|
||||
margin: 0 0 12px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.room-description {
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
color: #fff;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.invite-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.invite-label {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: rgba(254, 254, 254, 1);
|
||||
}
|
||||
|
||||
.invite-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 12px;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.link-text {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
font-family: 'Courier New', monospace;
|
||||
color: rgba(254, 254, 254, 0.9);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
color: rgba(254, 254, 254, 0.8);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.copy-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.players-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.players-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.players-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: rgba(254, 254, 254, 1);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.players-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.start-btn:disabled {
|
||||
background: rgba(254, 254, 254, 0.3);
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.room-screen {
|
||||
padding-top: 60px;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Debug utility for development mode
|
||||
*/
|
||||
export class Debug {
|
||||
private static isEnabled = import.meta.env.DEBUG === 'true'
|
||||
|
||||
static get enabled(): boolean {
|
||||
return this.isEnabled
|
||||
}
|
||||
|
||||
static log(message: any, ...args: any[]): void {
|
||||
if (this.isEnabled) {
|
||||
console.log(`[DEBUG]`, message, ...args)
|
||||
}
|
||||
}
|
||||
|
||||
static warn(message: any, ...args: any[]): void {
|
||||
if (this.isEnabled) {
|
||||
console.warn(`[DEBUG WARN]`, message, ...args)
|
||||
}
|
||||
}
|
||||
|
||||
static error(message: any, ...args: any[]): void {
|
||||
if (this.isEnabled) {
|
||||
console.error(`[DEBUG ERROR]`, message, ...args)
|
||||
}
|
||||
}
|
||||
|
||||
static info(label: string, data: any): void {
|
||||
if (this.isEnabled) {
|
||||
console.group(`[DEBUG INFO] ${label}`)
|
||||
console.log(data)
|
||||
console.groupEnd()
|
||||
}
|
||||
}
|
||||
|
||||
static time<T>(label: string, fn: () => T): T {
|
||||
if (this.isEnabled) {
|
||||
console.time(`[DEBUG TIME] ${label}`)
|
||||
const result = fn()
|
||||
console.timeEnd(`[DEBUG TIME] ${label}`)
|
||||
return result
|
||||
}
|
||||
return fn()
|
||||
}
|
||||
|
||||
static async timeAsync<T>(label: string, fn: () => Promise<T>): Promise<T> {
|
||||
if (this.isEnabled) {
|
||||
console.time(`[DEBUG TIME] ${label}`)
|
||||
const result = await fn()
|
||||
console.timeEnd(`[DEBUG TIME] ${label}`)
|
||||
return result
|
||||
}
|
||||
return fn()
|
||||
}
|
||||
}
|
||||
|
||||
if (Debug.enabled) {
|
||||
(window as any).debug = Debug
|
||||
Debug.log('Debug mode enabled. Access debug utilities via window.debug')
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly DEBUG: string
|
||||
// Add other env variables here as needed
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2023",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { defineConfig, loadEnv } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
// import vueDevTools from 'vite-plugin-vue-devtools'
|
||||
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, process.cwd(), '')
|
||||
|
||||
return {
|
||||
plugins: [vue()],
|
||||
/* говно вью тулс, я вырубил, можешь врубить если надо
|
||||
plugins: [vue(), vueDevTools()], */
|
||||
define: {
|
||||
'import.meta.env.DEBUG': JSON.stringify(env.DEBUG || 'false'),
|
||||
},
|
||||
// можно убрать, эт я для удобства врубил
|
||||
server: {
|
||||
host: true,
|
||||
open: false
|
||||
},
|
||||
build: {
|
||||
sourcemap: env.DEBUG === 'true'
|
||||
}
|
||||
}
|
||||
})
|
||||