Compare commits

4 الالتزامات

المؤلف SHA1 الرسالة التاريخ
Your Name
93d9a598a8 Merge branch 'main' of https://app.gitpasha.com/mohammedsaid18/dashboard 2025-10-11 07:30:49 +00:00
Your Name
b9c53be445 Merge branch 'main' of https://app.gitpasha.com/mohammedsaid18/dashboard 2025-10-10 15:53:32 +00:00
Your Name
9230fd8e4e connect to campaign graphql 2025-10-08 10:36:57 +00:00
Your Name
a837154231 working nhost client 2025-10-08 09:29:32 +00:00
11 ملفات معدلة مع 1538 إضافات و801 حذوفات

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
عرض الملف

@@ -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
عرض الملف

@@ -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 مولّد
عرض الملف

@@ -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>

عرض الملف

@@ -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>
);
};

تم حذف اختلاف الملف لأن الملف كبير جداً تحميل الاختلاف

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
};
};