Compare commits
4 الالتزامات
a975f9c5d1
...
93d9a598a8
| المؤلف | SHA1 | التاريخ | |
|---|---|---|---|
|
|
93d9a598a8 | ||
|
|
b9c53be445 | ||
|
|
9230fd8e4e | ||
|
|
a837154231 |
46
.dockerignore
Normal file
46
.dockerignore
Normal file
@@ -0,0 +1,46 @@
|
||||
# التبعيات
|
||||
node_modules
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# ملفات البيئة
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# ملفات السجلات
|
||||
logs
|
||||
*.log
|
||||
|
||||
# ملفات التخزين المؤقت
|
||||
.cache
|
||||
.parcel-cache
|
||||
.nuxt
|
||||
.next
|
||||
dist
|
||||
build
|
||||
|
||||
# نظام التحكم بالنسخ
|
||||
.git
|
||||
.gitignore
|
||||
README.md
|
||||
|
||||
# ملفات النظام
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# ملفات IDE
|
||||
.vscode
|
||||
.idea
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# ملفات الاختبار
|
||||
coverage
|
||||
.nyc_output
|
||||
|
||||
|
||||
node_modules
|
||||
17
.ghaymah.json
Normal file
17
.ghaymah.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"id": "d7d9b033-eb33-4dcd-93f7-aa6f03ed706b",
|
||||
"name": "dashboard",
|
||||
"projectId": "2f99d8bc-53ba-44fc-ac40-2ff3dc380c87",
|
||||
"ports": [
|
||||
{
|
||||
"expose": true,
|
||||
"number": 5173
|
||||
}
|
||||
],
|
||||
"publicAccess": {
|
||||
"enabled": true,
|
||||
"domain": "auto"
|
||||
},
|
||||
"resourceTier": "t5",
|
||||
"dockerFileName": "Dockerfile"
|
||||
}
|
||||
15
Dockerfile
Normal file
15
Dockerfile
Normal file
@@ -0,0 +1,15 @@
|
||||
FROM node:20-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json .
|
||||
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
|
||||
ENV PORT=8080
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
CMD [ "npm", "run", "dev"]
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React + TS</title>
|
||||
<title>Onekeyword</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
432
package-lock.json
مولّد
432
package-lock.json
مولّد
@@ -8,6 +8,11 @@
|
||||
"name": "my-app",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@apollo/client": "^4.0.7",
|
||||
"@apollo/react-hooks": "^4.0.0",
|
||||
"@nhost/react": "^3.11.2",
|
||||
"graphql": "^16.11.0",
|
||||
"graphql-ws": "^6.0.6",
|
||||
"lucide-react": "^0.544.0",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
@@ -44,6 +49,55 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@apollo/client": {
|
||||
"version": "4.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@apollo/client/-/client-4.0.7.tgz",
|
||||
"integrity": "sha512-hZp/mKtAqM+g6buUnu6Wqtyc33QebvfdY0SE46xWea4lU1CxwI57VORy2N2vA9CoCRgYM4ELNXzr6nNErAdhfg==",
|
||||
"workspaces": [
|
||||
"dist",
|
||||
"codegen",
|
||||
"scripts/codemods/ac3-to-ac4"
|
||||
],
|
||||
"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",
|
||||
"optimism": "^0.18.0",
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"graphql": "^16.0.0",
|
||||
"graphql-ws": "^5.5.5 || ^6.0.3",
|
||||
"react": "^17.0.0 || ^18.0.0 || >=19.0.0-rc",
|
||||
"react-dom": "^17.0.0 || ^18.0.0 || >=19.0.0-rc",
|
||||
"rxjs": "^7.3.0",
|
||||
"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/@apollo/react-hooks": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@apollo/react-hooks/-/react-hooks-4.0.0.tgz",
|
||||
"integrity": "sha512-fCu0cbne3gbUl0QbA8X4L33iuuFVQbC5Jo2MIKRK8CyawR6PoxDpFdFA1kc6033ODZuZZ9Eo4RdeJFlFIIYcLA==",
|
||||
"dependencies": {
|
||||
"@apollo/client": "latest"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
||||
@@ -922,6 +976,14 @@
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"peerDependencies": {
|
||||
"graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanfs/core": {
|
||||
"version": "0.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||
@@ -1042,6 +1104,126 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@nhost/graphql-js": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@nhost/graphql-js/-/graphql-js-0.3.0.tgz",
|
||||
"integrity": "sha512-CVYq6wx0VbaYdpUBmfNO/6mZatHB5+YBCqFjWyxhpN1nzHCHEO6rgdL7j0qk31OFE6XAX0z7AQZSXg1Pn63GUw==",
|
||||
"deprecated": "⚠ DEPRECATED: This package is deprecated in favor of @nhost/nhost-js@^4.0.0. The new SDK is auto-generated from OpenAPI specs, provides better consistency, and works isomorphically across all JavaScript environments. This package will receive security patches and bug fixes until March 31st but no new features. Migrate to @nhost/nhost-js for future compatibility. Learn more: https://nhost.io/blog/introducing-new-javascript-sdk",
|
||||
"dependencies": {
|
||||
"@graphql-typed-document-node/core": "^3.2.0",
|
||||
"base-64": "^1.0.0",
|
||||
"isomorphic-unfetch": "^3.1.0",
|
||||
"jwt-decode": "^4.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"graphql": "^14.0.0 || ^15.0.0 || ^16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nhost/hasura-auth-js": {
|
||||
"version": "2.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@nhost/hasura-auth-js/-/hasura-auth-js-2.12.0.tgz",
|
||||
"integrity": "sha512-yOugrb9VCJr7Br/aSmv3iSN0rHvSBmynWppD1jiWuGqGVE0Fe/nwmYzJka1+5xDEdjtJvqaLnINEd84FI+pXSQ==",
|
||||
"deprecated": "⚠ DEPRECATED: This package is deprecated in favor of @nhost/nhost-js@^4.0.0. The new SDK is auto-generated from OpenAPI specs, provides better consistency, and works isomorphically across all JavaScript environments. This package will receive security patches and bug fixes until March 31st but no new features. Migrate to @nhost/nhost-js for future compatibility. Learn more: https://nhost.io/blog/introducing-new-javascript-sdk",
|
||||
"dependencies": {
|
||||
"@simplewebauthn/browser": "^9.0.1",
|
||||
"fetch-ponyfill": "^7.1.0",
|
||||
"js-cookie": "^3.0.5",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"xstate": "^4.38.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@nhost/hasura-storage-js": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@nhost/hasura-storage-js/-/hasura-storage-js-2.9.0.tgz",
|
||||
"integrity": "sha512-FCeQpiqxH9JAAMwsS5kEv0OwyN8WhSD65XYM3P8CAeOqbun8yreQno7/wovFsGPVZqMgFDJ0VG9RhR1MFsPqdA==",
|
||||
"deprecated": "⚠ DEPRECATED: This package is deprecated in favor of @nhost/nhost-js@^4.0.0. The new SDK is auto-generated from OpenAPI specs, provides better consistency, and works isomorphically across all JavaScript environments. This package will receive security patches and bug fixes until March 31st but no new features. Migrate to @nhost/nhost-js for future compatibility. Learn more: https://nhost.io/blog/introducing-new-javascript-sdk",
|
||||
"dependencies": {
|
||||
"graphql": "16.8.1",
|
||||
"xstate": "^4.38.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@nhost/hasura-storage-js/node_modules/graphql": {
|
||||
"version": "16.8.1",
|
||||
"resolved": "https://registry.npmjs.org/graphql/-/graphql-16.8.1.tgz",
|
||||
"integrity": "sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==",
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nhost/nhost-js": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@nhost/nhost-js/-/nhost-js-3.3.1.tgz",
|
||||
"integrity": "sha512-AzOUalIQr0BS2psgEqxI+AMbNjSKB5RUOGzZX82U7YVcbJ9YvYf6a25EmtnJpItmN7H/h22Vxu8AbXqdtsGVkQ==",
|
||||
"deprecated": "⚠ DEPRECATED: This version is deprecated in favor of @nhost/nhost-js@^4.0.0. The new SDK is auto-generated from OpenAPI specs, provides better consistency, and works isomorphically across all JavaScript environments. This package will receive security patches and bug fixes until March 31st but no new features. Migrate to @nhost/nhost-js for future compatibility. Learn more: https://nhost.io/blog/introducing-new-javascript-sdk",
|
||||
"dependencies": {
|
||||
"@nhost/graphql-js": "0.3.0",
|
||||
"@nhost/hasura-auth-js": "2.12.0",
|
||||
"@nhost/hasura-storage-js": "2.9.0",
|
||||
"isomorphic-unfetch": "^3.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"graphql": "^14.0.0 || ^15.0.0 || ^16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nhost/react": {
|
||||
"version": "3.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@nhost/react/-/react-3.11.2.tgz",
|
||||
"integrity": "sha512-OQLDFltmFo7l11VHoR6b17fNNG5qxGF3UU21md0dglo3exZ2XT3lFqI/6rITIAy5kiXK66ciBAN060Zmy0E4dA==",
|
||||
"deprecated": "⚠ DEPRECATED: This package is deprecated in favor of @nhost/nhost-js@^4.0.0. The new SDK is auto-generated from OpenAPI specs, provides better consistency, and works isomorphically across all JavaScript environments. This package will receive security patches and bug fixes until March 31st but no new features. Migrate to @nhost/nhost-js for future compatibility. Learn more: https://nhost.io/blog/introducing-new-javascript-sdk",
|
||||
"dependencies": {
|
||||
"@nhost/nhost-js": "3.3.1",
|
||||
"@xstate/react": "^3.2.2",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"xstate": "^4.38.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^17.0.0 || ^18.1.0 || ^19.0.0",
|
||||
"react-dom": "^17.0.0 || ^18.1.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nhost/react/node_modules/@xstate/react": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@xstate/react/-/react-3.2.2.tgz",
|
||||
"integrity": "sha512-feghXWLedyq8JeL13yda3XnHPZKwYDN5HPBLykpLeuNpr9178tQd2/3d0NrH6gSd0sG5mLuLeuD+ck830fgzLQ==",
|
||||
"dependencies": {
|
||||
"use-isomorphic-layout-effect": "^1.1.2",
|
||||
"use-sync-external-store": "^1.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@xstate/fsm": "^2.0.0",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||
"xstate": "^4.37.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@xstate/fsm": {
|
||||
"optional": true
|
||||
},
|
||||
"xstate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@nhost/react/node_modules/react-dom": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"scheduler": "^0.23.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@nhost/react/node_modules/scheduler": {
|
||||
"version": "0.23.2",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
|
||||
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
@@ -1432,6 +1614,20 @@
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@simplewebauthn/browser": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-9.0.1.tgz",
|
||||
"integrity": "sha512-wD2WpbkaEP4170s13/HUxPcAV5y4ZXaKo1TfNklS5zDefPinIgXOpgz1kpEvobAsaLPa2KeH7AKKX/od1mrBJw==",
|
||||
"dependencies": {
|
||||
"@simplewebauthn/types": "^9.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@simplewebauthn/types": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@simplewebauthn/types/-/types-9.0.1.tgz",
|
||||
"integrity": "sha512-tGSRP1QvsAvsJmnOlRQyw/mvK9gnPtjEc5fg2+m8n+QUa+D7rvrKkOYyfpy42GTs90X3RDOnqJgfHt+qO67/+w==",
|
||||
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info."
|
||||
},
|
||||
"node_modules/@standard-schema/spec": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
|
||||
@@ -1884,6 +2080,50 @@
|
||||
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.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==",
|
||||
"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==",
|
||||
"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==",
|
||||
"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==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.15.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
@@ -2033,6 +2273,11 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/base-64": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/base-64/-/base-64-1.0.0.tgz",
|
||||
"integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg=="
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.8.6",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.6.tgz",
|
||||
@@ -2814,6 +3059,14 @@
|
||||
"reusify": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/fetch-ponyfill": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fetch-ponyfill/-/fetch-ponyfill-7.1.0.tgz",
|
||||
"integrity": "sha512-FhbbL55dj/qdVO3YNK7ZEkshvj3eQ7EuIGV2I6ic/2YiocvyWv+7jg2s4AyS0wdRU75s3tA8ZxI/xPigb0v5Aw==",
|
||||
"dependencies": {
|
||||
"node-fetch": "~2.6.1"
|
||||
}
|
||||
},
|
||||
"node_modules/file-entry-cache": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
||||
@@ -3024,6 +3277,57 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/graphql": {
|
||||
"version": "16.11.0",
|
||||
"resolved": "https://registry.npmjs.org/graphql/-/graphql-16.11.0.tgz",
|
||||
"integrity": "sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==",
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/graphql-tag": {
|
||||
"version": "2.12.6",
|
||||
"resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.12.6.tgz",
|
||||
"integrity": "sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg==",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"node_modules/graphql-ws": {
|
||||
"version": "6.0.6",
|
||||
"resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-6.0.6.tgz",
|
||||
"integrity": "sha512-zgfER9s+ftkGKUZgc0xbx8T7/HMO4AV5/YuYiFc+AtgcO5T0v8AxYYNQ+ltzuzDZgNkYJaFspm5MMYLjQzrkmw==",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@fastify/websocket": "^10 || ^11",
|
||||
"crossws": "~0.3",
|
||||
"graphql": "^15.10.1 || ^16",
|
||||
"uWebSockets.js": "^20",
|
||||
"ws": "^8"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@fastify/websocket": {
|
||||
"optional": true
|
||||
},
|
||||
"crossws": {
|
||||
"optional": true
|
||||
},
|
||||
"uWebSockets.js": {
|
||||
"optional": true
|
||||
},
|
||||
"ws": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
@@ -3182,6 +3486,15 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/isomorphic-unfetch": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/isomorphic-unfetch/-/isomorphic-unfetch-3.1.0.tgz",
|
||||
"integrity": "sha512-geDJjpoZ8N0kWexiwkX8F9NkTsXhetLPVbZFQ+JTW239QNOwvB0gniuR1Wc6f0AMTn7/mFGyXvHTifrCp/GH8Q==",
|
||||
"dependencies": {
|
||||
"node-fetch": "^2.6.1",
|
||||
"unfetch": "^4.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jackspeak": {
|
||||
"version": "3.4.3",
|
||||
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
|
||||
@@ -3208,11 +3521,18 @@
|
||||
"jiti": "bin/jiti.js"
|
||||
}
|
||||
},
|
||||
"node_modules/js-cookie": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
|
||||
"integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
@@ -3275,6 +3595,14 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/jwt-decode": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz",
|
||||
"integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/keyv": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
@@ -3339,6 +3667,17 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"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==",
|
||||
"dependencies": {
|
||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"loose-envify": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||
@@ -3353,7 +3692,6 @@
|
||||
"version": "0.544.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.544.0.tgz",
|
||||
"integrity": "sha512-t5tS44bqd825zAW45UQxpG2CvcC4urOwn2TrwSH8u+MjeE+1NnWl6QqeQ/6NdjMqdOygyiT9p3Ev0p1NJykxjw==",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
@@ -3450,6 +3788,25 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "2.6.13",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.13.tgz",
|
||||
"integrity": "sha512-StxNAxh15zr77QvvkmveSQ8uCQ4+v5FkvNTj0OESmiHu+VRi/gXArXtkWMElOsOUNLtUEvI4yS+rdtOHZTwlQA==",
|
||||
"dependencies": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "4.x || >=6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"encoding": "^0.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"encoding": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.21",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.21.tgz",
|
||||
@@ -3497,6 +3854,17 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"dependencies": {
|
||||
"@wry/caches": "^1.0.0",
|
||||
"@wry/context": "^0.7.0",
|
||||
"@wry/trie": "^0.5.0",
|
||||
"tslib": "^2.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/optionator": {
|
||||
"version": "0.9.4",
|
||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||
@@ -4146,6 +4514,15 @@
|
||||
"queue-microtask": "^1.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/rxjs": {
|
||||
"version": "7.8.2",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
|
||||
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.26.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz",
|
||||
@@ -4508,6 +4885,11 @@
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tr46": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
|
||||
},
|
||||
"node_modules/ts-api-utils": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
|
||||
@@ -4528,6 +4910,11 @@
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||
@@ -4579,6 +4966,11 @@
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/unfetch": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.2.0.tgz",
|
||||
"integrity": "sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA=="
|
||||
},
|
||||
"node_modules/update-browserslist-db": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
|
||||
@@ -4620,6 +5012,19 @@
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/use-isomorphic-layout-effect": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.1.tgz",
|
||||
"integrity": "sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/use-sync-external-store": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
|
||||
@@ -4764,6 +5169,20 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||
"dependencies": {
|
||||
"tr46": "~0.0.3",
|
||||
"webidl-conversions": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
@@ -4885,6 +5304,15 @@
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/xstate": {
|
||||
"version": "4.38.3",
|
||||
"resolved": "https://registry.npmjs.org/xstate/-/xstate-4.38.3.tgz",
|
||||
"integrity": "sha512-SH7nAaaPQx57dx6qvfcIgqKRXIh4L0A1iYEqim4s1u7c9VoCgzZc+63FY90AKU4ZzOC2cfJzTnpO4zK7fCUzzw==",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/xstate"
|
||||
}
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||
|
||||
@@ -10,6 +10,11 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/client": "^4.0.7",
|
||||
"@apollo/react-hooks": "^4.0.0",
|
||||
"@nhost/react": "^3.11.2",
|
||||
"graphql": "^16.11.0",
|
||||
"graphql-ws": "^6.0.6",
|
||||
"lucide-react": "^0.544.0",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
|
||||
@@ -23,6 +23,14 @@ function App() {
|
||||
<Route path="/portfolio" element={<ProtectedRoute><Portfolio /></ProtectedRoute>} />
|
||||
<Route path="/strategy" element={<ProtectedRoute><Strategy /></ProtectedRoute>} />
|
||||
<Route path="/settings" element={<ProtectedRoute><Settings /></ProtectedRoute>} />
|
||||
|
||||
|
||||
{/* <Route path="/" element={<Dashboard />} />
|
||||
<Route path="/portfolio" element={<Portfolio />} />
|
||||
<Route path="/strategy" element={<Strategy />} />
|
||||
<Route path="/settings" element={<Settings />} /> */}
|
||||
|
||||
|
||||
</Routes>
|
||||
<Footer />
|
||||
</div>
|
||||
|
||||
103
src/main.tsx
103
src/main.tsx
@@ -1,11 +1,106 @@
|
||||
// بسم الله الرحمن الرحيم
|
||||
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import './App.css'
|
||||
import App from './App.tsx'
|
||||
import { NhostProvider, NhostClient } from '@nhost/react'
|
||||
import { ApolloClient, InMemoryCache, createHttpLink, split } from "@apollo/client"
|
||||
import { ApolloProvider } from "@apollo/client/react";
|
||||
import { setContext } from '@apollo/client/link/context'
|
||||
import { GraphQLWsLink } from '@apollo/client/link/subscriptions'
|
||||
import { createClient } from 'graphql-ws'
|
||||
import { getMainDefinition } from '@apollo/client/utilities'
|
||||
|
||||
// Initialize Nhost client
|
||||
const nhost = new NhostClient({
|
||||
authUrl: 'https://onekeyword-auth-76aaf2ee939c.hosted.ghaymah.systems',
|
||||
storageUrl: 'https://onekeyword-auth-76aaf2ee939c.hosted.ghaymah.systems',
|
||||
functionsUrl: 'https://onekeyword-auth-76aaf2ee939c.hosted.ghaymah.systems',
|
||||
graphqlUrl: 'https://onekeyword-auth-76aaf2ee939c.hosted.ghaymah.systems',
|
||||
})
|
||||
|
||||
// Auth link for Apollo Client
|
||||
const authLink = setContext(async (_, { headers }) => {
|
||||
// Get the authentication state from Nhost
|
||||
const isAuthenticated = await nhost.auth.isAuthenticatedAsync()
|
||||
|
||||
if (isAuthenticated) {
|
||||
const token = nhost.auth.getAccessToken()
|
||||
return {
|
||||
headers: {
|
||||
...headers,
|
||||
authorization: token ? `Bearer ${token}` : '',
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Use public role when not authenticated
|
||||
return {
|
||||
headers: {
|
||||
...headers,
|
||||
'x-hasura-role': 'public',
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// HTTP link for queries and mutations
|
||||
const httpLink = createHttpLink({
|
||||
uri: 'https://hasura-bc7db43160df.hosted.ghaymah.systems/v1/graphql',
|
||||
|
||||
})
|
||||
|
||||
// WebSocket link for subscriptions
|
||||
const wsLink = new GraphQLWsLink(
|
||||
createClient({
|
||||
url: 'wss://hasura-bc7db43160df.hosted.ghaymah.systems/v1/graphql',
|
||||
connectionParams: async () => {
|
||||
const isAuthenticated = await nhost.auth.isAuthenticatedAsync()
|
||||
|
||||
if (isAuthenticated) {
|
||||
const token = nhost.auth.getAccessToken()
|
||||
return {
|
||||
headers: {
|
||||
authorization: token ? `Bearer ${token}` : '',
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
headers: {
|
||||
'x-hasura-role': 'public',
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
// Split links based on operation type
|
||||
const splitLink = split(
|
||||
({ query }) => {
|
||||
const definition = getMainDefinition(query)
|
||||
return (
|
||||
definition.kind === 'OperationDefinition' &&
|
||||
definition.operation === 'subscription'
|
||||
)
|
||||
},
|
||||
wsLink,
|
||||
authLink.concat(httpLink)
|
||||
)
|
||||
|
||||
// Apollo Client setup
|
||||
const client = new ApolloClient({
|
||||
link: splitLink,
|
||||
cache: new InMemoryCache(),
|
||||
})
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
<NhostProvider nhost={nhost}>
|
||||
<StrictMode>
|
||||
<ApolloProvider client={client}>
|
||||
<App />
|
||||
</ApolloProvider>
|
||||
</StrictMode>
|
||||
</NhostProvider>,
|
||||
)
|
||||
@@ -1,8 +1,212 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { gql } from '@apollo/client';
|
||||
import { useQuery, useMutation } from '@apollo/client/react';
|
||||
|
||||
|
||||
|
||||
import { useUserData, useAuthenticationStatus } from '@nhost/react';
|
||||
|
||||
// GraphQL Queries and Mutations
|
||||
const GET_USER_PROFILE = gql`
|
||||
|
||||
query GetUserProfile{
|
||||
user_profiles{
|
||||
id
|
||||
display_name
|
||||
created_at
|
||||
metadata
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const UPDATE_USER_PROFILE = gql`
|
||||
mutation UpdateUserProfile($userId: uuid!, $displayName: String!, $metadata: jsonb) {
|
||||
update_user_profiles_by_pk(
|
||||
pk_columns: { id: $userId }
|
||||
_set: { display_name: $displayName, metadata: $metadata }
|
||||
) {
|
||||
id
|
||||
display_name
|
||||
metadata
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const GET_USER_CAMPAIGNS = gql`
|
||||
query GetUserCampaigns($userId: uuid!) {
|
||||
campaigns(where: { user_id: { _eq: $userId } }) {
|
||||
id
|
||||
name
|
||||
status
|
||||
keywords_count
|
||||
performance_metrics
|
||||
created_at
|
||||
updated_at
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const CREATE_CAMPAIGN = gql`
|
||||
mutation CreateCampaign($userId: uuid!, $name: String!, $keywords: jsonb) {
|
||||
insert_campaigns_one(object: {
|
||||
user_id: $userId,
|
||||
name: $name,
|
||||
keywords: $keywords,
|
||||
status: "draft",
|
||||
keywords_count: 0
|
||||
}) {
|
||||
id
|
||||
name
|
||||
status
|
||||
created_at
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const UPDATE_CAMPAIGN = gql`
|
||||
mutation UpdateCampaign($campaignId: uuid!, $name: String, $status: String) {
|
||||
update_campaigns_by_pk(
|
||||
pk_columns: { id: $campaignId }
|
||||
_set: { name: $name, status: $status }
|
||||
) {
|
||||
id
|
||||
name
|
||||
status
|
||||
updated_at
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const DELETE_CAMPAIGN = gql`
|
||||
mutation DeleteCampaign($campaignId: uuid!) {
|
||||
delete_campaigns_by_pk(id: $campaignId) {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const Settings = () => {
|
||||
const [activeSection, setActiveSection] = useState("profile");
|
||||
const [activeSubSection, setActiveSubSection] = useState("payment-history");
|
||||
const [formData, setFormData] = useState({
|
||||
displayName: '',
|
||||
email: ''
|
||||
});
|
||||
|
||||
// NHost Authentication
|
||||
const user = useUserData();
|
||||
const { isAuthenticated } = useAuthenticationStatus();
|
||||
|
||||
// GraphQL Queries
|
||||
const { data: profileData, loading: profileLoading, refetch: refetchProfile } = useQuery(GET_USER_PROFILE, {
|
||||
variables: { userId: user?.id },
|
||||
skip: !user?.id
|
||||
});
|
||||
|
||||
const { data: campaignsData, loading: campaignsLoading, refetch: refetchCampaigns } = useQuery(GET_USER_CAMPAIGNS, {
|
||||
variables: { userId: user?.id },
|
||||
skip: !user?.id
|
||||
});
|
||||
|
||||
// GraphQL Mutations
|
||||
const [updateUserProfile, { loading: updatingProfile }] = useMutation(UPDATE_USER_PROFILE);
|
||||
const [createCampaign, { loading: creatingCampaign }] = useMutation(CREATE_CAMPAIGN);
|
||||
const [updateCampaign] = useMutation(UPDATE_CAMPAIGN);
|
||||
const [deleteCampaign] = useMutation(DELETE_CAMPAIGN);
|
||||
|
||||
// Initialize form data when profile loads
|
||||
useEffect(() => {
|
||||
if (profileData?.user_profiles) {
|
||||
const userProfile = profileData.user_profiles[0];
|
||||
setFormData({
|
||||
displayName: userProfile.display_name || '',
|
||||
email: userProfile.email || ''
|
||||
});
|
||||
}
|
||||
}, [profileData]);
|
||||
|
||||
const handleSectionClick = (sectionId: string) => {
|
||||
setActiveSection(sectionId);
|
||||
if (sectionId === "billing") {
|
||||
setActiveSubSection("payment-history");
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubSectionClick = (subSection: string) => {
|
||||
setActiveSubSection(subSection);
|
||||
};
|
||||
|
||||
const handleInputChange = (field: string, value: string) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[field]: value
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSaveProfile = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!user?.id) return;
|
||||
|
||||
try {
|
||||
await updateUserProfile({
|
||||
variables: {
|
||||
userId: user.id,
|
||||
displayName: formData.displayName,
|
||||
metadata: { last_updated: new Date().toISOString() }
|
||||
}
|
||||
});
|
||||
await refetchProfile();
|
||||
// You can add a toast notification here for success
|
||||
} catch (error) {
|
||||
console.error('Error updating profile:', error);
|
||||
// Handle error (show toast, etc.)
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateCampaign = async () => {
|
||||
if (!user?.id) return;
|
||||
|
||||
try {
|
||||
await createCampaign({
|
||||
variables: {
|
||||
userId: user.id,
|
||||
name: `New Campaign ${new Date().toLocaleDateString()}`,
|
||||
keywords: []
|
||||
}
|
||||
});
|
||||
await refetchCampaigns();
|
||||
} catch (error) {
|
||||
console.error('Error creating campaign:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateCampaign = async (campaignId: string, updates: { name?: string; status?: string }) => {
|
||||
try {
|
||||
await updateCampaign({
|
||||
variables: {
|
||||
campaignId,
|
||||
...updates
|
||||
}
|
||||
});
|
||||
await refetchCampaigns();
|
||||
} catch (error) {
|
||||
console.error('Error updating campaign:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteCampaign = async (campaignId: string) => {
|
||||
if (!confirm('Are you sure you want to delete this campaign?')) return;
|
||||
|
||||
try {
|
||||
await deleteCampaign({
|
||||
variables: { campaignId }
|
||||
});
|
||||
await refetchCampaigns();
|
||||
} catch (error) {
|
||||
console.error('Error deleting campaign:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const sidebarItems = [
|
||||
{
|
||||
@@ -66,111 +270,105 @@ const Settings = () => {
|
||||
}
|
||||
];
|
||||
|
||||
const handleSectionClick = (sectionId: string) => {
|
||||
setActiveSection(sectionId);
|
||||
if (sectionId === "billing") {
|
||||
setActiveSubSection("payment-history");
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubSectionClick = (subSection: string) => {
|
||||
setActiveSubSection(subSection);
|
||||
};
|
||||
if (!isAuthenticated) {
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 py-8">
|
||||
<div className="text-center">
|
||||
<h2 className="text-2xl font-bold text-gray-900">Please log in to access settings</h2>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 py-8">
|
||||
<div className="flex flex-col lg:flex-row gap-8">
|
||||
{/* Sidebar */}
|
||||
<div className="w-full lg:w-64 flex-shrink-0">
|
||||
<h1 className="text-2xl sm:text-3xl font-bold text-gray-900 mb-6 lg:mb-8">Settings</h1>
|
||||
|
||||
<nav className="space-y-2">
|
||||
{sidebarItems.map((item) => (
|
||||
<div key={item.id}>
|
||||
<button
|
||||
onClick={() => handleSectionClick(item.id)}
|
||||
className={`w-full flex items-center space-x-3 px-4 py-3 rounded-lg text-left transition-colors ${
|
||||
activeSection === item.id
|
||||
? "bg-blue-50 text-blue-600"
|
||||
: "text-gray-700 hover:bg-gray-50"
|
||||
}`}
|
||||
>
|
||||
{item.icon}
|
||||
<span className="font-medium">{item.label}</span>
|
||||
{item.hasSubItems && (
|
||||
<svg className="w-4 h-4 ml-auto" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clipRule="evenodd" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{item.hasSubItems && activeSection === item.id && (
|
||||
<div className="ml-8 mt-2 space-y-1">
|
||||
{item.subItems?.map((subItem) => (
|
||||
<button
|
||||
key={subItem}
|
||||
onClick={() => handleSubSectionClick(subItem.toLowerCase().replace(/\s+/g, "-"))}
|
||||
className={`w-full flex items-center px-4 py-2 rounded-lg text-left transition-colors ${
|
||||
activeSubSection === subItem.toLowerCase().replace(/\s+/g, "-")
|
||||
? "bg-blue-600 text-white"
|
||||
: "text-gray-600 hover:bg-gray-50"
|
||||
}`}
|
||||
>
|
||||
{activeSubSection === subItem.toLowerCase().replace(/\s+/g, "-") && (
|
||||
<div className="w-1 h-6 bg-blue-600 rounded-full mr-3"></div>
|
||||
)}
|
||||
<span className="text-sm">{subItem}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 py-8">
|
||||
<div className="flex flex-col lg:flex-row gap-8">
|
||||
{/* Sidebar */}
|
||||
<div className="w-full lg:w-64 flex-shrink-0">
|
||||
<h1 className="text-2xl sm:text-3xl font-bold text-gray-900 mb-6 lg:mb-8">Settings</h1>
|
||||
|
||||
<nav className="space-y-2">
|
||||
{sidebarItems.map((item) => (
|
||||
<div key={item.id}>
|
||||
<button
|
||||
onClick={() => handleSectionClick(item.id)}
|
||||
className={`w-full flex items-center space-x-3 px-4 py-3 rounded-lg text-left transition-colors ${
|
||||
activeSection === item.id
|
||||
? "bg-blue-50 text-blue-600"
|
||||
: "text-gray-700 hover:bg-gray-50"
|
||||
}`}
|
||||
>
|
||||
{item.icon}
|
||||
<span className="font-medium">{item.label}</span>
|
||||
{item.hasSubItems && (
|
||||
<svg className="w-4 h-4 ml-auto" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clipRule="evenodd" />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{item.hasSubItems && activeSection === item.id && (
|
||||
<div className="ml-8 mt-2 space-y-1">
|
||||
{item.subItems?.map((subItem) => (
|
||||
<button
|
||||
key={subItem}
|
||||
onClick={() => handleSubSectionClick(subItem.toLowerCase().replace(/\s+/g, "-"))}
|
||||
className={`w-full flex items-center px-4 py-2 rounded-lg text-left transition-colors ${
|
||||
activeSubSection === subItem.toLowerCase().replace(/\s+/g, "-")
|
||||
? "bg-blue-600 text-white"
|
||||
: "text-gray-600 hover:bg-gray-50"
|
||||
}`}
|
||||
>
|
||||
{activeSubSection === subItem.toLowerCase().replace(/\s+/g, "-") && (
|
||||
<div className="w-1 h-6 bg-blue-600 rounded-full mr-3"></div>
|
||||
)}
|
||||
<span className="text-sm">{subItem}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="flex-1">
|
||||
{activeSection === "profile" && (
|
||||
<div className="bg-white">
|
||||
<h2 className="text-3xl font-bold text-gray-900 mb-2">Basic Information</h2>
|
||||
<p className="text-gray-600 mb-8">
|
||||
View and update your personal details and account information.
|
||||
</p>
|
||||
{/* Main Content */}
|
||||
<div className="flex-1">
|
||||
{activeSection === "profile" && (
|
||||
<div className="bg-white">
|
||||
<h2 className="text-3xl font-bold text-gray-900 mb-2">Basic Information</h2>
|
||||
<p className="text-gray-600 mb-8">
|
||||
View and update your personal details and account information.
|
||||
</p>
|
||||
|
||||
<form className="space-y-6">
|
||||
{profileLoading ? (
|
||||
<div className="text-center py-8">Loading profile...</div>
|
||||
) : (
|
||||
<form onSubmit={handleSaveProfile} className="space-y-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
defaultValue="Eugiene Jonathan"
|
||||
value={formData.displayName}
|
||||
onChange={(e) => handleInputChange('displayName', e.target.value)}
|
||||
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{/* <div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
defaultValue="Eugiene Jonathan"
|
||||
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
value={formData.email}
|
||||
disabled
|
||||
className="w-full px-4 py-3 border border-gray-300 rounded-lg bg-gray-50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
defaultValue="Eugiene Jonathan"
|
||||
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 mt-1">Email cannot be changed</p>
|
||||
</div> */}
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
@@ -178,88 +376,84 @@ const Settings = () => {
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
defaultValue="12 Aug 2025"
|
||||
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span>Total Keywords Campaigns</span>
|
||||
<svg className="w-4 h-4 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-3a1 1 0 00-.867.5 1 1 0 11-1.731-1A3 3 0 0113 8a3.001 3.001 0 01-2 2.83V11a1 1 0 11-2 0v-1a1 1 0 011-1 1 1 0 100-2zm0 8a1 1 0 100-2 1 1 0 000 2z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
defaultValue="234,029"
|
||||
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
value={new Date(profileData?.user_profiles[0]?.created_at).toLocaleDateString() || 'N/A'}
|
||||
disabled
|
||||
className="w-full px-4 py-3 border border-gray-300 rounded-lg bg-gray-50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700 transition-colors"
|
||||
disabled={updatingProfile}
|
||||
className="bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50"
|
||||
>
|
||||
Save
|
||||
{updatingProfile ? 'Saving...' : 'Save'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeSection === "campaigns" && (
|
||||
<div className="bg-white">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-8">
|
||||
<div>
|
||||
<h2 className="text-2xl sm:text-3xl font-bold text-gray-900 mb-2">Campaigns</h2>
|
||||
<p className="text-sm sm:text-base text-gray-600">
|
||||
Manage your keyword campaigns and track performance.
|
||||
</p>
|
||||
</div>
|
||||
<button className="bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700 transition-colors whitespace-nowrap self-start sm:self-auto">
|
||||
Create Campaign
|
||||
</button>
|
||||
{activeSection === "campaigns" && (
|
||||
<div className="bg-white">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-8">
|
||||
<div>
|
||||
<h2 className="text-2xl sm:text-3xl font-bold text-gray-900 mb-2">Campaigns</h2>
|
||||
<p className="text-sm sm:text-base text-gray-600">
|
||||
Manage your keyword campaigns and track performance.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleCreateCampaign}
|
||||
disabled={creatingCampaign}
|
||||
className="bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700 transition-colors whitespace-nowrap self-start sm:self-auto disabled:opacity-50"
|
||||
>
|
||||
{creatingCampaign ? 'Creating...' : 'Create Campaign'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{campaignsLoading ? (
|
||||
<div className="text-center py-8">Loading campaigns...</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{[
|
||||
{ name: "Summer Sale 2025", keywords: 127, status: "Active", performance: "+12.5%" },
|
||||
{ name: "Product Launch Q3", keywords: 89, status: "Active", performance: "+8.3%" },
|
||||
{ name: "Brand Awareness", keywords: 234, status: "Paused", performance: "+5.2%" },
|
||||
{ name: "Holiday Campaign", keywords: 156, status: "Active", performance: "+15.7%" },
|
||||
].map((campaign, index) => (
|
||||
<div key={index} className="border border-gray-200 rounded-lg p-4 sm:p-6 hover:border-blue-300 transition-colors">
|
||||
{campaignsData?.campaigns?.map((campaign: any) => (
|
||||
<div key={campaign.id} className="border border-gray-200 rounded-lg p-4 sm:p-6 hover:border-blue-300 transition-colors">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div className="flex-1">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4 mb-2">
|
||||
<h3 className="text-base sm:text-lg font-semibold text-gray-900">{campaign.name}</h3>
|
||||
<span className={`px-3 py-1 rounded-full text-xs font-medium self-start ${
|
||||
campaign.status === "Active"
|
||||
campaign.status === "active"
|
||||
? "bg-green-100 text-green-700"
|
||||
: "bg-yellow-100 text-yellow-700"
|
||||
: campaign.status === "paused"
|
||||
? "bg-yellow-100 text-yellow-700"
|
||||
: "bg-gray-100 text-gray-700"
|
||||
}`}>
|
||||
{campaign.status}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-6 text-sm text-gray-600">
|
||||
<span>{campaign.keywords} keywords</span>
|
||||
<span className="flex items-center space-x-1">
|
||||
<span>Performance:</span>
|
||||
<span className="text-green-600 font-medium">{campaign.performance}</span>
|
||||
</span>
|
||||
<span>{campaign.keywords_count || 0} keywords</span>
|
||||
<span>Created: {new Date(campaign.created_at).toLocaleDateString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<button className="p-2 text-gray-400 hover:text-blue-600 transition-colors">
|
||||
<button
|
||||
onClick={() => handleUpdateCampaign(campaign.id, {
|
||||
name: prompt('Enter new campaign name:', campaign.name) || campaign.name
|
||||
})}
|
||||
className="p-2 text-gray-400 hover:text-blue-600 transition-colors"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button className="p-2 text-gray-400 hover:text-red-600 transition-colors">
|
||||
<button
|
||||
onClick={() => handleDeleteCampaign(campaign.id)}
|
||||
className="p-2 text-gray-400 hover:text-red-600 transition-colors"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clipRule="evenodd" />
|
||||
</svg>
|
||||
@@ -268,12 +462,19 @@ const Settings = () => {
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{(!campaignsData?.campaigns || campaignsData.campaigns.length === 0) && (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
No campaigns found. Create your first campaign!
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
1021
src/pages/login.tsx
1021
src/pages/login.tsx
تم حذف اختلاف الملف لأن الملف كبير جداً
تحميل الاختلاف
213
src/pages/useAuth.tsx
Normal file
213
src/pages/useAuth.tsx
Normal file
@@ -0,0 +1,213 @@
|
||||
import { useState } from 'react';
|
||||
import { useNhostClient } from '@nhost/react';
|
||||
|
||||
export const useAuth = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const nhost = useNhostClient();
|
||||
|
||||
const handleLogin = async (email: string, password: string) => {
|
||||
setIsLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
const { session, error } = await nhost.auth.signIn({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw new Error(error.message || 'Failed to login. Please check your credentials.');
|
||||
}
|
||||
|
||||
if (session) {
|
||||
const accessToken = session.accessToken;
|
||||
const refreshToken = session.refreshToken;
|
||||
const userId = session.user?.id;
|
||||
|
||||
// Store user data in localStorage
|
||||
const userData = {
|
||||
email,
|
||||
accessToken,
|
||||
userId,
|
||||
isLoggedIn: true,
|
||||
lastLogin: new Date().toISOString()
|
||||
};
|
||||
|
||||
localStorage.setItem('sp_user', JSON.stringify(userData));
|
||||
localStorage.setItem('user_id', userId);
|
||||
window.dispatchEvent(new Event('userChanged'));
|
||||
|
||||
// Store tokens in HttpOnly cookies (if still needed)
|
||||
try {
|
||||
await fetch('/api/auth/store-tokens', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({
|
||||
access_token: accessToken,
|
||||
refresh_token: refreshToken,
|
||||
expires_in: 3600
|
||||
})
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error storing token in HttpOnly cookie:', error);
|
||||
}
|
||||
|
||||
return { success: true, data: { session } };
|
||||
} else {
|
||||
throw new Error('Login failed: No session returned');
|
||||
}
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'An unknown error occurred';
|
||||
setError(errorMessage);
|
||||
return { success: false, error: errorMessage };
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSignUp = async (email: string, password: string) => {
|
||||
setIsLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
const { session, error } = await nhost.auth.signUp({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw new Error(error.message || 'Signup failed. Please check your info.');
|
||||
}
|
||||
|
||||
if (session) {
|
||||
const accessToken = session.accessToken;
|
||||
const refreshToken = session.refreshToken;
|
||||
const userId = session.user?.id;
|
||||
|
||||
const userData = {
|
||||
email,
|
||||
accessToken,
|
||||
userId,
|
||||
isLoggedIn: true,
|
||||
lastLogin: new Date().toISOString()
|
||||
};
|
||||
|
||||
localStorage.setItem('sp_user', JSON.stringify(userData));
|
||||
localStorage.setItem('user_id', userId);
|
||||
window.dispatchEvent(new Event('userChanged'));
|
||||
|
||||
// Store tokens in HttpOnly cookies (if still needed)
|
||||
try {
|
||||
await fetch('/api/auth/store-tokens', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({
|
||||
access_token: accessToken,
|
||||
refresh_token: refreshToken,
|
||||
expires_in: 3600
|
||||
})
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error storing token in HttpOnly cookie:', error);
|
||||
}
|
||||
|
||||
return { success: true, data: { session } };
|
||||
} else {
|
||||
// Note: NHost may not return a session immediately if email verification is required
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
message: 'Signup successful! Please check your email for verification.'
|
||||
}
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'An unknown error occurred';
|
||||
setError(errorMessage);
|
||||
return { success: false, error: errorMessage };
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleForgotPassword = async (email: string) => {
|
||||
setIsLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
const { error } = await nhost.auth.resetPassword({ email });
|
||||
|
||||
if (error) {
|
||||
throw new Error(error.message || 'Failed to send reset email.');
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'If the email exists, a reset link has been sent.'
|
||||
};
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'An unknown error occurred';
|
||||
setError(errorMessage);
|
||||
return { success: false, error: errorMessage };
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Additional NHost auth methods you might find useful
|
||||
const handleSignOut = async () => {
|
||||
setIsLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
const { error } = await nhost.auth.signOut();
|
||||
|
||||
if (error) {
|
||||
throw new Error(error.message || 'Failed to sign out.');
|
||||
}
|
||||
|
||||
// Clear local storage
|
||||
localStorage.removeItem('sp_user');
|
||||
localStorage.removeItem('user_id');
|
||||
window.dispatchEvent(new Event('userChanged'));
|
||||
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'An unknown error occurred';
|
||||
setError(errorMessage);
|
||||
return { success: false, error: errorMessage };
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChangePassword = async (newPassword: string) => {
|
||||
setIsLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
const { error } = await nhost.auth.changePassword({ newPassword });
|
||||
|
||||
if (error) {
|
||||
throw new Error(error.message || 'Failed to change password.');
|
||||
}
|
||||
|
||||
return { success: true, message: 'Password changed successfully.' };
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'An unknown error occurred';
|
||||
setError(errorMessage);
|
||||
return { success: false, error: errorMessage };
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
error,
|
||||
handleLogin,
|
||||
handleSignUp,
|
||||
handleForgotPassword,
|
||||
handleSignOut,
|
||||
handleChangePassword,
|
||||
setError
|
||||
};
|
||||
};
|
||||
المرجع في مشكلة جديدة
حظر مستخدم