1404/06/25-add audioPlayer && challenges page connect to server

This commit is contained in:
MojtabaSoumi 2025-09-16 18:17:10 +03:30
parent eed7b6e3a2
commit ffe8f0e63d
39 changed files with 1567 additions and 384 deletions

View File

@ -2,9 +2,9 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="icon" type="image/svg+xml" href="/e1.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Vue + TS</title>
<title>ماچارتا - Macharta</title>
</head>
<body>
<div id="app"></div>

133
package-lock.json generated
View File

@ -9,6 +9,7 @@
"version": "0.0.0",
"dependencies": {
"@lucky-canvas/vue": "^0.1.11",
"@supabase/supabase-js": "^2.57.4",
"@types/node": "^24.3.0",
"@vitejs/plugin-vue-jsx": "^5.0.1",
"@vueuse/core": "^13.7.0",
@ -1322,6 +1323,80 @@
"nanopop": "^2.1.0"
}
},
"node_modules/@supabase/auth-js": {
"version": "2.71.1",
"resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.71.1.tgz",
"integrity": "sha512-mMIQHBRc+SKpZFRB2qtupuzulaUhFYupNyxqDj5Jp/LyPvcWvjaJzZzObv6URtL/O6lPxkanASnotGtNpS3H2Q==",
"license": "MIT",
"dependencies": {
"@supabase/node-fetch": "^2.6.14"
}
},
"node_modules/@supabase/functions-js": {
"version": "2.4.6",
"resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.4.6.tgz",
"integrity": "sha512-bhjZ7rmxAibjgmzTmQBxJU6ZIBCCJTc3Uwgvdi4FewueUTAGO5hxZT1Sj6tiD+0dSXf9XI87BDdJrg12z8Uaew==",
"license": "MIT",
"dependencies": {
"@supabase/node-fetch": "^2.6.14"
}
},
"node_modules/@supabase/node-fetch": {
"version": "2.6.15",
"resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz",
"integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==",
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
}
},
"node_modules/@supabase/postgrest-js": {
"version": "1.21.4",
"resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.21.4.tgz",
"integrity": "sha512-TxZCIjxk6/dP9abAi89VQbWWMBbybpGWyvmIzTd79OeravM13OjR/YEYeyUOPcM1C3QyvXkvPZhUfItvmhY1IQ==",
"license": "MIT",
"dependencies": {
"@supabase/node-fetch": "^2.6.14"
}
},
"node_modules/@supabase/realtime-js": {
"version": "2.15.5",
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.15.5.tgz",
"integrity": "sha512-/Rs5Vqu9jejRD8ZeuaWXebdkH+J7V6VySbCZ/zQM93Ta5y3mAmocjioa/nzlB6qvFmyylUgKVS1KpE212t30OA==",
"license": "MIT",
"dependencies": {
"@supabase/node-fetch": "^2.6.13",
"@types/phoenix": "^1.6.6",
"@types/ws": "^8.18.1",
"ws": "^8.18.2"
}
},
"node_modules/@supabase/storage-js": {
"version": "2.12.1",
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.12.1.tgz",
"integrity": "sha512-QWg3HV6Db2J81VQx0PqLq0JDBn4Q8B1FYn1kYcbla8+d5WDmTdwwMr+EJAxNOSs9W4mhKMv+EYCpCrTFlTj4VQ==",
"license": "MIT",
"dependencies": {
"@supabase/node-fetch": "^2.6.14"
}
},
"node_modules/@supabase/supabase-js": {
"version": "2.57.4",
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.57.4.tgz",
"integrity": "sha512-LcbTzFhHYdwfQ7TRPfol0z04rLEyHabpGYANME6wkQ/kLtKNmI+Vy+WEM8HxeOZAtByUFxoUTTLwhXmrh+CcVw==",
"license": "MIT",
"dependencies": {
"@supabase/auth-js": "2.71.1",
"@supabase/functions-js": "2.4.6",
"@supabase/node-fetch": "2.6.15",
"@supabase/postgrest-js": "1.21.4",
"@supabase/realtime-js": "2.15.5",
"@supabase/storage-js": "2.12.1"
}
},
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@ -1337,12 +1412,27 @@
"undici-types": "~7.10.0"
}
},
"node_modules/@types/phoenix": {
"version": "1.6.6",
"resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz",
"integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==",
"license": "MIT"
},
"node_modules/@types/web-bluetooth": {
"version": "0.0.21",
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
"integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==",
"license": "MIT"
},
"node_modules/@types/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@unocss/astro": {
"version": "66.4.2",
"resolved": "https://registry.npmjs.org/@unocss/astro/-/astro-66.4.2.tgz",
@ -3798,6 +3888,12 @@
"node": ">=6"
}
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
@ -4138,6 +4234,43 @@
"loose-envify": "^1.0.0"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause"
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"license": "MIT",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/ws": {
"version": "8.18.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",

View File

