Compare commits
No commits in common. "febcdc111effb2b68b15a263dc1b6aaac3571471" and "fc12cecd306ed7515bfbe4397deb7eda987c74db" have entirely different histories.
febcdc111e
...
fc12cecd30
Binary file not shown.
179
apps/mobile/package-lock.json
generated
179
apps/mobile/package-lock.json
generated
@ -23,8 +23,6 @@
|
|||||||
"expo-constants": "^18.0.10",
|
"expo-constants": "^18.0.10",
|
||||||
"expo-crypto": "^15.0.7",
|
"expo-crypto": "^15.0.7",
|
||||||
"expo-font": "~14.0.9",
|
"expo-font": "~14.0.9",
|
||||||
"expo-haptics": "^15.0.7",
|
|
||||||
"expo-linear-gradient": "~15.0.7",
|
|
||||||
"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",
|
||||||
@ -37,7 +35,6 @@
|
|||||||
"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",
|
||||||
"react-native-screens": "~4.16.0",
|
"react-native-screens": "~4.16.0",
|
||||||
"react-native-svg": "^15.15.0",
|
|
||||||
"react-native-web": "^0.21.2",
|
"react-native-web": "^0.21.2",
|
||||||
"zod": "^3.22.0"
|
"zod": "^3.22.0"
|
||||||
},
|
},
|
||||||
@ -5598,12 +5595,6 @@
|
|||||||
"node": ">=0.6"
|
"node": ">=0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/boolbase": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
|
|
||||||
"license": "ISC"
|
|
||||||
},
|
|
||||||
"node_modules/bplist-creator": {
|
"node_modules/bplist-creator": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz",
|
||||||
@ -6276,56 +6267,6 @@
|
|||||||
"hyphenate-style-name": "^1.0.3"
|
"hyphenate-style-name": "^1.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/css-select": {
|
|
||||||
"version": "5.2.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
|
|
||||||
"integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==",
|
|
||||||
"license": "BSD-2-Clause",
|
|
||||||
"dependencies": {
|
|
||||||
"boolbase": "^1.0.0",
|
|
||||||
"css-what": "^6.1.0",
|
|
||||||
"domhandler": "^5.0.2",
|
|
||||||
"domutils": "^3.0.1",
|
|
||||||
"nth-check": "^2.0.1"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/fb55"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/css-tree": {
|
|
||||||
"version": "1.1.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz",
|
|
||||||
"integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"mdn-data": "2.0.14",
|
|
||||||
"source-map": "^0.6.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/css-tree/node_modules/source-map": {
|
|
||||||
"version": "0.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
|
||||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
|
||||||
"license": "BSD-3-Clause",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/css-what": {
|
|
||||||
"version": "6.2.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
|
|
||||||
"integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==",
|
|
||||||
"license": "BSD-2-Clause",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 6"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/fb55"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/csstype": {
|
"node_modules/csstype": {
|
||||||
"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",
|
||||||
@ -6551,61 +6492,6 @@
|
|||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/dom-serializer": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"domelementtype": "^2.3.0",
|
|
||||||
"domhandler": "^5.0.2",
|
|
||||||
"entities": "^4.2.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/domelementtype": {
|
|
||||||
"version": "2.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
|
|
||||||
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/fb55"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"license": "BSD-2-Clause"
|
|
||||||
},
|
|
||||||
"node_modules/domhandler": {
|
|
||||||
"version": "5.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
|
|
||||||
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
|
||||||
"license": "BSD-2-Clause",
|
|
||||||
"dependencies": {
|
|
||||||
"domelementtype": "^2.3.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 4"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/domutils": {
|
|
||||||
"version": "3.2.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
|
|
||||||
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
|
|
||||||
"license": "BSD-2-Clause",
|
|
||||||
"dependencies": {
|
|
||||||
"dom-serializer": "^2.0.0",
|
|
||||||
"domelementtype": "^2.3.0",
|
|
||||||
"domhandler": "^5.0.3"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/fb55/domutils?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/dotenv": {
|
"node_modules/dotenv": {
|
||||||
"version": "16.4.7",
|
"version": "16.4.7",
|
||||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
|
||||||
@ -6693,18 +6579,6 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/entities": {
|
|
||||||
"version": "4.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
|
||||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
|
||||||
"license": "BSD-2-Clause",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.12"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/env-editor": {
|
"node_modules/env-editor": {
|
||||||
"version": "0.4.2",
|
"version": "0.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/env-editor/-/env-editor-0.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/env-editor/-/env-editor-0.4.2.tgz",
|
||||||
@ -7260,26 +7134,6 @@
|
|||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/expo-haptics": {
|
|
||||||
"version": "15.0.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/expo-haptics/-/expo-haptics-15.0.7.tgz",
|
|
||||||
"integrity": "sha512-7flWsYPrwjJxZ8x82RiJtzsnk1Xp9ahnbd9PhCy3NnsemyMApoWIEUr4waPqFr80DtiLZfhD9VMLL1CKa8AImQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"peerDependencies": {
|
|
||||||
"expo": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/expo-linear-gradient": {
|
|
||||||
"version": "15.0.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/expo-linear-gradient/-/expo-linear-gradient-15.0.7.tgz",
|
|
||||||
"integrity": "sha512-yF+y+9Shpr/OQFfy/wglB/0bykFMbwHBTuMRa5Of/r2P1wbkcacx8rg0JsUWkXH/rn2i2iWdubyqlxSJa3ggZA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"peerDependencies": {
|
|
||||||
"expo": "*",
|
|
||||||
"react": "*",
|
|
||||||
"react-native": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/expo-linking": {
|
"node_modules/expo-linking": {
|
||||||
"version": "8.0.9",
|
"version": "8.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-8.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-8.0.9.tgz",
|
||||||
@ -10012,12 +9866,6 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mdn-data": {
|
|
||||||
"version": "2.0.14",
|
|
||||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
|
|
||||||
"integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==",
|
|
||||||
"license": "CC0-1.0"
|
|
||||||
},
|
|
||||||
"node_modules/memoize-one": {
|
"node_modules/memoize-one": {
|
||||||
"version": "5.2.1",
|
"version": "5.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
|
||||||
@ -10615,18 +10463,6 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/nth-check": {
|
|
||||||
"version": "2.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
|
|
||||||
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
|
|
||||||
"license": "BSD-2-Clause",
|
|
||||||
"dependencies": {
|
|
||||||
"boolbase": "^1.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/fb55/nth-check?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/nullthrows": {
|
"node_modules/nullthrows": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz",
|
||||||
@ -11648,21 +11484,6 @@
|
|||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-native-svg": {
|
|
||||||
"version": "15.15.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.15.0.tgz",
|
|
||||||
"integrity": "sha512-/Wx6F/IZ88B/GcF88bK8K7ZseJDYt+7WGaiggyzLvTowChQ8BM5idmcd4pK+6QJP6a6DmzL2sfOMukFUn/NArg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"css-select": "^5.1.0",
|
|
||||||
"css-tree": "^1.1.3",
|
|
||||||
"warn-once": "0.1.1"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": "*",
|
|
||||||
"react-native": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-native-url-polyfill": {
|
"node_modules/react-native-url-polyfill": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-url-polyfill/-/react-native-url-polyfill-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-url-polyfill/-/react-native-url-polyfill-2.0.0.tgz",
|
||||||
|
|||||||
@ -29,8 +29,6 @@
|
|||||||
"expo-constants": "^18.0.10",
|
"expo-constants": "^18.0.10",
|
||||||
"expo-crypto": "^15.0.7",
|
"expo-crypto": "^15.0.7",
|
||||||
"expo-font": "~14.0.9",
|
"expo-font": "~14.0.9",
|
||||||
"expo-haptics": "^15.0.7",
|
|
||||||
"expo-linear-gradient": "~15.0.7",
|
|
||||||
"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",
|
||||||
@ -43,7 +41,6 @@
|
|||||||
"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",
|
||||||
"react-native-screens": "~4.16.0",
|
"react-native-screens": "~4.16.0",
|
||||||
"react-native-svg": "^15.15.0",
|
|
||||||
"react-native-web": "^0.21.2",
|
"react-native-web": "^0.21.2",
|
||||||
"zod": "^3.22.0"
|
"zod": "^3.22.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { Tabs, useRouter, useSegments } from "expo-router";
|
import { Tabs, useRouter, useSegments } from "expo-router";
|
||||||
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import { useAuth } from "@clerk/clerk-expo";
|
import { useAuth } from "@clerk/clerk-expo";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { CustomTabBar } from "../../components/CustomTabBar";
|
|
||||||
|
|
||||||
export default function TabLayout() {
|
export default function TabLayout() {
|
||||||
const { isSignedIn, isLoaded } = useAuth();
|
const { isSignedIn, isLoaded } = useAuth();
|
||||||
@ -25,40 +25,56 @@ export default function TabLayout() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs
|
<Tabs
|
||||||
tabBar={(props) => <CustomTabBar {...props} />}
|
|
||||||
screenOptions={{
|
screenOptions={{
|
||||||
headerShown: false, // We'll use custom headers in screens or layout
|
tabBarActiveTintColor: "#2563eb",
|
||||||
tabBarShowLabel: false,
|
tabBarInactiveTintColor: "#6b7280",
|
||||||
|
headerShown: true,
|
||||||
|
headerStyle: {
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
},
|
||||||
|
headerTitleStyle: {
|
||||||
|
fontWeight: "600",
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Tabs.Screen
|
<Tabs.Screen
|
||||||
name="index"
|
name="index"
|
||||||
options={{
|
options={{
|
||||||
title: "Home",
|
title: "Home",
|
||||||
}}
|
headerTitle: "FitAI",
|
||||||
/>
|
tabBarIcon: ({ color, size }) => (
|
||||||
<Tabs.Screen
|
<Ionicons name="home" size={size} color={color} />
|
||||||
name="goals"
|
),
|
||||||
options={{
|
|
||||||
title: "Goals",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Tabs.Screen
|
|
||||||
name="recommendations"
|
|
||||||
options={{
|
|
||||||
title: "AI",
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Tabs.Screen
|
<Tabs.Screen
|
||||||
name="attendance"
|
name="attendance"
|
||||||
options={{
|
options={{
|
||||||
title: "Attendance",
|
title: "Attendance",
|
||||||
|
headerTitle: "Attendance",
|
||||||
|
tabBarIcon: ({ color, size }) => (
|
||||||
|
<Ionicons name="calendar" size={size} color={color} />
|
||||||
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Tabs.Screen
|
<Tabs.Screen
|
||||||
name="profile"
|
name="profile"
|
||||||
options={{
|
options={{
|
||||||
title: "Profile",
|
title: "Profile",
|
||||||
|
headerTitle: "My Profile",
|
||||||
|
tabBarIcon: ({ color, size }) => (
|
||||||
|
<Ionicons name="person" size={size} color={color} />
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Tabs.Screen
|
||||||
|
name="goals"
|
||||||
|
options={{
|
||||||
|
title: "Goals",
|
||||||
|
headerTitle: "Fitness Goals",
|
||||||
|
tabBarIcon: ({ color, size }) => (
|
||||||
|
<Ionicons name="trophy" size={size} color={color} />
|
||||||
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|||||||
@ -1,39 +1,15 @@
|
|||||||
import { View, Text, StyleSheet, TouchableOpacity, ActivityIndicator, ScrollView, Alert } from 'react-native'
|
import { View, Text, StyleSheet, TouchableOpacity, ActivityIndicator, ScrollView, Alert } from 'react-native'
|
||||||
import { useState, useEffect, useRef } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { useAuth } from '@clerk/clerk-expo'
|
import { useAuth } from '@clerk/clerk-expo'
|
||||||
import { LinearGradient } from 'expo-linear-gradient'
|
|
||||||
import { Ionicons } from '@expo/vector-icons'
|
|
||||||
import { attendanceApi, Attendance } from '../../api/attendance'
|
import { attendanceApi, Attendance } from '../../api/attendance'
|
||||||
import { theme } from '../../styles/theme'
|
|
||||||
import { Animated } from 'react-native'
|
|
||||||
|
|
||||||
export default function AttendanceScreen() {
|
export default function AttendanceScreen() {
|
||||||
const { getToken, userId } = useAuth()
|
const { getToken, userId } = useAuth()
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [activeCheckIn, setActiveCheckIn] = useState<Attendance | null>(null)
|
const [activeCheckIn, setActiveCheckIn] = useState<Attendance | null>(null)
|
||||||
const [history, setHistory] = useState<Attendance[]>([])
|
const [history, setHistory] = useState<Attendance[]>([])
|
||||||
const pulseAnim = useRef(new Animated.Value(1)).current
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (activeCheckIn) {
|
|
||||||
const pulse = Animated.loop(
|
|
||||||
Animated.sequence([
|
|
||||||
Animated.timing(pulseAnim, {
|
|
||||||
toValue: 1.05,
|
|
||||||
duration: 1000,
|
|
||||||
useNativeDriver: true,
|
|
||||||
}),
|
|
||||||
Animated.timing(pulseAnim, {
|
|
||||||
toValue: 1,
|
|
||||||
duration: 1000,
|
|
||||||
useNativeDriver: true,
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
)
|
|
||||||
pulse.start()
|
|
||||||
return () => pulse.stop()
|
|
||||||
}
|
|
||||||
}, [activeCheckIn])
|
|
||||||
|
|
||||||
const fetchAttendance = async () => {
|
const fetchAttendance = async () => {
|
||||||
try {
|
try {
|
||||||
@ -94,100 +70,42 @@ export default function AttendanceScreen() {
|
|||||||
if (loading && !history.length) {
|
if (loading && !history.length) {
|
||||||
return (
|
return (
|
||||||
<View style={styles.centered}>
|
<View style={styles.centered}>
|
||||||
<ActivityIndicator size="large" color={theme.colors.primary} />
|
<ActivityIndicator size="large" color="#000" />
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
|
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
|
||||||
<LinearGradient
|
<Text style={styles.title}>Attendance</Text>
|
||||||
colors={theme.gradients.primary}
|
<Text style={styles.subtitle}>Track your gym visits</Text>
|
||||||
start={{ x: 0, y: 0 }}
|
|
||||||
end={{ x: 1, y: 1 }}
|
|
||||||
style={styles.header}
|
|
||||||
>
|
|
||||||
<Text style={styles.title}>Attendance</Text>
|
|
||||||
<Text style={styles.subtitle}>Track your gym visits</Text>
|
|
||||||
</LinearGradient>
|
|
||||||
|
|
||||||
<View style={styles.actionContainer}>
|
<View style={styles.actionContainer}>
|
||||||
{activeCheckIn ? (
|
{activeCheckIn ? (
|
||||||
<LinearGradient
|
<View style={styles.activeCard}>
|
||||||
colors={['rgba(16, 185, 129, 0.15)', 'rgba(5, 150, 105, 0.1)']}
|
<Text style={styles.activeText}>Currently Checked In</Text>
|
||||||
style={[styles.activeCard, theme.shadows.medium]}
|
<Text style={styles.timeText}>
|
||||||
>
|
Since {new Date(activeCheckIn.checkInTime).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
||||||
<View style={styles.activeCardContent}>
|
</Text>
|
||||||
<View style={styles.activeIconContainer}>
|
<TouchableOpacity style={styles.checkOutButton} onPress={handleCheckOut}>
|
||||||
<LinearGradient
|
<Text style={styles.buttonText}>Check Out</Text>
|
||||||
colors={theme.gradients.success}
|
|
||||||
style={styles.activeIcon}
|
|
||||||
>
|
|
||||||
<Ionicons name="checkmark-circle" size={32} color="#fff" />
|
|
||||||
</LinearGradient>
|
|
||||||
</View>
|
|
||||||
<View style={styles.activeTextContainer}>
|
|
||||||
<Text style={styles.activeText}>Currently Checked In</Text>
|
|
||||||
<Text style={styles.timeText}>
|
|
||||||
Since {new Date(activeCheckIn.checkInTime).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
<TouchableOpacity onPress={handleCheckOut} activeOpacity={0.8}>
|
|
||||||
<LinearGradient
|
|
||||||
colors={theme.gradients.danger}
|
|
||||||
start={{ x: 0, y: 0 }}
|
|
||||||
end={{ x: 1, y: 0 }}
|
|
||||||
style={[styles.checkOutButton, theme.shadows.medium]}
|
|
||||||
>
|
|
||||||
<Ionicons name="log-out-outline" size={20} color="#fff" style={{ marginRight: 8 }} />
|
|
||||||
<Text style={styles.buttonText}>Check Out</Text>
|
|
||||||
</LinearGradient>
|
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</LinearGradient>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<Animated.View style={{ transform: [{ scale: pulseAnim }] }}>
|
<TouchableOpacity style={styles.checkInButton} onPress={handleCheckIn}>
|
||||||
<TouchableOpacity onPress={handleCheckIn} activeOpacity={0.8}>
|
<Text style={styles.buttonText}>Check In</Text>
|
||||||
<LinearGradient
|
</TouchableOpacity>
|
||||||
colors={theme.gradients.primary}
|
|
||||||
start={{ x: 0, y: 0 }}
|
|
||||||
end={{ x: 1, y: 0 }}
|
|
||||||
style={[styles.checkInButton, theme.shadows.glow]}
|
|
||||||
>
|
|
||||||
<Ionicons name="log-in-outline" size={24} color="#fff" style={{ marginRight: 8 }} />
|
|
||||||
<Text style={styles.checkInButtonText}>Check In</Text>
|
|
||||||
</LinearGradient>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</Animated.View>
|
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Text style={styles.sectionTitle}>Recent History</Text>
|
<Text style={styles.sectionTitle}>Recent History</Text>
|
||||||
{history.map((item) => (
|
{history.map((item) => (
|
||||||
<LinearGradient
|
<View key={item.id} style={styles.historyItem}>
|
||||||
key={item.id}
|
<View>
|
||||||
colors={['rgba(255, 255, 255, 1)', 'rgba(249, 250, 251, 1)'] as const}
|
<Text style={styles.dateText}>
|
||||||
style={[styles.historyItem, theme.shadows.medium]}
|
{new Date(item.checkInTime).toLocaleDateString()}
|
||||||
>
|
</Text>
|
||||||
<View style={styles.historyLeft}>
|
<Text style={styles.typeText}>{item.type.toUpperCase()}</Text>
|
||||||
<View style={styles.historyIconContainer}>
|
|
||||||
<LinearGradient
|
|
||||||
colors={item.checkOutTime ? theme.gradients.success : theme.gradients.primary}
|
|
||||||
style={styles.historyIcon}
|
|
||||||
>
|
|
||||||
<Ionicons
|
|
||||||
name={item.checkOutTime ? "checkmark" : "time-outline"}
|
|
||||||
size={16}
|
|
||||||
color="#fff"
|
|
||||||
/>
|
|
||||||
</LinearGradient>
|
|
||||||
</View>
|
|
||||||
<View>
|
|
||||||
<Text style={styles.dateText}>
|
|
||||||
{new Date(item.checkInTime).toLocaleDateString()}
|
|
||||||
</Text>
|
|
||||||
<Text style={styles.typeText}>{item.type.toUpperCase()}</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.timeContainer}>
|
<View style={styles.timeContainer}>
|
||||||
<Text style={styles.historyTime}>
|
<Text style={styles.historyTime}>
|
||||||
@ -199,7 +117,7 @@ export default function AttendanceScreen() {
|
|||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</LinearGradient>
|
</View>
|
||||||
))}
|
))}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
)
|
)
|
||||||
@ -208,147 +126,105 @@ export default function AttendanceScreen() {
|
|||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: theme.colors.background,
|
backgroundColor: '#f5f5f5',
|
||||||
},
|
},
|
||||||
content: {
|
content: {
|
||||||
paddingBottom: 20,
|
padding: 20,
|
||||||
},
|
},
|
||||||
centered: {
|
centered: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
header: {
|
|
||||||
paddingTop: 60,
|
|
||||||
paddingBottom: 24,
|
|
||||||
paddingHorizontal: 24,
|
|
||||||
marginBottom: 24,
|
|
||||||
borderBottomLeftRadius: theme.borderRadius.xl,
|
|
||||||
borderBottomRightRadius: theme.borderRadius.xl,
|
|
||||||
},
|
|
||||||
title: {
|
title: {
|
||||||
fontSize: theme.typography.fontSize['3xl'],
|
fontSize: 28,
|
||||||
fontWeight: theme.typography.fontWeight.bold,
|
fontWeight: 'bold',
|
||||||
color: theme.colors.white,
|
|
||||||
marginBottom: 4,
|
marginBottom: 4,
|
||||||
|
color: '#1a1a1a',
|
||||||
},
|
},
|
||||||
subtitle: {
|
subtitle: {
|
||||||
fontSize: theme.typography.fontSize.base,
|
fontSize: 16,
|
||||||
color: 'rgba(255, 255, 255, 0.9)',
|
color: '#666',
|
||||||
|
marginBottom: 24,
|
||||||
},
|
},
|
||||||
actionContainer: {
|
actionContainer: {
|
||||||
marginBottom: 32,
|
marginBottom: 32,
|
||||||
paddingHorizontal: 20,
|
|
||||||
},
|
},
|
||||||
checkInButton: {
|
checkInButton: {
|
||||||
paddingVertical: 20,
|
backgroundColor: '#000',
|
||||||
paddingHorizontal: 24,
|
paddingVertical: 16,
|
||||||
borderRadius: theme.borderRadius.xl,
|
borderRadius: 12,
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
shadowColor: '#000',
|
||||||
},
|
shadowOffset: { width: 0, height: 2 },
|
||||||
checkInButtonText: {
|
shadowOpacity: 0.1,
|
||||||
color: theme.colors.white,
|
shadowRadius: 4,
|
||||||
fontSize: theme.typography.fontSize.xl,
|
elevation: 3,
|
||||||
fontWeight: theme.typography.fontWeight.semibold,
|
|
||||||
},
|
},
|
||||||
checkOutButton: {
|
checkOutButton: {
|
||||||
paddingVertical: 14,
|
backgroundColor: '#ff3b30',
|
||||||
paddingHorizontal: 20,
|
paddingVertical: 16,
|
||||||
borderRadius: theme.borderRadius.lg,
|
borderRadius: 12,
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
|
||||||
marginTop: 16,
|
marginTop: 16,
|
||||||
},
|
},
|
||||||
buttonText: {
|
buttonText: {
|
||||||
color: theme.colors.white,
|
color: '#fff',
|
||||||
fontSize: theme.typography.fontSize.base,
|
fontSize: 18,
|
||||||
fontWeight: theme.typography.fontWeight.semibold,
|
fontWeight: '600',
|
||||||
},
|
},
|
||||||
activeCard: {
|
activeCard: {
|
||||||
|
backgroundColor: '#fff',
|
||||||
padding: 20,
|
padding: 20,
|
||||||
borderRadius: theme.borderRadius.xl,
|
borderRadius: 16,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: 'rgba(16, 185, 129, 0.2)',
|
borderColor: '#e0e0e0',
|
||||||
},
|
|
||||||
activeCardContent: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginBottom: 16,
|
|
||||||
},
|
|
||||||
activeIconContainer: {
|
|
||||||
marginRight: 16,
|
|
||||||
},
|
|
||||||
activeIcon: {
|
|
||||||
width: 56,
|
|
||||||
height: 56,
|
|
||||||
borderRadius: 28,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
activeTextContainer: {
|
|
||||||
flex: 1,
|
|
||||||
},
|
},
|
||||||
activeText: {
|
activeText: {
|
||||||
fontSize: theme.typography.fontSize.lg,
|
fontSize: 18,
|
||||||
fontWeight: theme.typography.fontWeight.semibold,
|
fontWeight: '600',
|
||||||
color: theme.colors.gray900,
|
color: '#2e7d32',
|
||||||
marginBottom: 4,
|
marginBottom: 4,
|
||||||
},
|
},
|
||||||
timeText: {
|
timeText: {
|
||||||
fontSize: theme.typography.fontSize.sm,
|
fontSize: 14,
|
||||||
color: theme.colors.gray600,
|
color: '#666',
|
||||||
},
|
},
|
||||||
sectionTitle: {
|
sectionTitle: {
|
||||||
fontSize: theme.typography.fontSize.xl,
|
fontSize: 20,
|
||||||
fontWeight: theme.typography.fontWeight.semibold,
|
fontWeight: '600',
|
||||||
marginBottom: 16,
|
marginBottom: 16,
|
||||||
paddingHorizontal: 20,
|
color: '#1a1a1a',
|
||||||
color: theme.colors.gray900,
|
|
||||||
},
|
},
|
||||||
historyItem: {
|
historyItem: {
|
||||||
|
backgroundColor: '#fff',
|
||||||
padding: 16,
|
padding: 16,
|
||||||
borderRadius: theme.borderRadius.xl,
|
borderRadius: 12,
|
||||||
marginBottom: 12,
|
marginBottom: 12,
|
||||||
marginHorizontal: 20,
|
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
borderWidth: 1,
|
shadowColor: '#000',
|
||||||
borderColor: 'rgba(59, 130, 246, 0.1)',
|
shadowOffset: { width: 0, height: 1 },
|
||||||
},
|
shadowOpacity: 0.05,
|
||||||
historyLeft: {
|
shadowRadius: 2,
|
||||||
flexDirection: 'row',
|
elevation: 2,
|
||||||
alignItems: 'center',
|
|
||||||
gap: 12,
|
|
||||||
},
|
|
||||||
historyIconContainer: {
|
|
||||||
marginRight: 4,
|
|
||||||
},
|
|
||||||
historyIcon: {
|
|
||||||
width: 32,
|
|
||||||
height: 32,
|
|
||||||
borderRadius: 16,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
},
|
||||||
dateText: {
|
dateText: {
|
||||||
fontSize: theme.typography.fontSize.base,
|
fontSize: 16,
|
||||||
fontWeight: theme.typography.fontWeight.semibold,
|
fontWeight: '500',
|
||||||
color: theme.colors.gray900,
|
color: '#1a1a1a',
|
||||||
},
|
},
|
||||||
typeText: {
|
typeText: {
|
||||||
fontSize: theme.typography.fontSize.xs,
|
fontSize: 12,
|
||||||
color: theme.colors.gray600,
|
color: '#666',
|
||||||
marginTop: 2,
|
marginTop: 2,
|
||||||
},
|
},
|
||||||
timeContainer: {
|
timeContainer: {
|
||||||
alignItems: 'flex-end',
|
alignItems: 'flex-end',
|
||||||
},
|
},
|
||||||
historyTime: {
|
historyTime: {
|
||||||
fontSize: theme.typography.fontSize.sm,
|
fontSize: 14,
|
||||||
color: theme.colors.gray700,
|
color: '#444',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -1,86 +1,113 @@
|
|||||||
import React, { useState, useCallback, useRef } from 'react';
|
import { useState, useEffect } from "react";
|
||||||
import { View, Text, StyleSheet, ScrollView, RefreshControl, TouchableOpacity, Animated, Alert } from 'react-native';
|
import {
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
View,
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
Text,
|
||||||
import { theme } from '../../styles/theme';
|
StyleSheet,
|
||||||
import { GoalProgressCard } from '../../components/GoalProgressCard';
|
ScrollView,
|
||||||
import { GoalCreationModal } from '../../components/GoalCreationModal';
|
TouchableOpacity,
|
||||||
import { useUser, useAuth } from "@clerk/clerk-expo";
|
ActivityIndicator,
|
||||||
import { fitnessGoalsService, type FitnessGoal, type CreateGoalData } from '../../services/fitnessGoals';
|
RefreshControl,
|
||||||
import { useFocusEffect } from 'expo-router';
|
Alert,
|
||||||
|
} from "react-native";
|
||||||
|
import { useAuth } from "@clerk/clerk-expo";
|
||||||
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
|
import { fitnessGoalsService, type FitnessGoal, type CreateGoalData } from "../../services/fitnessGoals";
|
||||||
|
import { GoalProgressCard } from "../../components/GoalProgressCard";
|
||||||
|
import { GoalCreationModal } from "../../components/GoalCreationModal";
|
||||||
|
|
||||||
export default function GoalsScreen() {
|
export default function GoalsScreen() {
|
||||||
const { user } = useUser();
|
const { userId, getToken } = useAuth();
|
||||||
const { getToken } = useAuth();
|
|
||||||
const [goals, setGoals] = useState<FitnessGoal[]>([]);
|
const [goals, setGoals] = useState<FitnessGoal[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||||
const fabScale = useRef(new Animated.Value(1)).current;
|
|
||||||
|
|
||||||
const loadGoals = useCallback(async () => {
|
const fetchGoals = async () => {
|
||||||
if (!user?.id) return;
|
if (!userId) return;
|
||||||
try {
|
try {
|
||||||
const token = await getToken();
|
const token = await getToken();
|
||||||
const loadedGoals = await fitnessGoalsService.getGoals(user.id, token);
|
const data = await fitnessGoalsService.getGoals(userId, token);
|
||||||
setGoals(loadedGoals);
|
setGoals(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading goals:', error);
|
console.error("Error fetching fitness goals:", error);
|
||||||
|
Alert.alert("Error", "Failed to load goals. Please try again.");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
setRefreshing(false);
|
||||||
}
|
}
|
||||||
}, [user?.id]); // Removed getToken from dependencies
|
};
|
||||||
|
|
||||||
useFocusEffect(
|
useEffect(() => {
|
||||||
useCallback(() => {
|
fetchGoals();
|
||||||
loadGoals();
|
}, [userId]);
|
||||||
}, [loadGoals])
|
|
||||||
);
|
|
||||||
|
|
||||||
const onRefresh = async () => {
|
const onRefresh = () => {
|
||||||
setRefreshing(true);
|
setRefreshing(true);
|
||||||
await loadGoals();
|
fetchGoals();
|
||||||
setRefreshing(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCreateGoal = async (newGoal: CreateGoalData) => {
|
const handleCreateGoal = async (goalData: CreateGoalData) => {
|
||||||
const token = await getToken();
|
try {
|
||||||
await fitnessGoalsService.createGoal(newGoal, token);
|
const token = await getToken();
|
||||||
await loadGoals();
|
const newGoal = await fitnessGoalsService.createGoal(goalData, token);
|
||||||
setIsModalVisible(false);
|
setGoals(prev => [newGoal, ...prev]);
|
||||||
|
Alert.alert("Success", "Goal created successfully!");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating goal:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCompleteGoal = async (goal: FitnessGoal) => {
|
const handleCompleteGoal = async (goalId: string) => {
|
||||||
const token = await getToken();
|
try {
|
||||||
await fitnessGoalsService.completeGoal(goal.id, token);
|
const token = await getToken();
|
||||||
await loadGoals();
|
const updatedGoal = await fitnessGoalsService.completeGoal(goalId, token);
|
||||||
|
setGoals(prev => prev.map(g => g.id === goalId ? updatedGoal : g));
|
||||||
|
Alert.alert("Success", "Goal completed! 🎉");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error completing goal:", error);
|
||||||
|
Alert.alert("Error", "Failed to complete goal. Please try again.");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteGoal = async (goalId: string) => {
|
const handleDeleteGoal = async (goalId: string) => {
|
||||||
const token = await getToken();
|
try {
|
||||||
await fitnessGoalsService.deleteGoal(goalId, token);
|
const token = await getToken();
|
||||||
await loadGoals();
|
await fitnessGoalsService.deleteGoal(goalId, token);
|
||||||
|
setGoals(prev => prev.filter(g => g.id !== goalId));
|
||||||
|
Alert.alert("Success", "Goal deleted");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error deleting goal:", error);
|
||||||
|
Alert.alert("Error", "Failed to delete goal. Please try again.");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const activeGoals = goals.filter(g => g.status === 'active');
|
const activeGoals = goals.filter(g => g.status === 'active');
|
||||||
const completedGoals = goals.filter(g => g.status === 'completed');
|
const completedGoals = goals.filter(g => g.status === 'completed');
|
||||||
|
|
||||||
|
if (loading && !refreshing) {
|
||||||
|
return (
|
||||||
|
<View style={styles.center}>
|
||||||
|
<ActivityIndicator size="large" color="#2563eb" />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
contentContainerStyle={styles.scrollContent}
|
|
||||||
refreshControl={
|
refreshControl={
|
||||||
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={theme.colors.primary} />
|
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<LinearGradient
|
<View style={styles.header}>
|
||||||
colors={theme.gradients.primary}
|
|
||||||
style={styles.header}
|
|
||||||
>
|
|
||||||
<View>
|
<View>
|
||||||
<Text style={styles.headerTitle}>Fitness Goals</Text>
|
<Text style={styles.headerTitle}>My Fitness Goals</Text>
|
||||||
<Text style={styles.headerSubtitle}>
|
<Text style={styles.headerSubtitle}>
|
||||||
Track your fitness journey progress
|
Track your fitness journey progress
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</LinearGradient>
|
</View>
|
||||||
|
|
||||||
{/* Stats Summary */}
|
{/* Stats Summary */}
|
||||||
{goals.length > 0 && (
|
{goals.length > 0 && (
|
||||||
@ -97,7 +124,7 @@ export default function GoalsScreen() {
|
|||||||
<Text style={styles.statValue}>
|
<Text style={styles.statValue}>
|
||||||
{activeGoals.length > 0
|
{activeGoals.length > 0
|
||||||
? Math.round(
|
? Math.round(
|
||||||
activeGoals.reduce((sum, g) => sum + (g.progress || 0), 0) /
|
activeGoals.reduce((sum, g) => sum + g.progress, 0) /
|
||||||
activeGoals.length
|
activeGoals.length
|
||||||
)
|
)
|
||||||
: 0}%
|
: 0}%
|
||||||
@ -125,7 +152,7 @@ export default function GoalsScreen() {
|
|||||||
<GoalProgressCard
|
<GoalProgressCard
|
||||||
key={goal.id}
|
key={goal.id}
|
||||||
goal={goal}
|
goal={goal}
|
||||||
onComplete={() => handleCompleteGoal(goal)}
|
onComplete={() => handleCompleteGoal(goal.id)}
|
||||||
onDelete={() => handleDeleteGoal(goal.id)}
|
onDelete={() => handleDeleteGoal(goal.id)}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
@ -152,42 +179,17 @@ export default function GoalsScreen() {
|
|||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
{/* Floating Action Button */}
|
{/* Floating Action Button */}
|
||||||
<Animated.View style={[styles.fabContainer, { transform: [{ scale: fabScale }] }]}>
|
<TouchableOpacity
|
||||||
<TouchableOpacity
|
style={styles.fab}
|
||||||
onPress={() => setIsModalVisible(true)}
|
onPress={() => setShowCreateModal(true)}
|
||||||
onPressIn={() => {
|
>
|
||||||
Animated.spring(fabScale, {
|
<Ionicons name="add" size={28} color="#fff" />
|
||||||
toValue: 0.9,
|
</TouchableOpacity>
|
||||||
friction: 8,
|
|
||||||
tension: 100,
|
|
||||||
useNativeDriver: true,
|
|
||||||
}).start();
|
|
||||||
}}
|
|
||||||
onPressOut={() => {
|
|
||||||
Animated.spring(fabScale, {
|
|
||||||
toValue: 1,
|
|
||||||
friction: 8,
|
|
||||||
tension: 100,
|
|
||||||
useNativeDriver: true,
|
|
||||||
}).start();
|
|
||||||
}}
|
|
||||||
activeOpacity={0.9}
|
|
||||||
>
|
|
||||||
<LinearGradient
|
|
||||||
colors={theme.gradients.primary}
|
|
||||||
start={{ x: 0, y: 0 }}
|
|
||||||
end={{ x: 1, y: 1 }}
|
|
||||||
style={styles.fab}
|
|
||||||
>
|
|
||||||
<Ionicons name="add" size={28} color="#fff" />
|
|
||||||
</LinearGradient>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</Animated.View>
|
|
||||||
|
|
||||||
{/* Create Goal Modal */}
|
{/* Create Goal Modal */}
|
||||||
<GoalCreationModal
|
<GoalCreationModal
|
||||||
visible={isModalVisible}
|
visible={showCreateModal}
|
||||||
onClose={() => setIsModalVisible(false)}
|
onClose={() => setShowCreateModal(false)}
|
||||||
onSubmit={handleCreateGoal}
|
onSubmit={handleCreateGoal}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
@ -197,27 +199,26 @@ export default function GoalsScreen() {
|
|||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: theme.colors.background,
|
backgroundColor: "#f3f4f6",
|
||||||
},
|
},
|
||||||
scrollContent: {
|
center: {
|
||||||
paddingBottom: 20,
|
flex: 1,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
},
|
},
|
||||||
header: {
|
header: {
|
||||||
padding: 24,
|
padding: 20,
|
||||||
paddingTop: 60,
|
backgroundColor: "#fff",
|
||||||
paddingBottom: 24,
|
|
||||||
marginBottom: 10,
|
marginBottom: 10,
|
||||||
borderBottomLeftRadius: theme.borderRadius.xl,
|
|
||||||
borderBottomRightRadius: theme.borderRadius.xl,
|
|
||||||
},
|
},
|
||||||
headerTitle: {
|
headerTitle: {
|
||||||
fontSize: theme.typography.fontSize['3xl'],
|
fontSize: 28,
|
||||||
fontWeight: theme.typography.fontWeight.bold,
|
fontWeight: "bold",
|
||||||
color: theme.colors.white,
|
color: "#111827",
|
||||||
},
|
},
|
||||||
headerSubtitle: {
|
headerSubtitle: {
|
||||||
fontSize: theme.typography.fontSize.base,
|
fontSize: 16,
|
||||||
color: "rgba(255, 255, 255, 0.9)",
|
color: "#6b7280",
|
||||||
marginTop: 4,
|
marginTop: 4,
|
||||||
},
|
},
|
||||||
statsContainer: {
|
statsContainer: {
|
||||||
@ -227,18 +228,20 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
statCard: {
|
statCard: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: theme.colors.white,
|
backgroundColor: "#fff",
|
||||||
padding: 16,
|
padding: 16,
|
||||||
borderRadius: theme.borderRadius.xl,
|
borderRadius: 12,
|
||||||
alignItems: 'center',
|
alignItems: "center",
|
||||||
...theme.shadows.medium,
|
shadowColor: "#000",
|
||||||
borderWidth: 1,
|
shadowOffset: { width: 0, height: 1 },
|
||||||
borderColor: "rgba(59, 130, 246, 0.1)",
|
shadowOpacity: 0.05,
|
||||||
|
shadowRadius: 2,
|
||||||
|
elevation: 2,
|
||||||
},
|
},
|
||||||
statValue: {
|
statValue: {
|
||||||
fontSize: theme.typography.fontSize['2xl'],
|
fontSize: 24,
|
||||||
fontWeight: theme.typography.fontWeight.bold,
|
fontWeight: "bold",
|
||||||
color: theme.colors.primary,
|
color: "#2563eb",
|
||||||
marginBottom: 4,
|
marginBottom: 4,
|
||||||
},
|
},
|
||||||
statLabel: {
|
statLabel: {
|
||||||
@ -274,17 +277,20 @@ const styles = StyleSheet.create({
|
|||||||
footer: {
|
footer: {
|
||||||
height: 100,
|
height: 100,
|
||||||
},
|
},
|
||||||
fabContainer: {
|
fab: {
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
right: 20,
|
right: 20,
|
||||||
bottom: 110, // Adjusted for tab bar height
|
bottom: 20,
|
||||||
},
|
width: 56,
|
||||||
fab: {
|
height: 56,
|
||||||
width: 64,
|
borderRadius: 28,
|
||||||
height: 64,
|
backgroundColor: "#2563eb",
|
||||||
borderRadius: 32,
|
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
...theme.shadows.glow,
|
shadowColor: "#000",
|
||||||
|
shadowOffset: { width: 0, height: 4 },
|
||||||
|
shadowOpacity: 0.3,
|
||||||
|
shadowRadius: 8,
|
||||||
|
elevation: 8,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,229 +1,320 @@
|
|||||||
import { View, Text, StyleSheet, ScrollView, RefreshControl, Image } from "react-native";
|
import React from "react";
|
||||||
|
import {
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
StyleSheet,
|
||||||
|
ScrollView,
|
||||||
|
TouchableOpacity,
|
||||||
|
} from "react-native";
|
||||||
import { useUser } from "@clerk/clerk-expo";
|
import { useUser } from "@clerk/clerk-expo";
|
||||||
import { LinearGradient } from "expo-linear-gradient";
|
|
||||||
import { useState, useCallback } from "react";
|
|
||||||
import { theme } from "../../styles/theme";
|
|
||||||
import { ActivityWidget } from "../../components/ActivityWidget";
|
|
||||||
import { QuickActionGrid } from "../../components/QuickActionGrid";
|
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
|
import { useRouter } from "expo-router";
|
||||||
|
|
||||||
export default function HomeScreen() {
|
export default function HomeScreen() {
|
||||||
const { user } = useUser();
|
const { user, isLoaded } = useUser();
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
const router = useRouter();
|
||||||
|
|
||||||
const onRefresh = useCallback(() => {
|
if (!isLoaded || !user) {
|
||||||
setRefreshing(true);
|
return (
|
||||||
setTimeout(() => {
|
<View style={styles.container}>
|
||||||
setRefreshing(false);
|
<Text>Loading...</Text>
|
||||||
}, 2000);
|
</View>
|
||||||
}, []);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const getGreeting = () => {
|
const firstName = user.firstName || "User";
|
||||||
const hour = new Date().getHours();
|
const greeting = getGreeting();
|
||||||
if (hour < 12) return "Good Morning";
|
|
||||||
if (hour < 18) return "Good Afternoon";
|
|
||||||
return "Good Evening";
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<ScrollView style={styles.container}>
|
||||||
<ScrollView
|
<View style={styles.content}>
|
||||||
contentContainerStyle={styles.scrollContent}
|
{/* Welcome Header */}
|
||||||
refreshControl={
|
|
||||||
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={theme.colors.primary} />
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{/* Header Section */}
|
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<View>
|
<Text style={styles.greeting}>{greeting}!</Text>
|
||||||
<Text style={styles.greeting}>{getGreeting()},</Text>
|
<Text style={styles.name}>{firstName}</Text>
|
||||||
<Text style={styles.name}>{user?.firstName || "Athlete"}</Text>
|
|
||||||
</View>
|
|
||||||
<View style={styles.avatarContainer}>
|
|
||||||
{user?.imageUrl ? (
|
|
||||||
<Image source={{ uri: user.imageUrl }} style={styles.avatar} />
|
|
||||||
) : (
|
|
||||||
<View style={styles.placeholderAvatar}>
|
|
||||||
<Ionicons name="person" size={24} color="#fff" />
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Activity Widget */}
|
{/* Quick Stats */}
|
||||||
<ActivityWidget
|
<View style={styles.statsContainer}>
|
||||||
steps={8432}
|
<View style={styles.statCard}>
|
||||||
calories={640}
|
<Ionicons name="calendar-outline" size={32} color="#2563eb" />
|
||||||
duration={45}
|
<Text style={styles.statValue}>0</Text>
|
||||||
/>
|
<Text style={styles.statLabel}>This Month</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.statCard}>
|
||||||
|
<Ionicons name="flame-outline" size={32} color="#ef4444" />
|
||||||
|
<Text style={styles.statValue}>0</Text>
|
||||||
|
<Text style={styles.statLabel}>Day Streak</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.statCard}>
|
||||||
|
<Ionicons name="trophy-outline" size={32} color="#f59e0b" />
|
||||||
|
<Text style={styles.statValue}>0</Text>
|
||||||
|
<Text style={styles.statLabel}>Total Visits</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
{/* Quick Actions */}
|
{/* Quick Actions */}
|
||||||
<QuickActionGrid />
|
|
||||||
|
|
||||||
{/* Recent Activity Section */}
|
|
||||||
<View style={styles.section}>
|
<View style={styles.section}>
|
||||||
<View style={styles.sectionHeader}>
|
<Text style={styles.sectionTitle}>Quick Actions</Text>
|
||||||
<Text style={styles.sectionTitle}>Recent Activity</Text>
|
|
||||||
<Text style={styles.seeAll}>See All</Text>
|
<TouchableOpacity
|
||||||
|
style={styles.actionButton}
|
||||||
|
onPress={() => router.push("/fitness-profile")}
|
||||||
|
>
|
||||||
|
<View style={styles.actionIcon}>
|
||||||
|
<Ionicons name="fitness-outline" size={24} color="#ec4899" />
|
||||||
|
</View>
|
||||||
|
<View style={styles.actionContent}>
|
||||||
|
<Text style={styles.actionTitle}>Fitness Profile</Text>
|
||||||
|
<Text style={styles.actionSubtitle}>
|
||||||
|
Manage your fitness information
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Ionicons name="chevron-forward" size={20} color="#9ca3af" />
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<View style={styles.actionButton}>
|
||||||
|
<View style={styles.actionIcon}>
|
||||||
|
<Ionicons name="log-in-outline" size={24} color="#2563eb" />
|
||||||
|
</View>
|
||||||
|
<View style={styles.actionContent}>
|
||||||
|
<Text style={styles.actionTitle}>Check In</Text>
|
||||||
|
<Text style={styles.actionSubtitle}>
|
||||||
|
Start your workout session
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Ionicons name="chevron-forward" size={20} color="#9ca3af" />
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.activityCard}>
|
|
||||||
<LinearGradient
|
|
||||||
colors={['rgba(255, 255, 255, 0.8)', 'rgba(255, 255, 255, 0.5)']}
|
|
||||||
style={[styles.recentItem, theme.shadows.subtle]}
|
|
||||||
>
|
|
||||||
<View style={styles.recentIconContainer}>
|
|
||||||
<LinearGradient
|
|
||||||
colors={theme.gradients.primary}
|
|
||||||
style={styles.recentIcon}
|
|
||||||
>
|
|
||||||
<Ionicons name="barbell" size={20} color="#fff" />
|
|
||||||
</LinearGradient>
|
|
||||||
</View>
|
|
||||||
<View style={styles.recentInfo}>
|
|
||||||
<Text style={styles.recentTitle}>Upper Body Power</Text>
|
|
||||||
<Text style={styles.recentSubtitle}>Today, 10:00 AM</Text>
|
|
||||||
</View>
|
|
||||||
<Text style={styles.recentValue}>45m</Text>
|
|
||||||
</LinearGradient>
|
|
||||||
|
|
||||||
<LinearGradient
|
<View style={styles.actionButton}>
|
||||||
colors={['rgba(255, 255, 255, 0.8)', 'rgba(255, 255, 255, 0.5)']}
|
<View style={styles.actionIcon}>
|
||||||
style={[styles.recentItem, theme.shadows.subtle]}
|
<Ionicons name="calendar-outline" size={24} color="#10b981" />
|
||||||
>
|
</View>
|
||||||
<View style={styles.recentIconContainer}>
|
<View style={styles.actionContent}>
|
||||||
<LinearGradient
|
<Text style={styles.actionTitle}>View Schedule</Text>
|
||||||
colors={theme.gradients.success}
|
<Text style={styles.actionSubtitle}>
|
||||||
style={styles.recentIcon}
|
Check your upcoming classes
|
||||||
>
|
</Text>
|
||||||
<Ionicons name="bicycle" size={20} color="#fff" />
|
</View>
|
||||||
</LinearGradient>
|
<Ionicons name="chevron-forward" size={20} color="#9ca3af" />
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.recentInfo}>
|
|
||||||
<Text style={styles.recentTitle}>Morning Cardio</Text>
|
<View style={styles.actionButton}>
|
||||||
<Text style={styles.recentSubtitle}>Yesterday, 7:30 AM</Text>
|
<View style={styles.actionIcon}>
|
||||||
</View>
|
<Ionicons name="card-outline" size={24} color="#8b5cf6" />
|
||||||
<Text style={styles.recentValue}>30m</Text>
|
</View>
|
||||||
</LinearGradient>
|
<View style={styles.actionContent}>
|
||||||
|
<Text style={styles.actionTitle}>Payments</Text>
|
||||||
|
<Text style={styles.actionSubtitle}>View payment history</Text>
|
||||||
|
</View>
|
||||||
|
<Ionicons name="chevron-forward" size={20} color="#9ca3af" />
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Bottom Spacer for Tab Bar */}
|
{/* Membership Info */}
|
||||||
<View style={{ height: 100 }} />
|
<View style={styles.section}>
|
||||||
</ScrollView>
|
<Text style={styles.sectionTitle}>Membership</Text>
|
||||||
</View>
|
<View style={styles.membershipCard}>
|
||||||
|
<View style={styles.membershipHeader}>
|
||||||
|
<Text style={styles.membershipType}>Basic Plan</Text>
|
||||||
|
<View style={styles.statusBadge}>
|
||||||
|
<Text style={styles.statusText}>Active</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<Text style={styles.membershipEmail}>
|
||||||
|
{user.primaryEmailAddress?.emailAddress}
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.membershipDate}>
|
||||||
|
Member since {new Date(user.createdAt!).toLocaleDateString()}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Recent Activity */}
|
||||||
|
<View style={styles.section}>
|
||||||
|
<Text style={styles.sectionTitle}>Recent Activity</Text>
|
||||||
|
<View style={styles.emptyState}>
|
||||||
|
<Ionicons name="barbell-outline" size={48} color="#d1d5db" />
|
||||||
|
<Text style={styles.emptyStateText}>No recent activity</Text>
|
||||||
|
<Text style={styles.emptyStateSubtext}>
|
||||||
|
Check in to start tracking your workouts
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getGreeting(): string {
|
||||||
|
const hour = new Date().getHours();
|
||||||
|
if (hour < 12) return "Good morning";
|
||||||
|
if (hour < 18) return "Good afternoon";
|
||||||
|
return "Good evening";
|
||||||
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: theme.colors.background,
|
backgroundColor: "#f5f5f5",
|
||||||
},
|
},
|
||||||
scrollContent: {
|
content: {
|
||||||
paddingTop: 60,
|
padding: 20,
|
||||||
},
|
},
|
||||||
header: {
|
header: {
|
||||||
flexDirection: "row",
|
marginBottom: 24,
|
||||||
justifyContent: "space-between",
|
|
||||||
alignItems: "center",
|
|
||||||
paddingHorizontal: 24,
|
|
||||||
marginBottom: 32,
|
|
||||||
},
|
},
|
||||||
greeting: {
|
greeting: {
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: theme.colors.gray600,
|
color: "#6b7280",
|
||||||
fontWeight: "500",
|
|
||||||
marginBottom: 4,
|
marginBottom: 4,
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
fontSize: 32,
|
fontSize: 32,
|
||||||
fontWeight: "800",
|
fontWeight: "bold",
|
||||||
color: theme.colors.gray900,
|
color: "#1a1a1a",
|
||||||
letterSpacing: -0.5,
|
|
||||||
},
|
},
|
||||||
avatarContainer: {
|
statsContainer: {
|
||||||
shadowColor: "#000",
|
|
||||||
shadowOffset: { width: 0, height: 4 },
|
|
||||||
shadowOpacity: 0.1,
|
|
||||||
shadowRadius: 12,
|
|
||||||
elevation: 5,
|
|
||||||
},
|
|
||||||
avatar: {
|
|
||||||
width: 56,
|
|
||||||
height: 56,
|
|
||||||
borderRadius: 20,
|
|
||||||
borderWidth: 2,
|
|
||||||
borderColor: "#fff",
|
|
||||||
},
|
|
||||||
placeholderAvatar: {
|
|
||||||
width: 56,
|
|
||||||
height: 56,
|
|
||||||
borderRadius: 20,
|
|
||||||
backgroundColor: theme.colors.primary,
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
borderWidth: 2,
|
|
||||||
borderColor: "#fff",
|
|
||||||
},
|
|
||||||
section: {
|
|
||||||
paddingHorizontal: 20,
|
|
||||||
marginBottom: 24,
|
|
||||||
},
|
|
||||||
sectionHeader: {
|
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
|
marginBottom: 24,
|
||||||
|
},
|
||||||
|
statCard: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: "white",
|
||||||
|
borderRadius: 12,
|
||||||
|
padding: 16,
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
marginBottom: 16,
|
marginHorizontal: 4,
|
||||||
|
shadowColor: "#000",
|
||||||
|
shadowOffset: { width: 0, height: 1 },
|
||||||
|
shadowOpacity: 0.05,
|
||||||
|
shadowRadius: 2,
|
||||||
|
elevation: 2,
|
||||||
|
},
|
||||||
|
statValue: {
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: "bold",
|
||||||
|
color: "#1a1a1a",
|
||||||
|
marginTop: 8,
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
statLabel: {
|
||||||
|
fontSize: 12,
|
||||||
|
color: "#6b7280",
|
||||||
|
textAlign: "center",
|
||||||
|
},
|
||||||
|
section: {
|
||||||
|
marginBottom: 24,
|
||||||
},
|
},
|
||||||
sectionTitle: {
|
sectionTitle: {
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: "700",
|
|
||||||
color: theme.colors.gray900,
|
|
||||||
},
|
|
||||||
seeAll: {
|
|
||||||
fontSize: 14,
|
|
||||||
color: theme.colors.primary,
|
|
||||||
fontWeight: "600",
|
fontWeight: "600",
|
||||||
|
color: "#1a1a1a",
|
||||||
|
marginBottom: 12,
|
||||||
},
|
},
|
||||||
activityCard: {
|
actionButton: {
|
||||||
gap: 12,
|
|
||||||
},
|
|
||||||
recentItem: {
|
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
backgroundColor: "white",
|
||||||
|
borderRadius: 12,
|
||||||
padding: 16,
|
padding: 16,
|
||||||
borderRadius: 20,
|
marginBottom: 8,
|
||||||
backgroundColor: "#fff",
|
shadowColor: "#000",
|
||||||
borderWidth: 1,
|
shadowOffset: { width: 0, height: 1 },
|
||||||
borderColor: "rgba(255, 255, 255, 0.6)",
|
shadowOpacity: 0.05,
|
||||||
|
shadowRadius: 2,
|
||||||
|
elevation: 2,
|
||||||
},
|
},
|
||||||
recentIconContainer: {
|
actionIcon: {
|
||||||
marginRight: 16,
|
|
||||||
},
|
|
||||||
recentIcon: {
|
|
||||||
width: 48,
|
width: 48,
|
||||||
height: 48,
|
height: 48,
|
||||||
borderRadius: 16,
|
borderRadius: 24,
|
||||||
|
backgroundColor: "#f0f9ff",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
marginRight: 12,
|
||||||
},
|
},
|
||||||
recentInfo: {
|
actionContent: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
recentTitle: {
|
actionTitle: {
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: "600",
|
fontWeight: "600",
|
||||||
color: theme.colors.gray900,
|
color: "#1a1a1a",
|
||||||
|
marginBottom: 2,
|
||||||
|
},
|
||||||
|
actionSubtitle: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: "#6b7280",
|
||||||
|
},
|
||||||
|
membershipCard: {
|
||||||
|
backgroundColor: "white",
|
||||||
|
borderRadius: 12,
|
||||||
|
padding: 16,
|
||||||
|
shadowColor: "#000",
|
||||||
|
shadowOffset: { width: 0, height: 1 },
|
||||||
|
shadowOpacity: 0.05,
|
||||||
|
shadowRadius: 2,
|
||||||
|
elevation: 2,
|
||||||
|
},
|
||||||
|
membershipHeader: {
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
membershipType: {
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: "600",
|
||||||
|
color: "#1a1a1a",
|
||||||
|
},
|
||||||
|
statusBadge: {
|
||||||
|
backgroundColor: "#dcfce7",
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
paddingVertical: 4,
|
||||||
|
borderRadius: 12,
|
||||||
|
},
|
||||||
|
statusText: {
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: "600",
|
||||||
|
color: "#16a34a",
|
||||||
|
},
|
||||||
|
membershipEmail: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: "#6b7280",
|
||||||
marginBottom: 4,
|
marginBottom: 4,
|
||||||
},
|
},
|
||||||
recentSubtitle: {
|
membershipDate: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: theme.colors.gray500,
|
color: "#9ca3af",
|
||||||
},
|
},
|
||||||
recentValue: {
|
emptyState: {
|
||||||
fontSize: 14,
|
backgroundColor: "white",
|
||||||
|
borderRadius: 12,
|
||||||
|
padding: 32,
|
||||||
|
alignItems: "center",
|
||||||
|
shadowColor: "#000",
|
||||||
|
shadowOffset: { width: 0, height: 1 },
|
||||||
|
shadowOpacity: 0.05,
|
||||||
|
shadowRadius: 2,
|
||||||
|
elevation: 2,
|
||||||
|
},
|
||||||
|
emptyStateText: {
|
||||||
|
fontSize: 16,
|
||||||
fontWeight: "600",
|
fontWeight: "600",
|
||||||
color: theme.colors.gray900,
|
color: "#6b7280",
|
||||||
|
marginTop: 12,
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
emptyStateSubtext: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: "#9ca3af",
|
||||||
|
textAlign: "center",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,267 +1,307 @@
|
|||||||
import { View, Text, StyleSheet, TouchableOpacity, Image, Alert } from "react-native";
|
import React from "react";
|
||||||
import { useUser, useClerk } from "@clerk/clerk-expo";
|
import {
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
StyleSheet,
|
||||||
|
TouchableOpacity,
|
||||||
|
Alert,
|
||||||
|
ScrollView,
|
||||||
|
} from "react-native";
|
||||||
|
import { useUser, useAuth } from "@clerk/clerk-expo";
|
||||||
|
import { useRouter } from "expo-router";
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import { LinearGradient } from "expo-linear-gradient";
|
|
||||||
import { theme } from "../../styles/theme";
|
|
||||||
import { AnimatedButton } from "../../components/AnimatedButton";
|
|
||||||
import { GradientBackground } from "../../components/GradientBackground";
|
|
||||||
|
|
||||||
export default function ProfileScreen() {
|
export default function ProfileScreen() {
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
const { signOut } = useClerk();
|
const { signOut } = useAuth();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const handleSignOut = async () => {
|
const handleLogout = async () => {
|
||||||
try {
|
Alert.alert("Sign Out", "Are you sure you want to sign out?", [
|
||||||
await signOut();
|
{ text: "Cancel", style: "cancel" },
|
||||||
} catch (err) {
|
{
|
||||||
console.error("Error signing out:", err);
|
text: "Sign Out",
|
||||||
}
|
style: "destructive",
|
||||||
|
onPress: async () => {
|
||||||
|
try {
|
||||||
|
await signOut();
|
||||||
|
router.replace("/(auth)/sign-in");
|
||||||
|
} catch (error) {
|
||||||
|
Alert.alert("Error", "Failed to sign out");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmSignOut = () => {
|
if (!user) {
|
||||||
Alert.alert(
|
return (
|
||||||
"Sign Out",
|
<View style={styles.container}>
|
||||||
"Are you sure you want to sign out?",
|
<Text>Loading...</Text>
|
||||||
[
|
</View>
|
||||||
{ text: "Cancel", style: "cancel" },
|
|
||||||
{ text: "Sign Out", style: "destructive", onPress: handleSignOut },
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<ScrollView style={styles.container}>
|
||||||
<GradientBackground variant="primary" style={styles.header}>
|
<View style={styles.content}>
|
||||||
|
{/* Profile Header */}
|
||||||
<View style={styles.profileCard}>
|
<View style={styles.profileCard}>
|
||||||
<View style={styles.avatarContainer}>
|
<View style={styles.avatarContainer}>
|
||||||
{user?.imageUrl ? (
|
{user.imageUrl ? (
|
||||||
<Image source={{ uri: user.imageUrl }} style={styles.avatar} />
|
<View style={styles.avatar}>
|
||||||
|
<Text style={styles.avatarText}>
|
||||||
|
{user.firstName?.charAt(0)}
|
||||||
|
{user.lastName?.charAt(0)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<View style={styles.placeholderAvatar}>
|
<View style={styles.avatar}>
|
||||||
<Ionicons name="person" size={40} color="#fff" />
|
<Ionicons name="person" size={40} color="#fff" />
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
<View style={styles.editBadge}>
|
</View>
|
||||||
<Ionicons name="pencil" size={12} color={theme.colors.primary} />
|
|
||||||
|
<Text style={styles.name}>
|
||||||
|
{user.firstName} {user.lastName}
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.email}>
|
||||||
|
{user.primaryEmailAddress?.emailAddress}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{user.primaryPhoneNumber && (
|
||||||
|
<Text style={styles.phone}>
|
||||||
|
{user.primaryPhoneNumber.phoneNumber}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Account Information */}
|
||||||
|
<View style={styles.section}>
|
||||||
|
<Text style={styles.sectionTitle}>Account Information</Text>
|
||||||
|
|
||||||
|
<View style={styles.infoRow}>
|
||||||
|
<View style={styles.infoLabel}>
|
||||||
|
<Ionicons name="mail-outline" size={20} color="#666" />
|
||||||
|
<Text style={styles.infoLabelText}>Email</Text>
|
||||||
</View>
|
</View>
|
||||||
|
<Text style={styles.infoValue}>
|
||||||
|
{user.primaryEmailAddress?.emailAddress}
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.name}>{user?.fullName || "User"}</Text>
|
|
||||||
<Text style={styles.email}>{user?.primaryEmailAddress?.emailAddress}</Text>
|
{user.primaryPhoneNumber && (
|
||||||
<View style={styles.memberBadge}>
|
<View style={styles.infoRow}>
|
||||||
<Text style={styles.memberText}>Premium Member</Text>
|
<View style={styles.infoLabel}>
|
||||||
|
<Ionicons name="call-outline" size={20} color="#666" />
|
||||||
|
<Text style={styles.infoLabelText}>Phone</Text>
|
||||||
|
</View>
|
||||||
|
<Text style={styles.infoValue}>
|
||||||
|
{user.primaryPhoneNumber.phoneNumber}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<View style={styles.infoRow}>
|
||||||
|
<View style={styles.infoLabel}>
|
||||||
|
<Ionicons name="calendar-outline" size={20} color="#666" />
|
||||||
|
<Text style={styles.infoLabelText}>Member Since</Text>
|
||||||
|
</View>
|
||||||
|
<Text style={styles.infoValue}>
|
||||||
|
{new Date(user.createdAt!).toLocaleDateString()}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.infoRow}>
|
||||||
|
<View style={styles.infoLabel}>
|
||||||
|
<Ionicons
|
||||||
|
name="shield-checkmark-outline"
|
||||||
|
size={20}
|
||||||
|
color="#666"
|
||||||
|
/>
|
||||||
|
<Text style={styles.infoLabelText}>Email Verified</Text>
|
||||||
|
</View>
|
||||||
|
<Text style={styles.infoValue}>
|
||||||
|
{user.primaryEmailAddress?.verification?.status === "verified"
|
||||||
|
? "Yes"
|
||||||
|
: "No"}
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</GradientBackground>
|
|
||||||
|
|
||||||
<View style={styles.content}>
|
{/* Quick Actions */}
|
||||||
<View style={styles.section}>
|
<View style={styles.section}>
|
||||||
<Text style={styles.sectionTitle}>Account</Text>
|
<Text style={styles.sectionTitle}>Quick Actions</Text>
|
||||||
<View style={[styles.infoCard, theme.shadows.subtle]}>
|
|
||||||
<TouchableOpacity style={styles.infoRow}>
|
<TouchableOpacity style={styles.actionButton}>
|
||||||
<LinearGradient
|
<Ionicons name="person-outline" size={24} color="#2563eb" />
|
||||||
colors={['rgba(59, 130, 246, 0.1)', 'rgba(59, 130, 246, 0.05)']}
|
<Text style={styles.actionButtonText}>Edit Profile</Text>
|
||||||
style={styles.iconContainer}
|
<Ionicons name="chevron-forward" size={20} color="#999" />
|
||||||
>
|
</TouchableOpacity>
|
||||||
<Ionicons name="person-outline" size={20} color={theme.colors.primary} />
|
|
||||||
</LinearGradient>
|
<TouchableOpacity style={styles.actionButton}>
|
||||||
<Text style={styles.infoLabel}>Personal Details</Text>
|
<Ionicons name="notifications-outline" size={24} color="#2563eb" />
|
||||||
<Ionicons name="chevron-forward" size={20} color={theme.colors.gray400} />
|
<Text style={styles.actionButtonText}>Notifications</Text>
|
||||||
</TouchableOpacity>
|
<Ionicons name="chevron-forward" size={20} color="#999" />
|
||||||
<View style={styles.divider} />
|
</TouchableOpacity>
|
||||||
<TouchableOpacity style={styles.infoRow}>
|
|
||||||
<LinearGradient
|
<TouchableOpacity style={styles.actionButton}>
|
||||||
colors={['rgba(16, 185, 129, 0.1)', 'rgba(16, 185, 129, 0.05)']}
|
<Ionicons name="card-outline" size={24} color="#2563eb" />
|
||||||
style={styles.iconContainer}
|
<Text style={styles.actionButtonText}>Payment History</Text>
|
||||||
>
|
<Ionicons name="chevron-forward" size={20} color="#999" />
|
||||||
<Ionicons name="fitness-outline" size={20} color={theme.colors.success} />
|
</TouchableOpacity>
|
||||||
</LinearGradient>
|
|
||||||
<Text style={styles.infoLabel}>Fitness Profile</Text>
|
<TouchableOpacity style={styles.actionButton}>
|
||||||
<Ionicons name="chevron-forward" size={20} color={theme.colors.gray400} />
|
<Ionicons name="settings-outline" size={24} color="#2563eb" />
|
||||||
</TouchableOpacity>
|
<Text style={styles.actionButtonText}>Settings</Text>
|
||||||
<View style={styles.divider} />
|
<Ionicons name="chevron-forward" size={20} color="#999" />
|
||||||
<TouchableOpacity style={styles.infoRow}>
|
</TouchableOpacity>
|
||||||
<LinearGradient
|
|
||||||
colors={['rgba(245, 158, 11, 0.1)', 'rgba(245, 158, 11, 0.05)']}
|
|
||||||
style={styles.iconContainer}
|
|
||||||
>
|
|
||||||
<Ionicons name="notifications-outline" size={20} color={theme.colors.warning} />
|
|
||||||
</LinearGradient>
|
|
||||||
<Text style={styles.infoLabel}>Notifications</Text>
|
|
||||||
<Ionicons name="chevron-forward" size={20} color={theme.colors.gray400} />
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.section}>
|
{/* Sign Out Button */}
|
||||||
<Text style={styles.sectionTitle}>Support</Text>
|
<TouchableOpacity style={styles.logoutButton} onPress={handleLogout}>
|
||||||
<View style={[styles.infoCard, theme.shadows.subtle]}>
|
<Ionicons name="log-out-outline" size={20} color="#fff" />
|
||||||
<TouchableOpacity style={styles.infoRow}>
|
<Text style={styles.logoutText}>Sign Out</Text>
|
||||||
<LinearGradient
|
</TouchableOpacity>
|
||||||
colors={['rgba(139, 92, 246, 0.1)', 'rgba(139, 92, 246, 0.05)']}
|
|
||||||
style={styles.iconContainer}
|
|
||||||
>
|
|
||||||
<Ionicons name="help-circle-outline" size={20} color={theme.colors.secondary} />
|
|
||||||
</LinearGradient>
|
|
||||||
<Text style={styles.infoLabel}>Help Center</Text>
|
|
||||||
<Ionicons name="chevron-forward" size={20} color={theme.colors.gray400} />
|
|
||||||
</TouchableOpacity>
|
|
||||||
<View style={styles.divider} />
|
|
||||||
<TouchableOpacity style={styles.infoRow}>
|
|
||||||
<LinearGradient
|
|
||||||
colors={['rgba(107, 114, 128, 0.1)', 'rgba(107, 114, 128, 0.05)']}
|
|
||||||
style={styles.iconContainer}
|
|
||||||
>
|
|
||||||
<Ionicons name="shield-checkmark-outline" size={20} color={theme.colors.gray600} />
|
|
||||||
</LinearGradient>
|
|
||||||
<Text style={styles.infoLabel}>Privacy & Security</Text>
|
|
||||||
<Ionicons name="chevron-forward" size={20} color={theme.colors.gray400} />
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<AnimatedButton
|
|
||||||
title="Sign Out"
|
|
||||||
onPress={confirmSignOut}
|
|
||||||
variant="danger"
|
|
||||||
style={styles.signOutButton}
|
|
||||||
icon={<Ionicons name="log-out-outline" size={20} color="#fff" />}
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
{/* App Version */}
|
||||||
<Text style={styles.version}>Version 1.0.0</Text>
|
<Text style={styles.version}>Version 1.0.0</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</ScrollView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: theme.colors.background,
|
backgroundColor: "#f5f5f5",
|
||||||
},
|
|
||||||
header: {
|
|
||||||
paddingTop: 60,
|
|
||||||
paddingBottom: 30,
|
|
||||||
borderBottomLeftRadius: theme.borderRadius.xl,
|
|
||||||
borderBottomRightRadius: theme.borderRadius.xl,
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
profileCard: {
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
avatarContainer: {
|
|
||||||
position: 'relative',
|
|
||||||
marginBottom: 16,
|
|
||||||
},
|
|
||||||
avatar: {
|
|
||||||
width: 100,
|
|
||||||
height: 100,
|
|
||||||
borderRadius: 50,
|
|
||||||
borderWidth: 4,
|
|
||||||
borderColor: 'rgba(255, 255, 255, 0.3)',
|
|
||||||
},
|
|
||||||
placeholderAvatar: {
|
|
||||||
width: 100,
|
|
||||||
height: 100,
|
|
||||||
borderRadius: 50,
|
|
||||||
backgroundColor: 'rgba(255, 255, 255, 0.2)',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
borderWidth: 4,
|
|
||||||
borderColor: 'rgba(255, 255, 255, 0.3)',
|
|
||||||
},
|
|
||||||
editBadge: {
|
|
||||||
position: 'absolute',
|
|
||||||
bottom: 0,
|
|
||||||
right: 0,
|
|
||||||
backgroundColor: theme.colors.white,
|
|
||||||
width: 32,
|
|
||||||
height: 32,
|
|
||||||
borderRadius: 16,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
shadowColor: "#000",
|
|
||||||
shadowOffset: { width: 0, height: 2 },
|
|
||||||
shadowOpacity: 0.1,
|
|
||||||
shadowRadius: 4,
|
|
||||||
elevation: 3,
|
|
||||||
},
|
|
||||||
name: {
|
|
||||||
fontSize: theme.typography.fontSize['2xl'],
|
|
||||||
fontWeight: theme.typography.fontWeight.bold,
|
|
||||||
color: theme.colors.white,
|
|
||||||
marginBottom: 4,
|
|
||||||
},
|
|
||||||
email: {
|
|
||||||
fontSize: theme.typography.fontSize.sm,
|
|
||||||
color: 'rgba(255, 255, 255, 0.8)',
|
|
||||||
marginBottom: 12,
|
|
||||||
},
|
|
||||||
memberBadge: {
|
|
||||||
backgroundColor: 'rgba(255, 255, 255, 0.2)',
|
|
||||||
paddingHorizontal: 12,
|
|
||||||
paddingVertical: 6,
|
|
||||||
borderRadius: theme.borderRadius.full,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: 'rgba(255, 255, 255, 0.3)',
|
|
||||||
},
|
|
||||||
memberText: {
|
|
||||||
color: theme.colors.white,
|
|
||||||
fontSize: theme.typography.fontSize.xs,
|
|
||||||
fontWeight: theme.typography.fontWeight.bold,
|
|
||||||
letterSpacing: 0.5,
|
|
||||||
},
|
},
|
||||||
content: {
|
content: {
|
||||||
padding: 20,
|
padding: 20,
|
||||||
marginTop: -20,
|
|
||||||
},
|
},
|
||||||
section: {
|
profileCard: {
|
||||||
|
backgroundColor: "white",
|
||||||
|
borderRadius: 16,
|
||||||
|
padding: 24,
|
||||||
|
alignItems: "center",
|
||||||
|
shadowColor: "#000",
|
||||||
|
shadowOffset: { width: 0, height: 2 },
|
||||||
|
shadowOpacity: 0.1,
|
||||||
|
shadowRadius: 8,
|
||||||
|
elevation: 4,
|
||||||
marginBottom: 24,
|
marginBottom: 24,
|
||||||
},
|
},
|
||||||
sectionTitle: {
|
avatarContainer: {
|
||||||
fontSize: theme.typography.fontSize.lg,
|
marginBottom: 16,
|
||||||
fontWeight: theme.typography.fontWeight.bold,
|
|
||||||
color: theme.colors.gray900,
|
|
||||||
marginBottom: 12,
|
|
||||||
marginLeft: 4,
|
|
||||||
},
|
},
|
||||||
infoCard: {
|
avatar: {
|
||||||
backgroundColor: theme.colors.white,
|
width: 80,
|
||||||
borderRadius: theme.borderRadius.xl,
|
height: 80,
|
||||||
padding: 8,
|
borderRadius: 40,
|
||||||
borderWidth: 1,
|
backgroundColor: "#2563eb",
|
||||||
borderColor: theme.colors.gray100,
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
avatarText: {
|
||||||
|
fontSize: 32,
|
||||||
|
fontWeight: "bold",
|
||||||
|
color: "#fff",
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: "bold",
|
||||||
|
color: "#1a1a1a",
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
fontSize: 16,
|
||||||
|
color: "#666",
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
phone: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: "#999",
|
||||||
|
},
|
||||||
|
section: {
|
||||||
|
backgroundColor: "white",
|
||||||
|
borderRadius: 16,
|
||||||
|
padding: 20,
|
||||||
|
marginBottom: 16,
|
||||||
|
shadowColor: "#000",
|
||||||
|
shadowOffset: { width: 0, height: 1 },
|
||||||
|
shadowOpacity: 0.05,
|
||||||
|
shadowRadius: 4,
|
||||||
|
elevation: 2,
|
||||||
|
},
|
||||||
|
sectionTitle: {
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: "600",
|
||||||
|
color: "#1a1a1a",
|
||||||
|
marginBottom: 16,
|
||||||
},
|
},
|
||||||
infoRow: {
|
infoRow: {
|
||||||
flexDirection: 'row',
|
flexDirection: "row",
|
||||||
alignItems: 'center',
|
justifyContent: "space-between",
|
||||||
padding: 12,
|
alignItems: "center",
|
||||||
},
|
paddingVertical: 12,
|
||||||
iconContainer: {
|
borderBottomWidth: 1,
|
||||||
width: 36,
|
borderBottomColor: "#f0f0f0",
|
||||||
height: 36,
|
|
||||||
borderRadius: 10,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginRight: 12,
|
|
||||||
},
|
},
|
||||||
infoLabel: {
|
infoLabel: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 8,
|
||||||
|
},
|
||||||
|
infoLabelText: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: "#666",
|
||||||
|
fontWeight: "500",
|
||||||
|
},
|
||||||
|
infoValue: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: "#1a1a1a",
|
||||||
|
fontWeight: "500",
|
||||||
|
},
|
||||||
|
actionButton: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
paddingVertical: 16,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: "#f0f0f0",
|
||||||
|
},
|
||||||
|
actionButtonText: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
fontSize: theme.typography.fontSize.base,
|
fontSize: 16,
|
||||||
color: theme.colors.gray900,
|
color: "#1a1a1a",
|
||||||
fontWeight: theme.typography.fontWeight.medium,
|
marginLeft: 12,
|
||||||
|
fontWeight: "500",
|
||||||
},
|
},
|
||||||
divider: {
|
logoutButton: {
|
||||||
height: 1,
|
backgroundColor: "#ef4444",
|
||||||
backgroundColor: theme.colors.gray100,
|
paddingVertical: 16,
|
||||||
marginLeft: 60,
|
borderRadius: 12,
|
||||||
},
|
alignItems: "center",
|
||||||
signOutButton: {
|
justifyContent: "center",
|
||||||
|
flexDirection: "row",
|
||||||
|
gap: 8,
|
||||||
marginTop: 8,
|
marginTop: 8,
|
||||||
|
marginBottom: 16,
|
||||||
|
shadowColor: "#ef4444",
|
||||||
|
shadowOffset: { width: 0, height: 4 },
|
||||||
|
shadowOpacity: 0.3,
|
||||||
|
shadowRadius: 8,
|
||||||
|
elevation: 4,
|
||||||
|
},
|
||||||
|
logoutText: {
|
||||||
|
color: "white",
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "600",
|
||||||
},
|
},
|
||||||
version: {
|
version: {
|
||||||
textAlign: 'center',
|
textAlign: "center",
|
||||||
marginTop: 24,
|
fontSize: 12,
|
||||||
color: theme.colors.gray400,
|
color: "#999",
|
||||||
fontSize: theme.typography.fontSize.xs,
|
marginTop: 8,
|
||||||
|
marginBottom: 20,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -2,9 +2,7 @@ import { useEffect, useState } from "react";
|
|||||||
import { View, Text, FlatList, ActivityIndicator, StyleSheet, RefreshControl } from "react-native";
|
import { View, Text, FlatList, ActivityIndicator, StyleSheet, RefreshControl } from "react-native";
|
||||||
import { useAuth } from "@clerk/clerk-expo";
|
import { useAuth } from "@clerk/clerk-expo";
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import { LinearGradient } from "expo-linear-gradient";
|
|
||||||
import { API_BASE_URL, API_ENDPOINTS } from "../../config/api";
|
import { API_BASE_URL, API_ENDPOINTS } from "../../config/api";
|
||||||
import { theme } from "../../styles/theme";
|
|
||||||
|
|
||||||
interface Recommendation {
|
interface Recommendation {
|
||||||
id: string;
|
id: string;
|
||||||
@ -72,71 +70,37 @@ export default function RecommendationsScreen() {
|
|||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<View style={styles.centered}>
|
<View style={styles.centered}>
|
||||||
<ActivityIndicator size="large" color={theme.colors.primary} />
|
<ActivityIndicator size="large" color="#000" />
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<LinearGradient
|
<Text style={styles.header}>AI Recommendations</Text>
|
||||||
colors={theme.gradients.primary}
|
|
||||||
start={{ x: 0, y: 0 }}
|
|
||||||
end={{ x: 1, y: 1 }}
|
|
||||||
style={styles.header}
|
|
||||||
>
|
|
||||||
<Text style={styles.headerTitle}>AI Recommendations</Text>
|
|
||||||
</LinearGradient>
|
|
||||||
|
|
||||||
{/* AI Context Info Banner with Glassmorphism */}
|
{/* AI Context Info Banner */}
|
||||||
<LinearGradient
|
<View style={styles.infoBanner}>
|
||||||
colors={['rgba(59, 130, 246, 0.15)', 'rgba(139, 92, 246, 0.1)'] as const}
|
<Ionicons name="sparkles" size={20} color="#2563eb" />
|
||||||
style={styles.infoBanner}
|
|
||||||
>
|
|
||||||
<View style={styles.infoBannerIconContainer}>
|
|
||||||
<LinearGradient
|
|
||||||
colors={theme.gradients.primary}
|
|
||||||
style={styles.infoBannerIcon}
|
|
||||||
>
|
|
||||||
<Ionicons name="sparkles" size={16} color="#fff" />
|
|
||||||
</LinearGradient>
|
|
||||||
</View>
|
|
||||||
<Text style={styles.infoBannerText}>
|
<Text style={styles.infoBannerText}>
|
||||||
Personalized based on your active fitness goals and progress
|
Personalized based on your active fitness goals and progress
|
||||||
</Text>
|
</Text>
|
||||||
</LinearGradient>
|
</View>
|
||||||
|
|
||||||
<FlatList
|
<FlatList
|
||||||
data={recommendations}
|
data={recommendations}
|
||||||
keyExtractor={(item) => item.id}
|
keyExtractor={(item) => item.id}
|
||||||
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}
|
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}
|
||||||
contentContainerStyle={styles.listContent}
|
|
||||||
ListEmptyComponent={
|
ListEmptyComponent={
|
||||||
<View style={styles.emptyContainer}>
|
<View style={styles.emptyContainer}>
|
||||||
<View style={styles.emptyIconContainer}>
|
|
||||||
<LinearGradient
|
|
||||||
colors={['rgba(209, 213, 219, 0.3)', 'rgba(209, 213, 219, 0.1)'] as const}
|
|
||||||
style={styles.emptyIconGradient}
|
|
||||||
>
|
|
||||||
<Ionicons name="sparkles-outline" size={48} color="#9ca3af" />
|
|
||||||
</LinearGradient>
|
|
||||||
</View>
|
|
||||||
<Text style={styles.empty}>No recommendations available yet.</Text>
|
<Text style={styles.empty}>No recommendations available yet.</Text>
|
||||||
<Text style={styles.emptySub}>Pull down to refresh</Text>
|
<Text style={styles.emptySub}>Pull down to refresh</Text>
|
||||||
</View>
|
</View>
|
||||||
}
|
}
|
||||||
renderItem={({ item }) => (
|
renderItem={({ item }) => (
|
||||||
<LinearGradient
|
<View style={styles.card}>
|
||||||
colors={['rgba(255, 255, 255, 1)', 'rgba(249, 250, 251, 1)'] as const}
|
|
||||||
style={[styles.card, theme.shadows.medium]}
|
|
||||||
>
|
|
||||||
<View style={styles.cardHeader}>
|
<View style={styles.cardHeader}>
|
||||||
<LinearGradient
|
<Text style={styles.status}>{item.status.toUpperCase()}</Text>
|
||||||
colors={theme.gradients.success}
|
|
||||||
style={styles.statusBadge}
|
|
||||||
>
|
|
||||||
<Text style={styles.statusText}>{item.status.toUpperCase()}</Text>
|
|
||||||
</LinearGradient>
|
|
||||||
<Text style={styles.date}>{new Date(item.createdAt).toLocaleDateString()}</Text>
|
<Text style={styles.date}>{new Date(item.createdAt).toLocaleDateString()}</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@ -156,7 +120,7 @@ export default function RecommendationsScreen() {
|
|||||||
<Text style={styles.content}>{item.dietPlan}</Text>
|
<Text style={styles.content}>{item.dietPlan}</Text>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</LinearGradient>
|
</View>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
@ -166,99 +130,83 @@ export default function RecommendationsScreen() {
|
|||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: theme.colors.background,
|
backgroundColor: '#f5f5f5',
|
||||||
},
|
},
|
||||||
header: {
|
header: {
|
||||||
paddingTop: 60,
|
fontSize: 28,
|
||||||
paddingBottom: 24,
|
fontWeight: 'bold',
|
||||||
paddingHorizontal: 24,
|
padding: 20,
|
||||||
borderBottomLeftRadius: theme.borderRadius.xl,
|
paddingBottom: 12,
|
||||||
borderBottomRightRadius: theme.borderRadius.xl,
|
color: '#1a1a1a',
|
||||||
},
|
|
||||||
headerTitle: {
|
|
||||||
fontSize: theme.typography.fontSize['3xl'],
|
|
||||||
fontWeight: theme.typography.fontWeight.bold,
|
|
||||||
color: theme.colors.white,
|
|
||||||
},
|
},
|
||||||
infoBanner: {
|
infoBanner: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
backgroundColor: '#eff6ff',
|
||||||
marginHorizontal: 16,
|
marginHorizontal: 16,
|
||||||
marginTop: 16,
|
|
||||||
marginBottom: 12,
|
marginBottom: 12,
|
||||||
padding: 14,
|
padding: 12,
|
||||||
borderRadius: theme.borderRadius.lg,
|
borderRadius: 8,
|
||||||
borderWidth: 1,
|
borderLeftWidth: 3,
|
||||||
borderColor: 'rgba(59, 130, 246, 0.2)',
|
borderLeftColor: '#2563eb',
|
||||||
gap: 10,
|
gap: 8,
|
||||||
},
|
|
||||||
infoBannerIconContainer: {
|
|
||||||
marginRight: 4,
|
|
||||||
},
|
|
||||||
infoBannerIcon: {
|
|
||||||
width: 32,
|
|
||||||
height: 32,
|
|
||||||
borderRadius: 16,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
},
|
||||||
infoBannerText: {
|
infoBannerText: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
fontSize: theme.typography.fontSize.sm,
|
fontSize: 13,
|
||||||
color: theme.colors.gray700,
|
color: '#1e40af',
|
||||||
lineHeight: 18,
|
lineHeight: 18,
|
||||||
fontWeight: theme.typography.fontWeight.medium,
|
|
||||||
},
|
},
|
||||||
centered: {
|
centered: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
backgroundColor: theme.colors.background,
|
backgroundColor: '#f5f5f5',
|
||||||
},
|
|
||||||
listContent: {
|
|
||||||
padding: 16,
|
|
||||||
},
|
},
|
||||||
card: {
|
card: {
|
||||||
padding: 18,
|
backgroundColor: '#fff',
|
||||||
marginBottom: 14,
|
padding: 16,
|
||||||
borderRadius: theme.borderRadius.xl,
|
marginHorizontal: 16,
|
||||||
borderWidth: 1,
|
marginBottom: 12,
|
||||||
borderColor: 'rgba(59, 130, 246, 0.1)',
|
borderRadius: 12,
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOpacity: 0.1,
|
||||||
|
shadowRadius: 4,
|
||||||
|
shadowOffset: { width: 0, height: 2 },
|
||||||
|
elevation: 3,
|
||||||
},
|
},
|
||||||
cardHeader: {
|
cardHeader: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginBottom: 14,
|
marginBottom: 12,
|
||||||
paddingBottom: 12,
|
paddingBottom: 12,
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
borderBottomColor: theme.colors.gray200,
|
borderBottomColor: '#e0e0e0',
|
||||||
},
|
},
|
||||||
statusBadge: {
|
status: {
|
||||||
paddingHorizontal: 10,
|
fontSize: 12,
|
||||||
paddingVertical: 5,
|
fontWeight: '600',
|
||||||
borderRadius: theme.borderRadius.md,
|
color: '#2e7d32',
|
||||||
},
|
backgroundColor: '#e8f5e9',
|
||||||
statusText: {
|
paddingHorizontal: 8,
|
||||||
fontSize: theme.typography.fontSize.xs,
|
paddingVertical: 4,
|
||||||
fontWeight: theme.typography.fontWeight.semibold,
|
borderRadius: 4,
|
||||||
color: theme.colors.white,
|
|
||||||
},
|
},
|
||||||
date: {
|
date: {
|
||||||
fontSize: theme.typography.fontSize.xs,
|
fontSize: 12,
|
||||||
color: theme.colors.gray600,
|
color: '#666',
|
||||||
fontWeight: theme.typography.fontWeight.medium,
|
|
||||||
},
|
},
|
||||||
sectionTitle: {
|
sectionTitle: {
|
||||||
fontSize: theme.typography.fontSize.base,
|
fontSize: 14,
|
||||||
fontWeight: theme.typography.fontWeight.semibold,
|
fontWeight: '600',
|
||||||
color: theme.colors.gray900,
|
color: '#1a1a1a',
|
||||||
marginTop: 12,
|
marginTop: 12,
|
||||||
marginBottom: 6,
|
marginBottom: 6,
|
||||||
},
|
},
|
||||||
content: {
|
content: {
|
||||||
fontSize: theme.typography.fontSize.sm,
|
fontSize: 14,
|
||||||
color: theme.colors.gray700,
|
color: '#333',
|
||||||
lineHeight: 20,
|
lineHeight: 20,
|
||||||
},
|
},
|
||||||
emptyContainer: {
|
emptyContainer: {
|
||||||
@ -266,26 +214,16 @@ const styles = StyleSheet.create({
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
paddingVertical: 60,
|
paddingVertical: 60,
|
||||||
},
|
},
|
||||||
emptyIconContainer: {
|
|
||||||
marginBottom: 16,
|
|
||||||
},
|
|
||||||
emptyIconGradient: {
|
|
||||||
width: 96,
|
|
||||||
height: 96,
|
|
||||||
borderRadius: 48,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
empty: {
|
empty: {
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
fontSize: theme.typography.fontSize.base,
|
fontSize: 16,
|
||||||
color: theme.colors.gray700,
|
color: '#666',
|
||||||
fontWeight: theme.typography.fontWeight.semibold,
|
fontWeight: '500',
|
||||||
marginBottom: 4,
|
|
||||||
},
|
},
|
||||||
emptySub: {
|
emptySub: {
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
fontSize: theme.typography.fontSize.sm,
|
fontSize: 14,
|
||||||
color: theme.colors.gray500,
|
color: '#999',
|
||||||
|
marginTop: 8,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,157 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { View, Text, StyleSheet, Dimensions } from 'react-native';
|
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
|
||||||
import { theme } from '../styles/theme';
|
|
||||||
|
|
||||||
const { width } = Dimensions.get('window');
|
|
||||||
|
|
||||||
interface ActivityWidgetProps {
|
|
||||||
steps: number;
|
|
||||||
calories: number;
|
|
||||||
duration: number; // in minutes
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ActivityWidget({ steps, calories, duration }: ActivityWidgetProps) {
|
|
||||||
return (
|
|
||||||
<View style={styles.container}>
|
|
||||||
<LinearGradient
|
|
||||||
colors={theme.gradients.dark}
|
|
||||||
start={{ x: 0, y: 0 }}
|
|
||||||
end={{ x: 1, y: 1 }}
|
|
||||||
style={[styles.card, theme.shadows.medium]}
|
|
||||||
>
|
|
||||||
<View style={styles.header}>
|
|
||||||
<Text style={styles.title}>Daily Activity</Text>
|
|
||||||
<Ionicons name="stats-chart" size={20} color={theme.colors.primary} />
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.statsRow}>
|
|
||||||
<View style={styles.statItem}>
|
|
||||||
<View style={[styles.iconContainer, { backgroundColor: 'rgba(59, 130, 246, 0.2)' }]}>
|
|
||||||
<Ionicons name="footsteps" size={20} color="#3b82f6" />
|
|
||||||
</View>
|
|
||||||
<Text style={styles.statValue}>{steps.toLocaleString()}</Text>
|
|
||||||
<Text style={styles.statLabel}>Steps</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.divider} />
|
|
||||||
|
|
||||||
<View style={styles.statItem}>
|
|
||||||
<View style={[styles.iconContainer, { backgroundColor: 'rgba(239, 68, 68, 0.2)' }]}>
|
|
||||||
<Ionicons name="flame" size={20} color="#ef4444" />
|
|
||||||
</View>
|
|
||||||
<Text style={styles.statValue}>{calories}</Text>
|
|
||||||
<Text style={styles.statLabel}>Kcal</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.divider} />
|
|
||||||
|
|
||||||
<View style={styles.statItem}>
|
|
||||||
<View style={[styles.iconContainer, { backgroundColor: 'rgba(16, 185, 129, 0.2)' }]}>
|
|
||||||
<Ionicons name="time" size={20} color="#10b981" />
|
|
||||||
</View>
|
|
||||||
<Text style={styles.statValue}>{duration}m</Text>
|
|
||||||
<Text style={styles.statLabel}>Active</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Simple Bar Chart Visualization */}
|
|
||||||
<View style={styles.chartContainer}>
|
|
||||||
{[0.4, 0.6, 0.3, 0.8, 0.5, 0.9, 0.7].map((height, index) => (
|
|
||||||
<View key={index} style={styles.barContainer}>
|
|
||||||
<LinearGradient
|
|
||||||
colors={theme.gradients.primaryVertical}
|
|
||||||
style={[styles.bar, { height: height * 60 }]}
|
|
||||||
/>
|
|
||||||
<Text style={styles.dayLabel}>
|
|
||||||
{['M', 'T', 'W', 'T', 'F', 'S', 'S'][index]}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
</LinearGradient>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
marginHorizontal: 20,
|
|
||||||
marginBottom: 20,
|
|
||||||
},
|
|
||||||
card: {
|
|
||||||
borderRadius: 24,
|
|
||||||
padding: 20,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: 'rgba(255, 255, 255, 0.1)',
|
|
||||||
},
|
|
||||||
header: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginBottom: 20,
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: '700',
|
|
||||||
color: '#fff',
|
|
||||||
},
|
|
||||||
statsRow: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginBottom: 24,
|
|
||||||
},
|
|
||||||
statItem: {
|
|
||||||
alignItems: 'center',
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
iconContainer: {
|
|
||||||
width: 40,
|
|
||||||
height: 40,
|
|
||||||
borderRadius: 20,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginBottom: 8,
|
|
||||||
},
|
|
||||||
statValue: {
|
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: '700',
|
|
||||||
color: '#fff',
|
|
||||||
marginBottom: 2,
|
|
||||||
},
|
|
||||||
statLabel: {
|
|
||||||
fontSize: 12,
|
|
||||||
color: theme.colors.gray400,
|
|
||||||
fontWeight: '500',
|
|
||||||
},
|
|
||||||
divider: {
|
|
||||||
width: 1,
|
|
||||||
height: 40,
|
|
||||||
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
|
||||||
},
|
|
||||||
chartContainer: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'flex-end',
|
|
||||||
height: 80,
|
|
||||||
paddingTop: 10,
|
|
||||||
borderTopWidth: 1,
|
|
||||||
borderTopColor: 'rgba(255, 255, 255, 0.1)',
|
|
||||||
},
|
|
||||||
barContainer: {
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 8,
|
|
||||||
},
|
|
||||||
bar: {
|
|
||||||
width: 6,
|
|
||||||
borderRadius: 3,
|
|
||||||
opacity: 0.8,
|
|
||||||
},
|
|
||||||
dayLabel: {
|
|
||||||
fontSize: 10,
|
|
||||||
color: theme.colors.gray500,
|
|
||||||
fontWeight: '600',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@ -1,154 +0,0 @@
|
|||||||
/**
|
|
||||||
* Animated Button Component
|
|
||||||
* Modern button with gradient backgrounds and smooth animations
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React, { useRef } from 'react';
|
|
||||||
import {
|
|
||||||
TouchableOpacity,
|
|
||||||
Text,
|
|
||||||
StyleSheet,
|
|
||||||
Animated,
|
|
||||||
ActivityIndicator,
|
|
||||||
ViewStyle,
|
|
||||||
TextStyle,
|
|
||||||
} from 'react-native';
|
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
|
||||||
import { theme } from '../styles/theme';
|
|
||||||
import { haptics } from '../utils/haptics';
|
|
||||||
|
|
||||||
interface AnimatedButtonProps {
|
|
||||||
title: string;
|
|
||||||
onPress: () => void;
|
|
||||||
variant?: 'primary' | 'secondary' | 'success' | 'danger' | 'warning';
|
|
||||||
size?: 'sm' | 'md' | 'lg';
|
|
||||||
loading?: boolean;
|
|
||||||
disabled?: boolean;
|
|
||||||
style?: ViewStyle;
|
|
||||||
textStyle?: TextStyle;
|
|
||||||
icon?: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AnimatedButton({
|
|
||||||
title,
|
|
||||||
onPress,
|
|
||||||
variant = 'primary',
|
|
||||||
size = 'md',
|
|
||||||
loading = false,
|
|
||||||
disabled = false,
|
|
||||||
style,
|
|
||||||
textStyle,
|
|
||||||
icon,
|
|
||||||
}: AnimatedButtonProps) {
|
|
||||||
const scale = useRef(new Animated.Value(1)).current;
|
|
||||||
|
|
||||||
const handlePressIn = () => {
|
|
||||||
Animated.spring(scale, {
|
|
||||||
toValue: 0.95,
|
|
||||||
useNativeDriver: true,
|
|
||||||
}).start();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePressOut = () => {
|
|
||||||
Animated.spring(scale, {
|
|
||||||
toValue: 1,
|
|
||||||
useNativeDriver: true,
|
|
||||||
}).start();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePress = () => {
|
|
||||||
if (!disabled && !loading) {
|
|
||||||
haptics.light();
|
|
||||||
onPress();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getGradientColors = () => {
|
|
||||||
if (disabled) return ['#9ca3af', '#6b7280'] as const;
|
|
||||||
switch (variant) {
|
|
||||||
case 'secondary':
|
|
||||||
return theme.gradients.purple;
|
|
||||||
case 'success':
|
|
||||||
return theme.gradients.success;
|
|
||||||
case 'danger':
|
|
||||||
return theme.gradients.danger;
|
|
||||||
case 'warning':
|
|
||||||
return theme.gradients.warning;
|
|
||||||
default:
|
|
||||||
return theme.gradients.primary;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getSizeStyles = () => {
|
|
||||||
switch (size) {
|
|
||||||
case 'sm':
|
|
||||||
return { paddingVertical: 8, paddingHorizontal: 16, fontSize: 14 };
|
|
||||||
case 'lg':
|
|
||||||
return { paddingVertical: 16, paddingHorizontal: 32, fontSize: 18 };
|
|
||||||
default:
|
|
||||||
return { paddingVertical: 12, paddingHorizontal: 24, fontSize: 16 };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const sizeStyles = getSizeStyles();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TouchableOpacity
|
|
||||||
onPressIn={handlePressIn}
|
|
||||||
onPressOut={handlePressOut}
|
|
||||||
onPress={handlePress}
|
|
||||||
disabled={disabled || loading}
|
|
||||||
activeOpacity={0.9}
|
|
||||||
style={[styles.container, style]}
|
|
||||||
>
|
|
||||||
<Animated.View style={{ transform: [{ scale }], width: '100%' }}>
|
|
||||||
<LinearGradient
|
|
||||||
colors={getGradientColors()}
|
|
||||||
start={{ x: 0, y: 0 }}
|
|
||||||
end={{ x: 1, y: 0 }}
|
|
||||||
style={[
|
|
||||||
styles.gradient,
|
|
||||||
{ paddingVertical: sizeStyles.paddingVertical, paddingHorizontal: sizeStyles.paddingHorizontal },
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{loading ? (
|
|
||||||
<ActivityIndicator color="#fff" />
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{icon && <React.Fragment>{icon}</React.Fragment>}
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
styles.text,
|
|
||||||
{ fontSize: sizeStyles.fontSize },
|
|
||||||
icon ? { marginLeft: 8 } : {},
|
|
||||||
textStyle,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{title}
|
|
||||||
</Text>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</LinearGradient>
|
|
||||||
</Animated.View>
|
|
||||||
</TouchableOpacity>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
},
|
|
||||||
gradient: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
borderRadius: 12,
|
|
||||||
...theme.shadows.medium,
|
|
||||||
},
|
|
||||||
text: {
|
|
||||||
color: '#fff',
|
|
||||||
fontWeight: '600',
|
|
||||||
textAlign: 'center',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@ -1,72 +0,0 @@
|
|||||||
import React, { useEffect, useRef } from 'react';
|
|
||||||
import { View, Animated, StyleSheet, Text } from 'react-native';
|
|
||||||
import Svg, { Circle } from 'react-native-svg';
|
|
||||||
import { theme } from '../styles/theme';
|
|
||||||
|
|
||||||
const AnimatedCircle = Animated.createAnimatedComponent(Circle);
|
|
||||||
|
|
||||||
interface CircularProgressProps {
|
|
||||||
size?: number;
|
|
||||||
strokeWidth?: number;
|
|
||||||
progress: number; // 0 to 100
|
|
||||||
color?: string;
|
|
||||||
backgroundColor?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CircularProgress({
|
|
||||||
size = 60,
|
|
||||||
strokeWidth = 6,
|
|
||||||
progress,
|
|
||||||
color = theme.colors.primary,
|
|
||||||
backgroundColor = theme.colors.gray200,
|
|
||||||
}: CircularProgressProps) {
|
|
||||||
const animatedValue = useRef(new Animated.Value(0)).current;
|
|
||||||
const radius = (size - strokeWidth) / 2;
|
|
||||||
const circumference = radius * 2 * Math.PI;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
Animated.timing(animatedValue, {
|
|
||||||
toValue: progress,
|
|
||||||
duration: 1000,
|
|
||||||
useNativeDriver: true,
|
|
||||||
}).start();
|
|
||||||
}, [progress]);
|
|
||||||
|
|
||||||
const strokeDashoffset = animatedValue.interpolate({
|
|
||||||
inputRange: [0, 100],
|
|
||||||
outputRange: [circumference, 0],
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View style={{ width: size, height: size, justifyContent: 'center', alignItems: 'center' }}>
|
|
||||||
<Svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
|
|
||||||
<Circle
|
|
||||||
cx={size / 2}
|
|
||||||
cy={size / 2}
|
|
||||||
r={radius}
|
|
||||||
stroke={backgroundColor}
|
|
||||||
strokeWidth={strokeWidth}
|
|
||||||
fill="transparent"
|
|
||||||
/>
|
|
||||||
<AnimatedCircle
|
|
||||||
cx={size / 2}
|
|
||||||
cy={size / 2}
|
|
||||||
r={radius}
|
|
||||||
stroke={color}
|
|
||||||
strokeWidth={strokeWidth}
|
|
||||||
fill="transparent"
|
|
||||||
strokeDasharray={circumference}
|
|
||||||
strokeDashoffset={strokeDashoffset}
|
|
||||||
strokeLinecap="round"
|
|
||||||
rotation="-90"
|
|
||||||
origin={`${size / 2}, ${size / 2}`}
|
|
||||||
/>
|
|
||||||
</Svg>
|
|
||||||
<View style={[StyleSheet.absoluteFillObject, { justifyContent: 'center', alignItems: 'center' }]}>
|
|
||||||
<Text style={{ fontSize: size * 0.25, fontWeight: 'bold', color: theme.colors.gray900 }}>
|
|
||||||
{Math.round(progress)}%
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,143 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { View, StyleSheet, TouchableOpacity, Platform, Dimensions } from 'react-native';
|
|
||||||
import { BottomTabBarProps } from '@react-navigation/bottom-tabs';
|
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
|
||||||
import { theme } from '../styles/theme';
|
|
||||||
import { Animated } from 'react-native';
|
|
||||||
|
|
||||||
const { width } = Dimensions.get('window');
|
|
||||||
|
|
||||||
export function CustomTabBar({ state, descriptors, navigation }: BottomTabBarProps) {
|
|
||||||
return (
|
|
||||||
<View style={styles.container}>
|
|
||||||
<LinearGradient
|
|
||||||
colors={['rgba(255, 255, 255, 0.9)', 'rgba(255, 255, 255, 0.7)']}
|
|
||||||
style={[styles.tabBar, theme.shadows.medium]}
|
|
||||||
start={{ x: 0, y: 0 }}
|
|
||||||
end={{ x: 0, y: 1 }}
|
|
||||||
>
|
|
||||||
{state.routes.map((route, index) => {
|
|
||||||
const { options } = descriptors[route.key];
|
|
||||||
const isFocused = state.index === index;
|
|
||||||
|
|
||||||
const onPress = () => {
|
|
||||||
const event = navigation.emit({
|
|
||||||
type: 'tabPress',
|
|
||||||
target: route.key,
|
|
||||||
canPreventDefault: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!isFocused && !event.defaultPrevented) {
|
|
||||||
navigation.navigate(route.name);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getIconName = (routeName: string, focused: boolean): keyof typeof Ionicons.glyphMap => {
|
|
||||||
switch (routeName) {
|
|
||||||
case 'index':
|
|
||||||
return focused ? 'home' : 'home-outline';
|
|
||||||
case 'goals':
|
|
||||||
return focused ? 'trophy' : 'trophy-outline';
|
|
||||||
case 'attendance':
|
|
||||||
return focused ? 'calendar' : 'calendar-outline';
|
|
||||||
case 'recommendations':
|
|
||||||
return focused ? 'sparkles' : 'sparkles-outline';
|
|
||||||
case 'profile':
|
|
||||||
return focused ? 'person' : 'person-outline';
|
|
||||||
default:
|
|
||||||
return 'ellipse-outline';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Animation for scale
|
|
||||||
const scaleValue = React.useRef(new Animated.Value(1)).current;
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
Animated.spring(scaleValue, {
|
|
||||||
toValue: isFocused ? 1.2 : 1,
|
|
||||||
useNativeDriver: true,
|
|
||||||
friction: 8,
|
|
||||||
}).start();
|
|
||||||
}, [isFocused]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TouchableOpacity
|
|
||||||
key={index}
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityState={isFocused ? { selected: true } : {}}
|
|
||||||
accessibilityLabel={options.tabBarAccessibilityLabel}
|
|
||||||
testID={(options as any).tabBarTestID}
|
|
||||||
onPress={onPress}
|
|
||||||
style={styles.tabItem}
|
|
||||||
activeOpacity={0.7}
|
|
||||||
>
|
|
||||||
<Animated.View style={{ transform: [{ scale: scaleValue }] }}>
|
|
||||||
{isFocused ? (
|
|
||||||
<LinearGradient
|
|
||||||
colors={theme.gradients.primary}
|
|
||||||
style={styles.iconContainer}
|
|
||||||
>
|
|
||||||
<Ionicons
|
|
||||||
name={getIconName(route.name, true)}
|
|
||||||
size={20}
|
|
||||||
color="#fff"
|
|
||||||
/>
|
|
||||||
</LinearGradient>
|
|
||||||
) : (
|
|
||||||
<Ionicons
|
|
||||||
name={getIconName(route.name, false)}
|
|
||||||
size={24}
|
|
||||||
color={theme.colors.gray500}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Animated.View>
|
|
||||||
</TouchableOpacity>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</LinearGradient>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
position: 'absolute',
|
|
||||||
bottom: 0,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
alignItems: 'center',
|
|
||||||
paddingBottom: Platform.OS === 'ios' ? 30 : 20,
|
|
||||||
pointerEvents: 'box-none',
|
|
||||||
},
|
|
||||||
tabBar: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
backgroundColor: 'rgba(255, 255, 255, 0.8)',
|
|
||||||
borderRadius: 35,
|
|
||||||
height: 70,
|
|
||||||
width: width - 40,
|
|
||||||
justifyContent: 'space-around',
|
|
||||||
alignItems: 'center',
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: 'rgba(255, 255, 255, 0.5)',
|
|
||||||
paddingHorizontal: 10,
|
|
||||||
},
|
|
||||||
tabItem: {
|
|
||||||
flex: 1,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
height: '100%',
|
|
||||||
},
|
|
||||||
iconContainer: {
|
|
||||||
width: 40,
|
|
||||||
height: 40,
|
|
||||||
borderRadius: 20,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
shadowColor: theme.colors.primary,
|
|
||||||
shadowOffset: { width: 0, height: 4 },
|
|
||||||
shadowOpacity: 0.3,
|
|
||||||
shadowRadius: 8,
|
|
||||||
elevation: 4,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@ -1,223 +0,0 @@
|
|||||||
import React, { useRef } from 'react';
|
|
||||||
import { View, Text, StyleSheet, Dimensions, ScrollView, TouchableOpacity, Animated } from 'react-native';
|
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
|
||||||
import { theme } from '../styles/theme';
|
|
||||||
import { CircularProgress } from './CircularProgress';
|
|
||||||
import type { FitnessGoal } from '../services/fitnessGoals';
|
|
||||||
|
|
||||||
const { width } = Dimensions.get('window');
|
|
||||||
const CARD_WIDTH = width * 0.8;
|
|
||||||
const SPACING = 20;
|
|
||||||
|
|
||||||
interface GoalCarouselProps {
|
|
||||||
goals: FitnessGoal[];
|
|
||||||
onGoalPress: (goal: FitnessGoal) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function GoalCarousel({ goals, onGoalPress }: GoalCarouselProps) {
|
|
||||||
const scrollX = useRef(new Animated.Value(0)).current;
|
|
||||||
|
|
||||||
if (goals.length === 0) {
|
|
||||||
return (
|
|
||||||
<View style={styles.emptyContainer}>
|
|
||||||
<Text style={styles.emptyText}>No active goals</Text>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View>
|
|
||||||
<Animated.ScrollView
|
|
||||||
horizontal
|
|
||||||
showsHorizontalScrollIndicator={false}
|
|
||||||
snapToInterval={CARD_WIDTH + SPACING}
|
|
||||||
decelerationRate="fast"
|
|
||||||
contentContainerStyle={styles.scrollContent}
|
|
||||||
onScroll={Animated.event(
|
|
||||||
[{ nativeEvent: { contentOffset: { x: scrollX } } }],
|
|
||||||
{ useNativeDriver: true }
|
|
||||||
)}
|
|
||||||
scrollEventThrottle={16}
|
|
||||||
>
|
|
||||||
{goals.map((goal, index) => {
|
|
||||||
const inputRange = [
|
|
||||||
(index - 1) * (CARD_WIDTH + SPACING),
|
|
||||||
index * (CARD_WIDTH + SPACING),
|
|
||||||
(index + 1) * (CARD_WIDTH + SPACING),
|
|
||||||
];
|
|
||||||
|
|
||||||
const scale = scrollX.interpolate({
|
|
||||||
inputRange,
|
|
||||||
outputRange: [0.9, 1, 0.9],
|
|
||||||
extrapolate: 'clamp',
|
|
||||||
});
|
|
||||||
|
|
||||||
const opacity = scrollX.interpolate({
|
|
||||||
inputRange,
|
|
||||||
outputRange: [0.7, 1, 0.7],
|
|
||||||
extrapolate: 'clamp',
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TouchableOpacity
|
|
||||||
key={goal.id}
|
|
||||||
activeOpacity={0.9}
|
|
||||||
onPress={() => onGoalPress(goal)}
|
|
||||||
>
|
|
||||||
<Animated.View
|
|
||||||
style={[
|
|
||||||
styles.cardContainer,
|
|
||||||
{ transform: [{ scale }], opacity },
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<LinearGradient
|
|
||||||
colors={['#ffffff', '#f9fafb']}
|
|
||||||
style={[styles.card, theme.shadows.medium]}
|
|
||||||
>
|
|
||||||
<View style={styles.cardHeader}>
|
|
||||||
<View style={styles.iconContainer}>
|
|
||||||
<LinearGradient
|
|
||||||
colors={theme.gradients.primary}
|
|
||||||
style={styles.iconGradient}
|
|
||||||
>
|
|
||||||
<Ionicons name="trophy" size={24} color="#fff" />
|
|
||||||
</LinearGradient>
|
|
||||||
</View>
|
|
||||||
<View style={styles.priorityBadge}>
|
|
||||||
<Text style={styles.priorityText}>{goal.priority.toUpperCase()}</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<Text style={styles.title} numberOfLines={1}>{goal.title}</Text>
|
|
||||||
<Text style={styles.description} numberOfLines={2}>{goal.description}</Text>
|
|
||||||
|
|
||||||
<View style={styles.progressContainer}>
|
|
||||||
<CircularProgress
|
|
||||||
progress={goal.progress || 0}
|
|
||||||
size={80}
|
|
||||||
strokeWidth={8}
|
|
||||||
color={theme.colors.primary}
|
|
||||||
/>
|
|
||||||
<View style={styles.progressDetails}>
|
|
||||||
<Text style={styles.progressValue}>
|
|
||||||
{goal.currentValue} / {goal.targetValue}
|
|
||||||
</Text>
|
|
||||||
<Text style={styles.progressUnit}>{goal.unit}</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.footer}>
|
|
||||||
<Text style={styles.date}>
|
|
||||||
Target: {goal.targetDate ? new Date(goal.targetDate).toLocaleDateString() : 'No date'}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</LinearGradient>
|
|
||||||
</Animated.View>
|
|
||||||
</TouchableOpacity>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Animated.ScrollView>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
scrollContent: {
|
|
||||||
paddingHorizontal: (width - CARD_WIDTH) / 2,
|
|
||||||
paddingVertical: 20,
|
|
||||||
},
|
|
||||||
emptyContainer: {
|
|
||||||
height: 200,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
emptyText: {
|
|
||||||
color: theme.colors.gray500,
|
|
||||||
fontSize: 16,
|
|
||||||
},
|
|
||||||
cardContainer: {
|
|
||||||
width: CARD_WIDTH,
|
|
||||||
marginRight: SPACING,
|
|
||||||
},
|
|
||||||
card: {
|
|
||||||
borderRadius: 24,
|
|
||||||
padding: 24,
|
|
||||||
height: 320,
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: 'rgba(0,0,0,0.05)',
|
|
||||||
},
|
|
||||||
cardHeader: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'flex-start',
|
|
||||||
marginBottom: 16,
|
|
||||||
},
|
|
||||||
iconContainer: {
|
|
||||||
shadowColor: theme.colors.primary,
|
|
||||||
shadowOffset: { width: 0, height: 4 },
|
|
||||||
shadowOpacity: 0.3,
|
|
||||||
shadowRadius: 8,
|
|
||||||
elevation: 4,
|
|
||||||
},
|
|
||||||
iconGradient: {
|
|
||||||
width: 48,
|
|
||||||
height: 48,
|
|
||||||
borderRadius: 16,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
priorityBadge: {
|
|
||||||
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
|
||||||
paddingHorizontal: 12,
|
|
||||||
paddingVertical: 6,
|
|
||||||
borderRadius: 12,
|
|
||||||
},
|
|
||||||
priorityText: {
|
|
||||||
color: theme.colors.primary,
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: '700',
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
fontSize: 24,
|
|
||||||
fontWeight: '800',
|
|
||||||
color: theme.colors.gray900,
|
|
||||||
marginBottom: 8,
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
fontSize: 14,
|
|
||||||
color: theme.colors.gray500,
|
|
||||||
marginBottom: 24,
|
|
||||||
lineHeight: 20,
|
|
||||||
},
|
|
||||||
progressContainer: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
marginBottom: 24,
|
|
||||||
},
|
|
||||||
progressDetails: {
|
|
||||||
alignItems: 'flex-end',
|
|
||||||
},
|
|
||||||
progressValue: {
|
|
||||||
fontSize: 24,
|
|
||||||
fontWeight: '700',
|
|
||||||
color: theme.colors.gray900,
|
|
||||||
},
|
|
||||||
progressUnit: {
|
|
||||||
fontSize: 14,
|
|
||||||
color: theme.colors.gray500,
|
|
||||||
fontWeight: '500',
|
|
||||||
},
|
|
||||||
footer: {
|
|
||||||
borderTopWidth: 1,
|
|
||||||
borderTopColor: theme.colors.gray100,
|
|
||||||
paddingTop: 16,
|
|
||||||
},
|
|
||||||
date: {
|
|
||||||
fontSize: 12,
|
|
||||||
color: theme.colors.gray400,
|
|
||||||
fontWeight: '500',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@ -8,7 +8,6 @@ import {
|
|||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
Platform,
|
Platform,
|
||||||
Alert,
|
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import DateTimePicker from '@react-native-community/datetimepicker';
|
import DateTimePicker from '@react-native-community/datetimepicker';
|
||||||
@ -60,7 +59,7 @@ export function GoalCreationModal({ visible, onClose, onSubmit }: GoalCreationMo
|
|||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!title.trim()) {
|
if (!title.trim()) {
|
||||||
Alert.alert('Error', 'Please enter a goal title');
|
alert('Please enter a goal title');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,7 +81,7 @@ export function GoalCreationModal({ visible, onClose, onSubmit }: GoalCreationMo
|
|||||||
onClose();
|
onClose();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating goal:', error);
|
console.error('Error creating goal:', error);
|
||||||
Alert.alert('Error', 'Failed to create goal. Please try again.');
|
alert('Failed to create goal. Please try again.');
|
||||||
} finally {
|
} finally {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
}
|
}
|
||||||
@ -204,31 +203,28 @@ export function GoalCreationModal({ visible, onClose, onSubmit }: GoalCreationMo
|
|||||||
<Text style={styles.label}>Target Date</Text>
|
<Text style={styles.label}>Target Date</Text>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.dateButton}
|
style={styles.dateButton}
|
||||||
onPress={() => setShowDatePicker(!showDatePicker)}
|
onPress={() => setShowDatePicker(true)}
|
||||||
>
|
>
|
||||||
<Text style={targetDate ? styles.dateText : styles.datePlaceholder}>
|
<Text style={targetDate ? styles.dateText : styles.datePlaceholder}>
|
||||||
{targetDate ? targetDate.toLocaleDateString() : 'Select target date'}
|
{targetDate ? targetDate.toLocaleDateString() : 'Select target date'}
|
||||||
</Text>
|
</Text>
|
||||||
<Ionicons name={showDatePicker ? "chevron-up" : "calendar-outline"} size={20} color="#6b7280" />
|
<Ionicons name="calendar-outline" size={20} color="#6b7280" />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{showDatePicker && (
|
{showDatePicker && (
|
||||||
<View style={Platform.OS === 'ios' ? styles.datePickerContainer : undefined}>
|
<DateTimePicker
|
||||||
<DateTimePicker
|
value={targetDate || new Date()}
|
||||||
value={targetDate || new Date()}
|
mode="date"
|
||||||
mode="date"
|
display={Platform.OS === 'ios' ? 'spinner' : 'default'}
|
||||||
display={Platform.OS === 'ios' ? 'inline' : 'default'}
|
onChange={(event, selectedDate) => {
|
||||||
onChange={(event, selectedDate) => {
|
setShowDatePicker(Platform.OS === 'ios');
|
||||||
setShowDatePicker(Platform.OS === 'ios');
|
if (selectedDate) {
|
||||||
if (selectedDate) {
|
setTargetDate(selectedDate);
|
||||||
setTargetDate(selectedDate);
|
}
|
||||||
}
|
}}
|
||||||
}}
|
minimumDate={new Date()}
|
||||||
minimumDate={new Date()}
|
/>
|
||||||
themeVariant="light"
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Priority */}
|
{/* Priority */}
|
||||||
@ -415,12 +411,4 @@ const styles = StyleSheet.create({
|
|||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
},
|
},
|
||||||
datePickerContainer: {
|
|
||||||
backgroundColor: '#fff',
|
|
||||||
borderRadius: 8,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: '#d1d5db',
|
|
||||||
marginTop: 8,
|
|
||||||
overflow: 'hidden',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,9 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { View, Text, StyleSheet, TouchableOpacity, Alert } from 'react-native';
|
import { View, Text, StyleSheet, TouchableOpacity, Alert } from 'react-native';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
|
||||||
import type { FitnessGoal } from '../services/fitnessGoals';
|
import type { FitnessGoal } from '../services/fitnessGoals';
|
||||||
import { theme } from '../styles/theme';
|
|
||||||
|
|
||||||
interface GoalProgressCardProps {
|
interface GoalProgressCardProps {
|
||||||
goal: FitnessGoal;
|
goal: FitnessGoal;
|
||||||
@ -32,12 +30,12 @@ export function GoalProgressCard({ goal, onPress, onComplete, onDelete }: GoalPr
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPriorityGradient = (priority: string): readonly [string, string] => {
|
const getPriorityColor = (priority: string) => {
|
||||||
switch (priority) {
|
switch (priority) {
|
||||||
case 'high': return theme.gradients.danger;
|
case 'high': return '#ef4444';
|
||||||
case 'medium': return theme.gradients.warning;
|
case 'medium': return '#f59e0b';
|
||||||
case 'low': return theme.gradients.success;
|
case 'low': return '#10b981';
|
||||||
default: return theme.gradients.primary;
|
default: return '#6b7280';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -54,175 +52,134 @@ export function GoalProgressCard({ goal, onPress, onComplete, onDelete }: GoalPr
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
|
style={[styles.card, isCompleted && styles.cardCompleted]}
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
<LinearGradient
|
<View style={styles.header}>
|
||||||
colors={isCompleted
|
<View style={styles.titleRow}>
|
||||||
? ['rgba(16, 185, 129, 0.05)', 'rgba(5, 150, 105, 0.02)'] as const
|
<Ionicons
|
||||||
: ['rgba(255, 255, 255, 1)', 'rgba(249, 250, 251, 1)'] as const
|
name={getGoalTypeIcon(goal.goalType) as any}
|
||||||
}
|
size={24}
|
||||||
style={[
|
color={isCompleted ? '#9ca3af' : '#2563eb'}
|
||||||
styles.card,
|
/>
|
||||||
theme.shadows.medium,
|
<View style={styles.titleContainer}>
|
||||||
isCompleted && styles.cardCompleted
|
<Text style={[styles.title, isCompleted && styles.titleCompleted]}>
|
||||||
]}
|
{goal.title}
|
||||||
>
|
</Text>
|
||||||
{/* Priority Accent Bar */}
|
{goal.description && (
|
||||||
<LinearGradient
|
<Text style={styles.description} numberOfLines={2}>
|
||||||
colors={getPriorityGradient(goal.priority)}
|
{goal.description}
|
||||||
start={{ x: 0, y: 0 }}
|
|
||||||
end={{ x: 0, y: 1 }}
|
|
||||||
style={styles.priorityAccent}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<View style={styles.header}>
|
|
||||||
<View style={styles.titleRow}>
|
|
||||||
<LinearGradient
|
|
||||||
colors={isCompleted ? theme.gradients.success : getPriorityGradient(goal.priority)}
|
|
||||||
style={styles.iconContainer}
|
|
||||||
>
|
|
||||||
<Ionicons
|
|
||||||
name={getGoalTypeIcon(goal.goalType) as any}
|
|
||||||
size={20}
|
|
||||||
color="#fff"
|
|
||||||
/>
|
|
||||||
</LinearGradient>
|
|
||||||
<View style={styles.titleContainer}>
|
|
||||||
<Text style={[styles.title, isCompleted && styles.titleCompleted]}>
|
|
||||||
{goal.title}
|
|
||||||
</Text>
|
</Text>
|
||||||
{goal.description && (
|
|
||||||
<Text style={styles.description} numberOfLines={2}>
|
|
||||||
{goal.description}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.actions}>
|
|
||||||
{!isCompleted && onComplete && (
|
|
||||||
<TouchableOpacity onPress={onComplete} style={styles.actionButton}>
|
|
||||||
<Ionicons name="checkmark-circle-outline" size={24} color={theme.colors.success} />
|
|
||||||
</TouchableOpacity>
|
|
||||||
)}
|
|
||||||
{onDelete && (
|
|
||||||
<TouchableOpacity onPress={handleDelete} style={styles.actionButton}>
|
|
||||||
<Ionicons name="trash-outline" size={22} color={theme.colors.danger} />
|
|
||||||
</TouchableOpacity>
|
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{goal.targetValue && (
|
<View style={styles.actions}>
|
||||||
<View style={styles.progressSection}>
|
{!isCompleted && onComplete && (
|
||||||
<View style={styles.progressInfo}>
|
<TouchableOpacity onPress={onComplete} style={styles.actionButton}>
|
||||||
<Text style={styles.progressText}>
|
<Ionicons name="checkmark-circle-outline" size={24} color="#10b981" />
|
||||||
{goal.currentValue || 0} / {goal.targetValue} {goal.unit || ''}
|
</TouchableOpacity>
|
||||||
</Text>
|
)}
|
||||||
<Text style={[styles.progressPercentage, isCompleted && { color: theme.colors.success }]}>
|
{onDelete && (
|
||||||
{progress.toFixed(0)}%
|
<TouchableOpacity onPress={handleDelete} style={styles.actionButton}>
|
||||||
</Text>
|
<Ionicons name="trash-outline" size={22} color="#ef4444" />
|
||||||
</View>
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
<View style={styles.progressBarContainer}>
|
{goal.targetValue && (
|
||||||
<LinearGradient
|
<View style={styles.progressSection}>
|
||||||
colors={isCompleted ? theme.gradients.success : getPriorityGradient(goal.priority)}
|
<View style={styles.progressInfo}>
|
||||||
start={{ x: 0, y: 0 }}
|
<Text style={styles.progressText}>
|
||||||
end={{ x: 1, y: 0 }}
|
{goal.currentValue || 0} / {goal.targetValue} {goal.unit || ''}
|
||||||
style={[
|
</Text>
|
||||||
styles.progressBar,
|
<Text style={styles.progressPercentage}>{progress.toFixed(0)}%</Text>
|
||||||
{ width: `${Math.min(progress, 100)}%` }
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.progressBarContainer}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.progressBar,
|
||||||
|
{ width: `${Math.min(progress, 100)}%` },
|
||||||
|
isCompleted && styles.progressBarCompleted
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<View style={styles.footer}>
|
||||||
|
<View style={[styles.priorityBadge, { backgroundColor: getPriorityColor(goal.priority) }]}>
|
||||||
|
<Text style={styles.priorityText}>{goal.priority.toUpperCase()}</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{daysRemaining !== null && !isCompleted && (
|
||||||
|
<Text style={[styles.daysRemaining, daysRemaining < 0 && styles.overdue]}>
|
||||||
|
{daysRemaining < 0
|
||||||
|
? `${Math.abs(daysRemaining)} days overdue`
|
||||||
|
: `${daysRemaining} days remaining`
|
||||||
|
}
|
||||||
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<View style={styles.footer}>
|
{isCompleted && goal.completedDate && (
|
||||||
<LinearGradient
|
<Text style={styles.completedDate}>
|
||||||
colors={getPriorityGradient(goal.priority)}
|
Completed {new Date(goal.completedDate).toLocaleDateString()}
|
||||||
style={styles.priorityBadge}
|
</Text>
|
||||||
>
|
)}
|
||||||
<Text style={styles.priorityText}>{goal.priority.toUpperCase()}</Text>
|
</View>
|
||||||
</LinearGradient>
|
|
||||||
|
|
||||||
{daysRemaining !== null && !isCompleted && (
|
|
||||||
<Text style={[styles.daysRemaining, daysRemaining < 0 && styles.overdue]}>
|
|
||||||
{daysRemaining < 0
|
|
||||||
? `${Math.abs(daysRemaining)} days overdue`
|
|
||||||
: `${daysRemaining} days remaining`
|
|
||||||
}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isCompleted && goal.completedDate && (
|
|
||||||
<Text style={styles.completedDate}>
|
|
||||||
Completed {new Date(goal.completedDate).toLocaleDateString()}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</LinearGradient>
|
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
card: {
|
card: {
|
||||||
borderRadius: theme.borderRadius.xl,
|
backgroundColor: '#fff',
|
||||||
|
borderRadius: 12,
|
||||||
padding: 16,
|
padding: 16,
|
||||||
marginBottom: 12,
|
marginBottom: 12,
|
||||||
borderWidth: 1,
|
shadowColor: '#000',
|
||||||
borderColor: 'rgba(59, 130, 246, 0.1)',
|
shadowOffset: { width: 0, height: 2 },
|
||||||
overflow: 'hidden',
|
shadowOpacity: 0.1,
|
||||||
|
shadowRadius: 4,
|
||||||
|
elevation: 3,
|
||||||
},
|
},
|
||||||
cardCompleted: {
|
cardCompleted: {
|
||||||
borderColor: 'rgba(16, 185, 129, 0.2)',
|
backgroundColor: '#f0fdf4',
|
||||||
},
|
borderColor: '#bbf7d0',
|
||||||
priorityAccent: {
|
borderWidth: 1,
|
||||||
position: 'absolute',
|
|
||||||
left: 0,
|
|
||||||
top: 0,
|
|
||||||
bottom: 0,
|
|
||||||
width: 4,
|
|
||||||
},
|
},
|
||||||
header: {
|
header: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
alignItems: 'flex-start',
|
alignItems: 'flex-start',
|
||||||
marginBottom: 12,
|
marginBottom: 12,
|
||||||
marginLeft: 8,
|
|
||||||
},
|
},
|
||||||
titleRow: {
|
titleRow: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'flex-start',
|
alignItems: 'flex-start',
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
iconContainer: {
|
|
||||||
width: 40,
|
|
||||||
height: 40,
|
|
||||||
borderRadius: 20,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginRight: 12,
|
|
||||||
},
|
|
||||||
titleContainer: {
|
titleContainer: {
|
||||||
|
marginLeft: 12,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
fontSize: theme.typography.fontSize.lg,
|
fontSize: 18,
|
||||||
fontWeight: theme.typography.fontWeight.semibold,
|
fontWeight: '600',
|
||||||
color: theme.colors.gray900,
|
color: '#111827',
|
||||||
marginBottom: 4,
|
marginBottom: 4,
|
||||||
},
|
},
|
||||||
titleCompleted: {
|
titleCompleted: {
|
||||||
color: theme.colors.gray600,
|
color: '#9ca3af',
|
||||||
textDecorationLine: 'line-through',
|
textDecorationLine: 'line-through',
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
fontSize: theme.typography.fontSize.sm,
|
fontSize: 14,
|
||||||
color: theme.colors.gray600,
|
color: '#6b7280',
|
||||||
lineHeight: 18,
|
lineHeight: 20,
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
@ -233,7 +190,6 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
progressSection: {
|
progressSection: {
|
||||||
marginBottom: 12,
|
marginBottom: 12,
|
||||||
marginLeft: 8,
|
|
||||||
},
|
},
|
||||||
progressInfo: {
|
progressInfo: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
@ -241,53 +197,55 @@ const styles = StyleSheet.create({
|
|||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
},
|
},
|
||||||
progressText: {
|
progressText: {
|
||||||
fontSize: theme.typography.fontSize.sm,
|
fontSize: 14,
|
||||||
fontWeight: theme.typography.fontWeight.medium,
|
fontWeight: '500',
|
||||||
color: theme.colors.gray700,
|
color: '#374151',
|
||||||
},
|
},
|
||||||
progressPercentage: {
|
progressPercentage: {
|
||||||
fontSize: theme.typography.fontSize.sm,
|
fontSize: 14,
|
||||||
fontWeight: theme.typography.fontWeight.semibold,
|
fontWeight: '600',
|
||||||
color: theme.colors.primary,
|
color: '#2563eb',
|
||||||
},
|
},
|
||||||
progressBarContainer: {
|
progressBarContainer: {
|
||||||
height: 8,
|
height: 8,
|
||||||
backgroundColor: theme.colors.gray200,
|
backgroundColor: '#e5e7eb',
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
},
|
},
|
||||||
progressBar: {
|
progressBar: {
|
||||||
height: '100%',
|
height: '100%',
|
||||||
|
backgroundColor: '#2563eb',
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
},
|
},
|
||||||
|
progressBarCompleted: {
|
||||||
|
backgroundColor: '#10b981',
|
||||||
|
},
|
||||||
footer: {
|
footer: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
marginLeft: 8,
|
|
||||||
},
|
},
|
||||||
priorityBadge: {
|
priorityBadge: {
|
||||||
paddingHorizontal: 10,
|
paddingHorizontal: 8,
|
||||||
paddingVertical: 5,
|
paddingVertical: 4,
|
||||||
borderRadius: theme.borderRadius.md,
|
borderRadius: 4,
|
||||||
},
|
},
|
||||||
priorityText: {
|
priorityText: {
|
||||||
fontSize: theme.typography.fontSize.xs,
|
fontSize: 10,
|
||||||
fontWeight: theme.typography.fontWeight.semibold,
|
fontWeight: '600',
|
||||||
color: theme.colors.white,
|
color: '#fff',
|
||||||
},
|
},
|
||||||
daysRemaining: {
|
daysRemaining: {
|
||||||
fontSize: theme.typography.fontSize.xs,
|
fontSize: 12,
|
||||||
color: theme.colors.gray600,
|
color: '#6b7280',
|
||||||
fontWeight: theme.typography.fontWeight.medium,
|
|
||||||
},
|
},
|
||||||
overdue: {
|
overdue: {
|
||||||
color: theme.colors.danger,
|
color: '#ef4444',
|
||||||
fontWeight: theme.typography.fontWeight.semibold,
|
fontWeight: '600',
|
||||||
},
|
},
|
||||||
completedDate: {
|
completedDate: {
|
||||||
fontSize: theme.typography.fontSize.xs,
|
fontSize: 12,
|
||||||
color: theme.colors.success,
|
color: '#10b981',
|
||||||
fontWeight: theme.typography.fontWeight.medium,
|
fontWeight: '500',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,46 +0,0 @@
|
|||||||
/**
|
|
||||||
* Gradient Background Component
|
|
||||||
* Reusable gradient background with multiple presets
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import { StyleSheet, ViewStyle } from 'react-native';
|
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
|
||||||
import { theme } from '../styles/theme';
|
|
||||||
|
|
||||||
interface GradientBackgroundProps {
|
|
||||||
variant?: 'primary' | 'success' | 'warning' | 'danger' | 'purple' | 'ocean' | 'sunset' | 'dark';
|
|
||||||
colors?: string[];
|
|
||||||
style?: ViewStyle;
|
|
||||||
children?: React.ReactNode;
|
|
||||||
start?: { x: number; y: number };
|
|
||||||
end?: { x: number; y: number };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function GradientBackground({
|
|
||||||
variant = 'primary',
|
|
||||||
colors,
|
|
||||||
style,
|
|
||||||
children,
|
|
||||||
start = { x: 0, y: 0 },
|
|
||||||
end = { x: 1, y: 1 },
|
|
||||||
}: GradientBackgroundProps) {
|
|
||||||
const gradientColors = (colors || theme.gradients[variant]) as readonly [string, string, ...string[]];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<LinearGradient
|
|
||||||
colors={gradientColors}
|
|
||||||
start={start}
|
|
||||||
end={end}
|
|
||||||
style={[styles.gradient, style]}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</LinearGradient>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
gradient: {
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@ -1,131 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { StyleSheet, View, Animated, Dimensions, ImageSourcePropType } from 'react-native';
|
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
|
||||||
import { theme } from '../styles/theme';
|
|
||||||
|
|
||||||
const { width } = Dimensions.get('window');
|
|
||||||
const HEADER_HEIGHT = 300;
|
|
||||||
|
|
||||||
interface ParallaxScrollViewProps {
|
|
||||||
children: React.ReactNode;
|
|
||||||
headerImage?: ImageSourcePropType;
|
|
||||||
headerBackgroundColor?: string;
|
|
||||||
headerContent?: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ParallaxScrollView({
|
|
||||||
children,
|
|
||||||
headerImage,
|
|
||||||
headerBackgroundColor = theme.colors.primary,
|
|
||||||
headerContent,
|
|
||||||
}: ParallaxScrollViewProps) {
|
|
||||||
const scrollY = React.useRef(new Animated.Value(0)).current;
|
|
||||||
|
|
||||||
const headerTranslateY = scrollY.interpolate({
|
|
||||||
inputRange: [0, HEADER_HEIGHT],
|
|
||||||
outputRange: [0, -HEADER_HEIGHT / 2],
|
|
||||||
extrapolate: 'clamp',
|
|
||||||
});
|
|
||||||
|
|
||||||
const imageScale = scrollY.interpolate({
|
|
||||||
inputRange: [-HEADER_HEIGHT, 0, HEADER_HEIGHT],
|
|
||||||
outputRange: [2, 1, 1],
|
|
||||||
extrapolate: 'clamp',
|
|
||||||
});
|
|
||||||
|
|
||||||
const headerOpacity = scrollY.interpolate({
|
|
||||||
inputRange: [0, HEADER_HEIGHT / 2],
|
|
||||||
outputRange: [1, 0],
|
|
||||||
extrapolate: 'clamp',
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View style={styles.container}>
|
|
||||||
<Animated.ScrollView
|
|
||||||
contentContainerStyle={styles.scrollContent}
|
|
||||||
onScroll={Animated.event(
|
|
||||||
[{ nativeEvent: { contentOffset: { y: scrollY } } }],
|
|
||||||
{ useNativeDriver: true }
|
|
||||||
)}
|
|
||||||
scrollEventThrottle={16}
|
|
||||||
showsVerticalScrollIndicator={false}
|
|
||||||
>
|
|
||||||
<View style={styles.contentContainer}>{children}</View>
|
|
||||||
</Animated.ScrollView>
|
|
||||||
|
|
||||||
<Animated.View
|
|
||||||
style={[
|
|
||||||
styles.header,
|
|
||||||
{
|
|
||||||
transform: [{ translateY: headerTranslateY }],
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Animated.View
|
|
||||||
style={[
|
|
||||||
StyleSheet.absoluteFill,
|
|
||||||
{
|
|
||||||
transform: [{ scale: imageScale }],
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<LinearGradient
|
|
||||||
colors={theme.gradients.primary}
|
|
||||||
style={StyleSheet.absoluteFill}
|
|
||||||
start={{ x: 0, y: 0 }}
|
|
||||||
end={{ x: 1, y: 1 }}
|
|
||||||
/>
|
|
||||||
{headerImage && (
|
|
||||||
<Animated.Image
|
|
||||||
source={headerImage}
|
|
||||||
style={[styles.headerImage, { opacity: 0.5 }]}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Animated.View>
|
|
||||||
|
|
||||||
<Animated.View style={[styles.headerContent, { opacity: headerOpacity }]}>
|
|
||||||
{headerContent}
|
|
||||||
</Animated.View>
|
|
||||||
</Animated.View>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
flex: 1,
|
|
||||||
backgroundColor: theme.colors.background,
|
|
||||||
},
|
|
||||||
scrollContent: {
|
|
||||||
paddingTop: HEADER_HEIGHT,
|
|
||||||
},
|
|
||||||
contentContainer: {
|
|
||||||
backgroundColor: theme.colors.background,
|
|
||||||
borderTopLeftRadius: 32,
|
|
||||||
borderTopRightRadius: 32,
|
|
||||||
marginTop: -32,
|
|
||||||
paddingTop: 32,
|
|
||||||
minHeight: 800,
|
|
||||||
},
|
|
||||||
header: {
|
|
||||||
position: 'absolute',
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
height: HEADER_HEIGHT,
|
|
||||||
overflow: 'hidden',
|
|
||||||
zIndex: 1,
|
|
||||||
},
|
|
||||||
headerImage: {
|
|
||||||
...StyleSheet.absoluteFillObject,
|
|
||||||
width: undefined,
|
|
||||||
height: undefined,
|
|
||||||
resizeMode: 'cover',
|
|
||||||
},
|
|
||||||
headerContent: {
|
|
||||||
flex: 1,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
paddingBottom: 32,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@ -1,114 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { View, Text, StyleSheet, TouchableOpacity, Dimensions } from 'react-native';
|
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
|
||||||
import { theme } from '../styles/theme';
|
|
||||||
|
|
||||||
const { width } = Dimensions.get('window');
|
|
||||||
const ITEM_WIDTH = (width - 40 - 16) / 2; // (Screen width - padding - gap) / 2
|
|
||||||
|
|
||||||
interface QuickActionProps {
|
|
||||||
icon: keyof typeof Ionicons.glyphMap;
|
|
||||||
label: string;
|
|
||||||
gradient: readonly [string, string, ...string[]];
|
|
||||||
onPress?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function QuickActionItem({ icon, label, gradient, onPress }: QuickActionProps) {
|
|
||||||
return (
|
|
||||||
<TouchableOpacity onPress={onPress} activeOpacity={0.8} style={styles.itemContainer}>
|
|
||||||
<LinearGradient
|
|
||||||
colors={['rgba(255, 255, 255, 0.8)', 'rgba(255, 255, 255, 0.6)']}
|
|
||||||
style={[styles.item, theme.shadows.subtle]}
|
|
||||||
>
|
|
||||||
<LinearGradient
|
|
||||||
colors={gradient}
|
|
||||||
style={styles.iconContainer}
|
|
||||||
start={{ x: 0, y: 0 }}
|
|
||||||
end={{ x: 1, y: 1 }}
|
|
||||||
>
|
|
||||||
<Ionicons name={icon} size={24} color="#fff" />
|
|
||||||
</LinearGradient>
|
|
||||||
<Text style={styles.label}>{label}</Text>
|
|
||||||
<Ionicons name="chevron-forward" size={16} color={theme.colors.gray400} style={styles.arrow} />
|
|
||||||
</LinearGradient>
|
|
||||||
</TouchableOpacity>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function QuickActionGrid() {
|
|
||||||
return (
|
|
||||||
<View style={styles.container}>
|
|
||||||
<Text style={styles.sectionTitle}>Quick Actions</Text>
|
|
||||||
<View style={styles.grid}>
|
|
||||||
<QuickActionItem
|
|
||||||
icon="barbell"
|
|
||||||
label="Log Workout"
|
|
||||||
gradient={theme.gradients.primary}
|
|
||||||
/>
|
|
||||||
<QuickActionItem
|
|
||||||
icon="restaurant"
|
|
||||||
label="Track Meal"
|
|
||||||
gradient={theme.gradients.success}
|
|
||||||
/>
|
|
||||||
<QuickActionItem
|
|
||||||
icon="water"
|
|
||||||
label="Add Water"
|
|
||||||
gradient={theme.gradients.ocean}
|
|
||||||
/>
|
|
||||||
<QuickActionItem
|
|
||||||
icon="scan"
|
|
||||||
label="Scan Food"
|
|
||||||
gradient={theme.gradients.purple}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
paddingHorizontal: 20,
|
|
||||||
marginBottom: 24,
|
|
||||||
},
|
|
||||||
sectionTitle: {
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: '700',
|
|
||||||
color: theme.colors.gray900,
|
|
||||||
marginBottom: 16,
|
|
||||||
},
|
|
||||||
grid: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
flexWrap: 'wrap',
|
|
||||||
gap: 16,
|
|
||||||
},
|
|
||||||
itemContainer: {
|
|
||||||
width: ITEM_WIDTH,
|
|
||||||
},
|
|
||||||
item: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
padding: 16,
|
|
||||||
borderRadius: 20,
|
|
||||||
backgroundColor: '#fff',
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: 'rgba(255, 255, 255, 0.6)',
|
|
||||||
},
|
|
||||||
iconContainer: {
|
|
||||||
width: 40,
|
|
||||||
height: 40,
|
|
||||||
borderRadius: 14,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginRight: 12,
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: '600',
|
|
||||||
color: theme.colors.gray800,
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
arrow: {
|
|
||||||
opacity: 0.5,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@ -34,8 +34,8 @@ export interface CreateGoalData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class FitnessGoalsService {
|
export class FitnessGoalsService {
|
||||||
private async getAuthHeaders(token: string | null): Promise<any> {
|
private async getAuthHeaders(token: string | null): Promise<HeadersInit> {
|
||||||
const headers: any = {
|
const headers: HeadersInit = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,191 +0,0 @@
|
|||||||
/**
|
|
||||||
* Modern Design System Theme
|
|
||||||
* Centralized theme configuration with gradients, colors, shadows, and spacing
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const theme = {
|
|
||||||
// Color Palette
|
|
||||||
colors: {
|
|
||||||
// Primary colors
|
|
||||||
primary: '#3b82f6',
|
|
||||||
primaryDark: '#2563eb',
|
|
||||||
primaryLight: '#60a5fa',
|
|
||||||
secondary: '#8b5cf6',
|
|
||||||
|
|
||||||
// Accent colors
|
|
||||||
purple: '#8b5cf6',
|
|
||||||
purpleDark: '#7c3aed',
|
|
||||||
pink: '#ec4899',
|
|
||||||
|
|
||||||
// Success
|
|
||||||
success: '#10b981',
|
|
||||||
successDark: '#059669',
|
|
||||||
successLight: '#34d399',
|
|
||||||
|
|
||||||
// Warning
|
|
||||||
warning: '#f59e0b',
|
|
||||||
warningDark: '#d97706',
|
|
||||||
|
|
||||||
// Danger
|
|
||||||
danger: '#ef4444',
|
|
||||||
dangerDark: '#dc2626',
|
|
||||||
|
|
||||||
// Neutrals
|
|
||||||
white: '#ffffff',
|
|
||||||
black: '#000000',
|
|
||||||
gray50: '#f9fafb',
|
|
||||||
gray100: '#f3f4f6',
|
|
||||||
gray200: '#e5e7eb',
|
|
||||||
gray300: '#d1d5db',
|
|
||||||
gray400: '#9ca3af',
|
|
||||||
gray500: '#6b7280',
|
|
||||||
gray600: '#4b5563',
|
|
||||||
gray700: '#374151',
|
|
||||||
gray800: '#1f2937',
|
|
||||||
gray900: '#111827',
|
|
||||||
|
|
||||||
// Backgrounds
|
|
||||||
background: '#f5f5f5',
|
|
||||||
backgroundDark: '#0f172a',
|
|
||||||
surface: '#ffffff',
|
|
||||||
surfaceDark: '#1e293b',
|
|
||||||
},
|
|
||||||
|
|
||||||
// Gradient Definitions
|
|
||||||
gradients: {
|
|
||||||
primary: ['#3b82f6', '#8b5cf6'] as const,
|
|
||||||
primaryVertical: ['#3b82f6', '#2563eb'] as const,
|
|
||||||
success: ['#10b981', '#059669'] as const,
|
|
||||||
warning: ['#f59e0b', '#d97706'] as const,
|
|
||||||
danger: ['#ef4444', '#ec4899'] as const,
|
|
||||||
purple: ['#8b5cf6', '#7c3aed'] as const,
|
|
||||||
ocean: ['#06b6d4', '#3b82f6'] as const,
|
|
||||||
sunset: ['#f59e0b', '#ef4444'] as const,
|
|
||||||
forest: ['#10b981', '#059669'] as const,
|
|
||||||
lavender: ['#a78bfa', '#ec4899'] as const,
|
|
||||||
dark: ['#1e293b', '#0f172a'] as const,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Shadow System
|
|
||||||
shadows: {
|
|
||||||
subtle: {
|
|
||||||
shadowColor: '#000',
|
|
||||||
shadowOffset: { width: 0, height: 1 },
|
|
||||||
shadowOpacity: 0.05,
|
|
||||||
shadowRadius: 2,
|
|
||||||
elevation: 2,
|
|
||||||
},
|
|
||||||
medium: {
|
|
||||||
shadowColor: '#000',
|
|
||||||
shadowOffset: { width: 0, height: 2 },
|
|
||||||
shadowOpacity: 0.1,
|
|
||||||
shadowRadius: 4,
|
|
||||||
elevation: 3,
|
|
||||||
},
|
|
||||||
strong: {
|
|
||||||
shadowColor: '#000',
|
|
||||||
shadowOffset: { width: 0, height: 4 },
|
|
||||||
shadowOpacity: 0.15,
|
|
||||||
shadowRadius: 8,
|
|
||||||
elevation: 5,
|
|
||||||
},
|
|
||||||
glow: {
|
|
||||||
shadowColor: '#3b82f6',
|
|
||||||
shadowOffset: { width: 0, height: 4 },
|
|
||||||
shadowOpacity: 0.3,
|
|
||||||
shadowRadius: 12,
|
|
||||||
elevation: 8,
|
|
||||||
},
|
|
||||||
glowDanger: {
|
|
||||||
shadowColor: '#ef4444',
|
|
||||||
shadowOffset: { width: 0, height: 4 },
|
|
||||||
shadowOpacity: 0.3,
|
|
||||||
shadowRadius: 12,
|
|
||||||
elevation: 8,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
// Typography
|
|
||||||
typography: {
|
|
||||||
// Font sizes
|
|
||||||
fontSize: {
|
|
||||||
xs: 12,
|
|
||||||
sm: 14,
|
|
||||||
base: 16,
|
|
||||||
lg: 18,
|
|
||||||
xl: 20,
|
|
||||||
'2xl': 24,
|
|
||||||
'3xl': 28,
|
|
||||||
'4xl': 32,
|
|
||||||
'5xl': 36,
|
|
||||||
},
|
|
||||||
// Font weights
|
|
||||||
fontWeight: {
|
|
||||||
normal: '400' as const,
|
|
||||||
medium: '500' as const,
|
|
||||||
semibold: '600' as const,
|
|
||||||
bold: '700' as const,
|
|
||||||
extrabold: '800' as const,
|
|
||||||
},
|
|
||||||
// Line heights
|
|
||||||
lineHeight: {
|
|
||||||
tight: 1.2,
|
|
||||||
normal: 1.5,
|
|
||||||
relaxed: 1.75,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
// Spacing Scale
|
|
||||||
spacing: {
|
|
||||||
xs: 4,
|
|
||||||
sm: 8,
|
|
||||||
md: 12,
|
|
||||||
lg: 16,
|
|
||||||
xl: 20,
|
|
||||||
'2xl': 24,
|
|
||||||
'3xl': 32,
|
|
||||||
'4xl': 40,
|
|
||||||
'5xl': 48,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Border Radius
|
|
||||||
borderRadius: {
|
|
||||||
sm: 4,
|
|
||||||
md: 8,
|
|
||||||
lg: 12,
|
|
||||||
xl: 16,
|
|
||||||
'2xl': 20,
|
|
||||||
'3xl': 24,
|
|
||||||
full: 9999,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Animation Timing
|
|
||||||
animation: {
|
|
||||||
duration: {
|
|
||||||
fast: 150,
|
|
||||||
normal: 250,
|
|
||||||
slow: 350,
|
|
||||||
},
|
|
||||||
easing: {
|
|
||||||
easeIn: 'ease-in',
|
|
||||||
easeOut: 'ease-out',
|
|
||||||
easeInOut: 'ease-in-out',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
// Glassmorphism
|
|
||||||
glass: {
|
|
||||||
light: {
|
|
||||||
backgroundColor: 'rgba(255, 255, 255, 0.7)',
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: 'rgba(255, 255, 255, 0.3)',
|
|
||||||
},
|
|
||||||
dark: {
|
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.3)',
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: 'rgba(255, 255, 255, 0.1)',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Theme = typeof theme;
|
|
||||||
@ -1,149 +0,0 @@
|
|||||||
/**
|
|
||||||
* Reusable Animation Utilities
|
|
||||||
* Pre-configured animations for consistent motion design
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Animated, Easing } from 'react-native';
|
|
||||||
|
|
||||||
export const animations = {
|
|
||||||
// Fade animations
|
|
||||||
fadeIn: (animatedValue: Animated.Value, duration = 250) => {
|
|
||||||
return Animated.timing(animatedValue, {
|
|
||||||
toValue: 1,
|
|
||||||
duration,
|
|
||||||
easing: Easing.out(Easing.ease),
|
|
||||||
useNativeDriver: true,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
fadeOut: (animatedValue: Animated.Value, duration = 250) => {
|
|
||||||
return Animated.timing(animatedValue, {
|
|
||||||
toValue: 0,
|
|
||||||
duration,
|
|
||||||
easing: Easing.in(Easing.ease),
|
|
||||||
useNativeDriver: true,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
// Scale animations
|
|
||||||
scaleIn: (animatedValue: Animated.Value, duration = 250) => {
|
|
||||||
return Animated.spring(animatedValue, {
|
|
||||||
toValue: 1,
|
|
||||||
friction: 8,
|
|
||||||
tension: 40,
|
|
||||||
useNativeDriver: true,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
scaleOut: (animatedValue: Animated.Value, duration = 200) => {
|
|
||||||
return Animated.timing(animatedValue, {
|
|
||||||
toValue: 0,
|
|
||||||
duration,
|
|
||||||
easing: Easing.in(Easing.ease),
|
|
||||||
useNativeDriver: true,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
// Press animation (scale down slightly)
|
|
||||||
pressIn: (animatedValue: Animated.Value) => {
|
|
||||||
return Animated.spring(animatedValue, {
|
|
||||||
toValue: 0.95,
|
|
||||||
friction: 8,
|
|
||||||
tension: 100,
|
|
||||||
useNativeDriver: true,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
pressOut: (animatedValue: Animated.Value) => {
|
|
||||||
return Animated.spring(animatedValue, {
|
|
||||||
toValue: 1,
|
|
||||||
friction: 8,
|
|
||||||
tension: 100,
|
|
||||||
useNativeDriver: true,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
// Slide animations
|
|
||||||
slideInUp: (animatedValue: Animated.Value, duration = 300) => {
|
|
||||||
return Animated.timing(animatedValue, {
|
|
||||||
toValue: 0,
|
|
||||||
duration,
|
|
||||||
easing: Easing.out(Easing.cubic),
|
|
||||||
useNativeDriver: true,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
slideOutDown: (animatedValue: Animated.Value, duration = 250) => {
|
|
||||||
return Animated.timing(animatedValue, {
|
|
||||||
toValue: 100,
|
|
||||||
duration,
|
|
||||||
easing: Easing.in(Easing.cubic),
|
|
||||||
useNativeDriver: true,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
// Pulse animation (for FAB or notifications)
|
|
||||||
pulse: (animatedValue: Animated.Value) => {
|
|
||||||
return Animated.sequence([
|
|
||||||
Animated.timing(animatedValue, {
|
|
||||||
toValue: 1.1,
|
|
||||||
duration: 150,
|
|
||||||
easing: Easing.out(Easing.ease),
|
|
||||||
useNativeDriver: true,
|
|
||||||
}),
|
|
||||||
Animated.timing(animatedValue, {
|
|
||||||
toValue: 1,
|
|
||||||
duration: 150,
|
|
||||||
easing: Easing.in(Easing.ease),
|
|
||||||
useNativeDriver: true,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
},
|
|
||||||
|
|
||||||
// Stagger animation for lists
|
|
||||||
stagger: (animations: Animated.CompositeAnimation[], delay = 50) => {
|
|
||||||
return Animated.stagger(delay, animations);
|
|
||||||
},
|
|
||||||
|
|
||||||
// Parallel animations
|
|
||||||
parallel: (animations: Animated.CompositeAnimation[]) => {
|
|
||||||
return Animated.parallel(animations);
|
|
||||||
},
|
|
||||||
|
|
||||||
// Sequence animations
|
|
||||||
sequence: (animations: Animated.CompositeAnimation[]) => {
|
|
||||||
return Animated.sequence(animations);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Spring configurations
|
|
||||||
export const springConfig = {
|
|
||||||
gentle: {
|
|
||||||
friction: 10,
|
|
||||||
tension: 40,
|
|
||||||
},
|
|
||||||
bouncy: {
|
|
||||||
friction: 5,
|
|
||||||
tension: 40,
|
|
||||||
},
|
|
||||||
stiff: {
|
|
||||||
friction: 8,
|
|
||||||
tension: 100,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Timing configurations
|
|
||||||
export const timingConfig = {
|
|
||||||
fast: {
|
|
||||||
duration: 150,
|
|
||||||
easing: Easing.out(Easing.ease),
|
|
||||||
},
|
|
||||||
normal: {
|
|
||||||
duration: 250,
|
|
||||||
easing: Easing.out(Easing.ease),
|
|
||||||
},
|
|
||||||
slow: {
|
|
||||||
duration: 350,
|
|
||||||
easing: Easing.out(Easing.ease),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
import * as Haptics from 'expo-haptics';
|
|
||||||
import { Platform } from 'react-native';
|
|
||||||
|
|
||||||
export const haptics = {
|
|
||||||
light: () => {
|
|
||||||
if (Platform.OS !== 'web') {
|
|
||||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
medium: () => {
|
|
||||||
if (Platform.OS !== 'web') {
|
|
||||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
heavy: () => {
|
|
||||||
if (Platform.OS !== 'web') {
|
|
||||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
success: () => {
|
|
||||||
if (Platform.OS !== 'web') {
|
|
||||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
warning: () => {
|
|
||||||
if (Platform.OS !== 'web') {
|
|
||||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
error: () => {
|
|
||||||
if (Platform.OS !== 'web') {
|
|
||||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
selection: () => {
|
|
||||||
if (Platform.OS !== 'web') {
|
|
||||||
Haptics.selectionAsync();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
Loading…
Reference in New Issue
Block a user