wip
@@ -2,5 +2,5 @@
|
||||
"$schema": "https://json.schemastore.org/prettierrc",
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"printWidth": 100
|
||||
"printWidth": 120
|
||||
}
|
||||
|
||||
@@ -8,9 +8,12 @@
|
||||
"name": "ulitka-app",
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.14.1",
|
||||
"@fontsource/roboto": "^5.3.0",
|
||||
"@vue/apollo-composable": "^4.2.2",
|
||||
"@vueuse/core": "^14.4.0",
|
||||
"better-auth": "^1.7.1",
|
||||
"graphql": "^17.0.2",
|
||||
"pinia": "^4.0.3",
|
||||
"vue": "^3.5.41",
|
||||
"vue-router": "^5.2.0",
|
||||
@@ -32,7 +35,7 @@
|
||||
"oxlint": "~1.80.0",
|
||||
"prettier": "3.9.6",
|
||||
"sass-embedded": "^1.103.1",
|
||||
"typescript": "~7.0.2",
|
||||
"typescript": "^6.0.3",
|
||||
"unplugin-fonts": "^2.0.0",
|
||||
"vite": "^8.2.2",
|
||||
"vite-plugin-vue-devtools": "^8.2.1",
|
||||
@@ -43,6 +46,48 @@
|
||||
"node": "^22.18.0 || >=24.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@apollo/client": {
|
||||
"version": "3.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.14.1.tgz",
|
||||
"integrity": "sha512-SgGX6E23JsZhUdG2anxiyHvEvvN6CUaI4ZfMsndZFeuHPXL3H0IsaiNAhLITSISbeyeYd+CBd9oERXQDdjXWZw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@graphql-typed-document-node/core": "^3.1.1",
|
||||
"@wry/caches": "^1.0.0",
|
||||
"@wry/equality": "^0.5.6",
|
||||
"@wry/trie": "^0.5.0",
|
||||
"graphql-tag": "^2.12.6",
|
||||
"hoist-non-react-statics": "^3.3.2",
|
||||
"optimism": "^0.18.0",
|
||||
"prop-types": "^15.7.2",
|
||||
"rehackt": "^0.1.0",
|
||||
"symbol-observable": "^4.0.0",
|
||||
"ts-invariant": "^0.10.3",
|
||||
"tslib": "^2.3.0",
|
||||
"zen-observable-ts": "^1.2.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"graphql": "^15.0.0 || ^16.0.0",
|
||||
"graphql-ws": "^5.5.5 || ^6.0.3",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc",
|
||||
"subscriptions-transport-ws": "^0.9.0 || ^0.11.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"graphql-ws": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"subscriptions-transport-ws": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
|
||||
@@ -784,6 +829,15 @@
|
||||
"url": "https://github.com/sponsors/ayuhito"
|
||||
}
|
||||
},
|
||||
"node_modules/@graphql-typed-document-node/core": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz",
|
||||
"integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanfs/core": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
|
||||
@@ -2236,346 +2290,6 @@
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-aix-ppc64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz",
|
||||
"integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-darwin-arm64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz",
|
||||
"integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-darwin-x64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz",
|
||||
"integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-freebsd-arm64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz",
|
||||
"integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-freebsd-x64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz",
|
||||
"integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-linux-arm": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz",
|
||||
"integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-linux-arm64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz",
|
||||
"integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-linux-loong64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz",
|
||||
"integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-linux-mips64el": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz",
|
||||
"integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-linux-ppc64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz",
|
||||
"integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-linux-riscv64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz",
|
||||
"integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-linux-s390x": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz",
|
||||
"integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-linux-x64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz",
|
||||
"integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-netbsd-arm64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz",
|
||||
"integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-netbsd-x64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz",
|
||||
"integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-openbsd-arm64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz",
|
||||
"integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-openbsd-x64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz",
|
||||
"integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-sunos-x64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz",
|
||||
"integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-win32-arm64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz",
|
||||
"integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-win32-x64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz",
|
||||
"integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitejs/plugin-vue": {
|
||||
"version": "6.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.8.tgz",
|
||||
@@ -2649,6 +2363,28 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/apollo-composable": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@vue/apollo-composable/-/apollo-composable-4.2.2.tgz",
|
||||
"integrity": "sha512-5j+Jl07Gemz5vmuS8u/FfWtYgr04Rh0rjQ5HBv6DZDP7d+pvQfsCIRgX5adJoZJcznJLsQ0JupO/mZmRCBWGaQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"throttle-debounce": "^5.0.0",
|
||||
"ts-essentials": "^9.4.0",
|
||||
"vue-demi": "^0.14.6"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@apollo/client": "^3.4.13",
|
||||
"@vue/composition-api": "^1.0.0",
|
||||
"graphql": ">=15",
|
||||
"vue": "^2.6.0 || ^3.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vue/composition-api": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/babel-helper-vue-transform-on": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.5.0.tgz",
|
||||
@@ -2956,6 +2692,54 @@
|
||||
"vue": "^3.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@wry/caches": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@wry/caches/-/caches-1.0.1.tgz",
|
||||
"integrity": "sha512-bXuaUNLVVkD20wcGBWRyo7j9N3TxePEWFZj2Y+r9OoUzfqmavM84+mFykRicNsBqatba5JLay1t48wxaXaWnlA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@wry/context": {
|
||||
"version": "0.7.4",
|
||||
"resolved": "https://registry.npmjs.org/@wry/context/-/context-0.7.4.tgz",
|
||||
"integrity": "sha512-jmT7Sb4ZQWI5iyu3lobQxICu2nC/vbUhP0vIdd6tHC9PTfenmRmuIFqktc6GH9cgi+ZHnsLWPvfSvc4DrYmKiQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@wry/equality": {
|
||||
"version": "0.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@wry/equality/-/equality-0.5.7.tgz",
|
||||
"integrity": "sha512-BRFORjsTuQv5gxcXsuDXx6oGRhuVsEGwZy6LOzRRfgu+eSfxbhUQ9L9YtSEIuIjY/o7g3iWFjrc5eSY1GXP2Dw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@wry/trie": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@wry/trie/-/trie-0.5.0.tgz",
|
||||
"integrity": "sha512-FNoYzHawTMk/6KMQoEG5O4PuioX19UbwdQKF44yw0nLfOypfQdjtfZzo/UIJWAJ23sNIFbD1Ug9lbaDGMwbqQA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.18.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz",
|
||||
@@ -4046,6 +3830,30 @@
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/graphql": {
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmjs.org/graphql/-/graphql-17.0.2.tgz",
|
||||
"integrity": "sha512-FRWbddMxfkjiB7z+aQDWIR+E34xo9I8c9mtK2RPv8PmMzKRvrdsreHL/Ui/TmwHJfhHChEtsFPyMHKI+xuarQQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^22.0.0 || ^24.0.0 || ^25.0.0 || >=26.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/graphql-tag": {
|
||||
"version": "2.12.7",
|
||||
"resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.12.7.tgz",
|
||||
"integrity": "sha512-xnE/NFzy+0eIesvAsREJZ284zTl/wYuBAvpsFSDhRGRdRHdnE90M21Q3xAWyYInb0J756c6x0pIQ62+vtvOs1Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"graphql": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
@@ -4056,6 +3864,15 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/hoist-non-react-statics": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
|
||||
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"react-is": "^16.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/hookable": {
|
||||
"version": "5.5.3",
|
||||
"resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
|
||||
@@ -4216,7 +4033,6 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jsesc": {
|
||||
@@ -4627,6 +4443,18 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"loose-envify": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||
@@ -4977,6 +4805,15 @@
|
||||
"url": "https://github.com/fb55/nth-check?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/obug": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz",
|
||||
@@ -5019,6 +4856,18 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/optimism": {
|
||||
"version": "0.18.1",
|
||||
"resolved": "https://registry.npmjs.org/optimism/-/optimism-0.18.1.tgz",
|
||||
"integrity": "sha512-mLXNwWPa9dgFyDqkNi54sjDyNJ9/fTI6WGBLgnXku1vdKY/jovHfZT5r+aiVeFFLOz+foPNOm5YJ4mqgld2GBQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@wry/caches": "^1.0.0",
|
||||
"@wry/context": "^0.7.0",
|
||||
"@wry/trie": "^0.5.0",
|
||||
"tslib": "^2.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/optionator": {
|
||||
"version": "0.9.4",
|
||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||
@@ -5306,6 +5155,17 @@
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
@@ -5353,6 +5213,12 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/read-package-json-fast": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-6.0.0.tgz",
|
||||
@@ -5390,6 +5256,24 @@
|
||||
"regexp-tree": "bin/regexp-tree"
|
||||
}
|
||||
},
|
||||
"node_modules/rehackt": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/rehackt/-/rehackt-0.1.0.tgz",
|
||||
"integrity": "sha512-7kRDOuLHB87D/JESKxQoRwv4DzbIdwkAGQ7p6QKGdVlY1IZheUnVhlk/4UZlNUVxdAXpyxikE3URsG067ybVzw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/reusify": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
|
||||
@@ -5967,6 +5851,15 @@
|
||||
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/symbol-observable": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz",
|
||||
"integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/sync-child-process": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/sync-child-process/-/sync-child-process-1.0.2.tgz",
|
||||
@@ -5990,6 +5883,15 @@
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/throttle-debounce": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz",
|
||||
"integrity": "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.22"
|
||||
}
|
||||
},
|
||||
"node_modules/tiny-inflate": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
|
||||
@@ -6061,11 +5963,36 @@
|
||||
"typescript": ">=4.8.4"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-essentials": {
|
||||
"version": "9.4.2",
|
||||
"resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-9.4.2.tgz",
|
||||
"integrity": "sha512-mB/cDhOvD7pg3YCLk2rOtejHjjdSi9in/IBYE13S+8WA5FBSraYf4V/ws55uvs0IvQ/l0wBOlXy5yBNZ9Bl8ZQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/ts-invariant": {
|
||||
"version": "0.10.3",
|
||||
"resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.10.3.tgz",
|
||||
"integrity": "sha512-uivwYcQaxAucv1CzRp2n/QdYPo4ILf9VXgH19zEIjFx2EJufV16P0JtJVpYHy89DItG6Kwj2oIUjrcK5au+4tQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"dev": true,
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
@@ -6089,38 +6016,17 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz",
|
||||
"integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==",
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
|
||||
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc"
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@typescript/typescript-aix-ppc64": "7.0.2",
|
||||
"@typescript/typescript-darwin-arm64": "7.0.2",
|
||||
"@typescript/typescript-darwin-x64": "7.0.2",
|
||||
"@typescript/typescript-freebsd-arm64": "7.0.2",
|
||||
"@typescript/typescript-freebsd-x64": "7.0.2",
|
||||
"@typescript/typescript-linux-arm": "7.0.2",
|
||||
"@typescript/typescript-linux-arm64": "7.0.2",
|
||||
"@typescript/typescript-linux-loong64": "7.0.2",
|
||||
"@typescript/typescript-linux-mips64el": "7.0.2",
|
||||
"@typescript/typescript-linux-ppc64": "7.0.2",
|
||||
"@typescript/typescript-linux-riscv64": "7.0.2",
|
||||
"@typescript/typescript-linux-s390x": "7.0.2",
|
||||
"@typescript/typescript-linux-x64": "7.0.2",
|
||||
"@typescript/typescript-netbsd-arm64": "7.0.2",
|
||||
"@typescript/typescript-netbsd-x64": "7.0.2",
|
||||
"@typescript/typescript-openbsd-arm64": "7.0.2",
|
||||
"@typescript/typescript-openbsd-x64": "7.0.2",
|
||||
"@typescript/typescript-sunos-x64": "7.0.2",
|
||||
"@typescript/typescript-win32-arm64": "7.0.2",
|
||||
"@typescript/typescript-win32-x64": "7.0.2"
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-eslint": {
|
||||
@@ -6566,6 +6472,32 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vue-demi": {
|
||||
"version": "0.14.10",
|
||||
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
|
||||
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"vue-demi-fix": "bin/vue-demi-fix.js",
|
||||
"vue-demi-switch": "bin/vue-demi-switch.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vue/composition-api": "^1.0.0-rc.1",
|
||||
"vue": "^3.0.0-0 || ^2.6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vue/composition-api": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vue-eslint-parser": {
|
||||
"version": "10.4.1",
|
||||
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-10.4.1.tgz",
|
||||
@@ -6866,6 +6798,21 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/zen-observable": {
|
||||
"version": "0.8.15",
|
||||
"resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz",
|
||||
"integrity": "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/zen-observable-ts": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-1.2.5.tgz",
|
||||
"integrity": "sha512-QZWQekv6iB72Naeake9hS1KxHlotfRpe+WGNbNx5/ta+R3DNjVO2bswf63gXlWDcs+EMd7XY8HfVQyP1X6T4Zg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"zen-observable": "0.8.15"
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
|
||||
|
||||
@@ -15,9 +15,12 @@
|
||||
"format": "prettier --write --experimental-cli src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.14.1",
|
||||
"@fontsource/roboto": "^5.3.0",
|
||||
"@vue/apollo-composable": "^4.2.2",
|
||||
"@vueuse/core": "^14.4.0",
|
||||
"better-auth": "^1.7.1",
|
||||
"graphql": "^17.0.2",
|
||||
"pinia": "^4.0.3",
|
||||
"vue": "^3.5.41",
|
||||
"vue-router": "^5.2.0",
|
||||
@@ -39,7 +42,7 @@
|
||||
"oxlint": "~1.80.0",
|
||||
"prettier": "3.9.6",
|
||||
"sass-embedded": "^1.103.1",
|
||||
"typescript": "~7.0.2",
|
||||
"typescript": "^6.0.3",
|
||||
"unplugin-fonts": "^2.0.0",
|
||||
"vite": "^8.2.2",
|
||||
"vite-plugin-vue-devtools": "^8.2.1",
|
||||
|
||||
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
@@ -1,33 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import { useEventSource } from '@vueuse/core'
|
||||
import { computed } from 'vue'
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useStatusStore } from '@/stores/status.ts'
|
||||
import { useIssueStore } from '@/stores/issue.ts'
|
||||
|
||||
//issue.created
|
||||
const route = useRoute()
|
||||
// const { status, data, error, close } = useEventSource(
|
||||
// 'https://event-source-url',
|
||||
// ['notice', 'update'],
|
||||
// { autoReconnect: {
|
||||
// retries: 3,
|
||||
// delay: 1000,
|
||||
// onFailed() {
|
||||
// alert('Failed to connect EventSource after 3 retries')
|
||||
// },
|
||||
// }, autoConnect: false },
|
||||
// )
|
||||
// console.log(data, status, error, close)
|
||||
const { status, data, error, close } = useEventSource(
|
||||
'http://localhost:3333/api/issues/events',
|
||||
['notice', 'update', 'issue.created'],
|
||||
{
|
||||
autoReconnect: {
|
||||
retries: 3,
|
||||
delay: 1000,
|
||||
onFailed() {
|
||||
console.log('Failed to connect EventSource after 3 retries')
|
||||
},
|
||||
},
|
||||
autoConnect: true,
|
||||
},
|
||||
)
|
||||
console.log(data.value, status.value, error.value, close)
|
||||
|
||||
const layout = computed(() => {
|
||||
const layout = route?.meta?.layout
|
||||
return layout ? `${layout}Layout` : 'DefaultLayout'
|
||||
})
|
||||
|
||||
const statusStore = useStatusStore()
|
||||
const issueStore = useIssueStore()
|
||||
|
||||
onMounted(async () => {
|
||||
await statusStore.list()
|
||||
await issueStore.getStat()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- <h1>You did it!</h1>-->
|
||||
<!-- <router-view />-->
|
||||
|
||||
<component :is="layout">
|
||||
<router-view v-slot="{ Component, route }">
|
||||
<div :key="route.name">
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
import { gql, type TypedDocumentNode } from '@apollo/client/core'
|
||||
import { apolloClient } from '@/lib/apollo-client.ts'
|
||||
|
||||
export interface Issue {
|
||||
id: number
|
||||
key: string
|
||||
title: string
|
||||
content: string
|
||||
status: 'OPEN' | 'IN_PROGRESS' | 'DONE' | 'CANCELLED'
|
||||
priority: 0 | 1 | 2 | 3 | 4
|
||||
assignee: string
|
||||
reporter: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export interface Meta {
|
||||
page: number
|
||||
totalCount: number
|
||||
totalPages: number
|
||||
}
|
||||
|
||||
interface IssuesQueryData {
|
||||
issues: { list: Issue[]; meta: Meta }
|
||||
}
|
||||
|
||||
interface IssuesQueryVariables {
|
||||
page: number
|
||||
limit: number
|
||||
}
|
||||
|
||||
interface IssueByKeyQueryData {
|
||||
issueByKey: Issue | null
|
||||
}
|
||||
|
||||
interface IssueByKeyQueryVariables {
|
||||
ckey: string
|
||||
}
|
||||
|
||||
export interface CreateIssueInput {
|
||||
key: string
|
||||
title: string
|
||||
content?: string
|
||||
status?: Issue['status']
|
||||
priority?: Issue['priority']
|
||||
assignee?: string
|
||||
}
|
||||
|
||||
interface CreateIssueMutationData {
|
||||
createIssue: Issue
|
||||
}
|
||||
|
||||
interface CreateIssueMutationVariables {
|
||||
input: CreateIssueInput
|
||||
}
|
||||
|
||||
export type UpdateIssueInput = Partial<CreateIssueInput>
|
||||
|
||||
interface UpdateIssueMutationData {
|
||||
updateIssue: Issue
|
||||
}
|
||||
|
||||
interface UpdateIssueMutationVariables {
|
||||
id: number
|
||||
input: UpdateIssueInput
|
||||
}
|
||||
|
||||
export interface IssueStatByStatus {
|
||||
status: Issue['status']
|
||||
label: string
|
||||
count: number
|
||||
}
|
||||
|
||||
export interface IssueStat {
|
||||
total: number
|
||||
byStatus: IssueStatByStatus[]
|
||||
}
|
||||
|
||||
interface IssueStatResponse {
|
||||
data: IssueStat
|
||||
code: number
|
||||
}
|
||||
|
||||
// QUERY
|
||||
|
||||
const ISSUE_FIELDS = gql`
|
||||
fragment IssueFields on Issue {
|
||||
id
|
||||
key
|
||||
title
|
||||
content
|
||||
status
|
||||
priority
|
||||
assignee
|
||||
reporter
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
`
|
||||
|
||||
const ISSUES_QUERY: TypedDocumentNode<IssuesQueryData, IssuesQueryVariables> = gql`
|
||||
query Issues($page: Int!, $limit: Int!) {
|
||||
issues(page: $page, limit: $limit) {
|
||||
list {
|
||||
...IssueFields
|
||||
}
|
||||
meta {
|
||||
totalCount
|
||||
totalPages
|
||||
page
|
||||
}
|
||||
}
|
||||
}
|
||||
${ISSUE_FIELDS}
|
||||
`
|
||||
|
||||
const ISSUE_BY_KEY_QUERY: TypedDocumentNode<IssueByKeyQueryData, IssueByKeyQueryVariables> = gql`
|
||||
query IssueByKey($ckey: String!) {
|
||||
issueByKey(ckey: $ckey) {
|
||||
...IssueFields
|
||||
}
|
||||
}
|
||||
${ISSUE_FIELDS}
|
||||
`
|
||||
|
||||
const CREATE_ISSUE_MUTATION: TypedDocumentNode<CreateIssueMutationData, CreateIssueMutationVariables> = gql`
|
||||
mutation CreateIssue($input: CreateIssueInput!) {
|
||||
createIssue(input: $input) {
|
||||
...IssueFields
|
||||
}
|
||||
}
|
||||
${ISSUE_FIELDS}
|
||||
`
|
||||
|
||||
const UPDATE_ISSUE_MUTATION: TypedDocumentNode<UpdateIssueMutationData, UpdateIssueMutationVariables> = gql`
|
||||
mutation UpdateIssue($id: Int!, $input: UpdateIssueInput!) {
|
||||
updateIssue(id: $id, input: $input) {
|
||||
...IssueFields
|
||||
}
|
||||
}
|
||||
${ISSUE_FIELDS}
|
||||
`
|
||||
|
||||
// CLIENT REQUEST
|
||||
|
||||
export async function itemsList(params: { page: number; limit: number; text?: string | null }) {
|
||||
const { data } = await apolloClient.query({
|
||||
query: ISSUES_QUERY,
|
||||
variables: params,
|
||||
fetchPolicy: 'network-only',
|
||||
})
|
||||
|
||||
if (!data) throw new Error('Failed to load issues')
|
||||
return data.issues
|
||||
}
|
||||
|
||||
export async function itemGetByKey(ckey: string) {
|
||||
const { data } = await apolloClient.query({
|
||||
query: ISSUE_BY_KEY_QUERY,
|
||||
variables: { ckey },
|
||||
fetchPolicy: 'network-only',
|
||||
})
|
||||
|
||||
if (!data?.issueByKey) throw new Error('Issue not found')
|
||||
return data.issueByKey
|
||||
}
|
||||
|
||||
export async function itemCreate(input: CreateIssueInput) {
|
||||
const { data } = await apolloClient.mutate({
|
||||
mutation: CREATE_ISSUE_MUTATION,
|
||||
variables: { input },
|
||||
})
|
||||
|
||||
if (!data?.createIssue) throw new Error('Failed to create issue')
|
||||
return data.createIssue
|
||||
}
|
||||
|
||||
export async function itemUpdate(id: number, input: UpdateIssueInput) {
|
||||
const { data } = await apolloClient.mutate({
|
||||
mutation: UPDATE_ISSUE_MUTATION,
|
||||
variables: { id, input },
|
||||
})
|
||||
|
||||
if (!data?.updateIssue) throw new Error('Failed to update issue')
|
||||
return data.updateIssue
|
||||
}
|
||||
|
||||
export async function itemStat() {
|
||||
const res = await fetch('/api/issues/stats', { method: 'GET' })
|
||||
|
||||
if (!res.ok) throw new Error('Failed to load issue stats')
|
||||
const { data } = (await res.json()) as IssueStatResponse
|
||||
return data
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { gql, type TypedDocumentNode } from '@apollo/client/core'
|
||||
import { apolloClient } from '@/lib/apollo-client.ts'
|
||||
|
||||
export interface Status {
|
||||
name: string
|
||||
alias: string
|
||||
color: string
|
||||
}
|
||||
|
||||
interface StatusesQueryData {
|
||||
statuses: Status[]
|
||||
}
|
||||
|
||||
// QUERY
|
||||
|
||||
const STATUSES_QUERY: TypedDocumentNode<StatusesQueryData> = gql`
|
||||
query Statuses {
|
||||
statuses {
|
||||
alias
|
||||
name
|
||||
color
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
// CLIENT REQUEST
|
||||
|
||||
export async function statusesList() {
|
||||
const { data } = await apolloClient.query({
|
||||
query: STATUSES_QUERY,
|
||||
fetchPolicy: 'network-only',
|
||||
})
|
||||
|
||||
if (!data) throw new Error('Failed to load statuses')
|
||||
return data.statuses
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
/*@import "tailwindcss";*/
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
font-family: "Montserrat", sans-serif;
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
font-optical-sizing: auto;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
import { type Status, useStatusStore } from '@/stores/status.ts'
|
||||
import { ref, watchEffect } from 'vue'
|
||||
|
||||
interface Props {
|
||||
value?: string
|
||||
}
|
||||
|
||||
const model = defineModel<string>()
|
||||
|
||||
const { value } = defineProps<Props>()
|
||||
|
||||
const statuses = ref<Status[]>([])
|
||||
|
||||
const select = (value: unknown) => {
|
||||
if (Array.isArray(value)) {
|
||||
model.value = value[0]
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="menu">
|
||||
<v-menu>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn density="comfortable" icon variant="text" v-bind="props" :text="model">
|
||||
<v-avatar density="compact" border color="primary"></v-avatar>
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<v-list class="mt-1" select-strategy="single-leaf" @update:selected="select">
|
||||
<v-list-item v-for="el in statuses" :key="el.id" :value="el.name">
|
||||
<v-list-item-title>{{ el.name }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.menu {
|
||||
//width: 3rem;
|
||||
//overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
interface Props {
|
||||
title: string
|
||||
}
|
||||
|
||||
const icons = ['mdi-chevron-right', 'mdi-chevron-down']
|
||||
const isOpen = ref(true)
|
||||
const { title } = defineProps<Props>()
|
||||
|
||||
const icon = computed(() => {
|
||||
return isOpen.value ? icons[1] : icons[0]
|
||||
})
|
||||
|
||||
const open = () => {
|
||||
isOpen.value = !isOpen.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="d-flex flex-column ga-1">
|
||||
<div class="d-flex align-center ga-1">
|
||||
<v-btn density="compact" size="small" :icon="icon" @click="open" />
|
||||
<div @click="open" class="font-weight-bold text-capitalize text-title-medium cursor-pointer">{{ title }}</div>
|
||||
</div>
|
||||
<div v-if="isOpen" class="ml-6 mt-2">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -0,0 +1,53 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
interface Props {
|
||||
value?: string
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<{ changed: [value: string] }>()
|
||||
|
||||
const editing = ref(false)
|
||||
const draft = ref(props.value ?? '')
|
||||
|
||||
const startEdit = () => {
|
||||
draft.value = props.value ?? ''
|
||||
editing.value = true
|
||||
}
|
||||
|
||||
const save = () => {
|
||||
emit('changed', draft.value)
|
||||
editing.value = false
|
||||
}
|
||||
|
||||
const cancel = () => {
|
||||
draft.value = props.value ?? ''
|
||||
editing.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="issue-content">
|
||||
<template v-if="!editing">
|
||||
<v-sheet class="pa-2 cursor-pointer content-sheet" @click="startEdit">
|
||||
{{ value ?? 'click to add content' }}
|
||||
</v-sheet>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<v-textarea class="mb-1" hide-details v-model="draft" variant="outlined" auto-grow rows="4" />
|
||||
|
||||
<div class="d-flex justify-end ga-1">
|
||||
<v-btn density="compact" variant="text" text="Cancel" @click="cancel" />
|
||||
<v-btn density="compact" color="primary" variant="tonal" text="Save" @click="save" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.content-sheet:hover {
|
||||
background-color: rgba(var(--v-theme-on-surface), 0.06);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,58 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue'
|
||||
import { useIssueStore } from '@/stores/issue.ts'
|
||||
import type { Issue } from '@/api/issue.api.ts'
|
||||
|
||||
const issueStore = useIssueStore()
|
||||
const emit = defineEmits<{ created: [issue: Issue] }>()
|
||||
|
||||
const showForm = ref(false)
|
||||
const newIssue = reactive({
|
||||
key: 'type',
|
||||
title: '',
|
||||
})
|
||||
|
||||
const onCreate = async () => {
|
||||
const created = await issueStore.create(newIssue)
|
||||
if (!created) return
|
||||
|
||||
emit('created', created)
|
||||
newIssue.title = ''
|
||||
showForm.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-list-item v-if="showForm" class="">
|
||||
<template #prepend>
|
||||
<v-menu v-if="true">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-icon v-bind="props" icon="mdi-square" />
|
||||
</template>
|
||||
|
||||
<v-list select-strategy="single-leaf">
|
||||
<v-list-item v-for="(el, idx) in ['aaa', 'bbb']" :key="idx" :value="el">
|
||||
<v-list-item-title>{{ el }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</template>
|
||||
|
||||
<template #title>
|
||||
<div class="d-flex ga-2 align-center">
|
||||
<v-text-field v-model="newIssue.title" variant="filled" placeholder="Describe your issue" />
|
||||
<v-btn density="compact" variant="tonal" text="Create" @click="onCreate" />
|
||||
<v-btn density="compact" variant="text" icon="mdi-close" @click="showForm = false" />
|
||||
</div>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<v-list-item class="d-flex justify-center" v-else @click="showForm = !showForm">
|
||||
<div class="d-flex ga-2 align-center">
|
||||
<v-icon icon="mdi-plus" />
|
||||
<div>add issue</div>
|
||||
</div>
|
||||
<!-- <v-btn density="compact" variant="plain" prepend-icon="mdi-plus"> create </v-btn>-->
|
||||
</v-list-item>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -0,0 +1,103 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watchEffect } from 'vue'
|
||||
import { useIssueStore } from '@/stores/issue.js'
|
||||
import { useStatusStore } from '@/stores/status.ts'
|
||||
import ExpansionPanel from '@/components/ExpansionPanel.vue'
|
||||
import IssueContent from '@/components/IssueContent.vue'
|
||||
import type { Issue } from '@/api/issue.api.ts'
|
||||
import StatusMenu from '@/components/StatusMenu.vue'
|
||||
|
||||
interface Props {
|
||||
ckey: string
|
||||
}
|
||||
|
||||
const emit = defineEmits<{ statusChanged: [issue: Issue] }>()
|
||||
|
||||
const issueStore = useIssueStore()
|
||||
const statusStore = useStatusStore()
|
||||
const issue = ref<Issue>()
|
||||
|
||||
const { ckey } = defineProps<Props>()
|
||||
|
||||
const statusName = computed(() => {
|
||||
return statusStore.statuses?.find((el) => el.alias === issue.value?.status)?.name ?? issue.value?.status
|
||||
})
|
||||
|
||||
const createdAtFormatted = computed(() => {
|
||||
if (!issue.value?.createdAt) return ''
|
||||
return new Intl.DateTimeFormat('ru-RU', { dateStyle: 'medium', timeStyle: 'short' }).format(
|
||||
new Date(issue.value.createdAt),
|
||||
)
|
||||
})
|
||||
|
||||
async function fetch() {
|
||||
const res = await issueStore.getByKey(ckey)
|
||||
if (!res) {
|
||||
return
|
||||
}
|
||||
|
||||
issue.value = res
|
||||
}
|
||||
|
||||
watchEffect(async () => fetch())
|
||||
|
||||
const onContentChanged = async (content: string) => {
|
||||
if (!issue.value) return
|
||||
const updated = await issueStore.update(issue.value.id, { content })
|
||||
if (updated) issue.value = updated
|
||||
}
|
||||
|
||||
const onStatusChanged = async (status: Issue['status']) => {
|
||||
if (!issue.value) return
|
||||
|
||||
const updated = await issueStore.update(issue.value.id, { status })
|
||||
if (updated) {
|
||||
issue.value = updated
|
||||
emit('statusChanged', updated)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="issue" class="d-flex flex-column ga-4 pa-4">
|
||||
<div>{{ ckey }}</div>
|
||||
<div class="text-uppercase text-title-large">{{ issue.title }}</div>
|
||||
|
||||
<v-card elevation="0" border>
|
||||
<v-card-text class="u-grid-2">
|
||||
<div class="text-medium-emphasis">Status</div>
|
||||
<status-menu :value="issue.status" @changed="onStatusChanged" />
|
||||
|
||||
<div class="text-medium-emphasis">Reporter</div>
|
||||
<div>{{ issue.reporter }}</div>
|
||||
|
||||
<div class="text-medium-emphasis">Assignee</div>
|
||||
<div>{{ issue.assignee }}</div>
|
||||
|
||||
<div class="text-medium-emphasis">Created At</div>
|
||||
<div>{{ createdAtFormatted }}</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<!-- <status-menu v-model="issue.status" />-->
|
||||
|
||||
<expansion-panel title="Details">
|
||||
<issue-content :value="issue.content" @changed="onContentChanged" />
|
||||
</expansion-panel>
|
||||
|
||||
<expansion-panel title="Activity">
|
||||
<v-btn-group divided border density="compact">
|
||||
<v-btn>comments</v-btn>
|
||||
<v-btn>history</v-btn>
|
||||
</v-btn-group>
|
||||
</expansion-panel>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.u-grid-2 {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
column-gap: 1rem;
|
||||
row-gap: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
@@ -9,18 +9,25 @@ async function onSearch() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="u-grid2 ga-2">
|
||||
<v-text-field
|
||||
v-model="text"
|
||||
clearable
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
placeholder="Search name..."
|
||||
hide-details
|
||||
@keydown.enter.prevent="onSearch"
|
||||
@click:clear="onSearch"
|
||||
/>
|
||||
<v-select variant="outlined" clearable :items="['abc', 'def']"></v-select>
|
||||
|
||||
<!-- <v-btn>filter</v-btn>-->
|
||||
|
||||
<!-- <v-btn variant="plain">clear filters</v-btn>-->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
<style scoped lang="scss">
|
||||
.u-grid2 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
<script setup lang="ts">
|
||||
import PriorityMenu from '@/components/PriorityMenu.vue'
|
||||
import StatusMenu from '@/components/StatusMenu.vue'
|
||||
import { useIssueStore } from '@/stores/issue.ts'
|
||||
import type { Issue } from '@/api/issue.api.ts'
|
||||
|
||||
interface Props {
|
||||
issue: Issue
|
||||
}
|
||||
|
||||
const { issue } = defineProps<Props>()
|
||||
const emit = defineEmits<{ statusChanged: [issue: Issue]; priorityChanged: [issue: Issue] }>()
|
||||
|
||||
const issueStore = useIssueStore()
|
||||
|
||||
const onStatusChanged = async (status: Issue['status']) => {
|
||||
const updated = await issueStore.update(issue.id, { status })
|
||||
if (updated) emit('statusChanged', updated)
|
||||
}
|
||||
|
||||
const onPriorityChanged = async (priority: Issue['priority']) => {
|
||||
const updated = await issueStore.update(issue.id, { priority })
|
||||
if (updated) emit('priorityChanged', updated)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-list-item
|
||||
class="bg-on-surface-variant"
|
||||
:class="{
|
||||
'border border-sm border-opacity-75 border-error': issue.priority === 4 && issue.status === 'IN_PROGRESS',
|
||||
}"
|
||||
:value="issue.key"
|
||||
>
|
||||
<template #title>
|
||||
<div class="d-flex ga-4">
|
||||
<div>{{ issue.key }}</div>
|
||||
<div :class="{ 'text-decoration-line-through': issue.status === 'DONE' }">{{ issue.title }}</div>
|
||||
<!-- <div :class="{ 'text-decoration-line-through': issue.status === 'DONE' }">{{ issue.content }}</div>-->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #prepend>
|
||||
<div class="d-flex ga-1 align-center mr-2">
|
||||
<v-checkbox density="compact" :value="issue.id" />
|
||||
<v-icon icon="mdi-square" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #append>
|
||||
<div class="d-flex ga-4 align-center">
|
||||
<status-menu :value="issue.status" @changed="onStatusChanged" />
|
||||
<priority-menu :value="issue.priority" @changed="onPriorityChanged" />
|
||||
<!-- <assignee-menu v-model="el.status" />-->
|
||||
</div>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive } from 'vue'
|
||||
import { authClient } from '@/lib/auth-client.js'
|
||||
|
||||
const passwordData = reactive({
|
||||
newPassword: '',
|
||||
currentPassword: '',
|
||||
})
|
||||
|
||||
const changePassword = async () => {
|
||||
await authClient.changePassword({
|
||||
newPassword: passwordData.newPassword,
|
||||
currentPassword: passwordData.currentPassword,
|
||||
revokeOtherSessions: true,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-sheet>
|
||||
<div class="text-uppercase">Change password</div>
|
||||
<div class="d-flex ga-2">
|
||||
<v-text-field placeholder="current password" v-model="passwordData.currentPassword" />
|
||||
<v-text-field placeholder="new password" v-model="passwordData.newPassword" />
|
||||
<v-icon-btn variant="tonal" icon="mdi-check" @click="changePassword" />
|
||||
</div>
|
||||
</v-sheet>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -0,0 +1,62 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import type { Issue } from '@/api/issue.api.ts'
|
||||
|
||||
enum Icons {
|
||||
highest = 'mdi-chevron-double-up',
|
||||
high = 'mdi-chevron-up',
|
||||
eq = 'mdi-equal',
|
||||
low = 'mdi-chevron-down',
|
||||
lowest = 'mdi-chevron-double-down',
|
||||
}
|
||||
|
||||
interface Props {
|
||||
value?: number
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<{ changed: [value: Issue['priority']] }>()
|
||||
|
||||
const priorityList = ref([
|
||||
{ icon: Icons.highest, value: 4 },
|
||||
{ icon: Icons.high, value: 3 },
|
||||
{ icon: Icons.eq, value: 2 },
|
||||
{ icon: Icons.low, value: 1 },
|
||||
{ icon: Icons.lowest, value: 0 },
|
||||
])
|
||||
|
||||
const currentIcon = computed(() => priorityList.value.find((el) => el.value === props.value)?.icon ?? Icons.eq)
|
||||
|
||||
const availablePriorities = computed(() => priorityList.value.filter((el) => el.value !== props.value))
|
||||
|
||||
const select = (value: unknown) => {
|
||||
if (Array.isArray(value)) {
|
||||
emit('changed', value[0])
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="menu">
|
||||
<v-menu>
|
||||
<template v-slot:activator="{ props: activatorProps }">
|
||||
<v-btn density="compact" :icon="currentIcon" variant="text" v-bind="activatorProps" />
|
||||
</template>
|
||||
|
||||
<v-list class="mt-1" select-strategy="single-leaf" density="compact" @update:selected="select">
|
||||
<v-list-item nav v-for="el in availablePriorities" :key="el.value" :value="el.value">
|
||||
<div class="d-flex justify-center align-center">
|
||||
<v-icon size="small" :icon="el.icon" />
|
||||
</div>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.menu {
|
||||
//width: 3rem;
|
||||
//overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,63 @@
|
||||
<script setup lang="ts">
|
||||
import { authClient } from '@/lib/auth-client.ts'
|
||||
import { ref, watchEffect } from 'vue'
|
||||
|
||||
interface Session {
|
||||
id: string
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
userId: string
|
||||
expiresAt: Date
|
||||
token: string
|
||||
ipAddress?: string | null | undefined
|
||||
userAgent?: string | null | undefined
|
||||
}
|
||||
|
||||
const sessions = ref<Session[]>([])
|
||||
|
||||
const listSessions = async () => {
|
||||
const result = await authClient.listSessions()
|
||||
if (result.error) {
|
||||
return
|
||||
}
|
||||
|
||||
sessions.value = result.data
|
||||
}
|
||||
|
||||
const revokeSession = async (token: string) => {
|
||||
await authClient.revokeSession({
|
||||
token: token,
|
||||
})
|
||||
}
|
||||
|
||||
const revokeOtherSessions = async () => {
|
||||
await authClient.revokeOtherSessions()
|
||||
}
|
||||
|
||||
const revokeAllSessions = async () => {
|
||||
await authClient.revokeSessions()
|
||||
}
|
||||
|
||||
watchEffect(async () => {
|
||||
await listSessions()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="text-uppercase">Active sessions</div>
|
||||
<div class="mb-2">
|
||||
<v-btn prepend-icon="mdi-skull" @click="revokeOtherSessions()">revoke other sessions</v-btn>
|
||||
</div>
|
||||
<v-list border density="compact">
|
||||
<v-list-item class="hover:bg-primary-300" v-for="session in sessions" :key="session.id">
|
||||
<div class="text-truncate">{{ session.userAgent }}</div>
|
||||
<template #append>
|
||||
<v-icon-btn size="small" @click="revokeSession(session.token)" color="error" icon="mdi-trash-can" />
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -1,25 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import { type Status, useStatusStore } from '@/stores/status.ts'
|
||||
import { ref, watchEffect } from 'vue'
|
||||
import { useStatusStore } from '@/stores/status.ts'
|
||||
import { computed } from 'vue'
|
||||
import type { Issue } from '@/api/issue.api.ts'
|
||||
|
||||
interface Props {
|
||||
value?: string
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<{ changed: [value: Issue['status']] }>()
|
||||
|
||||
const statusStore = useStatusStore()
|
||||
const model = defineModel<string>()
|
||||
|
||||
const { value } = defineProps<Props>()
|
||||
const currentStatusName = computed(() => {
|
||||
return statusStore.statuses?.find((el) => el.alias === props.value)?.name ?? props.value
|
||||
})
|
||||
|
||||
const statuses = ref<Status[]>([])
|
||||
|
||||
watchEffect(async () => {
|
||||
statuses.value = await statusStore.list()
|
||||
const currentStatusColor = computed(() => {
|
||||
return statusStore.statuses?.find((el) => el.alias === props.value)?.color ?? 'white'
|
||||
})
|
||||
|
||||
const select = (value: unknown) => {
|
||||
if (Array.isArray(value)) {
|
||||
model.value = value[0]
|
||||
emit('changed', value[0])
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -27,12 +30,19 @@ const select = (value: unknown) => {
|
||||
<template>
|
||||
<div class="menu">
|
||||
<v-menu>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn density="compact" v-bind="props" :text="model" />
|
||||
<template v-slot:activator="{ props: activatorProps }">
|
||||
<v-btn
|
||||
slim
|
||||
:color="currentStatusColor"
|
||||
density="compact"
|
||||
variant="flat"
|
||||
v-bind="activatorProps"
|
||||
:text="currentStatusName"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<v-list density="compact" select-strategy="single-leaf" @update:selected="select">
|
||||
<v-list-item v-for="el in statuses" :key="el.id" :value="el.name">
|
||||
<v-list class="mt-1" select-strategy="single-leaf" density="compact" @update:selected="select">
|
||||
<v-list-item slim v-for="(el, idx) in statusStore.statuses" :key="idx" :value="el.alias">
|
||||
<v-list-item-title>{{ el.name }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
@@ -42,7 +52,8 @@ const select = (value: unknown) => {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.menu {
|
||||
width: 8rem;
|
||||
overflow: hidden;
|
||||
//width: 6rem;
|
||||
//overflow: hidden;
|
||||
//background: blue;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
interface Props {
|
||||
user?: {
|
||||
readonly id: string
|
||||
readonly createdAt: Date
|
||||
readonly updatedAt: Date
|
||||
readonly email: string
|
||||
readonly emailVerified: boolean
|
||||
readonly name: string
|
||||
readonly image?: string | null | undefined
|
||||
}
|
||||
}
|
||||
|
||||
const { user } = defineProps<Props>()
|
||||
|
||||
const avatar = computed(() => {
|
||||
return user?.image ?? undefined
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="user" class="d-flex ga-4">
|
||||
<v-avatar size="x-large" color="info" :image="avatar" />
|
||||
|
||||
<div>
|
||||
<div>name: {{ user.name }}</div>
|
||||
<div>email: {{ user.email }}</div>
|
||||
<div>created at: {{ user.createdAt }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<v-skeleton-loader v-else type="avatar, list-item-three-line" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -1,27 +1,68 @@
|
||||
<script setup lang="ts">
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import AppMenu from '@/components/AppMenu.vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { authClient } from '@/lib/auth-client.ts'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useTheme } from 'vuetify'
|
||||
|
||||
const router = useRouter()
|
||||
const session = authClient.useSession()
|
||||
const theme = useTheme()
|
||||
|
||||
const title = 'Ulitka'
|
||||
const title = 'APP'
|
||||
const version = '0.0.1'
|
||||
|
||||
function logout() {
|
||||
console.log('logout')
|
||||
const isAuth = computed(() => !!session.value.data)
|
||||
|
||||
const menu = ref([
|
||||
{
|
||||
title: 'Home',
|
||||
value: 1,
|
||||
props: { prependIcon: 'mdi-home', to: '/', requiresAuth: false },
|
||||
},
|
||||
{
|
||||
title: 'issues',
|
||||
value: 2,
|
||||
props: { prependIcon: 'mdi-list-status', to: '/issues', requiresAuth: true },
|
||||
},
|
||||
{
|
||||
title: 'summary',
|
||||
value: 3,
|
||||
props: { prependIcon: 'mdi-view-dashboard', to: '/summary', requiresAuth: true },
|
||||
},
|
||||
{
|
||||
title: 'games',
|
||||
value: 4,
|
||||
props: { prependIcon: 'mdi-view-dashboard', to: '/games', requiresAuth: false },
|
||||
},
|
||||
])
|
||||
|
||||
function goToSignIn() {
|
||||
router.push({ path: '/sign-in' })
|
||||
}
|
||||
|
||||
function goToLogin() {
|
||||
router.push({ path: '/login' })
|
||||
}
|
||||
|
||||
function goToRegister() {
|
||||
router.push({ path: '/register' })
|
||||
function goToSignUp() {
|
||||
router.push({ path: '/sign-up' })
|
||||
}
|
||||
|
||||
function goToProfile() {
|
||||
router.push({ path: '/profile' })
|
||||
}
|
||||
|
||||
async function goToSignOut() {
|
||||
await authClient.signOut({
|
||||
fetchOptions: {
|
||||
onSuccess: () => {
|
||||
router.push('/sign-in')
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function toggleTheme(e: MouseEvent) {
|
||||
const target = e.target as HTMLElement
|
||||
theme.setTransitionOrigin(target)
|
||||
theme.toggle()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -30,35 +71,62 @@ function goToProfile() {
|
||||
<template v-slot:prepend>
|
||||
<v-menu>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-app-bar-nav-icon variant="text" rounded="0" v-bind="props"></v-app-bar-nav-icon>
|
||||
<v-app-bar-nav-icon variant="text" v-bind="props" />
|
||||
</template>
|
||||
|
||||
<app-menu />
|
||||
<v-list class="mt-1" width="200" density="compact">
|
||||
<template v-for="item in menu" :key="item.value">
|
||||
<v-list-item v-if="!item.props.requiresAuth || isAuth === item.props.requiresAuth" :to="item.props.to">
|
||||
<v-list-item-title class="d-flex align-center ga-4">
|
||||
<v-icon :icon="item.props.prependIcon" />
|
||||
<div>{{ item.title }}</div>
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</template>
|
||||
|
||||
<v-app-bar-title>
|
||||
<div class="d-flex ga-2 align-center">
|
||||
<span>{{ title }}</span>
|
||||
<router-link class="text-decoration-none text-link font-weight-bold" to="/">
|
||||
{{ title }}
|
||||
</router-link>
|
||||
<!-- <span class="text-label-small">ver: {{ version }}</span>-->
|
||||
<span>v{{ version }}</span>
|
||||
</div>
|
||||
</v-app-bar-title>
|
||||
|
||||
<v-btn @click="goToLogin">Login</v-btn>
|
||||
<v-btn @click="goToRegister">Register</v-btn>
|
||||
<div class="mr-2" v-if="session.data">
|
||||
Hello,
|
||||
<router-link class="text-decoration-none text-link text-capitalize" to="/profile">
|
||||
{{ session?.data.user.name }}
|
||||
</router-link>
|
||||
</div>
|
||||
<v-btn v-if="!session.data" @click="goToSignIn">Sign In</v-btn>
|
||||
<v-btn v-if="!session.data" @click="goToSignUp">Sign Up</v-btn>
|
||||
|
||||
<v-divider class="mx-1" vertical />
|
||||
|
||||
<v-menu>
|
||||
<v-btn icon="mdi-brightness-6" @click="toggleTheme" />
|
||||
|
||||
<v-divider class="mx-1" vertical />
|
||||
|
||||
<v-menu v-if="session.data">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn rounded="0" icon v-bind="props">
|
||||
<v-icon icon="mdi-account-outline" />
|
||||
<v-btn icon v-bind="props">
|
||||
<v-icon icon="mdi-account" />
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list class="mt-1" width="200">
|
||||
<v-list-item @click="goToProfile">Profile</v-list-item>
|
||||
<v-list-item @click="logout">Logout</v-list-item>
|
||||
<v-list-item @click="goToProfile">
|
||||
<v-icon icon="mdi-account" />
|
||||
Profile
|
||||
</v-list-item>
|
||||
<v-list-item @click="goToSignOut">
|
||||
<v-icon icon="mdi-logout" />
|
||||
Sign Out
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-app-bar>
|
||||
|
||||
@@ -15,6 +15,5 @@ export function registerLayouts(app: App<Element>) {
|
||||
if (componentName) {
|
||||
app.component(componentName.replace('.vue', ''), component.default)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client/core'
|
||||
|
||||
export const apolloClient = new ApolloClient({
|
||||
link: new HttpLink({ uri: '/graphql' }),
|
||||
cache: new InMemoryCache(),
|
||||
})
|
||||
@@ -0,0 +1,6 @@
|
||||
import { createAuthClient } from 'better-auth/vue'
|
||||
|
||||
export const authClient = createAuthClient({
|
||||
baseURL: 'http://localhost:3333',
|
||||
plugins: [],
|
||||
})
|
||||
@@ -1,8 +1,10 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import { DefaultApolloClient } from '@vue/apollo-composable'
|
||||
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import { apolloClient } from '@/lib/apollo-client.ts'
|
||||
|
||||
import './assets/main.css'
|
||||
import 'unfonts.css'
|
||||
@@ -18,15 +20,32 @@ const app = createApp(App)
|
||||
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
app.provide(DefaultApolloClient, apolloClient)
|
||||
|
||||
const vuetify = createVuetify({
|
||||
components,
|
||||
directives,
|
||||
theme: {
|
||||
defaultTheme: 'dark', // 'system' | 'light' | 'dark'
|
||||
defaultTheme: 'light', // 'system' | 'light' | 'dark'
|
||||
themes: {
|
||||
dark: {
|
||||
colors: {
|
||||
link: '#CFD8DC',
|
||||
alt: '#616161',
|
||||
},
|
||||
},
|
||||
light: {
|
||||
colors: {
|
||||
link: '#37474F',
|
||||
alt: '#E0E0E0',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
defaults: {
|
||||
global: {},
|
||||
global: {
|
||||
rounded: '0',
|
||||
},
|
||||
VBtn: {
|
||||
variant: 'tonal',
|
||||
},
|
||||
@@ -35,6 +54,28 @@ const vuetify = createVuetify({
|
||||
density: 'compact',
|
||||
hideDetails: true,
|
||||
},
|
||||
VSwitch: {
|
||||
density: 'compact',
|
||||
hideDetails: true,
|
||||
},
|
||||
VSheet: {},
|
||||
VCombobox: {
|
||||
variant: 'outlined',
|
||||
density: 'compact',
|
||||
hideDetails: true,
|
||||
},
|
||||
VCheckbox: {
|
||||
density: 'compact',
|
||||
hideDetails: true,
|
||||
},
|
||||
VList: {
|
||||
density: 'compact',
|
||||
},
|
||||
VSelect: {
|
||||
variant: 'solo',
|
||||
density: 'compact',
|
||||
hideDetails: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -14,35 +14,6 @@ export const statusListMock: Status[] = [
|
||||
{ id: '5', name: 'canceled' },
|
||||
]
|
||||
|
||||
export const issueListMock = [
|
||||
{
|
||||
id: '1',
|
||||
key: 'AT-111',
|
||||
title: 'title1',
|
||||
text: 'test 1',
|
||||
status: 'new',
|
||||
priority: 1,
|
||||
assignee: '1',
|
||||
reporter: '2',
|
||||
account_id: '1',
|
||||
created_at: '',
|
||||
tags: ['aa', 'bb'],
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
key: 'AT-112',
|
||||
title: 'title2',
|
||||
text: 'test 2',
|
||||
status: 'working',
|
||||
priority: 1,
|
||||
assignee: '1',
|
||||
reporter: '2',
|
||||
account_id: '1',
|
||||
created_at: '',
|
||||
tags: ['aa', 'bb'],
|
||||
},
|
||||
]
|
||||
|
||||
export const type = [
|
||||
{ id: '1', name: 'type one' },
|
||||
{ id: '2', name: 'type two' },
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import GamesView from '@/views/games/GamesView.vue'
|
||||
|
||||
export const gamesRoutes: RouteRecordRaw = {
|
||||
path: '/games',
|
||||
meta: { requiresAuth: false },
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: GamesView,
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -1,11 +1,14 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import LoginView from '@/views/LoginView.vue'
|
||||
import ProfileView from '@/views/ProfileView.vue'
|
||||
import ProfileView from '@/views/users/ProfileView.vue'
|
||||
import { issuesRoutes } from '@/router/issues.ts'
|
||||
import { reportsRoutes } from '@/router/reports.ts'
|
||||
import { summaryRoutes } from '@/router/summary.ts'
|
||||
import RegisterView from '@/views/RegisterView.vue'
|
||||
import HomeView from '@/views/HomeView.vue'
|
||||
import VerifyEmailView from '@/views/users/VerifyEmailView.vue'
|
||||
import SignUpView from '@/views/users/SignUpView.vue'
|
||||
import SignInView from '@/views/users/SignInView.vue'
|
||||
import { authClient } from '@/lib/auth-client.ts'
|
||||
import { gamesRoutes } from '@/router/games.ts'
|
||||
|
||||
declare module 'vue-router' {
|
||||
interface RouteMeta {
|
||||
@@ -22,21 +25,49 @@ const router = createRouter({
|
||||
meta: { layout: 'Default' },
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
component: LoginView,
|
||||
name: 'sign-in',
|
||||
path: '/sign-in',
|
||||
component: SignInView,
|
||||
},
|
||||
{
|
||||
path: '/register',
|
||||
component: RegisterView,
|
||||
path: '/sign-up',
|
||||
component: SignUpView,
|
||||
},
|
||||
{
|
||||
path: '/verify-email',
|
||||
component: VerifyEmailView,
|
||||
},
|
||||
{
|
||||
path: '/profile',
|
||||
component: ProfileView,
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
issuesRoutes,
|
||||
summaryRoutes,
|
||||
reportsRoutes,
|
||||
gamesRoutes,
|
||||
],
|
||||
})
|
||||
|
||||
router.beforeEach(async (to, from) => {
|
||||
// 1. Fetch the current session from Better Auth
|
||||
const { data: session } = await authClient.getSession()
|
||||
|
||||
// console.log('before each ', { session })
|
||||
// const session = 'true'
|
||||
const isAuthenticated = !!session
|
||||
|
||||
// 2. If the route requires auth and user is missing, redirect to login
|
||||
if (to.meta.requiresAuth && !isAuthenticated) {
|
||||
return { name: 'sign-in', query: { redirect: to.fullPath } }
|
||||
}
|
||||
|
||||
// 3. If route requires a guest (like Login page) and user is logged in, redirect to dashboard
|
||||
if (to.meta.requiresGuest && isAuthenticated) {
|
||||
return { name: 'dashboard' }
|
||||
}
|
||||
|
||||
// Implicitly returns undefined / true to allow navigation if conditions don't match
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
@@ -1,7 +1,19 @@
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import IssuesView from '@/views/IssuesView.vue'
|
||||
import IssuesView from '@/views/Issues/IssuesListView.vue'
|
||||
import IssueView from '@/views/Issues/IssueView.vue'
|
||||
|
||||
export const issuesRoutes: RouteRecordRaw = {
|
||||
path: '/issues',
|
||||
component: IssuesView
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: IssuesView,
|
||||
},
|
||||
{
|
||||
path: ':id',
|
||||
component: IssueView,
|
||||
props: true,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import ReportsView from '@/views/ReportsView.vue'
|
||||
import ReportsView from '@/views/reports/ReportsView.vue'
|
||||
|
||||
export const reportsRoutes: RouteRecordRaw = {
|
||||
path: '/reports',
|
||||
component: ReportsView,
|
||||
meta: { requiresAuth: true },
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import SummaryView from '@/views/SummaryView.vue'
|
||||
import SummaryView from '@/views/summary/SummaryView.vue'
|
||||
|
||||
export const summaryRoutes: RouteRecordRaw = {
|
||||
path: '/summary',
|
||||
component: SummaryView,
|
||||
meta: { requiresAuth: true },
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useCounterStore = defineStore('counter', () => {
|
||||
const count = ref(0)
|
||||
const doubleCount = computed(() => count.value * 2)
|
||||
function increment() {
|
||||
count.value++
|
||||
}
|
||||
|
||||
return { count, doubleCount, increment }
|
||||
})
|
||||
@@ -1,38 +1,74 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
import { issueListMock } from '@/mocks.ts'
|
||||
|
||||
export interface Item {
|
||||
id: string
|
||||
key: string
|
||||
title: string
|
||||
text: string
|
||||
status: string
|
||||
priority: number
|
||||
assignee: string
|
||||
reporter: string
|
||||
account_id: string
|
||||
created_at: string
|
||||
tags: string[]
|
||||
}
|
||||
import { apolloClient } from '@/lib/apollo-client.ts'
|
||||
import { ref } from 'vue'
|
||||
import {
|
||||
type CreateIssueInput,
|
||||
type Issue,
|
||||
itemCreate,
|
||||
itemGetByKey,
|
||||
itemsList,
|
||||
itemStat,
|
||||
itemUpdate,
|
||||
type IssueStat,
|
||||
type Meta,
|
||||
type UpdateIssueInput,
|
||||
} from '@/api/issue.api.ts'
|
||||
|
||||
export const useIssueStore = defineStore('item', () => {
|
||||
const load = ref<Item[]>(issueListMock)
|
||||
const issues = ref<Issue[]>([])
|
||||
const stats = ref<IssueStat>()
|
||||
const error = ref<string | null>(null)
|
||||
const lastMeta = ref<Meta>({
|
||||
page: 1,
|
||||
totalCount: 0,
|
||||
totalPages: 0,
|
||||
})
|
||||
|
||||
async function list(params: { page: number; itemsPerPage: number; text: string | null }) {
|
||||
const { page, itemsPerPage, text } = params
|
||||
|
||||
return new Promise<{ items: Item[]; total: number }>((resolve) => {
|
||||
const start = (page - 1) * itemsPerPage
|
||||
const end = start + itemsPerPage
|
||||
const items = load.value.slice()
|
||||
|
||||
const filtered = text ? items.filter((el) => el.text.includes(text)) : items
|
||||
|
||||
const paginated = filtered.slice(start, end === -1 ? undefined : end)
|
||||
|
||||
resolve({ items: paginated, total: filtered.length })
|
||||
})
|
||||
async function list(params: { page: number; limit: number; text?: string | null }) {
|
||||
try {
|
||||
const res = await itemsList(params)
|
||||
issues.value = res.list
|
||||
return res
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : 'error get list of items'
|
||||
return { list: [], meta: lastMeta.value }
|
||||
}
|
||||
}
|
||||
return { list }
|
||||
|
||||
async function getByKey(ckey: string) {
|
||||
try {
|
||||
const res = await itemGetByKey(ckey)
|
||||
return res
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : 'error get item by key'
|
||||
}
|
||||
}
|
||||
|
||||
async function create(input: CreateIssueInput) {
|
||||
try {
|
||||
return await itemCreate(input)
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : 'error creating issue'
|
||||
}
|
||||
}
|
||||
|
||||
async function update(id: number, input: UpdateIssueInput) {
|
||||
try {
|
||||
return await itemUpdate(id, input)
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : 'error updating issue'
|
||||
}
|
||||
}
|
||||
|
||||
async function getStat(): Promise<IssueStat | undefined> {
|
||||
try {
|
||||
stats.value = await itemStat()
|
||||
|
||||
return stats.value
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : 'error getting issue stat'
|
||||
}
|
||||
}
|
||||
|
||||
return { stats, list, getByKey, create, update, getStat }
|
||||
})
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
import { statusListMock } from '@/mocks.ts'
|
||||
|
||||
export interface Status {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
import { ref } from 'vue'
|
||||
import { type Status, statusesList } from '@/api/status.api.ts'
|
||||
|
||||
export const useStatusStore = defineStore('counter', () => {
|
||||
const statuses = ref<Status[]>(statusListMock)
|
||||
const statuses = ref<Status[]>()
|
||||
|
||||
async function list(): Promise<Status[]> {
|
||||
console.log('useStatusStore.list')
|
||||
return Promise.resolve(statuses.value)
|
||||
async function list() {
|
||||
statuses.value = await statusesList()
|
||||
}
|
||||
|
||||
return { list }
|
||||
return { statuses, list }
|
||||
})
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>test</template>
|
||||
<template>
|
||||
<div>Home</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watchEffect } from 'vue'
|
||||
import { type Issue, useIssueStore } from '@/stores/issue.ts'
|
||||
import StatusMenu from '@/components/StatusMenu.vue'
|
||||
import ExpansionPanel from '@/components/ExpansionPanel.vue'
|
||||
import IssueDetails from '@/components/IssueDetails.vue'
|
||||
|
||||
interface Props {
|
||||
ckey: string
|
||||
}
|
||||
|
||||
const issueStore = useIssueStore()
|
||||
const issue = ref<Issue>()
|
||||
|
||||
const { ckey } = defineProps<Props>()
|
||||
|
||||
async function fetch() {
|
||||
const res = await issueStore.getByKey(ckey)
|
||||
if (!res) return
|
||||
|
||||
issue.value = res
|
||||
}
|
||||
|
||||
watchEffect(async () => fetch())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-sheet border>
|
||||
<issue-details :ckey="ckey" />
|
||||
</v-sheet>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -0,0 +1,185 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useIssueStore } from '@/stores/issue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import IssueDetails from '@/components/IssueDetails.vue'
|
||||
import IssueCreate from '@/components/IssueCreate.vue'
|
||||
import type { Issue } from '@/api/issue.api.ts'
|
||||
import IssuesListElement from '@/components/IssuesListElement.vue'
|
||||
import IssueFilter from '@/components/IssueFilter.vue'
|
||||
|
||||
const issueStore = useIssueStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const itemsPerPage = ref(10)
|
||||
const page = ref(1)
|
||||
const totalItems = ref(0)
|
||||
const totalPages = ref(1)
|
||||
|
||||
const serverItems = ref<Issue[]>([])
|
||||
|
||||
const text = ref<string | null>(null)
|
||||
const selected = ref<[string] | []>([])
|
||||
const batch = ref<string[]>([])
|
||||
|
||||
async function fetch() {
|
||||
const res = await issueStore.list({
|
||||
page: page.value,
|
||||
limit: itemsPerPage.value,
|
||||
text: text.value,
|
||||
})
|
||||
|
||||
serverItems.value = [...res.list]
|
||||
totalPages.value = res.meta.totalPages
|
||||
totalItems.value = res.meta.totalCount
|
||||
}
|
||||
|
||||
async function load() {
|
||||
syncQuery()
|
||||
await fetch()
|
||||
}
|
||||
|
||||
// async function onSearch() {
|
||||
// await updatePage(1)
|
||||
// }
|
||||
|
||||
function syncQuery() {
|
||||
const query: Record<string, string> = { page: String(page.value) }
|
||||
|
||||
if (text.value) query.text = text.value
|
||||
if (selected.value.length) query.selected_issue = selected.value[0]
|
||||
|
||||
router.replace({ query })
|
||||
}
|
||||
|
||||
watch(
|
||||
() => route.query,
|
||||
(query) => {
|
||||
page.value = query.page ? Number(query.page) : 1
|
||||
text.value = query.text ? String(query.text) : null
|
||||
selected.value = query.selected_issue ? [String(query.selected_issue)] : []
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
watch(selected, () => syncQuery())
|
||||
|
||||
onMounted(async () => await load())
|
||||
const gridTemplate = computed(() => (selected.value.length ? '1fr 1fr' : '1fr'))
|
||||
const goToIssue = (key: string[]) => router.push(`/issues/${key[0]}`)
|
||||
|
||||
async function updatePage(val: number) {
|
||||
page.value = val
|
||||
await load()
|
||||
}
|
||||
|
||||
function close() {
|
||||
selected.value = []
|
||||
}
|
||||
|
||||
function onIssueUpdated(updated: Issue) {
|
||||
const idx = serverItems.value.findIndex((el) => el.id === updated.id)
|
||||
if (idx !== -1) {
|
||||
serverItems.value.splice(idx, 1, updated)
|
||||
}
|
||||
}
|
||||
|
||||
async function onIssueCreated() {
|
||||
await fetch()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="d-flex flex-column ga-2">
|
||||
{{ issueStore.stats }}
|
||||
<div class="u-grid ga-1">
|
||||
<div class="u-height">
|
||||
<v-list class="pa-0" density="compact" v-model:selected="selected" mandatory>
|
||||
<issue-filter />
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<div class="mr-2">
|
||||
<v-checkbox />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #title>
|
||||
<div>
|
||||
<div>({{ serverItems.length }} of {{ totalItems }} elements visible)</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #append>
|
||||
<div class="d-flex ga-1">
|
||||
<v-chip color="primary" variant="flat" label density="compact">4</v-chip>
|
||||
<v-chip color="success" variant="flat" label density="compact">5</v-chip>
|
||||
<v-chip label variant="flat" density="compact">7</v-chip>
|
||||
</div>
|
||||
</template>
|
||||
</v-list-item>
|
||||
|
||||
<issue-create @created="onIssueCreated" />
|
||||
|
||||
<issues-list-element
|
||||
v-for="el in serverItems"
|
||||
:key="el.id"
|
||||
:issue="el"
|
||||
@status-changed="onIssueUpdated"
|
||||
@priority-changed="onIssueUpdated"
|
||||
/>
|
||||
</v-list>
|
||||
|
||||
<div class="d-flex justify-center pa-2">
|
||||
<v-pagination
|
||||
:model-value="page"
|
||||
:length="totalPages"
|
||||
:total-visible="7"
|
||||
density="comfortable"
|
||||
@update:model-value="updatePage"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-center ga-2">
|
||||
<v-banner max-width="30rem" border lines="one">
|
||||
<div class="d-flex align-center ga-2">
|
||||
<div>{{ batch.length }} selected</div>
|
||||
<v-divider vertical />
|
||||
|
||||
<v-btn prepend-icon="mdi-circle" density="compact" variant="tonal"> Done </v-btn>
|
||||
</div>
|
||||
|
||||
<template #actions>
|
||||
<div class="d-flex align-center ga-2">
|
||||
<v-divider vertical />
|
||||
<v-btn density="comfortable" icon="mdi-close"></v-btn>
|
||||
</div>
|
||||
</template>
|
||||
</v-banner>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-sheet border v-if="selected.length" class="overflow-y-auto">
|
||||
<div class="d-flex align-center ga-2 px-4 py-2">
|
||||
<div>Work item</div>
|
||||
<v-spacer />
|
||||
<v-icon-btn icon="mdi-open-in-new" @click="goToIssue(selected)" />
|
||||
<v-icon-btn icon="mdi-close" @click="close" />
|
||||
</div>
|
||||
<v-divider />
|
||||
<issue-details :ckey="selected[0]" @status-changed="onIssueUpdated" />
|
||||
</v-sheet>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.u-grid {
|
||||
display: grid;
|
||||
grid-template-columns: v-bind(gridTemplate);
|
||||
}
|
||||
|
||||
.u-height {
|
||||
height: calc(100vh - 6rem);
|
||||
}
|
||||
</style>
|
||||
@@ -1,112 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, watchEffect } from 'vue'
|
||||
import { type Item, useIssueStore } from '@/stores/issue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import IssueFilter from '@/components/IssueFilter.vue'
|
||||
import StatusMenu from '@/components/StatusMenu.vue'
|
||||
|
||||
const issueStore = useIssueStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const itemsPerPage = ref(10)
|
||||
const page = ref(1)
|
||||
const text = ref<string | null>(null)
|
||||
const serverItems = ref<Item[]>([])
|
||||
const loading = ref(true)
|
||||
const totalItems = ref(0)
|
||||
|
||||
async function fetch() {
|
||||
syncQuery()
|
||||
|
||||
loading.value = true
|
||||
const res = await issueStore.list({
|
||||
page: page.value,
|
||||
itemsPerPage: itemsPerPage.value,
|
||||
text: text.value,
|
||||
})
|
||||
|
||||
serverItems.value = res.items
|
||||
totalItems.value = res.total
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
async function loadItems(params: { page: number; itemsPerPage: number; sortBy?: unknown }) {
|
||||
page.value = params.page
|
||||
itemsPerPage.value = params.itemsPerPage
|
||||
|
||||
await fetch()
|
||||
}
|
||||
|
||||
async function onSearch() {
|
||||
page.value = 1
|
||||
await fetch()
|
||||
}
|
||||
|
||||
function syncQuery() {
|
||||
const query: Record<string, string> = {
|
||||
page: String(page.value),
|
||||
}
|
||||
if (text.value) query.text = text.value
|
||||
|
||||
router.replace({ query })
|
||||
}
|
||||
|
||||
watch(
|
||||
() => route.query,
|
||||
(query) => {
|
||||
page.value = query.page ? Number(query.page) : 1
|
||||
text.value = query.text ? String(query.text) : null
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
watchEffect(async () => {
|
||||
await loadItems({ page: 1, itemsPerPage: 10, sortBy: 1 })
|
||||
})
|
||||
|
||||
const click = () => {
|
||||
console.log('click')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="d-flex flex-column ga-2">
|
||||
<issue-filter />
|
||||
|
||||
<v-list border rounded density="compact" :lines="false">
|
||||
<v-list-item v-for="el in serverItems" :key="el.id" @click.stop="click">
|
||||
<template v-slot:title>
|
||||
<div>{{ el.key }}</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:prepend>
|
||||
<v-icon icon="mdi-square" />
|
||||
</template>
|
||||
|
||||
<template v-slot:append>
|
||||
<div class="d-flex ga-2">
|
||||
<!-- <div>{{el.status}}</div>-->
|
||||
|
||||
<!-- <v-btn density="compact">btn</v-btn>-->
|
||||
<status-menu v-model="el.status" />
|
||||
<!-- <v-menu>-->
|
||||
<!-- <template v-slot:activator="{ props }">-->
|
||||
<!-- <v-btn variant="tonal" density="compact" v-bind="props"> Dropdown </v-btn>-->
|
||||
<!-- </template>-->
|
||||
|
||||
<!-- <v-list density="compact">-->
|
||||
<!-- <v-list-item v-for="(item, index) in [{title: 'menu 1'}]" :key="index" :value="index">-->
|
||||
<!-- <v-list-item-title>{{ item.title }}</v-list-item-title>-->
|
||||
<!-- </v-list-item>-->
|
||||
<!-- </v-list>-->
|
||||
<!-- </v-menu>-->
|
||||
</div>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<v-list-item class="bg-black">
|
||||
<v-btn density="compact" variant="text" prepend-icon="mdi-plus">create</v-btn>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,18 +0,0 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<v-row>
|
||||
<v-col cols="4" offset="4">
|
||||
<div class="d-flex flex-column ga-2">
|
||||
<div class="text-uppercase">login</div>
|
||||
<v-text-field prepend-inner-icon="mdi-email" />
|
||||
<v-text-field prepend-inner-icon="mdi-lock" />
|
||||
<div>
|
||||
<v-btn>login</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -1,18 +0,0 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<v-row>
|
||||
<v-col cols="4" offset="4">
|
||||
<div class="d-flex flex-column ga-2">
|
||||
<div class="text-uppercase">register</div>
|
||||
<v-text-field prepend-inner-icon="mdi-email" />
|
||||
<v-text-field prepend-inner-icon="mdi-lock" />
|
||||
<div>
|
||||
<v-btn>register</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -1,5 +1,7 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>profile</template>
|
||||
<template>
|
||||
<div>Games</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -0,0 +1,115 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
const cards = ref<string[]>([])
|
||||
|
||||
const size = ref(2)
|
||||
const data = ['1', '2', '3', '4', '5', '6']
|
||||
const result = ref(new Map<string, [string, number]>())
|
||||
const currentCheck = ref(new Map<string, [string, number]>())
|
||||
const checking = ref(false)
|
||||
const startDate = ref<Date>()
|
||||
|
||||
const timeout = ref<number>(0)
|
||||
|
||||
const generate = () => {
|
||||
checking.value = false
|
||||
result.value.clear()
|
||||
currentCheck.value.clear()
|
||||
|
||||
const arr = Array.from({ length: size.value }, () => data).flat()
|
||||
cards.value = shuffle(arr)
|
||||
|
||||
startDate.value = new Date()
|
||||
}
|
||||
|
||||
function shuffle(array: string[]) {
|
||||
const arr = array.slice()
|
||||
|
||||
let i = arr.length
|
||||
let j = 0
|
||||
let temp: string
|
||||
|
||||
while (--i > 0) {
|
||||
j = Math.floor(Math.random() * (i + 1))
|
||||
temp = arr[j]!
|
||||
arr[j] = arr[i]!
|
||||
arr[i] = temp
|
||||
}
|
||||
|
||||
return arr
|
||||
}
|
||||
|
||||
function compareCards(dataMap: Map<string, [string, number]>) {
|
||||
const set = new Set()
|
||||
dataMap.forEach((el) => set.add(el[0]))
|
||||
return set.size === 1
|
||||
}
|
||||
|
||||
function addCorrect(dataMap: Map<string, [string, number]>) {
|
||||
for (const [key, value] of dataMap) {
|
||||
result.value.set(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
function checkCard(card: string, idx: number) {
|
||||
if (checking.value) return
|
||||
if (currentCheck.value.has(`${card}:${idx}`)) return
|
||||
|
||||
currentCheck.value.set(`${card}:${idx}`, [card, idx])
|
||||
|
||||
if (currentCheck.value.size === size.value) {
|
||||
checking.value = true
|
||||
const ok = compareCards(currentCheck.value)
|
||||
|
||||
if (ok) {
|
||||
addCorrect(currentCheck.value)
|
||||
currentCheck.value.clear()
|
||||
checking.value = false
|
||||
} else {
|
||||
timeout.value = setTimeout(() => {
|
||||
currentCheck.value.clear()
|
||||
checking.value = false
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const isWin = computed(() => data.length === result.value.size / size.value)
|
||||
|
||||
const roundTime = computed(() => {
|
||||
const endDate = new Date()
|
||||
return isWin.value && startDate.value ? endDate.valueOf() - startDate.value.valueOf() / 1000 : null
|
||||
})
|
||||
|
||||
function checkPressed(card: string, idx: number) {
|
||||
return result.value.has(`${card}:${idx}`) || currentCheck.value.has(`${card}:${idx}`)
|
||||
}
|
||||
|
||||
onUnmounted(() => {})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="d-flex flex-column ga-2">
|
||||
<div class="d-flex ga-2 align-center">
|
||||
<v-btn height="40px" @click="generate">Generate</v-btn>
|
||||
<v-text-field v-model="size" />
|
||||
</div>
|
||||
<div v-if="cards.length" class="field rounded">
|
||||
<div class="card" v-for="(card, idx) in cards" :key="idx">
|
||||
<v-img v-if="checkPressed(card, idx)" :src="`${card}.jpeg`" rounded />
|
||||
<v-img v-else src="back.jpeg" @click="checkCard(card, idx)" rounded />
|
||||
</div>
|
||||
</div>
|
||||
<v-alert v-if="isWin" :text="`Congratulations ${roundTime} sec`" type="success"></v-alert>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.field {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
gap: 0.5rem;
|
||||
width: 50rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import { authClient } from '@/lib/auth-client.js'
|
||||
import SessionsList from '@/components/SessionsList.vue'
|
||||
import UserProfile from '@/components/UserProfile.vue'
|
||||
import PasswordChange from '@/components/PasswordChange.vue'
|
||||
|
||||
const session = authClient.useSession()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-sheet rounded border class="pa-4 d-flex flex-column ga-4">
|
||||
<div class="text-headline-medium text-uppercase">profile</div>
|
||||
|
||||
<user-profile :user="session.data?.user" />
|
||||
|
||||
<password-change />
|
||||
|
||||
<sessions-list />
|
||||
</v-sheet>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -0,0 +1,44 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive } from 'vue'
|
||||
import { authClient } from '@/lib/auth-client.js'
|
||||
|
||||
const signInData = reactive({
|
||||
email: 'vadim.olonin@gmail.com',
|
||||
password: '',
|
||||
rememberMe: true,
|
||||
callbackURL: 'https://madsky.ru',
|
||||
})
|
||||
|
||||
const signIn = async () => {
|
||||
const { data, error } = await authClient.signIn.email({
|
||||
email: signInData.email,
|
||||
password: signInData.password,
|
||||
rememberMe: signInData.rememberMe,
|
||||
callbackURL: '/',
|
||||
})
|
||||
|
||||
console.log(data)
|
||||
console.log(error)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-row>
|
||||
<v-col cols="4" offset="4">
|
||||
<div class="d-flex flex-column ga-2">
|
||||
<div class="text-uppercase">sign in</div>
|
||||
<v-text-field prepend-inner-icon="mdi-email" v-model="signInData.email" />
|
||||
<v-text-field prepend-inner-icon="mdi-lock" v-model="signInData.password" />
|
||||
<div class="d-flex ga-2 align-center">
|
||||
<v-switch color="info" v-model="signInData.rememberMe" label="Remember me" />
|
||||
<v-spacer />
|
||||
<div>
|
||||
<v-btn @click="signIn">login</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -0,0 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
import { authClient } from '@/lib/auth-client.js'
|
||||
import { reactive } from 'vue'
|
||||
|
||||
const signUpData = reactive({
|
||||
name: 'Vadim',
|
||||
email: 'vadim.olonin@gmail.com',
|
||||
password: '',
|
||||
})
|
||||
|
||||
const signUp = async () => {
|
||||
const { data, error } = await authClient.signUp.email({
|
||||
name: signUpData.name,
|
||||
email: signUpData.email,
|
||||
password: signUpData.password,
|
||||
// image: 'https://example.com/image.png',
|
||||
callbackURL: 'https://madsky.ru',
|
||||
})
|
||||
|
||||
console.log(data)
|
||||
console.log(error)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-row>
|
||||
<v-col cols="4" offset="4">
|
||||
<div class="d-flex flex-column ga-2">
|
||||
<div class="text-uppercase">sign up</div>
|
||||
<v-text-field placeholder="name" prepend-inner-icon="mdi-account" v-model="signUpData.name" />
|
||||
<v-text-field placeholder="email" prepend-inner-icon="mdi-email" v-model="signUpData.email" />
|
||||
<v-text-field placeholder="password" prepend-inner-icon="mdi-lock" v-model="signUpData.password" />
|
||||
<div class="d-flex ga-2 align-center">
|
||||
<div>
|
||||
<v-btn @click="signUp">sign up</v-btn>
|
||||
</div>
|
||||
<v-spacer />
|
||||
<router-link to="/verify-email" class="text-warning text-decoration-none"> Verify Email </router-link>
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -0,0 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
import { authClient } from '@/lib/auth-client.js'
|
||||
import { reactive } from 'vue'
|
||||
|
||||
const verifyData = reactive({
|
||||
email: 'vadim.olonin@gmail.com',
|
||||
})
|
||||
|
||||
const verify = async () => {
|
||||
const { data, error } = await authClient.sendVerificationEmail({
|
||||
email: verifyData.email,
|
||||
callbackURL: 'https://madsky.ru',
|
||||
})
|
||||
|
||||
console.log(data)
|
||||
console.log(error)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-row>
|
||||
<v-col cols="4" offset="4">
|
||||
<div class="d-flex flex-column ga-2">
|
||||
<div class="text-uppercase">verify email</div>
|
||||
|
||||
<v-text-field placeholder="email" prepend-inner-icon="mdi-email" v-model="verifyData.email" />
|
||||
<div>
|
||||
<v-btn @click="verify">verify</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -2,15 +2,22 @@ import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||
// import vueDevTools from 'vite-plugin-vue-devtools'
|
||||
import ViteFonts from 'unplugin-fonts/vite'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
server: { port: 3333 },
|
||||
server: {
|
||||
port: 3333,
|
||||
strictPort: true,
|
||||
proxy: {
|
||||
'/api': 'http://localhost:3000',
|
||||
'/graphql': 'http://localhost:3000',
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
vue(),
|
||||
vueDevTools(),
|
||||
// vueDevTools(),
|
||||
ViteFonts({
|
||||
fontsource: {
|
||||
families: [
|
||||
|
||||