basic clerk auth
need polishing
This commit is contained in:
parent
ca790a7b97
commit
73907568ef
4
apps/admin/.gitignore
vendored
4
apps/admin/.gitignore
vendored
@ -42,4 +42,6 @@ android/app/build/generated/
|
||||
*/fastlane/test_output
|
||||
|
||||
# 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",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@clerk/nextjs": "^6.34.5",
|
||||
"@fitai/shared": "file:../../packages/shared",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@tailwindcss/postcss": "^4.1.17",
|
||||
@ -600,6 +601,103 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.0.tgz",
|
||||
@ -2203,6 +2301,12 @@
|
||||
"@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": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
|
||||
@ -4607,6 +4711,15 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
@ -4633,7 +4746,6 @@
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/d3-array": {
|
||||
@ -4954,9 +5066,7 @@
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
||||
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
@ -5933,6 +6043,12 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "1.19.1",
|
||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
|
||||
@ -6414,6 +6530,12 @@
|
||||
"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": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
@ -8333,6 +8455,15 @@
|
||||
"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": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
@ -10652,6 +10783,12 @@
|
||||
"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": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||
@ -11091,6 +11228,22 @@
|
||||
"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": {
|
||||
"version": "1.1.0",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "0.11.11",
|
||||
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz",
|
||||
|
||||
@ -11,6 +11,7 @@
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@clerk/nextjs": "^6.34.5",
|
||||
"@fitai/shared": "file:../../packages/shared",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@tailwindcss/postcss": "^4.1.17",
|
||||
|
||||
@ -1,22 +1,27 @@
|
||||
import type { Metadata } from 'next'
|
||||
import { Inter } from 'next/font/google'
|
||||
import './globals.css'
|
||||
import type { Metadata } from "next";
|
||||
import { Inter } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { ClerkProvider } from "@clerk/nextjs";
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] })
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'FitAI Admin',
|
||||
description: 'Fitness management admin dashboard',
|
||||
}
|
||||
title: "FitAI Admin",
|
||||
description: "Fitness management admin dashboard",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={inter.className}>{children}</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
<ClerkProvider
|
||||
publishableKey={process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY}
|
||||
>
|
||||
<html lang="en">
|
||||
<body className={inter.className}>{children}</body>
|
||||
</html>
|
||||
</ClerkProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
import Link from "next/link";
|
||||
import { UserManagement } from "@/components/users/UserManagement";
|
||||
import { AnalyticsDashboard } from "@/components/analytics/AnalyticsDashboard";
|
||||
import { SignedIn, SignedOut, SignInButton, UserButton } from "@clerk/nextjs";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
@ -12,56 +13,65 @@ export default function Home() {
|
||||
<h1 className="text-4xl font-bold text-gray-900">
|
||||
FitAI Admin Dashboard
|
||||
</h1>
|
||||
<nav className="flex gap-4">
|
||||
<Link
|
||||
href="/users"
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
User Management
|
||||
</Link>
|
||||
<Link
|
||||
href="/analytics"
|
||||
className="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 transition-colors"
|
||||
>
|
||||
Analytics
|
||||
</Link>
|
||||
</nav>
|
||||
<div className="flex items-center gap-4">
|
||||
<SignedOut>
|
||||
<SignInButton />
|
||||
</SignedOut>
|
||||
<SignedIn>
|
||||
<UserButton />
|
||||
</SignedIn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<h2 className="text-xl font-semibold mb-4">Client Management</h2>
|
||||
<p className="text-gray-600">
|
||||
Manage fitness clients and their profiles
|
||||
<SignedIn>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<h2 className="text-xl font-semibold mb-4">Client Management</h2>
|
||||
<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>
|
||||
<SignInButton />
|
||||
</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>
|
||||
</SignedOut>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
|
||||
@ -5,7 +5,7 @@ import { AgCharts } from "ag-charts-react";
|
||||
import { AgChartOptions } from "ag-charts-community";
|
||||
|
||||
interface BarData {
|
||||
category: string;
|
||||
label: string;
|
||||
value: number;
|
||||
color?: string;
|
||||
}
|
||||
@ -30,7 +30,7 @@ export function RevenueChart({
|
||||
series: [
|
||||
{
|
||||
type: "bar",
|
||||
xKey: "category",
|
||||
xKey: "label",
|
||||
yKey: "value",
|
||||
fills: data.map((item) => item.color || "#10b981"),
|
||||
strokes: ["#ffffff"],
|
||||
@ -55,7 +55,7 @@ export function RevenueChart({
|
||||
enabled: true,
|
||||
renderer: (params: any) => {
|
||||
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>`;
|
||||
},
|
||||
|
||||
@ -163,14 +163,17 @@ export function UserGrid({
|
||||
const gridRef = React.useRef<AgGridReact<User>>(null);
|
||||
|
||||
const gridOptions = {
|
||||
theme: "legacy",
|
||||
theme: "legacy" as const,
|
||||
columnDefs,
|
||||
defaultColDef,
|
||||
rowData: users,
|
||||
rowSelection: "multiple",
|
||||
rowSelection: { mode: "multiRow" as const },
|
||||
onSelectionChanged: () => {
|
||||
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);
|
||||
if (selectedData.length === 1 && onUserSelect) {
|
||||
onUserSelect(selectedData[0]);
|
||||
|
||||
@ -191,14 +191,14 @@ export function UserManagement() {
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={handleEditUser}
|
||||
onClick={() => handleEditUser(selectedUser!)}
|
||||
disabled={!selectedUser}
|
||||
>
|
||||
Edit User
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={handleDeleteUser}
|
||||
onClick={() => handleDeleteUser(selectedUser!)}
|
||||
disabled={!selectedUser}
|
||||
>
|
||||
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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@clerk/clerk-expo": "^2.18.3",
|
||||
"@expo/vector-icons": "^15.0.0",
|
||||
"@hookform/resolvers": "^3.3.0",
|
||||
"@tanstack/react-query": "^5.0.0",
|
||||
@ -21,12 +22,15 @@
|
||||
"ajv-keywords": "^5.1.0",
|
||||
"axios": "^1.6.0",
|
||||
"expo": "~54.0.23",
|
||||
"expo-auth-session": "^7.0.8",
|
||||
"expo-camera": "~17.0.0",
|
||||
"expo-linking": "~8.0.0",
|
||||
"expo-notifications": "~0.32.0",
|
||||
"expo-router": "~6.0.14",
|
||||
"expo-secure-store": "~15.0.7",
|
||||
"expo-web-browser": "^15.0.9",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"react-hook-form": "^7.47.0",
|
||||
"react-native": "0.81.5",
|
||||
"react-native-safe-area-context": "~5.6.0",
|
||||
@ -35,15 +39,15 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.0",
|
||||
"@testing-library/react-native": "^12.4.0",
|
||||
"@types/react": "~19.1.10",
|
||||
"@types/react-native": "^0.73.0",
|
||||
"typescript": "^5.1.3",
|
||||
"eslint": "^8.45.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"babel-preset-expo": "~54.0.7",
|
||||
"eslint": "^8.45.0",
|
||||
"jest": "^29.2.1",
|
||||
"@testing-library/react-native": "^12.4.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 { Ionicons } from '@expo/vector-icons'
|
||||
import { Tabs } from "expo-router";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
|
||||
export default function TabLayout() {
|
||||
return (
|
||||
@ -7,7 +7,7 @@ export default function TabLayout() {
|
||||
<Tabs.Screen
|
||||
name="index"
|
||||
options={{
|
||||
title: 'Home',
|
||||
title: "Home",
|
||||
tabBarIcon: ({ color, size }) => (
|
||||
<Ionicons name="home" size={size} color={color} />
|
||||
),
|
||||
@ -16,7 +16,7 @@ export default function TabLayout() {
|
||||
<Tabs.Screen
|
||||
name="profile"
|
||||
options={{
|
||||
title: 'Profile',
|
||||
title: "Profile",
|
||||
tabBarIcon: ({ color, size }) => (
|
||||
<Ionicons name="person" size={size} color={color} />
|
||||
),
|
||||
@ -25,12 +25,12 @@ export default function TabLayout() {
|
||||
<Tabs.Screen
|
||||
name="attendance"
|
||||
options={{
|
||||
title: 'Attendance',
|
||||
title: "Attendance",
|
||||
tabBarIcon: ({ color, size }) => (
|
||||
<Ionicons name="calendar" size={size} color={color} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Tabs>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,33 +1,32 @@
|
||||
import React from 'react'
|
||||
import { View, Text, StyleSheet, TouchableOpacity, Alert } from 'react-native'
|
||||
import { useAuth } from '@/contexts/AuthContext'
|
||||
import { useRouter } from 'expo-router'
|
||||
import React, { useEffect } from "react";
|
||||
import { View, Text, StyleSheet, TouchableOpacity } from "react-native";
|
||||
import { useUser, useAuth } from "@clerk/clerk-expo";
|
||||
import { useRouter } from "expo-router";
|
||||
import axios from "axios";
|
||||
import { API_BASE_URL, API_ENDPOINTS } from "../../config/api";
|
||||
|
||||
export default function ProfileScreen() {
|
||||
const { user, logout } = useAuth()
|
||||
const router = useRouter()
|
||||
const { user } = useUser();
|
||||
const { signOut } = useAuth();
|
||||
const router = useRouter();
|
||||
|
||||
const handleLogout = async () => {
|
||||
Alert.alert(
|
||||
'Logout',
|
||||
'Are you sure you want to logout?',
|
||||
[
|
||||
{ text: 'Cancel', style: 'cancel' },
|
||||
{
|
||||
text: 'Logout',
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
try {
|
||||
await logout()
|
||||
router.replace('/login')
|
||||
} catch (error) {
|
||||
Alert.alert('Error', 'Failed to logout')
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
)
|
||||
}
|
||||
useEffect(() => {
|
||||
const checkProfile = async () => {
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`${API_BASE_URL}${API_ENDPOINTS.PROFILE.FITNESS}`,
|
||||
);
|
||||
if (response.status === 200 && response.data) {
|
||||
router.replace("/activities");
|
||||
}
|
||||
} catch (error) {
|
||||
// Profile not found, stay on profile
|
||||
}
|
||||
};
|
||||
if (user) {
|
||||
checkProfile();
|
||||
}
|
||||
}, [user, router]);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
@ -36,35 +35,39 @@ export default function ProfileScreen() {
|
||||
<Text style={styles.name}>
|
||||
{user?.firstName} {user?.lastName}
|
||||
</Text>
|
||||
<Text style={styles.email}>{user?.email}</Text>
|
||||
{user?.phone && <Text style={styles.phone}>{user.phone}</Text>}
|
||||
|
||||
<Text style={styles.email}>
|
||||
{user?.primaryEmailAddress?.emailAddress}
|
||||
</Text>
|
||||
{user?.phoneNumbers?.[0] && (
|
||||
<Text style={styles.phone}>{user.phoneNumbers[0].phoneNumber}</Text>
|
||||
)}
|
||||
|
||||
<View style={styles.roleBadge}>
|
||||
<Text style={styles.roleText}>
|
||||
{user?.role.charAt(0).toUpperCase() + user?.role.slice(1)}
|
||||
</Text>
|
||||
<Text style={styles.roleText}>User</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity style={styles.logoutButton} onPress={handleLogout}>
|
||||
<Text style={styles.logoutText}>Logout</Text>
|
||||
</TouchableOpacity>
|
||||
<View style={styles.logoutButton}>
|
||||
<TouchableOpacity onPress={() => signOut()}>
|
||||
<Text style={styles.logoutText}>Logout</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
padding: 20,
|
||||
backgroundColor: '#f5f5f5',
|
||||
backgroundColor: "#f5f5f5",
|
||||
},
|
||||
profileCard: {
|
||||
backgroundColor: 'white',
|
||||
backgroundColor: "white",
|
||||
borderRadius: 12,
|
||||
padding: 24,
|
||||
alignItems: 'center',
|
||||
shadowColor: '#000',
|
||||
alignItems: "center",
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
@ -72,45 +75,45 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
title: {
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
fontWeight: "bold",
|
||||
marginBottom: 16,
|
||||
},
|
||||
name: {
|
||||
fontSize: 20,
|
||||
fontWeight: '600',
|
||||
fontWeight: "600",
|
||||
marginBottom: 4,
|
||||
},
|
||||
email: {
|
||||
fontSize: 16,
|
||||
color: '#666',
|
||||
color: "#666",
|
||||
marginBottom: 4,
|
||||
},
|
||||
phone: {
|
||||
fontSize: 16,
|
||||
color: '#666',
|
||||
color: "#666",
|
||||
marginBottom: 16,
|
||||
},
|
||||
roleBadge: {
|
||||
backgroundColor: '#3b82f6',
|
||||
backgroundColor: "#3b82f6",
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 20,
|
||||
},
|
||||
roleText: {
|
||||
color: 'white',
|
||||
color: "white",
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
fontWeight: "600",
|
||||
},
|
||||
logoutButton: {
|
||||
backgroundColor: '#ef4444',
|
||||
backgroundColor: "#ef4444",
|
||||
paddingVertical: 14,
|
||||
borderRadius: 8,
|
||||
alignItems: 'center',
|
||||
alignItems: "center",
|
||||
marginTop: 24,
|
||||
},
|
||||
logoutText: {
|
||||
color: 'white',
|
||||
color: "white",
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
fontWeight: "600",
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
@ -1,15 +1,12 @@
|
||||
import { AuthProvider } from '@/contexts/AuthContext'
|
||||
import { Stack } from 'expo-router'
|
||||
import { View, Text } from 'react-native'
|
||||
import { ClerkProvider } from "@clerk/clerk-expo";
|
||||
import { Slot } from "expo-router";
|
||||
|
||||
export default function RootLayout() {
|
||||
return (
|
||||
<AuthProvider>
|
||||
<Stack>
|
||||
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="login" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="register" options={{ headerShown: false }} />
|
||||
</Stack>
|
||||
</AuthProvider>
|
||||
)
|
||||
}
|
||||
<ClerkProvider
|
||||
publishableKey={process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY}
|
||||
>
|
||||
<Slot />
|
||||
</ClerkProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,161 +1,139 @@
|
||||
import React, { useState } from 'react'
|
||||
import { View, Text, TextInput, TouchableOpacity, StyleSheet, Alert } from 'react-native'
|
||||
import { useRouter } from 'expo-router'
|
||||
import axios from 'axios'
|
||||
import * as SecureStore from 'expo-secure-store'
|
||||
import { API_BASE_URL, API_ENDPOINTS } from '../config/api'
|
||||
import { useSignIn, useOAuth, useUser } from "@clerk/clerk-expo";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
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() {
|
||||
const [formData, setFormData] = useState({
|
||||
email: '',
|
||||
password: '',
|
||||
})
|
||||
const [loading, setLoading] = useState(false)
|
||||
const router = useRouter()
|
||||
const { signIn, setActive } = useSignIn();
|
||||
const { startOAuthFlow } = useOAuth({ strategy: "oauth_google" });
|
||||
const { user } = useUser();
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const router = useRouter();
|
||||
|
||||
const handleLogin = async () => {
|
||||
if (!formData.email || !formData.password) {
|
||||
Alert.alert('Error', 'Please fill in all fields')
|
||||
return
|
||||
const handleSignIn = async () => {
|
||||
if (!email || !password) {
|
||||
Alert.alert("Error", "Please fill in all fields");
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const response = await axios.post(`${API_BASE_URL}${API_ENDPOINTS.AUTH.LOGIN}`, formData)
|
||||
|
||||
if (response.data.user) {
|
||||
await SecureStore.setItemAsync('user', JSON.stringify(response.data.user))
|
||||
|
||||
// Check if user has completed fitness profile
|
||||
try {
|
||||
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') }
|
||||
])
|
||||
}
|
||||
const result = await signIn.create({
|
||||
identifier: email,
|
||||
password,
|
||||
});
|
||||
|
||||
if (result.status === "complete") {
|
||||
await setActive({ session: result.createdSessionId });
|
||||
router.replace("/(tabs)/profile");
|
||||
} else {
|
||||
Alert.alert("Error", "Sign in failed");
|
||||
}
|
||||
} catch (error: any) {
|
||||
Alert.alert('Error', error.response?.data?.error || 'Login failed')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
} catch (err: any) {
|
||||
Alert.alert("Error", err.message || "Sign in failed");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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 (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>Welcome Back</Text>
|
||||
<Text style={styles.subtitle}>Login to your FitAI account</Text>
|
||||
|
||||
<View style={styles.form}>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Email"
|
||||
value={formData.email}
|
||||
onChangeText={(text) => setFormData({ ...formData, email: text })}
|
||||
keyboardType="email-address"
|
||||
autoCapitalize="none"
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Password"
|
||||
value={formData.password}
|
||||
onChangeText={(text) => setFormData({ ...formData, password: text })}
|
||||
secureTextEntry
|
||||
/>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.button, loading && styles.buttonDisabled]}
|
||||
onPress={handleLogin}
|
||||
disabled={loading}
|
||||
>
|
||||
<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>
|
||||
<Text style={styles.title}>Sign In</Text>
|
||||
<TextInput
|
||||
placeholder="Email"
|
||||
value={email}
|
||||
onChangeText={setEmail}
|
||||
style={styles.input}
|
||||
keyboardType="email-address"
|
||||
autoCapitalize="none"
|
||||
/>
|
||||
<TextInput
|
||||
placeholder="Password"
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
style={styles.input}
|
||||
secureTextEntry
|
||||
/>
|
||||
<TouchableOpacity onPress={handleSignIn} style={styles.button}>
|
||||
<Text style={styles.buttonText}>Sign In</Text>
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.orText}>Or</Text>
|
||||
<TouchableOpacity
|
||||
onPress={handleGoogleSignIn}
|
||||
style={styles.googleButton}
|
||||
>
|
||||
<Text style={styles.googleButtonText}>Sign In with Google</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#f5f5f5',
|
||||
justifyContent: "center",
|
||||
padding: 20,
|
||||
backgroundColor: "#f5f5f5",
|
||||
},
|
||||
title: {
|
||||
fontSize: 32,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 8,
|
||||
color: '#333',
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: 16,
|
||||
color: '#666',
|
||||
marginBottom: 32,
|
||||
},
|
||||
form: {
|
||||
width: '100%',
|
||||
maxWidth: 400,
|
||||
fontSize: 24,
|
||||
fontWeight: "bold",
|
||||
textAlign: "center",
|
||||
marginBottom: 20,
|
||||
},
|
||||
input: {
|
||||
backgroundColor: 'white',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
borderRadius: 8,
|
||||
marginBottom: 16,
|
||||
borderWidth: 1,
|
||||
borderColor: '#ddd',
|
||||
borderColor: "#ddd",
|
||||
padding: 15,
|
||||
marginBottom: 15,
|
||||
borderRadius: 8,
|
||||
backgroundColor: "white",
|
||||
},
|
||||
button: {
|
||||
backgroundColor: '#3b82f6',
|
||||
paddingVertical: 14,
|
||||
backgroundColor: "#3b82f6",
|
||||
padding: 15,
|
||||
borderRadius: 8,
|
||||
alignItems: 'center',
|
||||
marginBottom: 16,
|
||||
},
|
||||
buttonDisabled: {
|
||||
backgroundColor: '#9ca3af',
|
||||
alignItems: "center",
|
||||
marginBottom: 10,
|
||||
},
|
||||
buttonText: {
|
||||
color: 'white',
|
||||
color: "white",
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
fontWeight: "600",
|
||||
},
|
||||
linkButton: {
|
||||
alignItems: 'center',
|
||||
orText: {
|
||||
textAlign: "center",
|
||||
marginVertical: 10,
|
||||
fontSize: 16,
|
||||
color: "#666",
|
||||
},
|
||||
linkText: {
|
||||
color: '#3b82f6',
|
||||
fontSize: 14,
|
||||
googleButton: {
|
||||
backgroundColor: "#db4437",
|
||||
padding: 15,
|
||||
borderRadius: 8,
|
||||
alignItems: "center",
|
||||
},
|
||||
})
|
||||
googleButtonText: {
|
||||
color: "white",
|
||||
fontSize: 16,
|
||||
fontWeight: "600",
|
||||
},
|
||||
});
|
||||
|
||||
@ -1,164 +1,117 @@
|
||||
import React, { useState } from 'react'
|
||||
import { View, Text, TextInput, TouchableOpacity, StyleSheet, Alert } from 'react-native'
|
||||
import { useRouter } from 'expo-router'
|
||||
import axios from 'axios'
|
||||
import { API_BASE_URL, API_ENDPOINTS } from '../config/api'
|
||||
import { useSignUp } from "@clerk/clerk-expo";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
Alert,
|
||||
StyleSheet,
|
||||
} from "react-native";
|
||||
import { useRouter } from "expo-router";
|
||||
|
||||
export default function RegisterScreen() {
|
||||
const [formData, setFormData] = useState({
|
||||
email: '',
|
||||
password: '',
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
phone: '',
|
||||
})
|
||||
const [loading, setLoading] = useState(false)
|
||||
const router = useRouter()
|
||||
const { signUp, setActive } = useSignUp();
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [firstName, setFirstName] = useState("");
|
||||
const [lastName, setLastName] = useState("");
|
||||
const router = useRouter();
|
||||
|
||||
const handleRegister = async () => {
|
||||
if (!formData.email || !formData.password || !formData.firstName || !formData.lastName) {
|
||||
Alert.alert('Error', 'Please fill in all required fields')
|
||||
return
|
||||
const handleSignUp = async () => {
|
||||
if (!email || !password || !firstName || !lastName) {
|
||||
Alert.alert("Error", "Please fill in all fields");
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const response = await axios.post(`${API_BASE_URL}${API_ENDPOINTS.AUTH.REGISTER}`, formData)
|
||||
|
||||
if (response.status === 201) {
|
||||
Alert.alert('Success', 'Registration successful! Please login.', [
|
||||
{ text: 'OK', onPress: () => router.push('/login') }
|
||||
])
|
||||
const result = await signUp.create({
|
||||
emailAddress: email,
|
||||
password,
|
||||
firstName,
|
||||
lastName,
|
||||
});
|
||||
|
||||
if (result.status === "complete") {
|
||||
await setActive({ session: result.createdSessionId });
|
||||
router.replace("/(tabs)/profile");
|
||||
} else {
|
||||
Alert.alert("Error", "Sign up failed");
|
||||
}
|
||||
} catch (error: any) {
|
||||
Alert.alert('Error', error.response?.data?.error || 'Registration failed')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
} catch (err: any) {
|
||||
Alert.alert("Error", err.message || "Sign up failed");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>Create Account</Text>
|
||||
<Text style={styles.subtitle}>Join FitAI today</Text>
|
||||
|
||||
<View style={styles.form}>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="First Name"
|
||||
value={formData.firstName}
|
||||
onChangeText={(text) => setFormData({ ...formData, firstName: text })}
|
||||
autoCapitalize="words"
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Last Name"
|
||||
value={formData.lastName}
|
||||
onChangeText={(text) => setFormData({ ...formData, lastName: text })}
|
||||
autoCapitalize="words"
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Email"
|
||||
value={formData.email}
|
||||
onChangeText={(text) => setFormData({ ...formData, email: text })}
|
||||
keyboardType="email-address"
|
||||
autoCapitalize="none"
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Phone (optional)"
|
||||
value={formData.phone}
|
||||
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>
|
||||
<Text style={styles.title}>Sign Up</Text>
|
||||
<TextInput
|
||||
placeholder="First Name"
|
||||
value={firstName}
|
||||
onChangeText={setFirstName}
|
||||
style={styles.input}
|
||||
autoCapitalize="words"
|
||||
/>
|
||||
<TextInput
|
||||
placeholder="Last Name"
|
||||
value={lastName}
|
||||
onChangeText={setLastName}
|
||||
style={styles.input}
|
||||
autoCapitalize="words"
|
||||
/>
|
||||
<TextInput
|
||||
placeholder="Email"
|
||||
value={email}
|
||||
onChangeText={setEmail}
|
||||
style={styles.input}
|
||||
keyboardType="email-address"
|
||||
autoCapitalize="none"
|
||||
/>
|
||||
<TextInput
|
||||
placeholder="Password"
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
style={styles.input}
|
||||
secureTextEntry
|
||||
/>
|
||||
<TouchableOpacity onPress={handleSignUp} style={styles.button}>
|
||||
<Text style={styles.buttonText}>Sign Up</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#f5f5f5',
|
||||
justifyContent: "center",
|
||||
padding: 20,
|
||||
backgroundColor: "#f5f5f5",
|
||||
},
|
||||
title: {
|
||||
fontSize: 32,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 8,
|
||||
color: '#333',
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: 16,
|
||||
color: '#666',
|
||||
marginBottom: 32,
|
||||
},
|
||||
form: {
|
||||
width: '100%',
|
||||
maxWidth: 400,
|
||||
fontSize: 24,
|
||||
fontWeight: "bold",
|
||||
textAlign: "center",
|
||||
marginBottom: 20,
|
||||
},
|
||||
input: {
|
||||
backgroundColor: 'white',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
borderRadius: 8,
|
||||
marginBottom: 16,
|
||||
borderWidth: 1,
|
||||
borderColor: '#ddd',
|
||||
borderColor: "#ddd",
|
||||
padding: 15,
|
||||
marginBottom: 15,
|
||||
borderRadius: 8,
|
||||
backgroundColor: "white",
|
||||
},
|
||||
button: {
|
||||
backgroundColor: '#3b82f6',
|
||||
paddingVertical: 14,
|
||||
backgroundColor: "#3b82f6",
|
||||
padding: 15,
|
||||
borderRadius: 8,
|
||||
alignItems: 'center',
|
||||
marginBottom: 16,
|
||||
},
|
||||
buttonDisabled: {
|
||||
backgroundColor: '#9ca3af',
|
||||
alignItems: "center",
|
||||
},
|
||||
buttonText: {
|
||||
color: 'white',
|
||||
color: "white",
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
fontWeight: "600",
|
||||
},
|
||||
linkButton: {
|
||||
alignItems: 'center',
|
||||
},
|
||||
linkText: {
|
||||
color: '#3b82f6',
|
||||
fontSize: 14,
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
@ -1,99 +1,106 @@
|
||||
import React, { useState } from 'react'
|
||||
import { View, Text, TextInput, TouchableOpacity, StyleSheet, Alert, ScrollView } from 'react-native'
|
||||
import { useRouter } from 'expo-router'
|
||||
import axios from 'axios'
|
||||
import * as SecureStore from 'expo-secure-store'
|
||||
import { API_BASE_URL, API_ENDPOINTS } from '../config/api'
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
TextInput,
|
||||
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 {
|
||||
height: string
|
||||
weight: string
|
||||
age: string
|
||||
gender: 'male' | 'female' | 'other'
|
||||
activityLevel: 'sedentary' | 'light' | 'moderate' | 'active' | 'very_active'
|
||||
fitnessGoals: string[]
|
||||
exerciseHabits: string
|
||||
dietHabits: string
|
||||
medicalConditions: string
|
||||
height: string;
|
||||
weight: string;
|
||||
age: string;
|
||||
gender: "male" | "female" | "other";
|
||||
activityLevel: "sedentary" | "light" | "moderate" | "active" | "very_active";
|
||||
fitnessGoals: string[];
|
||||
exerciseHabits: string;
|
||||
dietHabits: string;
|
||||
medicalConditions: string;
|
||||
}
|
||||
|
||||
export default function WelcomeScreen() {
|
||||
const [profile, setProfile] = useState<FitnessProfile>({
|
||||
height: '',
|
||||
weight: '',
|
||||
age: '',
|
||||
gender: 'male',
|
||||
activityLevel: 'moderate',
|
||||
height: "",
|
||||
weight: "",
|
||||
age: "",
|
||||
gender: "male",
|
||||
activityLevel: "moderate",
|
||||
fitnessGoals: [],
|
||||
exerciseHabits: '',
|
||||
dietHabits: '',
|
||||
medicalConditions: '',
|
||||
})
|
||||
const [loading, setLoading] = useState(false)
|
||||
const router = useRouter()
|
||||
exerciseHabits: "",
|
||||
dietHabits: "",
|
||||
medicalConditions: "",
|
||||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
const fitnessGoalsOptions = [
|
||||
'Weight Loss',
|
||||
'Muscle Gain',
|
||||
'Improve Endurance',
|
||||
'Better Flexibility',
|
||||
'General Fitness',
|
||||
'Strength Training',
|
||||
'Cardio Health'
|
||||
]
|
||||
"Weight Loss",
|
||||
"Muscle Gain",
|
||||
"Improve Endurance",
|
||||
"Better Flexibility",
|
||||
"General Fitness",
|
||||
"Strength Training",
|
||||
"Cardio Health",
|
||||
];
|
||||
|
||||
const activityLevels = [
|
||||
{ value: 'sedentary', label: 'Sedentary (little or no exercise)' },
|
||||
{ value: 'light', label: 'Light (1-3 days/week)' },
|
||||
{ value: 'moderate', label: 'Moderate (3-5 days/week)' },
|
||||
{ value: 'active', label: 'Active (6-7 days/week)' },
|
||||
{ value: 'very_active', label: 'Very Active (twice per day)' }
|
||||
]
|
||||
{ value: "sedentary", label: "Sedentary (little or no exercise)" },
|
||||
{ value: "light", label: "Light (1-3 days/week)" },
|
||||
{ value: "moderate", label: "Moderate (3-5 days/week)" },
|
||||
{ value: "active", label: "Active (6-7 days/week)" },
|
||||
{ value: "very_active", label: "Very Active (twice per day)" },
|
||||
];
|
||||
|
||||
const toggleGoal = (goal: string) => {
|
||||
setProfile(prev => ({
|
||||
setProfile((prev) => ({
|
||||
...prev,
|
||||
fitnessGoals: prev.fitnessGoals.includes(goal)
|
||||
? prev.fitnessGoals.filter(g => g !== goal)
|
||||
: [...prev.fitnessGoals, goal]
|
||||
}))
|
||||
}
|
||||
? prev.fitnessGoals.filter((g) => g !== goal)
|
||||
: [...prev.fitnessGoals, goal],
|
||||
}));
|
||||
};
|
||||
|
||||
const { user } = useUser();
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!profile.height || !profile.weight || !profile.age) {
|
||||
Alert.alert('Error', 'Please fill in all required fields')
|
||||
return
|
||||
Alert.alert("Error", "Please fill in all required fields");
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
setLoading(true);
|
||||
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(
|
||||
`${API_BASE_URL}${API_ENDPOINTS.PROFILE.FITNESS}`,
|
||||
{
|
||||
userId: userData.id,
|
||||
...profile
|
||||
}
|
||||
)
|
||||
userId: user?.id,
|
||||
...profile,
|
||||
},
|
||||
);
|
||||
|
||||
if (response.status === 201) {
|
||||
Alert.alert('Success', 'Profile completed successfully!', [
|
||||
{ text: 'OK', onPress: () => router.replace('/(tabs)') }
|
||||
])
|
||||
Alert.alert("Success", "Profile completed successfully!", [
|
||||
{ text: "OK", onPress: () => router.replace("/(tabs)") },
|
||||
]);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.log('Profile save error:', error)
|
||||
Alert.alert('Error', error.response?.data?.error || 'Failed to save profile. Please try again.')
|
||||
console.log("Profile save error:", error);
|
||||
Alert.alert(
|
||||
"Error",
|
||||
error.response?.data?.error ||
|
||||
"Failed to save profile. Please try again.",
|
||||
);
|
||||
} finally {
|
||||
setLoading(false)
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ScrollView style={styles.container}>
|
||||
@ -103,25 +110,29 @@ export default function WelcomeScreen() {
|
||||
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>Basic Information</Text>
|
||||
|
||||
|
||||
<View style={styles.row}>
|
||||
<View style={[styles.inputContainer, { flex: 1, marginRight: 8 }]}>
|
||||
<Text style={styles.label}>Height (cm)</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={profile.height}
|
||||
onChangeText={(text) => setProfile({ ...profile, height: text })}
|
||||
onChangeText={(text) =>
|
||||
setProfile({ ...profile, height: text })
|
||||
}
|
||||
keyboardType="numeric"
|
||||
placeholder="170"
|
||||
/>
|
||||
</View>
|
||||
|
||||
|
||||
<View style={[styles.inputContainer, { flex: 1, marginLeft: 8 }]}>
|
||||
<Text style={styles.label}>Weight (kg)</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={profile.weight}
|
||||
onChangeText={(text) => setProfile({ ...profile, weight: text })}
|
||||
onChangeText={(text) =>
|
||||
setProfile({ ...profile, weight: text })
|
||||
}
|
||||
keyboardType="numeric"
|
||||
placeholder="70"
|
||||
/>
|
||||
@ -139,23 +150,26 @@ export default function WelcomeScreen() {
|
||||
placeholder="25"
|
||||
/>
|
||||
</View>
|
||||
|
||||
|
||||
<View style={[styles.inputContainer, { flex: 1, marginLeft: 8 }]}>
|
||||
<Text style={styles.label}>Gender</Text>
|
||||
<View style={styles.genderRow}>
|
||||
{(['male', 'female', 'other'] as const).map((gender) => (
|
||||
{(["male", "female", "other"] as const).map((gender) => (
|
||||
<TouchableOpacity
|
||||
key={gender}
|
||||
style={[
|
||||
styles.genderButton,
|
||||
profile.gender === gender && styles.genderButtonSelected
|
||||
profile.gender === gender && styles.genderButtonSelected,
|
||||
]}
|
||||
onPress={() => setProfile({ ...profile, gender })}
|
||||
>
|
||||
<Text style={[
|
||||
styles.genderButtonText,
|
||||
profile.gender === gender && styles.genderButtonTextSelected
|
||||
]}>
|
||||
<Text
|
||||
style={[
|
||||
styles.genderButtonText,
|
||||
profile.gender === gender &&
|
||||
styles.genderButtonTextSelected,
|
||||
]}
|
||||
>
|
||||
{gender.charAt(0).toUpperCase() + gender.slice(1)}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
@ -172,14 +186,20 @@ export default function WelcomeScreen() {
|
||||
key={level.value}
|
||||
style={[
|
||||
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={[
|
||||
styles.activityText,
|
||||
profile.activityLevel === level.value && styles.activityTextSelected
|
||||
]}>
|
||||
<Text
|
||||
style={[
|
||||
styles.activityText,
|
||||
profile.activityLevel === level.value &&
|
||||
styles.activityTextSelected,
|
||||
]}
|
||||
>
|
||||
{level.label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
@ -195,14 +215,18 @@ export default function WelcomeScreen() {
|
||||
key={goal}
|
||||
style={[
|
||||
styles.goalButton,
|
||||
profile.fitnessGoals.includes(goal) && styles.goalButtonSelected
|
||||
profile.fitnessGoals.includes(goal) &&
|
||||
styles.goalButtonSelected,
|
||||
]}
|
||||
onPress={() => toggleGoal(goal)}
|
||||
>
|
||||
<Text style={[
|
||||
styles.goalButtonText,
|
||||
profile.fitnessGoals.includes(goal) && styles.goalButtonTextSelected
|
||||
]}>
|
||||
<Text
|
||||
style={[
|
||||
styles.goalButtonText,
|
||||
profile.fitnessGoals.includes(goal) &&
|
||||
styles.goalButtonTextSelected,
|
||||
]}
|
||||
>
|
||||
{goal}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
@ -215,7 +239,9 @@ export default function WelcomeScreen() {
|
||||
<TextInput
|
||||
style={[styles.input, styles.textArea]}
|
||||
value={profile.exerciseHabits}
|
||||
onChangeText={(text) => setProfile({ ...profile, exerciseHabits: text })}
|
||||
onChangeText={(text) =>
|
||||
setProfile({ ...profile, exerciseHabits: text })
|
||||
}
|
||||
placeholder="Describe your current exercise routine..."
|
||||
multiline
|
||||
numberOfLines={3}
|
||||
@ -227,7 +253,9 @@ export default function WelcomeScreen() {
|
||||
<TextInput
|
||||
style={[styles.input, styles.textArea]}
|
||||
value={profile.dietHabits}
|
||||
onChangeText={(text) => setProfile({ ...profile, dietHabits: text })}
|
||||
onChangeText={(text) =>
|
||||
setProfile({ ...profile, dietHabits: text })
|
||||
}
|
||||
placeholder="Describe your current eating habits..."
|
||||
multiline
|
||||
numberOfLines={3}
|
||||
@ -239,7 +267,9 @@ export default function WelcomeScreen() {
|
||||
<TextInput
|
||||
style={[styles.input, styles.textArea]}
|
||||
value={profile.medicalConditions}
|
||||
onChangeText={(text) => setProfile({ ...profile, medicalConditions: text })}
|
||||
onChangeText={(text) =>
|
||||
setProfile({ ...profile, medicalConditions: text })
|
||||
}
|
||||
placeholder="Any medical conditions we should know about..."
|
||||
multiline
|
||||
numberOfLines={3}
|
||||
@ -252,51 +282,51 @@ export default function WelcomeScreen() {
|
||||
disabled={loading}
|
||||
>
|
||||
<Text style={styles.buttonText}>
|
||||
{loading ? 'Saving...' : 'Complete Profile'}
|
||||
{loading ? "Saving..." : "Complete Profile"}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</ScrollView>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#f5f5f5',
|
||||
backgroundColor: "#f5f5f5",
|
||||
},
|
||||
content: {
|
||||
padding: 20,
|
||||
},
|
||||
title: {
|
||||
fontSize: 28,
|
||||
fontWeight: 'bold',
|
||||
fontWeight: "bold",
|
||||
marginBottom: 8,
|
||||
color: '#333',
|
||||
textAlign: 'center',
|
||||
color: "#333",
|
||||
textAlign: "center",
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: 16,
|
||||
color: '#666',
|
||||
color: "#666",
|
||||
marginBottom: 32,
|
||||
textAlign: 'center',
|
||||
textAlign: "center",
|
||||
},
|
||||
section: {
|
||||
marginBottom: 24,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
fontWeight: "600",
|
||||
marginBottom: 12,
|
||||
color: '#333',
|
||||
color: "#333",
|
||||
},
|
||||
sectionSubtitle: {
|
||||
fontSize: 14,
|
||||
color: '#666',
|
||||
color: "#666",
|
||||
marginBottom: 12,
|
||||
},
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
flexDirection: "row",
|
||||
marginBottom: 16,
|
||||
},
|
||||
inputContainer: {
|
||||
@ -304,105 +334,105 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
label: {
|
||||
fontSize: 14,
|
||||
fontWeight: '500',
|
||||
fontWeight: "500",
|
||||
marginBottom: 6,
|
||||
color: '#333',
|
||||
color: "#333",
|
||||
},
|
||||
input: {
|
||||
backgroundColor: 'white',
|
||||
backgroundColor: "white",
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
borderRadius: 8,
|
||||
borderWidth: 1,
|
||||
borderColor: '#ddd',
|
||||
borderColor: "#ddd",
|
||||
fontSize: 16,
|
||||
},
|
||||
textArea: {
|
||||
height: 80,
|
||||
textAlignVertical: 'top',
|
||||
textAlignVertical: "top",
|
||||
},
|
||||
genderRow: {
|
||||
flexDirection: 'row',
|
||||
flexDirection: "row",
|
||||
},
|
||||
genderButton: {
|
||||
flex: 1,
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 8,
|
||||
borderWidth: 1,
|
||||
borderColor: '#ddd',
|
||||
borderColor: "#ddd",
|
||||
borderRadius: 8,
|
||||
alignItems: 'center',
|
||||
alignItems: "center",
|
||||
marginRight: 4,
|
||||
},
|
||||
genderButtonSelected: {
|
||||
backgroundColor: '#3b82f6',
|
||||
borderColor: '#3b82f6',
|
||||
backgroundColor: "#3b82f6",
|
||||
borderColor: "#3b82f6",
|
||||
},
|
||||
genderButtonText: {
|
||||
fontSize: 12,
|
||||
color: '#666',
|
||||
color: "#666",
|
||||
},
|
||||
genderButtonTextSelected: {
|
||||
color: 'white',
|
||||
color: "white",
|
||||
},
|
||||
activityOption: {
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 16,
|
||||
borderWidth: 1,
|
||||
borderColor: '#ddd',
|
||||
borderColor: "#ddd",
|
||||
borderRadius: 8,
|
||||
marginBottom: 8,
|
||||
backgroundColor: 'white',
|
||||
backgroundColor: "white",
|
||||
},
|
||||
activityOptionSelected: {
|
||||
backgroundColor: '#3b82f6',
|
||||
borderColor: '#3b82f6',
|
||||
backgroundColor: "#3b82f6",
|
||||
borderColor: "#3b82f6",
|
||||
},
|
||||
activityText: {
|
||||
fontSize: 14,
|
||||
color: '#333',
|
||||
color: "#333",
|
||||
},
|
||||
activityTextSelected: {
|
||||
color: 'white',
|
||||
color: "white",
|
||||
},
|
||||
goalsContainer: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
flexDirection: "row",
|
||||
flexWrap: "wrap",
|
||||
marginHorizontal: -4,
|
||||
},
|
||||
goalButton: {
|
||||
backgroundColor: 'white',
|
||||
backgroundColor: "white",
|
||||
borderWidth: 1,
|
||||
borderColor: '#ddd',
|
||||
borderColor: "#ddd",
|
||||
borderRadius: 20,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 8,
|
||||
margin: 4,
|
||||
},
|
||||
goalButtonSelected: {
|
||||
backgroundColor: '#3b82f6',
|
||||
borderColor: '#3b82f6',
|
||||
backgroundColor: "#3b82f6",
|
||||
borderColor: "#3b82f6",
|
||||
},
|
||||
goalButtonText: {
|
||||
fontSize: 12,
|
||||
color: '#666',
|
||||
color: "#666",
|
||||
},
|
||||
goalButtonTextSelected: {
|
||||
color: 'white',
|
||||
color: "white",
|
||||
},
|
||||
button: {
|
||||
backgroundColor: '#3b82f6',
|
||||
backgroundColor: "#3b82f6",
|
||||
paddingVertical: 16,
|
||||
borderRadius: 8,
|
||||
alignItems: 'center',
|
||||
alignItems: "center",
|
||||
marginTop: 16,
|
||||
},
|
||||
buttonDisabled: {
|
||||
backgroundColor: '#9ca3af',
|
||||
backgroundColor: "#9ca3af",
|
||||
},
|
||||
buttonText: {
|
||||
color: 'white',
|
||||
color: "white",
|
||||
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 { useRouter } from 'expo-router'
|
||||
import { useEffect } from 'react'
|
||||
import { useUser } from "@clerk/clerk-expo";
|
||||
import { useRouter } from "expo-router";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export function useRequireAuth() {
|
||||
const { user, isLoading } = useAuth()
|
||||
const router = useRouter()
|
||||
const { user, isLoaded, isSignedIn } = useUser();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading && !user) {
|
||||
router.replace('/login')
|
||||
if (isLoaded && !isSignedIn) {
|
||||
router.replace("/login");
|
||||
}
|
||||
}, [user, isLoading, router])
|
||||
}, [isSignedIn, isLoaded, router]);
|
||||
|
||||
return { user, isLoading }
|
||||
}
|
||||
return { user, isLoading: !isLoaded };
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user