This commit is contained in:
+7
-2
@@ -528,7 +528,12 @@
|
|||||||
"of": "of",
|
"of": "of",
|
||||||
"items-per-page": "Items per page",
|
"items-per-page": "Items per page",
|
||||||
"showing": "Showing",
|
"showing": "Showing",
|
||||||
"results": "results"
|
"results": "results",
|
||||||
|
"SUCCESS_USER_STATUS_UPDATED": "User status updated successfully.",
|
||||||
|
"SUCCESS_USER_ROLE_UPDATED": "User role updated successfully.",
|
||||||
|
"SUCCESS_USER_DELETED": "User deleted successfully.",
|
||||||
|
"SUCCESS_USER_LIMITS_RESET": "User limits reset successfully.",
|
||||||
|
"SUCCESS_USER_SUBSCRIPTION_UPDATED": "User subscription updated successfully."
|
||||||
},
|
},
|
||||||
"seo": {
|
"seo": {
|
||||||
"global": {
|
"global": {
|
||||||
@@ -684,4 +689,4 @@
|
|||||||
"remaining": "remaining"
|
"remaining": "remaining"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+7
-2
@@ -518,7 +518,12 @@
|
|||||||
"of": "/",
|
"of": "/",
|
||||||
"items-per-page": "Sayfa başına öğe",
|
"items-per-page": "Sayfa başına öğe",
|
||||||
"showing": "Gösterilen",
|
"showing": "Gösterilen",
|
||||||
"results": "sonuç"
|
"results": "sonuç",
|
||||||
|
"SUCCESS_USER_STATUS_UPDATED": "Kullanıcı durumu başarıyla güncellendi.",
|
||||||
|
"SUCCESS_USER_ROLE_UPDATED": "Kullanıcı rolü başarıyla güncellendi.",
|
||||||
|
"SUCCESS_USER_DELETED": "Kullanıcı başarıyla silindi.",
|
||||||
|
"SUCCESS_USER_LIMITS_RESET": "Kullanıcı limitleri başarıyla sıfırlandı.",
|
||||||
|
"SUCCESS_USER_SUBSCRIPTION_UPDATED": "Kullanıcı aboneliği başarıyla güncellendi."
|
||||||
},
|
},
|
||||||
"seo": {
|
"seo": {
|
||||||
"global": {
|
"global": {
|
||||||
@@ -674,4 +679,4 @@
|
|||||||
"remaining": "kalan"
|
"remaining": "kalan"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Generated
+25
-3
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "Suggest-Bet-FE-v2",
|
"name": "iddaai-fe",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "Suggest-Bet-FE-v2",
|
"name": "iddaai-fe",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chakra-ui/react": "^3.28.0",
|
"@chakra-ui/react": "^3.28.0",
|
||||||
@@ -149,6 +149,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
|
||||||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.29.0",
|
"@babel/code-frame": "^7.29.0",
|
||||||
"@babel/generator": "^7.29.0",
|
"@babel/generator": "^7.29.0",
|
||||||
@@ -517,6 +518,7 @@
|
|||||||
"version": "3.33.0",
|
"version": "3.33.0",
|
||||||
"resolved": "https://registry.npmjs.org/@chakra-ui/react/-/react-3.33.0.tgz",
|
"resolved": "https://registry.npmjs.org/@chakra-ui/react/-/react-3.33.0.tgz",
|
||||||
"integrity": "sha512-HNbUFsFABjVL5IHBxsqtuT+AH/vQT1+xsEWrxnG0GBM2VjlzlMqlqCxNiDyQOsjLZXQC1ciCMbzPNcSCc63Y9w==",
|
"integrity": "sha512-HNbUFsFABjVL5IHBxsqtuT+AH/vQT1+xsEWrxnG0GBM2VjlzlMqlqCxNiDyQOsjLZXQC1ciCMbzPNcSCc63Y9w==",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ark-ui/react": "^5.31.0",
|
"@ark-ui/react": "^5.31.0",
|
||||||
"@emotion/is-prop-valid": "^1.4.0",
|
"@emotion/is-prop-valid": "^1.4.0",
|
||||||
@@ -627,6 +629,7 @@
|
|||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz",
|
||||||
"integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==",
|
"integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/memoize": "^0.9.0"
|
"@emotion/memoize": "^0.9.0"
|
||||||
}
|
}
|
||||||
@@ -640,6 +643,7 @@
|
|||||||
"version": "11.14.0",
|
"version": "11.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
|
||||||
"integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
|
"integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.18.3",
|
"@babel/runtime": "^7.18.3",
|
||||||
"@emotion/babel-plugin": "^11.13.5",
|
"@emotion/babel-plugin": "^11.13.5",
|
||||||
@@ -1830,6 +1834,7 @@
|
|||||||
"version": "3.10.0",
|
"version": "3.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.10.0.tgz",
|
||||||
"integrity": "sha512-oxDR/NTEJ1k+UFVQElaNIk65E/Z83HK1z1WI3lQyhTtnNg4R5oVXaPzK3jcpKG8UHKDVuDQHzn+wsxSz8RP3aw==",
|
"integrity": "sha512-oxDR/NTEJ1k+UFVQElaNIk65E/Z83HK1z1WI3lQyhTtnNg4R5oVXaPzK3jcpKG8UHKDVuDQHzn+wsxSz8RP3aw==",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@swc/helpers": "^0.5.0"
|
"@swc/helpers": "^0.5.0"
|
||||||
}
|
}
|
||||||
@@ -2092,7 +2097,8 @@
|
|||||||
"node_modules/@paddle/paddle-js": {
|
"node_modules/@paddle/paddle-js": {
|
||||||
"version": "1.6.4",
|
"version": "1.6.4",
|
||||||
"resolved": "https://registry.npmjs.org/@paddle/paddle-js/-/paddle-js-1.6.4.tgz",
|
"resolved": "https://registry.npmjs.org/@paddle/paddle-js/-/paddle-js-1.6.4.tgz",
|
||||||
"integrity": "sha512-ncfnS6I8mCX6krZ3Sgz2iAYivGmhdI81yt9mT6prtPj4Ipd9J3M12LCJRUFL4FB7BYeeuV04c33RSEnbZUBCaA=="
|
"integrity": "sha512-ncfnS6I8mCX6krZ3Sgz2iAYivGmhdI81yt9mT6prtPj4Ipd9J3M12LCJRUFL4FB7BYeeuV04c33RSEnbZUBCaA==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/@pandacss/is-valid-prop": {
|
"node_modules/@pandacss/is-valid-prop": {
|
||||||
"version": "1.8.1",
|
"version": "1.8.1",
|
||||||
@@ -2686,6 +2692,7 @@
|
|||||||
"version": "0.5.18",
|
"version": "0.5.18",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.18.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.18.tgz",
|
||||||
"integrity": "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==",
|
"integrity": "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.8.0"
|
"tslib": "^2.8.0"
|
||||||
}
|
}
|
||||||
@@ -2837,6 +2844,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
|
||||||
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
|
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
}
|
}
|
||||||
@@ -2892,6 +2900,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.0.tgz",
|
||||||
"integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==",
|
"integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.56.0",
|
"@typescript-eslint/scope-manager": "8.56.0",
|
||||||
"@typescript-eslint/types": "8.56.0",
|
"@typescript-eslint/types": "8.56.0",
|
||||||
@@ -4226,6 +4235,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -4562,6 +4572,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-1.0.0.tgz",
|
||||||
"integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==",
|
"integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/types": "^7.26.0"
|
"@babel/types": "^7.26.0"
|
||||||
}
|
}
|
||||||
@@ -4647,6 +4658,7 @@
|
|||||||
"url": "https://github.com/sponsors/ai"
|
"url": "https://github.com/sponsors/ai"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.9.0",
|
"baseline-browser-mapping": "^2.9.0",
|
||||||
"caniuse-lite": "^1.0.30001759",
|
"caniuse-lite": "^1.0.30001759",
|
||||||
@@ -5355,6 +5367,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz",
|
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz",
|
||||||
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.8.0",
|
"@eslint-community/eslint-utils": "^4.8.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
@@ -5547,6 +5560,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz",
|
||||||
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@rtsao/scc": "^1.1.0",
|
"@rtsao/scc": "^1.1.0",
|
||||||
"array-includes": "^3.1.9",
|
"array-includes": "^3.1.9",
|
||||||
@@ -7415,6 +7429,7 @@
|
|||||||
"version": "16.2.5",
|
"version": "16.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/next/-/next-16.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/next/-/next-16.2.5.tgz",
|
||||||
"integrity": "sha512-TkVTm9F2WEulkgGljm4wPwNgvCCWCVw6StUHsZb8WZpHFRjepoUWg3d7L4IMg7IyjcJ4Co9eVhpro8e8O+KarQ==",
|
"integrity": "sha512-TkVTm9F2WEulkgGljm4wPwNgvCCWCVw6StUHsZb8WZpHFRjepoUWg3d7L4IMg7IyjcJ4Co9eVhpro8e8O+KarQ==",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@next/env": "16.2.5",
|
"@next/env": "16.2.5",
|
||||||
"@swc/helpers": "0.5.15",
|
"@swc/helpers": "0.5.15",
|
||||||
@@ -8080,6 +8095,7 @@
|
|||||||
"version": "10.28.3",
|
"version": "10.28.3",
|
||||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.28.3.tgz",
|
"resolved": "https://registry.npmjs.org/preact/-/preact-10.28.3.tgz",
|
||||||
"integrity": "sha512-tCmoRkPQLpBeWzpmbhryairGnhW9tKV6c6gr/w+RhoRoKEJwsjzipwp//1oCpGPOchvSLaAPlpcJi9MwMmoPyA==",
|
"integrity": "sha512-tCmoRkPQLpBeWzpmbhryairGnhW9tKV6c6gr/w+RhoRoKEJwsjzipwp//1oCpGPOchvSLaAPlpcJi9MwMmoPyA==",
|
||||||
|
"peer": true,
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
"url": "https://opencollective.com/preact"
|
"url": "https://opencollective.com/preact"
|
||||||
@@ -8217,6 +8233,7 @@
|
|||||||
"version": "19.2.0",
|
"version": "19.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
||||||
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
|
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -8225,6 +8242,7 @@
|
|||||||
"version": "19.2.0",
|
"version": "19.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
|
||||||
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
|
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.27.0"
|
"scheduler": "^0.27.0"
|
||||||
},
|
},
|
||||||
@@ -8236,6 +8254,7 @@
|
|||||||
"version": "7.71.1",
|
"version": "7.71.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.1.tgz",
|
||||||
"integrity": "sha512-9SUJKCGKo8HUSsCO+y0CtqkqI5nNuaDqTxyqPsZPqIwudpj4rCrAz/jZV+jn57bx5gtZKOh3neQu94DXMc+w5w==",
|
"integrity": "sha512-9SUJKCGKo8HUSsCO+y0CtqkqI5nNuaDqTxyqPsZPqIwudpj4rCrAz/jZV+jn57bx5gtZKOh3neQu94DXMc+w5w==",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
},
|
},
|
||||||
@@ -9086,6 +9105,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -9253,6 +9273,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -9684,6 +9705,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"peer": true,
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "Suggest-Bet-FE-v2",
|
"name": "iddaai-fe",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export async function generateMetadata(props: {
|
|||||||
tr: `${siteUrl}/tr/${pathSegment}`,
|
tr: `${siteUrl}/tr/${pathSegment}`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export async function generateMetadata(props: {
|
|||||||
tr: `${siteUrl}/tr/${pathSegment}`,
|
tr: `${siteUrl}/tr/${pathSegment}`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ export async function generateMetadata(props: {
|
|||||||
const pathSegment = "matches";
|
const pathSegment = "matches";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
||||||
|
|
||||||
title: t("matches.title"),
|
title: t("matches.title"),
|
||||||
description: t("matches.description"),
|
description: t("matches.description"),
|
||||||
alternates: {
|
alternates: {
|
||||||
@@ -26,6 +28,7 @@ export async function generateMetadata(props: {
|
|||||||
tr: `${siteUrl}/tr/${pathSegment}`,
|
tr: `${siteUrl}/tr/${pathSegment}`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export async function generateMetadata(props: {
|
|||||||
tr: `${siteUrl}/tr/${pathSegment}`,
|
tr: `${siteUrl}/tr/${pathSegment}`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export async function generateMetadata(props: {
|
|||||||
tr: `${siteUrl}/tr/${pathSegment}`,
|
tr: `${siteUrl}/tr/${pathSegment}`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export async function generateMetadata(props: {
|
|||||||
tr: `${siteUrl}/tr/${pathSegment}`,
|
tr: `${siteUrl}/tr/${pathSegment}`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/* ═══════════════════════════════════════════════════════
|
/* ═══════════════════════════════════════════════════════
|
||||||
Suggest-Bet — Global CSS
|
iddaai — Global CSS
|
||||||
Premium animations, gradients, and utility keyframes
|
Premium animations, gradients, and utility keyframes
|
||||||
═══════════════════════════════════════════════════════ */
|
═══════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ export default async function RootLayout({
|
|||||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(orgJsonLd) }}
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(orgJsonLd) }}
|
||||||
/>
|
/>
|
||||||
</head>
|
</head>
|
||||||
<body className={bricolage.variable}>
|
<body className={bricolage.variable} suppressHydrationWarning>
|
||||||
<NextIntlClientProvider>
|
<NextIntlClientProvider>
|
||||||
<Provider>{children}</Provider>
|
<Provider>{children}</Provider>
|
||||||
</NextIntlClientProvider>
|
</NextIntlClientProvider>
|
||||||
|
|||||||
@@ -13,8 +13,10 @@ import {
|
|||||||
Spinner,
|
Spinner,
|
||||||
Button,
|
Button,
|
||||||
Separator,
|
Separator,
|
||||||
|
Input,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { useTranslations } from "next-intl";
|
import { NativeSelectRoot, NativeSelectField } from "@/components/ui/forms/native-select";
|
||||||
|
import { useTranslations, useFormatter } from "next-intl";
|
||||||
import { useColorModeValue } from "@/components/ui/color-mode";
|
import { useColorModeValue } from "@/components/ui/color-mode";
|
||||||
import {
|
import {
|
||||||
SlideUp,
|
SlideUp,
|
||||||
@@ -25,9 +27,10 @@ import {
|
|||||||
import { useAdminAnalytics, useAdminUsers } from "@/lib/api/admin/use-hooks";
|
import { useAdminAnalytics, useAdminUsers } from "@/lib/api/admin/use-hooks";
|
||||||
import type { AdminUserDto, AnalyticsOverviewDto } from "@/lib/api/admin/types";
|
import type { AdminUserDto, AnalyticsOverviewDto } from "@/lib/api/admin/types";
|
||||||
import { formatRoleLabel, isAdminRole } from "@/lib/auth/roles";
|
import { formatRoleLabel, isAdminRole } from "@/lib/auth/roles";
|
||||||
import { LuUsers, LuChartBar, LuActivity, LuShield } from "react-icons/lu";
|
import { LuUsers, LuChartBar, LuActivity, LuShield, LuPencil } from "react-icons/lu";
|
||||||
import { useState } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
|
import { EditUserModal } from "./edit-user-modal";
|
||||||
|
|
||||||
type AdminTab = "overview" | "users";
|
type AdminTab = "overview" | "users";
|
||||||
|
|
||||||
@@ -82,7 +85,20 @@ function AdminStat({ label, value, icon, colorPalette }: AdminStatProps) {
|
|||||||
export default function AdminContent() {
|
export default function AdminContent() {
|
||||||
const t = useTranslations("admin");
|
const t = useTranslations("admin");
|
||||||
const tCommon = useTranslations("common");
|
const tCommon = useTranslations("common");
|
||||||
|
const format = useFormatter();
|
||||||
const [activeTab, setActiveTab] = useState<AdminTab>("overview");
|
const [activeTab, setActiveTab] = useState<AdminTab>("overview");
|
||||||
|
const [editingUser, setEditingUser] = useState<AdminUserDto | null>(null);
|
||||||
|
const [searchParams, setSearchParams] = useState({ search: "", role: "", subscriptionStatus: "", page: 1, limit: 10 });
|
||||||
|
const [debouncedSearch, setDebouncedSearch] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = setTimeout(() => {
|
||||||
|
setDebouncedSearch(searchParams.search);
|
||||||
|
setSearchParams(prev => ({ ...prev, page: 1 }));
|
||||||
|
}, 500);
|
||||||
|
return () => clearTimeout(handler);
|
||||||
|
}, [searchParams.search]);
|
||||||
|
|
||||||
const { data: session, status } = useSession();
|
const { data: session, status } = useSession();
|
||||||
|
|
||||||
const cardBg = useColorModeValue("white", "gray.800");
|
const cardBg = useColorModeValue("white", "gray.800");
|
||||||
@@ -92,12 +108,19 @@ export default function AdminContent() {
|
|||||||
const { data: analyticsData, isLoading: analyticsLoading } =
|
const { data: analyticsData, isLoading: analyticsLoading } =
|
||||||
useAdminAnalytics(canAccessAdmin);
|
useAdminAnalytics(canAccessAdmin);
|
||||||
const { data: usersData, isLoading: usersLoading } = useAdminUsers(
|
const { data: usersData, isLoading: usersLoading } = useAdminUsers(
|
||||||
undefined,
|
{
|
||||||
|
search: debouncedSearch,
|
||||||
|
role: searchParams.role,
|
||||||
|
subscriptionStatus: searchParams.subscriptionStatus,
|
||||||
|
page: searchParams.page,
|
||||||
|
limit: searchParams.limit
|
||||||
|
},
|
||||||
canAccessAdmin,
|
canAccessAdmin,
|
||||||
);
|
);
|
||||||
|
|
||||||
const analytics = analyticsData?.data as AnalyticsOverviewDto | undefined;
|
const analytics = analyticsData?.data as AnalyticsOverviewDto | undefined;
|
||||||
const users = usersData?.data?.items ?? [];
|
const users = usersData?.data?.items ?? [];
|
||||||
|
const meta = usersData?.data?.meta;
|
||||||
|
|
||||||
const tabs: { key: AdminTab; label: string }[] = [
|
const tabs: { key: AdminTab; label: string }[] = [
|
||||||
{ key: "overview", label: t("overview") },
|
{ key: "overview", label: t("overview") },
|
||||||
@@ -242,113 +265,215 @@ export default function AdminContent() {
|
|||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Users Tab */}
|
{/* Users Tab */}
|
||||||
{activeTab === "users" &&
|
{activeTab === "users" && (
|
||||||
(usersLoading ? (
|
<VStack gap={4} align="stretch">
|
||||||
<Flex justify="center" py={16}>
|
{/* Filters */}
|
||||||
<Spinner size="lg" color="primary.500" />
|
|
||||||
</Flex>
|
|
||||||
) : users.length > 0 ? (
|
|
||||||
<Card.Root bg={cardBg} borderColor={borderColor} borderRadius="xl">
|
<Card.Root bg={cardBg} borderColor={borderColor} borderRadius="xl">
|
||||||
<Card.Body>
|
<Card.Body py={4}>
|
||||||
<VStack gap={0} align="stretch">
|
<SimpleGrid columns={{ base: 1, md: 3 }} gap={4}>
|
||||||
{/* Table Header */}
|
<Input
|
||||||
<Flex
|
placeholder="E-posta veya isim ara..."
|
||||||
px={4}
|
value={searchParams.search}
|
||||||
py={2}
|
onChange={(e) => setSearchParams({ ...searchParams, search: e.target.value })}
|
||||||
bg="bg.muted"
|
/>
|
||||||
borderRadius="lg"
|
<NativeSelectRoot>
|
||||||
mb={2}
|
<NativeSelectField
|
||||||
fontWeight="semibold"
|
placeholder="Tüm Rolleri Gör"
|
||||||
fontSize="xs"
|
value={searchParams.role}
|
||||||
color="fg.muted"
|
onChange={(e) => setSearchParams({ ...searchParams, role: e.target.value, page: 1 })}
|
||||||
>
|
items={[
|
||||||
<Text flex={2}>{t("user-name")}</Text>
|
{ label: "Standart Kullanıcı", value: "user" },
|
||||||
<Text flex={2}>{t("user-email")}</Text>
|
{ label: "Admin", value: "superadmin" }
|
||||||
<Text flex={1} textAlign="center">
|
]}
|
||||||
{t("user-role")}
|
/>
|
||||||
</Text>
|
</NativeSelectRoot>
|
||||||
<Text flex={1} textAlign="center">
|
<NativeSelectRoot>
|
||||||
{t("subscription", { fallback: "Subscription" })}
|
<NativeSelectField
|
||||||
</Text>
|
placeholder="Tüm Paketleri Gör"
|
||||||
<Text flex={1} textAlign="center">
|
value={searchParams.subscriptionStatus}
|
||||||
{t("user-status")}
|
onChange={(e) => setSearchParams({ ...searchParams, subscriptionStatus: e.target.value, page: 1 })}
|
||||||
</Text>
|
items={[
|
||||||
</Flex>
|
{ label: "Ücretsiz (Free)", value: "free" },
|
||||||
|
{ label: "Plus", value: "plus" },
|
||||||
{/* User Rows */}
|
{ label: "Premium", value: "premium" },
|
||||||
{users.map((user: AdminUserDto, idx: number) => (
|
{ label: "Gecikmiş", value: "past_due" },
|
||||||
<Box key={user.id ?? idx}>
|
{ label: "İptal", value: "cancelled" }
|
||||||
{idx > 0 && <Separator />}
|
]}
|
||||||
<Flex
|
/>
|
||||||
px={4}
|
</NativeSelectRoot>
|
||||||
py={3}
|
</SimpleGrid>
|
||||||
align="center"
|
|
||||||
_hover={{ bg: "bg.muted" }}
|
|
||||||
borderRadius="lg"
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
flex={2}
|
|
||||||
fontSize="sm"
|
|
||||||
fontWeight="medium"
|
|
||||||
truncate
|
|
||||||
>
|
|
||||||
{getUserDisplayName(user)}
|
|
||||||
</Text>
|
|
||||||
<Text flex={2} fontSize="sm" color="fg.muted" truncate>
|
|
||||||
{user.email}
|
|
||||||
</Text>
|
|
||||||
<Flex flex={1} justify="center">
|
|
||||||
<Badge
|
|
||||||
colorPalette={
|
|
||||||
isAdminRole([user.role]) ? "red" : "gray"
|
|
||||||
}
|
|
||||||
variant="subtle"
|
|
||||||
fontSize="2xs"
|
|
||||||
borderRadius="full"
|
|
||||||
>
|
|
||||||
{formatRoleLabel(user.role)}
|
|
||||||
</Badge>
|
|
||||||
</Flex>
|
|
||||||
<Flex flex={1} justify="center">
|
|
||||||
<Badge
|
|
||||||
colorPalette={
|
|
||||||
user.subscriptionStatus === "premium"
|
|
||||||
? "purple"
|
|
||||||
: user.subscriptionStatus === "plus"
|
|
||||||
? "blue"
|
|
||||||
: "gray"
|
|
||||||
}
|
|
||||||
variant="subtle"
|
|
||||||
fontSize="2xs"
|
|
||||||
borderRadius="full"
|
|
||||||
textTransform="capitalize"
|
|
||||||
>
|
|
||||||
{user.subscriptionStatus || "free"}
|
|
||||||
</Badge>
|
|
||||||
</Flex>
|
|
||||||
<Flex flex={1} justify="center">
|
|
||||||
<Badge
|
|
||||||
colorPalette={user.isActive ? "green" : "gray"}
|
|
||||||
variant="subtle"
|
|
||||||
fontSize="2xs"
|
|
||||||
borderRadius="full"
|
|
||||||
>
|
|
||||||
{user.isActive
|
|
||||||
? tCommon("active")
|
|
||||||
: tCommon("inactive")}
|
|
||||||
</Badge>
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
</Box>
|
|
||||||
))}
|
|
||||||
</VStack>
|
|
||||||
</Card.Body>
|
</Card.Body>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
) : (
|
|
||||||
<Flex justify="center" py={16}>
|
{usersLoading ? (
|
||||||
<Text color="fg.muted">{t("no-users")}</Text>
|
<Flex justify="center" py={16}>
|
||||||
</Flex>
|
<Spinner size="lg" color="primary.500" />
|
||||||
))}
|
</Flex>
|
||||||
|
) : users.length > 0 ? (
|
||||||
|
<Card.Root bg={cardBg} borderColor={borderColor} borderRadius="xl">
|
||||||
|
<Card.Body>
|
||||||
|
<VStack gap={0} align="stretch">
|
||||||
|
{/* Table Header */}
|
||||||
|
<Flex
|
||||||
|
px={4}
|
||||||
|
py={2}
|
||||||
|
bg="bg.muted"
|
||||||
|
borderRadius="lg"
|
||||||
|
mb={2}
|
||||||
|
fontWeight="semibold"
|
||||||
|
fontSize="xs"
|
||||||
|
color="fg.muted"
|
||||||
|
>
|
||||||
|
<Text flex={2}>{t("user-name")}</Text>
|
||||||
|
<Text flex={2}>{t("user-email")}</Text>
|
||||||
|
<Text flex={1} textAlign="center">
|
||||||
|
{t("user-role")}
|
||||||
|
</Text>
|
||||||
|
<Text flex={1} textAlign="center">
|
||||||
|
{t("subscription", { fallback: "Subscription" })}
|
||||||
|
</Text>
|
||||||
|
<Text flex={1} textAlign="center">
|
||||||
|
{t("user-status")}
|
||||||
|
</Text>
|
||||||
|
<Text width="40px" textAlign="center"></Text>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
{/* User Rows */}
|
||||||
|
{users.map((user: AdminUserDto, idx: number) => (
|
||||||
|
<Box key={user.id ?? idx}>
|
||||||
|
{idx > 0 && <Separator />}
|
||||||
|
<Flex
|
||||||
|
px={4}
|
||||||
|
py={3}
|
||||||
|
align="center"
|
||||||
|
_hover={{ bg: "bg.muted" }}
|
||||||
|
borderRadius="lg"
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
flex={2}
|
||||||
|
fontSize="sm"
|
||||||
|
fontWeight="medium"
|
||||||
|
truncate
|
||||||
|
>
|
||||||
|
{getUserDisplayName(user)}
|
||||||
|
</Text>
|
||||||
|
<Text flex={2} fontSize="sm" color="fg.muted" truncate>
|
||||||
|
{user.email}
|
||||||
|
</Text>
|
||||||
|
<Flex flex={1} justify="center">
|
||||||
|
<Badge
|
||||||
|
colorPalette={
|
||||||
|
isAdminRole([user.role]) ? "red" : "gray"
|
||||||
|
}
|
||||||
|
variant="subtle"
|
||||||
|
fontSize="2xs"
|
||||||
|
borderRadius="full"
|
||||||
|
>
|
||||||
|
{formatRoleLabel(user.role)}
|
||||||
|
</Badge>
|
||||||
|
</Flex>
|
||||||
|
<Flex flex={1} justify="center" direction="column" align="center" gap={1}>
|
||||||
|
<Badge
|
||||||
|
colorPalette={user.subscriptionStatus === "premium" || user.subscriptionStatus === "plus" ? "purple" : "gray"}
|
||||||
|
variant="subtle"
|
||||||
|
fontSize="2xs"
|
||||||
|
borderRadius="full"
|
||||||
|
textTransform="capitalize"
|
||||||
|
>
|
||||||
|
{user.subscriptionStatus || "free"}
|
||||||
|
</Badge>
|
||||||
|
{user.subscriptionExpiresAt ? (
|
||||||
|
<Text fontSize="2xs" color="fg.muted">
|
||||||
|
{format.dateTime(new Date(user.subscriptionExpiresAt), { year: 'numeric', month: '2-digit', day: '2-digit' })}
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<Text fontSize="2xs" color="fg.muted">
|
||||||
|
-
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
<Flex flex={1} justify="center">
|
||||||
|
<Badge
|
||||||
|
colorPalette={
|
||||||
|
user.subscriptionStatus === "premium"
|
||||||
|
? "purple"
|
||||||
|
: user.subscriptionStatus === "plus"
|
||||||
|
? "blue"
|
||||||
|
: "gray"
|
||||||
|
}
|
||||||
|
variant="subtle"
|
||||||
|
fontSize="2xs"
|
||||||
|
borderRadius="full"
|
||||||
|
textTransform="capitalize"
|
||||||
|
>
|
||||||
|
{user.subscriptionStatus || "free"}
|
||||||
|
</Badge>
|
||||||
|
</Flex>
|
||||||
|
<Flex flex={1} justify="center">
|
||||||
|
<Badge
|
||||||
|
colorPalette={user.isActive ? "green" : "gray"}
|
||||||
|
variant="subtle"
|
||||||
|
fontSize="2xs"
|
||||||
|
borderRadius="full"
|
||||||
|
>
|
||||||
|
{user.isActive
|
||||||
|
? tCommon("active")
|
||||||
|
: tCommon("inactive")}
|
||||||
|
</Badge>
|
||||||
|
</Flex>
|
||||||
|
<Flex width="40px" justify="center">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => setEditingUser(user)}
|
||||||
|
>
|
||||||
|
<LuPencil />
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Pagination */}
|
||||||
|
{meta && meta.totalPages > 1 && (
|
||||||
|
<Flex justify="center" pt={4} pb={2} gap={2} borderTopWidth="1px" borderColor={borderColor} mt={2}>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
disabled={!meta.hasPreviousPage}
|
||||||
|
onClick={() => setSearchParams({ ...searchParams, page: meta.page - 1 })}
|
||||||
|
>
|
||||||
|
Önceki
|
||||||
|
</Button>
|
||||||
|
<Flex align="center" gap={2} fontSize="sm">
|
||||||
|
<Text>Sayfa {meta.page} / {meta.totalPages}</Text>
|
||||||
|
</Flex>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
disabled={!meta.hasNextPage}
|
||||||
|
onClick={() => setSearchParams({ ...searchParams, page: meta.page + 1 })}
|
||||||
|
>
|
||||||
|
Sonraki
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</VStack>
|
||||||
|
</Card.Body>
|
||||||
|
</Card.Root>
|
||||||
|
) : (
|
||||||
|
<Flex justify="center" py={16}>
|
||||||
|
<Text color="fg.muted">{t("no-users")}</Text>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</VStack>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<EditUserModal
|
||||||
|
user={editingUser}
|
||||||
|
isOpen={!!editingUser}
|
||||||
|
onClose={() => setEditingUser(null)}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</SlideUp>
|
</SlideUp>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,173 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
VStack,
|
||||||
|
Input,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import {
|
||||||
|
DialogRoot,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogBody,
|
||||||
|
DialogFooter,
|
||||||
|
DialogCloseTrigger,
|
||||||
|
} from "@/components/ui/overlays/dialog";
|
||||||
|
import { Field } from "@/components/ui/forms/field";
|
||||||
|
import { NativeSelectRoot, NativeSelectField } from "@/components/ui/forms/native-select";
|
||||||
|
import { Switch } from "@/components/ui/forms/switch";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { AdminUserDto } from "@/lib/api/admin/types";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import {
|
||||||
|
useUpdateUserRole,
|
||||||
|
useUpdateUserSubscription,
|
||||||
|
useToggleUserActive,
|
||||||
|
} from "@/lib/api/admin/use-hooks";
|
||||||
|
|
||||||
|
interface EditUserModalProps {
|
||||||
|
user: AdminUserDto | null;
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EditUserModal({ user, isOpen, onClose }: EditUserModalProps) {
|
||||||
|
const t = useTranslations("admin");
|
||||||
|
const tCommon = useTranslations("common");
|
||||||
|
|
||||||
|
const [role, setRole] = useState("user");
|
||||||
|
const [plan, setPlan] = useState("free");
|
||||||
|
const [expiresAt, setExpiresAt] = useState<string>("");
|
||||||
|
const [isActive, setIsActive] = useState(true);
|
||||||
|
|
||||||
|
const { mutateAsync: updateRole, isPending: rolePending } = useUpdateUserRole();
|
||||||
|
const { mutateAsync: updateSub, isPending: subPending } = useUpdateUserSubscription();
|
||||||
|
const { mutateAsync: toggleActive, isPending: togglePending } = useToggleUserActive();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (user) {
|
||||||
|
setRole(user.role || "user");
|
||||||
|
setPlan(user.subscriptionStatus || "free");
|
||||||
|
setIsActive(user.isActive);
|
||||||
|
if (user.subscriptionExpiresAt) {
|
||||||
|
try {
|
||||||
|
const date = new Date(user.subscriptionExpiresAt);
|
||||||
|
setExpiresAt(date.toISOString().split('T')[0]);
|
||||||
|
} catch(e) { setExpiresAt(""); }
|
||||||
|
} else {
|
||||||
|
setExpiresAt("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!user) return;
|
||||||
|
try {
|
||||||
|
if (role !== user.role) {
|
||||||
|
await updateRole({ id: user.id, dto: { role } });
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentExpiresAtStr = user.subscriptionExpiresAt
|
||||||
|
? new Date(user.subscriptionExpiresAt).toISOString().split('T')[0]
|
||||||
|
: "";
|
||||||
|
|
||||||
|
if (plan !== user.subscriptionStatus || expiresAt !== currentExpiresAtStr) {
|
||||||
|
await updateSub({
|
||||||
|
id: user.id,
|
||||||
|
dto: {
|
||||||
|
plan,
|
||||||
|
expiresAt: expiresAt ? new Date(expiresAt).toISOString() : null
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (isActive !== user.isActive) {
|
||||||
|
await toggleActive(user.id);
|
||||||
|
}
|
||||||
|
onClose();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isPending = rolePending || subPending || togglePending;
|
||||||
|
|
||||||
|
if (!user) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DialogRoot open={isOpen} onOpenChange={(e) => !e.open && onClose()}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Kullanıcı Düzenle: {user.email}</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogBody>
|
||||||
|
<VStack gap={4} align="stretch">
|
||||||
|
<Field label="Kullanıcı Rolü">
|
||||||
|
<NativeSelectRoot>
|
||||||
|
<NativeSelectField
|
||||||
|
value={role}
|
||||||
|
onChange={(e) => setRole(e.target.value)}
|
||||||
|
items={[
|
||||||
|
{ label: "Standart Kullanıcı", value: "user" },
|
||||||
|
{ label: "Sistem Yöneticisi (Admin)", value: "superadmin" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</NativeSelectRoot>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field label="Abonelik Paketi">
|
||||||
|
<NativeSelectRoot>
|
||||||
|
<NativeSelectField
|
||||||
|
value={plan}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newPlan = e.target.value;
|
||||||
|
setPlan(newPlan);
|
||||||
|
if ((newPlan === "premium" || newPlan === "plus") && !expiresAt) {
|
||||||
|
const d = new Date();
|
||||||
|
d.setDate(d.getDate() + 30);
|
||||||
|
setExpiresAt(d.toISOString().split('T')[0]);
|
||||||
|
} else if (newPlan === "free" || newPlan === "cancelled" || newPlan === "past_due") {
|
||||||
|
setExpiresAt("");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
items={[
|
||||||
|
{ label: "Ücretsiz (Free)", value: "free" },
|
||||||
|
{ label: "Plus Paketi", value: "plus" },
|
||||||
|
{ label: "Premium Paketi", value: "premium" },
|
||||||
|
{ label: "Ödeme Gecikti (Past Due)", value: "past_due" },
|
||||||
|
{ label: "İptal Edildi (Cancelled)", value: "cancelled" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</NativeSelectRoot>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
{plan !== "free" && (
|
||||||
|
<Field label="Abonelik Bitiş Tarihi (Opsiyonel)">
|
||||||
|
<Input
|
||||||
|
type="date"
|
||||||
|
value={expiresAt}
|
||||||
|
onChange={(e) => setExpiresAt(e.target.value)}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Field label="Hesap Aktif mi?">
|
||||||
|
<Switch checked={isActive} onCheckedChange={(e) => setIsActive(e.checked)}>
|
||||||
|
{isActive ? "Aktif" : "Pasif"}
|
||||||
|
</Switch>
|
||||||
|
</Field>
|
||||||
|
</VStack>
|
||||||
|
</DialogBody>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={onClose} disabled={isPending}>
|
||||||
|
İptal
|
||||||
|
</Button>
|
||||||
|
<Button colorPalette="primary" onClick={handleSave} loading={isPending}>
|
||||||
|
Kaydet
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
<DialogCloseTrigger />
|
||||||
|
</DialogContent>
|
||||||
|
</DialogRoot>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -27,7 +27,7 @@ export default function Footer() {
|
|||||||
focusRing="none"
|
focusRing="none"
|
||||||
fontWeight="semibold"
|
fontWeight="semibold"
|
||||||
>
|
>
|
||||||
Suggest Bet
|
iddaai
|
||||||
</ChakraLink>
|
</ChakraLink>
|
||||||
. {t("all-right-reserved")}
|
. {t("all-right-reserved")}
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -246,27 +246,34 @@ export default function MatchDetailContent() {
|
|||||||
{/* Score */}
|
{/* Score */}
|
||||||
<VStack gap={1} flexShrink={0}>
|
<VStack gap={1} flexShrink={0}>
|
||||||
{match.score && (isLive || isFinished) ? (
|
{match.score && (isLive || isFinished) ? (
|
||||||
<HStack gap={3}>
|
|
||||||
<Text
|
<>
|
||||||
fontSize="5xl"
|
<HStack gap={3}>
|
||||||
fontWeight="900"
|
<Text
|
||||||
lineHeight="1"
|
fontSize="4xl"
|
||||||
color={isLive ? "red.500" : "fg"}
|
fontWeight="900"
|
||||||
>
|
color={isLive ? "red.500" : "fg"}
|
||||||
{match.score.home}
|
>
|
||||||
</Text>
|
{match.score.home}
|
||||||
<Text fontSize="2xl" color="fg.muted" fontWeight="300">
|
</Text>
|
||||||
:
|
<Text fontSize="2xl" color="fg.muted">
|
||||||
</Text>
|
-
|
||||||
<Text
|
</Text>
|
||||||
fontSize="5xl"
|
<Text
|
||||||
fontWeight="900"
|
fontSize="4xl"
|
||||||
lineHeight="1"
|
fontWeight="900"
|
||||||
color={isLive ? "red.500" : "fg"}
|
color={isLive ? "red.500" : "fg"}
|
||||||
>
|
>
|
||||||
{match.score.away}
|
{match.score.away}
|
||||||
</Text>
|
</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
|
{match.score.htHome != null &&
|
||||||
|
match.score.htAway != null && (
|
||||||
|
<Text fontSize="xs" color="fg.muted">
|
||||||
|
(HT: {match.score.htHome}-{match.score.htAway})
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Text fontSize="xl" fontWeight="bold" color="fg.muted">
|
<Text fontSize="xl" fontWeight="bold" color="fg.muted">
|
||||||
{t("vs")}
|
{t("vs")}
|
||||||
|
|||||||
@@ -143,6 +143,36 @@ function formatUnits(value?: number): string {
|
|||||||
return `${value.toFixed(1)}u`;
|
return `${value.toFixed(1)}u`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getEngineLabelPalette(label?: string): string {
|
||||||
|
switch ((label || "").toUpperCase()) {
|
||||||
|
case "YUKSEK":
|
||||||
|
return "green";
|
||||||
|
case "ORTA":
|
||||||
|
return "yellow";
|
||||||
|
case "DUSUK":
|
||||||
|
return "orange";
|
||||||
|
case "COK_DUSUK":
|
||||||
|
return "red";
|
||||||
|
default:
|
||||||
|
return "gray";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEngineLabelText(label?: string): string {
|
||||||
|
switch ((label || "").toUpperCase()) {
|
||||||
|
case "YUKSEK":
|
||||||
|
return "Yüksek";
|
||||||
|
case "ORTA":
|
||||||
|
return "Orta";
|
||||||
|
case "DUSUK":
|
||||||
|
return "Düşük";
|
||||||
|
case "COK_DUSUK":
|
||||||
|
return "Çok Düşük";
|
||||||
|
default:
|
||||||
|
return label || "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getRiskPalette(level: string) {
|
function getRiskPalette(level: string) {
|
||||||
switch (level.toUpperCase()) {
|
switch (level.toUpperCase()) {
|
||||||
case "LOW":
|
case "LOW":
|
||||||
@@ -731,6 +761,58 @@ function SummaryTable({
|
|||||||
</HStack>
|
</HStack>
|
||||||
</Flex>
|
</Flex>
|
||||||
))}
|
))}
|
||||||
|
{/* <Flex
|
||||||
|
key={`${item.market}-${item.pick}`}
|
||||||
|
justify="space-between"
|
||||||
|
align={{ base: "start", md: "center" }}
|
||||||
|
direction={{ base: "column", md: "row" }}
|
||||||
|
gap={3}
|
||||||
|
px={3}
|
||||||
|
py={3}
|
||||||
|
borderRadius="xl"
|
||||||
|
bg={item.playable ? highlightBg : "transparent"}
|
||||||
|
borderWidth="1px"
|
||||||
|
borderColor={item.playable ? "green.200" : borderColor}
|
||||||
|
>
|
||||||
|
<HStack gap={2} flexWrap="wrap">
|
||||||
|
<Badge colorPalette={item.playable ? "green" : "gray"} variant="subtle">
|
||||||
|
{item.bet_grade}
|
||||||
|
</Badge>
|
||||||
|
<Badge colorPalette={getSignalTierPalette(item.signal_tier)} variant="subtle">
|
||||||
|
{getSignalTierLabel(item.signal_tier)}
|
||||||
|
</Badge>
|
||||||
|
{item.is_underdog_reference ? (
|
||||||
|
<Badge colorPalette="gray" variant="outline" title="Underdog tarafının model olasılığı (bilgi amaçlı)">
|
||||||
|
Underdog ref.
|
||||||
|
</Badge>
|
||||||
|
) : null}
|
||||||
|
{item.betting_brain?.trap_market_flag ? (
|
||||||
|
<Badge colorPalette="red" variant="subtle" title={`Piyasa aşırı güveniyor (gap ${(item.betting_brain.trap_market_gap || 0) * 100 | 0}pp)`}>
|
||||||
|
Trap
|
||||||
|
</Badge>
|
||||||
|
) : null}
|
||||||
|
{item.betting_brain?.action === "WATCH_NO_VALUE" ? (
|
||||||
|
<Badge colorPalette="orange" variant="subtle" title="Model favoriyle hemfikir ama oran çok düşük">
|
||||||
|
No-value
|
||||||
|
</Badge>
|
||||||
|
) : null}
|
||||||
|
<Text fontWeight="semibold">{getMarketLabel(item.market, marketLabels)}</Text>
|
||||||
|
<Text color="fg.muted">{item.pick}</Text>
|
||||||
|
</HStack>
|
||||||
|
<HStack gap={5} fontSize="sm">
|
||||||
|
<Text minW="48px">{formatOdds(item.odds)}</Text>
|
||||||
|
<Text minW="68px" color={item.ev_edge > 0 ? "green.500" : "red.500"} fontWeight="semibold">
|
||||||
|
{item.ev_edge > 0 ? "+" : ""}
|
||||||
|
{formatPercent(item.ev_edge * 100, 1)}
|
||||||
|
</Text>
|
||||||
|
<Text minW="48px">{formatPercent(item.calibrated_confidence, 0)}</Text>
|
||||||
|
<Badge colorPalette={getConfidenceBandPalette(item.confidence_interval?.band)} variant="subtle">
|
||||||
|
{getConfidenceBandLabel(item.confidence_interval?.band)}
|
||||||
|
</Badge>
|
||||||
|
<Badge variant="surface">{formatUnits(item.stake_units)}</Badge>
|
||||||
|
</HStack>
|
||||||
|
</Flex> */}
|
||||||
|
|
||||||
</VStack>
|
</VStack>
|
||||||
</Card.Body>
|
</Card.Body>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
@@ -869,7 +951,7 @@ function MarketBoardSection({
|
|||||||
value={probability * 100}
|
value={probability * 100}
|
||||||
color={
|
color={
|
||||||
entry.pick === outcome ||
|
entry.pick === outcome ||
|
||||||
entry.pick?.toUpperCase() === outcome.toUpperCase()
|
entry.pick?.toUpperCase() === outcome.toUpperCase()
|
||||||
? "green.400"
|
? "green.400"
|
||||||
: "blue.400"
|
: "blue.400"
|
||||||
}
|
}
|
||||||
@@ -980,6 +1062,7 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
|||||||
const sport = getPredictionSport(prediction);
|
const sport = getPredictionSport(prediction);
|
||||||
const isBasketball = sport === "basketball";
|
const isBasketball = sport === "basketball";
|
||||||
|
|
||||||
|
const engineDetail = prediction.engine_breakdown.detail;
|
||||||
const engineItems = [
|
const engineItems = [
|
||||||
{
|
{
|
||||||
key: "team",
|
key: "team",
|
||||||
@@ -987,6 +1070,7 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
|||||||
label: isBasketball ? "Takim Formu" : "Takim Gucu",
|
label: isBasketball ? "Takim Formu" : "Takim Gucu",
|
||||||
value: prediction.engine_breakdown.team,
|
value: prediction.engine_breakdown.team,
|
||||||
color: "blue.400",
|
color: "blue.400",
|
||||||
|
detail: engineDetail?.team,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "player",
|
key: "player",
|
||||||
@@ -994,6 +1078,7 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
|||||||
label: isBasketball ? "Kadro Etkisi" : "Oyuncu Etkisi",
|
label: isBasketball ? "Kadro Etkisi" : "Oyuncu Etkisi",
|
||||||
value: prediction.engine_breakdown.player,
|
value: prediction.engine_breakdown.player,
|
||||||
color: "green.400",
|
color: "green.400",
|
||||||
|
detail: engineDetail?.player,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "odds",
|
key: "odds",
|
||||||
@@ -1002,17 +1087,84 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
|||||||
value: prediction.engine_breakdown.odds,
|
value: prediction.engine_breakdown.odds,
|
||||||
color: "orange.400",
|
color: "orange.400",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "odds",
|
||||||
|
icon: LuTrendingUp,
|
||||||
|
label: "Oran Analizi",
|
||||||
|
value: prediction.engine_breakdown.odds,
|
||||||
|
color: "orange.400",
|
||||||
|
detail: engineDetail?.odds,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: "referee",
|
key: "referee",
|
||||||
icon: LuShieldAlert,
|
icon: LuShieldAlert,
|
||||||
label: isBasketball ? "Yardimci Sinyaller" : "Hakem Etkisi",
|
label: isBasketball ? "Yardimci Sinyaller" : "Hakem Etkisi",
|
||||||
value: prediction.engine_breakdown.referee,
|
value: prediction.engine_breakdown.referee,
|
||||||
color: "purple.400",
|
color: "purple.400",
|
||||||
|
detail: engineDetail?.referee,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const liveScoreHome = prediction.match_info?.current_score_home;
|
||||||
|
const liveScoreAway = prediction.match_info?.current_score_away;
|
||||||
|
const isLive = Boolean(prediction.match_info?.is_live);
|
||||||
|
const isStale = Boolean(prediction.prediction_freshness?.is_stale_for_live);
|
||||||
|
const contradictions = prediction.match_commentary?.contradictions || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VStack align="stretch" gap={5}>
|
<VStack align="stretch" gap={5}>
|
||||||
|
{isLive ? (
|
||||||
|
<Box
|
||||||
|
p={3}
|
||||||
|
bg={useColorModeValue("red.50", "red.950")}
|
||||||
|
borderWidth="1px"
|
||||||
|
borderColor={useColorModeValue("red.300", "red.800")}
|
||||||
|
borderRadius="xl"
|
||||||
|
>
|
||||||
|
<HStack justify="space-between" align="center">
|
||||||
|
<HStack gap={2}>
|
||||||
|
<Icon as={LuFlame} color="red.500" />
|
||||||
|
<Text fontWeight="bold" color="red.600">
|
||||||
|
🔴 CANLI
|
||||||
|
</Text>
|
||||||
|
{liveScoreHome != null && liveScoreAway != null ? (
|
||||||
|
<Text fontWeight="semibold">
|
||||||
|
{prediction.match_info.home_team} {liveScoreHome} - {liveScoreAway}{" "}
|
||||||
|
{prediction.match_info.away_team}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
</HStack>
|
||||||
|
{isStale ? (
|
||||||
|
<Badge colorPalette="orange" variant="solid">
|
||||||
|
Maç öncesi tahmin
|
||||||
|
</Badge>
|
||||||
|
) : null}
|
||||||
|
</HStack>
|
||||||
|
</Box>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{contradictions.length ? (
|
||||||
|
<Box
|
||||||
|
p={3}
|
||||||
|
bg={useColorModeValue("yellow.50", "yellow.950")}
|
||||||
|
borderWidth="1px"
|
||||||
|
borderColor={useColorModeValue("yellow.300", "yellow.800")}
|
||||||
|
borderRadius="xl"
|
||||||
|
>
|
||||||
|
<HStack align="start" gap={2}>
|
||||||
|
<Icon as={LuTriangleAlert} color="yellow.600" mt={0.5} />
|
||||||
|
<VStack align="start" gap={1}>
|
||||||
|
<Text fontWeight="semibold">Tahmin Çelişkileri</Text>
|
||||||
|
{contradictions.map((text, idx) => (
|
||||||
|
<Text key={idx} fontSize="sm" color="fg.muted">
|
||||||
|
• {text}
|
||||||
|
</Text>
|
||||||
|
))}
|
||||||
|
</VStack>
|
||||||
|
</HStack>
|
||||||
|
</Box>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<Card.Root bg={pageBg} borderColor={borderColor} borderRadius="2xl">
|
<Card.Root bg={pageBg} borderColor={borderColor} borderRadius="2xl">
|
||||||
<Card.Body gap={5}>
|
<Card.Body gap={5}>
|
||||||
<SectionTitle
|
<SectionTitle
|
||||||
@@ -1114,7 +1266,7 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
|||||||
label={uiText("stake-label", "Onerilen Miktar (Stake)")}
|
label={uiText("stake-label", "Onerilen Miktar (Stake)")}
|
||||||
value={formatUnits(
|
value={formatUnits(
|
||||||
recommendedPick.stake_units ||
|
recommendedPick.stake_units ||
|
||||||
prediction.bet_advice.suggested_stake_units,
|
prediction.bet_advice.suggested_stake_units,
|
||||||
)}
|
)}
|
||||||
helper={uiText(
|
helper={uiText(
|
||||||
"stake-info",
|
"stake-info",
|
||||||
@@ -1171,7 +1323,7 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
|||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
|
|
||||||
{prediction.risk.is_surprise_risk ||
|
{prediction.risk.is_surprise_risk ||
|
||||||
prediction.risk.warnings?.length ? (
|
prediction.risk.warnings?.length ? (
|
||||||
<Box
|
<Box
|
||||||
p={4}
|
p={4}
|
||||||
bg={useColorModeValue("orange.50", "orange.950")}
|
bg={useColorModeValue("orange.50", "orange.950")}
|
||||||
@@ -1204,13 +1356,31 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
|||||||
{formatPercent(prediction.risk.surprise_score, 0)}
|
{formatPercent(prediction.risk.surprise_score, 0)}
|
||||||
</Text>
|
</Text>
|
||||||
) : null}
|
) : null}
|
||||||
<ReasonList
|
{prediction.risk.surprise_breakdown?.length ? (
|
||||||
items={[
|
<VStack align="start" gap={1} mt={1}>
|
||||||
...(prediction.risk.surprise_reasons || []),
|
{prediction.risk.surprise_breakdown.map((entry) => (
|
||||||
...prediction.risk.warnings,
|
<HStack key={entry.code} gap={2}>
|
||||||
]}
|
<Badge
|
||||||
resolveReason={resolveReason}
|
colorPalette={entry.points >= 15 ? "red" : entry.points >= 8 ? "orange" : "yellow"}
|
||||||
/>
|
variant="subtle"
|
||||||
|
>
|
||||||
|
+{entry.points.toFixed(0)}
|
||||||
|
</Badge>
|
||||||
|
<Text fontSize="sm" color="fg.muted">
|
||||||
|
{entry.label}
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
))}
|
||||||
|
</VStack>
|
||||||
|
) : (
|
||||||
|
<ReasonList
|
||||||
|
items={[
|
||||||
|
...(prediction.risk.surprise_reasons || []),
|
||||||
|
...prediction.risk.warnings,
|
||||||
|
]}
|
||||||
|
resolveReason={resolveReason}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</VStack>
|
</VStack>
|
||||||
</HStack>
|
</HStack>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -1245,15 +1415,31 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
|||||||
{item.label}
|
{item.label}
|
||||||
</Text>
|
</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
<Text fontSize="sm" fontWeight="bold">
|
<HStack gap={2}>
|
||||||
+{item.value.toFixed(1)}
|
{item.detail?.label ? (
|
||||||
</Text>
|
<Badge
|
||||||
|
colorPalette={getEngineLabelPalette(item.detail.label)}
|
||||||
|
variant="subtle"
|
||||||
|
>
|
||||||
|
{getEngineLabelText(item.detail.label)}
|
||||||
|
</Badge>
|
||||||
|
) : null}
|
||||||
|
<Text fontSize="sm" fontWeight="bold">
|
||||||
|
+{item.value.toFixed(1)}
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
</HStack>
|
</HStack>
|
||||||
<Bar
|
<Bar
|
||||||
value={Math.min(item.value, 100)}
|
value={Math.min(item.value, 100)}
|
||||||
color={item.color}
|
color={item.color}
|
||||||
trackBg={useColorModeValue("gray.100", "gray.700")}
|
trackBg={useColorModeValue("gray.100", "gray.700")}
|
||||||
/>
|
/>
|
||||||
|
<Bar value={Math.min(item.value, 100)} color={item.color} trackBg={useColorModeValue("gray.100", "gray.700")} />
|
||||||
|
{item.detail?.interpretation ? (
|
||||||
|
<Text fontSize="xs" color="fg.muted" mt={2}>
|
||||||
|
{item.detail.interpretation}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
</Box>
|
</Box>
|
||||||
))}
|
))}
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
@@ -1325,6 +1511,36 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
|||||||
"Butun secenekleri tek tabloda karsilastir.",
|
"Butun secenekleri tek tabloda karsilastir.",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
{prediction.match_commentary?.headline || prediction.match_commentary?.summary ? (
|
||||||
|
<Card.Root bg={cardBg} borderColor={borderColor} borderRadius="2xl">
|
||||||
|
<Card.Body gap={3}>
|
||||||
|
<SectionTitle
|
||||||
|
icon={LuBrain}
|
||||||
|
title="Maç Yorumu"
|
||||||
|
info="Modelin maç hakkındaki insan-okunabilir özeti"
|
||||||
|
/>
|
||||||
|
{prediction.match_commentary.headline ? (
|
||||||
|
<Text fontSize="md" fontWeight="bold">
|
||||||
|
{prediction.match_commentary.headline}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
{prediction.match_commentary.summary ? (
|
||||||
|
<Text fontSize="sm" color="fg.muted">
|
||||||
|
{prediction.match_commentary.summary}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
{prediction.match_commentary.notes?.length ? (
|
||||||
|
<VStack align="start" gap={1}>
|
||||||
|
{prediction.match_commentary.notes.map((note, idx) => (
|
||||||
|
<Text key={idx} fontSize="sm">
|
||||||
|
• {note}
|
||||||
|
</Text>
|
||||||
|
))}
|
||||||
|
</VStack>
|
||||||
|
) : null}
|
||||||
|
</Card.Body>
|
||||||
|
</Card.Root>
|
||||||
|
) : null}
|
||||||
<ScoreCard prediction={prediction} sport={sport} />
|
<ScoreCard prediction={prediction} sport={sport} />
|
||||||
<MarketBoardSection
|
<MarketBoardSection
|
||||||
marketBoard={prediction.market_board}
|
marketBoard={prediction.market_board}
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ export function Provider(props: ColorModeProviderProps) {
|
|||||||
</AOSProvider>
|
</AOSProvider>
|
||||||
</PaddleProvider>
|
</PaddleProvider>
|
||||||
</ReactQueryProvider>
|
</ReactQueryProvider>
|
||||||
|
|
||||||
</ChakraProvider>
|
</ChakraProvider>
|
||||||
|
|
||||||
</SessionProvider>
|
</SessionProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,18 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import NextTopLoader from "nextjs-toploader";
|
import NextTopLoader from 'nextjs-toploader';
|
||||||
import { useToken } from "@chakra-ui/react";
|
import { useToken } from '@chakra-ui/react';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
export default function TopLoader() {
|
export default function TopLoader() {
|
||||||
const [color] = useToken("colors", ["primary.500"]);
|
const [color] = useToken('colors', ['primary.500']);
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
|
||||||
return <NextTopLoader color={color} showSpinner={false} />;
|
useEffect(() => {
|
||||||
|
setMounted(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!mounted) return null;
|
||||||
|
|
||||||
|
return <NextTopLoader color={color || '#319795'} showSpinner={false} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ export interface AdminPaginationParams extends PaginationDto {
|
|||||||
sortBy?: string;
|
sortBy?: string;
|
||||||
sortOrder?: "asc" | "desc";
|
sortOrder?: "asc" | "desc";
|
||||||
search?: string;
|
search?: string;
|
||||||
|
role?: string;
|
||||||
|
subscriptionStatus?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================
|
// ========================
|
||||||
@@ -50,6 +52,7 @@ export interface AdminUserDto {
|
|||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
subscription?: string;
|
subscription?: string;
|
||||||
subscriptionStatus?: string;
|
subscriptionStatus?: string;
|
||||||
|
subscriptionExpiresAt?: string | null;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
@@ -60,6 +63,7 @@ export interface UpdateUserRoleDto {
|
|||||||
|
|
||||||
export interface UpdateUserSubscriptionDto {
|
export interface UpdateUserSubscriptionDto {
|
||||||
plan: string;
|
plan: string;
|
||||||
|
expiresAt?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================
|
// ========================
|
||||||
|
|||||||
@@ -13,8 +13,9 @@ export const AdminQueryKeys = {
|
|||||||
settings: () => [...AdminQueryKeys.all, "settings"] as const,
|
settings: () => [...AdminQueryKeys.all, "settings"] as const,
|
||||||
usageLimits: (params?: AdminPaginationParams) =>
|
usageLimits: (params?: AdminPaginationParams) =>
|
||||||
[...AdminQueryKeys.all, "usageLimits", params] as const,
|
[...AdminQueryKeys.all, "usageLimits", params] as const,
|
||||||
|
usersList: () => [...AdminQueryKeys.all, "users"] as const,
|
||||||
users: (params?: AdminPaginationParams) =>
|
users: (params?: AdminPaginationParams) =>
|
||||||
[...AdminQueryKeys.all, "users", params] as const,
|
[...AdminQueryKeys.usersList(), params] as const,
|
||||||
user: (id: string) => [...AdminQueryKeys.all, "user", id] as const,
|
user: (id: string) => [...AdminQueryKeys.all, "user", id] as const,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -104,7 +105,7 @@ export const useDeleteUser = () => {
|
|||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (id: string) => adminService.deleteUser(id),
|
mutationFn: (id: string) => adminService.deleteUser(id),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: AdminQueryKeys.users() });
|
queryClient.invalidateQueries({ queryKey: AdminQueryKeys.usersList() });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -116,7 +117,7 @@ export const useUpdateUserRole = () => {
|
|||||||
mutationFn: ({ id, dto }: { id: string; dto: UpdateUserRoleDto }) =>
|
mutationFn: ({ id, dto }: { id: string; dto: UpdateUserRoleDto }) =>
|
||||||
adminService.updateUserRole(id, dto),
|
adminService.updateUserRole(id, dto),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: AdminQueryKeys.users() });
|
queryClient.invalidateQueries({ queryKey: AdminQueryKeys.usersList() });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -128,7 +129,7 @@ export const useUpdateUserSubscription = () => {
|
|||||||
mutationFn: ({ id, dto }: { id: string; dto: UpdateUserSubscriptionDto }) =>
|
mutationFn: ({ id, dto }: { id: string; dto: UpdateUserSubscriptionDto }) =>
|
||||||
adminService.updateUserSubscription(id, dto),
|
adminService.updateUserSubscription(id, dto),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: AdminQueryKeys.users() });
|
queryClient.invalidateQueries({ queryKey: AdminQueryKeys.usersList() });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -139,7 +140,7 @@ export const useToggleUserActive = () => {
|
|||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (id: string) => adminService.toggleUserActive(id),
|
mutationFn: (id: string) => adminService.toggleUserActive(id),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: AdminQueryKeys.users() });
|
queryClient.invalidateQueries({ queryKey: AdminQueryKeys.usersList() });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -65,7 +65,12 @@ export interface MatchResponseDto {
|
|||||||
// Scores
|
// Scores
|
||||||
scoreHome?: number;
|
scoreHome?: number;
|
||||||
scoreAway?: number;
|
scoreAway?: number;
|
||||||
score?: { home: number; away: number };
|
score?: {
|
||||||
|
home: number;
|
||||||
|
away: number;
|
||||||
|
htHome?: number | null;
|
||||||
|
htAway?: number | null;
|
||||||
|
};
|
||||||
htScoreHome?: number;
|
htScoreHome?: number;
|
||||||
htScoreAway?: number;
|
htScoreAway?: number;
|
||||||
|
|
||||||
|
|||||||
@@ -14,9 +14,27 @@ export interface MatchInfoDto {
|
|||||||
league_id?: string | null;
|
league_id?: string | null;
|
||||||
is_top_league?: boolean;
|
is_top_league?: boolean;
|
||||||
sport?: SportType;
|
sport?: SportType;
|
||||||
|
// Live snapshot — set when the match is in play (used to detect stale predictions)
|
||||||
|
status?: string | null;
|
||||||
|
state?: string | null;
|
||||||
|
is_live?: boolean;
|
||||||
|
current_score_home?: number | null;
|
||||||
|
current_score_away?: number | null;
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PredictionFreshnessDto {
|
||||||
|
generated_at_ms: number;
|
||||||
|
is_pre_match_snapshot: boolean;
|
||||||
|
is_stale_for_live: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SurpriseBreakdownEntryDto {
|
||||||
|
code: string;
|
||||||
|
points: number;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface DataQualityDto {
|
export interface DataQualityDto {
|
||||||
label: "HIGH" | "MEDIUM" | "LOW";
|
label: "HIGH" | "MEDIUM" | "LOW";
|
||||||
score: number;
|
score: number;
|
||||||
@@ -43,14 +61,29 @@ export interface RiskDto {
|
|||||||
surprise_score?: number;
|
surprise_score?: number;
|
||||||
surprise_comment?: string | null;
|
surprise_comment?: string | null;
|
||||||
surprise_reasons?: string[];
|
surprise_reasons?: string[];
|
||||||
|
surprise_breakdown?: SurpriseBreakdownEntryDto[];
|
||||||
warnings: string[];
|
warnings: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface EngineBreakdownDetailEntryDto {
|
||||||
|
score: number;
|
||||||
|
label: "YUKSEK" | "ORTA" | "DUSUK" | "COK_DUSUK" | string;
|
||||||
|
display_name: string;
|
||||||
|
interpretation: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface EngineBreakdownDto {
|
export interface EngineBreakdownDto {
|
||||||
team: number;
|
team: number;
|
||||||
player: number;
|
player: number;
|
||||||
odds: number;
|
odds: number;
|
||||||
referee: number;
|
referee: number;
|
||||||
|
detail?: {
|
||||||
|
team?: EngineBreakdownDetailEntryDto;
|
||||||
|
player?: EngineBreakdownDetailEntryDto;
|
||||||
|
odds?: EngineBreakdownDetailEntryDto;
|
||||||
|
referee?: EngineBreakdownDetailEntryDto;
|
||||||
|
[key: string]: EngineBreakdownDetailEntryDto | undefined;
|
||||||
|
};
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,10 +119,34 @@ export interface MatchPickDto {
|
|||||||
is_guaranteed?: boolean;
|
is_guaranteed?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type BettingBrainAction =
|
||||||
|
| "BET"
|
||||||
|
| "WATCH"
|
||||||
|
| "WATCH_NO_VALUE"
|
||||||
|
| "REJECT";
|
||||||
|
|
||||||
|
export interface BettingBrainEntryDto {
|
||||||
|
action: BettingBrainAction;
|
||||||
|
score: number;
|
||||||
|
summary: string;
|
||||||
|
positives: string[];
|
||||||
|
issues: string[];
|
||||||
|
vetoes: string[];
|
||||||
|
sniper_bypassed?: string[];
|
||||||
|
trap_market_flag?: boolean;
|
||||||
|
trap_market_gap?: number | null;
|
||||||
|
model_prob?: number | null;
|
||||||
|
implied_prob?: number;
|
||||||
|
model_market_gap?: number | null;
|
||||||
|
triple_key?: string | null;
|
||||||
|
triple_value?: Record<string, unknown> | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface MatchBetAdviceDto {
|
export interface MatchBetAdviceDto {
|
||||||
playable: boolean;
|
playable: boolean;
|
||||||
suggested_stake_units: number;
|
suggested_stake_units: number;
|
||||||
reason: string;
|
reason: string;
|
||||||
|
decision?: "BET" | "WATCHLIST" | "WATCH_NO_VALUE" | "NO_BET" | string;
|
||||||
confidence_band?: "HIGH" | "MEDIUM" | "LOW";
|
confidence_band?: "HIGH" | "MEDIUM" | "LOW";
|
||||||
min_confidence_for_play?: number;
|
min_confidence_for_play?: number;
|
||||||
signal_tier?: SignalTier;
|
signal_tier?: SignalTier;
|
||||||
@@ -118,6 +175,9 @@ export interface MatchBetSummaryItemDto {
|
|||||||
reasons: string[];
|
reasons: string[];
|
||||||
confidence_interval?: ConfidenceIntervalDto;
|
confidence_interval?: ConfidenceIntervalDto;
|
||||||
signal_tier?: SignalTier;
|
signal_tier?: SignalTier;
|
||||||
|
// New (post-engine-upgrade)
|
||||||
|
is_underdog_reference?: boolean;
|
||||||
|
betting_brain?: BettingBrainEntryDto;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AggressivePickDto {
|
export interface AggressivePickDto {
|
||||||
@@ -266,6 +326,15 @@ export interface MatchPredictionDto {
|
|||||||
reasoning_factors: string[];
|
reasoning_factors: string[];
|
||||||
ai_commentary?: string | null;
|
ai_commentary?: string | null;
|
||||||
v27_engine?: V27EngineDto;
|
v27_engine?: V27EngineDto;
|
||||||
|
prediction_freshness?: PredictionFreshnessDto;
|
||||||
|
match_commentary?: {
|
||||||
|
action?: string;
|
||||||
|
headline?: string;
|
||||||
|
summary?: string;
|
||||||
|
notes?: string[];
|
||||||
|
contradictions?: string[];
|
||||||
|
confidence_label?: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ValueBetDto {
|
export interface ValueBetDto {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user