This commit is contained in:
2026-09-18 14:36:32 +03:00
parent e86085c2f0
commit 5c0963bc51
57 changed files with 1946 additions and 684 deletions
+1 -1
View File
@@ -2,5 +2,5 @@
"$schema": "https://json.schemastore.org/prettierrc", "$schema": "https://json.schemastore.org/prettierrc",
"semi": false, "semi": false,
"singleQuote": true, "singleQuote": true,
"printWidth": 100 "printWidth": 120
} }
+317 -370
View File
@@ -8,9 +8,12 @@
"name": "ulitka-app", "name": "ulitka-app",
"version": "0.0.1", "version": "0.0.1",
"dependencies": { "dependencies": {
"@apollo/client": "^3.14.1",
"@fontsource/roboto": "^5.3.0", "@fontsource/roboto": "^5.3.0",
"@vue/apollo-composable": "^4.2.2",
"@vueuse/core": "^14.4.0", "@vueuse/core": "^14.4.0",
"better-auth": "^1.7.1", "better-auth": "^1.7.1",
"graphql": "^17.0.2",
"pinia": "^4.0.3", "pinia": "^4.0.3",
"vue": "^3.5.41", "vue": "^3.5.41",
"vue-router": "^5.2.0", "vue-router": "^5.2.0",
@@ -32,7 +35,7 @@
"oxlint": "~1.80.0", "oxlint": "~1.80.0",
"prettier": "3.9.6", "prettier": "3.9.6",
"sass-embedded": "^1.103.1", "sass-embedded": "^1.103.1",
"typescript": "~7.0.2", "typescript": "^6.0.3",
"unplugin-fonts": "^2.0.0", "unplugin-fonts": "^2.0.0",
"vite": "^8.2.2", "vite": "^8.2.2",
"vite-plugin-vue-devtools": "^8.2.1", "vite-plugin-vue-devtools": "^8.2.1",
@@ -43,6 +46,48 @@
"node": "^22.18.0 || >=24.12.0" "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": { "node_modules/@babel/code-frame": {
"version": "7.29.7", "version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
@@ -784,6 +829,15 @@
"url": "https://github.com/sponsors/ayuhito" "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": { "node_modules/@humanfs/core": {
"version": "0.19.2", "version": "0.19.2",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
@@ -2236,346 +2290,6 @@
"url": "https://opencollective.com/eslint" "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": { "node_modules/@vitejs/plugin-vue": {
"version": "6.0.8", "version": "6.0.8",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.8.tgz", "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": { "node_modules/@vue/babel-helper-vue-transform-on": {
"version": "1.5.0", "version": "1.5.0",
"resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.5.0.tgz", "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" "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": { "node_modules/acorn": {
"version": "8.18.0", "version": "8.18.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz",
@@ -4046,6 +3830,30 @@
"node": ">=10.13.0" "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": { "node_modules/has-flag": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -4056,6 +3864,15 @@
"node": ">=8" "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": { "node_modules/hookable": {
"version": "5.5.3", "version": "5.5.3",
"resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
@@ -4216,7 +4033,6 @@
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/jsesc": { "node_modules/jsesc": {
@@ -4627,6 +4443,18 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/lru-cache": {
"version": "5.1.1", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "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" "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": { "node_modules/obug": {
"version": "2.1.4", "version": "2.1.4",
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz",
@@ -5019,6 +4856,18 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/optionator": {
"version": "0.9.4", "version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -5306,6 +5155,17 @@
"url": "https://github.com/prettier/prettier?sponsor=1" "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": { "node_modules/punycode": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -5353,6 +5213,12 @@
], ],
"license": "MIT" "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": { "node_modules/read-package-json-fast": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-6.0.0.tgz", "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" "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": { "node_modules/reusify": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
@@ -5967,6 +5851,15 @@
"url": "https://github.com/chalk/supports-color?sponsor=1" "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": { "node_modules/sync-child-process": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/sync-child-process/-/sync-child-process-1.0.2.tgz", "resolved": "https://registry.npmjs.org/sync-child-process/-/sync-child-process-1.0.2.tgz",
@@ -5990,6 +5883,15 @@
"node": ">=16.0.0" "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": { "node_modules/tiny-inflate": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
@@ -6061,11 +5963,36 @@
"typescript": ">=4.8.4" "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": { "node_modules/tslib": {
"version": "2.8.1", "version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"dev": true,
"license": "0BSD" "license": "0BSD"
}, },
"node_modules/type-check": { "node_modules/type-check": {
@@ -6089,38 +6016,17 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/typescript": { "node_modules/typescript": {
"version": "7.0.2", "version": "6.0.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
"integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
"tsc": "bin/tsc" "tsc": "bin/tsc",
"tsserver": "bin/tsserver"
}, },
"engines": { "engines": {
"node": ">=16.20.0" "node": ">=14.17"
},
"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_modules/typescript-eslint": { "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": { "node_modules/vue-eslint-parser": {
"version": "10.4.1", "version": "10.4.1",
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-10.4.1.tgz", "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" "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": { "node_modules/zod": {
"version": "4.4.3", "version": "4.4.3",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
+4 -1
View File
@@ -15,9 +15,12 @@
"format": "prettier --write --experimental-cli src/" "format": "prettier --write --experimental-cli src/"
}, },
"dependencies": { "dependencies": {
"@apollo/client": "^3.14.1",
"@fontsource/roboto": "^5.3.0", "@fontsource/roboto": "^5.3.0",
"@vue/apollo-composable": "^4.2.2",
"@vueuse/core": "^14.4.0", "@vueuse/core": "^14.4.0",
"better-auth": "^1.7.1", "better-auth": "^1.7.1",
"graphql": "^17.0.2",
"pinia": "^4.0.3", "pinia": "^4.0.3",
"vue": "^3.5.41", "vue": "^3.5.41",
"vue-router": "^5.2.0", "vue-router": "^5.2.0",
@@ -39,7 +42,7 @@
"oxlint": "~1.80.0", "oxlint": "~1.80.0",
"prettier": "3.9.6", "prettier": "3.9.6",
"sass-embedded": "^1.103.1", "sass-embedded": "^1.103.1",
"typescript": "~7.0.2", "typescript": "^6.0.3",
"unplugin-fonts": "^2.0.0", "unplugin-fonts": "^2.0.0",
"vite": "^8.2.2", "vite": "^8.2.2",
"vite-plugin-vue-devtools": "^8.2.1", "vite-plugin-vue-devtools": "^8.2.1",
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

+27 -17
View File
@@ -1,33 +1,43 @@
<script setup lang="ts"> <script setup lang="ts">
import { useEventSource } from '@vueuse/core' import { useEventSource } from '@vueuse/core'
import { computed } from 'vue' import { computed, onMounted } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { useStatusStore } from '@/stores/status.ts'
import { useIssueStore } from '@/stores/issue.ts'
//issue.created
const route = useRoute() const route = useRoute()
// const { status, data, error, close } = useEventSource( const { status, data, error, close } = useEventSource(
// 'https://event-source-url', 'http://localhost:3333/api/issues/events',
// ['notice', 'update'], ['notice', 'update', 'issue.created'],
// { autoReconnect: { {
// retries: 3, autoReconnect: {
// delay: 1000, retries: 3,
// onFailed() { delay: 1000,
// alert('Failed to connect EventSource after 3 retries') onFailed() {
// }, console.log('Failed to connect EventSource after 3 retries')
// }, autoConnect: false }, },
// ) },
// console.log(data, status, error, close) autoConnect: true,
},
)
console.log(data.value, status.value, error.value, close)
const layout = computed(() => { const layout = computed(() => {
const layout = route?.meta?.layout const layout = route?.meta?.layout
return layout ? `${layout}Layout` : 'DefaultLayout' return layout ? `${layout}Layout` : 'DefaultLayout'
}) })
const statusStore = useStatusStore()
const issueStore = useIssueStore()
onMounted(async () => {
await statusStore.list()
await issueStore.getStat()
})
</script> </script>
<template> <template>
<!-- <h1>You did it!</h1>-->
<!-- <router-view />-->
<component :is="layout"> <component :is="layout">
<router-view v-slot="{ Component, route }"> <router-view v-slot="{ Component, route }">
<div :key="route.name"> <div :key="route.name">
+194
View File
@@ -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
}
+36
View File
@@ -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
}
+2 -2
View File
@@ -1,9 +1,9 @@
/*@import "tailwindcss";*/ /*@import "tailwindcss";*/
html { html {
font-size: 16px; font-size: 15px;
font-weight: 400; font-weight: 400;
font-family: "Montserrat", sans-serif; font-family: 'Montserrat', sans-serif;
font-optical-sizing: auto; font-optical-sizing: auto;
font-style: normal; font-style: normal;
} }
+45
View File
@@ -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>
+33
View File
@@ -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>
+53
View File
@@ -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>
+58
View File
@@ -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>
+103
View File
@@ -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>
+12 -5
View File
@@ -9,18 +9,25 @@ async function onSearch() {
</script> </script>
<template> <template>
<div> <div class="u-grid2 ga-2">
<v-text-field <v-text-field
v-model="text" v-model="text"
clearable clearable
variant="outlined"
density="compact"
placeholder="Search name..." placeholder="Search name..."
hide-details
@keydown.enter.prevent="onSearch" @keydown.enter.prevent="onSearch"
@click:clear="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> </div>
</template> </template>
<style scoped lang="scss"></style> <style scoped lang="scss">
.u-grid2 {
display: grid;
grid-template-columns: repeat(4, 1fr);
}
</style>
+60
View File
@@ -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>
+30
View File
@@ -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>
+62
View File
@@ -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>
+63
View File
@@ -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>
+26 -15
View File
@@ -1,25 +1,28 @@
<script setup lang="ts"> <script setup lang="ts">
import { type Status, useStatusStore } from '@/stores/status.ts' import { useStatusStore } from '@/stores/status.ts'
import { ref, watchEffect } from 'vue' import { computed } from 'vue'
import type { Issue } from '@/api/issue.api.ts'
interface Props { interface Props {
value?: string value?: string
} }
const props = defineProps<Props>()
const emit = defineEmits<{ changed: [value: Issue['status']] }>()
const statusStore = useStatusStore() 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[]>([]) const currentStatusColor = computed(() => {
return statusStore.statuses?.find((el) => el.alias === props.value)?.color ?? 'white'
watchEffect(async () => {
statuses.value = await statusStore.list()
}) })
const select = (value: unknown) => { const select = (value: unknown) => {
if (Array.isArray(value)) { if (Array.isArray(value)) {
model.value = value[0] emit('changed', value[0])
} }
} }
</script> </script>
@@ -27,12 +30,19 @@ const select = (value: unknown) => {
<template> <template>
<div class="menu"> <div class="menu">
<v-menu> <v-menu>
<template v-slot:activator="{ props }"> <template v-slot:activator="{ props: activatorProps }">
<v-btn density="compact" v-bind="props" :text="model" /> <v-btn
slim
:color="currentStatusColor"
density="compact"
variant="flat"
v-bind="activatorProps"
:text="currentStatusName"
/>
</template> </template>
<v-list density="compact" select-strategy="single-leaf" @update:selected="select"> <v-list class="mt-1" select-strategy="single-leaf" density="compact" @update:selected="select">
<v-list-item v-for="el in statuses" :key="el.id" :value="el.name"> <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-title>{{ el.name }}</v-list-item-title>
</v-list-item> </v-list-item>
</v-list> </v-list>
@@ -42,7 +52,8 @@ const select = (value: unknown) => {
<style scoped lang="scss"> <style scoped lang="scss">
.menu { .menu {
width: 8rem; //width: 6rem;
overflow: hidden; //overflow: hidden;
//background: blue;
} }
</style> </style>
+38
View File
@@ -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>
+89 -21
View File
@@ -1,27 +1,68 @@
<script setup lang="ts"> <script setup lang="ts">
import { useRoute, useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import AppMenu from '@/components/AppMenu.vue' import { authClient } from '@/lib/auth-client.ts'
import { computed, ref } from 'vue'
import { useTheme } from 'vuetify'
const router = useRouter() const router = useRouter()
const session = authClient.useSession()
const theme = useTheme()
const title = 'Ulitka' const title = 'APP'
const version = '0.0.1' const version = '0.0.1'
function logout() { const isAuth = computed(() => !!session.value.data)
console.log('logout')
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() { function goToSignUp() {
router.push({ path: '/login' }) router.push({ path: '/sign-up' })
}
function goToRegister() {
router.push({ path: '/register' })
} }
function goToProfile() { function goToProfile() {
router.push({ path: '/profile' }) 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> </script>
<template> <template>
@@ -30,35 +71,62 @@ function goToProfile() {
<template v-slot:prepend> <template v-slot:prepend>
<v-menu> <v-menu>
<template v-slot:activator="{ props }"> <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> </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> </v-menu>
</template> </template>
<v-app-bar-title> <v-app-bar-title>
<div class="d-flex ga-2 align-center"> <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 class="text-label-small">ver: {{ version }}</span>-->
<span>v{{ version }}</span> <span>v{{ version }}</span>
</div> </div>
</v-app-bar-title> </v-app-bar-title>
<v-btn @click="goToLogin">Login</v-btn> <div class="mr-2" v-if="session.data">
<v-btn @click="goToRegister">Register</v-btn> 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-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 }"> <template v-slot:activator="{ props }">
<v-btn rounded="0" icon v-bind="props"> <v-btn icon v-bind="props">
<v-icon icon="mdi-account-outline" /> <v-icon icon="mdi-account" />
</v-btn> </v-btn>
</template> </template>
<v-list class="mt-1" width="200"> <v-list class="mt-1" width="200">
<v-list-item @click="goToProfile">Profile</v-list-item> <v-list-item @click="goToProfile">
<v-list-item @click="logout">Logout</v-list-item> <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-list>
</v-menu> </v-menu>
</v-app-bar> </v-app-bar>
-1
View File
@@ -15,6 +15,5 @@ export function registerLayouts(app: App<Element>) {
if (componentName) { if (componentName) {
app.component(componentName.replace('.vue', ''), component.default) app.component(componentName.replace('.vue', ''), component.default)
} }
}) })
} }
+6
View File
@@ -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(),
})
+6
View File
@@ -0,0 +1,6 @@
import { createAuthClient } from 'better-auth/vue'
export const authClient = createAuthClient({
baseURL: 'http://localhost:3333',
plugins: [],
})
+43 -2
View File
@@ -1,8 +1,10 @@
import { createApp } from 'vue' import { createApp } from 'vue'
import { createPinia } from 'pinia' import { createPinia } from 'pinia'
import { DefaultApolloClient } from '@vue/apollo-composable'
import App from './App.vue' import App from './App.vue'
import router from './router' import router from './router'
import { apolloClient } from '@/lib/apollo-client.ts'
import './assets/main.css' import './assets/main.css'
import 'unfonts.css' import 'unfonts.css'
@@ -18,15 +20,32 @@ const app = createApp(App)
app.use(createPinia()) app.use(createPinia())
app.use(router) app.use(router)
app.provide(DefaultApolloClient, apolloClient)
const vuetify = createVuetify({ const vuetify = createVuetify({
components, components,
directives, directives,
theme: { 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: { defaults: {
global: {}, global: {
rounded: '0',
},
VBtn: { VBtn: {
variant: 'tonal', variant: 'tonal',
}, },
@@ -35,6 +54,28 @@ const vuetify = createVuetify({
density: 'compact', density: 'compact',
hideDetails: true, 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,
},
}, },
}) })
-29
View File
@@ -14,35 +14,6 @@ export const statusListMock: Status[] = [
{ id: '5', name: 'canceled' }, { 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 = [ export const type = [
{ id: '1', name: 'type one' }, { id: '1', name: 'type one' },
{ id: '2', name: 'type two' }, { id: '2', name: 'type two' },
+13
View File
@@ -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,
},
],
}
+38 -7
View File
@@ -1,11 +1,14 @@
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import LoginView from '@/views/LoginView.vue' import ProfileView from '@/views/users/ProfileView.vue'
import ProfileView from '@/views/ProfileView.vue'
import { issuesRoutes } from '@/router/issues.ts' import { issuesRoutes } from '@/router/issues.ts'
import { reportsRoutes } from '@/router/reports.ts' import { reportsRoutes } from '@/router/reports.ts'
import { summaryRoutes } from '@/router/summary.ts' import { summaryRoutes } from '@/router/summary.ts'
import RegisterView from '@/views/RegisterView.vue'
import HomeView from '@/views/HomeView.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' { declare module 'vue-router' {
interface RouteMeta { interface RouteMeta {
@@ -22,21 +25,49 @@ const router = createRouter({
meta: { layout: 'Default' }, meta: { layout: 'Default' },
}, },
{ {
path: '/login', name: 'sign-in',
component: LoginView, path: '/sign-in',
component: SignInView,
}, },
{ {
path: '/register', path: '/sign-up',
component: RegisterView, component: SignUpView,
},
{
path: '/verify-email',
component: VerifyEmailView,
}, },
{ {
path: '/profile', path: '/profile',
component: ProfileView, component: ProfileView,
meta: { requiresAuth: true },
}, },
issuesRoutes, issuesRoutes,
summaryRoutes, summaryRoutes,
reportsRoutes, 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 export default router
+14 -2
View File
@@ -1,7 +1,19 @@
import type { RouteRecordRaw } from 'vue-router' 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 = { export const issuesRoutes: RouteRecordRaw = {
path: '/issues', path: '/issues',
component: IssuesView meta: { requiresAuth: true },
children: [
{
path: '',
component: IssuesView,
},
{
path: ':id',
component: IssueView,
props: true,
},
],
} }
+2 -1
View File
@@ -1,7 +1,8 @@
import type { RouteRecordRaw } from 'vue-router' import type { RouteRecordRaw } from 'vue-router'
import ReportsView from '@/views/ReportsView.vue' import ReportsView from '@/views/reports/ReportsView.vue'
export const reportsRoutes: RouteRecordRaw = { export const reportsRoutes: RouteRecordRaw = {
path: '/reports', path: '/reports',
component: ReportsView, component: ReportsView,
meta: { requiresAuth: true },
} }
+2 -1
View File
@@ -1,7 +1,8 @@
import type { RouteRecordRaw } from 'vue-router' import type { RouteRecordRaw } from 'vue-router'
import SummaryView from '@/views/SummaryView.vue' import SummaryView from '@/views/summary/SummaryView.vue'
export const summaryRoutes: RouteRecordRaw = { export const summaryRoutes: RouteRecordRaw = {
path: '/summary', path: '/summary',
component: SummaryView, component: SummaryView,
meta: { requiresAuth: true },
} }
-12
View File
@@ -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 }
})
+68 -32
View File
@@ -1,38 +1,74 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { computed, ref } from 'vue' import { apolloClient } from '@/lib/apollo-client.ts'
import { issueListMock } from '@/mocks.ts' import { ref } from 'vue'
import {
export interface Item { type CreateIssueInput,
id: string type Issue,
key: string itemCreate,
title: string itemGetByKey,
text: string itemsList,
status: string itemStat,
priority: number itemUpdate,
assignee: string type IssueStat,
reporter: string type Meta,
account_id: string type UpdateIssueInput,
created_at: string } from '@/api/issue.api.ts'
tags: string[]
}
export const useIssueStore = defineStore('item', () => { 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 }) { async function list(params: { page: number; limit: number; text?: string | null }) {
const { page, itemsPerPage, text } = params try {
const res = await itemsList(params)
return new Promise<{ items: Item[]; total: number }>((resolve) => { issues.value = res.list
const start = (page - 1) * itemsPerPage return res
const end = start + itemsPerPage } catch (e) {
const items = load.value.slice() error.value = e instanceof Error ? e.message : 'error get list of items'
return { list: [], meta: lastMeta.value }
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 })
})
} }
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 }
}) })
+6 -12
View File
@@ -1,19 +1,13 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { computed, ref } from 'vue' import { ref } from 'vue'
import { statusListMock } from '@/mocks.ts' import { type Status, statusesList } from '@/api/status.api.ts'
export interface Status {
id: string
name: string
}
export const useStatusStore = defineStore('counter', () => { export const useStatusStore = defineStore('counter', () => {
const statuses = ref<Status[]>(statusListMock) const statuses = ref<Status[]>()
async function list(): Promise<Status[]> { async function list() {
console.log('useStatusStore.list') statuses.value = await statusesList()
return Promise.resolve(statuses.value)
} }
return { list } return { statuses, list }
}) })
+3 -1
View File
@@ -1,5 +1,7 @@
<script setup lang="ts"></script> <script setup lang="ts"></script>
<template>test</template> <template>
<div>Home</div>
</template>
<style scoped lang="scss"></style> <style scoped lang="scss"></style>
+33
View File
@@ -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>
+185
View File
@@ -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>
-112
View File
@@ -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>
-18
View File
@@ -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>
-18
View File
@@ -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> <script setup lang="ts"></script>
<template>profile</template> <template>
<div>Games</div>
</template>
<style scoped lang="scss"></style> <style scoped lang="scss"></style>
+115
View File
@@ -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>
+22
View File
@@ -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>
+44
View File
@@ -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>
+45
View File
@@ -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>
+35
View File
@@ -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>
+10 -3
View File
@@ -2,15 +2,22 @@ import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue' 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' import ViteFonts from 'unplugin-fonts/vite'
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
server: { port: 3333 }, server: {
port: 3333,
strictPort: true,
proxy: {
'/api': 'http://localhost:3000',
'/graphql': 'http://localhost:3000',
},
},
plugins: [ plugins: [
vue(), vue(),
vueDevTools(), // vueDevTools(),
ViteFonts({ ViteFonts({
fontsource: { fontsource: {
families: [ families: [