@ -10,6 +10,7 @@
},
"dependencies": {
"@lucky-canvas/vue": "^0.1.11",
"@supabase/supabase-js": "^2.57.4",
"@types/node": "^24.3.0",
"@vitejs/plugin-vue-jsx": "^5.0.1",
"@vueuse/core": "^13.7.0",

BIN
public/e1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -45,14 +45,7 @@
<!-- </AAffix>-->
<AAffix offset-top="0" >
<div v-if="router.currentRoute.value.meta.showNavbar" class="flex px-4 items-center pt-10 mb-3 bg-gradient-to-b from-[#EAEAEA] ">
<div class="bg-[#C1FCE2] flex items-center relative rounded-full h-6vh w-1/2">
<div class="absolute bottom-0"><img src="@/assets/img/boy_3752622%201.svg"></div>
<div class=" text-center w-full font-700 pr-8 ">امیر علی</div>
</div>
<div class="flex gap-3 w-1/2 justify-end ">
<div><img src="@/assets/img/alertIcon.svg"></div>
<div><img src="@/assets/img/menu_8917404%201.svg"></div>
</div>
<Header/>
</div>
</AAffix>
<RouterView v-slot="{ Component }">
@ -80,6 +73,7 @@ import {colors} from "@/themeConfig.ts";
import router from "@/router";
import Menu from "@/components/Menu.vue";
import {route} from "vant/es/composables/use-route";
import Header from "@/components/Header.vue";
const keepAliveList = ['product-list']
</script>

6
src/api/challenges.ts Normal file
View File

@ -0,0 +1,6 @@
import axios from '@/utils/useAxios'
import { supabaseService} from "../../supabase.ts";
export const getChallenges = async () => {
return await supabaseService.fetch('challenge',"select_group")
}

View File

@ -258,9 +258,7 @@ body :where(.css-dev-only-do-not-override-1yhcnou).ant-input-affix-wrapper{
body .ant-pagination .ant-pagination-item a {
color: #616161;
}
body .ant-select {
background-color: #ffffff;
}
body :where(.css-dev-only-do-not-override-ajjz7p).ant-select:not(.ant-select-customize-input) .ant-select-selector {
position: relative;
background-color: #ffffff;
@ -272,9 +270,9 @@ body :where(.css-dev-only-do-not-override-1yhcnou).ant-btn-default:not(:disable
body :where(.css-dev-only-do-not-override-1yhcnou).ant-tabs .ant-tabs-tab.ant-tabs-tab-active .ant-tabs-tab-btn{
//color: #2563EB;
}
body .ant-select-selector, .ant-tooltip-open, .ant-select-selection-item, .ant-select-item .ant-select-item-option{
color: #616161;
}
//body .ant-select-selector, .ant-tooltip-open, .ant-select-selection-item, .ant-select-item .ant-select-item-option{
// color: #616161;
//}
body .ant-tabs-nav-wrap{
justify-content: center;
}
@ -299,4 +297,7 @@ body .backProductPriceCard {
body .iconProductPriceCard {
color: #2563EB;
background-color: rgba(37, 99, 235, 0.29);
}
.ant-select:not(.ant-select-customize-input) .ant-select-selector{
background: none;
}

BIN
src/assets/img/Group31.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

BIN
src/assets/img/img-b1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

BIN
src/assets/img/img-b2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

4
src/assets/img/line.svg Normal file
View File

@ -0,0 +1,4 @@
<svg width="412" height="634" viewBox="0 0 412 634" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M-141 622.5C69.9284 486.934 531 -66.5 531 -66.5" stroke="black" stroke-opacity="0.32" stroke-width="3"/>
<path d="M-141 632.5C69.9284 496.737 531 -57.5 531 -57.5" stroke="black" stroke-opacity="0.32" stroke-width="3"/>
</svg>

After

Width:  |  Height:  |  Size: 335 B

BIN
src/assets/img/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 178 KiB

7
src/assets/img/plane.svg Normal file
View File

@ -0,0 +1,7 @@
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.389 39.519C13.177 40.07 12.698 41.554 12.578 43.933C14.618 43.528 16.156 41.711 16.156 39.527H13.492C13.457 39.526 13.424 39.519 13.389 39.519Z" fill="black"/>
<path d="M22.371 41.213C22.8614 41.213 23.259 40.4582 23.259 39.527C23.259 38.5958 22.8614 37.841 22.371 37.841C21.8805 37.841 21.483 38.5958 21.483 39.527C21.483 40.4582 21.8805 41.213 22.371 41.213Z" fill="black"/>
<path d="M25.924 40.089C26.415 40.089 26.813 39.3346 26.813 38.404C26.813 37.4734 26.415 36.719 25.924 36.719C25.433 36.719 25.035 37.4734 25.035 38.404C25.035 39.3346 25.433 40.089 25.924 40.089Z" fill="black"/>
<path d="M29.475 38.965C29.9649 38.965 30.362 38.2102 30.362 37.279C30.362 36.3478 29.9649 35.593 29.475 35.593C28.9851 35.593 28.588 36.3478 28.588 37.279C28.588 38.2102 28.9851 38.965 29.475 38.965Z" fill="black"/>
<path d="M56.113 47.531L38.667 39.734C38.8263 39.5187 38.986 39.3013 39.146 39.082C39.576 38.492 40.032 37.871 40.539 37.261C42.856 37.07 47.336 35.394 48.924 33.817C49.071 33.671 49.172 33.376 49.237 33.007L62 25.532L51.871 28.382L52.902 23.382L56.256 22.104L53.082 22.507L53.48 20.578L55.125 20.988L58.777 18.384L54.218 16.998L54.243 16.872L54.927 13.558L51.917 14.828L51.21 15.126L48.123 12L44.57 13.123L46.392 17.158L46.04 17.305L45.743 17.841L42.415 23.861L40.758 24.072C40.6734 23.998 40.5663 23.9547 40.454 23.949C40.454 23.949 34.64 24.713 32.462 26.753C32.046 27.143 31.613 27.722 31.581 28.867C28.9788 29.6483 26.3804 30.4423 23.786 31.249L15.267 24.359V18.741L13.491 19.865L11.715 25.482L15.779 33.839C15.397 33.971 15.033 34.098 14.703 34.218C9.794 35.987 8.763 37.971 7.57 40.269C6.862 41.632 6.13 43.041 4.3 44.888C3.035 46.166 1.442 48.088 2.192 50.027C2.698 51.335 4.017 52 6.111 52C9.993 52 17.429 49.879 27.822 45.83L55.225 51.324L58.777 48.721V42.334L57.053 43.513L56.113 47.531ZM55.556 28.276L49.318 31.929C49.3209 31.3091 49.2518 30.6909 49.112 30.087L55.556 28.276ZM41.877 24.836L41.338 25.811C41.3354 25.51 41.2961 25.2105 41.221 24.919L41.877 24.836ZM35.69 38.404L22.066 44.676L24.535 45.171C15.17 48.685 9.152 50.2 6.111 50.2C3.083 50.2 3.002 48.7 5.496 46.18C6.916 44.747 7.755 43.516 8.38 42.43L8.369 42.449C9.182 43.403 10.372 44.019 11.716 44.019L11.738 44.017C11.824 41.853 12.182 40.302 12.469 39.388C11.8389 39.2387 11.2496 38.9523 10.743 38.549C9.957 39.391 9.506 40.313 8.949 41.379C10.127 39.121 10.818 37.518 15.263 35.916C22.325 33.371 42.367 27.537 42.367 27.537L47.162 18.865L47.232 19.02L51.804 20.159L50 28.905L48.763 29.253C48.67 29.141 48.568 29.081 48.455 29.095C46.212 29.389 42.138 30.715 40.128 32.785C39.714 33.211 39.239 33.79 39.239 35.031C39.239 35.369 39.271 35.686 39.324 35.975C38.438 37.03 37.719 38.097 37.025 38.999L35.69 38.404ZM8.668 41.912L8.712 41.829C8.69867 41.857 8.684 41.8847 8.668 41.912Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

10
src/assets/img/star.svg Normal file
View File

@ -0,0 +1,10 @@
<svg width="51" height="51" viewBox="0 0 51 51" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_617_231)">
<path d="M37.3561 24.0181C40.3843 27.1924 41.8986 28.7787 41.4263 30.3043C40.9539 31.8299 38.8082 32.283 34.5161 33.1909L33.4053 33.4261C32.1851 33.6838 31.5749 33.8127 31.0922 34.1575C30.6094 34.5023 30.3075 35.0268 29.7039 36.0756L29.1547 37.0284C27.0304 40.7135 25.9696 42.5548 24.3075 42.5574C22.6476 42.559 21.4852 40.7197 19.1625 37.0401L18.5622 36.088C17.9024 35.0434 17.5721 34.5192 17.0713 34.1753C16.5689 33.8308 15.9516 33.7033 14.7188 33.4487L13.5953 33.2174C9.25319 32.3197 7.08271 31.8719 6.52879 30.3481C5.97488 28.8243 7.40326 27.2329 10.259 24.0531L10.9982 23.228C11.8109 22.3251 12.2156 21.8731 12.3887 21.3141C12.5618 20.7551 12.4832 20.1535 12.3229 18.9493L12.1788 17.8525C11.6198 13.6138 11.3406 11.4936 12.6565 10.5505C13.9766 9.6052 16.0204 10.4622 20.1101 12.175L21.1662 12.6185C22.3288 13.1056 22.9101 13.3491 23.5182 13.3467C24.1268 13.348 24.6955 13.1021 25.8286 12.6126L26.8634 12.1668C30.8573 10.4428 32.8557 9.58214 34.2259 10.5219C35.5962 11.4616 35.4317 13.5839 35.1004 17.8241L35.0152 18.9205C34.9211 20.1254 34.8743 20.727 35.0773 21.2859C35.2802 21.8449 35.7113 22.2943 36.5724 23.1965L37.3561 24.0181Z" stroke="black" stroke-width="2"/>
</g>
<defs>
<clipPath id="clip0_617_231">
<rect width="40.4737" height="40.4737" fill="white" transform="translate(50.6333 11.9703) rotate(107.203)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

View File

@ -1,5 +1,5 @@
{
"primary": "#1D4ED8",
"primary": "#28C792",
"secondary": "#16A34A",
"background": "#FFFFFF",
"bgm": "#FAFAFA"

27
src/components/Header.vue Normal file
View File

@ -0,0 +1,27 @@
<template>
<div class="bg-[#C1FCE2] flex items-center relative rounded-full h-6vh w-2/3">
<div class="absolute bottom-0"><img src="@/assets/img/boy_3752622%201.svg"></div>
<div class=" text-center w-full font-700 pr-9 ">{{ userInfo?.userInfo?.name }}</div>
<div id="18436231725"></div>
</div>
<div class="flex gap-3 w-1/3 justify-end ">
<div><img src="@/assets/img/alertIcon.svg"></div>
<div><img src="@/assets/img/menu_8917404%201.svg"></div>
</div>
</template>
<script setup lang="ts">
import {useUserStore} from "@/store/user.ts";
import {onMounted} from "vue";
const userInfo=useUserStore()
onMounted(async ()=>{
await userInfo.getUserInfo()
console.log(userInfo.user)
})
</script>
<style scoped lang="less">
</style>

View File

@ -59,7 +59,7 @@ _axios.interceptors.response.use(
if (error.response && (error.response.status == 401)) {
//debugger
removeToken()
router.push({ name: 'login' })
router.push({ name: 'MainLogin' })
}
// Any status codes that falls outside the range of 2xx cause this function to trigger
// Do something with response error

19
src/models/user.ts Normal file
View File

@ -0,0 +1,19 @@
export interface User {
user_id: string;
name: string;
email: string | null;
phone: string;
created_at: Date;
updated_at: Date;
username: string | null;
last_name: string;
invite_code: any;
type: string;
id: string;
avatar: string | null;
birthday: string | null;
stats: object;
description: string | null;
bio: string | null;
first_name: string;
}

View File

@ -1,13 +1,22 @@
import { createRouter, createWebHistory } from 'vue-router'
import { isToken } from '@/utils/is'
import {isSignUpUser, isToken} from '@/utils/is'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/main_login',
name: 'MainLogin',
component: () => import('@/views/login/index.vue'),
meta: {
showMenu: false,
showNavbar: false
}
},
{
path: '/login',
name: 'login',
component: () => import('@/views/login/login.vue'),
component: () => import('@/views/login/components/login.vue'),
meta: {
showMenu: false,
showNavbar: false
@ -16,7 +25,7 @@ const router = createRouter({
{
path: '/register',
name: 'register',
component: () => import('@/views/login/register.vue'),
component: () => import('@/views/login/components/register.vue'),
meta: {
showMenu: false,
showNavbar: false
@ -156,16 +165,18 @@ const router = createRouter({
})
router.beforeEach((to, from, next) => {
if (isToken()) {
if (to.name === 'login') {
if (isToken()&& isSignUpUser()) {
if (to.name === 'login' || to.name === 'MainLogin' ) {
return next({ name: 'home' })
}
return next()
} else {
if (['login', 'notFound', 'register'].includes(to.name as string)) {
if (['login', 'notFound', 'register','MainLogin'].includes(to.name as string)) {
return next()
}
return next({ name: 'login' })
// return next()
return next({ name: 'MainLogin' })
}
})

View File

@ -1,30 +1,31 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
// import { getMe } from '@/api/user'
// import type { User } from '@/model/user'
import type { User } from '@/models/user'
import { removeToken } from '@/utils'
import router from '@/router'
import {getMe} from "@/api/auth.ts";
export const useUserStore = defineStore('user', () => {
const user = ref<User | null>(null)
const userInfo = ref<User | null>(null)
const priceStarted = ref<boolean>(false)
async function getUserInfo() {
user.value = await getMe()
const {user} = await getMe()
userInfo.value=user
// await setIntervalGetPrice()
// console.log(user.value, 'user')
console.log(userInfo.value, 'user')
}
async function logout() {
removeToken()
user.value = null
await router.replace({ name: 'login' })
userInfo.value = null
await router.replace({ name: 'MainLogin' })
}
return {
priceStarted,
user,
userInfo,
getUserInfo,
logout,
}

View File

@ -6,7 +6,6 @@ const loadingMore = ref<boolean>(false)
function setToken(token: string) {
return localStorage.setItem(tokenName, token)
}
function getToken() {
return localStorage.getItem(tokenName)
}

View File

@ -1,4 +1,5 @@
import { getToken } from '@/utils/index'
import {useUserStore} from "@/store/user.ts";
function isDev() {
return import.meta.env.DEV
@ -9,4 +10,10 @@ function isToken() {
return !!token
}
export { isDev, isToken }
function isSignUpUser() {
const name = useUserStore()?.userInfo?.name
console.log(!!name)
return !!name
}
export { isDev, isToken ,isSignUpUser }

View File

@ -66,7 +66,7 @@ _axios.interceptors.response.use(
console.log('error')
removeToken()
// router.push({ name: 'login' })
await router.push({name: 'login'})
await router.push({name: 'MainLogin'})
} else if (error.response && error.response.status == 403) {
message.error('شما دسترسی لازم برای انجام این عملیات را ندارید')
} else if (error.response && error.response.status == 422) {

View File

@ -0,0 +1,341 @@
<template>
<div>
<div class=" mx-auto p-4 rounded-2xl shadow-md flex flex-col gap-4">
<!-- Header -->
<div class="flex flex-col gap-4 justify-center items-center">
<div class="flex flex-col gap-3 items-center bg-background/50 border-black border-1 rounded-7.7 p-5 w-full ">
<img :src="currentTrack.cover" alt="cover" class=" rounded-2xl w-full justify-self-end border-black border-2"/>
<div class="">
<h3 class="font-400 text-lg">{{ currentTrack.title }}</h3>
<p class="text-sm opacity-80">{{ currentTrack.artist }}</p>
</div>
</div>
<div class=" w-full bg-background/50 border-black border-1 rounded-3xl p-5">
<div class="flex gap-7 justify-center items-center w-full ">
<!-- <a-select-->
<!-- v-model:value="playbackRate"-->
<!-- class="rounded-md px-2 py-1 "-->
<!-- :dropdown-match-select-width="false"-->
<!-- >-->
<!-- <a-select-option v-for="r in rates" :value="r">{{ r }}x</a-select-option>-->
<!-- </a-select>-->
<div class="flex items-center gap-2 text-sm">
<span>سرعت</span>
<AButton
class="rounded-md px-3 py-1 !bg-transparent text-sm w-14"
@click="nextRate"
>
{{ playbackRate }}x
</AButton>
</div>
<div @click="prev" aria-label="قبلی">
<img src="@/assets/img/Iconnext1.svg" >
</div>
<div @click="togglePlay" class="!bg-none hover:scale-[1.03]"
aria-label="پخش/توقف">
<span v-if="!isPlaying"><img src="@/assets/img/iconplay1.svg" ></span>
<span v-else><icon icon="vuesax-linear:pause" size="33" ></icon></span>
</div>
<div @click="next" class="p-2 rounded-md hover:bg-white/5" aria-label="بعدی">
<img src="@/assets/img/Iconprevious1.svg" >
</div>
</div>
</div>
</div>
<!-- Progress -->
<div class="flex flex-col gap-2">
<div class="flex items-center gap-3 text-xs ">
<span>{{ formatTime(currentTime) }}</span>
<div class="flex-1">
<a-slider
v-model:value="currentTime"
:max="duration"
:step="0.1"
@change="onSeek"
:tooltipOpen="false"
/>
</div>
<span>{{ formatTime(duration) }}</span>
</div>
<!-- <div class="flex items-center gap-2 text-sm">-->
<!-- <span class="">صدا</span>-->
<!-- <a-slider v-model:value="volume" :min="0" :max="1" :step="0.1" class="w-36" :tooltipOpen="false"/>-->
<!-- </div>-->
<!-- controls row -->
<!-- <button @click="toggleLoop" :class="loop ? 'text-primary' : ''" class="px-2 py-1 rounded-md bg-white/5">-->
<!-- تکرار-->
<!-- </button>-->
<!-- <button @click="toggleShuffle" :class="shuffle ? 'text-primary' : ''" class="px-2 py-1 rounded-md bg-white/5">-->
<!-- پراکنده-->
<!-- </button>-->
<!-- <div class="ml-auto text-sm opacity-80">{{ currentIndex + 1 }} / {{ playlist.length }}</div>-->
</div>
</div>
<div class="flex items-center gap-3">
<!-- Playlist -->
<!-- <div class="mt-2">-->
<!-- <ul class="flex flex-col gap-2 max-h-40 overflow-auto">-->
<!-- <li v-for="(t, i) in playlist" :key="t.id"-->
<!-- class="flex items-center gap-3 p-2 rounded-md hover:bg-white/3 cursor-pointer"-->
<!-- :class="i === currentIndex ? 'bg-white/5' : ''"-->
<!-- @click="playIndex(i)">-->
<!-- <img :src="t.cover" alt="cover" class="w-12 h-12 rounded-md object-cover"/>-->
<!-- <div class="flex-1">-->
<!-- <div class="text-sm font-medium">{{ t.title }}</div>-->
<!-- <div class="text-xs opacity-80">{{ t.artist }}</div>-->
<!-- </div>-->
<!-- <div class="text-xs opacity-70">{{ formatTime(t.duration || 0) }}</div>-->
<!-- </li>-->
<!-- </ul>-->
<!-- </div>-->
<!-- hidden audio element -->
<audio ref="audio" :src="currentTrack.src" preload="metadata"></audio>
</div>
</div>
</template>
<script setup>
import {ref, computed, onMounted, watch, nextTick, onBeforeUnmount} from 'vue'
import story from '@/assets/storySound/story1.mp3'
import Icon from "@/components/Icon.vue";
// ======= Playlist (sample data) =======
const playlist = ref([
{
id: 1,
title: 'قطعهٔ اول',
artist: 'خوانندهٔ الف',
src: 'https://server4.iranmusic.ir/music/Arman/Songs/Alireza%20Ghorbani%20-%20Roozegare%20Gharib.mp3',
cover: 'https://picsum.photos/seed/1/200/200'
},
{
id: 2,
title: 'قطعهٔ دوم',
artist: 'خوانندهٔ ب',
src: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-3.mp3',
cover: 'https://picsum.photos/seed/2/200/200'
},
{id: 3, title: 'جالی خجالتی', artist: 'قصه فسقلی ها', src: story, cover: 'https://player.iranseda.ir/picture/?attid=101453&s=c'},
])
// ======= Refs & State =======
const audio = ref(null)
const currentIndex = ref(0)
const isPlaying = ref(false)
const currentTime = ref(0)
const duration = ref(0)
const volume = ref(1)
const playbackRate = ref(1)
const rates = [0.5, 0.75, 1, 1.25, 1.5, 2]
const loop = ref(false)
const shuffle = ref(false)
const currentTrack = computed(() => playlist.value[currentIndex.value] || {})
const progressPercent = computed(() => (duration.value ? (currentTime.value / duration.value) * 100 : 0))
function nextRate() {
const index = rates.indexOf(playbackRate.value);
playbackRate.value = rates[(index + 1) % rates.length];
}
// ======= Actions =======
function play() {
if (!audio.value) return
audio.value.play()
}
function pause() {
if (!audio.value) return
audio.value.pause()
}
function togglePlay() {
if (!audio.value) return
if (isPlaying.value) pause()
else play()
}
function next() {
if (shuffle.value) {
currentIndex.value = Math.floor(Math.random() * playlist.value.length)
} else {
currentIndex.value = (currentIndex.value + 1) % playlist.value.length
}
playIndex(currentIndex.value)
}
function prev() {
if (audio.value && audio.value.currentTime > 3) {
audio.value.currentTime = 0
} else {
currentIndex.value = (currentIndex.value - 1 + playlist.value.length) % playlist.value.length
playIndex(currentIndex.value)
}
}
function playIndex(i) {
currentIndex.value = i
// ensure audio src updated before play
nextTick(() => {
audio.value.load()
play()
})
}
function seekByClick(e) {
const rect = e.currentTarget.getBoundingClientRect()
const x = e.clientX - rect.left
const pct = x / rect.width
if (audio.value && duration.value) audio.value.currentTime = pct * duration.value
}
function onSeek() {
if (audio.value) audio.value.currentTime = currentTime.value
}
function toggleLoop() {
loop.value = !loop.value;
if (audio.value) audio.value.loop = loop.value
}
function toggleShuffle() {
shuffle.value = !shuffle.value
}
function formatTime(sec) {
if (!sec || isNaN(sec)) return '00:00'
const m = Math.floor(sec / 60).toString().padStart(2, '0')
const s = Math.floor(sec % 60).toString().padStart(2, '0')
return `${m}:${s}`
}
// ======= Lifecycle & watchers =======
onMounted(() => {
if (!audio.value) return
audio.value.volume = volume.value
audio.value.playbackRate = playbackRate.value
const onTimeUpdate = () => {
currentTime.value = audio.value.currentTime
}
const onLoadedMeta = () => {
duration.value = audio.value.duration
}
const onPlay = () => {
isPlaying.value = true
}
const onPause = () => {
isPlaying.value = false
}
const onEnded = () => {
if (!loop.value) next()
}
audio.value.addEventListener('timeupdate', onTimeUpdate)
audio.value.addEventListener('loadedmetadata', onLoadedMeta)
audio.value.addEventListener('play', onPlay)
audio.value.addEventListener('pause', onPause)
audio.value.addEventListener('ended', onEnded)
// keyboard shortcuts
const onKey = (ev) => {
if (ev.target && (ev.target.tagName === 'INPUT' || ev.target.tagName === 'TEXTAREA')) return
if (ev.code === 'Space') {
ev.preventDefault();
togglePlay()
}
if (ev.code === 'ArrowRight') {
if (audio.value) audio.value.currentTime += 5
}
if (ev.code === 'ArrowLeft') {
if (audio.value) audio.value.currentTime -= 5
}
}
window.addEventListener('keydown', onKey)
onBeforeUnmount(() => {
audio.value.removeEventListener('timeupdate', onTimeUpdate)
audio.value.removeEventListener('loadedmetadata', onLoadedMeta)
audio.value.removeEventListener('play', onPlay)
audio.value.removeEventListener('pause', onPause)
audio.value.removeEventListener('ended', onEnded)
window.removeEventListener('keydown', onKey)
})
})
// reactive side-effects
watch(volume, (v) => {
if (audio.value) audio.value.volume = v
})
watch(playbackRate, (r) => {
if (audio.value) audio.value.playbackRate = r
})
watch(currentIndex, () => {
duration.value = 0;
currentTime.value = 0
})
</script>
<!-- UnoCSS uses utility classes; add any extra small CSS only for knobs if needed -->
<style >
/* subtle progress fill using currentColor (inherits from UnoCSS utilities if you set them) */
.ant-slider .ant-slider-track{
}
.slider {
-webkit-appearance: none;
width: 100%;
height: 15px;
border-radius: 5px;
background-color: rgb(255 255 255 / 0.5) /* #FFFFFF */;
border:solid 1px black;
outline: none;
opacity: 1;
-webkit-transition: .2s;
transition: opacity .2s;
}
.slider:hover {
opacity: 1;
}
.slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 25px;
height: 25px;
border: solid 1px black;
border-radius: 50%;
background: #DC7DCB;
cursor: pointer;
}
.slider::-moz-range-thumb {
width: 25px;
height: 25px;
border-radius: 50%;
background: #C1FCE2;
cursor: pointer;
}
.slider::-moz-range-progress {
width: 25px;
height: 25px;
border-radius: 50%;
background: #C1FCE2;
}
</style>

View File

@ -4,23 +4,23 @@
<div class="font-700">برگشت</div>
<img src="@/assets/img/Arrow%20left-circle.svg">
</div>
<div class="flex flex-col gap-4 justify-center items-center">
<div class="flex flex-col gap-3 items-center bg-background/50 border-black border-1 rounded-7.7 p-5 ">
<div class="rounded-2xl justify-self-end border-black border-2">
<img class=" rounded-xl" src="@/assets/img/cover.png">
</div>
<div class="font-400 text-lg "> داستان صوتی فسقلی ها</div>
</div>
<div class=" w-full bg-background/50 border-black border-1 rounded-3xl p-5">
<div class="flex gap-7 justify-center w-full ">
<img src="@/assets/img/Iconnext1.svg" >
<img src="@/assets/img/iconplay1.svg" >
<img src="@/assets/img/Iconprevious1.svg" >
</div>
</div>
</div>
<!-- <div class="flex flex-col gap-4 justify-center items-center">-->
<!-- <div class="flex flex-col gap-3 items-center bg-background/50 border-black border-1 rounded-7.7 p-5 ">-->
<!-- <div class="rounded-2xl justify-self-end border-black border-2">-->
<!-- <img class=" rounded-xl" src="@/assets/img/cover.png">-->
<!-- </div>-->
<!-- <div class="font-400 text-lg "> داستان صوتی فسقلی ها</div>-->
<!-- </div>-->
<!-- <div class=" w-full bg-background/50 border-black border-1 rounded-3xl p-5">-->
<!-- <div class="flex gap-7 justify-center w-full ">-->
<!-- <img src="@/assets/img/Iconnext1.svg" >-->
<!-- <img src="@/assets/img/iconplay1.svg" >-->
<!-- <img src="@/assets/img/Iconprevious1.svg" >-->
<!-- </div>-->
<!-- </div>-->
<!-- </div>-->
<AudioPlayer />
</div>
@ -33,6 +33,7 @@
import router from "@/router";
import {onMounted} from "vue";
import {useRoute} from "vue-router";
import AudioPlayer from "@/views/audioStories/components/audioPlayer.vue";
const route=useRoute()
onMounted(()=>{
console.log(route)

View File

@ -0,0 +1,156 @@
<template>
<div class="flex flex-col relative items-center h-screen overflow-hidden !overflow-y-hidden ">
<div class="relative h-2/7 flex justify-center items-center ">
<div class="absolute top-8 left-6 z-2 ">
<img src="@/assets/img/plane.svg">
</div>
<div class="absolute bottom-10 right-6 z-2 ">
<img src="@/assets/img/star.svg">
</div>
<div class="absolute rounded-full top-1 left-6 bg-[#FABF3D] z-2 h-3.5 w-3.5"></div>
<div class="absolute rounded-full top-10 left-30 bg-[#28C792] z-2 h-3.5 w-3.5"></div>
<div class="absolute rounded-full top-10 right-4 bg-[#A07DDC] z-2 h-3.5 w-3.5"></div>
<div class="absolute rounded-full bottom-10 right-20 bg-[#DC7DCB] z-2 h-3.5 w-3.5"></div>
<div class="absolute rounded-full bottom-10 left-3 bg-[#DC7D7D] z-2 h-3.5 w-3.5"></div>
<div class="absolute rounded-full bottom-3 left-25 bg-[#28C792] z-2 h-3.5 w-3.5"></div>
<div class="absolute rounded-full top-3 right-25 bg-[#FABF3D] z-2 h-3.5 w-3.5"></div>
<div class="flex justify-center justify-items-center items-center w-3/4 gap-2 font-700 text-2xl text-[#28C792] text-center ">
<span>به</span>
<div class="">
<img class="" src="@/assets/img/logo.png">
</div>
<div class="shrink-0" >خوش آمدید</div>
</div>
</div>
<div class=" px-3 mt-8 w-full h-4/7">
<div class="relative flex justify-center w-full h-full">
<div class=" absolute w-full h-full flex items-center justify-center border-black rounded-17 border-1 bg-[#EAEAEA] z-12">
<div class="pt-5 w-full px-5">
<VerifyPhone v-if="isSMS" @goToLogin="isSMS=false" state="login" @completeOtp="otpLogin"></VerifyPhone>
<AForm v-else layout="vertical" :model="loginForm" @submit="submitFormLogin">
<AFormItem label="تلفن همراه"
name="phoneNumber"
:rules="inputRules('تلفن همراه')">
<box-temp bg-color-back="#C1FCE2" bg-color-front="#ffff">
<AInput class="rounded-full w-full" :bordered="false" v-model:value="loginForm.phoneNumber"></AInput>
</box-temp>
</AFormItem>
<AFormItem label="رمز عبور"
name="password"
:rules="inputRules('رمز عبور')">
<box-temp bg-color-back="#EEDDAF" bg-color-front="#ffff">
<AInputPassword class="rounded-full" :bordered="false"
v-model:value="loginForm.password"></AInputPassword>
</box-temp>
</AFormItem>
<AFormItem>
<div class="flex flex-col justify-center gap-3">
<box-temp bg-color-back="#EAEAEA" :bg-color-front="disabled?'#EAEAEA':'#EEDDAF'">
<AButton class="w-full h-9 text-lg font-700"
:loading="loading" bordered type="text" :disabled="disabled" html-type="submit" @click="">
ورود
</AButton>
</box-temp>
<box-temp bg-color-back="#EAEAEA" bg-color-front="#F0F3B1">
<AButton class="w-full h-9 text-lg font-700"
bordered type="text" html-type="submit" @click="isSMS=true">
ورود با رمز یکبار مصرف
</AButton>
</box-temp>
<div>
اگر حساب کاربری ندارید,
<span class="text-primary" @click="router.replace('register')"> ثبت نام کنید</span>
</div>
</div>
</AFormItem>
</AForm>
</div>
</div>
<div class="absolute border-black border-1 rounded-17 h-103% w-90% z-2 bg-[#EAEAEA] top--1.75 "></div>
<div class="absolute border-black border-1 rounded-17 h-106% w-80% z-1 bg-[#EAEAEA] top--3.5"></div>
</div>
</div>
<div class="absolute rounded-full bottom-20 left--20 bg-[#DC7D7D] z-1 h-60 w-60"></div>
<div class="absolute rounded-full bottom-70 left--20 bg-[#FDDB7C] z-1 h-60 w-60"></div>
<div class="absolute rounded-full bottom--10 right-13 bg-[#DC7D7D] z-0 h-40 w-40"></div>
<div class="absolute rounded-full bottom--20 left--20 bg-[#C4E4D1] z-0 h-60 w-60"></div>
<div class="absolute rounded-full bottom-23 right--20 bg-[#DC7D7D] z-0 h-60 w-60"></div>
<div class="absolute rounded-full bottom-50 right--20 bg-[#28C792] z-0 h-60 w-60"></div>
<div class="absolute rounded-full bottom-85 right--20 bg-[#EEDDAF] z-0 h-60 w-60"></div>
</div>
</template>
<script setup lang="ts">
import BoxTemp from "@/views/audioStories/components/boxTemp.vue";
import {computed, reactive, ref} from "vue";
import {inputRules} from "@/utils/inputRules.ts";
import router from "@/router";
import {login} from "@/api/auth.ts";
import {loading, setToken} from "@/utils";
import {useUserStore} from "@/store/user.ts";
import Icon from "@/components/Icon.vue";
import VerifyPhone from "@/views/login/components/verifyPhone.vue";
interface LoginForm {
phoneNumber: string
password: string
otp:string
}
const loginForm = reactive<LoginForm>({
phoneNumber: '',
password: '',
otp:''
})
const user = useUserStore()
const isSMS=ref<boolean>(false)
const disabled = computed(() => {
return !(loginForm.phoneNumber && loginForm.password);
});
async function submitFormLogin() {
try {
loading.value = true
const res = await login(loginForm.phoneNumber, loginForm.password)
setToken(res.session.access_token)
await user.getUserInfo()
await router.push({name: 'home'})
console.log('res==>', res)
} catch (e) {
console.log(e)
}
finally {
loading.value = false
}
}
async function otpLogin() {
try {
loading.value = true
await user.getUserInfo()
await router.push({name: 'home'})
isSMS.value=false
loading.value = false
} catch (e) {
console.log(e)
}
}
</script>
<style scoped lang="less">
</style>

View File

@ -0,0 +1,163 @@
<template>
<div class="flex flex-col relative items-center gap-1 h-screen overflow-hidden !overflow-y-hidden ">
<div class="relative h-2/7 flex justify-center items-center ">
<div class="absolute top-8 left-6 z-2 ">
<img src="@/assets/img/plane.svg">
</div>
<div class="absolute bottom-10 right-6 z-2 ">
<img src="@/assets/img/star.svg">
</div>
<div class="absolute rounded-full top-1 left-6 bg-[#FABF3D] z-2 h-3.5 w-3.5"></div>
<div class="absolute rounded-full top-10 left-30 bg-[#28C792] z-2 h-3.5 w-3.5"></div>
<div class="absolute rounded-full top-10 right-4 bg-[#A07DDC] z-2 h-3.5 w-3.5"></div>
<div class="absolute rounded-full bottom-10 right-20 bg-[#DC7DCB] z-2 h-3.5 w-3.5"></div>
<div class="absolute rounded-full bottom-10 left-3 bg-[#DC7D7D] z-2 h-3.5 w-3.5"></div>
<div class="absolute rounded-full bottom-3 left-25 bg-[#28C792] z-2 h-3.5 w-3.5"></div>
<div class="absolute rounded-full top-3 right-25 bg-[#FABF3D] z-2 h-3.5 w-3.5"></div>
<div class="flex justify-center justify-items-center items-center w-3/4 gap-2 font-700 text-2xl text-[#28C792] text-center ">
<span>به</span>
<div class="">
<img class="" src="@/assets/img/logo.png">
</div>
<div class="shrink-0" >خوش آمدید</div>
</div>
</div>
<div class=" px-3 mt-2 w-full h-5.3/8">
<div class="relative flex justify-center w-full h-full">
<div class=" absolute w-full h-full flex items-center justify-center border-black rounded-17 border-1 bg-[#EAEAEA] z-12">
<div class="py-20 w-full px-5">
<VerifyPhone v-if="step=='phone'" @completeOtp="handleOnComplete" ></VerifyPhone>
<div v-if="step=='name'">
<AForm layout="vertical" :model="loginForm" @submit="submitFormLogin" autocomplete="off">
<AFormItem label="نام"
name="firstName"
:rules="inputRules('نام')">
<box-temp bg-color-back="#C1FCE2" bg-color-front="#ffff">
<AInput autocomplete="off" class="rounded-full " :bordered="false" v-model:value="loginForm.firstName"></AInput>
</box-temp>
</AFormItem>
<AFormItem label="نام خانوادگی"
name="lastName"
:rules="inputRules('نام خانوادگی')">
<box-temp bg-color-back="#C1FCE2" bg-color-front="#ffff">
<AInput class="rounded-full" autocomplete="off" :bordered="false" v-model:value="loginForm.lastName"></AInput>
</box-temp>
</AFormItem>
<!-- <AFormItem label="تلفن همراه"-->
<!-- name="phoneNumber"-->
<!-- >-->
<!-- <box-temp bg-color-back="#C1FCE2" bg-color-front="#ffff">-->
<!-- <AInput class="rounded-full" autocomplete="off" :bordered="false" v-model:value="loginForm.phoneNumber" disabled></AInput>-->
<!-- </box-temp>-->
<!-- </AFormItem>-->
<AFormItem label="رمز عبور"
name="password"
:rules="inputRules('رمز عبور')">
<box-temp bg-color-back="#C1FCE2" bg-color-front="#ffff">
<AInputPassword class="rounded-full" autocomplete="off" :bordered="false" v-model:value="loginForm.password"></AInputPassword>
</box-temp>
</AFormItem>
<AFormItem>
<div class="flex flex-col gap-2">
<AButton class="rounded-full h-12 bg-#EEDDAF border-black text-lg font-700" :disabled="disabled"
html-type="submit" @click="">
ورود
</AButton>
<div>
اگر حساب کاربری دارید,
<span class="text-primary" @click="router.replace('login')"> وارد شوید</span>
</div>
</div>
</AFormItem>
</AForm>
</div>
</div>
</div>
<div class="absolute border-black border-1 rounded-17 h-103% w-90% z-3 bg-[#EAEAEA] top--1.75 "></div>
<div class="absolute border-black border-1 rounded-17 h-106% w-80% z-2 bg-[#EAEAEA] top--3.5"></div>
</div>
</div>
<div class="absolute rounded-full bottom-20 left--20 bg-[#DC7D7D] z-1 h-60 w-60"></div>
<div class="absolute rounded-full bottom-70 left--20 bg-[#FDDB7C] z-1 h-60 w-60"></div>
<div class="absolute rounded-full bottom--10 right-13 bg-[#DC7D7D] z-0 h-40 w-40"></div>
<div class="absolute rounded-full bottom--20 left--20 bg-[#C4E4D1] z-0 h-60 w-60"></div>
<div class="absolute rounded-full bottom-23 right--20 bg-[#DC7D7D] z-0 h-60 w-60"></div>
<div class="absolute rounded-full bottom-50 right--20 bg-[#28C792] z-0 h-60 w-60"></div>
<div class="absolute rounded-full bottom-85 right--20 bg-[#EEDDAF] z-0 h-60 w-60"></div>
</div>
</template>
<script setup lang="ts">
import BoxTemp from "@/views/audioStories/components/boxTemp.vue";
import {computed, onMounted, reactive, ref} from "vue";
import {inputRules} from "@/utils/inputRules.ts";
import router from "@/router";
import OtpInput from 'vue3-otp-input'
import {generateOtp, setUserInfo, verifyOtp} from "@/api/auth.ts";
import {message} from "ant-design-vue";
import {loading, removeToken, setToken} from "@/utils";
import {useUserStore} from "@/store/user.ts";
import VerifyPhone from "@/views/login/components/verifyPhone.vue";
import {isSignUpUser} from "@/utils/is.ts";
const user = useUserStore()
const step = ref<string>('phone')
async function handleOnComplete(otp,phone) {
loginForm.phoneNumber=phone
loginForm.otp=otp
// await supabase.auth.setSession(res.session)
// user.getUserInfo()
step.value = 'name'
console.log('OTP completed: ');
}
interface LoginForm {
firstName: string
lastName: string
phoneNumber: string
password: string
otp: string
}
const loginForm = reactive<LoginForm>({
firstName: '',
lastName: '',
phoneNumber: '',
password: '',
otp: ''
})
const disabled = computed(() => {
return !(loginForm.firstName && loginForm.lastName && loginForm.password);
});
async function submitFormLogin() {
if (step.value == 'name') {
const res = await setUserInfo('', loginForm.firstName, loginForm.lastName, loginForm.password)
console.log("res ===>", res)
// await supabase.auth.setSession(res.session)
router.push('home')
}
}
</script>
<style scoped lang="less">
</style>

View File

@ -0,0 +1,136 @@
<template>
<div v-if="step=='phone'">
<AForm layout="vertical" :model="loginForm" @submit="submitFormLogin">
<AFormItem label="تلفن همراه"
name="phoneNumber"
:rules="inputRules('تلفن همراه')">
<box-temp bg-color-back="#C1FCE2" bg-color-front="#ffff">
<AInput class="rounded-full" :bordered="false" v-model:value="loginForm.phoneNumber"></AInput>
</box-temp>
</AFormItem>
<AFormItem>
<div class="flex flex-col gap-2">
<box-temp bg-color-back="#EAEAEA" :bg-color-front="disabled?'#EAEAEA':'#EEDDAF'">
<AButton class="w-full h-9 text-lg font-700" :disabled="disabled" type="text" html-type="submit"
:loading="loading" @click="">
ارسال کد
</AButton>
</box-temp>
<div>
اگر حساب کاربری دارید,
<span class="text-primary" @click="()=>{router.replace('login');emits('goToLogin')}"> وارد شوید</span>
</div>
</div>
</AFormItem>
</AForm>
</div>
<div v-if="step=='otp'">
<div class="flex flex-col items-center gap-3">
<OtpInput
style="direction: ltr"
class=" justify-center"
ref="otpInput"
autocomplete="one-time-code"
input-classes="otp-input"
separator=""
:num-inputs="5"
:should-auto-focus="true"
:is-input-num="true"
@on-change=""
@on-complete="handleOnComplete"
/>
<span class="text-sm">کد ارسال شده را وارد نمایید.</span>
<div>
<span class="text-primary text-sm"
@click="()=>{router.replace('login');step='phone'}"> ویرایش شماره همراه </span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import {inputRules} from "@/utils/inputRules.ts";
import router from "@/router";
import {computed, reactive, ref} from "vue";
import {generateOtp, verifyOtp} from "@/api/auth.ts";
import {message} from "ant-design-vue";
import OtpInput from "vue3-otp-input";
import {loading, setToken} from "@/utils";
import BoxTemp from "@/views/audioStories/components/boxTemp.vue";
const step = ref<string>('phone')
interface Props {
state: string
}
const props = withDefaults(defineProps<Props>(), {
state: 'signup'
})
interface LoginForm {
phoneNumber: string
otp: string
}
const loginForm = reactive<LoginForm>({
phoneNumber: '',
otp: ''
})
const emits = defineEmits(['submitPhone', 'completeOtp', 'goToLogin'])
const disabled = computed(() => {
return !(loginForm.phoneNumber);
});
async function handleOnComplete(value: string) {
loading.value = true
loginForm.otp = value
const res = await verifyOtp(loginForm.phoneNumber, loginForm.otp)
console.log("res ===>", res)
setToken(res.session.access_token)
// await supabase.auth.setSession(res.session)
// user.getUserInfo()
emits('completeOtp', value, loginForm.phoneNumber)
loading.value = false
//console.log('OTP completed: ', value);
}
async function submitFormLogin() {
loading.value = true
if (step.value == 'phone') {
const res = await generateOtp(loginForm.phoneNumber, props.state)
if (res?.user?.name && props.state == 'signup') {
message.error("حساب کاربری با این شماره تلفن موجود می باشد.")
router.push({name: 'login'})
loading.value = false
return
}
step.value = 'otp'
} else if (step.value == 'otp') {
const res = await verifyOtp(loginForm.phoneNumber, loginForm.otp)
console.log("res ===>", res)
await setToken(res.session.access_token)
// await supabase.auth.setSession(res.session)
// user.getUserInfo()
step.value = 'name'
}
loading.value = false
}
</script>
<style scoped lang="less">
</style>

57
src/views/login/index.vue Normal file
View File

@ -0,0 +1,57 @@
<template>
<div class="flex flex-col h-screen gap-2 ">
<div class="h-3/5 relative">
<div class="absolute z-3 top-20 right-6">
<img src="@/assets/img/img-b2.png" width="160">
</div>
<div class="absolute z-3 bottom-20 left-6">
<img src="@/assets/img/img-b1.png" width="160">
</div>
<div class="absolute flex justify-center -right-4 w-110">
<img src="@/assets/img/line.svg" width="500">
</div>
<div class="absolute top-20 left-6 z-2 ">
<img src="@/assets/img/plane.svg">
</div>
<div class="absolute bottom-35 right-6 z-2 ">
<img src="@/assets/img/star.svg">
</div>
<div class="absolute rounded-full top-50 left-6 bg-[#FABF3D] z-2 h-3.5 w-3.5"></div>
<div class="absolute rounded-full top-30 left-30 bg-[#28C792] z-2 h-3.5 w-3.5"></div>
<div class="absolute rounded-full top-10 right-4 bg-[#A07DDC] z-2 h-3.5 w-3.5"></div>
<div class="absolute rounded-full bottom-50 right-20 bg-[#DC7DCB] z-2 h-3.5 w-3.5"></div>
<div class="absolute rounded-full bottom-40 left-3 bg-[#DC7D7D] z-2 h-3.5 w-3.5"></div>
<div class="absolute rounded-full bottom-20 right-25 bg-[#28C792] z-2 h-3.5 w-3.5"></div>
<div class="absolute rounded-full bottom-10 left-25 bg-[#FABF3D] z-2 h-3.5 w-3.5"></div>
</div>
<div class="flex justify-center justify-items-center items-center w-full gap-2 font-700 text-2xl text-[#28C792] text-center ">
<span>به</span>
<div class="">
<img class="" src="@/assets/img/logo.png">
</div>
<div class="shrink-0" >خوش آمدید</div>
</div>
<div class="flex flex-col gap-2">
<AButton class="rounded-full h-12 bg-#EEDDAF border-black text-lg font-700" @click="router.push({name:'login'})">
ورود
</AButton>
<AButton class="rounded-full h-12 bg-#EAEAEA border-black text-lg font-700" @click="router.push({name:'register'})">
ثبت نام
</AButton>
</div>
</div>
</template>
<script setup lang="ts">
import BoxTemp from "@/views/audioStories/components/boxTemp.vue";
import router from "@/router";
</script>
<style scoped lang="less">
</style>

View File

@ -1,80 +0,0 @@
<template>
<div class="flex flex-col items-center justify-center h-screen ">
<div class="relative w-full">
<div class="absolute -top-10 w-full flex justify-center z-2"><img src="@/assets/img/logo-1.svg"></div>
<box-temp bg-color-front="#ffff" bg-color-back="#28C792">
<div class="pt-10 w-full">
<AForm layout="vertical" :model="loginForm" @submit="submitFormLogin">
<AFormItem label="تلفن همراه"
name="phoneNumber"
:rules="inputRules('تلفن همراه')">
<AInput v-model:value="loginForm.phoneNumber"></AInput>
</AFormItem>
<AFormItem label="رمز عبور"
name="password"
:rules="inputRules('رمز عبور')">
<AInputPassword v-model:value="loginForm.password"></AInputPassword>
</AFormItem>
<AFormItem>
<div class="flex flex-col gap-3">
<AButton :disabled="disabled" type="primary" html-type="submit" @click="">
ورود
</AButton>
<div>
اگر حساب کاربری ندارید,
<span class="text-primary" @click="router.replace('register')"> ثبت نام کنید</span>
</div>
</div>
</AFormItem>
</AForm>
</div>
</box-temp>
</div>
</div>
</template>
<script setup lang="ts">
import BoxTemp from "@/views/audioStories/components/boxTemp.vue";
import {computed, reactive} from "vue";
import {inputRules} from "@/utils/inputRules.ts";
import router from "@/router";
import {login} from "@/api/auth.ts";
import {setToken} from "@/utils";
import {useUserStore} from "@/store/user.ts";
interface LoginForm {
phoneNumber: string
password: string
}
const loginForm = reactive<LoginForm>({
phoneNumber: '',
password: ''
})
const user = useUserStore()
const disabled = computed(() => {
return !(loginForm.phoneNumber && loginForm.password);
});
async function submitFormLogin() {
try {
const res = await login(loginForm.phoneNumber, loginForm.password)
setToken(res.session.access_token)
await user.getUserInfo()
await router.push({name:'home'})
console.log('res==>', res)
} catch (e) {
console.log(e)
}
}
</script>
<style scoped lang="less">
</style>

View File

@ -1,173 +0,0 @@
<template>
<div class="flex flex-col items-center justify-center h-screen ">
<div class="relative w-full">
<div class="absolute -top-10 w-full flex justify-center z-2"><img src="@/assets/img/logo-1.svg"></div>
<box-temp bg-color-front="#ffff" bg-color-back="#28C792">
<div class="pt-10 w-full">
<div v-if="step=='phone'">
<AForm layout="vertical" :model="loginForm" @submit="submitFormLogin">
<AFormItem label="تلفن همراه"
name="phoneNumber"
:rules="inputRules('تلفن همراه')">
<AInput v-model:value="loginForm.phoneNumber"></AInput>
</AFormItem>
<AFormItem>
<div class="flex flex-col gap-2">
<AButton type="primary" html-type="submit" @click="" >
ارسال کد
</AButton>
<div>
اگر حساب کاربری دارید,
<span class="text-primary" @click="router.replace('login')"> وارد شوید</span>
</div>
</div>
</AFormItem>
</AForm>
</div>
<div v-if="step=='otp'">
<div class="flex flex-col items-center gap-3">
<OtpInput
style="direction: ltr"
class=" justify-center"
ref="otpInput"
autocomplete="one-time-code"
input-classes="otp-input"
separator=""
:num-inputs="5"
:should-auto-focus="true"
:is-input-num="true"
@on-change=""
@on-complete="handleOnComplete"
/>
<span class="text-sm">کد ارسال شده را وارد نمایید.</span>
<div>
<span class="text-primary text-sm" @click="router.replace('login')"> ویرایش شماره همراه </span>
</div>
</div>
</div>
<div v-if="step=='name'">
<AForm layout="vertical" :model="loginForm" @submit="submitFormLogin">
<AFormItem label="نام"
name="firstName"
:rules="inputRules('نام')">
<AInput v-model:value="loginForm.firstName"></AInput>
</AFormItem>
<AFormItem label="نام خانوادگی"
name="lastName"
:rules="inputRules('نام خانوادگی')">
<AInput v-model:value="loginForm.lastName"></AInput>
</AFormItem>
<AFormItem label="تلفن همراه"
name="phoneNumber"
>
<AInput v-model:value="loginForm.phoneNumber" disabled></AInput>
</AFormItem>
<AFormItem label="رمز عبور"
name="password"
:rules="inputRules('رمز عبور')">
<AInputPassword v-model:value="loginForm.password"></AInputPassword>
</AFormItem>
<AFormItem>
<div class="flex flex-col gap-2">
<AButton :disabled="disabled" type="primary" html-type="submit" @click="">
ورود
</AButton>
<div>
اگر حساب کاربری دارید,
<span class="text-primary" @click="router.replace('login')"> وارد شوید</span>
</div>
</div>
</AFormItem>
</AForm>
</div>
</div>
</box-temp>
</div>
</div>
</template>
<script setup lang="ts">
import BoxTemp from "@/views/audioStories/components/boxTemp.vue";
import {computed, reactive, ref} from "vue";
import {inputRules} from "@/utils/inputRules.ts";
import router from "@/router";
import OtpInput from 'vue3-otp-input'
import {generateOtp, setUserInfo, verifyOtp} from "@/api/auth.ts";
import {message} from "ant-design-vue";
import {setToken} from "@/utils";
import {useUserStore} from "@/store/user.ts";
const user = useUserStore()
const step = ref<string>('phone')
async function handleOnComplete(value: string) {
loginForm.otp = value
const res = await verifyOtp(loginForm.phoneNumber,loginForm.otp)
console.log("res ===>", res)
setToken(res.session.access_token)
// await supabase.auth.setSession(res.session)
// user.getUserInfo()
step.value ='name'
//console.log('OTP completed: ', value);
}
interface LoginForm {
firstName: string
lastName: string
phoneNumber: string
password: string
otp:string
}
const loginForm = reactive<LoginForm>({
firstName: '',
lastName: '',
phoneNumber: '',
password: '',
otp:''
})
const disabled = computed(() => {
return !(loginForm.firstName && loginForm.lastName && loginForm.password);
});
async function submitFormLogin() {
if (step.value=='phone'){
const res = await generateOtp(loginForm.phoneNumber,'signup')
if (res?.user?.id){
message.error("حساب کاربری با این شماره تلفن موجود می باشد.")
router.push({name: 'login'})
return
}
step.value='otp'
}else if (step.value=='otp'){
const res = await verifyOtp(loginForm.phoneNumber,loginForm.otp)
console.log("res ===>", res)
setToken(res.session.access_token)
// await supabase.auth.setSession(res.session)
// user.getUserInfo()
step.value = 'name'
}else if (step.value=='name'){
const res = await setUserInfo('',loginForm.firstName,loginForm.lastName,loginForm.password)
console.log("res ===>", res)
// await supabase.auth.setSession(res.session)
// user.getUserInfo()
step.value = 'name'
}
}
</script>
<style scoped lang="less">
</style>

View File

@ -1,12 +1,15 @@
<template>
<div class="flex flex-col items-center gap-3 px-10">
<div ><img src="@/assets/img/Group%2031.svg"></div>
<div class="font-700 text-[#DC7D7D] text-10">چالش شماره {{ challengeId }}</div>
<div class="font-500 text-center ">سلام قهرمان حالت چطوره<br> یه چالش فوقالعاده برات داریم! توی این چالش قراره یه ماموریت جذاب را انجام بدی، کشیدن یه نقاشی خلاقانه ، و درست کردن چیزی با وسایل ساده یا انجام یه حرکت ورزشی پرانرژی .</div>
<div>
<basic-button class="bg-[#C1FCE2] !hover:bg-[#28C792] !hover:color-white text-#DC7D7D font-700 text-3xl rounded-lg h-17 w-full border-1 border-black " type="text" @click="" >بزن بریم</basic-button>
<div class=" gap-2 px-10">
<div class="flex flex-col items-center justify-center gap-2 ">
<div class="w-1/2" ><img src="@/assets/img/Group31.png"></div>
<div class=" font-700 text-[#DC7D7D] text-10">چالش شماره {{ challengeId }}</div>
<div class=" font-500 text-center ">سلام قهرمان حالت چطوره<br> یه چالش فوقالعاده برات داریم! توی این چالش قراره یه ماموریت جذاب را انجام بدی، کشیدن یه نقاشی خلاقانه ، و درست کردن چیزی با وسایل ساده یا انجام یه حرکت ورزشی پرانرژی .</div>
<div class=" ">
<basic-button class="bg-[#C1FCE2] !hover:bg-[#28C792] !hover:color-white text-#DC7D7D font-700 text-3xl rounded-lg h-17 w-full border-1 border-black " type="text" @click="" >بزن بریم</basic-button>
</div>
</div>
</div>
</template>

View File

@ -1,61 +1,66 @@
<template>
<div>
<div class=" flex justify-center">
<div class="absolute rounded-full z-1 top-75 shadow-lg sha ">
<img src="@/assets/img/elipse.svg">
<div >
<div class=" flex justify-center relative ">
<div class="absolute rounded-full z-1 top-72 shadow-lg ">
<img src="@/assets/img/elipse.svg">
</div>
<ASpin tip="در حال دریافت اطلاعات" class="bg-#C1FCE2 rounded-full !max-h-none rotate-90 " :spinning="loading">
<svg :width="size" :height="size" class="circle-svg">
<circle
cx="50%"
cy="50%"
:r="radius"
fill="none"
stroke=""
stroke-width="2"
/>
<path
v-for="(section, index) in circleSections"
:key="'path-' + index"
:d="section.path"
:fill="section.color"
/>
<line
v-for="(line, index) in divisionLines"
:key="'line-' + index"
x1="50%"
y1="50%"
:x2="line.x2"
:y2="line.y2"
stroke="#ffff"
stroke-width="7"
/>
<!-- متنهای همراستا با شعاع -->
<g class="-translate-13 ">
<text
v-for="(text, index) in sectionTexts"
:key="'text-' + index"
:x="text.x"
:y="text.y"
fill="#000"
:font-size="textSize"
font-weight="bold"
text-anchor="middle"
dominant-baseline="middle"
:transform="text.transform"
class="section-text "
>
{{ text.content }}
</text>
</g>
</svg>
</ASpin>
</div>
<svg :width="size" :height="size" class="circle-svg">
<circle
cx="50%"
cy="50%"
:r="radius"
fill="none"
stroke=""
stroke-width="2"
/>
<path
v-for="(section, index) in circleSections"
:key="'path-' + index"
:d="section.path"
:fill="section.color"
/>
<line
v-for="(line, index) in divisionLines"
:key="'line-' + index"
x1="50%"
y1="50%"
:x2="line.x2"
:y2="line.y2"
stroke="#ffff"
stroke-width="7"
/>
<!-- متنهای همراستا با شعاع -->
<g class="-translate-13 ">
<text
v-for="(text, index) in sectionTexts"
:key="'text-' + index"
:x="text.x"
:y="text.y"
fill="#000"
:font-size="textSize"
font-weight="bold"
text-anchor="middle"
dominant-baseline="middle"
:transform="text.transform"
class="section-text "
>
{{ text.content }}
</text>
</g>
</svg>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import {loading} from "@/utils";
interface Prop {
challengesItems: []
}
@ -68,9 +73,9 @@ const parts = computed(()=>{
return props.challengesItems.length
else return 10
})
const size = ref(800)
const size = ref("800")
const radius = ref(350)
const textSize = ref(25)
const textSize = ref(30)
const circleSections = computed(() => {
const sections = []
const anglePerSection = (2 * Math.PI) / parts.value
@ -79,7 +84,7 @@ const circleSections = computed(() => {
for (let i = 0; i < parts.value; i++) {
const startAngle = i * anglePerSection
const endAngle = (i + 1) * anglePerSection
console.log(startAngle ,'startAngle')
const startX = 350 + radius.value * Math.cos(startAngle)
const startY = 350 + radius.value * Math.sin(startAngle)
const endX = 350 + radius.value * Math.cos(endAngle)
@ -93,12 +98,12 @@ const circleSections = computed(() => {
A ${radius.value} ${radius.value} 0 ${largeArcFlag} 1 ${endX} ${endY}
Z
`
sections.push({
path,
color:(i % 2 === 0)?colors[0]:colors[1],
index: i
})
}
return sections
@ -151,7 +156,7 @@ const sectionTexts = computed(() => {
texts.push({
x: x, // مقدار پیکسل مستقیم
y: y, // مقدار پیکسل مستقیم
content: props.challengesItems[i]?.name || `بخش ${i + 1}`,
content: props.challengesItems[i]?.title || `بخش ${i + 1}`,
transform,
index: i
})
@ -160,16 +165,13 @@ const sectionTexts = computed(() => {
return texts
})
const sectionTitles = computed(() => {
const titles = [
'چالش 6', 'چالش 5', 'چالش 4', 'چالش 3', 'چالش 2', 'چالش 1',
'چالش 7', 'چالش 8', 'چالش 9', 'چالش 10', 'چالش 11', 'چالش 12' ]
return titles.slice(0, parts.value)
})
</script>
<style scoped>
.ant-spin-nested-loading .ant-spin-blur::after {
opacity: 0 !important;
}
.circle-container {
display: flex;
justify-content: center;

View File

@ -1,7 +1,7 @@
<template>
<div class=" overflow-hidden !overflow-y-hidden ">
<transition name="fade">
<div v-if="challengeId==0" class="flex mt-10 flex-col items-center gap-8">
<div v-if="challengeId==0" class="flex mt-10 flex-col items-center gap-8">
<basic-button
class="bg-[#FABF3D] !hover:bg-[#28C792] !hover:color-white font-700 text-3xl shadow-lg shadow-[#FFE48F] rounded-full h-17 w-full border-1 border-black "
type="text" @click="clickWheelButton">بزن بریم
@ -17,6 +17,7 @@
</div>
</div>
</div>
</transition>
<transition name="fade">
<challenge-des v-if="challengeId!=0" :challenge-id="challengeId"></challenge-des>
@ -32,12 +33,14 @@
import LuckyWheel from "@/views/luckyWheel/components/luckyWheel.vue";
import BasicButton from "@/components/BasicPacks/BasicButton.vue";
import {ref} from "vue";
import {onMounted, ref} from "vue";
import {route} from "vant/es/composables/use-route";
import {routes} from "vue-router/vue-router-auto-routes";
import router from "../../../router";
import {useRoute} from "vue-router";
import ChallengeDes from "@/views/luckyWheel/components/challengeDes.vue";
import {getChallenges} from "@/api/challenges.ts";
import {loading} from "@/utils";
const value = ref(-90);
let countClicked = 0;
@ -76,16 +79,18 @@ function getPosition(position) {
console.log(anglePart)
let middleAngle = anglePart / 2
const challenge = challenges.value.find((_, i) => {
console.log(middleAngle + ((i + 1) * anglePart), i ,position)
return position <= middleAngle + ((i + 1) * anglePart)
console.log(middleAngle + ((i + 1) * anglePart), i, position)
return (position-middleAngle) <= middleAngle + ((i + 1) * anglePart)
}
);
console.log(challenge)
console.log(challenge)
console.log(challenges.value)
if (challenge) {
challengeId.value = challenge.id;
console.log(challengeId.value,'11111');
console.log(challengeId.value, '11111');
}
value.value = -90
// } else if (position <= 150) {
//
// } else if (position <= 210) {
@ -118,18 +123,27 @@ function clickWheelButton() {
if (!clicked) {
playSound()
console.log(value.value,'1115');
console.log(value.value, '1115');
let random = Math.floor((Math.random() * 360) + 720);
value.value += random;
console.log(random, random % 360);
console.log(value.value % 360);
setTimeout(() => {
getPosition(((value.value+180) % 360) );
}, 7000);
getPosition(((value.value + 180) % 360));
// getPosition(247);
}, 6500);
}
clicked = true;
}
onMounted(async () => {
loading.value=true
const res = await getChallenges()
challenges.value = res
loading.value=false
})
</script>
<style scoped lang="less">

249
supabase.ts Normal file
View File

@ -0,0 +1,249 @@
// import { createClient } from '@supabase/supabase-js';
import type {AdminUserAttributes, Session, UserResponse} from '@supabase/supabase-js';
// import {type AuthTokenResponsePassword, createClient} from '@supabase/supabase-js';
import type {Token, User, UserLoginState} from "@/models/login";
import {message} from "ant-design-vue";
// import type {User} from "@/model/User";
const SUPABASE_URL = 'https://supa.machartaa.ir';
const SUPABASE_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlhdCI6MTcxMDAwMDAwMCwiZXhwIjoxOTkwMDAwMDAwfQ.x_hfpoEIOp4E9IcUnSaseq8ONrfxuhNGKVq86Em6U6o';
import { createClient, type SupabaseClient } from '@supabase/supabase-js'
// --- Lazy runtime init (بدون تغییر API) ---
let _client: SupabaseClient | null = null
export const supabase = createClient(SUPABASE_URL,SUPABASE_KEY);
// function ensureClient(): SupabaseClient {
// if (_client) return _client
// const cfg = useRuntimeConfig().public as {
// SUPABASE_URL: string
// SUPABASE_KEY: string
// }
// if (!cfg?.SUPABASE_URL || !cfg?.SUPABASE_KEY) {
// console.warn('[supabase] Missing SUPABASE_URL/KEY in runtimeConfig.public')
// }
// _client = createClient(cfg.SUPABASE_URL, cfg.SUPABASE_KEY)
// return _client
// }
// ✅ همان export قبلی، اما به‌صورت Proxy که هنگام اولین استفاده، کلاینت را با runtimeConfig می‌سازد
// export const supabase = new Proxy({} as SupabaseClient, {
// get(_t, prop, receiver) {
// const c = ensureClient()
// // @ts-ignore
// return Reflect.get(c, prop, receiver)
// },
// apply(_t, thisArg, argArray) {
// // احتیاط برای مواقعی که به‌صورت تابعی صدا زده شود
// // @ts-ignore
// return (ensureClient() as any)?.apply?.(thisArg, argArray)
// },
// })
// ✅ بدون نیاز به ثابت build-time؛ هر بار از runtimeConfig می‌سازد
export const getSupabaseStorageUrl = (path: string) => {
const cfg = useRuntimeConfig().public as { SUPABASE_BASE_IMAGE_URL: string }
return `${cfg.SUPABASE_BASE_IMAGE_URL}/${path}`
}
export const uploadFile = async (file: FileList, busket: string = 'productimg') => {
const uniqueFileName = `${Date.now()}-${file.name}`;
return await supabase
.storage
.from(busket)
.upload(uniqueFileName, file, {
cacheControl: '3600',
upsert: false
})
}
interface Query {
columns?: string,
field?: string | string[],
value?: string | string[] | number | number[]
data?: any
}
export const supabaseService = {
// Generic fetch method for CRUD operations
async fetch<T>(
table: string,
method: 'select' | 'insert' | 'update' | 'delete' | 'select_group',
query: Query = {},
customErrorMessage?: string,
order?:{column:string,ascending:boolean}
): Promise<T | null> {
try {
let result;
switch (method) {
case 'select': {
let queryBuilder = supabase.from(table);
let selectQuery = queryBuilder.select(query.columns || '*');
// Apply multiple .eq() conditions dynamically
if (query.field && query.value) {
if (Array.isArray(query.field) && Array.isArray(query.value)) {
query.field.forEach((f, i) => {
selectQuery = selectQuery.eq(f, query.value[i]);
});
} else {
// console.log('salam', query.field, query.value)
selectQuery = selectQuery.eq(query.field, query.value);
}
}
if (order?.column){
selectQuery = selectQuery.order(order.column,{ascending:order.ascending})
}
// console.log('salam', selectQuery)
result = await selectQuery;
break;
}
case 'select_group':
result = await supabase.from(table).select(query.columns|| '*').in(query.field ?? '', query.value)
break;
case 'insert':
result = await supabase.from(table).insert(query.data).select('*')
break;
case 'update':
result = await supabase.from(table).update(query.data).eq(query.field ?? '', query.value).select();
break;
case 'delete': {
let deleteQuery = supabase.from(table).delete();
if (Array.isArray(query.field) && Array.isArray(query.value)) {
query.field.forEach((f, i) => {
deleteQuery = deleteQuery.eq(f, query.value[i]);
});
} else {
deleteQuery = deleteQuery.eq(query.field ?? '', query.value);
}
result = await deleteQuery;
break;
}
default:
throw new Error('Unsupported method');
}
if (result.error) {
// errorMessage(result.error.message)
throw result.error
} else {
return result.data
}
} catch (err) {
console.error(customErrorMessage || 'Unexpected Supabase Error:', err);
errorMessage(err.message as string)
throw err;
}
},
// async updateUserDetails(
// email,
// phone,
// password
// ): Promise<User> {
// try {
// const res = await supabase.auth.updateUser({
// email,
// phone,
// password
// });
//
// /* if (error) {
// console.error('Error updating user:', error);
// errorMessage(error.message as string)
// throw error;
// }*/
// if (res.error) {
// throw res.error
// } else {
// return res.data.user
// }
// } catch (err) {
// console.error('Unexpected error while updating user:', err);
// errorMessage(err.message as string)
// throw err;
// }
// },
// async createUser(params: User): Promise<UserResponse> {
// try {
// return (await supabase.auth.admin.createUser(params))
// } catch (err) {
// console.error('Unexpected Error:', err);
// errorMessage(err as string)
// throw err
// }
// },
// // Authentication methods
// async login(params: UserLoginState): Promise<Token> {
// try {
// const res = await supabase.auth.signInWithPassword(params)
// if (res.error) {
// throw res.error
// } else {
// return res.data.user
// }
// } catch (err) {
// console.error('Unexpected Login Error:', err);
// errorMessage(err.message as string)
// throw err
// }
// },
//
// async deleteUser(user_id: string): Promise<UserResponse> {
// try {
// return (await supabase.auth.admin.deleteUser(<string>user_id))
// } catch (err) {
// console.error('Unexpected Login Error:', err);
// errorMessage(err as string)
// throw err
// }
// },
// async signup(email: string, password: string): Promise<{ user: any; session: Session | null } | null> {
// try {
// const {user, session, error} = await supabase.auth.signUp({
// email,
// password,
// });
//
// if (error) {
// console.error('Signup Error:', error.message);
// errorMessage(error.message)
// return null;
// }
//
// return {user, session};
// } catch (err) {
// console.error('Unexpected Signup Error:', err);
// return null;
// }
// },
//
// async logout(): Promise<boolean> {
// try {
// const {error} = await supabase.auth.signOut();
//
// if (error) {
// console.error('Logout Error:', error.message);
// return false;
// }
//
// return true;
// } catch (err) {
// console.error('Unexpected Logout Error:', err);
// return false;
// }
// },
//
// getUser(): any | null {
// return supabase.auth.getUser();
// },
//
// onAuthStateChange(callback: (event: string, session: Session | null) => void) {
// supabase.auth.onAuthStateChange(callback);
// },
};
function errorMessage(msg: string) {
message.error(msg).then(r => '');
}

View File

@ -373,23 +373,92 @@
core-js "^3.15.1"
nanopop "^2.1.0"
"@supabase/auth-js@2.71.1":
version "2.71.1"
resolved "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.71.1.tgz"
integrity sha512-mMIQHBRc+SKpZFRB2qtupuzulaUhFYupNyxqDj5Jp/LyPvcWvjaJzZzObv6URtL/O6lPxkanASnotGtNpS3H2Q==
dependencies:
"@supabase/node-fetch" "^2.6.14"
"@supabase/functions-js@2.4.6":
version "2.4.6"
resolved "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.4.6.tgz"
integrity sha512-bhjZ7rmxAibjgmzTmQBxJU6ZIBCCJTc3Uwgvdi4FewueUTAGO5hxZT1Sj6tiD+0dSXf9XI87BDdJrg12z8Uaew==
dependencies:
"@supabase/node-fetch" "^2.6.14"
"@supabase/node-fetch@^2.6.13", "@supabase/node-fetch@^2.6.14", "@supabase/node-fetch@2.6.15":
version "2.6.15"
resolved "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz"
integrity sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==
dependencies:
whatwg-url "^5.0.0"
"@supabase/postgrest-js@1.21.4":
version "1.21.4"
resolved "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.21.4.tgz"
integrity sha512-TxZCIjxk6/dP9abAi89VQbWWMBbybpGWyvmIzTd79OeravM13OjR/YEYeyUOPcM1C3QyvXkvPZhUfItvmhY1IQ==
dependencies:
"@supabase/node-fetch" "^2.6.14"
"@supabase/realtime-js@2.15.5":
version "2.15.5"
resolved "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.15.5.tgz"
integrity sha512-/Rs5Vqu9jejRD8ZeuaWXebdkH+J7V6VySbCZ/zQM93Ta5y3mAmocjioa/nzlB6qvFmyylUgKVS1KpE212t30OA==
dependencies:
"@supabase/node-fetch" "^2.6.13"
"@types/phoenix" "^1.6.6"
"@types/ws" "^8.18.1"
ws "^8.18.2"
"@supabase/storage-js@2.12.1":
version "2.12.1"
resolved "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.12.1.tgz"
integrity sha512-QWg3HV6Db2J81VQx0PqLq0JDBn4Q8B1FYn1kYcbla8+d5WDmTdwwMr+EJAxNOSs9W4mhKMv+EYCpCrTFlTj4VQ==
dependencies:
"@supabase/node-fetch" "^2.6.14"
"@supabase/supabase-js@^2.57.4":
version "2.57.4"
resolved "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.57.4.tgz"
integrity sha512-LcbTzFhHYdwfQ7TRPfol0z04rLEyHabpGYANME6wkQ/kLtKNmI+Vy+WEM8HxeOZAtByUFxoUTTLwhXmrh+CcVw==
dependencies:
"@supabase/auth-js" "2.71.1"
"@supabase/functions-js" "2.4.6"
"@supabase/node-fetch" "2.6.15"
"@supabase/postgrest-js" "1.21.4"
"@supabase/realtime-js" "2.15.5"
"@supabase/storage-js" "2.12.1"
"@types/estree@1.0.8":
version "1.0.8"
resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz"
integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==
"@types/node@^20.19.0 || >=22.12.0", "@types/node@^24.3.0":
"@types/node@*", "@types/node@^20.19.0 || >=22.12.0", "@types/node@^24.3.0":
version "24.3.0"
resolved "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz"
integrity sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==
dependencies:
undici-types "~7.10.0"
"@types/phoenix@^1.6.6":
version "1.6.6"
resolved "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz"
integrity sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==
"@types/web-bluetooth@^0.0.21":
version "0.0.21"
resolved "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz"
integrity sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==
"@types/ws@^8.18.1":
version "8.18.1"
resolved "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz"
integrity sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==
dependencies:
"@types/node" "*"
"@unocss/astro@66.4.2":
version "66.4.2"
resolved "https://registry.npmjs.org/@unocss/astro/-/astro-66.4.2.tgz"
@ -1832,6 +1901,11 @@ totalist@^3.0.0:
resolved "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz"
integrity sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==
tr46@~0.0.3:
version "0.0.3"
resolved "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz"
integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
tslib@^2.3.0:
version "2.8.1"
resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz"
@ -1988,6 +2062,24 @@ warning@^4.0.0:
dependencies:
loose-envify "^1.0.0"
webidl-conversions@^3.0.0:
version "3.0.1"
resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz"
integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==
whatwg-url@^5.0.0:
version "5.0.0"
resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz"
integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==
dependencies:
tr46 "~0.0.3"
webidl-conversions "^3.0.0"
ws@^8.18.2:
version "8.18.3"
resolved "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz"
integrity sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==
yallist@^3.0.2:
version "3.1.1"
resolved "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz"