refinenments
This commit is contained in:
parent
5d6166df1b
commit
064dafad57
Binary file not shown.
@ -258,58 +258,95 @@ export default function GoalsScreen() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Analytics Section */}
|
{/* Analytics Section */}
|
||||||
{statistics && (
|
{statistics &&
|
||||||
|
(statistics.weeklyTrend.length > 0 ||
|
||||||
|
statistics.goals.goalsByType.length > 0) && (
|
||||||
<View style={styles.section}>
|
<View style={styles.section}>
|
||||||
<MinimalCard variant="default">
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.analyticsHeader}
|
|
||||||
onPress={() => setShowAnalytics(!showAnalytics)}
|
onPress={() => setShowAnalytics(!showAnalytics)}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.85}
|
||||||
>
|
>
|
||||||
|
<MinimalCard variant="elevated" style={styles.analyticsCard}>
|
||||||
|
<View style={styles.analyticsHeader}>
|
||||||
<View style={styles.analyticsHeaderLeft}>
|
<View style={styles.analyticsHeaderLeft}>
|
||||||
<Ionicons
|
<View
|
||||||
name="bar-chart-outline"
|
|
||||||
size={20}
|
|
||||||
color={colors.primary}
|
|
||||||
/>
|
|
||||||
<Text
|
|
||||||
style={[
|
style={[
|
||||||
typography.h3,
|
styles.analyticsIcon,
|
||||||
{ color: colors.textPrimary, marginLeft: 8 },
|
{ backgroundColor: `${colors.primary}15` },
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
📈 Progress Analytics
|
<Ionicons
|
||||||
|
name="bar-chart"
|
||||||
|
size={24}
|
||||||
|
color={colors.primary}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View>
|
||||||
|
<Text
|
||||||
|
style={[typography.h3, { color: colors.textPrimary }]}
|
||||||
|
>
|
||||||
|
Progress Analytics
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
typography.caption,
|
||||||
|
{ color: colors.textTertiary, marginTop: 2 },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{showAnalytics ? "Tap to collapse" : "Tap to expand"}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.analyticsToggle,
|
||||||
|
{ backgroundColor: colors.surfaceElevated },
|
||||||
|
]}
|
||||||
|
>
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name={showAnalytics ? "chevron-up" : "chevron-down"}
|
name={showAnalytics ? "chevron-up" : "chevron-down"}
|
||||||
size={20}
|
size={20}
|
||||||
color={colors.textTertiary}
|
color={colors.textSecondary}
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
{showAnalytics && (
|
{showAnalytics && (
|
||||||
<View
|
<View style={styles.analyticsContent}>
|
||||||
|
{statistics.weeklyTrend.length > 0 && (
|
||||||
|
<View style={styles.chartSection}>
|
||||||
|
<Text
|
||||||
style={[
|
style={[
|
||||||
styles.analyticsContent,
|
typography.h4,
|
||||||
{ borderTopColor: colors.border },
|
{ color: colors.textPrimary, marginBottom: 16 },
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{statistics.weeklyTrend.length > 0 && (
|
Weekly Trend
|
||||||
|
</Text>
|
||||||
<WeeklyProgressChart
|
<WeeklyProgressChart
|
||||||
weeklyData={statistics.weeklyTrend}
|
weeklyData={statistics.weeklyTrend}
|
||||||
title="8-Week Trend"
|
|
||||||
/>
|
/>
|
||||||
|
</View>
|
||||||
)}
|
)}
|
||||||
{statistics.goals.goalsByType.length > 0 && (
|
{statistics.goals.goalsByType.length > 0 && (
|
||||||
|
<View style={styles.chartSection}>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
typography.h4,
|
||||||
|
{ color: colors.textPrimary, marginBottom: 16 },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
Goals by Type
|
||||||
|
</Text>
|
||||||
<GoalTypeBreakdownChart
|
<GoalTypeBreakdownChart
|
||||||
data={statistics.goals.goalsByType}
|
data={statistics.goals.goalsByType}
|
||||||
title="Goals by Type"
|
|
||||||
/>
|
/>
|
||||||
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</MinimalCard>
|
</MinimalCard>
|
||||||
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -439,6 +476,9 @@ const styles = StyleSheet.create({
|
|||||||
paddingHorizontal: 12,
|
paddingHorizontal: 12,
|
||||||
borderRadius: 20,
|
borderRadius: 20,
|
||||||
},
|
},
|
||||||
|
analyticsCard: {
|
||||||
|
padding: 20,
|
||||||
|
},
|
||||||
analyticsHeader: {
|
analyticsHeader: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
@ -448,13 +488,32 @@ const styles = StyleSheet.create({
|
|||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
},
|
},
|
||||||
|
analyticsIcon: {
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
borderRadius: 14,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
marginRight: 14,
|
||||||
|
},
|
||||||
|
analyticsToggle: {
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
borderRadius: 10,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
analyticsContent: {
|
analyticsContent: {
|
||||||
paddingTop: 16,
|
paddingTop: 24,
|
||||||
marginTop: 16,
|
marginTop: 20,
|
||||||
borderTopWidth: 1,
|
borderTopWidth: 1,
|
||||||
|
borderTopColor: "rgba(0,0,0,0.05)",
|
||||||
|
},
|
||||||
|
chartSection: {
|
||||||
|
marginBottom: 20,
|
||||||
},
|
},
|
||||||
goalsList: {
|
goalsList: {
|
||||||
gap: 12,
|
gap: 16,
|
||||||
},
|
},
|
||||||
emptyState: {
|
emptyState: {
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import { MinimalButton } from "../../components/MinimalButton";
|
|||||||
import { Badge } from "../../components/Badge";
|
import { Badge } from "../../components/Badge";
|
||||||
import { IconContainer } from "../../components/IconContainer";
|
import { IconContainer } from "../../components/IconContainer";
|
||||||
import { API_BASE_URL, API_ENDPOINTS } from "../../config/api";
|
import { API_BASE_URL, API_ENDPOINTS } from "../../config/api";
|
||||||
|
import { fitnessProfileApi, FitnessProfile } from "../../api/fitnessProfile";
|
||||||
import log from "../../utils/logger";
|
import log from "../../utils/logger";
|
||||||
|
|
||||||
export default function ProfileScreen() {
|
export default function ProfileScreen() {
|
||||||
@ -35,6 +36,11 @@ export default function ProfileScreen() {
|
|||||||
const [selectedGymId, setSelectedGymId] = useState<string | null>(null);
|
const [selectedGymId, setSelectedGymId] = useState<string | null>(null);
|
||||||
const [currentGymId, setCurrentGymId] = useState<string | null>(null);
|
const [currentGymId, setCurrentGymId] = useState<string | null>(null);
|
||||||
const [currentGymName, setCurrentGymName] = useState<string | null>(null);
|
const [currentGymName, setCurrentGymName] = useState<string | null>(null);
|
||||||
|
const [showGymDropdown, setShowGymDropdown] = useState(false);
|
||||||
|
const [fitnessProfile, setFitnessProfile] = useState<FitnessProfile | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
const [profileLoading, setProfileLoading] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const gid =
|
const gid =
|
||||||
@ -49,8 +55,23 @@ export default function ProfileScreen() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadGyms();
|
loadGyms();
|
||||||
|
loadFitnessProfile();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const loadFitnessProfile = async () => {
|
||||||
|
try {
|
||||||
|
setProfileLoading(true);
|
||||||
|
const token = await getToken();
|
||||||
|
if (!token) return;
|
||||||
|
const profile = await fitnessProfileApi.getFitnessProfile(token);
|
||||||
|
setFitnessProfile(profile);
|
||||||
|
} catch (error) {
|
||||||
|
log.error("Failed to load fitness profile", error);
|
||||||
|
} finally {
|
||||||
|
setProfileLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const loadGyms = async () => {
|
const loadGyms = async () => {
|
||||||
try {
|
try {
|
||||||
setGymsLoading(true);
|
setGymsLoading(true);
|
||||||
@ -115,8 +136,12 @@ export default function ProfileScreen() {
|
|||||||
const handleApplyGym = async () => {
|
const handleApplyGym = async () => {
|
||||||
try {
|
try {
|
||||||
const token = await getToken();
|
const token = await getToken();
|
||||||
const url = `${API_BASE_URL}${API_ENDPOINTS.USERS}/gym`;
|
const url = `${API_BASE_URL}${API_ENDPOINTS.USERS.GYM}`;
|
||||||
log.debug("Updating gym selection", { url, gymId: selectedGymId });
|
log.debug("Updating gym selection", {
|
||||||
|
url,
|
||||||
|
gymId: selectedGymId,
|
||||||
|
token: token ? "present" : "missing",
|
||||||
|
});
|
||||||
const res = await fetch(url, {
|
const res = await fetch(url, {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
headers: {
|
headers: {
|
||||||
@ -331,29 +356,145 @@ export default function ProfileScreen() {
|
|||||||
onPress={() => router.push("/personal-details")}
|
onPress={() => router.push("/personal-details")}
|
||||||
/>
|
/>
|
||||||
<View style={[styles.divider, { backgroundColor: colors.border }]} />
|
<View style={[styles.divider, { backgroundColor: colors.border }]} />
|
||||||
<ListItem
|
|
||||||
title="Fitness Profile"
|
{/* Fitness Profile Card */}
|
||||||
leftIcon={
|
<TouchableOpacity
|
||||||
<IconContainer
|
onPress={() => router.push("/fitness-profile")}
|
||||||
variant="colored"
|
activeOpacity={0.85}
|
||||||
backgroundColor={`${colors.success}20`}
|
|
||||||
>
|
>
|
||||||
<Ionicons
|
<MinimalCard variant="elevated" style={styles.fitnessProfileCard}>
|
||||||
name="fitness-outline"
|
<View style={styles.fitnessProfileHeader}>
|
||||||
size={20}
|
<View
|
||||||
color={colors.success}
|
style={[
|
||||||
|
styles.fitnessProfileIcon,
|
||||||
|
{ backgroundColor: colors.success },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Ionicons name="fitness" size={24} color={colors.white} />
|
||||||
|
</View>
|
||||||
|
<View style={{ flex: 1, marginLeft: 14 }}>
|
||||||
|
<Text style={[typography.h4, { color: colors.textPrimary }]}>
|
||||||
|
Fitness Profile
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
typography.caption,
|
||||||
|
{ color: colors.textTertiary, marginTop: 2 },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{fitnessProfile ? "Tap to edit" : "Set up your profile"}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.editButton,
|
||||||
|
{ backgroundColor: colors.primary },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Ionicons name="pencil" size={16} color={colors.white} />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{profileLoading ? (
|
||||||
|
<View style={styles.profileLoading}>
|
||||||
|
<ActivityIndicator color={colors.primary} />
|
||||||
|
</View>
|
||||||
|
) : fitnessProfile ? (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.fitnessProfileStats,
|
||||||
|
{ borderTopColor: colors.border },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<View style={styles.profileStat}>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
typography.statLarge,
|
||||||
|
{ color: colors.primary, fontSize: 28 },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{fitnessProfile.height || "-"}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
typography.caption,
|
||||||
|
{ color: colors.textTertiary },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
Height (cm)
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.profileStatDivider,
|
||||||
|
{ backgroundColor: colors.border },
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
</IconContainer>
|
<View style={styles.profileStat}>
|
||||||
}
|
<Text
|
||||||
rightElement={
|
style={[
|
||||||
<Ionicons
|
typography.statLarge,
|
||||||
name="chevron-forward"
|
{ color: colors.success, fontSize: 28 },
|
||||||
size={20}
|
]}
|
||||||
color={colors.textTertiary}
|
>
|
||||||
|
{fitnessProfile.weight || "-"}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
typography.caption,
|
||||||
|
{ color: colors.textTertiary },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
Weight (kg)
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.profileStatDivider,
|
||||||
|
{ backgroundColor: colors.border },
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
}
|
<View style={styles.profileStat}>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
typography.statLarge,
|
||||||
|
{ color: colors.warning, fontSize: 28 },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{fitnessProfile.age || "-"}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
typography.caption,
|
||||||
|
{ color: colors.textTertiary },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
Age
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<View
|
||||||
|
style={[styles.noProfile, { borderTopColor: colors.border }]}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={[typography.body, { color: colors.textSecondary }]}
|
||||||
|
>
|
||||||
|
Complete your fitness profile to get personalized
|
||||||
|
recommendations
|
||||||
|
</Text>
|
||||||
|
<MinimalButton
|
||||||
|
title="Set Up Profile"
|
||||||
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
style={{ marginTop: 12 }}
|
||||||
onPress={() => router.push("/fitness-profile")}
|
onPress={() => router.push("/fitness-profile")}
|
||||||
/>
|
/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</MinimalCard>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
<View style={[styles.divider, { backgroundColor: colors.border }]} />
|
<View style={[styles.divider, { backgroundColor: colors.border }]} />
|
||||||
<ListItem
|
<ListItem
|
||||||
title="Notifications"
|
title="Notifications"
|
||||||
@ -382,24 +523,22 @@ export default function ProfileScreen() {
|
|||||||
|
|
||||||
{/* Gym Selection */}
|
{/* Gym Selection */}
|
||||||
<View style={styles.section}>
|
<View style={styles.section}>
|
||||||
<View style={styles.sectionHeader}>
|
|
||||||
<Text style={[typography.h3, { color: colors.textPrimary }]}>
|
|
||||||
Gym Selection
|
|
||||||
</Text>
|
|
||||||
<TouchableOpacity onPress={loadGyms}>
|
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
typography.body,
|
typography.h3,
|
||||||
{ color: colors.primary, fontWeight: "600" },
|
{ color: colors.textPrimary, marginBottom: 12 },
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
Refresh
|
Gym Selection
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
<TouchableOpacity
|
||||||
<MinimalCard variant="default">
|
onPress={() => setShowGymDropdown(!showGymDropdown)}
|
||||||
{currentGymName && (
|
activeOpacity={0.85}
|
||||||
<View style={styles.currentGym}>
|
>
|
||||||
|
<MinimalCard variant="bordered" style={styles.dropdownCard}>
|
||||||
|
<View style={styles.dropdownHeader}>
|
||||||
|
<View>
|
||||||
<Text
|
<Text
|
||||||
style={[typography.caption, { color: colors.textTertiary }]}
|
style={[typography.caption, { color: colors.textTertiary }]}
|
||||||
>
|
>
|
||||||
@ -407,40 +546,48 @@ export default function ProfileScreen() {
|
|||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
typography.h3,
|
typography.bodyEmphasis,
|
||||||
{ color: colors.textPrimary, marginTop: 4 },
|
{ color: colors.textPrimary, marginTop: 2 },
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{currentGymName}
|
{currentGymName || "No gym selected"}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
<View
|
||||||
|
style={[
|
||||||
|
styles.dropdownIcon,
|
||||||
|
{ backgroundColor: colors.surfaceElevated },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Ionicons
|
||||||
|
name={showGymDropdown ? "chevron-up" : "chevron-down"}
|
||||||
|
size={20}
|
||||||
|
color={colors.textSecondary}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</MinimalCard>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
{showGymDropdown && (
|
||||||
|
<MinimalCard variant="elevated" style={styles.dropdownOptions}>
|
||||||
{gymsLoading ? (
|
{gymsLoading ? (
|
||||||
<View style={styles.loadingContainer}>
|
<View style={styles.loadingContainer}>
|
||||||
<ActivityIndicator color={colors.primary} />
|
<ActivityIndicator color={colors.primary} />
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<ScrollView
|
|
||||||
horizontal
|
|
||||||
showsHorizontalScrollIndicator={false}
|
|
||||||
style={styles.gymScroll}
|
|
||||||
contentContainerStyle={styles.gymScrollContent}
|
|
||||||
>
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
styles.gymChip,
|
styles.dropdownOption,
|
||||||
{
|
selectedGymId === null && {
|
||||||
backgroundColor:
|
backgroundColor: `${colors.primary}15`,
|
||||||
selectedGymId === null
|
|
||||||
? `${colors.primary}20`
|
|
||||||
: colors.surfaceElevated,
|
|
||||||
borderColor:
|
|
||||||
selectedGymId === null ? colors.primary : colors.border,
|
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
onPress={() => setSelectedGymId(null)}
|
onPress={() => {
|
||||||
|
setSelectedGymId(null);
|
||||||
|
setShowGymDropdown(false);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
@ -456,25 +603,29 @@ export default function ProfileScreen() {
|
|||||||
>
|
>
|
||||||
No Gym
|
No Gym
|
||||||
</Text>
|
</Text>
|
||||||
|
{selectedGymId === null && (
|
||||||
|
<Ionicons
|
||||||
|
name="checkmark"
|
||||||
|
size={20}
|
||||||
|
color={colors.primary}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
{gyms.map((gym) => (
|
{gyms.map((gym) => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
key={gym.id}
|
key={gym.id}
|
||||||
style={[
|
style={[
|
||||||
styles.gymChip,
|
styles.dropdownOption,
|
||||||
{
|
selectedGymId === gym.id && {
|
||||||
backgroundColor:
|
backgroundColor: `${colors.primary}15`,
|
||||||
selectedGymId === gym.id
|
|
||||||
? `${colors.primary}20`
|
|
||||||
: colors.surfaceElevated,
|
|
||||||
borderColor:
|
|
||||||
selectedGymId === gym.id
|
|
||||||
? colors.primary
|
|
||||||
: colors.border,
|
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
onPress={() => setSelectedGymId(gym.id)}
|
onPress={() => {
|
||||||
|
setSelectedGymId(gym.id);
|
||||||
|
setShowGymDropdown(false);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
|
<View style={{ flex: 1 }}>
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
typography.body,
|
typography.body,
|
||||||
@ -483,24 +634,47 @@ export default function ProfileScreen() {
|
|||||||
selectedGymId === gym.id
|
selectedGymId === gym.id
|
||||||
? colors.primary
|
? colors.primary
|
||||||
: colors.textSecondary,
|
: colors.textSecondary,
|
||||||
fontWeight: selectedGymId === gym.id ? "600" : "400",
|
fontWeight:
|
||||||
|
selectedGymId === gym.id ? "600" : "400",
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{gym.name}
|
{gym.name}
|
||||||
</Text>
|
</Text>
|
||||||
|
{gym.location && (
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
typography.caption,
|
||||||
|
{ color: colors.textTertiary, marginTop: 2 },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{gym.location}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
{selectedGymId === gym.id && (
|
||||||
|
<Ionicons
|
||||||
|
name="checkmark"
|
||||||
|
size={20}
|
||||||
|
color={colors.primary}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
))}
|
))}
|
||||||
</ScrollView>
|
{selectedGymId !== currentGymId && (
|
||||||
<MinimalButton
|
<MinimalButton
|
||||||
title="Apply Selection"
|
title="Apply Selection"
|
||||||
onPress={handleApplyGym}
|
onPress={handleApplyGym}
|
||||||
variant="primary"
|
variant="primary"
|
||||||
style={{ marginTop: 16 }}
|
size="lg"
|
||||||
|
fullWidth
|
||||||
|
style={{ marginTop: 12 }}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</MinimalCard>
|
</MinimalCard>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Support */}
|
{/* Support */}
|
||||||
@ -651,4 +825,78 @@ const styles = StyleSheet.create({
|
|||||||
borderWidth: 1.5,
|
borderWidth: 1.5,
|
||||||
marginRight: 8,
|
marginRight: 8,
|
||||||
},
|
},
|
||||||
|
dropdownCard: {
|
||||||
|
padding: 16,
|
||||||
|
},
|
||||||
|
dropdownHeader: {
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
dropdownIcon: {
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
borderRadius: 10,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
dropdownOptions: {
|
||||||
|
marginTop: 8,
|
||||||
|
padding: 8,
|
||||||
|
},
|
||||||
|
dropdownOption: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
paddingVertical: 14,
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
borderRadius: 12,
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
fitnessProfileCard: {
|
||||||
|
padding: 16,
|
||||||
|
},
|
||||||
|
fitnessProfileHeader: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
fitnessProfileIcon: {
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
borderRadius: 14,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
editButton: {
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
borderRadius: 8,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
profileLoading: {
|
||||||
|
paddingVertical: 20,
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
fitnessProfileStats: {
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "space-around",
|
||||||
|
marginTop: 20,
|
||||||
|
paddingTop: 16,
|
||||||
|
borderTopWidth: 1,
|
||||||
|
},
|
||||||
|
profileStat: {
|
||||||
|
alignItems: "center",
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
profileStatDivider: {
|
||||||
|
width: 1,
|
||||||
|
height: 40,
|
||||||
|
},
|
||||||
|
noProfile: {
|
||||||
|
marginTop: 16,
|
||||||
|
paddingTop: 16,
|
||||||
|
borderTopWidth: 1,
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { View, Text, StyleSheet, Dimensions } from "react-native";
|
import { View, Text, StyleSheet, Dimensions } from "react-native";
|
||||||
import { PieChart } from "react-native-chart-kit";
|
import { PieChart } from "react-native-chart-kit";
|
||||||
import { theme } from "../styles/theme";
|
import { useTheme } from "../contexts/ThemeContext";
|
||||||
|
|
||||||
interface GoalTypeData {
|
interface GoalTypeData {
|
||||||
goalType: string;
|
goalType: string;
|
||||||
@ -17,37 +17,40 @@ export function GoalTypeBreakdownChart({
|
|||||||
data,
|
data,
|
||||||
title = "Goals by Type",
|
title = "Goals by Type",
|
||||||
}: GoalTypeBreakdownChartProps) {
|
}: GoalTypeBreakdownChartProps) {
|
||||||
|
const { colors, typography } = useTheme();
|
||||||
const screenWidth = Dimensions.get("window").width;
|
const screenWidth = Dimensions.get("window").width;
|
||||||
|
|
||||||
// Color palette for different goal types
|
const chartColors = [
|
||||||
const colors = [
|
colors.primary,
|
||||||
"#3b82f6", // Blue
|
colors.success,
|
||||||
"#10b981", // Green
|
colors.warning,
|
||||||
"#f59e0b", // Orange
|
colors.accent,
|
||||||
"#8b5cf6", // Purple
|
colors.secondary,
|
||||||
"#ec4899", // Pink
|
colors.info,
|
||||||
"#06b6d4", // Cyan
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// Prepare chart data
|
|
||||||
const chartData = data.map((item, index) => ({
|
const chartData = data.map((item, index) => ({
|
||||||
name: item.goalType,
|
name: item.goalType
|
||||||
|
.replace(/_/g, " ")
|
||||||
|
.replace(/\b\w/g, (l) => l.toUpperCase()),
|
||||||
count: item.count,
|
count: item.count,
|
||||||
color: colors[index % colors.length],
|
color: chartColors[index % chartColors.length],
|
||||||
legendFontColor: theme.colors.gray600,
|
legendFontColor: colors.textSecondary,
|
||||||
legendFontSize: 12,
|
legendFontSize: 12,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const chartConfig = {
|
const totalGoals = data.reduce((sum, item) => sum + item.count, 0);
|
||||||
color: (opacity = 1) => `rgba(59, 130, 246, ${opacity})`,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (data.length === 0) {
|
if (data.length === 0) {
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Text style={styles.title}>{title}</Text>
|
<Text style={[typography.h4, { color: colors.textPrimary }]}>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
<View style={styles.emptyState}>
|
<View style={styles.emptyState}>
|
||||||
<Text style={styles.emptyText}>No goals yet</Text>
|
<Text style={[typography.body, { color: colors.textTertiary }]}>
|
||||||
|
No goals yet
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
@ -55,46 +58,123 @@ export function GoalTypeBreakdownChart({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Text style={styles.title}>{title}</Text>
|
<Text
|
||||||
|
style={[typography.h4, { color: colors.textPrimary, marginBottom: 16 }]}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{/* Summary Stats */}
|
||||||
|
<View style={styles.summaryRow}>
|
||||||
|
<View style={styles.summaryItem}>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
typography.statLarge,
|
||||||
|
{ color: colors.primary, fontSize: 32 },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{totalGoals}
|
||||||
|
</Text>
|
||||||
|
<Text style={[typography.caption, { color: colors.textTertiary }]}>
|
||||||
|
Total Goals
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.summaryItem}>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
typography.statLarge,
|
||||||
|
{ color: colors.success, fontSize: 32 },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{data.length}
|
||||||
|
</Text>
|
||||||
|
<Text style={[typography.caption, { color: colors.textTertiary }]}>
|
||||||
|
Types
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
<View style={styles.chartContainer}>
|
<View style={styles.chartContainer}>
|
||||||
<PieChart
|
<PieChart
|
||||||
data={chartData}
|
data={chartData}
|
||||||
width={screenWidth - 60}
|
width={screenWidth - 80}
|
||||||
height={200}
|
height={160}
|
||||||
chartConfig={chartConfig}
|
chartConfig={{
|
||||||
|
color: (opacity = 1) => `rgba(0, 0, 0, ${opacity})`,
|
||||||
|
}}
|
||||||
accessor="count"
|
accessor="count"
|
||||||
backgroundColor="transparent"
|
backgroundColor="transparent"
|
||||||
paddingLeft="15"
|
paddingLeft="15"
|
||||||
absolute
|
absolute
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* Legend */}
|
||||||
|
<View style={styles.legendGrid}>
|
||||||
|
{data.map((item, index) => (
|
||||||
|
<View key={item.goalType} style={styles.legendItem}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.legendDot,
|
||||||
|
{ backgroundColor: chartColors[index % chartColors.length] },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
typography.caption,
|
||||||
|
{ color: colors.textSecondary, flex: 1 },
|
||||||
|
]}
|
||||||
|
numberOfLines={1}
|
||||||
|
>
|
||||||
|
{item.goalType
|
||||||
|
.replace(/_/g, " ")
|
||||||
|
.replace(/\b\w/g, (l) => l.toUpperCase())}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={[typography.bodyEmphasis, { color: colors.textPrimary }]}
|
||||||
|
>
|
||||||
|
{item.count}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
backgroundColor: theme.colors.white,
|
borderRadius: 16,
|
||||||
borderRadius: theme.borderRadius.xl,
|
padding: 4,
|
||||||
padding: 16,
|
|
||||||
marginBottom: 16,
|
|
||||||
...theme.shadows.medium,
|
|
||||||
},
|
},
|
||||||
title: {
|
summaryRow: {
|
||||||
fontSize: theme.typography.fontSize.lg,
|
flexDirection: "row",
|
||||||
fontWeight: theme.typography.fontWeight.bold,
|
justifyContent: "space-around",
|
||||||
color: theme.colors.gray700,
|
marginBottom: 16,
|
||||||
marginBottom: 12,
|
paddingVertical: 12,
|
||||||
|
},
|
||||||
|
summaryItem: {
|
||||||
|
alignItems: "center",
|
||||||
},
|
},
|
||||||
chartContainer: {
|
chartContainer: {
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
marginBottom: 16,
|
||||||
|
},
|
||||||
|
legendGrid: {
|
||||||
|
gap: 10,
|
||||||
|
},
|
||||||
|
legendItem: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 10,
|
||||||
|
},
|
||||||
|
legendDot: {
|
||||||
|
width: 12,
|
||||||
|
height: 12,
|
||||||
|
borderRadius: 6,
|
||||||
},
|
},
|
||||||
emptyState: {
|
emptyState: {
|
||||||
paddingVertical: 40,
|
paddingVertical: 32,
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
},
|
},
|
||||||
emptyText: {
|
|
||||||
fontSize: theme.typography.fontSize.sm,
|
|
||||||
color: theme.colors.gray400,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { View, Text, StyleSheet, Dimensions } from "react-native";
|
import { View, Text, StyleSheet, Dimensions } from "react-native";
|
||||||
import { LineChart } from "react-native-chart-kit";
|
import { LineChart } from "react-native-chart-kit";
|
||||||
import { theme } from "../styles/theme";
|
import { useTheme } from "../contexts/ThemeContext";
|
||||||
import type { WeeklyTrendData } from "../api/types";
|
import type { WeeklyTrendData } from "../api/types";
|
||||||
|
|
||||||
interface WeeklyProgressChartProps {
|
interface WeeklyProgressChartProps {
|
||||||
@ -13,28 +13,26 @@ export function WeeklyProgressChart({
|
|||||||
weeklyData,
|
weeklyData,
|
||||||
title = "Weekly Progress",
|
title = "Weekly Progress",
|
||||||
}: WeeklyProgressChartProps) {
|
}: WeeklyProgressChartProps) {
|
||||||
|
const { colors, typography } = useTheme();
|
||||||
const screenWidth = Dimensions.get("window").width;
|
const screenWidth = Dimensions.get("window").width;
|
||||||
|
|
||||||
// Prepare chart data
|
const labels = weeklyData.map((week: WeeklyTrendData) => week.weekLabel);
|
||||||
const labels = weeklyData.map((week) => week.weekLabel);
|
const checkInsData = weeklyData.map((week: WeeklyTrendData) => week.checkIns);
|
||||||
const checkInsData = weeklyData.map((week) => week.checkIns);
|
const goalsCompletedData = weeklyData.map(
|
||||||
const goalsCompletedData = weeklyData.map((week) => week.goalsCompleted);
|
(week: WeeklyTrendData) => week.goalsCompleted,
|
||||||
const avgProgressData = weeklyData.map((week) => week.averageProgress);
|
);
|
||||||
|
|
||||||
const chartConfig = {
|
const chartConfig = {
|
||||||
backgroundColor: theme.colors.white,
|
backgroundColor: colors.surface,
|
||||||
backgroundGradientFrom: theme.colors.white,
|
backgroundGradientFrom: colors.surface,
|
||||||
backgroundGradientTo: theme.colors.white,
|
backgroundGradientTo: colors.surface,
|
||||||
decimalPlaces: 0,
|
decimalPlaces: 0,
|
||||||
color: (opacity = 1) => `rgba(59, 130, 246, ${opacity})`,
|
color: (opacity = 1) => `rgba(0, 102, 255, ${opacity})`,
|
||||||
labelColor: (opacity = 1) => `rgba(107, 114, 128, ${opacity})`,
|
labelColor: (opacity = 1) => colors.textTertiary,
|
||||||
style: {
|
|
||||||
borderRadius: theme.borderRadius.lg,
|
|
||||||
},
|
|
||||||
propsForDots: {
|
propsForDots: {
|
||||||
r: "4",
|
r: "5",
|
||||||
strokeWidth: "2",
|
strokeWidth: "2",
|
||||||
stroke: theme.colors.primary,
|
stroke: colors.primary,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -43,31 +41,40 @@ export function WeeklyProgressChart({
|
|||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
data: checkInsData,
|
data: checkInsData,
|
||||||
color: (opacity = 1) => `rgba(59, 130, 246, ${opacity})`, // Blue for check-ins
|
color: () => colors.primary,
|
||||||
strokeWidth: 2,
|
strokeWidth: 3,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
data: goalsCompletedData,
|
data: goalsCompletedData,
|
||||||
color: (opacity = 1) => `rgba(16, 185, 129, ${opacity})`, // Green for goals
|
color: () => colors.success,
|
||||||
strokeWidth: 2,
|
strokeWidth: 3,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
legend: ["Check-ins", "Goals Completed"],
|
legend: ["Check-ins", "Goals"],
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Text style={styles.title}>{title}</Text>
|
{title && (
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
typography.h4,
|
||||||
|
{ color: colors.textPrimary, marginBottom: 16 },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
<View style={styles.chartContainer}>
|
<View style={styles.chartContainer}>
|
||||||
<LineChart
|
<LineChart
|
||||||
data={data}
|
data={data}
|
||||||
width={screenWidth - 60}
|
width={screenWidth - 80}
|
||||||
height={220}
|
height={180}
|
||||||
chartConfig={chartConfig}
|
chartConfig={chartConfig}
|
||||||
bezier
|
bezier
|
||||||
style={styles.chart}
|
style={styles.chart}
|
||||||
withInnerLines={true}
|
withInnerLines={true}
|
||||||
withOuterLines={true}
|
withOuterLines={false}
|
||||||
withVerticalLabels={true}
|
withVerticalLabels={true}
|
||||||
withHorizontalLabels={true}
|
withHorizontalLabels={true}
|
||||||
fromZero={true}
|
fromZero={true}
|
||||||
@ -75,12 +82,20 @@ export function WeeklyProgressChart({
|
|||||||
</View>
|
</View>
|
||||||
<View style={styles.legend}>
|
<View style={styles.legend}>
|
||||||
<View style={styles.legendItem}>
|
<View style={styles.legendItem}>
|
||||||
<View style={[styles.legendDot, { backgroundColor: "#3b82f6" }]} />
|
<View
|
||||||
<Text style={styles.legendText}>Check-ins</Text>
|
style={[styles.legendDot, { backgroundColor: colors.primary }]}
|
||||||
|
/>
|
||||||
|
<Text style={[typography.caption, { color: colors.textSecondary }]}>
|
||||||
|
Check-ins
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.legendItem}>
|
<View style={styles.legendItem}>
|
||||||
<View style={[styles.legendDot, { backgroundColor: "#10b981" }]} />
|
<View
|
||||||
<Text style={styles.legendText}>Goals Completed</Text>
|
style={[styles.legendDot, { backgroundColor: colors.success }]}
|
||||||
|
/>
|
||||||
|
<Text style={[typography.caption, { color: colors.textSecondary }]}>
|
||||||
|
Goals
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@ -89,17 +104,8 @@ export function WeeklyProgressChart({
|
|||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
backgroundColor: theme.colors.white,
|
borderRadius: 16,
|
||||||
borderRadius: theme.borderRadius.xl,
|
padding: 4,
|
||||||
padding: 16,
|
|
||||||
marginBottom: 16,
|
|
||||||
...theme.shadows.medium,
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
fontSize: theme.typography.fontSize.lg,
|
|
||||||
fontWeight: theme.typography.fontWeight.bold,
|
|
||||||
color: theme.colors.gray700,
|
|
||||||
marginBottom: 12,
|
|
||||||
},
|
},
|
||||||
chartContainer: {
|
chartContainer: {
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
@ -107,26 +113,22 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
chart: {
|
chart: {
|
||||||
marginVertical: 8,
|
marginVertical: 8,
|
||||||
borderRadius: theme.borderRadius.lg,
|
borderRadius: 12,
|
||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
gap: 20,
|
gap: 24,
|
||||||
paddingTop: 8,
|
paddingTop: 8,
|
||||||
},
|
},
|
||||||
legendItem: {
|
legendItem: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: 6,
|
gap: 8,
|
||||||
},
|
},
|
||||||
legendDot: {
|
legendDot: {
|
||||||
width: 10,
|
width: 10,
|
||||||
height: 10,
|
height: 10,
|
||||||
borderRadius: 5,
|
borderRadius: 5,
|
||||||
},
|
},
|
||||||
legendText: {
|
|
||||||
fontSize: theme.typography.fontSize.sm,
|
|
||||||
color: theme.colors.gray500,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|||||||
@ -35,6 +35,7 @@ export const API_ENDPOINTS = {
|
|||||||
USERS: {
|
USERS: {
|
||||||
LIST: "/api/users",
|
LIST: "/api/users",
|
||||||
STATISTICS: "/api/users/statistics",
|
STATISTICS: "/api/users/statistics",
|
||||||
|
GYM: "/api/users/gym",
|
||||||
},
|
},
|
||||||
GYMS: "/api/gyms",
|
GYMS: "/api/gyms",
|
||||||
ATTENDANCE: {
|
ATTENDANCE: {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user