diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000..61ef9aa16c Binary files /dev/null and b/.DS_Store differ diff --git a/.env b/.env index 1d44286e25..b3a51cce41 100644 --- a/.env +++ b/.env @@ -5,22 +5,27 @@ DOMAIN=localhost # To test the local Traefik config # DOMAIN=localhost.tiangolo.com + # Used by the backend to generate links in emails to the frontend FRONTEND_HOST=http://localhost:5173 # In staging and production, set this env var to the frontend host, e.g. # FRONTEND_HOST=https://dashboard.example.com + # Environment: local, staging, production ENVIRONMENT=local + PROJECT_NAME="Full Stack FastAPI Project" STACK_NAME=full-stack-fastapi-project + # Backend -BACKEND_CORS_ORIGINS="http://localhost,http://localhost:5173,https://localhost,https://localhost:5173,http://localhost.tiangolo.com" -SECRET_KEY=changethis +BACKEND_CORS_ORIGINS="http://localhost,http://localhost:5173,http://localhost:8081,https://localhost,https://localhost:5173,https://localhost:8081,http://localhost.tiangolo.com" +SECRET_KEY=VfIGsNDeZkTOiSHIgoG9DjhpTyaLGBb-lZvYa8-wbTM FIRST_SUPERUSER=admin@example.com -FIRST_SUPERUSER_PASSWORD=changethis +FIRST_SUPERUSER_PASSWORD=ierjXObp0qpZq82J + # Emails SMTP_HOST= @@ -31,15 +36,21 @@ SMTP_TLS=True SMTP_SSL=False SMTP_PORT=587 + # Postgres POSTGRES_SERVER=localhost POSTGRES_PORT=5432 POSTGRES_DB=app POSTGRES_USER=postgres -POSTGRES_PASSWORD=changethis +POSTGRES_PASSWORD=SP52qKZTPKFTpi0w7adJJS80G4BpGDC4X2uxH5H7FYY + SENTRY_DSN= + # Configure these with your own Docker registry images DOCKER_IMAGE_BACKEND=backend DOCKER_IMAGE_FRONTEND=frontend + + + diff --git a/.expo/README.md b/.expo/README.md new file mode 100644 index 0000000000..fd146b4d3a --- /dev/null +++ b/.expo/README.md @@ -0,0 +1,15 @@ +> Why do I have a folder named ".expo" in my project? + +The ".expo" folder is created when an Expo project is started using "expo start" command. + +> What do the files contain? + +- "devices.json": contains information about devices that have recently opened this project. This is used to populate the "Development sessions" list in your development builds. +- "packager-info.json": contains port numbers and process PIDs that are used to serve the application to the mobile device/simulator. +- "settings.json": contains the server configuration that is used to serve the application manifest. + +> Should I commit the ".expo" folder? + +No, you should not share the ".expo" folder. It does not contain any information that is relevant for other developers working on the project, it is specific to your machine. + +Upon project creation, the ".expo" folder is already added to your ".gitignore" file. diff --git a/.expo/settings.json b/.expo/settings.json new file mode 100644 index 0000000000..92bc513bfd --- /dev/null +++ b/.expo/settings.json @@ -0,0 +1,8 @@ +{ + "hostType": "lan", + "lanType": "ip", + "dev": true, + "minify": false, + "urlRandomness": null, + "https": false +} diff --git a/KonditionExpo/.gitignore b/KonditionExpo/.gitignore new file mode 100644 index 0000000000..f610ec0d64 --- /dev/null +++ b/KonditionExpo/.gitignore @@ -0,0 +1,39 @@ +# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files + +# dependencies +node_modules/ + +# Expo +.expo/ +dist/ +web-build/ +expo-env.d.ts + +# Native +.kotlin/ +*.orig.* +*.jks +*.p8 +*.p12 +*.key +*.mobileprovision + +# Metro +.metro-health-check* + +# debug +npm-debug.* +yarn-debug.* +yarn-error.* + +# macOS +.DS_Store +*.pem + +# local env files +.env*.local + +# typescript +*.tsbuildinfo + +app-example diff --git a/KonditionExpo/README.md b/KonditionExpo/README.md new file mode 100644 index 0000000000..48dd63ff3e --- /dev/null +++ b/KonditionExpo/README.md @@ -0,0 +1,50 @@ +# Welcome to your Expo app πŸ‘‹ + +This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app). + +## Get started + +1. Install dependencies + + ```bash + npm install + ``` + +2. Start the app + + ```bash + npx expo start + ``` + +In the output, you'll find options to open the app in a + +- [development build](https://docs.expo.dev/develop/development-builds/introduction/) +- [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/) +- [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/) +- [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo + +You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction). + +## Get a fresh project + +When you're ready, run: + +```bash +npm run reset-project +``` + +This command will move the starter code to the **app-example** directory and create a blank **app** directory where you can start developing. + +## Learn more + +To learn more about developing your project with Expo, look at the following resources: + +- [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides). +- [Learn Expo tutorial](https://docs.expo.dev/tutorial/introduction/): Follow a step-by-step tutorial where you'll create a project that runs on Android, iOS, and the web. + +## Join the community + +Join our community of developers creating universal apps. + +- [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute. +- [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions. diff --git a/KonditionExpo/TESTING_GUIDE.md b/KonditionExpo/TESTING_GUIDE.md new file mode 100644 index 0000000000..7d406c883d --- /dev/null +++ b/KonditionExpo/TESTING_GUIDE.md @@ -0,0 +1,177 @@ +# Authentication Testing Guide + +## Quick Start + +### 1. Start the Backend + +**Option A: Using Docker Compose (Recommended)** +```bash +cd KonditionFastAPI +docker compose watch +``` + +**Option B: Direct Python** +```bash +cd KonditionFastAPI +uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload +``` + +### 2. Test on Different Platforms + +#### Web (Should work immediately) +```bash +cd KonditionExpo +npm run web +``` + +#### iOS Simulator (Should work immediately) +```bash +npm run ios +``` + +#### Android Emulator (Should work immediately) +```bash +npm run android +``` + +#### Physical Device / Expo Go +1. Find your computer's IP address: + - **macOS**: `ifconfig | grep "inet " | grep -v 127.0.0.1` + - **Windows**: `ipconfig` + - **Linux**: `ip addr show` + +2. Start Expo: + ```bash + npm start + ``` + +3. Open the app and go to **Profile > Developer Tools** + +4. Set custom URL: `http://[YOUR_IP]:8000/api/v1` + +5. Test the connection using the "Test API Connection" button + +## Testing Checklist + +### Signout Functionality +- [ ] Click signout button in profile screen +- [ ] Confirmation dialog appears +- [ ] Loading state shows "Signing Out..." +- [ ] Successfully redirects to login screen +- [ ] No errors in console + +### Network Connectivity +- [ ] Web platform connects to localhost:8000 +- [ ] iOS Simulator connects to localhost:8000 +- [ ] Android Emulator connects to 10.0.2.2:8000 +- [ ] Physical device connects with custom IP +- [ ] Error messages are user-friendly + +### DevTools +- [ ] Developer Tools accessible from profile +- [ ] Shows current API URL +- [ ] Connection test works +- [ ] Custom URL can be set +- [ ] Quick URL buttons work + +## Common Issues & Solutions + +### "Network request failed" +1. Check if backend is running: `curl http://localhost:8000/docs` +2. Verify backend is accessible externally: `curl http://[YOUR_IP]:8000/docs` +3. Check firewall settings +4. Ensure devices are on same network + +### Signout not working +1. Check console for error messages +2. Clear app data/cache +3. Restart the app +4. Check AsyncStorage permissions + +### DevTools not showing +1. Make sure you're on the Profile tab +2. Scroll down to "Other" section +3. Look for blue "Developer Tools" text + +## Debug Information + +The app now logs detailed information to help with debugging: + +``` +Platform detected: ios (ios_simulator) +Using API URL: http://localhost:8000/api/v1 +Making request to: http://localhost:8000/api/v1/login/access-token +Starting logout process... +Logout successful +``` + +Check the console/logs for these messages to understand what's happening. + +## Network Setup for Mobile Testing + +### Step 1: Find Your IP +```bash +# macOS/Linux +ifconfig | grep "inet " | grep -v 127.0.0.1 + +# Windows +ipconfig +``` + +### Step 2: Start Backend with External Access +```bash +uvicorn app.main:app --host 0.0.0.0 --port 8000 +``` + +### Step 3: Configure Mobile App +1. Open app on mobile device +2. Go to Profile > Developer Tools +3. Enter: `http://[YOUR_IP]:8000/api/v1` +4. Test connection + +### Step 4: Verify Setup +- Backend accessible: `http://[YOUR_IP]:8000/docs` +- API working: Test connection in DevTools +- Authentication: Try login/logout + +## Platform-Specific Notes + +### Web +- Uses localhost:8000 automatically +- No additional setup required +- Best for initial development + +### iOS Simulator +- Uses localhost:8000 automatically +- Behaves like web platform +- Good for iOS-specific testing + +### Android Emulator +- Uses 10.0.2.2:8000 automatically +- Special IP for Android emulator networking +- No manual configuration needed + +### Physical Device +- Requires manual IP configuration +- Use DevTools for easy setup +- Must be on same network as computer + +## Success Indicators + +βœ… **Authentication Working**: +- Login redirects to main app +- Logout shows confirmation and redirects to login +- User data loads correctly +- No network errors + +βœ… **Platform Compatibility**: +- Web works with localhost +- Mobile works with appropriate URLs +- Error messages are helpful +- DevTools provide easy testing + +βœ… **User Experience**: +- Loading states show during auth actions +- Clear error messages for failures +- Smooth navigation between screens +- No crashes or freezes \ No newline at end of file diff --git a/KonditionExpo/app.json b/KonditionExpo/app.json new file mode 100644 index 0000000000..d44ba2be29 --- /dev/null +++ b/KonditionExpo/app.json @@ -0,0 +1,53 @@ +{ + "expo": { + "name": "KonditionExpo", + "slug": "KonditionExpo", + "version": "1.0.0", + "orientation": "portrait", + "icon": "./assets/images/icon.png", + "scheme": "konditionexpo", + "userInterfaceStyle": "automatic", + "newArchEnabled": true, + "notification": { + "vapidPublicKey": "BHIYUSgOOx7MC1EXPT5HSZ6snrLWCe4uV2tloytm3uB8rZtTiNl5234SLOWPzdBa8TcIr1CubVkh_8jXipesK8E", + "serviceWorkerPath": "service-worker.js" + }, + "ios": { + "supportsTablet": true + }, + "android": { + "adaptiveIcon": { + "foregroundImage": "./assets/images/adaptive-icon.png", + "backgroundColor": "#ffffff" + }, + "edgeToEdgeEnabled": true + }, + "web": { + "bundler": "metro", + "output": "static", + "favicon": "./assets/images/favicon.png" + }, + "plugins": [ + "expo-router", + [ + "expo-splash-screen", + { + "image": "./assets/images/splash-icon.png", + "imageWidth": 200, + "resizeMode": "contain", + "backgroundColor": "#ffffff" + } + ], + [ + "expo-notifications", + { + "icon": "./assets/images/bell.png", + "color": "#ffffff" + } + ] + ], + "experiments": { + "typedRoutes": true + } + } +} diff --git a/KonditionExpo/app/(tabs)/_layout.tsx b/KonditionExpo/app/(tabs)/_layout.tsx new file mode 100644 index 0000000000..bb910c7a4e --- /dev/null +++ b/KonditionExpo/app/(tabs)/_layout.tsx @@ -0,0 +1,73 @@ +import { Tabs } from 'expo-router'; +import React from 'react'; +import { Platform } from 'react-native'; + +import { HapticTab } from '@/components/HapticTab'; +import { IconSymbol } from '@/components/ui/IconSymbol'; +import TabBarBackground from '@/components/ui/TabBarBackground'; +import { Colors } from '@/constants/Colors'; +import { useColorScheme } from '@/hooks/useColorScheme'; + +export default function TabLayout() { + const colorScheme = useColorScheme(); + + return ( + + , + }} + /> + , + }} + /> + , + }} + /> + , + }} + /> + , + }} + /> + , + }} + /> + + ); +} diff --git a/KonditionExpo/app/(tabs)/explore.tsx b/KonditionExpo/app/(tabs)/explore.tsx new file mode 100644 index 0000000000..1e3f34ba8d --- /dev/null +++ b/KonditionExpo/app/(tabs)/explore.tsx @@ -0,0 +1,147 @@ +import { StyleSheet, ScrollView, View } from 'react-native'; +import { Ionicons } from '@expo/vector-icons'; + +import { ThemedText } from '@/components/ThemedText'; +import { ThemedView } from '@/components/ThemedView'; +import { useThemeColor } from '@/hooks/useThemeColor'; + +interface FeatureItemProps { + number: number; + title: string; + icon: keyof typeof Ionicons.glyphMap; +} + +const FeatureItem = ({ number, title, icon }: FeatureItemProps) => { + const tintColor = useThemeColor({}, 'tint'); + + return ( + + + {number} + + + {title} + + + + ); +}; + +export default function ExploreScreen() { + const backgroundColor = useThemeColor({}, 'background'); + + return ( + + + + FEATURES + + + Explore all the features Kondition has to offer + + + + + + + + + + + + + + + + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + headerContainer: { + padding: 20, + paddingTop: 60, + alignItems: 'center', + marginBottom: 20, + }, + headerTitle: { + fontSize: 32, + fontWeight: 'bold', + marginBottom: 10, + textAlign: 'center', + }, + headerSubtitle: { + fontSize: 16, + textAlign: 'center', + opacity: 0.8, + marginBottom: 10, + }, + featureItem: { + marginHorizontal: 20, + marginBottom: 16, + borderRadius: 12, + overflow: 'hidden', + }, + featureNumberContainer: { + paddingVertical: 8, + paddingHorizontal: 16, + alignItems: 'center', + justifyContent: 'center', + }, + featureNumber: { + color: 'white', + fontWeight: 'bold', + fontSize: 18, + }, + featureContent: { + padding: 16, + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + }, + featureTitle: { + fontSize: 18, + fontWeight: 'bold', + }, + featureIcon: { + marginLeft: 8, + }, +}); diff --git a/KonditionExpo/app/(tabs)/feed.tsx b/KonditionExpo/app/(tabs)/feed.tsx new file mode 100644 index 0000000000..fbe72440c6 --- /dev/null +++ b/KonditionExpo/app/(tabs)/feed.tsx @@ -0,0 +1,123 @@ +import React, { useEffect, useState } from 'react'; +import { View, StyleSheet, Alert } from 'react-native'; +import { router } from 'expo-router'; +import { useFeed, FeedType } from '../../contexts/FeedContext'; +import { FeedToggle, PostList, CreatePostButton } from '../../components/feed'; +import { WorkoutPostResponse } from '../../services/api'; + +export default function FeedScreen() { + const { + getCurrentFeed, + currentFeedType, + setCurrentFeedType, + loadFeed, + loadMorePosts, + refreshFeed, + isLoading, + error, + hasMore, + personalFeed, + publicFeed, + combinedFeed, + clearError, + } = useFeed(); + + const [isRefreshing, setIsRefreshing] = useState(false); + + useEffect(() => { + // Load initial feed when component mounts + loadFeed(currentFeedType, true); + }, []); + + useEffect(() => { + // Show error alert if there's an error + if (error) { + Alert.alert('Error', error, [ + { text: 'OK', onPress: clearError } + ]); + } + }, [error, clearError]); + + const handleFeedTypeChange = async (feedType: FeedType) => { + setCurrentFeedType(feedType); + + // Load feed if it's empty or switch to a different type + const targetFeed = getFeedForType(feedType); + if (targetFeed.length === 0) { + await loadFeed(feedType, true); + } + }; + + const getFeedForType = (feedType: FeedType): WorkoutPostResponse[] => { + switch (feedType) { + case 'personal': + return personalFeed; + case 'public': + return publicFeed; + case 'combined': + return combinedFeed; + default: + return personalFeed; + } + }; + + const handleRefresh = async () => { + setIsRefreshing(true); + try { + await refreshFeed(); + } finally { + setIsRefreshing(false); + } + }; + + const handleLoadMore = async () => { + await loadMorePosts(); + }; + + const handleCreatePost = () => { + router.push('/create-post' as any); + }; + + const handlePostPress = (post: WorkoutPostResponse) => { + // Navigate to post detail or user profile + console.log('Post pressed:', post.id); + // TODO: Implement post detail navigation + }; + + const postCounts = { + personal: personalFeed.length, + public: publicFeed.length, + combined: combinedFeed.length, + }; + + const currentFeed = getCurrentFeed(); + + return ( + + + + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#f5f5f5', + }, +}); \ No newline at end of file diff --git a/KonditionExpo/app/(tabs)/history.tsx b/KonditionExpo/app/(tabs)/history.tsx new file mode 100644 index 0000000000..a995d73391 --- /dev/null +++ b/KonditionExpo/app/(tabs)/history.tsx @@ -0,0 +1,362 @@ +import React, { useState, useEffect } from 'react'; +import { + SafeAreaView, + View, + Text, + StyleSheet, + ScrollView, + TouchableOpacity, + RefreshControl, + ActivityIndicator, + Alert, +} from 'react-native'; +import { router } from 'expo-router'; +import { apiService, WorkoutResponse } from '@/services/api'; +import { useAuth } from '@/contexts/AuthContext'; + +const WorkoutHistoryScreen = () => { + const { user } = useAuth(); + const [workouts, setWorkouts] = useState([]); + const [loading, setLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); + const [error, setError] = useState(null); + + const fetchWorkouts = async (isRefresh = false) => { + try { + if (isRefresh) { + setRefreshing(true); + } else { + setLoading(true); + } + setError(null); + + const workoutData = await apiService.getWorkouts(); + // Ensure workoutData is an array + if (Array.isArray(workoutData)) { + setWorkouts(workoutData); + } else { + console.warn('API returned non-array data:', workoutData); + setWorkouts([]); + setError('Invalid data format received from server'); + } + } catch (err) { + console.error('Error fetching workouts:', err); + setError(err instanceof Error ? err.message : 'Failed to load workouts'); + setWorkouts([]); // Ensure workouts is always an array + } finally { + setLoading(false); + setRefreshing(false); + } + }; + + useEffect(() => { + fetchWorkouts(); + }, []); + + const onRefresh = () => { + fetchWorkouts(true); + }; + + const formatDate = (dateString: string) => { + const date = new Date(dateString); + return date.toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + }); + }; + + const formatDuration = (minutes?: number) => { + if (!minutes) return 'N/A'; + const hours = Math.floor(minutes / 60); + const mins = minutes % 60; + if (hours > 0) { + return `${hours}h ${mins}m`; + } + return `${mins}m`; + }; + + const getCompletionStatus = (workout: WorkoutResponse) => { + return workout.is_completed ? 'Completed' : 'Incomplete'; + }; + + const getStatusColor = (workout: WorkoutResponse) => { + return workout.is_completed ? '#4CAF50' : '#FF9800'; + }; + + const handleWorkoutPress = (workout: WorkoutResponse) => { + // Navigate to the detailed workout view + router.push({ + pathname: '/workout-detail', + params: { workoutId: workout.id } + }); + }; + + const renderWorkoutItem = (workout: WorkoutResponse) => ( + handleWorkoutPress(workout)} + > + + {workout.name} + + {getCompletionStatus(workout)} + + + + + {formatDate(workout.completed_date || workout.created_at)} + + + + + Duration + {formatDuration(workout.duration_minutes)} + + + + Exercises + {workout.exercise_count || 0} + + + + {workout.description && ( + + {workout.description} + + )} + + + Tap to view exercises β†’ + + + ); + + const renderEmptyState = () => ( + + No Workouts Yet + + Start your fitness journey by completing your first workout! + + + ); + + const renderErrorState = () => ( + + Unable to Load Workouts + {error} + fetchWorkouts()}> + Try Again + + + ); + + if (loading && !refreshing) { + return ( + + + + Loading workout history... + + + ); + } + + return ( + + + Workout History + + {workouts.length} {workouts.length === 1 ? 'workout' : 'workouts'} completed + + + + + } + > + {error ? ( + renderErrorState() + ) : !Array.isArray(workouts) || workouts.length === 0 ? ( + renderEmptyState() + ) : ( + workouts.map(renderWorkoutItem) + )} + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#FFFFFF', + }, + header: { + padding: 20, + backgroundColor: '#F8F9FA', + borderBottomWidth: 1, + borderBottomColor: '#E9ECEF', + }, + headerTitle: { + fontSize: 28, + fontWeight: 'bold', + color: '#333', + marginBottom: 4, + }, + headerSubtitle: { + fontSize: 16, + color: '#666', + }, + scrollView: { + flex: 1, + }, + scrollContent: { + padding: 16, + paddingBottom: 80, + }, + loadingContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + loadingText: { + marginTop: 16, + fontSize: 16, + color: '#666', + }, + workoutCard: { + backgroundColor: '#FFFFFF', + borderRadius: 16, + padding: 16, + marginBottom: 16, + shadowColor: '#000', + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 3, + borderWidth: 1, + borderColor: '#F0F0F0', + }, + workoutHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 8, + }, + workoutName: { + fontSize: 18, + fontWeight: 'bold', + color: '#333', + flex: 1, + marginRight: 12, + }, + statusBadge: { + paddingHorizontal: 8, + paddingVertical: 4, + borderRadius: 12, + }, + statusText: { + color: '#FFFFFF', + fontSize: 12, + fontWeight: '600', + }, + workoutDate: { + fontSize: 14, + color: '#666', + marginBottom: 12, + }, + workoutDetails: { + flexDirection: 'row', + justifyContent: 'space-between', + marginBottom: 8, + }, + detailItem: { + flex: 1, + }, + detailLabel: { + fontSize: 12, + color: '#999', + marginBottom: 2, + }, + detailValue: { + fontSize: 16, + fontWeight: '600', + color: '#333', + }, + workoutDescription: { + fontSize: 14, + color: '#666', + fontStyle: 'italic', + marginTop: 8, + }, + emptyState: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + paddingVertical: 60, + }, + emptyTitle: { + fontSize: 24, + fontWeight: 'bold', + color: '#333', + marginBottom: 8, + }, + emptyMessage: { + fontSize: 16, + color: '#666', + textAlign: 'center', + lineHeight: 24, + }, + errorState: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + paddingVertical: 60, + }, + errorTitle: { + fontSize: 20, + fontWeight: 'bold', + color: '#FF6B6B', + marginBottom: 8, + }, + errorMessage: { + fontSize: 16, + color: '#666', + textAlign: 'center', + marginBottom: 20, + lineHeight: 24, + }, + retryButton: { + backgroundColor: '#70A1FF', + paddingHorizontal: 24, + paddingVertical: 12, + borderRadius: 12, + }, + retryText: { + color: '#FFFFFF', + fontSize: 16, + fontWeight: '600', + }, + viewDetailsContainer: { + marginTop: 8, + paddingTop: 8, + borderTopWidth: 1, + borderTopColor: '#F0F0F0', + alignItems: 'center', + }, + viewDetailsText: { + fontSize: 14, + color: '#70A1FF', + fontWeight: '600', + }, +}); + +export default WorkoutHistoryScreen; \ No newline at end of file diff --git a/KonditionExpo/app/(tabs)/index.tsx b/KonditionExpo/app/(tabs)/index.tsx new file mode 100644 index 0000000000..d5bc3f4d0a --- /dev/null +++ b/KonditionExpo/app/(tabs)/index.tsx @@ -0,0 +1,2 @@ +import HomeScreen from '@/app/home'; +export default HomeScreen; \ No newline at end of file diff --git a/KonditionExpo/app/(tabs)/profile.tsx b/KonditionExpo/app/(tabs)/profile.tsx new file mode 100644 index 0000000000..e1b286fc79 --- /dev/null +++ b/KonditionExpo/app/(tabs)/profile.tsx @@ -0,0 +1,250 @@ +import React, { useState } from 'react'; +import { SafeAreaView, View, Text, StyleSheet, ScrollView, TouchableOpacity, Image, Switch } from 'react-native'; +import { useAuth } from '@/contexts/AuthContext'; +import { router } from 'expo-router'; +import { DevTools } from '@/components/DevTools'; +import { Dialog } from '@/components/ui/Dialog'; +import { Button } from '@/components/ui/Button'; + +const ProfileScreen = () => { + const { user, logout, isLoading } = useAuth(); + const [notificationsEnabled, setNotificationsEnabled] = useState(false); + const [showDevTools, setShowDevTools] = useState(false); + const [showLogoutDialog, setShowLogoutDialog] = useState(false); + + const toggleNotifications = () => setNotificationsEnabled(prev => !prev); + + // Unit conversion functions + const cmToFeetInches = (cm: number): { feet: number; inches: number } => { + const totalInches = cm / 2.54; + const feet = Math.floor(totalInches / 12); + const inches = Math.round(totalInches % 12); + return { feet, inches }; + }; + + const kgToLbs = (kg: number): number => { + return Math.round(kg * 2.20462); + }; + + // Calculate age from date of birth + const calculateAge = (dateOfBirth: string | undefined): number => { + if (!dateOfBirth) return 0; + const today = new Date(); + const birthDate = new Date(dateOfBirth); + let age = today.getFullYear() - birthDate.getFullYear(); + const monthDiff = today.getMonth() - birthDate.getMonth(); + if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) { + age--; + } + return age; + }; + + const age = calculateAge(user?.date_of_birth); + + // Display formatted height in feet and inches + const getDisplayHeight = (): string => { + if (!user?.height) return '-'; + const { feet, inches } = cmToFeetInches(user.height); + return `${feet}' ${inches}"`; + }; + + // Display formatted weight in pounds + const getDisplayWeight = (): string => { + if (!user?.weight) return '-'; + return `${kgToLbs(user.weight)} lbs`; + }; + + // Navigate to edit profile screen + const handleEditProfile = () => { + router.push('/edit-profile'); + }; + + const handleLogout = () => { + setShowLogoutDialog(true); + }; + + const confirmLogout = async () => { + try { + console.log('Starting logout process...'); + setShowLogoutDialog(false); + await logout(); + console.log('Logout successful'); + // Navigation will be handled automatically by ProtectedRoute + } catch (error) { + console.error('Logout error:', error); + // For web compatibility, we'll use a simple alert or could create another dialog + if (typeof window !== 'undefined') { + // Web environment + alert(`Failed to sign out: ${error instanceof Error ? error.message : 'Unknown error'}. Please try again.`); + } else { + // Mobile environment - this shouldn't happen but as fallback + console.error('Logout failed:', error); + } + } + }; + + const cancelLogout = () => { + setShowLogoutDialog(false); + }; + + return ( + + + {/* Profile Box */} + + {user?.full_name || user?.email || 'User'} + {user?.email || 'No email'} + + Edit + + + + + Height + {getDisplayHeight()} + + + Weight + {getDisplayWeight()} + + + Age + {age > 0 ? age.toString() : '-'} + + + + + {/* Account Box */} + + Account + Personal Data + Achievement + Activity History + Workout Progress + + + {/* Notification Box */} + + Notification + + Pop-up Notifications + + + + + {/* Other Box */} + + Other + Privacy Policy + Contact Us + Settings + setShowDevTools(true)}> + Developer Tools + + + + {/* Logout Button */} + + + {isLoading ? 'Signing Out...' : 'Sign Out'} + + + + + {/* DevTools Modal */} + setShowDevTools(false)} + /> + + {/* Logout Confirmation Dialog */} + + + + + ); +}; + +const styles = StyleSheet.create({ + container: { flex: 1, backgroundColor: '#FFFFFF' }, + content: { padding: 16, paddingBottom: 80 }, + profileBox: { backgroundColor: '#E5F1FF', borderRadius: 20, padding: 16, marginBottom: 24 }, + profileName: { fontSize: 24, fontWeight: 'bold', color: '#333' }, + profileEmail: { fontSize: 14, color: '#666', marginTop: 4 }, + editBtn: { backgroundColor: '#70A1FF', borderRadius: 12, paddingVertical: 6, paddingHorizontal: 12, alignSelf: 'flex-start', marginTop: 8 }, + editText: { color: '#FFF' }, + statsRow: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 16 }, + statBox: { backgroundColor: '#FFF', borderRadius: 12, padding: 12, width: '30%', alignItems: 'center' }, + statLabel: { fontSize: 12, color: '#555' }, + statValue: { fontSize: 16, fontWeight: 'bold', color: '#333' }, + sectionBox: { backgroundColor: '#F5F8FF', borderRadius: 16, padding: 16, marginBottom: 24 }, + sectionTitle: { fontSize: 16, fontWeight: 'bold', color: '#333', marginBottom: 8 }, + optionItem: { fontSize: 14, color: '#555', paddingVertical: 6 }, + toggleRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }, + logoutButton: { + backgroundColor: '#FF6B6B', + borderRadius: 16, + padding: 16, + alignItems: 'center', + marginTop: 16, + marginBottom: 24 + }, + logoutText: { + color: '#FFF', + fontSize: 16, + fontWeight: 'bold' + }, + dialogFooter: { + flexDirection: 'row', + gap: 12, + }, + cancelButton: { + flex: 1, + }, + confirmButton: { + flex: 1, + backgroundColor: '#FF6B6B', + }, + dialogText: { + fontSize: 16, + color: '#333', + textAlign: 'center', + lineHeight: 24, + }, +}); + +export default ProfileScreen; \ No newline at end of file diff --git a/KonditionExpo/app/(tabs)/progress.tsx b/KonditionExpo/app/(tabs)/progress.tsx new file mode 100644 index 0000000000..33e662d6ca --- /dev/null +++ b/KonditionExpo/app/(tabs)/progress.tsx @@ -0,0 +1,446 @@ +import React, { useState } from 'react'; +import { + SafeAreaView, + View, + Text, + StyleSheet, + ScrollView, + TouchableOpacity, + Modal, + Alert, + Dimensions +} from 'react-native'; +import { LineChart } from 'react-native-chart-kit'; +import { useWorkout, Workout } from '@/contexts/WorkoutContext'; +import { Button } from '@/components/ui/Button'; +import { Input } from '@/components/ui/Input'; +import { router } from 'expo-router'; + +const { width } = Dimensions.get('window'); + +interface WorkoutItemProps { + workout: Workout; + onPress: () => void; +} + +const WorkoutItem = ({ workout, onPress }: WorkoutItemProps) => { + const formatDate = (date: Date) => { + return date.toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric' + }); + }; + + const getTotalSets = () => { + return workout.exercises.reduce((total, exercise) => total + exercise.sets.length, 0); + }; + + return ( + + + {workout.name} + {formatDate(workout.date)} + + + {workout.exercises.length} exercises + {getTotalSets()} sets + {workout.duration}min + + + ); +}; + +const ProgressScreen = () => { + const { workouts, currentWorkout, startWorkout } = useWorkout(); + const [showNewWorkoutModal, setShowNewWorkoutModal] = useState(false); + const [newWorkoutName, setNewWorkoutName] = useState(''); + + const backgroundColor = '#FFFFFF'; + const textColor = '#333'; + const tintColor = '#70A1FF'; + + const handleStartWorkout = () => { + if (!newWorkoutName.trim()) { + Alert.alert('Error', 'Please enter a workout name'); + return; + } + startWorkout(newWorkoutName.trim()); + setNewWorkoutName(''); + setShowNewWorkoutModal(false); + router.push('../workout'); + }; + + const getWeeklyWorkouts = () => { + const last7Days = Array.from({ length: 7 }, (_, i) => { + const date = new Date(); + date.setDate(date.getDate() - (6 - i)); + return date.toDateString(); + }); + + return last7Days.map(dateString => { + const workoutsOnDay = workouts.filter(w => + w.date.toDateString() === dateString + ); + return workoutsOnDay.length; + }); + }; + + const getWeeklyLabels = () => { + return Array.from({ length: 7 }, (_, i) => { + const date = new Date(); + date.setDate(date.getDate() - (6 - i)); + return date.toLocaleDateString('en-US', { weekday: 'short' }); + }); + }; + + const getTotalWorkouts = () => workouts.length; + const getThisWeekWorkouts = () => { + const startOfWeek = new Date(); + startOfWeek.setDate(startOfWeek.getDate() - startOfWeek.getDay()); + return workouts.filter(w => w.date >= startOfWeek).length; + }; + + const getAverageWorkoutDuration = () => { + if (workouts.length === 0) return 0; + const total = workouts.reduce((sum, w) => sum + w.duration, 0); + return Math.round(total / workouts.length); + }; + + const weeklyData = getWeeklyWorkouts(); + const weeklyLabels = getWeeklyLabels(); + + return ( + + + {/* Header */} + + Progress + + Track your fitness journey + + + + {/* Current Workout Banner */} + {currentWorkout && ( + + + Workout in Progress: {currentWorkout.name} + + router.push('../workout')} + > + Continue + + + )} + + {/* Stats Cards */} + + + {getTotalWorkouts()} + Total Workouts + + + {getThisWeekWorkouts()} + This Week + + + {getAverageWorkoutDuration()} + Avg Duration + + + + {/* Weekly Chart */} + {workouts.length > 0 && ( + + + Weekly Activity + + tintColor, + strokeWidth: 2, + }, + ], + }} + width={width - 64} + height={200} + chartConfig={{ + backgroundGradientFrom: backgroundColor, + backgroundGradientTo: backgroundColor, + color: () => tintColor, + strokeWidth: 2, + decimalPlaces: 0, + }} + bezier + style={styles.chart} + /> + + )} + + {/* Start New Workout Button */} + +