basic clerk auth
need polishing
This commit is contained in:
parent
ca790a7b97
commit
73907568ef
2
apps/admin/.gitignore
vendored
2
apps/admin/.gitignore
vendored
@ -43,3 +43,5 @@ android/app/build/generated/
|
|||||||
|
|
||||||
# Bundle artifact
|
# Bundle artifact
|
||||||
*.jsbundle
|
*.jsbundle
|
||||||
|
# clerk configuration (can include secrets)
|
||||||
|
/.clerk/
|
||||||
|
|||||||
Binary file not shown.
12
apps/admin/middleware.ts
Normal file
12
apps/admin/middleware.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { clerkMiddleware } from '@clerk/nextjs/server'
|
||||||
|
|
||||||
|
export default clerkMiddleware()
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
matcher: [
|
||||||
|
// Skip Next.js internals and all static files, unless found in search params
|
||||||
|
'/((?!_next|[^?]*\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
|
||||||
|
// Always run for API routes
|
||||||
|
'/(api|trpc)(.*)',
|
||||||
|
],
|
||||||
|
}
|
||||||
172
apps/admin/package-lock.json
generated
172
apps/admin/package-lock.json
generated
@ -8,6 +8,7 @@
|
|||||||
"name": "@fitai/admin",
|
"name": "@fitai/admin",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@clerk/nextjs": "^6.34.5",
|
||||||
"@fitai/shared": "file:../../packages/shared",
|
"@fitai/shared": "file:../../packages/shared",
|
||||||
"@hookform/resolvers": "^5.2.2",
|
"@hookform/resolvers": "^5.2.2",
|
||||||
"@tailwindcss/postcss": "^4.1.17",
|
"@tailwindcss/postcss": "^4.1.17",
|
||||||
@ -600,6 +601,103 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@clerk/backend": {
|
||||||
|
"version": "2.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@clerk/backend/-/backend-2.20.0.tgz",
|
||||||
|
"integrity": "sha512-RcZN7CAxGkkLydGtWpxCyq4C0pSo/1ch0LJMDQnckrt10Jx8mAjwce2nZQa2xRykxsOla4+boF9a5kDw3nUvVg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@clerk/shared": "^3.31.1",
|
||||||
|
"@clerk/types": "^4.97.2",
|
||||||
|
"cookie": "1.0.2",
|
||||||
|
"standardwebhooks": "^1.0.0",
|
||||||
|
"tslib": "2.8.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.17.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@clerk/clerk-react": {
|
||||||
|
"version": "5.53.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@clerk/clerk-react/-/clerk-react-5.53.8.tgz",
|
||||||
|
"integrity": "sha512-TOiYk31rQUL9JOKZr/fhajf+fQCHicy1J4Rxq7vqtjHseJsnIBjzTigjOap/w8PrDAF28O6dbPC5CA0Tp7Md8w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@clerk/shared": "^3.31.1",
|
||||||
|
"tslib": "2.8.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.17.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18.0.0 || ^19.0.0 || ^19.0.0-0",
|
||||||
|
"react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@clerk/nextjs": {
|
||||||
|
"version": "6.34.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@clerk/nextjs/-/nextjs-6.34.5.tgz",
|
||||||
|
"integrity": "sha512-f1OyucHc5HHBZovzEtJrPR0MUePZxEH2mqu3dt24iGTWTmV2UPnHMB5uSi4XVSWcungnzHWKgTKnHKTVF3vxUA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@clerk/backend": "^2.20.0",
|
||||||
|
"@clerk/clerk-react": "^5.53.8",
|
||||||
|
"@clerk/shared": "^3.31.1",
|
||||||
|
"@clerk/types": "^4.97.2",
|
||||||
|
"server-only": "0.0.1",
|
||||||
|
"tslib": "2.8.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.17.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"next": "^13.5.7 || ^14.2.25 || ^15.2.3 || ^16",
|
||||||
|
"react": "^18.0.0 || ^19.0.0 || ^19.0.0-0",
|
||||||
|
"react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@clerk/shared": {
|
||||||
|
"version": "3.31.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@clerk/shared/-/shared-3.31.1.tgz",
|
||||||
|
"integrity": "sha512-mqxZqlzLJYJxA+ryLzhwFR0eO73teAvRd+wvA8bLUZLYvCRFvaiHsB9dEvbo9Z5bMYdq3NPwnx2uljMuu/tiQw==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"csstype": "3.1.3",
|
||||||
|
"dequal": "2.0.3",
|
||||||
|
"glob-to-regexp": "0.4.1",
|
||||||
|
"js-cookie": "3.0.5",
|
||||||
|
"std-env": "^3.9.0",
|
||||||
|
"swr": "2.3.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.17.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18.0.0 || ^19.0.0 || ^19.0.0-0",
|
||||||
|
"react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@clerk/types": {
|
||||||
|
"version": "4.97.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@clerk/types/-/types-4.97.2.tgz",
|
||||||
|
"integrity": "sha512-xnJq3xzpmuuDnNnWuUMKJLPPkaEaLDM0kiv2Hm0gKIcL1+1P3VaGf2vL9roIhmhLswB2PUwtVvZKBmGjT5yOVw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@clerk/shared": "^3.31.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.17.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@emnapi/core": {
|
"node_modules/@emnapi/core": {
|
||||||
"version": "1.7.0",
|
"version": "1.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.0.tgz",
|
||||||
@ -2203,6 +2301,12 @@
|
|||||||
"@sinonjs/commons": "^3.0.1"
|
"@sinonjs/commons": "^3.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@stablelib/base64": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@standard-schema/spec": {
|
"node_modules/@standard-schema/spec": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
|
||||||
@ -4607,6 +4711,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/cookie": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
@ -4633,7 +4746,6 @@
|
|||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/d3-array": {
|
"node_modules/d3-array": {
|
||||||
@ -4954,9 +5066,7 @@
|
|||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
||||||
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
|
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
@ -5933,6 +6043,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/fast-sha256": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==",
|
||||||
|
"license": "Unlicense"
|
||||||
|
},
|
||||||
"node_modules/fastq": {
|
"node_modules/fastq": {
|
||||||
"version": "1.19.1",
|
"version": "1.19.1",
|
||||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
|
||||||
@ -6414,6 +6530,12 @@
|
|||||||
"node": ">=10.13.0"
|
"node": ">=10.13.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/glob-to-regexp": {
|
||||||
|
"version": "0.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
|
||||||
|
"integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
|
||||||
|
"license": "BSD-2-Clause"
|
||||||
|
},
|
||||||
"node_modules/glob/node_modules/brace-expansion": {
|
"node_modules/glob/node_modules/brace-expansion": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||||
@ -8333,6 +8455,15 @@
|
|||||||
"jiti": "lib/jiti-cli.mjs"
|
"jiti": "lib/jiti-cli.mjs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/js-tokens": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
@ -10652,6 +10783,12 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/server-only": {
|
||||||
|
"version": "0.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz",
|
||||||
|
"integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/set-blocking": {
|
"node_modules/set-blocking": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||||
@ -11091,6 +11228,22 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/standardwebhooks": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@stablelib/base64": "^1.0.0",
|
||||||
|
"fast-sha256": "^1.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/std-env": {
|
||||||
|
"version": "3.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
|
||||||
|
"integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/stop-iteration-iterator": {
|
"node_modules/stop-iteration-iterator": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
|
||||||
@ -11433,6 +11586,19 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/swr": {
|
||||||
|
"version": "2.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/swr/-/swr-2.3.4.tgz",
|
||||||
|
"integrity": "sha512-bYd2lrhc+VarcpkgWclcUi92wYCpOgMws9Sd1hG1ntAu0NEy+14CbotuFjshBU2kt9rYj9TSmDcybpxpeTU1fg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"dequal": "^2.0.3",
|
||||||
|
"use-sync-external-store": "^1.4.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/synckit": {
|
"node_modules/synckit": {
|
||||||
"version": "0.11.11",
|
"version": "0.11.11",
|
||||||
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz",
|
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz",
|
||||||
|
|||||||
@ -11,6 +11,7 @@
|
|||||||
"test": "jest"
|
"test": "jest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@clerk/nextjs": "^6.34.5",
|
||||||
"@fitai/shared": "file:../../packages/shared",
|
"@fitai/shared": "file:../../packages/shared",
|
||||||
"@hookform/resolvers": "^5.2.2",
|
"@hookform/resolvers": "^5.2.2",
|
||||||
"@tailwindcss/postcss": "^4.1.17",
|
"@tailwindcss/postcss": "^4.1.17",
|
||||||
|
|||||||
@ -1,22 +1,27 @@
|
|||||||
import type { Metadata } from 'next'
|
import type { Metadata } from "next";
|
||||||
import { Inter } from 'next/font/google'
|
import { Inter } from "next/font/google";
|
||||||
import './globals.css'
|
import "./globals.css";
|
||||||
|
import { ClerkProvider } from "@clerk/nextjs";
|
||||||
|
|
||||||
const inter = Inter({ subsets: ['latin'] })
|
const inter = Inter({ subsets: ["latin"] });
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'FitAI Admin',
|
title: "FitAI Admin",
|
||||||
description: 'Fitness management admin dashboard',
|
description: "Fitness management admin dashboard",
|
||||||
}
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<ClerkProvider
|
||||||
<body className={inter.className}>{children}</body>
|
publishableKey={process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY}
|
||||||
</html>
|
>
|
||||||
)
|
<html lang="en">
|
||||||
|
<body className={inter.className}>{children}</body>
|
||||||
|
</html>
|
||||||
|
</ClerkProvider>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@ -3,6 +3,7 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { UserManagement } from "@/components/users/UserManagement";
|
import { UserManagement } from "@/components/users/UserManagement";
|
||||||
import { AnalyticsDashboard } from "@/components/analytics/AnalyticsDashboard";
|
import { AnalyticsDashboard } from "@/components/analytics/AnalyticsDashboard";
|
||||||
|
import { SignedIn, SignedOut, SignInButton, UserButton } from "@clerk/nextjs";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
@ -12,56 +13,65 @@ export default function Home() {
|
|||||||
<h1 className="text-4xl font-bold text-gray-900">
|
<h1 className="text-4xl font-bold text-gray-900">
|
||||||
FitAI Admin Dashboard
|
FitAI Admin Dashboard
|
||||||
</h1>
|
</h1>
|
||||||
<nav className="flex gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Link
|
<SignedOut>
|
||||||
href="/users"
|
<SignInButton />
|
||||||
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
|
</SignedOut>
|
||||||
>
|
<SignedIn>
|
||||||
User Management
|
<UserButton />
|
||||||
</Link>
|
</SignedIn>
|
||||||
<Link
|
</div>
|
||||||
href="/analytics"
|
|
||||||
className="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 transition-colors"
|
|
||||||
>
|
|
||||||
Analytics
|
|
||||||
</Link>
|
|
||||||
</nav>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
|
<SignedIn>
|
||||||
<div className="bg-white rounded-lg shadow p-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
|
||||||
<h2 className="text-xl font-semibold mb-4">Client Management</h2>
|
<div className="bg-white rounded-lg shadow p-6">
|
||||||
<p className="text-gray-600">
|
<h2 className="text-xl font-semibold mb-4">Client Management</h2>
|
||||||
Manage fitness clients and their profiles
|
<p className="text-gray-600">
|
||||||
|
Manage fitness clients and their profiles
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white rounded-lg shadow p-6">
|
||||||
|
<h2 className="text-xl font-semibold mb-4">Payment Tracking</h2>
|
||||||
|
<p className="text-gray-600">
|
||||||
|
Monitor payments and subscriptions
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white rounded-lg shadow p-6">
|
||||||
|
<h2 className="text-xl font-semibold mb-4">Attendance</h2>
|
||||||
|
<p className="text-gray-600">
|
||||||
|
Track client attendance and habits
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
<div className="bg-white rounded-lg shadow p-6">
|
||||||
|
<h2 className="text-2xl font-semibold mb-6">
|
||||||
|
Recent User Activity
|
||||||
|
</h2>
|
||||||
|
<div>
|
||||||
|
<UserManagement />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-lg shadow p-6">
|
||||||
|
<h2 className="text-2xl font-semibold mb-6">Quick Analytics</h2>
|
||||||
|
<div>
|
||||||
|
<AnalyticsDashboard />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SignedIn>
|
||||||
|
|
||||||
|
<SignedOut>
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-gray-600 mb-4">
|
||||||
|
Please sign in to access the admin dashboard.
|
||||||
</p>
|
</p>
|
||||||
|
<SignInButton />
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-white rounded-lg shadow p-6">
|
</SignedOut>
|
||||||
<h2 className="text-xl font-semibold mb-4">Payment Tracking</h2>
|
|
||||||
<p className="text-gray-600">Monitor payments and subscriptions</p>
|
|
||||||
</div>
|
|
||||||
<div className="bg-white rounded-lg shadow p-6">
|
|
||||||
<h2 className="text-xl font-semibold mb-4">Attendance</h2>
|
|
||||||
<p className="text-gray-600">Track client attendance and habits</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
||||||
<div className="bg-white rounded-lg shadow p-6">
|
|
||||||
<h2 className="text-2xl font-semibold mb-6">
|
|
||||||
Recent User Activity
|
|
||||||
</h2>
|
|
||||||
<div>
|
|
||||||
<UserManagement />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white rounded-lg shadow p-6">
|
|
||||||
<h2 className="text-2xl font-semibold mb-6">Quick Analytics</h2>
|
|
||||||
<div>
|
|
||||||
<AnalyticsDashboard />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { AgCharts } from "ag-charts-react";
|
|||||||
import { AgChartOptions } from "ag-charts-community";
|
import { AgChartOptions } from "ag-charts-community";
|
||||||
|
|
||||||
interface BarData {
|
interface BarData {
|
||||||
category: string;
|
label: string;
|
||||||
value: number;
|
value: number;
|
||||||
color?: string;
|
color?: string;
|
||||||
}
|
}
|
||||||
@ -30,7 +30,7 @@ export function RevenueChart({
|
|||||||
series: [
|
series: [
|
||||||
{
|
{
|
||||||
type: "bar",
|
type: "bar",
|
||||||
xKey: "category",
|
xKey: "label",
|
||||||
yKey: "value",
|
yKey: "value",
|
||||||
fills: data.map((item) => item.color || "#10b981"),
|
fills: data.map((item) => item.color || "#10b981"),
|
||||||
strokes: ["#ffffff"],
|
strokes: ["#ffffff"],
|
||||||
@ -55,7 +55,7 @@ export function RevenueChart({
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
renderer: (params: any) => {
|
renderer: (params: any) => {
|
||||||
return `<div class="bg-white p-2 rounded shadow-lg border">
|
return `<div class="bg-white p-2 rounded shadow-lg border">
|
||||||
<div class="font-bold">${params.datum.category}</div>
|
<div class="font-bold">${params.datum.label}</div>
|
||||||
<div class="text-sm">Revenue: $${params.datum.value.toLocaleString()}</div>
|
<div class="text-sm">Revenue: $${params.datum.value.toLocaleString()}</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
},
|
},
|
||||||
|
|||||||
@ -163,14 +163,17 @@ export function UserGrid({
|
|||||||
const gridRef = React.useRef<AgGridReact<User>>(null);
|
const gridRef = React.useRef<AgGridReact<User>>(null);
|
||||||
|
|
||||||
const gridOptions = {
|
const gridOptions = {
|
||||||
theme: "legacy",
|
theme: "legacy" as const,
|
||||||
columnDefs,
|
columnDefs,
|
||||||
defaultColDef,
|
defaultColDef,
|
||||||
rowData: users,
|
rowData: users,
|
||||||
rowSelection: "multiple",
|
rowSelection: { mode: "multiRow" as const },
|
||||||
onSelectionChanged: () => {
|
onSelectionChanged: () => {
|
||||||
const selectedNodes = gridRef.current?.api.getSelectedNodes();
|
const selectedNodes = gridRef.current?.api.getSelectedNodes();
|
||||||
const selectedData = selectedNodes?.map((node) => node.data) || [];
|
const selectedData =
|
||||||
|
selectedNodes
|
||||||
|
?.map((node) => node.data)
|
||||||
|
.filter((data): data is User => data !== undefined) || [];
|
||||||
setSelectedUsers(selectedData);
|
setSelectedUsers(selectedData);
|
||||||
if (selectedData.length === 1 && onUserSelect) {
|
if (selectedData.length === 1 && onUserSelect) {
|
||||||
onUserSelect(selectedData[0]);
|
onUserSelect(selectedData[0]);
|
||||||
|
|||||||
@ -191,14 +191,14 @@ export function UserManagement() {
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={handleEditUser}
|
onClick={() => handleEditUser(selectedUser!)}
|
||||||
disabled={!selectedUser}
|
disabled={!selectedUser}
|
||||||
>
|
>
|
||||||
Edit User
|
Edit User
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={handleDeleteUser}
|
onClick={() => handleDeleteUser(selectedUser!)}
|
||||||
disabled={!selectedUser}
|
disabled={!selectedUser}
|
||||||
>
|
>
|
||||||
Delete User
|
Delete User
|
||||||
|
|||||||
2107
apps/mobile/package-lock.json
generated
2107
apps/mobile/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -14,6 +14,7 @@
|
|||||||
"test": "jest"
|
"test": "jest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@clerk/clerk-expo": "^2.18.3",
|
||||||
"@expo/vector-icons": "^15.0.0",
|
"@expo/vector-icons": "^15.0.0",
|
||||||
"@hookform/resolvers": "^3.3.0",
|
"@hookform/resolvers": "^3.3.0",
|
||||||
"@tanstack/react-query": "^5.0.0",
|
"@tanstack/react-query": "^5.0.0",
|
||||||
@ -21,12 +22,15 @@
|
|||||||
"ajv-keywords": "^5.1.0",
|
"ajv-keywords": "^5.1.0",
|
||||||
"axios": "^1.6.0",
|
"axios": "^1.6.0",
|
||||||
"expo": "~54.0.23",
|
"expo": "~54.0.23",
|
||||||
|
"expo-auth-session": "^7.0.8",
|
||||||
"expo-camera": "~17.0.0",
|
"expo-camera": "~17.0.0",
|
||||||
"expo-linking": "~8.0.0",
|
"expo-linking": "~8.0.0",
|
||||||
"expo-notifications": "~0.32.0",
|
"expo-notifications": "~0.32.0",
|
||||||
"expo-router": "~6.0.14",
|
"expo-router": "~6.0.14",
|
||||||
"expo-secure-store": "~15.0.7",
|
"expo-secure-store": "~15.0.7",
|
||||||
|
"expo-web-browser": "^15.0.9",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
|
"react-dom": "19.1.0",
|
||||||
"react-hook-form": "^7.47.0",
|
"react-hook-form": "^7.47.0",
|
||||||
"react-native": "0.81.5",
|
"react-native": "0.81.5",
|
||||||
"react-native-safe-area-context": "~5.6.0",
|
"react-native-safe-area-context": "~5.6.0",
|
||||||
@ -35,15 +39,15 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.20.0",
|
"@babel/core": "^7.20.0",
|
||||||
|
"@testing-library/react-native": "^12.4.0",
|
||||||
"@types/react": "~19.1.10",
|
"@types/react": "~19.1.10",
|
||||||
"@types/react-native": "^0.73.0",
|
"@types/react-native": "^0.73.0",
|
||||||
"typescript": "^5.1.3",
|
|
||||||
"eslint": "^8.45.0",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||||
"@typescript-eslint/parser": "^6.0.0",
|
"@typescript-eslint/parser": "^6.0.0",
|
||||||
|
"babel-preset-expo": "~54.0.7",
|
||||||
|
"eslint": "^8.45.0",
|
||||||
"jest": "^29.2.1",
|
"jest": "^29.2.1",
|
||||||
"@testing-library/react-native": "^12.4.0",
|
|
||||||
"react-test-renderer": "19.1.0",
|
"react-test-renderer": "19.1.0",
|
||||||
"babel-preset-expo": "~54.0.7"
|
"typescript": "^5.1.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { Tabs } from 'expo-router'
|
import { Tabs } from "expo-router";
|
||||||
import { Ionicons } from '@expo/vector-icons'
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
|
|
||||||
export default function TabLayout() {
|
export default function TabLayout() {
|
||||||
return (
|
return (
|
||||||
@ -7,7 +7,7 @@ export default function TabLayout() {
|
|||||||
<Tabs.Screen
|
<Tabs.Screen
|
||||||
name="index"
|
name="index"
|
||||||
options={{
|
options={{
|
||||||
title: 'Home',
|
title: "Home",
|
||||||
tabBarIcon: ({ color, size }) => (
|
tabBarIcon: ({ color, size }) => (
|
||||||
<Ionicons name="home" size={size} color={color} />
|
<Ionicons name="home" size={size} color={color} />
|
||||||
),
|
),
|
||||||
@ -16,7 +16,7 @@ export default function TabLayout() {
|
|||||||
<Tabs.Screen
|
<Tabs.Screen
|
||||||
name="profile"
|
name="profile"
|
||||||
options={{
|
options={{
|
||||||
title: 'Profile',
|
title: "Profile",
|
||||||
tabBarIcon: ({ color, size }) => (
|
tabBarIcon: ({ color, size }) => (
|
||||||
<Ionicons name="person" size={size} color={color} />
|
<Ionicons name="person" size={size} color={color} />
|
||||||
),
|
),
|
||||||
@ -25,12 +25,12 @@ export default function TabLayout() {
|
|||||||
<Tabs.Screen
|
<Tabs.Screen
|
||||||
name="attendance"
|
name="attendance"
|
||||||
options={{
|
options={{
|
||||||
title: 'Attendance',
|
title: "Attendance",
|
||||||
tabBarIcon: ({ color, size }) => (
|
tabBarIcon: ({ color, size }) => (
|
||||||
<Ionicons name="calendar" size={size} color={color} />
|
<Ionicons name="calendar" size={size} color={color} />
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
@ -1,33 +1,32 @@
|
|||||||
import React from 'react'
|
import React, { useEffect } from "react";
|
||||||
import { View, Text, StyleSheet, TouchableOpacity, Alert } from 'react-native'
|
import { View, Text, StyleSheet, TouchableOpacity } from "react-native";
|
||||||
import { useAuth } from '@/contexts/AuthContext'
|
import { useUser, useAuth } from "@clerk/clerk-expo";
|
||||||
import { useRouter } from 'expo-router'
|
import { useRouter } from "expo-router";
|
||||||
|
import axios from "axios";
|
||||||
|
import { API_BASE_URL, API_ENDPOINTS } from "../../config/api";
|
||||||
|
|
||||||
export default function ProfileScreen() {
|
export default function ProfileScreen() {
|
||||||
const { user, logout } = useAuth()
|
const { user } = useUser();
|
||||||
const router = useRouter()
|
const { signOut } = useAuth();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const handleLogout = async () => {
|
useEffect(() => {
|
||||||
Alert.alert(
|
const checkProfile = async () => {
|
||||||
'Logout',
|
try {
|
||||||
'Are you sure you want to logout?',
|
const response = await axios.get(
|
||||||
[
|
`${API_BASE_URL}${API_ENDPOINTS.PROFILE.FITNESS}`,
|
||||||
{ text: 'Cancel', style: 'cancel' },
|
);
|
||||||
{
|
if (response.status === 200 && response.data) {
|
||||||
text: 'Logout',
|
router.replace("/activities");
|
||||||
style: 'destructive',
|
}
|
||||||
onPress: async () => {
|
} catch (error) {
|
||||||
try {
|
// Profile not found, stay on profile
|
||||||
await logout()
|
}
|
||||||
router.replace('/login')
|
};
|
||||||
} catch (error) {
|
if (user) {
|
||||||
Alert.alert('Error', 'Failed to logout')
|
checkProfile();
|
||||||
}
|
}
|
||||||
},
|
}, [user, router]);
|
||||||
},
|
|
||||||
]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
@ -36,35 +35,39 @@ export default function ProfileScreen() {
|
|||||||
<Text style={styles.name}>
|
<Text style={styles.name}>
|
||||||
{user?.firstName} {user?.lastName}
|
{user?.firstName} {user?.lastName}
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={styles.email}>{user?.email}</Text>
|
<Text style={styles.email}>
|
||||||
{user?.phone && <Text style={styles.phone}>{user.phone}</Text>}
|
{user?.primaryEmailAddress?.emailAddress}
|
||||||
|
</Text>
|
||||||
|
{user?.phoneNumbers?.[0] && (
|
||||||
|
<Text style={styles.phone}>{user.phoneNumbers[0].phoneNumber}</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
<View style={styles.roleBadge}>
|
<View style={styles.roleBadge}>
|
||||||
<Text style={styles.roleText}>
|
<Text style={styles.roleText}>User</Text>
|
||||||
{user?.role.charAt(0).toUpperCase() + user?.role.slice(1)}
|
|
||||||
</Text>
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<TouchableOpacity style={styles.logoutButton} onPress={handleLogout}>
|
<View style={styles.logoutButton}>
|
||||||
<Text style={styles.logoutText}>Logout</Text>
|
<TouchableOpacity onPress={() => signOut()}>
|
||||||
</TouchableOpacity>
|
<Text style={styles.logoutText}>Logout</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
padding: 20,
|
padding: 20,
|
||||||
backgroundColor: '#f5f5f5',
|
backgroundColor: "#f5f5f5",
|
||||||
},
|
},
|
||||||
profileCard: {
|
profileCard: {
|
||||||
backgroundColor: 'white',
|
backgroundColor: "white",
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
padding: 24,
|
padding: 24,
|
||||||
alignItems: 'center',
|
alignItems: "center",
|
||||||
shadowColor: '#000',
|
shadowColor: "#000",
|
||||||
shadowOffset: { width: 0, height: 2 },
|
shadowOffset: { width: 0, height: 2 },
|
||||||
shadowOpacity: 0.1,
|
shadowOpacity: 0.1,
|
||||||
shadowRadius: 4,
|
shadowRadius: 4,
|
||||||
@ -72,45 +75,45 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
fontWeight: 'bold',
|
fontWeight: "bold",
|
||||||
marginBottom: 16,
|
marginBottom: 16,
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
fontWeight: '600',
|
fontWeight: "600",
|
||||||
marginBottom: 4,
|
marginBottom: 4,
|
||||||
},
|
},
|
||||||
email: {
|
email: {
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: '#666',
|
color: "#666",
|
||||||
marginBottom: 4,
|
marginBottom: 4,
|
||||||
},
|
},
|
||||||
phone: {
|
phone: {
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: '#666',
|
color: "#666",
|
||||||
marginBottom: 16,
|
marginBottom: 16,
|
||||||
},
|
},
|
||||||
roleBadge: {
|
roleBadge: {
|
||||||
backgroundColor: '#3b82f6',
|
backgroundColor: "#3b82f6",
|
||||||
paddingHorizontal: 12,
|
paddingHorizontal: 12,
|
||||||
paddingVertical: 6,
|
paddingVertical: 6,
|
||||||
borderRadius: 20,
|
borderRadius: 20,
|
||||||
},
|
},
|
||||||
roleText: {
|
roleText: {
|
||||||
color: 'white',
|
color: "white",
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: '600',
|
fontWeight: "600",
|
||||||
},
|
},
|
||||||
logoutButton: {
|
logoutButton: {
|
||||||
backgroundColor: '#ef4444',
|
backgroundColor: "#ef4444",
|
||||||
paddingVertical: 14,
|
paddingVertical: 14,
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
alignItems: 'center',
|
alignItems: "center",
|
||||||
marginTop: 24,
|
marginTop: 24,
|
||||||
},
|
},
|
||||||
logoutText: {
|
logoutText: {
|
||||||
color: 'white',
|
color: "white",
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: '600',
|
fontWeight: "600",
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|||||||
@ -1,15 +1,12 @@
|
|||||||
import { AuthProvider } from '@/contexts/AuthContext'
|
import { ClerkProvider } from "@clerk/clerk-expo";
|
||||||
import { Stack } from 'expo-router'
|
import { Slot } from "expo-router";
|
||||||
import { View, Text } from 'react-native'
|
|
||||||
|
|
||||||
export default function RootLayout() {
|
export default function RootLayout() {
|
||||||
return (
|
return (
|
||||||
<AuthProvider>
|
<ClerkProvider
|
||||||
<Stack>
|
publishableKey={process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY}
|
||||||
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
>
|
||||||
<Stack.Screen name="login" options={{ headerShown: false }} />
|
<Slot />
|
||||||
<Stack.Screen name="register" options={{ headerShown: false }} />
|
</ClerkProvider>
|
||||||
</Stack>
|
);
|
||||||
</AuthProvider>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
@ -1,161 +1,139 @@
|
|||||||
import React, { useState } from 'react'
|
import { useSignIn, useOAuth, useUser } from "@clerk/clerk-expo";
|
||||||
import { View, Text, TextInput, TouchableOpacity, StyleSheet, Alert } from 'react-native'
|
import { useState } from "react";
|
||||||
import { useRouter } from 'expo-router'
|
import {
|
||||||
import axios from 'axios'
|
View,
|
||||||
import * as SecureStore from 'expo-secure-store'
|
Text,
|
||||||
import { API_BASE_URL, API_ENDPOINTS } from '../config/api'
|
TextInput,
|
||||||
|
TouchableOpacity,
|
||||||
|
Alert,
|
||||||
|
StyleSheet,
|
||||||
|
} from "react-native";
|
||||||
|
import { useRouter } from "expo-router";
|
||||||
|
import axios from "axios";
|
||||||
|
import { API_BASE_URL, API_ENDPOINTS } from "../config/api";
|
||||||
|
|
||||||
export default function LoginScreen() {
|
export default function LoginScreen() {
|
||||||
const [formData, setFormData] = useState({
|
const { signIn, setActive } = useSignIn();
|
||||||
email: '',
|
const { startOAuthFlow } = useOAuth({ strategy: "oauth_google" });
|
||||||
password: '',
|
const { user } = useUser();
|
||||||
})
|
const [email, setEmail] = useState("");
|
||||||
const [loading, setLoading] = useState(false)
|
const [password, setPassword] = useState("");
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
|
|
||||||
const handleLogin = async () => {
|
const handleSignIn = async () => {
|
||||||
if (!formData.email || !formData.password) {
|
if (!email || !password) {
|
||||||
Alert.alert('Error', 'Please fill in all fields')
|
Alert.alert("Error", "Please fill in all fields");
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoading(true)
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.post(`${API_BASE_URL}${API_ENDPOINTS.AUTH.LOGIN}`, formData)
|
const result = await signIn.create({
|
||||||
|
identifier: email,
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
|
||||||
if (response.data.user) {
|
if (result.status === "complete") {
|
||||||
await SecureStore.setItemAsync('user', JSON.stringify(response.data.user))
|
await setActive({ session: result.createdSessionId });
|
||||||
|
router.replace("/(tabs)/profile");
|
||||||
// Check if user has completed fitness profile
|
} else {
|
||||||
try {
|
Alert.alert("Error", "Sign in failed");
|
||||||
const profileResponse = await axios.get(
|
|
||||||
`${API_BASE_URL}${API_ENDPOINTS.PROFILE.FITNESS}?userId=${response.data.user.id}`
|
|
||||||
)
|
|
||||||
|
|
||||||
if (profileResponse.data.profile) {
|
|
||||||
// User has profile, go to main app
|
|
||||||
Alert.alert('Success', 'Login successful!', [
|
|
||||||
{ text: 'OK', onPress: () => router.replace('/(tabs)') }
|
|
||||||
])
|
|
||||||
} else {
|
|
||||||
// New user, go to welcome page
|
|
||||||
Alert.alert('Welcome!', 'Let\'s set up your fitness profile', [
|
|
||||||
{ text: 'OK', onPress: () => router.replace('/welcome') }
|
|
||||||
])
|
|
||||||
}
|
|
||||||
} catch (profileError) {
|
|
||||||
// Profile doesn't exist or server error, treat as new user
|
|
||||||
console.log('Profile check failed:', profileError)
|
|
||||||
Alert.alert('Welcome!', 'Let\'s set up your fitness profile', [
|
|
||||||
{ text: 'OK', onPress: () => router.replace('/welcome') }
|
|
||||||
])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (err: any) {
|
||||||
Alert.alert('Error', error.response?.data?.error || 'Login failed')
|
Alert.alert("Error", err.message || "Sign in failed");
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
const handleGoogleSignIn = async () => {
|
||||||
|
try {
|
||||||
|
const result = await startOAuthFlow();
|
||||||
|
if (result.createdSessionId) {
|
||||||
|
router.replace("/(tabs)/profile");
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
Alert.alert("Error", err.message || "Google sign in failed");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Text style={styles.title}>Welcome Back</Text>
|
<Text style={styles.title}>Sign In</Text>
|
||||||
<Text style={styles.subtitle}>Login to your FitAI account</Text>
|
<TextInput
|
||||||
|
placeholder="Email"
|
||||||
<View style={styles.form}>
|
value={email}
|
||||||
<TextInput
|
onChangeText={setEmail}
|
||||||
style={styles.input}
|
style={styles.input}
|
||||||
placeholder="Email"
|
keyboardType="email-address"
|
||||||
value={formData.email}
|
autoCapitalize="none"
|
||||||
onChangeText={(text) => setFormData({ ...formData, email: text })}
|
/>
|
||||||
keyboardType="email-address"
|
<TextInput
|
||||||
autoCapitalize="none"
|
placeholder="Password"
|
||||||
/>
|
value={password}
|
||||||
|
onChangeText={setPassword}
|
||||||
<TextInput
|
style={styles.input}
|
||||||
style={styles.input}
|
secureTextEntry
|
||||||
placeholder="Password"
|
/>
|
||||||
value={formData.password}
|
<TouchableOpacity onPress={handleSignIn} style={styles.button}>
|
||||||
onChangeText={(text) => setFormData({ ...formData, password: text })}
|
<Text style={styles.buttonText}>Sign In</Text>
|
||||||
secureTextEntry
|
</TouchableOpacity>
|
||||||
/>
|
<Text style={styles.orText}>Or</Text>
|
||||||
|
<TouchableOpacity
|
||||||
<TouchableOpacity
|
onPress={handleGoogleSignIn}
|
||||||
style={[styles.button, loading && styles.buttonDisabled]}
|
style={styles.googleButton}
|
||||||
onPress={handleLogin}
|
>
|
||||||
disabled={loading}
|
<Text style={styles.googleButtonText}>Sign In with Google</Text>
|
||||||
>
|
</TouchableOpacity>
|
||||||
<Text style={styles.buttonText}>
|
|
||||||
{loading ? 'Logging in...' : 'Login'}
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.linkButton}
|
|
||||||
onPress={() => router.push('/register')}
|
|
||||||
>
|
|
||||||
<Text style={styles.linkText}>
|
|
||||||
Don't have an account? Register
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
justifyContent: 'center',
|
justifyContent: "center",
|
||||||
alignItems: 'center',
|
|
||||||
backgroundColor: '#f5f5f5',
|
|
||||||
padding: 20,
|
padding: 20,
|
||||||
|
backgroundColor: "#f5f5f5",
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
fontSize: 32,
|
fontSize: 24,
|
||||||
fontWeight: 'bold',
|
fontWeight: "bold",
|
||||||
marginBottom: 8,
|
textAlign: "center",
|
||||||
color: '#333',
|
marginBottom: 20,
|
||||||
},
|
|
||||||
subtitle: {
|
|
||||||
fontSize: 16,
|
|
||||||
color: '#666',
|
|
||||||
marginBottom: 32,
|
|
||||||
},
|
|
||||||
form: {
|
|
||||||
width: '100%',
|
|
||||||
maxWidth: 400,
|
|
||||||
},
|
},
|
||||||
input: {
|
input: {
|
||||||
backgroundColor: 'white',
|
|
||||||
paddingHorizontal: 16,
|
|
||||||
paddingVertical: 12,
|
|
||||||
borderRadius: 8,
|
|
||||||
marginBottom: 16,
|
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: '#ddd',
|
borderColor: "#ddd",
|
||||||
|
padding: 15,
|
||||||
|
marginBottom: 15,
|
||||||
|
borderRadius: 8,
|
||||||
|
backgroundColor: "white",
|
||||||
},
|
},
|
||||||
button: {
|
button: {
|
||||||
backgroundColor: '#3b82f6',
|
backgroundColor: "#3b82f6",
|
||||||
paddingVertical: 14,
|
padding: 15,
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
alignItems: 'center',
|
alignItems: "center",
|
||||||
marginBottom: 16,
|
marginBottom: 10,
|
||||||
},
|
|
||||||
buttonDisabled: {
|
|
||||||
backgroundColor: '#9ca3af',
|
|
||||||
},
|
},
|
||||||
buttonText: {
|
buttonText: {
|
||||||
color: 'white',
|
color: "white",
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: '600',
|
fontWeight: "600",
|
||||||
},
|
},
|
||||||
linkButton: {
|
orText: {
|
||||||
alignItems: 'center',
|
textAlign: "center",
|
||||||
|
marginVertical: 10,
|
||||||
|
fontSize: 16,
|
||||||
|
color: "#666",
|
||||||
},
|
},
|
||||||
linkText: {
|
googleButton: {
|
||||||
color: '#3b82f6',
|
backgroundColor: "#db4437",
|
||||||
fontSize: 14,
|
padding: 15,
|
||||||
|
borderRadius: 8,
|
||||||
|
alignItems: "center",
|
||||||
},
|
},
|
||||||
})
|
googleButtonText: {
|
||||||
|
color: "white",
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "600",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
@ -1,164 +1,117 @@
|
|||||||
import React, { useState } from 'react'
|
import { useSignUp } from "@clerk/clerk-expo";
|
||||||
import { View, Text, TextInput, TouchableOpacity, StyleSheet, Alert } from 'react-native'
|
import { useState } from "react";
|
||||||
import { useRouter } from 'expo-router'
|
import {
|
||||||
import axios from 'axios'
|
View,
|
||||||
import { API_BASE_URL, API_ENDPOINTS } from '../config/api'
|
Text,
|
||||||
|
TextInput,
|
||||||
|
TouchableOpacity,
|
||||||
|
Alert,
|
||||||
|
StyleSheet,
|
||||||
|
} from "react-native";
|
||||||
|
import { useRouter } from "expo-router";
|
||||||
|
|
||||||
export default function RegisterScreen() {
|
export default function RegisterScreen() {
|
||||||
const [formData, setFormData] = useState({
|
const { signUp, setActive } = useSignUp();
|
||||||
email: '',
|
const [email, setEmail] = useState("");
|
||||||
password: '',
|
const [password, setPassword] = useState("");
|
||||||
firstName: '',
|
const [firstName, setFirstName] = useState("");
|
||||||
lastName: '',
|
const [lastName, setLastName] = useState("");
|
||||||
phone: '',
|
const router = useRouter();
|
||||||
})
|
|
||||||
const [loading, setLoading] = useState(false)
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const handleRegister = async () => {
|
const handleSignUp = async () => {
|
||||||
if (!formData.email || !formData.password || !formData.firstName || !formData.lastName) {
|
if (!email || !password || !firstName || !lastName) {
|
||||||
Alert.alert('Error', 'Please fill in all required fields')
|
Alert.alert("Error", "Please fill in all fields");
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoading(true)
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.post(`${API_BASE_URL}${API_ENDPOINTS.AUTH.REGISTER}`, formData)
|
const result = await signUp.create({
|
||||||
|
emailAddress: email,
|
||||||
|
password,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
});
|
||||||
|
|
||||||
if (response.status === 201) {
|
if (result.status === "complete") {
|
||||||
Alert.alert('Success', 'Registration successful! Please login.', [
|
await setActive({ session: result.createdSessionId });
|
||||||
{ text: 'OK', onPress: () => router.push('/login') }
|
router.replace("/(tabs)/profile");
|
||||||
])
|
} else {
|
||||||
|
Alert.alert("Error", "Sign up failed");
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (err: any) {
|
||||||
Alert.alert('Error', error.response?.data?.error || 'Registration failed')
|
Alert.alert("Error", err.message || "Sign up failed");
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Text style={styles.title}>Create Account</Text>
|
<Text style={styles.title}>Sign Up</Text>
|
||||||
<Text style={styles.subtitle}>Join FitAI today</Text>
|
<TextInput
|
||||||
|
placeholder="First Name"
|
||||||
<View style={styles.form}>
|
value={firstName}
|
||||||
<TextInput
|
onChangeText={setFirstName}
|
||||||
style={styles.input}
|
style={styles.input}
|
||||||
placeholder="First Name"
|
autoCapitalize="words"
|
||||||
value={formData.firstName}
|
/>
|
||||||
onChangeText={(text) => setFormData({ ...formData, firstName: text })}
|
<TextInput
|
||||||
autoCapitalize="words"
|
placeholder="Last Name"
|
||||||
/>
|
value={lastName}
|
||||||
|
onChangeText={setLastName}
|
||||||
<TextInput
|
style={styles.input}
|
||||||
style={styles.input}
|
autoCapitalize="words"
|
||||||
placeholder="Last Name"
|
/>
|
||||||
value={formData.lastName}
|
<TextInput
|
||||||
onChangeText={(text) => setFormData({ ...formData, lastName: text })}
|
placeholder="Email"
|
||||||
autoCapitalize="words"
|
value={email}
|
||||||
/>
|
onChangeText={setEmail}
|
||||||
|
style={styles.input}
|
||||||
<TextInput
|
keyboardType="email-address"
|
||||||
style={styles.input}
|
autoCapitalize="none"
|
||||||
placeholder="Email"
|
/>
|
||||||
value={formData.email}
|
<TextInput
|
||||||
onChangeText={(text) => setFormData({ ...formData, email: text })}
|
placeholder="Password"
|
||||||
keyboardType="email-address"
|
value={password}
|
||||||
autoCapitalize="none"
|
onChangeText={setPassword}
|
||||||
/>
|
style={styles.input}
|
||||||
|
secureTextEntry
|
||||||
<TextInput
|
/>
|
||||||
style={styles.input}
|
<TouchableOpacity onPress={handleSignUp} style={styles.button}>
|
||||||
placeholder="Phone (optional)"
|
<Text style={styles.buttonText}>Sign Up</Text>
|
||||||
value={formData.phone}
|
</TouchableOpacity>
|
||||||
onChangeText={(text) => setFormData({ ...formData, phone: text })}
|
|
||||||
keyboardType="phone-pad"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextInput
|
|
||||||
style={styles.input}
|
|
||||||
placeholder="Password"
|
|
||||||
value={formData.password}
|
|
||||||
onChangeText={(text) => setFormData({ ...formData, password: text })}
|
|
||||||
secureTextEntry
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TouchableOpacity
|
|
||||||
style={[styles.button, loading && styles.buttonDisabled]}
|
|
||||||
onPress={handleRegister}
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
<Text style={styles.buttonText}>
|
|
||||||
{loading ? 'Creating Account...' : 'Create Account'}
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.linkButton}
|
|
||||||
onPress={() => router.push('/login')}
|
|
||||||
>
|
|
||||||
<Text style={styles.linkText}>
|
|
||||||
Already have an account? Login
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
justifyContent: 'center',
|
justifyContent: "center",
|
||||||
alignItems: 'center',
|
|
||||||
backgroundColor: '#f5f5f5',
|
|
||||||
padding: 20,
|
padding: 20,
|
||||||
|
backgroundColor: "#f5f5f5",
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
fontSize: 32,
|
fontSize: 24,
|
||||||
fontWeight: 'bold',
|
fontWeight: "bold",
|
||||||
marginBottom: 8,
|
textAlign: "center",
|
||||||
color: '#333',
|
marginBottom: 20,
|
||||||
},
|
|
||||||
subtitle: {
|
|
||||||
fontSize: 16,
|
|
||||||
color: '#666',
|
|
||||||
marginBottom: 32,
|
|
||||||
},
|
|
||||||
form: {
|
|
||||||
width: '100%',
|
|
||||||
maxWidth: 400,
|
|
||||||
},
|
},
|
||||||
input: {
|
input: {
|
||||||
backgroundColor: 'white',
|
|
||||||
paddingHorizontal: 16,
|
|
||||||
paddingVertical: 12,
|
|
||||||
borderRadius: 8,
|
|
||||||
marginBottom: 16,
|
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: '#ddd',
|
borderColor: "#ddd",
|
||||||
|
padding: 15,
|
||||||
|
marginBottom: 15,
|
||||||
|
borderRadius: 8,
|
||||||
|
backgroundColor: "white",
|
||||||
},
|
},
|
||||||
button: {
|
button: {
|
||||||
backgroundColor: '#3b82f6',
|
backgroundColor: "#3b82f6",
|
||||||
paddingVertical: 14,
|
padding: 15,
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
alignItems: 'center',
|
alignItems: "center",
|
||||||
marginBottom: 16,
|
|
||||||
},
|
|
||||||
buttonDisabled: {
|
|
||||||
backgroundColor: '#9ca3af',
|
|
||||||
},
|
},
|
||||||
buttonText: {
|
buttonText: {
|
||||||
color: 'white',
|
color: "white",
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: '600',
|
fontWeight: "600",
|
||||||
},
|
},
|
||||||
linkButton: {
|
});
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
linkText: {
|
|
||||||
color: '#3b82f6',
|
|
||||||
fontSize: 14,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|||||||
@ -1,99 +1,106 @@
|
|||||||
import React, { useState } from 'react'
|
import React, { useState } from "react";
|
||||||
import { View, Text, TextInput, TouchableOpacity, StyleSheet, Alert, ScrollView } from 'react-native'
|
import {
|
||||||
import { useRouter } from 'expo-router'
|
View,
|
||||||
import axios from 'axios'
|
Text,
|
||||||
import * as SecureStore from 'expo-secure-store'
|
TextInput,
|
||||||
import { API_BASE_URL, API_ENDPOINTS } from '../config/api'
|
TouchableOpacity,
|
||||||
|
StyleSheet,
|
||||||
|
Alert,
|
||||||
|
ScrollView,
|
||||||
|
} from "react-native";
|
||||||
|
import { useRouter } from "expo-router";
|
||||||
|
import { useUser } from "@clerk/clerk-expo";
|
||||||
|
import axios from "axios";
|
||||||
|
import { API_BASE_URL, API_ENDPOINTS } from "../config/api";
|
||||||
|
|
||||||
interface FitnessProfile {
|
interface FitnessProfile {
|
||||||
height: string
|
height: string;
|
||||||
weight: string
|
weight: string;
|
||||||
age: string
|
age: string;
|
||||||
gender: 'male' | 'female' | 'other'
|
gender: "male" | "female" | "other";
|
||||||
activityLevel: 'sedentary' | 'light' | 'moderate' | 'active' | 'very_active'
|
activityLevel: "sedentary" | "light" | "moderate" | "active" | "very_active";
|
||||||
fitnessGoals: string[]
|
fitnessGoals: string[];
|
||||||
exerciseHabits: string
|
exerciseHabits: string;
|
||||||
dietHabits: string
|
dietHabits: string;
|
||||||
medicalConditions: string
|
medicalConditions: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function WelcomeScreen() {
|
export default function WelcomeScreen() {
|
||||||
const [profile, setProfile] = useState<FitnessProfile>({
|
const [profile, setProfile] = useState<FitnessProfile>({
|
||||||
height: '',
|
height: "",
|
||||||
weight: '',
|
weight: "",
|
||||||
age: '',
|
age: "",
|
||||||
gender: 'male',
|
gender: "male",
|
||||||
activityLevel: 'moderate',
|
activityLevel: "moderate",
|
||||||
fitnessGoals: [],
|
fitnessGoals: [],
|
||||||
exerciseHabits: '',
|
exerciseHabits: "",
|
||||||
dietHabits: '',
|
dietHabits: "",
|
||||||
medicalConditions: '',
|
medicalConditions: "",
|
||||||
})
|
});
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false);
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
|
|
||||||
const fitnessGoalsOptions = [
|
const fitnessGoalsOptions = [
|
||||||
'Weight Loss',
|
"Weight Loss",
|
||||||
'Muscle Gain',
|
"Muscle Gain",
|
||||||
'Improve Endurance',
|
"Improve Endurance",
|
||||||
'Better Flexibility',
|
"Better Flexibility",
|
||||||
'General Fitness',
|
"General Fitness",
|
||||||
'Strength Training',
|
"Strength Training",
|
||||||
'Cardio Health'
|
"Cardio Health",
|
||||||
]
|
];
|
||||||
|
|
||||||
const activityLevels = [
|
const activityLevels = [
|
||||||
{ value: 'sedentary', label: 'Sedentary (little or no exercise)' },
|
{ value: "sedentary", label: "Sedentary (little or no exercise)" },
|
||||||
{ value: 'light', label: 'Light (1-3 days/week)' },
|
{ value: "light", label: "Light (1-3 days/week)" },
|
||||||
{ value: 'moderate', label: 'Moderate (3-5 days/week)' },
|
{ value: "moderate", label: "Moderate (3-5 days/week)" },
|
||||||
{ value: 'active', label: 'Active (6-7 days/week)' },
|
{ value: "active", label: "Active (6-7 days/week)" },
|
||||||
{ value: 'very_active', label: 'Very Active (twice per day)' }
|
{ value: "very_active", label: "Very Active (twice per day)" },
|
||||||
]
|
];
|
||||||
|
|
||||||
const toggleGoal = (goal: string) => {
|
const toggleGoal = (goal: string) => {
|
||||||
setProfile(prev => ({
|
setProfile((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
fitnessGoals: prev.fitnessGoals.includes(goal)
|
fitnessGoals: prev.fitnessGoals.includes(goal)
|
||||||
? prev.fitnessGoals.filter(g => g !== goal)
|
? prev.fitnessGoals.filter((g) => g !== goal)
|
||||||
: [...prev.fitnessGoals, goal]
|
: [...prev.fitnessGoals, goal],
|
||||||
}))
|
}));
|
||||||
}
|
};
|
||||||
|
|
||||||
|
const { user } = useUser();
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!profile.height || !profile.weight || !profile.age) {
|
if (!profile.height || !profile.weight || !profile.age) {
|
||||||
Alert.alert('Error', 'Please fill in all required fields')
|
Alert.alert("Error", "Please fill in all required fields");
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoading(true)
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const user = await SecureStore.getItemAsync('user')
|
|
||||||
if (!user) {
|
|
||||||
throw new Error('No user found')
|
|
||||||
}
|
|
||||||
|
|
||||||
const userData = JSON.parse(user)
|
|
||||||
|
|
||||||
const response = await axios.post(
|
const response = await axios.post(
|
||||||
`${API_BASE_URL}${API_ENDPOINTS.PROFILE.FITNESS}`,
|
`${API_BASE_URL}${API_ENDPOINTS.PROFILE.FITNESS}`,
|
||||||
{
|
{
|
||||||
userId: userData.id,
|
userId: user?.id,
|
||||||
...profile
|
...profile,
|
||||||
}
|
},
|
||||||
)
|
);
|
||||||
|
|
||||||
if (response.status === 201) {
|
if (response.status === 201) {
|
||||||
Alert.alert('Success', 'Profile completed successfully!', [
|
Alert.alert("Success", "Profile completed successfully!", [
|
||||||
{ text: 'OK', onPress: () => router.replace('/(tabs)') }
|
{ text: "OK", onPress: () => router.replace("/(tabs)") },
|
||||||
])
|
]);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.log('Profile save error:', error)
|
console.log("Profile save error:", error);
|
||||||
Alert.alert('Error', error.response?.data?.error || 'Failed to save profile. Please try again.')
|
Alert.alert(
|
||||||
|
"Error",
|
||||||
|
error.response?.data?.error ||
|
||||||
|
"Failed to save profile. Please try again.",
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView style={styles.container}>
|
<ScrollView style={styles.container}>
|
||||||
@ -110,7 +117,9 @@ export default function WelcomeScreen() {
|
|||||||
<TextInput
|
<TextInput
|
||||||
style={styles.input}
|
style={styles.input}
|
||||||
value={profile.height}
|
value={profile.height}
|
||||||
onChangeText={(text) => setProfile({ ...profile, height: text })}
|
onChangeText={(text) =>
|
||||||
|
setProfile({ ...profile, height: text })
|
||||||
|
}
|
||||||
keyboardType="numeric"
|
keyboardType="numeric"
|
||||||
placeholder="170"
|
placeholder="170"
|
||||||
/>
|
/>
|
||||||
@ -121,7 +130,9 @@ export default function WelcomeScreen() {
|
|||||||
<TextInput
|
<TextInput
|
||||||
style={styles.input}
|
style={styles.input}
|
||||||
value={profile.weight}
|
value={profile.weight}
|
||||||
onChangeText={(text) => setProfile({ ...profile, weight: text })}
|
onChangeText={(text) =>
|
||||||
|
setProfile({ ...profile, weight: text })
|
||||||
|
}
|
||||||
keyboardType="numeric"
|
keyboardType="numeric"
|
||||||
placeholder="70"
|
placeholder="70"
|
||||||
/>
|
/>
|
||||||
@ -143,19 +154,22 @@ export default function WelcomeScreen() {
|
|||||||
<View style={[styles.inputContainer, { flex: 1, marginLeft: 8 }]}>
|
<View style={[styles.inputContainer, { flex: 1, marginLeft: 8 }]}>
|
||||||
<Text style={styles.label}>Gender</Text>
|
<Text style={styles.label}>Gender</Text>
|
||||||
<View style={styles.genderRow}>
|
<View style={styles.genderRow}>
|
||||||
{(['male', 'female', 'other'] as const).map((gender) => (
|
{(["male", "female", "other"] as const).map((gender) => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
key={gender}
|
key={gender}
|
||||||
style={[
|
style={[
|
||||||
styles.genderButton,
|
styles.genderButton,
|
||||||
profile.gender === gender && styles.genderButtonSelected
|
profile.gender === gender && styles.genderButtonSelected,
|
||||||
]}
|
]}
|
||||||
onPress={() => setProfile({ ...profile, gender })}
|
onPress={() => setProfile({ ...profile, gender })}
|
||||||
>
|
>
|
||||||
<Text style={[
|
<Text
|
||||||
styles.genderButtonText,
|
style={[
|
||||||
profile.gender === gender && styles.genderButtonTextSelected
|
styles.genderButtonText,
|
||||||
]}>
|
profile.gender === gender &&
|
||||||
|
styles.genderButtonTextSelected,
|
||||||
|
]}
|
||||||
|
>
|
||||||
{gender.charAt(0).toUpperCase() + gender.slice(1)}
|
{gender.charAt(0).toUpperCase() + gender.slice(1)}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
@ -172,14 +186,20 @@ export default function WelcomeScreen() {
|
|||||||
key={level.value}
|
key={level.value}
|
||||||
style={[
|
style={[
|
||||||
styles.activityOption,
|
styles.activityOption,
|
||||||
profile.activityLevel === level.value && styles.activityOptionSelected
|
profile.activityLevel === level.value &&
|
||||||
|
styles.activityOptionSelected,
|
||||||
]}
|
]}
|
||||||
onPress={() => setProfile({ ...profile, activityLevel: level.value as any })}
|
onPress={() =>
|
||||||
|
setProfile({ ...profile, activityLevel: level.value as any })
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Text style={[
|
<Text
|
||||||
styles.activityText,
|
style={[
|
||||||
profile.activityLevel === level.value && styles.activityTextSelected
|
styles.activityText,
|
||||||
]}>
|
profile.activityLevel === level.value &&
|
||||||
|
styles.activityTextSelected,
|
||||||
|
]}
|
||||||
|
>
|
||||||
{level.label}
|
{level.label}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
@ -195,14 +215,18 @@ export default function WelcomeScreen() {
|
|||||||
key={goal}
|
key={goal}
|
||||||
style={[
|
style={[
|
||||||
styles.goalButton,
|
styles.goalButton,
|
||||||
profile.fitnessGoals.includes(goal) && styles.goalButtonSelected
|
profile.fitnessGoals.includes(goal) &&
|
||||||
|
styles.goalButtonSelected,
|
||||||
]}
|
]}
|
||||||
onPress={() => toggleGoal(goal)}
|
onPress={() => toggleGoal(goal)}
|
||||||
>
|
>
|
||||||
<Text style={[
|
<Text
|
||||||
styles.goalButtonText,
|
style={[
|
||||||
profile.fitnessGoals.includes(goal) && styles.goalButtonTextSelected
|
styles.goalButtonText,
|
||||||
]}>
|
profile.fitnessGoals.includes(goal) &&
|
||||||
|
styles.goalButtonTextSelected,
|
||||||
|
]}
|
||||||
|
>
|
||||||
{goal}
|
{goal}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
@ -215,7 +239,9 @@ export default function WelcomeScreen() {
|
|||||||
<TextInput
|
<TextInput
|
||||||
style={[styles.input, styles.textArea]}
|
style={[styles.input, styles.textArea]}
|
||||||
value={profile.exerciseHabits}
|
value={profile.exerciseHabits}
|
||||||
onChangeText={(text) => setProfile({ ...profile, exerciseHabits: text })}
|
onChangeText={(text) =>
|
||||||
|
setProfile({ ...profile, exerciseHabits: text })
|
||||||
|
}
|
||||||
placeholder="Describe your current exercise routine..."
|
placeholder="Describe your current exercise routine..."
|
||||||
multiline
|
multiline
|
||||||
numberOfLines={3}
|
numberOfLines={3}
|
||||||
@ -227,7 +253,9 @@ export default function WelcomeScreen() {
|
|||||||
<TextInput
|
<TextInput
|
||||||
style={[styles.input, styles.textArea]}
|
style={[styles.input, styles.textArea]}
|
||||||
value={profile.dietHabits}
|
value={profile.dietHabits}
|
||||||
onChangeText={(text) => setProfile({ ...profile, dietHabits: text })}
|
onChangeText={(text) =>
|
||||||
|
setProfile({ ...profile, dietHabits: text })
|
||||||
|
}
|
||||||
placeholder="Describe your current eating habits..."
|
placeholder="Describe your current eating habits..."
|
||||||
multiline
|
multiline
|
||||||
numberOfLines={3}
|
numberOfLines={3}
|
||||||
@ -239,7 +267,9 @@ export default function WelcomeScreen() {
|
|||||||
<TextInput
|
<TextInput
|
||||||
style={[styles.input, styles.textArea]}
|
style={[styles.input, styles.textArea]}
|
||||||
value={profile.medicalConditions}
|
value={profile.medicalConditions}
|
||||||
onChangeText={(text) => setProfile({ ...profile, medicalConditions: text })}
|
onChangeText={(text) =>
|
||||||
|
setProfile({ ...profile, medicalConditions: text })
|
||||||
|
}
|
||||||
placeholder="Any medical conditions we should know about..."
|
placeholder="Any medical conditions we should know about..."
|
||||||
multiline
|
multiline
|
||||||
numberOfLines={3}
|
numberOfLines={3}
|
||||||
@ -252,51 +282,51 @@ export default function WelcomeScreen() {
|
|||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
<Text style={styles.buttonText}>
|
<Text style={styles.buttonText}>
|
||||||
{loading ? 'Saving...' : 'Complete Profile'}
|
{loading ? "Saving..." : "Complete Profile"}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: '#f5f5f5',
|
backgroundColor: "#f5f5f5",
|
||||||
},
|
},
|
||||||
content: {
|
content: {
|
||||||
padding: 20,
|
padding: 20,
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
fontSize: 28,
|
fontSize: 28,
|
||||||
fontWeight: 'bold',
|
fontWeight: "bold",
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
color: '#333',
|
color: "#333",
|
||||||
textAlign: 'center',
|
textAlign: "center",
|
||||||
},
|
},
|
||||||
subtitle: {
|
subtitle: {
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: '#666',
|
color: "#666",
|
||||||
marginBottom: 32,
|
marginBottom: 32,
|
||||||
textAlign: 'center',
|
textAlign: "center",
|
||||||
},
|
},
|
||||||
section: {
|
section: {
|
||||||
marginBottom: 24,
|
marginBottom: 24,
|
||||||
},
|
},
|
||||||
sectionTitle: {
|
sectionTitle: {
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: '600',
|
fontWeight: "600",
|
||||||
marginBottom: 12,
|
marginBottom: 12,
|
||||||
color: '#333',
|
color: "#333",
|
||||||
},
|
},
|
||||||
sectionSubtitle: {
|
sectionSubtitle: {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: '#666',
|
color: "#666",
|
||||||
marginBottom: 12,
|
marginBottom: 12,
|
||||||
},
|
},
|
||||||
row: {
|
row: {
|
||||||
flexDirection: 'row',
|
flexDirection: "row",
|
||||||
marginBottom: 16,
|
marginBottom: 16,
|
||||||
},
|
},
|
||||||
inputContainer: {
|
inputContainer: {
|
||||||
@ -304,105 +334,105 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
label: {
|
label: {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: '500',
|
fontWeight: "500",
|
||||||
marginBottom: 6,
|
marginBottom: 6,
|
||||||
color: '#333',
|
color: "#333",
|
||||||
},
|
},
|
||||||
input: {
|
input: {
|
||||||
backgroundColor: 'white',
|
backgroundColor: "white",
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
paddingVertical: 12,
|
paddingVertical: 12,
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: '#ddd',
|
borderColor: "#ddd",
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
},
|
},
|
||||||
textArea: {
|
textArea: {
|
||||||
height: 80,
|
height: 80,
|
||||||
textAlignVertical: 'top',
|
textAlignVertical: "top",
|
||||||
},
|
},
|
||||||
genderRow: {
|
genderRow: {
|
||||||
flexDirection: 'row',
|
flexDirection: "row",
|
||||||
},
|
},
|
||||||
genderButton: {
|
genderButton: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
paddingVertical: 12,
|
paddingVertical: 12,
|
||||||
paddingHorizontal: 8,
|
paddingHorizontal: 8,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: '#ddd',
|
borderColor: "#ddd",
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
alignItems: 'center',
|
alignItems: "center",
|
||||||
marginRight: 4,
|
marginRight: 4,
|
||||||
},
|
},
|
||||||
genderButtonSelected: {
|
genderButtonSelected: {
|
||||||
backgroundColor: '#3b82f6',
|
backgroundColor: "#3b82f6",
|
||||||
borderColor: '#3b82f6',
|
borderColor: "#3b82f6",
|
||||||
},
|
},
|
||||||
genderButtonText: {
|
genderButtonText: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: '#666',
|
color: "#666",
|
||||||
},
|
},
|
||||||
genderButtonTextSelected: {
|
genderButtonTextSelected: {
|
||||||
color: 'white',
|
color: "white",
|
||||||
},
|
},
|
||||||
activityOption: {
|
activityOption: {
|
||||||
paddingVertical: 12,
|
paddingVertical: 12,
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: '#ddd',
|
borderColor: "#ddd",
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
backgroundColor: 'white',
|
backgroundColor: "white",
|
||||||
},
|
},
|
||||||
activityOptionSelected: {
|
activityOptionSelected: {
|
||||||
backgroundColor: '#3b82f6',
|
backgroundColor: "#3b82f6",
|
||||||
borderColor: '#3b82f6',
|
borderColor: "#3b82f6",
|
||||||
},
|
},
|
||||||
activityText: {
|
activityText: {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: '#333',
|
color: "#333",
|
||||||
},
|
},
|
||||||
activityTextSelected: {
|
activityTextSelected: {
|
||||||
color: 'white',
|
color: "white",
|
||||||
},
|
},
|
||||||
goalsContainer: {
|
goalsContainer: {
|
||||||
flexDirection: 'row',
|
flexDirection: "row",
|
||||||
flexWrap: 'wrap',
|
flexWrap: "wrap",
|
||||||
marginHorizontal: -4,
|
marginHorizontal: -4,
|
||||||
},
|
},
|
||||||
goalButton: {
|
goalButton: {
|
||||||
backgroundColor: 'white',
|
backgroundColor: "white",
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: '#ddd',
|
borderColor: "#ddd",
|
||||||
borderRadius: 20,
|
borderRadius: 20,
|
||||||
paddingHorizontal: 12,
|
paddingHorizontal: 12,
|
||||||
paddingVertical: 8,
|
paddingVertical: 8,
|
||||||
margin: 4,
|
margin: 4,
|
||||||
},
|
},
|
||||||
goalButtonSelected: {
|
goalButtonSelected: {
|
||||||
backgroundColor: '#3b82f6',
|
backgroundColor: "#3b82f6",
|
||||||
borderColor: '#3b82f6',
|
borderColor: "#3b82f6",
|
||||||
},
|
},
|
||||||
goalButtonText: {
|
goalButtonText: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: '#666',
|
color: "#666",
|
||||||
},
|
},
|
||||||
goalButtonTextSelected: {
|
goalButtonTextSelected: {
|
||||||
color: 'white',
|
color: "white",
|
||||||
},
|
},
|
||||||
button: {
|
button: {
|
||||||
backgroundColor: '#3b82f6',
|
backgroundColor: "#3b82f6",
|
||||||
paddingVertical: 16,
|
paddingVertical: 16,
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
alignItems: 'center',
|
alignItems: "center",
|
||||||
marginTop: 16,
|
marginTop: 16,
|
||||||
},
|
},
|
||||||
buttonDisabled: {
|
buttonDisabled: {
|
||||||
backgroundColor: '#9ca3af',
|
backgroundColor: "#9ca3af",
|
||||||
},
|
},
|
||||||
buttonText: {
|
buttonText: {
|
||||||
color: 'white',
|
color: "white",
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: '600',
|
fontWeight: "600",
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|||||||
@ -1,76 +0,0 @@
|
|||||||
import React, { createContext, useContext, useEffect, useState } from 'react'
|
|
||||||
import * as SecureStore from 'expo-secure-store'
|
|
||||||
|
|
||||||
interface User {
|
|
||||||
id: string
|
|
||||||
email: string
|
|
||||||
firstName: string
|
|
||||||
lastName: string
|
|
||||||
role: string
|
|
||||||
phone?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AuthContextType {
|
|
||||||
user: User | null
|
|
||||||
login: (user: User) => Promise<void>
|
|
||||||
logout: () => Promise<void>
|
|
||||||
isLoading: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const AuthContext = createContext<AuthContextType | undefined>(undefined)
|
|
||||||
|
|
||||||
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|
||||||
const [user, setUser] = useState<User | null>(null)
|
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
loadUser()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const loadUser = async () => {
|
|
||||||
try {
|
|
||||||
const userData = await SecureStore.getItemAsync('user')
|
|
||||||
if (userData) {
|
|
||||||
setUser(JSON.parse(userData))
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load user:', error)
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const login = async (userData: User) => {
|
|
||||||
try {
|
|
||||||
await SecureStore.setItemAsync('user', JSON.stringify(userData))
|
|
||||||
setUser(userData)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to save user:', error)
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const logout = async () => {
|
|
||||||
try {
|
|
||||||
await SecureStore.deleteItemAsync('user')
|
|
||||||
setUser(null)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to logout:', error)
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AuthContext.Provider value={{ user, login, logout, isLoading }}>
|
|
||||||
{children}
|
|
||||||
</AuthContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useAuth() {
|
|
||||||
const context = useContext(AuthContext)
|
|
||||||
if (context === undefined) {
|
|
||||||
throw new Error('useAuth must be used within an AuthProvider')
|
|
||||||
}
|
|
||||||
return context
|
|
||||||
}
|
|
||||||
@ -1,16 +1,16 @@
|
|||||||
import { useAuth } from '@/contexts/AuthContext'
|
import { useUser } from "@clerk/clerk-expo";
|
||||||
import { useRouter } from 'expo-router'
|
import { useRouter } from "expo-router";
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from "react";
|
||||||
|
|
||||||
export function useRequireAuth() {
|
export function useRequireAuth() {
|
||||||
const { user, isLoading } = useAuth()
|
const { user, isLoaded, isSignedIn } = useUser();
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isLoading && !user) {
|
if (isLoaded && !isSignedIn) {
|
||||||
router.replace('/login')
|
router.replace("/login");
|
||||||
}
|
}
|
||||||
}, [user, isLoading, router])
|
}, [isSignedIn, isLoaded, router]);
|
||||||
|
|
||||||
return { user, isLoading }
|
return { user, isLoading: !isLoaded };
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user