├── RBUML.png ├── README.md ├── SQL.md ├── Server ├── .DS_Store ├── package-lock.json ├── package.json └── repbook.js ├── iOS ├── .DS_Store ├── ReminderX.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ ├── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── swiftpm │ │ │ │ └── Package.resolved │ │ └── xcuserdata │ │ │ └── aaronmclean.xcuserdatad │ │ │ ├── IDEFindNavigatorScopes.plist │ │ │ ├── UserInterfaceState.xcuserstate │ │ │ └── WorkspaceSettings.xcsettings │ └── xcuserdata │ │ └── aaronmclean.xcuserdatad │ │ ├── xcdebugger │ │ └── Breakpoints_v2.xcbkptlist │ │ └── xcschemes │ │ └── xcschememanagement.plist ├── ReminderX │ ├── .DS_Store │ ├── Assets.xcassets │ │ ├── .DS_Store │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ └── ion.png │ │ ├── Contents.json │ │ ├── login_banner.imageset │ │ │ ├── Contents.json │ │ │ └── Screenshot 2023-11-15 at 5.15.17 AM.png │ │ └── medals │ │ │ ├── Contents.json │ │ │ ├── achievement (1).imageset │ │ │ ├── Contents.json │ │ │ └── achievement (1).png │ │ │ ├── achievement (2).imageset │ │ │ ├── Contents.json │ │ │ └── achievement (2).png │ │ │ ├── achievement (3).imageset │ │ │ ├── Contents.json │ │ │ └── achievement (3).png │ │ │ └── achievement.imageset │ │ │ ├── Contents.json │ │ │ └── achievement.png │ ├── Camera.xcworkspace │ │ ├── contents.xcworkspacedata │ │ ├── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcuserdata │ │ │ └── aaronmclean.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ ├── ContentView.swft.swift │ ├── ContentView.swift │ ├── Info.plist │ ├── Managers │ │ ├── .DS_Store │ │ ├── ColorSchemeManager.swift │ │ ├── KeychainManager.swift │ │ └── NetworkManager.swift │ ├── Preview Content │ │ ├── .DS_Store │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ ├── ReminderXApp.swift │ ├── Safari.swift │ ├── Utilities │ │ ├── .DS_Store │ │ ├── Extensions.swift │ │ ├── Graph.swift │ │ ├── Models.swift │ │ └── VisualComponents.swift │ ├── Views │ │ ├── .DS_Store │ │ ├── AiView.swift │ │ ├── HomeView.swift │ │ ├── LoginAccountView.swift │ │ ├── LoginView.swift │ │ ├── MakeAccountView.swift │ │ ├── SettingsView.swift │ │ ├── VideoEditorHomeVIew.swift │ │ └── WorkoutBuilder.swift │ └── WorkoutDetailView.swift ├── ReminderXTests │ └── ReminderXTests.swift └── ReminderXUITests │ ├── ReminderXUITests.swift │ └── ReminderXUITestsLaunchTests.swift └── uml.mmd /RBUML.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronmcleancs/RepBook-DemoServer/c57d7c76c8b96530079b11d507ddc950e936227c/RBUML.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Full Stack iOS Fitness Tracking Application (Objective-C, Node.js, Express, PostresSQL) 2 | 3 | Good template for basic user auth and semantics fetching for chatbot 4 | 5 | 6 | 7 | 10 | 13 | 16 | 17 |
8 | 9 | 11 | 12 | 14 | 15 |
18 | 19 | 20 | 21 | =============================================== 22 | 23 | Installation and Setup 24 | ----------------------- 25 | git clone https://github.com/AaronM26/RepBook.git 26 | cd RepBook 27 | 28 | Backend Setup 29 | cd /backend 30 | npm install 31 | # Replace OpenAI API Key in configuration 32 | node repbook.js 33 | 34 | Server Setup 35 | - Run Demo Server in PostgreSQL 36 | 37 | Frontend Setup 38 | - Open `RepBook.xcodeproj` in Xcode 39 | - Replace Server IP with your Postgres server IP 40 | - Run the project in a simulator (iOS 17.x+) 41 | 42 | ### RESTful API Endpoints 43 | 44 | | Endpoint | Method | Description | 45 | |-----------------------------------|--------|------------------------------| 46 | | `/api/signup` | POST | User Signup | 47 | | `/api/login` | POST | User Login | 48 | | `/api/checkUsername/:username` | GET | Check Username Availability | 49 | | `/api/exercises` | POST | Add Exercises to Workout | 50 | | `/api/updateUserInfo/:memberId` | POST | Update User Information | 51 | | `/api/userDataAndMetrics/:memberId`| GET | Fetch User Data and Metrics | 52 | | `/api/setGymMembership` | POST | Set Gym Membership | 53 | | `/api/workouts/:memberId` | GET | Get Workouts | 54 | | `/api/membersMetrics/:memberId` | GET | Get Member's Metrics | 55 | | `/api/createWorkout/:memberId` | POST | Create Workout | 56 | | `/api/exercises` | GET | Fetch Exercises | 57 | -------------------------------------------------------------------------------- /SQL.md: -------------------------------------------------------------------------------- 1 | -- Table: public.admin_staff 2 | CREATE TABLE admin ( 3 | member_id INTEGER PRIMARY KEY REFERENCES members(member_id), 4 | hire_date DATE NOT NULL, 5 | salary NUMERIC 6 | ); 7 | 8 | -- Table: public.exercises 9 | CREATE TABLE exercises ( 10 | id INTEGER PRIMARY KEY, 11 | name CHARACTER VARYING(255) NOT NULL, 12 | muscle_group CHARACTER VARYING(255), 13 | difficulty CHARACTER VARYING(255), 14 | duration INTEGER, 15 | equipment_needed BOOLEAN, 16 | workout_type CHARACTER VARYING(255), 17 | description TEXT 18 | ); 19 | 20 | -- Table: public.fitness_achievements 21 | CREATE TABLE fitness_achievements ( 22 | member_id INTEGER PRIMARY KEY REFERENCES members(member_id), 23 | achievement_strength BOOLEAN, 24 | achievement_endurance BOOLEAN, 25 | achievement_flexibility BOOLEAN, 26 | achievement_date DATE NOT NULL 27 | ); 28 | 29 | -- Table: public.gym_memberships 30 | CREATE TABLE gym_memberships ( 31 | member_id INTEGER PRIMARY KEY REFERENCES members(member_id), 32 | gym CHARACTER VARYING(255) NOT NULL, 33 | address CHARACTER VARYING(255) NOT NULL 34 | ); 35 | 36 | 37 | CREATE TABLE members ( 38 | member_id INTEGER PRIMARY KEY, 39 | first_name CHARACTER VARYING(255) NOT NULL, 40 | last_name CHARACTER VARYING(255) NOT NULL, 41 | date_of_birth DATE NOT NULL, 42 | email CHARACTER VARYING(255) UNIQUE NOT NULL, 43 | password CHARACTER VARYING(255) NOT NULL, 44 | time_created TIMESTAMP WITHOUT TIME ZONE NOT NULL 45 | ); 46 | 47 | -- Table: public.members_metrics 48 | CREATE TABLE members_metrics ( 49 | member_id INTEGER PRIMARY KEY REFERENCES members(member_id), 50 | height_cm INTEGER NOT NULL, 51 | weight_kg NUMERIC NOT NULL, 52 | gender CHARACTER VARYING(255) NOT NULL, 53 | workout_frequency INTEGER NOT NULL 54 | ); 55 | 56 | CREATE TABLE nutrition_plans ( 57 | member_id INTEGER PRIMARY KEY REFERENCES members(member_id), 58 | daily_calories INTEGER NOT NULL, 59 | protein_target NUMERIC NOT NULL, 60 | carbs_target NUMERIC NOT NULL, 61 | fats_target NUMERIC NOT NULL, 62 | meal_timing TEXT, 63 | start_date DATE NOT NULL, 64 | end_date DATE, 65 | notes TEXT 66 | ); 67 | 68 | -- Table: public.pr_tracker 69 | CREATE TABLE pr_tracker ( 70 | member_id INTEGER PRIMARY KEY REFERENCES members(member_id), 71 | bench_max INTEGER NOT NULL, 72 | squat_max INTEGER NOT NULL, 73 | deadlift_max INTEGER NOT NULL, 74 | recorded_at TIMESTAMP WITHOUT TIME ZONE NOT NULL 75 | ); 76 | 77 | 78 | -- Table: public.user_workout_plans 79 | CREATE TABLE user_workout_plans ( 80 | member_id INTEGER PRIMARY KEY REFERENCES members(member_id), 81 | plan_name CHARACTER VARYING(255) NOT NULL, 82 | preference_intensity CHARACTER VARYING(255) NOT NULL, 83 | preference_duration INTEGER NOT NULL, 84 | focus_area CHARACTER VARYING(255), 85 | frequency_per_week INTEGER NOT NULL, 86 | created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, 87 | updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL 88 | ); 89 | 90 | 91 | -- Table: public.workouts 92 | CREATE TABLE workouts ( 93 | workout_id INTEGER PRIMARY KEY, 94 | workout_name CHARACTER VARYING(255) NOT NULL 95 | ); 96 | 97 | -- Junction Table: public.workout_exercises 98 | CREATE TABLE workout_exercises ( 99 | workout_id INTEGER REFERENCES workouts(workout_id), 100 | exercise_id INTEGER REFERENCES exercises(id), 101 | PRIMARY KEY (workout_id, exercise_id) 102 | ); 103 | -------------------------------------------------------------------------------- /Server/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronmcleancs/RepBook-DemoServer/c57d7c76c8b96530079b11d507ddc950e936227c/Server/.DS_Store -------------------------------------------------------------------------------- /Server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "appserver", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "bcrypt": "^5.1.1", 14 | "body-parser": "^1.20.2", 15 | "crypto": "^1.0.1", 16 | "dotenv": "^16.3.1", 17 | "express": "^4.18.2", 18 | "jsonwebtoken": "^9.0.2", 19 | "pg": "^8.11.3" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iOS/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronmcleancs/RepBook-DemoServer/c57d7c76c8b96530079b11d507ddc950e936227c/iOS/.DS_Store -------------------------------------------------------------------------------- /iOS/ReminderX.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /iOS/ReminderX.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /iOS/ReminderX.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /iOS/ReminderX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "b463964042cb16e81df3f32644fc1d57075f6f8e15cad0c8209f55b38b3e00a2", 3 | "pins" : [ 4 | { 5 | "identity" : "alamofire", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/Alamofire/Alamofire", 8 | "state" : { 9 | "revision" : "3dc6a42c7727c49bf26508e29b0a0b35f9c7e1ad", 10 | "version" : "5.8.1" 11 | } 12 | }, 13 | { 14 | "identity" : "async-http-client", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/swift-server/async-http-client.git", 17 | "state" : { 18 | "revision" : "5ccda442f103792d67680aefc8d0a87392fbd66c", 19 | "version" : "1.20.0" 20 | } 21 | }, 22 | { 23 | "identity" : "chartview", 24 | "kind" : "remoteSourceControl", 25 | "location" : "https://github.com/AppPear/ChartView", 26 | "state" : { 27 | "revision" : "9115a992c91fa19cbb4f2241084240d38654a1fc", 28 | "version" : "1.5.5" 29 | } 30 | }, 31 | { 32 | "identity" : "chatgptswift", 33 | "kind" : "remoteSourceControl", 34 | "location" : "https://github.com/alfianlosari/ChatGPTSwift.git", 35 | "state" : { 36 | "branch" : "main", 37 | "revision" : "3add305675795c1eeec93d6175a9b04d6f415c15" 38 | } 39 | }, 40 | { 41 | "identity" : "get", 42 | "kind" : "remoteSourceControl", 43 | "location" : "https://github.com/kean/Get", 44 | "state" : { 45 | "revision" : "31249885da1052872e0ac91a2943f62567c0d96d", 46 | "version" : "2.2.1" 47 | } 48 | }, 49 | { 50 | "identity" : "gptencoder", 51 | "kind" : "remoteSourceControl", 52 | "location" : "https://github.com/alfianlosari/GPTEncoder.git", 53 | "state" : { 54 | "revision" : "a86968867ab4380e36b904a14c42215f71efe8b4", 55 | "version" : "1.0.4" 56 | } 57 | }, 58 | { 59 | "identity" : "gptswift", 60 | "kind" : "remoteSourceControl", 61 | "location" : "https://github.com/SwiftedMind/GPTSwift", 62 | "state" : { 63 | "revision" : "c90d3fc4418119aa6cc796ded049fa73f4b28185", 64 | "version" : "3.0.1" 65 | } 66 | }, 67 | { 68 | "identity" : "openai-kit", 69 | "kind" : "remoteSourceControl", 70 | "location" : "https://github.com/dylanshine/openai-kit.git", 71 | "state" : { 72 | "revision" : "bf4e81668fe3c561afa29078121f3ff7d99da210", 73 | "version" : "1.8.2" 74 | } 75 | }, 76 | { 77 | "identity" : "swift-algorithms", 78 | "kind" : "remoteSourceControl", 79 | "location" : "https://github.com/apple/swift-algorithms", 80 | "state" : { 81 | "revision" : "f6919dfc309e7f1b56224378b11e28bab5bccc42", 82 | "version" : "1.2.0" 83 | } 84 | }, 85 | { 86 | "identity" : "swift-atomics", 87 | "kind" : "remoteSourceControl", 88 | "location" : "https://github.com/apple/swift-atomics.git", 89 | "state" : { 90 | "revision" : "cd142fd2f64be2100422d658e7411e39489da985", 91 | "version" : "1.2.0" 92 | } 93 | }, 94 | { 95 | "identity" : "swift-collections", 96 | "kind" : "remoteSourceControl", 97 | "location" : "https://github.com/apple/swift-collections.git", 98 | "state" : { 99 | "revision" : "d029d9d39c87bed85b1c50adee7c41795261a192", 100 | "version" : "1.0.6" 101 | } 102 | }, 103 | { 104 | "identity" : "swift-http-types", 105 | "kind" : "remoteSourceControl", 106 | "location" : "https://github.com/apple/swift-http-types", 107 | "state" : { 108 | "revision" : "1827dc94bdab2eb5f2fc804e9b0cb43574282566", 109 | "version" : "1.0.2" 110 | } 111 | }, 112 | { 113 | "identity" : "swift-log", 114 | "kind" : "remoteSourceControl", 115 | "location" : "https://github.com/apple/swift-log.git", 116 | "state" : { 117 | "revision" : "532d8b529501fb73a2455b179e0bbb6d49b652ed", 118 | "version" : "1.5.3" 119 | } 120 | }, 121 | { 122 | "identity" : "swift-nio", 123 | "kind" : "remoteSourceControl", 124 | "location" : "https://github.com/apple/swift-nio.git", 125 | "state" : { 126 | "revision" : "702cd7c56d5d44eeba73fdf83918339b26dc855c", 127 | "version" : "2.62.0" 128 | } 129 | }, 130 | { 131 | "identity" : "swift-nio-extras", 132 | "kind" : "remoteSourceControl", 133 | "location" : "https://github.com/apple/swift-nio-extras.git", 134 | "state" : { 135 | "revision" : "798c962495593a23fdea0c0c63fd55571d8dff51", 136 | "version" : "1.20.0" 137 | } 138 | }, 139 | { 140 | "identity" : "swift-nio-http2", 141 | "kind" : "remoteSourceControl", 142 | "location" : "https://github.com/apple/swift-nio-http2.git", 143 | "state" : { 144 | "revision" : "3bd9004b9d685ed6b629760fc84903e48efec806", 145 | "version" : "1.29.0" 146 | } 147 | }, 148 | { 149 | "identity" : "swift-nio-ssl", 150 | "kind" : "remoteSourceControl", 151 | "location" : "https://github.com/apple/swift-nio-ssl.git", 152 | "state" : { 153 | "revision" : "320bd978cceb8e88c125dcbb774943a92f6286e9", 154 | "version" : "2.25.0" 155 | } 156 | }, 157 | { 158 | "identity" : "swift-nio-transport-services", 159 | "kind" : "remoteSourceControl", 160 | "location" : "https://github.com/apple/swift-nio-transport-services.git", 161 | "state" : { 162 | "revision" : "ebf8b9c365a6ce043bf6e6326a04b15589bd285e", 163 | "version" : "1.20.0" 164 | } 165 | }, 166 | { 167 | "identity" : "swift-numerics", 168 | "kind" : "remoteSourceControl", 169 | "location" : "https://github.com/apple/swift-numerics.git", 170 | "state" : { 171 | "revision" : "0a5bc04095a675662cf24757cc0640aa2204253b", 172 | "version" : "1.0.2" 173 | } 174 | }, 175 | { 176 | "identity" : "swiftopenai", 177 | "kind" : "remoteSourceControl", 178 | "location" : "https://github.com/SwiftBeta/SwiftOpenAI.git", 179 | "state" : { 180 | "revision" : "495e4358b328391a240d977d97cc2512b4947f5f", 181 | "version" : "1.3.0" 182 | } 183 | } 184 | ], 185 | "version" : 3 186 | } 187 | -------------------------------------------------------------------------------- /iOS/ReminderX.xcodeproj/project.xcworkspace/xcuserdata/aaronmclean.xcuserdatad/IDEFindNavigatorScopes.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /iOS/ReminderX.xcodeproj/project.xcworkspace/xcuserdata/aaronmclean.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronmcleancs/RepBook-DemoServer/c57d7c76c8b96530079b11d507ddc950e936227c/iOS/ReminderX.xcodeproj/project.xcworkspace/xcuserdata/aaronmclean.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /iOS/ReminderX.xcodeproj/project.xcworkspace/xcuserdata/aaronmclean.xcuserdatad/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildLocationStyle 6 | UseAppPreferences 7 | CustomBuildLocationType 8 | RelativeToDerivedData 9 | DerivedDataLocationStyle 10 | Default 11 | ShowSharedSchemesAutomaticallyEnabled 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /iOS/ReminderX.xcodeproj/xcuserdata/aaronmclean.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 9 | 21 | 22 | 23 | 25 | 37 | 38 | 39 | 41 | 53 | 54 | 55 | 57 | 69 | 70 | 83 | 84 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /iOS/ReminderX.xcodeproj/xcuserdata/aaronmclean.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | ReminderX.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /iOS/ReminderX/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronmcleancs/RepBook-DemoServer/c57d7c76c8b96530079b11d507ddc950e936227c/iOS/ReminderX/.DS_Store -------------------------------------------------------------------------------- /iOS/ReminderX/Assets.xcassets/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronmcleancs/RepBook-DemoServer/c57d7c76c8b96530079b11d507ddc950e936227c/iOS/ReminderX/Assets.xcassets/.DS_Store -------------------------------------------------------------------------------- /iOS/ReminderX/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /iOS/ReminderX/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "ion.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | } 9 | ], 10 | "info" : { 11 | "author" : "xcode", 12 | "version" : 1 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /iOS/ReminderX/Assets.xcassets/AppIcon.appiconset/ion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronmcleancs/RepBook-DemoServer/c57d7c76c8b96530079b11d507ddc950e936227c/iOS/ReminderX/Assets.xcassets/AppIcon.appiconset/ion.png -------------------------------------------------------------------------------- /iOS/ReminderX/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /iOS/ReminderX/Assets.xcassets/login_banner.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Screenshot 2023-11-15 at 5.15.17 AM.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iOS/ReminderX/Assets.xcassets/login_banner.imageset/Screenshot 2023-11-15 at 5.15.17 AM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronmcleancs/RepBook-DemoServer/c57d7c76c8b96530079b11d507ddc950e936227c/iOS/ReminderX/Assets.xcassets/login_banner.imageset/Screenshot 2023-11-15 at 5.15.17 AM.png -------------------------------------------------------------------------------- /iOS/ReminderX/Assets.xcassets/medals/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /iOS/ReminderX/Assets.xcassets/medals/achievement (1).imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "achievement (1).png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iOS/ReminderX/Assets.xcassets/medals/achievement (1).imageset/achievement (1).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronmcleancs/RepBook-DemoServer/c57d7c76c8b96530079b11d507ddc950e936227c/iOS/ReminderX/Assets.xcassets/medals/achievement (1).imageset/achievement (1).png -------------------------------------------------------------------------------- /iOS/ReminderX/Assets.xcassets/medals/achievement (2).imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "achievement (2).png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iOS/ReminderX/Assets.xcassets/medals/achievement (2).imageset/achievement (2).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronmcleancs/RepBook-DemoServer/c57d7c76c8b96530079b11d507ddc950e936227c/iOS/ReminderX/Assets.xcassets/medals/achievement (2).imageset/achievement (2).png -------------------------------------------------------------------------------- /iOS/ReminderX/Assets.xcassets/medals/achievement (3).imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "achievement (3).png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iOS/ReminderX/Assets.xcassets/medals/achievement (3).imageset/achievement (3).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronmcleancs/RepBook-DemoServer/c57d7c76c8b96530079b11d507ddc950e936227c/iOS/ReminderX/Assets.xcassets/medals/achievement (3).imageset/achievement (3).png -------------------------------------------------------------------------------- /iOS/ReminderX/Assets.xcassets/medals/achievement.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "achievement.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iOS/ReminderX/Assets.xcassets/medals/achievement.imageset/achievement.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronmcleancs/RepBook-DemoServer/c57d7c76c8b96530079b11d507ddc950e936227c/iOS/ReminderX/Assets.xcassets/medals/achievement.imageset/achievement.png -------------------------------------------------------------------------------- /iOS/ReminderX/Camera.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /iOS/ReminderX/Camera.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /iOS/ReminderX/Camera.xcworkspace/xcuserdata/aaronmclean.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronmcleancs/RepBook-DemoServer/c57d7c76c8b96530079b11d507ddc950e936227c/iOS/ReminderX/Camera.xcworkspace/xcuserdata/aaronmclean.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /iOS/ReminderX/ContentView.swft.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import WebKit 3 | 4 | struct ContentView: View { 5 | private let cameraURL = URL(string: "https://192.168.0.113:8080/jsfs.html")! 6 | 7 | var body: some View { 8 | VStack { 9 | WebView(url: cameraURL) 10 | .edgesIgnoringSafeArea(.top) 11 | 12 | VStack(spacing: 20) { 13 | Toggle("Setting 1", isOn: .constant(true)) 14 | .toggleStyle(SwitchToggleStyle(tint: .black)) 15 | 16 | Toggle("Setting 2", isOn: .constant(false)) 17 | .toggleStyle(SwitchToggleStyle(tint: .black)) 18 | } 19 | .padding() 20 | .background(Color.white) 21 | } 22 | .preferredColorScheme(.dark) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /iOS/ReminderX/ContentView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import UniformTypeIdentifiers 3 | import CoreGraphics 4 | import Alamofire 5 | import Network 6 | import Foundation 7 | import Security 8 | 9 | struct ContentView: View { 10 | @State private var isAuthenticated: Bool = false 11 | @State private var userInfo: UserInfo? 12 | @State private var userMetrics: [MemberMetric] = [] 13 | 14 | var body: some View { 15 | Group { 16 | if isAuthenticated, let userInfo = userInfo { 17 | MainAppView(userMetrics: userMetrics, userInfo: userInfo) 18 | } else { 19 | LoginView(isAuthenticated: $isAuthenticated) 20 | } 21 | } 22 | .onAppear { 23 | isAuthenticated = KeychainManager.load(service: "YourAppService", account: "userId") != nil 24 | fetchUserDataIfNeeded() 25 | fetchUserMetricsIfNeeded() 26 | } 27 | .environment(\.colorScheme, .light) 28 | } 29 | 30 | private func fetchUserDataIfNeeded() { 31 | if isAuthenticated { 32 | if let memberIdData = KeychainManager.load(service: "YourAppService", account: "userId"), 33 | let memberIdString = String(data: memberIdData, encoding: .utf8), 34 | let memberId = Int(memberIdString), 35 | let authKeyData = KeychainManager.load(service: "YourAppService", account: "authKey"), 36 | let authKey = String(data: authKeyData, encoding: .utf8) { 37 | NetworkManager.fetchUserDataAndMetrics(memberId: memberId, authKey: authKey) { fetchedUserInfo in 38 | DispatchQueue.main.async { 39 | self.userInfo = fetchedUserInfo 40 | } 41 | } 42 | } 43 | } 44 | } 45 | private func fetchUserMetricsIfNeeded() { 46 | if isAuthenticated { 47 | if let memberIdData = KeychainManager.load(service: "YourAppService", account: "userId"), 48 | let memberIdString = String(data: memberIdData, encoding: .utf8), 49 | let memberId = Int(memberIdString), 50 | let authKeyData = KeychainManager.load(service: "YourAppService", account: "authKey"), 51 | let authKey = String(data: authKeyData, encoding: .utf8) { 52 | 53 | NetworkManager.fetchMemberMetrics(memberId: memberId, authKey: authKey) { result in 54 | DispatchQueue.main.async { 55 | switch result { 56 | case .success(let fetchedMetrics): 57 | self.userMetrics = fetchedMetrics 58 | case .failure(let error): 59 | print("Error fetching user metrics: \(error)") 60 | } 61 | } 62 | } 63 | } 64 | } 65 | } 66 | } 67 | struct MainAppView: View { 68 | var userMetrics: [MemberMetric] 69 | var userInfo: UserInfo 70 | @State private var selection: Int = 0 71 | @State private var keyboardVisible: Bool = false 72 | 73 | var body: some View { 74 | ZStack(alignment: .bottom) { 75 | NavigationView { 76 | Group { 77 | switch selection { 78 | case 0: 79 | HomeView(userInfo: userInfo, userMetrics: userMetrics) 80 | case 1: 81 | WorkoutView() 82 | case 2: 83 | AiView() 84 | case 3: 85 | SettingsView(userInfo: userInfo, userMetrics: userMetrics) 86 | default: 87 | EmptyView() 88 | } 89 | } 90 | } 91 | if !keyboardVisible { 92 | CustomTabBar(selection: $selection) 93 | } 94 | } 95 | .onAppear { 96 | NotificationCenter.default.addObserver( 97 | forName: UIResponder.keyboardWillShowNotification, 98 | object: nil, queue: .main) { _ in 99 | keyboardVisible = true 100 | } 101 | 102 | NotificationCenter.default.addObserver( 103 | forName: UIResponder.keyboardWillHideNotification, 104 | object: nil, queue: .main) { _ in 105 | keyboardVisible = false 106 | } 107 | } 108 | .onDisappear { 109 | NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil) 110 | NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil) 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /iOS/ReminderX/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSCameraUsageDescription 6 | We need to access the camera so users can take videos of their golf swings to edit 7 | 8 | 9 | -------------------------------------------------------------------------------- /iOS/ReminderX/Managers/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronmcleancs/RepBook-DemoServer/c57d7c76c8b96530079b11d507ddc950e936227c/iOS/ReminderX/Managers/.DS_Store -------------------------------------------------------------------------------- /iOS/ReminderX/Managers/ColorSchemeManager.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | class ColorSchemeManager: ObservableObject { 4 | static let shared = ColorSchemeManager() 5 | 6 | private init() { } 7 | 8 | @Published var transitionDuration: Double = 0.45 9 | 10 | private var userColorSchemeRawValue: Int { 11 | get { 12 | return UserDefaults.standard.integer(forKey: "userColorScheme") 13 | } 14 | set { 15 | UserDefaults.standard.set(newValue, forKey: "userColorScheme") 16 | } 17 | } 18 | 19 | var currentColorScheme: (dark: Color, med: Color, light: Color) { 20 | return ColorSchemeOption(rawValue: userColorSchemeRawValue)?.colors ?? (.darkMulti1, .medMulti1, .lightMulti1) 21 | } 22 | 23 | func updateColorScheme(to newColorScheme: ColorSchemeOption) { 24 | withAnimation(.easeInOut(duration: self.transitionDuration)) { 25 | self.userColorSchemeRawValue = newColorScheme.rawValue 26 | } 27 | } 28 | } 29 | 30 | enum ColorSchemeOption: Int, CaseIterable { 31 | case system 32 | case red 33 | case orange 34 | case green 35 | case newColor 36 | case blue 37 | case violet 38 | case pink 39 | 40 | var colors: (dark: Color, med: Color, light: Color) { 41 | switch self { 42 | case .system: 43 | return (Color.black.opacity(0.5), Color.black.opacity(0.70), Color(UIColor.systemGray5)) 44 | case .red: 45 | return (ColorScheme.darkColor, ColorScheme.medColor, ColorScheme.lightColor) 46 | case .orange: 47 | return (ColorScheme.darkBlue, ColorScheme.medBlue, ColorScheme.lightBlue) 48 | case .green: 49 | return (ColorScheme.darkOrange, ColorScheme.medOrange, ColorScheme.lightOrange) 50 | case .blue: 51 | return (ColorScheme.darkRed, ColorScheme.medRed, ColorScheme.lightRed) 52 | case .violet: 53 | return (ColorScheme.darkViolet, ColorScheme.medViolet, ColorScheme.lightViolet) 54 | case .pink: 55 | return (ColorScheme.darkPink, ColorScheme.medPink, ColorScheme.lightPink) 56 | case .newColor: 57 | return (ColorScheme.darkMulti1, ColorScheme.medMulti1, ColorScheme.lightMulti1) 58 | } 59 | } 60 | } 61 | 62 | struct ColorScheme { 63 | // Base Colors 64 | static let darkColor = Color(red: 0.50, green: 0.02, blue: 0.02) 65 | static let medColor = Color(red: 0.92, green: 0.30, blue: 0.28) 66 | static let lightColor = Color(red: 1.00, green: 0.65, blue: 0.67) 67 | 68 | // Blue Palette 69 | static let darkBlue = Color(red: 0.80, green: 0.40, blue: 0.05) 70 | static let medBlue = Color(red: 0.95, green: 0.70, blue: 0.25) 71 | static let lightBlue = Color(red: 1.00, green: 0.85, blue: 0.50) 72 | 73 | // Green Palette 74 | static let darkGreen = Color(red: 0.70, green: 0.45, blue: 0.05) 75 | static let medGreen = Color(red: 0.95, green: 0.90, blue: 0.45) 76 | static let lightGreen = Color(red: 1.00, green: 0.95, blue: 0.85) 77 | 78 | // Orange Palette 79 | static let darkOrange = Color(red: 0.12, green: 0.40, blue: 0.05) 80 | static let medOrange = Color(red: 0.35, green: 0.75, blue: 0.30) 81 | static let lightOrange = Color(red: 0.70, green: 0.98, blue: 0.68) 82 | 83 | // Red Palette 84 | static let darkRed = Color(red: 0.10, green: 0.20, blue: 0.80) 85 | static let medRed = Color(red: 0.30, green: 0.50, blue: 0.90) 86 | static let lightRed = Color(red: 0.60, green: 0.80, blue: 1.00) 87 | 88 | // Violet Palette 89 | static let darkViolet = Color(red: 0.10, green: 0.05, blue: 0.50) 90 | static let medViolet = Color(red: 0.65, green: 0.50, blue: 0.90) 91 | static let lightViolet = Color(red: 0.95, green: 0.85, blue: 1.00) 92 | 93 | // Pink Palette 94 | static let darkPink = Color(red: 0.70, green: 0.25, blue: 0.45) 95 | static let medPink = Color(red: 0.95, green: 0.50, blue: 0.75) 96 | static let lightPink = Color(red: 1.00, green: 0.85, blue: 0.90) 97 | 98 | // Multi Palette 99 | static let darkMulti1 = Color(red: 0.05, green: 0.25, blue: 0.45) 100 | static let medMulti1 = Color(red: 0.45, green: 0.75, blue: 0.95) 101 | static let lightMulti1 = Color(red: 0.85, green: 0.95, blue: 0.99) 102 | } 103 | -------------------------------------------------------------------------------- /iOS/ReminderX/Managers/KeychainManager.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Security 3 | 4 | class KeychainManager { 5 | static func save(_ data: Data, service: String, account: String) -> OSStatus { 6 | let query: [String: Any] = [ 7 | kSecClass as String: kSecClassGenericPassword, 8 | kSecAttrService as String: service, 9 | kSecAttrAccount as String: account, 10 | kSecValueData as String: data 11 | ] 12 | 13 | SecItemDelete(query as CFDictionary) 14 | return SecItemAdd(query as CFDictionary, nil) 15 | } 16 | 17 | static func load(service: String, account: String) -> Data? { 18 | let query: [String: Any] = [ 19 | kSecClass as String: kSecClassGenericPassword, 20 | kSecAttrService as String: service, 21 | kSecAttrAccount as String: account, 22 | kSecReturnData as String: kCFBooleanTrue!, 23 | kSecMatchLimit as String: kSecMatchLimitOne 24 | ] 25 | 26 | var item: CFTypeRef? 27 | let status = SecItemCopyMatching(query as CFDictionary, &item) 28 | guard status == noErr else { return nil } 29 | 30 | return (item as? Data) 31 | } 32 | static func delete(service: String, account: String) -> OSStatus { 33 | let query = [ 34 | kSecClass as String: kSecClassGenericPassword, 35 | kSecAttrService as String: service, 36 | kSecAttrAccount as String: account 37 | ] as [String: Any] 38 | 39 | return SecItemDelete(query as CFDictionary) 40 | } 41 | static func loadAuthKey() -> String? { 42 | guard let authKeyData = load(service: "YourAppService", account: "authKey"), 43 | let authKey = String(data: authKeyData, encoding: .utf8) else { 44 | return nil 45 | } 46 | return authKey 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /iOS/ReminderX/Managers/NetworkManager.swift: -------------------------------------------------------------------------------- 1 | import Alamofire 2 | import Network 3 | import Foundation 4 | import Security 5 | 6 | class NetworkManager { 7 | static func createWorkout(memberId: Int, workoutName: String, exerciseIds: [Int], authKey: String, completion: @escaping (Result) -> Void) { 8 | guard let url = URL(string: "http://192.168.0.139:3000/createWorkout/\(memberId)") else { 9 | print("Invalid URL") 10 | completion(.failure(NetworkError.invalidURL)) 11 | return 12 | } 13 | 14 | var request = URLRequest(url: url) 15 | request.httpMethod = "POST" 16 | request.addValue("application/json", forHTTPHeaderField: "Content-Type") 17 | request.addValue(authKey, forHTTPHeaderField: "Auth-Key") 18 | 19 | let body: [String: Any] = [ 20 | "workoutName": workoutName, 21 | "exerciseIds": exerciseIds 22 | ] 23 | 24 | do { 25 | request.httpBody = try JSONSerialization.data(withJSONObject: body, options: []) 26 | } catch { 27 | print("Error encoding request body: \(error)") 28 | completion(.failure(error)) 29 | return 30 | } 31 | 32 | URLSession.shared.dataTask(with: request) { data, response, error in 33 | if let error = error { 34 | print("Error creating workout: \(error.localizedDescription)") 35 | completion(.failure(error)) 36 | return 37 | } 38 | 39 | guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { 40 | print("Unexpected response status code") 41 | completion(.failure(NetworkError.serverError)) 42 | return 43 | } 44 | 45 | completion(.success(())) 46 | }.resume() 47 | } 48 | static func fetchDetailedWorkoutData(workoutId: Int, authKey: String, completion: @escaping (Result) -> Void) { 49 | guard let url = URL(string: "http://192.168.0.139:3000/detailedWorkout/\(workoutId)") else { 50 | print("Invalid URL") 51 | completion(.failure(NetworkError.invalidURL)) 52 | return 53 | } 54 | 55 | var request = URLRequest(url: url) 56 | request.httpMethod = "GET" 57 | request.addValue("application/json", forHTTPHeaderField: "Content-Type") 58 | request.addValue(authKey, forHTTPHeaderField: "Auth-Key") 59 | 60 | URLSession.shared.dataTask(with: request) { data, response, error in 61 | if let error = error { 62 | print("Error fetching detailed workout data: \(error.localizedDescription)") 63 | completion(.failure(error)) 64 | return 65 | } 66 | 67 | guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200, let data = data else { 68 | print("Unexpected response status code or no data") 69 | completion(.failure(NetworkError.unexpectedResponse)) 70 | return 71 | } 72 | 73 | do { 74 | let detailedWorkout = try JSONDecoder().decode(DetailedWorkout.self, from: data) 75 | completion(.success(detailedWorkout)) 76 | } catch { 77 | print("Error decoding detailed workout data: \(error)") 78 | completion(.failure(error)) 79 | } 80 | }.resume() 81 | } 82 | static func fetchWorkoutsForMember(memberId: Int, authKey: String, completion: @escaping (Result<[Workout], Error>) -> Void) { 83 | guard let url = URL(string: "http://192.168.0.139:3000/workouts/\(memberId)") else { 84 | print("Invalid URL") 85 | completion(.failure(NetworkError.invalidURL)) 86 | return 87 | } 88 | 89 | var request = URLRequest(url: url) 90 | request.httpMethod = "GET" 91 | request.addValue("application/json", forHTTPHeaderField: "Content-Type") 92 | request.addValue(authKey, forHTTPHeaderField: "Auth-Key") 93 | 94 | URLSession.shared.dataTask(with: request) { data, response, error in 95 | if let error = error { 96 | print("Error fetching workouts: \(error.localizedDescription)") 97 | completion(.failure(error)) 98 | return 99 | } 100 | 101 | if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200, let data = data { 102 | do { 103 | let workouts = try JSONDecoder().decode([Workout].self, from: data) 104 | completion(.success(workouts)) 105 | } catch { 106 | print("Error decoding workouts: \(error)") 107 | completion(.failure(error)) 108 | } 109 | } else { 110 | print("Unexpected response status code or no data") 111 | completion(.failure(NetworkError.unexpectedResponse)) 112 | } 113 | }.resume() 114 | } 115 | static func fetchSafeData(for memberId: Int, authKey: String, completion: @escaping (Result) -> Void) { 116 | guard let url = URL(string: "http://192.168.0.139:3000/fetchSafeData/\(memberId)") else { 117 | print("Invalid URL") 118 | completion(.failure(NetworkError.invalidURL)) 119 | return 120 | } 121 | 122 | var request = URLRequest(url: url) 123 | request.httpMethod = "GET" 124 | request.addValue("application/json", forHTTPHeaderField: "Content-Type") 125 | request.addValue(authKey, forHTTPHeaderField: "Auth-Key") 126 | 127 | URLSession.shared.dataTask(with: request) { data, response, error in 128 | if let error = error { 129 | print("Error fetching safe data: \(error.localizedDescription)") 130 | completion(.failure(error)) 131 | return 132 | } 133 | 134 | if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200, let data = data { 135 | do { 136 | let safeData = try JSONDecoder().decode(SafeDataResponse.self, from: data) 137 | completion(.success(safeData)) 138 | } catch { 139 | print("Error decoding safe data: \(error)") 140 | completion(.failure(error)) 141 | } 142 | } else { 143 | print("Unexpected response status code or no data") 144 | completion(.failure(NetworkError.unexpectedResponse)) 145 | } 146 | }.resume() 147 | } 148 | static func fetchMemberMetrics(memberId: Int, authKey: String, completion: @escaping (Result<[MemberMetric], Error>) -> Void) { 149 | guard let url = URL(string: "http://192.168.0.139:3000/membersMetrics/\(memberId)") else { 150 | print("Invalid URL") 151 | completion(.failure(NetworkError.invalidURL)) 152 | return 153 | } 154 | 155 | var request = URLRequest(url: url) 156 | request.httpMethod = "GET" 157 | request.addValue("application/json", forHTTPHeaderField: "Content-Type") 158 | request.addValue(authKey, forHTTPHeaderField: "Auth-Key") 159 | 160 | URLSession.shared.dataTask(with: request) { data, response, error in 161 | if let error = error { 162 | print("Error fetching member metrics: \(error.localizedDescription)") 163 | completion(.failure(error)) 164 | return 165 | } 166 | 167 | guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200, let data = data else { 168 | print("Unexpected response status code or no data") 169 | completion(.failure(NetworkError.unexpectedResponse)) 170 | return 171 | } 172 | 173 | do { 174 | let memberMetrics = try JSONDecoder().decode([MemberMetric].self, from: data) 175 | completion(.success(memberMetrics)) 176 | } catch { 177 | print("Error decoding member metrics: \(error)") 178 | completion(.failure(error)) 179 | } 180 | }.resume() 181 | } 182 | static func fetchUserDataAndMetrics(memberId: Int, authKey: String, completion: @escaping (UserInfo?) -> Void) { 183 | guard let url = URL(string: "http://192.168.0.139:3000/userDataAndMetrics/\(memberId)") else { 184 | print("Invalid URL") 185 | completion(nil) 186 | return 187 | } 188 | 189 | var request = URLRequest(url: url) 190 | request.httpMethod = "GET" 191 | request.addValue("application/json", forHTTPHeaderField: "Content-Type") 192 | request.addValue(authKey, forHTTPHeaderField: "Auth-Key") 193 | 194 | print("Fetching user data and metrics for memberId: \(memberId) with authKey: \(authKey)") 195 | 196 | URLSession.shared.dataTask(with: request) { data, response, error in 197 | if let error = error { 198 | print("Error fetching user data: \(error.localizedDescription)") 199 | completion(nil) 200 | return 201 | } 202 | 203 | if let httpResponse = response as? HTTPURLResponse { 204 | print("Response status code: \(httpResponse.statusCode)") 205 | if httpResponse.statusCode == 200, let data = data { 206 | do { 207 | let userData = try JSONDecoder().decode(UserInfo.self, from: data) 208 | completion(userData) 209 | } catch { 210 | print("Error decoding user data: \(error)") 211 | completion(nil) 212 | } 213 | } else { 214 | print("Unexpected response status code") 215 | completion(nil) 216 | } 217 | } else { 218 | print("No HTTP response received") 219 | completion(nil) 220 | } 221 | }.resume() 222 | } 223 | static func signUpUser(with data: [String: Any], completion: @escaping (Result) -> Void) { 224 | guard let url = URL(string: "http://192.168.0.139:3000/signup") else { 225 | print("Invalid URL") 226 | completion(.failure(NetworkError.invalidURL)) 227 | return 228 | } 229 | 230 | var request = URLRequest(url: url) 231 | request.httpMethod = "POST" 232 | request.addValue("application/json", forHTTPHeaderField: "Content-Type") 233 | 234 | do { 235 | // Log the outgoing data for debugging 236 | print("Sending signup data: \(data)") 237 | request.httpBody = try JSONSerialization.data(withJSONObject: data, options: []) 238 | } catch { 239 | print("Error encoding request body: \(error.localizedDescription)") 240 | completion(.failure(error)) 241 | return 242 | } 243 | 244 | URLSession.shared.dataTask(with: request) { data, response, error in 245 | if let error = error { 246 | print("Error during sign up: \(error.localizedDescription)") 247 | completion(.failure(error)) 248 | return 249 | } 250 | 251 | guard let httpResponse = response as? HTTPURLResponse else { 252 | print("No response from server") 253 | completion(.failure(NetworkError.serverError)) 254 | return 255 | } 256 | 257 | if httpResponse.statusCode == 201 { 258 | print("Sign up successful with status code 201") 259 | completion(.success(())) 260 | } else { 261 | // Log detailed error information if available 262 | if let data = data, let errorResponse = String(data: data, encoding: .utf8) { 263 | print("Server error response: \(errorResponse)") 264 | } 265 | print("Unexpected response status code: \(httpResponse.statusCode)") 266 | completion(.failure(NetworkError.serverError)) 267 | } 268 | }.resume() 269 | } 270 | static func fetchExercises(page: Int, completion: @escaping ([Exercise]) -> Void) { 271 | guard let url = URL(string: "http://192.168.0.139:3000/exercises?page=\(page)") else { 272 | print("Invalid URL") 273 | completion([]) 274 | return 275 | } 276 | 277 | var request = URLRequest(url: url) 278 | request.httpMethod = "GET" 279 | request.addValue("application/json", forHTTPHeaderField: "Content-Type") 280 | 281 | URLSession.shared.dataTask(with: request) { data, response, error in 282 | if let error = error { 283 | print("Error fetching exercises: \(error.localizedDescription)") 284 | completion([]) 285 | return 286 | } 287 | 288 | if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200, let data = data { 289 | do { 290 | let exercises = try JSONDecoder().decode([Exercise].self, from: data) 291 | completion(exercises) 292 | } catch { 293 | print("Error decoding exercises: \(error)") 294 | completion([]) 295 | } 296 | } else { 297 | print("Unexpected response status code or no data") 298 | completion([]) 299 | } 300 | }.resume() 301 | } 302 | static func fetchExercisesForWorkout(workoutId: Int, authKey: String, completion: @escaping (Result<[Exercise], Error>) -> Void) { 303 | guard let url = URL(string: "http://192.168.0.139:3000/exercises/search?workoutId=\(workoutId)") else { 304 | print("Invalid URL") 305 | completion(.failure(NetworkError.invalidURL)) 306 | return 307 | } 308 | 309 | var request = URLRequest(url: url) 310 | request.httpMethod = "GET" 311 | request.addValue("application/json", forHTTPHeaderField: "Content-Type") 312 | request.addValue(authKey, forHTTPHeaderField: "Auth-Key") 313 | 314 | URLSession.shared.dataTask(with: request) { data, response, error in 315 | if let error = error { 316 | print("Error fetching exercises: \(error.localizedDescription)") 317 | completion(.failure(error)) 318 | return 319 | } 320 | 321 | guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200, let data = data else { 322 | print("Unexpected response status code or no data") 323 | completion(.failure(NetworkError.unexpectedResponse)) 324 | return 325 | } 326 | 327 | do { 328 | let exercises = try JSONDecoder().decode([Exercise].self, from: data) 329 | completion(.success(exercises)) 330 | } catch { 331 | print("Error decoding exercises: \(error)") 332 | completion(.failure(error)) 333 | } 334 | }.resume() 335 | } 336 | static func deleteWorkout(workoutId: Int, authKey: String, completion: @escaping (Bool) -> Void) { 337 | guard let url = URL(string: "http://192.168.0.139:3000/workouts/\(workoutId)") else { 338 | print("Invalid URL") 339 | completion(false) 340 | return 341 | } 342 | 343 | var request = URLRequest(url: url) 344 | request.httpMethod = "DELETE" 345 | request.addValue(authKey, forHTTPHeaderField: "Auth-Key") 346 | 347 | URLSession.shared.dataTask(with: request) { _, response, error in 348 | if let error = error { 349 | print("Error deleting workout: \(error.localizedDescription)") 350 | completion(false) 351 | return 352 | } 353 | 354 | if let httpResponse = response as? HTTPURLResponse { 355 | completion(httpResponse.statusCode == 200) 356 | } else { 357 | completion(false) 358 | } 359 | }.resume() 360 | } 361 | 362 | static func renameWorkout(workoutId: Int, newName: String, authKey: String, completion: @escaping (Bool) -> Void) { 363 | guard let url = URL(string: "http://192.168.0.139:3000/workouts/\(workoutId)") else { 364 | print("Invalid URL") 365 | completion(false) 366 | return 367 | } 368 | 369 | var request = URLRequest(url: url) 370 | request.httpMethod = "PUT" 371 | request.addValue("application/json", forHTTPHeaderField: "Content-Type") 372 | request.addValue(authKey, forHTTPHeaderField: "Auth-Key") 373 | let params = ["newName": newName] 374 | request.httpBody = try? JSONSerialization.data(withJSONObject: params, options: []) 375 | 376 | URLSession.shared.dataTask(with: request) { _, response, error in 377 | if let error = error { 378 | print("Error renaming workout: \(error.localizedDescription)") 379 | completion(false) 380 | return 381 | } 382 | 383 | if let httpResponse = response as? HTTPURLResponse { 384 | completion(httpResponse.statusCode == 200) 385 | } else { 386 | completion(false) 387 | } 388 | }.resume() 389 | } 390 | static func logWorkout(memberId: Int, workoutId: Int, time: Int, authKey: String, completion: @escaping (Result) -> Void) { 391 | guard let url = URL(string: "http://192.168.0.139:3000/loggedWorkouts") else { 392 | print("Invalid URL") 393 | completion(.failure(NetworkError.invalidURL)) 394 | return 395 | } 396 | 397 | var request = URLRequest(url: url) 398 | request.httpMethod = "POST" 399 | request.addValue("application/json", forHTTPHeaderField: "Content-Type") 400 | request.addValue(authKey, forHTTPHeaderField: "Auth-Key") 401 | 402 | // Create the body data 403 | let workoutLog = WorkoutLog(memberId: memberId, workoutId: workoutId, time: time) 404 | 405 | do { 406 | let encoder = JSONEncoder() 407 | let jsonData = try encoder.encode(workoutLog) 408 | request.httpBody = jsonData 409 | print("Request Body: \(String(data: jsonData, encoding: .utf8)!)") 410 | } catch { 411 | print("Error encoding workout log: \(error)") 412 | completion(.failure(error)) 413 | return 414 | } 415 | 416 | URLSession.shared.dataTask(with: request) { data, response, error in 417 | if let error = error { 418 | print("Error logging workout: \(error.localizedDescription)") 419 | completion(.failure(error)) 420 | return 421 | } 422 | 423 | if let httpResponse = response as? HTTPURLResponse { 424 | print("Response Status Code: \(httpResponse.statusCode)") 425 | } 426 | 427 | if let data = data, let responseDataString = String(data: data, encoding: .utf8) { 428 | print("Response Data: \(responseDataString)") 429 | } 430 | 431 | guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200, let data = data else { 432 | print("Unexpected response status code or no data") 433 | completion(.failure(NetworkError.unexpectedResponse)) 434 | return 435 | } 436 | 437 | do { 438 | let loggedWorkout = try JSONDecoder().decode(WorkoutLog.self, from: data) 439 | completion(.success(loggedWorkout)) 440 | } catch { 441 | print("Error decoding logged workout: \(error)") 442 | completion(.failure(error)) 443 | } 444 | }.resume() 445 | } 446 | } 447 | -------------------------------------------------------------------------------- /iOS/ReminderX/Preview Content/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronmcleancs/RepBook-DemoServer/c57d7c76c8b96530079b11d507ddc950e936227c/iOS/ReminderX/Preview Content/.DS_Store -------------------------------------------------------------------------------- /iOS/ReminderX/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /iOS/ReminderX/ReminderXApp.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | @main 4 | struct ReminderXApp: App { 5 | 6 | var body: some Scene { 7 | WindowGroup { 8 | ContentView() 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /iOS/ReminderX/Safari.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import SafariServices 3 | 4 | struct SafariView: UIViewControllerRepresentable { 5 | let url: URL 6 | 7 | func makeUIViewController(context: UIViewControllerRepresentableContext) -> SFSafariViewController { 8 | let config = SFSafariViewController.Configuration() 9 | config.entersReaderIfAvailable = false 10 | config.barCollapsingEnabled = true 11 | 12 | let safariViewController = SFSafariViewController(url: url, configuration: config) 13 | safariViewController.preferredControlTintColor = UIColor.systemBlue 14 | safariViewController.dismissButtonStyle = .close 15 | 16 | return safariViewController 17 | } 18 | 19 | func updateUIViewController(_ uiViewController: SFSafariViewController, context: UIViewControllerRepresentableContext) { 20 | } 21 | 22 | static func dismantleUIViewController(_ uiViewController: SFSafariViewController, coordinator: ()) { 23 | uiViewController.dismiss(animated: true) 24 | } 25 | 26 | func makeCoordinator() -> Coordinator { 27 | Coordinator(self) 28 | } 29 | 30 | class Coordinator: NSObject, SFSafariViewControllerDelegate { 31 | let parent: SafariView 32 | 33 | init(_ parent: SafariView) { 34 | self.parent = parent 35 | } 36 | 37 | func safariViewControllerDidFinish(_ controller: SFSafariViewController) { 38 | controller.dismiss(animated: true) 39 | } 40 | } 41 | } 42 | 43 | extension View { 44 | func safariSheet(isPresented: Binding, url: URL) -> some View { 45 | self.sheet(isPresented: isPresented) { 46 | SafariView(url: url) 47 | .edgesIgnoringSafeArea(.all) 48 | .preferredColorScheme(.light) 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /iOS/ReminderX/Utilities/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronmcleancs/RepBook-DemoServer/c57d7c76c8b96530079b11d507ddc950e936227c/iOS/ReminderX/Utilities/.DS_Store -------------------------------------------------------------------------------- /iOS/ReminderX/Utilities/Extensions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftUI 3 | 4 | extension Color { 5 | func towAWDAWDAWDAWDHex() -> String? { 6 | guard let components = self.cgColor?.components else { return nil } 7 | let red = Int(components[0] * 255.0) 8 | let green = Int(components[1] * 255.0) 9 | let blue = Int(components[2] * 255.0) 10 | let hex = String(format: "#%02X%02X%02X", red, green, blue) 11 | return hex 12 | } 13 | 14 | init?(hex: String) { 15 | let scanner = Scanner(string: hex) 16 | if hex.hasPrefix("#") { 17 | scanner.currentIndex = hex.index(after: hex.startIndex) 18 | } 19 | var hexInt: UInt64 = 0 20 | if scanner.scanHexInt64(&hexInt) { 21 | let r = (hexInt & 0xff0000) >> 16 22 | let g = (hexInt & 0x00ff00) >> 8 23 | let b = hexInt & 0x0000ff 24 | self.init(red: Double(r) / 255.0, green: Double(g) / 255.0, blue: Double(b) / 255.0) 25 | } else { 26 | return nil 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /iOS/ReminderX/Utilities/Graph.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | let exampleGraphData = GraphData(points: [45, 25, 18, 22, 30, 15, 68, 14, 23]) 4 | 5 | struct GraphData { 6 | var points: [CGFloat] 7 | 8 | var horizontalPadding: CGFloat { 9 | return (points.max() ?? 0 - (points.min() ?? 0)) * 0.1 10 | } 11 | 12 | func normalizedPoint(index: Int, frame: CGRect) -> CGPoint { 13 | let xPosition = (frame.width - horizontalPadding * 2) * CGFloat(index) / CGFloat(points.count - 1) + horizontalPadding 14 | let yPosition = (1 - (points[index] - minValue) / (maxValue - minValue)) * (frame.height - horizontalPadding * 2) + horizontalPadding 15 | return CGPoint(x: xPosition, y: yPosition) 16 | } 17 | 18 | private var padding: CGFloat { 19 | let range = points.max() ?? 0 - (points.min() ?? 0) 20 | return range * 0.1 21 | } 22 | 23 | var maxValue: CGFloat { 24 | return (points.max() ?? 0) + padding 25 | } 26 | 27 | var minValue: CGFloat { 28 | return (points.min() ?? 0) - padding 29 | } 30 | 31 | var peakIndex: Int? { points.indices.max(by: { points[$0] < points[$1] }) } 32 | var valleyIndex: Int? { points.indices.min(by: { points[$0] < points[$1] }) } 33 | } 34 | 35 | struct LabelView: View { 36 | var text: String 37 | var position: CGPoint 38 | var colorScheme: (dark: Color, med: Color, light: Color) 39 | 40 | var body: some View { 41 | Text(text) 42 | .font(.system(size: 14)) 43 | .fontWeight(.medium) 44 | .padding(5) 45 | .background(colorScheme.light) 46 | .foregroundColor(colorScheme.dark) 47 | .cornerRadius(5) 48 | .position(position) 49 | } 50 | } 51 | 52 | struct LineGraph: View { 53 | var data: GraphData 54 | var colorScheme: (dark: Color, med: Color, light: Color) 55 | 56 | @State private var graphProgressLine: CGFloat = 0 57 | @State private var graphOpacityFill: Double = 0 58 | 59 | var body: some View { 60 | ZStack { 61 | 62 | GeometryReader { geometry in 63 | let frame = geometry.frame(in: .local) 64 | Path { path in 65 | let firstPoint = data.normalizedPoint(index: 0, frame: frame) 66 | path.move(to: firstPoint) 67 | 68 | for index in data.points.indices { 69 | let nextPoint = data.normalizedPoint(index: index, frame: frame) 70 | path.addLine(to: nextPoint) 71 | } 72 | 73 | let lastIndex = data.points.count - 1 74 | let lastPoint = data.normalizedPoint(index: lastIndex, frame: frame) 75 | path.addLine(to: CGPoint(x: lastPoint.x, y: frame.height)) 76 | path.addLine(to: CGPoint(x: firstPoint.x, y: frame.height)) 77 | path.closeSubpath() 78 | } 79 | .fill( 80 | LinearGradient( 81 | gradient: Gradient(colors: [colorScheme.light, .white]), 82 | startPoint: .top, 83 | endPoint: .bottom 84 | ) 85 | ) 86 | .opacity(graphOpacityFill) 87 | .animation( 88 | .easeInOut(duration: 1), 89 | value: graphOpacityFill 90 | ) 91 | } 92 | 93 | // Graph Line 94 | GeometryReader { geometry in 95 | Path { path in 96 | let firstPoint = data.normalizedPoint(index: 0, frame: geometry.frame(in: .local)) 97 | path.move(to: firstPoint) 98 | 99 | 100 | for index in data.points.indices { 101 | let nextPoint = data.normalizedPoint(index: index, frame: geometry.frame(in: .local)) 102 | path.addLine(to: nextPoint) 103 | } 104 | } 105 | .trim(from: 0, to: graphProgressLine) 106 | .stroke( 107 | colorScheme.light, 108 | style: StrokeStyle(lineWidth: 2, lineCap: .round, lineJoin: .round) 109 | ) 110 | .animation( 111 | .easeInOut(duration: 1), 112 | value: graphProgressLine 113 | ) 114 | } 115 | 116 | 117 | GraphPoints(data: data, colorScheme: colorScheme, graphProgress: graphProgressLine) 118 | } 119 | .clipped() 120 | .padding(.all, 15) 121 | .onAppear { 122 | withAnimation(.easeInOut(duration: 1)) { 123 | graphProgressLine = 1 124 | } 125 | 126 | DispatchQueue.main.asyncAfter(deadline: .now() + 1) { 127 | withAnimation(.easeInOut(duration: 1)) { 128 | graphOpacityFill = 1 129 | } 130 | } 131 | } 132 | } 133 | } 134 | 135 | struct CustomGraphCardView: View { 136 | var currentColorScheme: (dark: Color, med: Color, light: Color) 137 | 138 | var body: some View { 139 | VStack(alignment: .leading) { 140 | VStack(alignment: .leading) { 141 | Text("Main Title") 142 | .font(.title) 143 | .fontWeight(.bold) 144 | .foregroundColor(currentColorScheme.med.opacity(0.8)) 145 | 146 | Text("Subtitle") 147 | .font(.subheadline) 148 | .foregroundColor(Color(UIColor.systemGray2)) 149 | } 150 | .padding(.top) 151 | 152 | LineGraph(data: exampleGraphData, colorScheme: currentColorScheme) 153 | .frame(maxWidth: .infinity, maxHeight: .infinity) 154 | } 155 | .frame(width: UIScreen.main.bounds.width * 0.8, height: UIScreen.main.bounds.width * 0.6) 156 | .padding(.vertical, 0) 157 | } 158 | } 159 | 160 | struct GraphPoints: View { 161 | var data: GraphData 162 | var colorScheme: (dark: Color, med: Color, light: Color) 163 | var graphProgress: CGFloat 164 | 165 | var body: some View { 166 | GeometryReader { geometry in 167 | ForEach(data.points.indices, id: \.self) { index in 168 | if CGFloat(index) / CGFloat(data.points.count - 1) <= graphProgress { 169 | Circle() 170 | .frame(width: 8, height: 8) 171 | .foregroundColor(colorScheme.dark) 172 | .position(data.normalizedPoint(index: index, frame: geometry.frame(in: .local))) 173 | } 174 | } 175 | 176 | if let peakIndex = data.peakIndex, CGFloat(peakIndex) / CGFloat(data.points.count - 1) <= graphProgress { 177 | LabelView( 178 | text: "\(Int(data.points[peakIndex]))", 179 | position: adjustedLabelPosition(index: peakIndex, frame: geometry.frame(in: .local), geometry: geometry), 180 | colorScheme: colorScheme 181 | ) 182 | .zIndex(1) 183 | } 184 | 185 | // Valley Label 186 | if let valleyIndex = data.valleyIndex, CGFloat(valleyIndex) / CGFloat(data.points.count - 1) <= graphProgress { 187 | LabelView( 188 | text: "\(Int(data.points[valleyIndex]))", 189 | position: adjustedLabelPosition(index: valleyIndex, frame: geometry.frame(in: .local), geometry: geometry), 190 | colorScheme: colorScheme 191 | ) 192 | .zIndex(1) 193 | } 194 | } 195 | } 196 | 197 | private func adjustedLabelPosition(index: Int, frame: CGRect, geometry: GeometryProxy) -> CGPoint { 198 | var position = data.normalizedPoint(index: index, frame: frame) 199 | if position.x < 20 { position.x = 30 } 200 | if position.x > geometry.size.width - 30 { position.x = geometry.size.width - 30 } 201 | return position 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /iOS/ReminderX/Utilities/Models.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftUI 3 | import Combine 4 | 5 | enum NetworkError: Error { 6 | case invalidURL 7 | case serverError 8 | case unexpectedResponse 9 | } 10 | 11 | struct SafeDataResponse: Decodable { 12 | let firstName: String 13 | let dateOfBirth: String 14 | let workouts: [WorkoutDetail] 15 | } 16 | 17 | struct WorkoutDetail: Decodable { 18 | let workoutName: String 19 | let exerciseTitles: [String] 20 | } 21 | 22 | struct MemberMetric: Codable { 23 | let memberId: Int 24 | let heightCm: Int 25 | let weightKg: Float 26 | let gender: String 27 | let workoutFrequency: Int 28 | let bodyFatPercentage: Float? 29 | let goalType: String? 30 | let activityLevel: String? 31 | let restingHeartRate: Int? 32 | let bmrCalories: Float? 33 | 34 | enum CodingKeys: String, CodingKey { 35 | case memberId = "member_id" 36 | case heightCm = "height_cm" 37 | case weightKg = "weight_kg" 38 | case gender 39 | case workoutFrequency = "workout_frequency" 40 | case bodyFatPercentage = "body_fat_percentage" 41 | case goalType = "goal_type" 42 | case activityLevel = "activity_level" 43 | case restingHeartRate = "resting_heart_rate" 44 | case bmrCalories = "bmr_calories" 45 | } 46 | 47 | init(from decoder: Decoder) throws { 48 | let container = try decoder.container(keyedBy: CodingKeys.self) 49 | 50 | memberId = try container.decode(Int.self, forKey: .memberId) 51 | heightCm = try container.decode(Int.self, forKey: .heightCm) 52 | gender = try container.decode(String.self, forKey: .gender) 53 | workoutFrequency = try container.decode(Int.self, forKey: .workoutFrequency) 54 | 55 | if let weightFloat = try? container.decode(Float.self, forKey: .weightKg) { 56 | weightKg = weightFloat 57 | } else if let weightString = try? container.decode(String.self, forKey: .weightKg), 58 | let weightFloat = Float(weightString) { 59 | weightKg = weightFloat 60 | } else { 61 | throw DecodingError.typeMismatch( 62 | Float.self, 63 | DecodingError.Context( 64 | codingPath: [CodingKeys.weightKg], 65 | debugDescription: "Expected Float or String convertible to Float for weightKg" 66 | ) 67 | ) 68 | } 69 | 70 | bodyFatPercentage = try? container.decode(Float.self, forKey: .bodyFatPercentage) 71 | goalType = try? container.decode(String.self, forKey: .goalType) 72 | activityLevel = try? container.decode(String.self, forKey: .activityLevel) 73 | restingHeartRate = try? container.decode(Int.self, forKey: .restingHeartRate) 74 | bmrCalories = try? container.decode(Float.self, forKey: .bmrCalories) 75 | } 76 | } 77 | struct DetailedWorkout: Codable { 78 | let workoutId: Int 79 | let memberId: Int 80 | let workoutName: String 81 | let exercises: [ExerciseDetails] 82 | 83 | enum CodingKeys: String, CodingKey { 84 | case workoutId = "workout_id" 85 | case memberId = "member_id" 86 | case workoutName = "workout_name" 87 | case exercises 88 | } 89 | } 90 | 91 | struct ExerciseDetails: Codable, Identifiable { 92 | let id: Int 93 | let title: String 94 | let equipment: String 95 | let difficulty: String 96 | 97 | enum CodingKeys: String, CodingKey { 98 | case id 99 | case title 100 | case equipment 101 | case difficulty 102 | } 103 | } 104 | 105 | struct Achievement: Identifiable { 106 | let id: Int 107 | let image: String 108 | let title: String 109 | let subtitle: String 110 | } 111 | 112 | struct UserInfo: Decodable { 113 | var firstName: String 114 | var lastName: String 115 | var dateOfBirth: String 116 | var username: String 117 | var email: String 118 | 119 | enum CodingKeys: String, CodingKey { 120 | case firstName = "first_name" 121 | case lastName = "last_name" 122 | case dateOfBirth = "date_of_birth" 123 | case username 124 | case email 125 | } 126 | } 127 | 128 | struct Workout: Codable { 129 | let workoutId: Int 130 | let memberId: Int 131 | let workoutName: String 132 | let exerciseIds: [Int] 133 | 134 | enum CodingKeys: String, CodingKey { 135 | case workoutId = "workout_id" 136 | case memberId = "member_id" 137 | case workoutName = "workout_name" 138 | case exerciseIds = "exercise_ids" 139 | } 140 | } 141 | 142 | struct Exercise: Identifiable, Decodable { 143 | let id: Int 144 | let title: String 145 | let equipment: String 146 | let difficulty: String 147 | 148 | enum CodingKeys: String, CodingKey { 149 | case id 150 | case title 151 | case equipment 152 | case difficulty 153 | } 154 | } 155 | 156 | struct WorkoutLog: Codable { 157 | let memberId: Int 158 | let workoutId: Int 159 | let time: Int 160 | } 161 | 162 | enum WeightUnit: String, CaseIterable { 163 | case lbs = "lbs" 164 | case kg = "kg" 165 | } 166 | 167 | struct SignupResponse: Decodable { 168 | let memberId: Int 169 | let authKey: String 170 | } 171 | 172 | struct LoginResponse: Decodable { 173 | let memberId: Int 174 | let authKey: String 175 | 176 | enum CodingKeys: String, CodingKey { 177 | case memberId = "member_id" 178 | case authKey = "auth_key" 179 | } 180 | } 181 | 182 | class AppViewModel: ObservableObject { 183 | @Published var isAuthenticated: Bool = false 184 | } 185 | 186 | struct TagView: View { 187 | var text: String 188 | var color: Color 189 | 190 | var body: some View { 191 | Text(text) 192 | .font(.caption) 193 | .padding(5) 194 | .background(RoundedRectangle(cornerRadius: 10).fill(color.opacity(0.04))) 195 | .foregroundColor(color) 196 | .overlay( 197 | RoundedRectangle(cornerRadius: 10) 198 | .stroke(color, lineWidth: 1) 199 | ) 200 | } 201 | } 202 | 203 | struct CodableColor: Codable { 204 | var color: Color 205 | 206 | enum CodingKeys: String, CodingKey { 207 | case color 208 | } 209 | 210 | init(color: Color) { 211 | self.color = color 212 | } 213 | 214 | init(from decoder: Decoder) throws { 215 | let container = try decoder.container(keyedBy: CodingKeys.self) 216 | let colorData = try container.decode(String.self, forKey: .color) 217 | self.color = Color(hex: colorData) ?? Color.white 218 | } 219 | 220 | func encode(to encoder: Encoder) throws { 221 | var container = encoder.container(keyedBy: CodingKeys.self) 222 | } 223 | } 224 | 225 | struct Reminder: Identifiable, Codable { 226 | var id = UUID() 227 | var title: String 228 | var dueDate: Date 229 | } 230 | -------------------------------------------------------------------------------- /iOS/ReminderX/Utilities/VisualComponents.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftUI 3 | 4 | struct VisualEffectBlur: UIViewRepresentable { 5 | var blurStyle: UIBlurEffect.Style 6 | 7 | func makeUIView(context: Context) -> UIVisualEffectView { 8 | UIVisualEffectView(effect: UIBlurEffect(style: blurStyle)) 9 | } 10 | 11 | func updateUIView(_ uiView: UIVisualEffectView, context: Context) { 12 | uiView.effect = UIBlurEffect(style: blurStyle) 13 | } 14 | } 15 | 16 | extension Color { 17 | static let offWhite = Color(red: 225 / 255, green: 225 / 255, blue: 235 / 255) 18 | } 19 | 20 | struct NeumorphicGraphCardView: View { 21 | var data: GraphData 22 | var colorScheme: (dark: Color, med: Color, light: Color) 23 | 24 | var body: some View { 25 | RoundedRectangle(cornerRadius: 25) 26 | .fill(Color.offWhite) 27 | .frame(width: 350, height: 350) 28 | .shadow(color: Color.black.opacity(0.2), radius: 10, x: 10, y: 10) 29 | .shadow(color: Color.white.opacity(0.7), radius: 10, x: -5, y: -5) 30 | .overlay( 31 | LineGraph(data: data, colorScheme: colorScheme) 32 | ) 33 | } 34 | } 35 | 36 | struct WeightEntryView: View { 37 | @Binding var weight: String 38 | @Binding var unit: WeightUnit 39 | 40 | var body: some View { 41 | HStack { 42 | TextField("Weight", text: $weight) 43 | .keyboardType(.decimalPad) 44 | .onChange(of: weight) { newValue in 45 | let filtered = newValue.filter { "0123456789.".contains($0) } 46 | weight = filtered 47 | } 48 | .frame(width: 80) 49 | .textFieldStyle(RoundedBorderTextFieldStyle()) 50 | 51 | Picker("Unit", selection: $unit) { 52 | Text("lb").tag(WeightUnit.lbs) 53 | Text("kg").tag(WeightUnit.kg) 54 | } 55 | .pickerStyle(SegmentedPickerStyle()) 56 | .frame(width: 100) 57 | } 58 | } 59 | } 60 | 61 | struct HeightEntryView: View { 62 | @Binding var heightFeet: String 63 | @Binding var heightInches: String 64 | 65 | var body: some View { 66 | HStack { 67 | TextField("Feet", text: $heightFeet) 68 | .keyboardType(.numberPad) 69 | .onChange(of: heightFeet) { newValue in 70 | let filtered = newValue.filter { "0123456789".contains($0) } 71 | if let intValue = Int(filtered), intValue <= 9 { 72 | heightFeet = filtered 73 | } else { 74 | heightFeet = String(filtered.prefix(1)) 75 | } 76 | } 77 | .frame(width: 50) 78 | .textFieldStyle(RoundedBorderTextFieldStyle()) 79 | 80 | Text("ft") 81 | 82 | TextField("Inches", text: $heightInches) 83 | .keyboardType(.numberPad) 84 | .onChange(of: heightInches) { newValue in 85 | let filtered = newValue.filter { "0123456789".contains($0) } 86 | if let intValue = Int(filtered), intValue <= 12 { 87 | heightInches = filtered 88 | } else { 89 | heightInches = String(filtered.prefix(2)) 90 | } 91 | } 92 | .frame(width: 50) 93 | .textFieldStyle(RoundedBorderTextFieldStyle()) 94 | 95 | Text("in") 96 | } 97 | } 98 | } 99 | 100 | struct WorkoutPreviewCardView: View { 101 | @Binding var workoutName: String 102 | @Binding var selectedExercises: [Exercise] 103 | let colorScheme = ColorSchemeManager.shared.currentColorScheme 104 | var body: some View { 105 | VStack(alignment: .leading) { 106 | HStack { 107 | TextField("Workout Name", text: $workoutName) 108 | .font(.largeTitle) 109 | .fontWeight(.bold) 110 | Spacer() 111 | Text("\(selectedExercises.count) Exercises") 112 | .foregroundColor(.gray) 113 | .padding(.trailing) 114 | } 115 | .padding(5) 116 | ScrollView(.horizontal, showsIndicators: false) { 117 | HStack(spacing: 15) { 118 | ForEach(selectedExercises) { exercise in 119 | ExerciseCardView(exercise: exercise, onRemove: { 120 | if let index = selectedExercises.firstIndex(where: { $0.id == exercise.id }) { 121 | selectedExercises.remove(at: index) 122 | } 123 | }) 124 | } 125 | } 126 | } 127 | } 128 | .padding() 129 | } 130 | } 131 | 132 | struct ExerciseCardView: View { 133 | var exercise: Exercise 134 | var onRemove: () -> Void 135 | @State private var reps: Int = 10 136 | let colorScheme = ColorSchemeManager.shared.currentColorScheme 137 | 138 | private let cardWidth: CGFloat = 270 139 | private let cardHeight: CGFloat = 100 140 | 141 | var body: some View { 142 | VStack(alignment: .leading, spacing: 5) { 143 | Text(exercise.title) 144 | .font(.title3) 145 | .fontWeight(.bold) 146 | .foregroundColor(.primary) 147 | .lineLimit(1) 148 | .truncationMode(.tail) 149 | 150 | Text("Equipment: \(exercise.equipment), Difficulty: \(exercise.difficulty)") 151 | .font(.footnote) 152 | .foregroundColor(.gray) 153 | .lineLimit(1) 154 | .truncationMode(.tail) 155 | 156 | Spacer() 157 | 158 | HStack { 159 | Spacer() 160 | Button(action: onRemove) { 161 | Image(systemName: "trash") 162 | .foregroundColor(Color.gray) 163 | } 164 | } 165 | } 166 | .padding() 167 | .frame(width: cardWidth, height: cardHeight) 168 | .background(Color.gray.opacity(0.05)) 169 | .cornerRadius(15) 170 | .shadow(radius: 5) 171 | } 172 | } 173 | 174 | struct ExerciseCard: View { 175 | let exercise: Exercise 176 | let colorScheme = ColorSchemeManager.shared.currentColorScheme 177 | var onAdd: () -> Void 178 | 179 | var body: some View { 180 | HStack { 181 | VStack(alignment: .leading, spacing: 5) { 182 | Text(exercise.title) 183 | .font(.headline) 184 | } 185 | 186 | Spacer() 187 | 188 | Button(action: onAdd) { 189 | Image(systemName: "plus") 190 | .resizable() 191 | .scaledToFit() 192 | .frame(width: 12, height: 12) 193 | .padding(9) 194 | .foregroundColor(.black.opacity(0.7)) 195 | .cornerRadius(10) 196 | } 197 | .padding(.trailing, 10) 198 | } 199 | .padding() 200 | .background(RoundedRectangle(cornerRadius: 20).fill(Color.gray.opacity(0.05))) 201 | .padding(.horizontal) 202 | } 203 | } 204 | 205 | 206 | struct CompressedExerciseCard: View { 207 | let exercise: Exercise 208 | 209 | var body: some View { 210 | VStack(alignment: .leading) { 211 | Text(exercise.title) 212 | .font(.headline) 213 | Text("Difficulty: \(exercise.difficulty)") 214 | .font(.subheadline) 215 | } 216 | .padding() 217 | .frame(width: 150, height: 80) 218 | .background(RoundedRectangle(cornerRadius: 15).fill(Color.gray.opacity(0.05))) 219 | } 220 | } 221 | 222 | 223 | struct CustomTabBar: View { 224 | @Binding var selection: Int 225 | 226 | var body: some View { 227 | HStack(spacing: 0) { 228 | ForEach(0..<4) { index in 229 | Button(action: { 230 | withAnimation(.easeInOut(duration: 0.1)) { 231 | selection = index 232 | } 233 | }) { 234 | VStack { 235 | Image(systemName: tabImageName(for: index)) 236 | .resizable() 237 | .aspectRatio(contentMode: .fit) 238 | .frame(width: selection == index ? 32 : 24, height: selection == index ? 32 : 24) 239 | .foregroundColor(selection == index ? .black.opacity(0.7) : .gray.opacity(0.45)) 240 | } 241 | .frame(maxWidth: .infinity, maxHeight: .infinity) 242 | } 243 | .frame(maxWidth: .infinity) 244 | } 245 | } 246 | .frame(height: 60) 247 | .background( 248 | VisualEffectBlur(blurStyle: .systemThinMaterial) 249 | .clipShape(RoundedRectangle(cornerRadius: 19)) 250 | .padding([.leading, .trailing], 20) 251 | ) 252 | .shadow(color: .black.opacity(0.15), radius: 10, x: 0, y: 5) 253 | } 254 | 255 | func tabImageName(for index: Int) -> String { 256 | switch index { 257 | case 0: 258 | return "calendar.day.timeline.leading" 259 | case 1: 260 | return "figure.strengthtraining.traditional" 261 | case 2: 262 | return "message.fill" 263 | case 3: 264 | return "gearshape.fill" 265 | default: 266 | return "" 267 | } 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /iOS/ReminderX/Views/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronmcleancs/RepBook-DemoServer/c57d7c76c8b96530079b11d507ddc950e936227c/iOS/ReminderX/Views/.DS_Store -------------------------------------------------------------------------------- /iOS/ReminderX/Views/AiView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import Foundation 3 | import ChatGPTSwift 4 | 5 | struct ChatMessage: Identifiable, Codable { 6 | let id = UUID() 7 | let text: String 8 | let isUser: Bool 9 | var webLinks: [WebLink]? 10 | } 11 | 12 | struct WebLink: Identifiable, Codable { 13 | let id = UUID() 14 | let url: String 15 | let title: String 16 | let index: Int 17 | } 18 | 19 | struct AiView: View { 20 | @State private var userMessage = "" 21 | @State private var messages: [ChatMessage] = [] 22 | @State private var isWaitingForResponse = false 23 | @State private var showMiniCards = false 24 | @State private var keyboardHeight: CGFloat = 0 25 | @State private var navbarVisible: Bool = true 26 | @State private var isTyping = false 27 | @State private var typingMessage = "" 28 | @State private var lastSentMessageDate = Date(timeIntervalSince1970: 0) 29 | @State private var gradientRotation: Double = 0 30 | @State private var showWebView: Bool = false 31 | @State private var selectedURL: String = "" 32 | @State private var safeData: SafeDataResponse? 33 | 34 | @State private var conversationTitle: String = "" 35 | @State private var hasGeneratedTitle: Bool = false 36 | 37 | let columns: [GridItem] = Array(repeating: .init(.flexible()), count: 2) 38 | let gradientColors = [ColorSchemeManager.shared.currentColorScheme.med, ColorSchemeManager.shared.currentColorScheme.light] 39 | let api = ChatGPTAPI(apiKey: "") 40 | 41 | var body: some View { 42 | GeometryReader { geometry in 43 | VStack(alignment: .leading, spacing: 0) { 44 | ZStack { 45 | RoundedRectangle(cornerRadius: 0) 46 | .fill( 47 | AngularGradient( 48 | gradient: Gradient(colors: gradientColors), 49 | center: .center, 50 | startAngle: .degrees(gradientRotation), 51 | endAngle: .degrees(gradientRotation + 360) 52 | ) 53 | ) 54 | .blur(radius: 70) 55 | .edgesIgnoringSafeArea(.vertical) 56 | 57 | RoundedRectangle(cornerRadius: 30) 58 | .fill(Color.white) 59 | .shadow(color: .gray.opacity(0.2), radius: 10, x: -5, y: -5) 60 | .shadow(color: .gray.opacity(0.2), radius: 10, x: 5, y: 5) 61 | .padding(.horizontal) 62 | 63 | VStack { 64 | if !navbarVisible { Spacer(minLength: 10) } 65 | ScrollViewReader { proxy in 66 | ScrollView { 67 | VStack(alignment: .leading, spacing: 4) { 68 | if !conversationTitle.isEmpty { 69 | Text(conversationTitle) 70 | .font(.title) 71 | .foregroundColor(.primary) 72 | } 73 | } 74 | .padding(.horizontal) 75 | .padding(.top, 10) 76 | LazyVStack { 77 | ForEach(messages) { message in 78 | chatMessageView(message: message) 79 | } 80 | if isWaitingForResponse { 81 | loadingMessageView() 82 | } 83 | Color.clear 84 | .frame(height: 1) 85 | .id("bottom") 86 | } 87 | .padding(EdgeInsets(top: 20, leading: 20, bottom: 0, trailing: 20)) 88 | } 89 | .onChange(of: messages.count) { _ in 90 | withAnimation(.easeOut(duration: 0.25)) { 91 | proxy.scrollTo("bottom", anchor: .bottom) 92 | } 93 | } 94 | } 95 | .clipShape(RoundedRectangle(cornerRadius: 30)) 96 | 97 | Spacer() 98 | 99 | VStack { 100 | HStack { 101 | TextField("Ask Anything", text: $userMessage) 102 | .padding(10) 103 | .background(Color.gray.opacity(0.07)) 104 | .cornerRadius(15) 105 | Button(action: sendMessage) { 106 | Image(systemName: "paperplane") 107 | .foregroundColor(.gray) 108 | .padding(10) 109 | .frame(width: 30, height: 30) 110 | } 111 | } 112 | .padding(.horizontal) 113 | .padding(.horizontal) 114 | } 115 | } 116 | .padding(.bottom, 10) 117 | } 118 | .padding(.bottom, keyboardHeight > 0 ? 0 : 70) 119 | .animation(.easeOut(duration: 0.25), value: keyboardHeight) 120 | } 121 | .navigationBarTitleDisplayMode(.inline) 122 | .sheet(isPresented: $showWebView) { 123 | if let validURL = URL(string: selectedURL) { 124 | SafariView(url: validURL) 125 | } else { 126 | Text("Invalid URL") 127 | .foregroundColor(.red) 128 | .padding() 129 | } 130 | } 131 | .onAppear { 132 | startListeningForKeyboardNotifications() 133 | fetchSafeData() 134 | withAnimation(Animation.linear(duration: 8).repeatForever(autoreverses: false)) { 135 | gradientRotation = 360 136 | } 137 | } 138 | .onReceive(NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification)) { _ in 139 | messages.removeAll() 140 | conversationTitle = "" 141 | hasGeneratedTitle = false 142 | } 143 | } 144 | } 145 | 146 | private func fetchSafeData() { 147 | if let memberIdData = KeychainManager.load(service: "YourAppService", account: "userId"), 148 | let memberIdString = String(data: memberIdData, encoding: .utf8), 149 | let memberId = Int(memberIdString), 150 | let authKeyData = KeychainManager.load(service: "YourAppService", account: "authKey"), 151 | let authKey = String(data: authKeyData, encoding: .utf8) { 152 | 153 | print("Fetching safedata for memberId: \(memberId)") 154 | NetworkManager.fetchSafeData(for: memberId, authKey: authKey) { result in 155 | switch result { 156 | case .success(let data): 157 | DispatchQueue.main.async { 158 | self.safeData = data 159 | } 160 | case .failure(let error): 161 | print("Failed to fetch safe data: \(error.localizedDescription)") 162 | } 163 | } 164 | } 165 | } 166 | 167 | private func loadingMessageView() -> some View { 168 | HStack { 169 | VStack(alignment: .leading) { 170 | Text("Node") 171 | .font(.system(size: 15, design: .rounded)) 172 | .foregroundColor(.gray.opacity(0.5)) 173 | .padding(.horizontal) 174 | 175 | HStack { 176 | VStack { 177 | Text("Thinking...") 178 | .padding(EdgeInsets(top: 10, leading: 15, bottom: 10, trailing: 15)) 179 | .background(Color.white) 180 | .cornerRadius(15) 181 | .shadow(color: .gray.opacity(0.2), radius: 3, x: 2, y: 2) 182 | .foregroundColor(.gray) 183 | } 184 | Spacer() 185 | } 186 | .padding(.horizontal) 187 | } 188 | } 189 | .transition(.opacity) 190 | .animation(.easeInOut(duration: 0.5), value: isWaitingForResponse) 191 | } 192 | 193 | private func sendMessage() { 194 | guard !userMessage.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return } 195 | let message = ChatMessage(text: userMessage, isUser: true, webLinks: nil) 196 | messages.append(message) 197 | userMessage = "" 198 | lastSentMessageDate = Date() 199 | 200 | isWaitingForResponse = true 201 | fetchChatGPTResponse(prompt: message.text) { result in 202 | DispatchQueue.main.async { 203 | switch result { 204 | case .success(let aiMessageText): 205 | let webLinks = extractWebLinks(from: aiMessageText) 206 | let processedText = replaceLinksWithNumbers(text: aiMessageText, links: webLinks) 207 | messages.append(ChatMessage(text: processedText, isUser: false, webLinks: webLinks)) 208 | 209 | if !hasGeneratedTitle, messages.count >= 2 { 210 | let firstUserMessage = messages.first { $0.isUser }?.text ?? "" 211 | let firstAIResponse = messages.first { !$0.isUser }?.text ?? "" 212 | generateConversationTitle(firstUserMessage: firstUserMessage, firstAIResponse: firstAIResponse) 213 | } 214 | case .failure(let error): 215 | messages.append(ChatMessage(text: "Failed to get response: \(error.localizedDescription)", isUser: false, webLinks: nil)) 216 | } 217 | isWaitingForResponse = false 218 | } 219 | } 220 | } 221 | 222 | private func generateConversationTitle(firstUserMessage: String, firstAIResponse: String) { 223 | let prompt = "Create a concise 5-10 word title summarizing the following conversation, return nothing but the title in your response, it will be directly placed in the title card on the ui of the app, so any extra syntax or 'Title: ' for example is not wanted and should never be included :\nUser: \(firstUserMessage)\nAI: \(firstAIResponse)" 224 | fetchChatGPTTitle(prompt: prompt) { result in 225 | DispatchQueue.main.async { 226 | switch result { 227 | case .success(let title): 228 | self.conversationTitle = title.trimmingCharacters(in: .whitespacesAndNewlines) 229 | self.hasGeneratedTitle = true 230 | case .failure(let error): 231 | print("Failed to generate title: \(error.localizedDescription)") 232 | self.conversationTitle = "Conversation" 233 | } 234 | } 235 | } 236 | } 237 | 238 | private func fetchChatGPTTitle(prompt: String, completion: @escaping (Result) -> Void) { 239 | Task { 240 | do { 241 | let title = try await api.sendMessage(text: prompt) 242 | completion(.success(title)) 243 | } catch { 244 | completion(.failure(error)) 245 | } 246 | } 247 | } 248 | 249 | private func extractWebLinks(from text: String) -> [WebLink] { 250 | let pattern = #"\b((https?://)|(www\.))(([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,})(/[^\s]*)?\b"# 251 | 252 | let regex = try? NSRegularExpression(pattern: pattern, options: [.caseInsensitive]) 253 | let nsRange = NSRange(text.startIndex.. String { 274 | var modifiedText = text 275 | for link in links.sorted(by: { $0.index < $1.index }) { 276 | modifiedText = modifiedText.replacingOccurrences(of: link.url, with: "[\(link.index)]") 277 | } 278 | return modifiedText 279 | } 280 | 281 | private func fetchChatGPTResponse(prompt: String, completion: @escaping (Result) -> Void) { 282 | Task { 283 | do { 284 | let dateFormatter = DateFormatter() 285 | dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" 286 | let currentTime = dateFormatter.string(from: Date()) 287 | 288 | var userDataPrompt = "" 289 | if let safeData = self.safeData { 290 | let workoutDetails = safeData.workouts.map { workout in 291 | let exercises = workout.exerciseTitles.joined(separator: ", ") 292 | return "\(workout.workoutName): \(exercises)" 293 | }.joined(separator: " | ") 294 | 295 | userDataPrompt = "User: \(safeData.firstName), DOB: \(safeData.dateOfBirth). " + 296 | "Workouts: \(workoutDetails)." 297 | } 298 | let chat = """ 299 | Act as FitnessAI, an intelligent fitness assistant specializing in providing workout advice, exercise suggestions, and fitness planning. \ 300 | You're equipped with knowledge about various exercises, workout routines, fitness tips, and basic nutrition advice. \ 301 | Respond to user queries with helpful, accurate, and concise fitness guidance. The chatbot includes a web embeddor for links that you include in your response message and you are encouraged to include useful relevant fitness links when prompted. \ 302 | Current date: \(currentTime). User's prompt: \(prompt). \(userDataPrompt) 303 | """ 304 | 305 | let aiMessageText = try await api.sendMessage(text: chat) 306 | print("AI Response Received: \(aiMessageText)") // Debugging 307 | completion(.success(aiMessageText)) 308 | } catch { 309 | print("Error fetching AI response: \(error.localizedDescription)") // Debugging 310 | completion(.failure(error)) 311 | } 312 | } 313 | } 314 | 315 | private func chatMessageView(message: ChatMessage) -> some View { 316 | VStack(alignment: message.isUser ? .trailing : .leading) { 317 | 318 | HStack { 319 | if !message.isUser { 320 | Text("RepBot") 321 | .font(.system(size: 15, design: .rounded)) 322 | .foregroundColor(.gray.opacity(0.5)) 323 | } 324 | Spacer() 325 | if message.isUser { 326 | Text("You") 327 | .font(.system(size: 15, design: .rounded)) 328 | .foregroundColor(.gray.opacity(0.5)) 329 | } 330 | } 331 | .padding(.horizontal) 332 | 333 | VStack { 334 | HStack { 335 | if message.isUser { Spacer() } 336 | VStack(alignment: .leading) { 337 | Text(message.text) 338 | .padding(EdgeInsets(top: 10, leading: 15, bottom: 10, trailing: 15)) 339 | .background(Color.white) 340 | .cornerRadius(15) 341 | .shadow(color: .gray.opacity(0.2), radius: 3, x: 2, y: 2) 342 | .foregroundColor(.black) 343 | } 344 | if !message.isUser { Spacer() } 345 | } 346 | .padding(.horizontal) 347 | 348 | if let links = message.webLinks, !links.isEmpty { 349 | HStack { 350 | VStack(alignment: .leading, spacing: 8) { 351 | ForEach(links) { link in 352 | Button(action: { 353 | selectedURL = link.url 354 | showWebView = true 355 | }) { 356 | HStack { 357 | RoundedRectangle(cornerRadius: 4) 358 | .fill(Color.gray.opacity(0.1)) 359 | .frame(width: 50, height: 50) 360 | .overlay( 361 | Text("[\(link.index)]") 362 | .foregroundColor(.gray) 363 | .font(.system(size: 16, weight: .medium)) 364 | ) 365 | 366 | VStack(alignment: .leading) { 367 | Text(link.title) 368 | .font(.system(size: 14, weight: .medium)) 369 | Text(link.url) 370 | .font(.system(size: 12)) 371 | .foregroundColor(.gray) 372 | } 373 | Spacer() 374 | } 375 | } 376 | .buttonStyle(PlainButtonStyle()) 377 | .background(Color.white) 378 | .cornerRadius(15) 379 | .shadow(color: .gray.opacity(0.3), radius: 2) 380 | } 381 | } 382 | .padding([.top], 8) 383 | .padding(.horizontal, 20) 384 | } 385 | } 386 | } 387 | } 388 | } 389 | } 390 | 391 | extension AiView { 392 | private func startListeningForKeyboardNotifications() { 393 | NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillShowNotification, object: nil, queue: .main) { notification in 394 | guard let userInfo = notification.userInfo else { return } 395 | guard let keyboardSize = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return } 396 | keyboardHeight = keyboardSize.height 397 | navbarVisible = false 398 | } 399 | NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillHideNotification, object: nil, queue: .main) { _ in 400 | keyboardHeight = 0 401 | navbarVisible = true 402 | } 403 | } 404 | } 405 | -------------------------------------------------------------------------------- /iOS/ReminderX/Views/HomeView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import UniformTypeIdentifiers 3 | import SwiftUICharts 4 | import AVKit 5 | import Combine 6 | import CoreImage.CIFilterBuiltins 7 | 8 | struct AITool: Identifiable { 9 | let id = UUID() 10 | let title: String 11 | let description: String 12 | } 13 | 14 | struct HomeView: View { 15 | var userInfo: UserInfo 16 | var userMetrics: [MemberMetric] 17 | @State private var memberMetrics: MemberMetric? 18 | @State private var workouts: [Workout] = [] 19 | @State private var blurBackground: Bool = false 20 | @State private var accentColor: Color = Color.pink 21 | @State private var showingQuickReminderSheet = false 22 | @Environment(\.colorScheme) var colorScheme 23 | @State private var currentPage = 0 24 | @State private var currentTabIndex = 0 25 | @State private var optionSize: [ColorSchemeOption: CGSize] = [:] 26 | @State private var currentTime = Date() 27 | @State private var isScrolled = false 28 | @State private var showColorOptions = false 29 | @AppStorage("userColorScheme") private var userColorSchemeRawValue: Int = ColorSchemeOption.red.rawValue 30 | let totalPages = 3 31 | let autoSwitchInterval: TimeInterval = 5 32 | private let cardHeight: CGFloat = 93 33 | private let cardShadowRadius: CGFloat = 5 34 | private var greeting: String { 35 | let hour = Calendar.current.component(.hour, from: Date()) 36 | 37 | let nameGreeting = " \(userInfo.firstName)" 38 | 39 | if hour >= 4 && hour < 12 { 40 | return "Good Morning" + nameGreeting 41 | } else if hour >= 12 && hour < 17 { 42 | return "Good Afternoon" + nameGreeting 43 | } else { 44 | return "Good Evening" + nameGreeting 45 | } 46 | } 47 | 48 | private var currentColorScheme: (dark: Color, med: Color, light: Color) { 49 | return ColorSchemeOption(rawValue: userColorSchemeRawValue)?.colors ?? (.darkMulti1, .medMulti1, .lightMulti1) 50 | } 51 | @State private var selectedCardIndex = 0 52 | @State private var gradientRotation: Double = 0 53 | let colorSchemes: [[Color]] = [ 54 | [.darkColor, .medColor, .lightColor], 55 | [.darkBlue, .medBlue, .lightBlue], 56 | [.darkGreen, .medGreen, .lightGreen], 57 | [.darkOrange, .medOrange, .lightOrange], 58 | [.darkRed, .medRed, .lightRed], 59 | [.darkViolet, .medViolet, .lightViolet], 60 | [.darkPink, .medPink, .lightPink], 61 | [.darkMulti1, .medMulti1, .lightMulti1], 62 | [.darkMulti2, .medMulti2, .lightMulti2], 63 | [.darkMulti3, .medMulti3, .lightMulti3] 64 | ] 65 | 66 | var body: some View { 67 | GeometryReader { geometry in 68 | ZStack { 69 | VStack { 70 | VStack { 71 | ZStack { 72 | RoundedRectangle(cornerRadius: 0) 73 | .fill( 74 | AngularGradient( 75 | gradient: Gradient(colors: [currentColorScheme.dark, currentColorScheme.med, currentColorScheme.light]), 76 | center: .center, 77 | startAngle: .degrees(gradientRotation), 78 | endAngle: .degrees(gradientRotation + 360) 79 | ) 80 | ) 81 | .padding(.all, 0) 82 | .blur(radius: 45) 83 | .frame(height: 60) 84 | 85 | HStack { 86 | VStack() { 87 | Text(greeting) 88 | .font(.system(size: 36, weight: .bold)) 89 | .foregroundColor(.white) 90 | .padding(.bottom, 2) 91 | HStack { 92 | Image(systemName: "figure.walk.diamond") 93 | .foregroundColor(.white) 94 | .font(Font.system(size: 18, weight: .medium)) 95 | Text("1435lb Lifted") 96 | .font(.system(size: 18, weight: .medium)) 97 | .foregroundColor(.white) 98 | .padding(.trailing, 6) 99 | Image(systemName: "calendar") 100 | .foregroundColor(.white) 101 | .font(Font.system(size: 18, weight: .medium)) 102 | .padding(.leading, 6) 103 | Text("189 Sessions") 104 | .font(.system(size: 18, weight: .medium)) 105 | .foregroundColor(.white) 106 | } 107 | .padding(.bottom, 6) 108 | } 109 | } 110 | } 111 | } 112 | 113 | ScrollView { 114 | LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 20), count: 2), spacing: 20) { 115 | ForEach(0..<1) { _ in 116 | wrappedCardView { 117 | TripleHeightCardView(currentColorScheme: [currentColorScheme.dark, currentColorScheme.med, currentColorScheme.light], memberMetrics: $memberMetrics) 118 | } 119 | VStack(spacing: 20) { 120 | wrappedCardView { 121 | doubleHeightCardView(color: .white) 122 | } 123 | wrappedCardView { 124 | NavigationLink(destination: WorkoutView()) { 125 | cardView(color: .white, text: "Workouts", subtext: "14 workouts saved") 126 | } 127 | } 128 | } 129 | } 130 | } 131 | .padding(.horizontal) 132 | .padding(.bottom) 133 | wrappedCardView { 134 | NavigationLink(destination: WorkoutView()) { 135 | quadHeightCardView(selectedCardIndex: $selectedCardIndex, color: .blue, currentColorScheme: currentColorScheme) 136 | } 137 | .padding(.horizontal) 138 | .padding(.bottom) 139 | } 140 | .padding(.horizontal) 141 | .padding(.bottom) 142 | .onAppear { 143 | withAnimation(Animation.linear(duration: 8).repeatForever(autoreverses: false)) { 144 | gradientRotation = 360 145 | } 146 | } 147 | .blur(radius: showColorOptions ? 10 : 0) 148 | HStack { 149 | ScrollView(.horizontal, showsIndicators: false) { 150 | HStack { 151 | ForEach(ColorSchemeOption.allCases, id: \.self) { option in 152 | colorButton(option: option) 153 | } 154 | } 155 | .padding(.horizontal, 10) 156 | } 157 | } 158 | .background(Color(.white)) 159 | .clipShape(RoundedRectangle(cornerRadius: 25, style: .continuous)) 160 | .shadow(color: Color.black.opacity(0.15), radius: 5, x: 0, y: 4) 161 | .padding(.horizontal) 162 | .blur(radius: showColorOptions ? 10 : 0) 163 | .padding(.bottom, 72) 164 | } 165 | .scrollIndicators(.hidden) 166 | } 167 | } 168 | } 169 | } 170 | private func wrappedCardView(@ViewBuilder content: () -> Content) -> some View { 171 | content() 172 | .padding(5) 173 | .background(Color(.systemBackground)) 174 | .cornerRadius(25) 175 | .shadow(color: Color.primary.opacity(0.1), radius: cardShadowRadius, x: 0, y: cardShadowRadius) 176 | } 177 | 178 | private func wrappedCardViewGraph(@ViewBuilder content: () -> Content) -> some View { 179 | content() 180 | .padding(5) 181 | .background(Color.green) 182 | .cornerRadius(25) 183 | .shadow(color: Color.primary.opacity(0.1), radius: cardShadowRadius, x: 0, y: cardShadowRadius) 184 | } 185 | 186 | private func wrappedCardViewCalender(@ViewBuilder content: () -> Content) -> some View { 187 | content() 188 | .padding(5) 189 | .cornerRadius(25) 190 | .shadow(color: Color.primary.opacity(0.1), radius: cardShadowRadius, x: 0, y: cardShadowRadius) 191 | } 192 | 193 | private func cardView(color: Color, text: String, subtext: String = "", action: (() -> Void)? = nil, doubleHeight: Bool = false, mainTextFontSize: CGFloat = 20, subTextFontSize: CGFloat = 14) -> some View { 194 | NavigationLink(destination: WorkoutView()) { 195 | VStack(alignment: .leading) { 196 | Text(text) 197 | .font(.system(size: 20, weight: .bold)) 198 | .bold() 199 | .foregroundColor(.primary) 200 | .frame(maxWidth: .infinity, alignment: .leading) 201 | Text(subtext) 202 | .font(.system(size: subTextFontSize, weight: .regular, design: .rounded)) 203 | .foregroundColor(.primary.opacity(0.4)) 204 | .frame(maxWidth: .infinity, alignment: .leading) 205 | } 206 | .padding() 207 | .frame(height: doubleHeight ? cardHeight * 2 : cardHeight) 208 | .background(Color(.systemBackground)) 209 | .cornerRadius(20) 210 | } 211 | } 212 | 213 | let achievements: [Achievement] = [ 214 | Achievement(id: 0, image: "achievement (1)", title: "Plan", subtitle: "Craft your first personalized workout plan and set the foundation for your fitness journey."), 215 | Achievement(id: 1, image: "achievement (2)", title: "Streak", subtitle: "Maintain a workout streak by hitting the gym or working out at home for several consecutive days."), 216 | Achievement(id: 2, image: "achievement (3)", title: "PR", subtitle: "Achieve a new personal record in any of your favorite exercises or workouts.") 217 | ] 218 | 219 | private func doubleHeightCardView(color: Color) -> some View { 220 | VStack(spacing: 10) { 221 | } 222 | .frame(maxWidth: .infinity, maxHeight: .infinity) 223 | .background(color) 224 | .cornerRadius(20) 225 | } 226 | 227 | struct InfoCardView: View { 228 | var symbolName: String 229 | var title: String 230 | var subtitle: String 231 | var colorScheme: Color 232 | 233 | private let cardHeight: CGFloat = 45 234 | private let horizontalSpacing: CGFloat = 6 235 | 236 | var body: some View { 237 | HStack() { 238 | Image(systemName: symbolName) 239 | .foregroundColor(colorScheme) 240 | .font(Font.title) 241 | .frame(width: cardHeight, alignment: .center) 242 | 243 | VStack(alignment: .leading) { 244 | Text(title) 245 | .font(.headline) 246 | .bold() 247 | .foregroundColor(colorScheme) 248 | 249 | Text(subtitle) 250 | .foregroundColor(colorScheme) 251 | } 252 | .frame(maxWidth: .infinity, alignment: .leading) 253 | } 254 | .frame(maxWidth: .infinity, minHeight: cardHeight) 255 | .background(colorScheme.opacity(0)) 256 | .shadow(color: Color.black.opacity(0), radius: 3, x: 0, y: 2) 257 | } 258 | } 259 | 260 | struct TripleHeightCardView: View { 261 | var currentColorScheme: [Color] 262 | @Binding var memberMetrics: MemberMetric? 263 | 264 | private let cardHeight: CGFloat = 45 265 | private let verticalSpacing: CGFloat = 10 266 | private let additionalPadding: CGFloat = 20 267 | 268 | var body: some View { 269 | VStack(spacing: verticalSpacing) { 270 | if let metrics = memberMetrics { 271 | InfoCardView(symbolName: "ruler", title: "\(metrics.heightCm) cm", subtitle: "Height", colorScheme: currentColorScheme[1]) 272 | InfoCardView(symbolName: "scalemass", title: "\(metrics.weightKg) kg", subtitle: "Weight", colorScheme: currentColorScheme[1].opacity(0.85)) 273 | InfoCardView(symbolName: "person.fill", title: metrics.gender, subtitle: "Gender", colorScheme: currentColorScheme[1].opacity(0.7)) 274 | InfoCardView(symbolName: "flame.fill", title: "\(metrics.workoutFrequency)", subtitle: "Workout Frequency", colorScheme: currentColorScheme[1].opacity(0.55)) 275 | 276 | if let bodyFat = metrics.bodyFatPercentage { 277 | InfoCardView(symbolName: "drop.fill", title: "\(bodyFat)%", subtitle: "Body Fat", colorScheme: currentColorScheme[1].opacity(0.45)) 278 | } 279 | 280 | if let activityLevel = metrics.activityLevel { 281 | InfoCardView(symbolName: "bolt.fill", title: activityLevel, subtitle: "Activity Level", colorScheme: currentColorScheme[1].opacity(0.37)) 282 | } 283 | 284 | if let bmr = metrics.bmrCalories { 285 | InfoCardView(symbolName: "flame", title: "\(bmr) kcal", subtitle: "BMR Calories", colorScheme: currentColorScheme[1].opacity(0.3)) 286 | } 287 | } else { 288 | ForEach(0..<7, id: \.self) { _ in 289 | InfoCardLoadingView(colorScheme: currentColorScheme[1]) 290 | } 291 | } 292 | } 293 | .padding() 294 | .frame(maxWidth: .infinity, minHeight: totalHeight()) 295 | .background(Color(.systemBackground)) 296 | .cornerRadius(20) 297 | .overlay( 298 | RoundedRectangle(cornerRadius: 20) 299 | .stroke( 300 | LinearGradient( 301 | gradient: Gradient(colors: [currentColorScheme[1], currentColorScheme[2]]), 302 | startPoint: .top, 303 | endPoint: .bottom 304 | ), 305 | lineWidth: 6 306 | ) 307 | ) 308 | } 309 | 310 | private func totalHeight() -> CGFloat { 311 | let totalCardHeight = CGFloat(7) * cardHeight 312 | let totalSpacing = CGFloat(6) * verticalSpacing 313 | return totalCardHeight + totalSpacing + additionalPadding 314 | } 315 | } 316 | 317 | struct InfoCardLoadingView: View { 318 | var colorScheme: Color 319 | private let cardHeight: CGFloat = 65 320 | 321 | var body: some View { 322 | HStack { 323 | LoadingAnimationView() 324 | .frame(width: 30, height: 30) 325 | .foregroundColor(colorScheme) 326 | 327 | VStack { 328 | LoadingAnimationView() 329 | .frame(height: 20) 330 | LoadingAnimationView() 331 | .frame(height: 20) 332 | } 333 | } 334 | .frame(maxWidth: .infinity, minHeight: cardHeight) 335 | .background(colorScheme.opacity(0)) 336 | } 337 | } 338 | 339 | struct LoadingAnimationView: View { 340 | @State private var isAnimating = false 341 | private let cornerRadius: CGFloat = 10 342 | 343 | var body: some View { 344 | GeometryReader { geometry in 345 | Rectangle() 346 | .fill(Color.gray.opacity(0.5)) 347 | .cornerRadius(cornerRadius) 348 | .frame(width: geometry.size.width, height: geometry.size.height) 349 | .scaleEffect(isAnimating ? 1.02 : 1.0) 350 | .opacity(isAnimating ? 0.6 : 0.3) 351 | .animation(Animation.easeInOut(duration: 0.9).repeatForever(autoreverses: true), value: isAnimating) 352 | .onAppear { 353 | isAnimating = true 354 | } 355 | } 356 | } 357 | } 358 | private func quadHeightCardView(selectedCardIndex: Binding, color: Color, subtext: String = "", action: (() -> Void)? = nil, doubleHeight: Bool = false, mainTextFontSize: CGFloat = 20, subTextFontSize: CGFloat = 14, currentColorScheme: (dark: Color, med: Color, light: Color)) -> some View { 359 | VStack(spacing: 0) { 360 | Spacer(minLength: 0) 361 | aiToolCardView(selectedCardIndex: selectedCardIndex, currentColorScheme: currentColorScheme) 362 | .frame(maxWidth: .infinity) 363 | cardSelector(selectedCardIndex: selectedCardIndex, currentColorScheme: currentColorScheme) 364 | } 365 | } 366 | 367 | private func cardSelector(selectedCardIndex: Binding, currentColorScheme: (dark: Color, med: Color, light: Color)) -> some View { 368 | HStack { 369 | let cardTitles = ["Metrics", "Strength", "Friends"] 370 | ForEach(0.., currentColorScheme: (dark: Color, med: Color, light: Color)) -> some View { 389 | return CustomGraphCardView(currentColorScheme: currentColorScheme) 390 | } 391 | 392 | private func fetchWorkouts() { 393 | if let memberIdData = KeychainManager.load(service: "YourAppService", account: "userId"), 394 | let memberIdString = String(data: memberIdData, encoding: .utf8), 395 | let memberId = Int(memberIdString), 396 | let authKeyData = KeychainManager.load(service: "YourAppService", account: "authKey"), 397 | let authKey = String(data: authKeyData, encoding: .utf8) { 398 | 399 | print("Fetching workouts for: \(memberId)") 400 | NetworkManager.fetchWorkoutsForMember(memberId: memberId, authKey: authKey) { result in 401 | DispatchQueue.main.async { 402 | switch result { 403 | case .success(let fetchedWorkouts): 404 | print("Successfully fetched workouts: \(fetchedWorkouts)") 405 | self.workouts = fetchedWorkouts 406 | case .failure(let error): 407 | print("Error fetching workouts: \(error)") 408 | } 409 | } 410 | } 411 | } else { 412 | print("Unable to retrieve member ID and/or auth key from Keychain") 413 | } 414 | } 415 | private func fetchMemberMetrics() { 416 | if let memberIdData = KeychainManager.load(service: "YourAppService", account: "userId"), 417 | let memberIdString = String(data: memberIdData, encoding: .utf8), 418 | let memberId = Int(memberIdString), 419 | let authKeyData = KeychainManager.load(service: "YourAppService", account: "authKey"), 420 | let authKey = String(data: authKeyData, encoding: .utf8) { 421 | 422 | print("Fetching member metrics for memberId: \(memberId)") 423 | NetworkManager.fetchMemberMetrics(memberId: memberId, authKey: authKey) { result in 424 | DispatchQueue.main.async { 425 | switch result { 426 | case .success(let fetchedMetrics): 427 | print("Successfully fetched member metrics: \(fetchedMetrics)") 428 | self.memberMetrics = fetchedMetrics.first 429 | case .failure(let error): 430 | print("Error fetching member metrics: \(error)") 431 | } 432 | } 433 | } 434 | } else { 435 | print("Unable to retrieve member ID and/or auth key from Keychain") 436 | } 437 | } 438 | private func colorButton(option: ColorSchemeOption) -> some View { 439 | Button(action: { 440 | withAnimation { 441 | userColorSchemeRawValue = option.rawValue 442 | } 443 | }) { 444 | ZStack { 445 | Circle() 446 | .fill(AngularGradient( 447 | gradient: Gradient(colors: [option.colors.dark, option.colors.med, option.colors.light, option.colors.med]), 448 | center: .center 449 | )) 450 | .blur(radius: 6) 451 | .frame(width: userColorSchemeRawValue == option.rawValue ? 45 : 35, height: userColorSchemeRawValue == option.rawValue ? 45 : 35) 452 | .clipShape(Circle()) 453 | .overlay( 454 | Circle() 455 | .stroke(option.colors.light, lineWidth: 2) 456 | .blur(radius: 4) 457 | .offset(x: -2, y: -2) 458 | .mask(Circle()) 459 | ) 460 | .overlay( 461 | Circle() 462 | .stroke(option.colors.dark, lineWidth: 2) 463 | .blur(radius: 10) 464 | .offset(x: 2, y: 2) 465 | .mask(Circle()) 466 | ) 467 | .scaleEffect(optionSize[option, default: CGSize(width: 35, height: 35)].width / 35) 468 | .onTapGesture { 469 | withAnimation(.spring()) { 470 | if userColorSchemeRawValue == option.rawValue { 471 | optionSize[option] = CGSize(width: 35, height: 35) 472 | userColorSchemeRawValue = ColorSchemeOption.newColor.rawValue 473 | } else { 474 | optionSize[option] = CGSize(width: 35, height: 35) 475 | userColorSchemeRawValue = option.rawValue 476 | } 477 | } 478 | } 479 | } 480 | } 481 | .padding(9) 482 | .onAppear { 483 | fetchMemberMetrics() 484 | fetchWorkouts() 485 | if userColorSchemeRawValue == option.rawValue { 486 | DispatchQueue.main.async { 487 | optionSize[option] = CGSize(width: 35, height: 35) 488 | } 489 | } 490 | } 491 | } 492 | } 493 | 494 | extension Color { 495 | func withBrightness(_ brightness: CGFloat) -> Color { 496 | let uiColor = UIColor(self) 497 | var hue: CGFloat = 0 498 | var saturation: CGFloat = 0 499 | var brightnessComponent: CGFloat = 0 500 | var alpha: CGFloat = 0 501 | 502 | uiColor.getHue(&hue, saturation: &saturation, brightness: &brightnessComponent, alpha: &alpha) 503 | return Color(UIColor(hue: hue, saturation: saturation, brightness: min(brightnessComponent * brightness, 1.0), alpha: alpha)) 504 | } 505 | } 506 | 507 | extension Color { 508 | static let lightColor = Color(red: 0.12, green: 0.26, blue: 0.82) 509 | static let darkColor = Color(red: 0.38, green: 0.50, blue: 0.96) 510 | static let medColor = Color(red: 0.64, green: 0.78, blue: 0.96) 511 | 512 | static let darkBlue = Color(red: 0.88, green: 0.14, blue: 0.59) 513 | static let medBlue = Color(red: 0.94, green: 0.25, blue: 0.25) 514 | static let lightBlue = Color(red: 0.89, green: 0.75, blue: 0.73) 515 | 516 | static let darkGreen = Color(red: 0.18, green: 0.80, blue: 0.44) 517 | static let medGreen = Color(red: 0.06, green: 0.53, blue: 0.06) 518 | static let lightGreen = Color(red: 0.78, green: 0.88, blue: 0.78) 519 | 520 | static let darkOrange = Color(red: 0.96, green: 0.09, blue: 0.00) 521 | static let medOrange = Color(red: 1.00, green: 0.75, blue: 0.36) 522 | static let lightOrange = Color(red: 0.79, green: 0.45, blue: 0.59) 523 | 524 | static let darkRed = Color(red: 0.61, green: 0.04, blue: 0.08) 525 | static let medRed = Color(red: 0.96, green: 0.20, blue: 0.07) 526 | static let lightRed = Color(red: 0.99, green: 0.67, blue: 0.71) 527 | 528 | static let darkViolet = Color(red: 0.22, green: 0.02, blue: 0.34) 529 | static let medViolet = Color(red: 0.57, green: 0.19, blue: 0.54) 530 | static let lightViolet = Color(red: 0.90, green: 0.56, blue: 0.94) 531 | 532 | static let darkPink = Color(red: 0.80, green: 0.08, blue: 0.35) 533 | static let medPink = Color(red: 0.98, green: 0.43, blue: 0.70) 534 | static let lightPink = Color(red: 1.0, green: 0.83, blue: 0.92) 535 | 536 | static let darkMulti1 = Color(red: 0.114, green: 0.114, blue: 0.522) 537 | static let medMulti1 = Color(red: 0.427, green: 0.114, blue: 0.522) 538 | static let lightMulti1 = Color(red: 0.831, green: 0.114, blue: 0.522) 539 | 540 | static let darkMulti2 = Color(red: 1.000, green: 0.408, blue: 0.561) 541 | static let medMulti2 = Color(red: 1.000, green: 0.627, blue: 0.314) 542 | static let lightMulti2 = Color(red: 1.000, green: 0.863, blue: 0.314) 543 | 544 | static let darkMulti3 = Color(red: 0.204, green: 0.584, blue: 0.506) 545 | static let medMulti3 = Color(red: 0.620, green: 0.910, blue: 0.361) 546 | static let lightMulti3 = Color(red: 0.984, green: 0.973, blue: 0.420) 547 | } 548 | -------------------------------------------------------------------------------- /iOS/ReminderX/Views/LoginAccountView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import UniformTypeIdentifiers 3 | import CoreGraphics 4 | import Alamofire 5 | import Network 6 | import Foundation 7 | import Security 8 | 9 | struct LoginAccountView: View { 10 | @State private var email: String = "" 11 | @State private var password: String = "" 12 | @State private var errorMessage: String = "" 13 | @Binding var isAuthenticated: Bool 14 | 15 | var body: some View { 16 | VStack { 17 | Image("login_banner") 18 | .resizable() 19 | .aspectRatio(contentMode: .fit) 20 | .frame(maxWidth: .infinity) 21 | .padding(30) 22 | 23 | TextField("Username or Email", text: $email) 24 | .padding() 25 | .background(Color.white.opacity(0.95)) 26 | .clipShape(RoundedRectangle(cornerRadius: 15)) 27 | .overlay( 28 | RoundedRectangle(cornerRadius: 15) 29 | .stroke(Color.gray, lineWidth: 1) 30 | ) 31 | .padding(.horizontal) 32 | .autocapitalization(.none) 33 | .keyboardType(.emailAddress) 34 | 35 | SecureField("Password", text: $password) 36 | .padding() 37 | .background(Color.white.opacity(0.95)) 38 | .clipShape(RoundedRectangle(cornerRadius: 15)) 39 | .overlay( 40 | RoundedRectangle(cornerRadius: 15) 41 | .stroke(Color.gray, lineWidth: 1) 42 | ) 43 | .padding(.horizontal) 44 | 45 | if !errorMessage.isEmpty { 46 | Text(errorMessage) 47 | .foregroundColor(.red) 48 | .font(.caption) 49 | .padding() 50 | } 51 | 52 | Button(action: { 53 | loginUser() 54 | }) { 55 | Text("Login") 56 | .fontWeight(.bold) 57 | .font(.title2) 58 | .padding() 59 | .frame(maxWidth: .infinity) 60 | .background(Color.black.opacity(0.85)) 61 | .foregroundColor(.white) 62 | .clipShape(RoundedRectangle(cornerRadius: 20)) 63 | .padding(.horizontal) 64 | } 65 | Spacer() 66 | Button(action: { 67 | }) { 68 | Text("Privacy Policy") 69 | .underline() 70 | .font(.caption) 71 | .foregroundColor(.gray) 72 | } 73 | .padding(.bottom) 74 | } 75 | .background(Color.white.opacity(0.5).edgesIgnoringSafeArea(.all)) 76 | } 77 | 78 | func loginUser() { 79 | guard let url = URL(string: "http://192.168.0.139:3000/login") else { 80 | errorMessage = "Invalid URL" 81 | return 82 | } 83 | 84 | let userData: [String: Any] = ["email": email, "password": password] 85 | var request = URLRequest(url: url) 86 | request.httpMethod = "POST" 87 | request.addValue("application/json", forHTTPHeaderField: "Content-Type") 88 | 89 | print("Attempting login with Email: \(email), Password: \(password)") 90 | 91 | do { 92 | request.httpBody = try JSONSerialization.data(withJSONObject: userData, options: []) 93 | } catch let error { 94 | errorMessage = "Failed to encode user data: \(error.localizedDescription)" 95 | return 96 | } 97 | 98 | URLSession.shared.dataTask(with: request) { data, response, error in 99 | DispatchQueue.main.async { 100 | if let error = error { 101 | self.errorMessage = "Error occurred: \(error.localizedDescription)" 102 | return 103 | } 104 | 105 | if let httpResponse = response as? HTTPURLResponse { 106 | if httpResponse.statusCode == 200 { 107 | self.handleSuccessfulLogin(data: data) 108 | } else { 109 | self.errorMessage = httpResponse.statusCode == 401 ? "Invalid credentials" : "Server error" 110 | } 111 | } else { 112 | self.errorMessage = "No server response" 113 | } 114 | } 115 | }.resume() 116 | } 117 | 118 | private func handleSuccessfulLogin(data: Data?) { 119 | DispatchQueue.main.async { 120 | guard let data = data else { 121 | self.errorMessage = "Invalid server response" 122 | return 123 | } 124 | 125 | do { 126 | let loginResponse = try JSONDecoder().decode(LoginResponse.self, from: data) 127 | 128 | let userIdStatus = KeychainManager.save("\(loginResponse.memberId)".data(using: .utf8)!, 129 | service: "YourAppService", 130 | account: "userId") 131 | let authKeyStatus = KeychainManager.save(loginResponse.authKey.data(using: .utf8)!, 132 | service: "YourAppService", 133 | account: "authKey") 134 | 135 | if userIdStatus == noErr && authKeyStatus == noErr { 136 | self.isAuthenticated = true 137 | } else { 138 | self.errorMessage = "Failed to save credentials" 139 | } 140 | } catch { 141 | self.errorMessage = "Login failed: \(error.localizedDescription)" 142 | } 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /iOS/ReminderX/Views/LoginView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import UniformTypeIdentifiers 3 | import CoreGraphics 4 | import Alamofire 5 | import Network 6 | import Foundation 7 | import Security 8 | 9 | struct LoginView: View { 10 | @Binding var isAuthenticated: Bool 11 | @State private var showingNoInternetAlert = false 12 | @State private var showingMakeAccountView = false 13 | @State private var showingLoginAccountView = false 14 | 15 | var body: some View { 16 | VStack { 17 | Image("login_banner") 18 | .resizable() 19 | .aspectRatio(contentMode: .fit) 20 | .frame(maxWidth: .infinity) 21 | .padding(30) 22 | Spacer() 23 | 24 | Button(action: { 25 | checkInternetConnection(for: "login") 26 | }) { 27 | loginButtonContent(title: "I Have an Account", imageName: "person.fill") 28 | } 29 | .padding(3) 30 | Button(action: { 31 | checkInternetConnection(for: "makeAccount") 32 | }) { 33 | loginButtonContent(title: "Make an Account", imageName: "person.badge.plus") 34 | } 35 | .padding(3) 36 | Button(action: { 37 | }) { 38 | Text("Privacy Policy") 39 | .underline() 40 | .font(.caption) 41 | .foregroundColor(.gray) 42 | } 43 | .padding(.top) 44 | } 45 | .alert(isPresented: $showingNoInternetAlert) { 46 | Alert(title: Text("No Internet Connection"), message: Text("Please check your internet connection."), dismissButton: .default(Text("OK"))) 47 | } 48 | .background(Color.white.opacity(0.5).edgesIgnoringSafeArea(.all)) 49 | .sheet(isPresented: $showingMakeAccountView) { 50 | MakeAccountView(isAuthenticated: $isAuthenticated) 51 | } 52 | .sheet(isPresented: $showingLoginAccountView) { 53 | LoginAccountView(isAuthenticated: $isAuthenticated) 54 | } 55 | } 56 | 57 | private func loginButtonContent(title: String, imageName: String) -> some View { 58 | HStack { 59 | Image(systemName: imageName) 60 | Text(title) 61 | .fontWeight(.bold) 62 | .font(.title2) 63 | } 64 | .frame(maxWidth: .infinity, alignment: .leading) 65 | .padding() 66 | .background(title == "I Have an Account" ? Color.black.opacity(0.8) : Color.white) 67 | .foregroundColor(title == "I Have an Account" ? .white : .black) 68 | .clipShape(RoundedRectangle(cornerRadius: 20)) 69 | .overlay( 70 | RoundedRectangle(cornerRadius: 20) 71 | .stroke(Color.black.opacity(0.75), lineWidth: title == "I Have an Account" ? 0 : 2) 72 | ) 73 | .padding(.horizontal) 74 | .shadow(radius: 3) 75 | } 76 | 77 | private func checkInternetConnection(for action: String) { 78 | let monitor = NWPathMonitor() 79 | monitor.pathUpdateHandler = { path in 80 | if path.status == .satisfied { 81 | DispatchQueue.main.async { 82 | switch action { 83 | case "makeAccount": 84 | self.showingMakeAccountView = true 85 | case "login": 86 | self.showingLoginAccountView = true 87 | default: 88 | break 89 | } 90 | } 91 | } else { 92 | DispatchQueue.main.async { 93 | self.showingNoInternetAlert = true 94 | } 95 | } 96 | monitor.cancel() 97 | } 98 | let queue = DispatchQueue(label: "Monitor") 99 | monitor.start(queue: queue) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /iOS/ReminderX/Views/MakeAccountView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import UniformTypeIdentifiers 3 | import CoreGraphics 4 | import Alamofire 5 | import Network 6 | import Foundation 7 | import Security 8 | 9 | struct MemberMetricsView: View { 10 | @Binding var isAuthenticated: Bool 11 | 12 | var firstName: String 13 | var lastName: String 14 | var dateOfBirth: Date 15 | var email: String 16 | var password: String 17 | var username: String 18 | 19 | @State private var heightCm: Int = 0 20 | @State private var weightKg: Int = 0 21 | @State private var gender: String = "" 22 | @State private var workoutFrequency: Int = 0 23 | @State private var bodyFatPercentage: Float? = nil 24 | @State private var goalType: String = "" 25 | @State private var activityLevel: String = "" 26 | @State private var restingHeartRate: Int? = nil 27 | @State private var bmrCalories: Float? = nil 28 | 29 | @State private var currentTab: Int = 0 30 | 31 | var body: some View { 32 | VStack { 33 | Text("Enter Your Fitness Metrics") 34 | .font(.title) 35 | .padding() 36 | 37 | TabView(selection: $currentTab) { 38 | VStack { 39 | formFieldInt(title: "Height in cm", value: $heightCm) 40 | formFieldInt(title: "Weight in kg", value: $weightKg) 41 | formFieldFloat(title: "Body Fat Percentage", value: $bodyFatPercentage) 42 | } 43 | .tag(0) 44 | .padding() 45 | 46 | VStack { 47 | formFieldInt( 48 | title: "Resting Heart Rate (bpm)", 49 | value: Binding( 50 | get: { restingHeartRate ?? 0 }, 51 | set: { restingHeartRate = $0 } 52 | ) 53 | ) 54 | formFieldFloat(title: "Basal Metabolic Rate (BMR)", value: $bmrCalories) 55 | } 56 | .tag(1) 57 | .padding() 58 | 59 | VStack { 60 | formFieldText(title: "Gender", text: $gender) 61 | formFieldInt(title: "Workout Frequency per Week", value: $workoutFrequency) 62 | formFieldText(title: "Fitness Goal (e.g., Weight Loss)", text: $goalType) 63 | formFieldText(title: "Activity Level (e.g., Active)", text: $activityLevel) 64 | } 65 | .tag(2) 66 | .padding() 67 | } 68 | .tabViewStyle(PageTabViewStyle(indexDisplayMode: .always)) 69 | 70 | HStack { 71 | if currentTab > 0 { 72 | Button(action: { 73 | withAnimation { 74 | currentTab -= 1 75 | } 76 | }) { 77 | Text("Previous") 78 | .padding() 79 | .background(Color.gray.opacity(0.2)) 80 | .foregroundColor(.black) 81 | .cornerRadius(10) 82 | } 83 | } 84 | 85 | Spacer() 86 | 87 | if currentTab < 2 { 88 | Button(action: { 89 | withAnimation { 90 | currentTab += 1 91 | } 92 | }) { 93 | Text("Next") 94 | .padding() 95 | .background(Color.blue) 96 | .foregroundColor(.white) 97 | .cornerRadius(10) 98 | } 99 | } else { 100 | Button("Complete Sign Up") { 101 | uploadMemberMetrics() 102 | } 103 | .padding() 104 | .background(Color.black) 105 | .foregroundColor(.white) 106 | .clipShape(RoundedRectangle(cornerRadius: 15)) 107 | } 108 | } 109 | .padding(.horizontal) 110 | } 111 | } 112 | 113 | private func formFieldInt(title: String, value: Binding) -> some View { 114 | VStack(alignment: .leading) { 115 | Text(title) 116 | .font(.headline) 117 | TextField("", value: value, format: .number) 118 | .keyboardType(.numberPad) 119 | .padding() 120 | .background(Color.white.opacity(0.8)) 121 | .overlay( 122 | RoundedRectangle(cornerRadius: 15) 123 | .stroke(Color.gray, lineWidth: 1) 124 | ) 125 | .clipShape(RoundedRectangle(cornerRadius: 15)) 126 | .accessibility(label: Text(title)) 127 | .accessibility(hint: Text("Enter your \(title.lowercased())")) 128 | } 129 | .padding(.horizontal) 130 | } 131 | 132 | private func formFieldFloat(title: String, value: Binding) -> some View { 133 | VStack(alignment: .leading) { 134 | Text(title) 135 | .font(.headline) 136 | .padding(.top) 137 | TextField("", value: Binding( 138 | get: { value.wrappedValue ?? 0.0 }, 139 | set: { value.wrappedValue = $0 } 140 | ), format: .number) 141 | .keyboardType(.decimalPad) 142 | .padding() 143 | .background(Color.white.opacity(0.8)) 144 | .overlay( 145 | RoundedRectangle(cornerRadius: 15) 146 | .stroke(Color.gray, lineWidth: 1) 147 | ) 148 | .clipShape(RoundedRectangle(cornerRadius: 15)) 149 | .accessibility(label: Text(title)) 150 | .accessibility(hint: Text("Enter your \(title.lowercased())")) 151 | } 152 | .padding(.horizontal) 153 | } 154 | 155 | private func formFieldText(title: String, text: Binding) -> some View { 156 | VStack(alignment: .leading) { 157 | Text(title) 158 | .font(.headline) 159 | .padding(.top) 160 | TextField(title, text: text) 161 | .padding() 162 | .background(Color.white.opacity(0.8)) 163 | .overlay( 164 | RoundedRectangle(cornerRadius: 15) 165 | .stroke(Color.gray, lineWidth: 1) 166 | ) 167 | .clipShape(RoundedRectangle(cornerRadius: 15)) 168 | .accessibility(label: Text(title)) 169 | .accessibility(hint: Text("Enter your \(title.lowercased())")) 170 | } 171 | .padding(.horizontal) 172 | } 173 | 174 | private func uploadMemberMetrics() { 175 | let dateFormatter = DateFormatter() 176 | dateFormatter.dateFormat = "yyyy-MM-dd" 177 | 178 | let dobString = dateFormatter.string(from: dateOfBirth) 179 | let accountAndMetricsData: [String: Any] = [ 180 | "firstName": firstName, 181 | "lastName": lastName, 182 | "dateOfBirth": dobString, 183 | "email": email, 184 | "password": password, 185 | "username": username, 186 | "heightCm": heightCm, 187 | "weightKg": weightKg, 188 | "gender": gender, 189 | "workoutFrequency": workoutFrequency, 190 | "bodyFatPercentage": bodyFatPercentage ?? 0.0, 191 | "goalType": goalType, 192 | "activityLevel": activityLevel, 193 | "restingHeartRate": restingHeartRate ?? 0, 194 | "bmrCalories": bmrCalories ?? 0.0 195 | ] 196 | 197 | NetworkManager.signUpUser(with: accountAndMetricsData) { result in 198 | switch result { 199 | case .success: 200 | print("Sign up successful") 201 | DispatchQueue.main.async { 202 | self.isAuthenticated = true 203 | } 204 | case .failure(let error): 205 | print("Error signing up: \(error)") 206 | } 207 | } 208 | } 209 | } 210 | 211 | struct MakeAccountView: View { 212 | @Binding var isAuthenticated: Bool 213 | @State private var isAccountInfoCompleted = false 214 | @State private var isMetricsInfoCompleted = false 215 | @State private var isEmailValid: Bool = true 216 | @State private var passwordWarning: String = "" 217 | @State private var username: String = "" 218 | @State private var isUsernameAvailable: Bool? = nil 219 | @State private var debounceTimer: Timer? 220 | @State private var firstName: String = "" 221 | @State private var lastName: String = "" 222 | @State private var dateOfBirth = Date() 223 | @State private var email: String = "" 224 | @State private var password: String = "" 225 | 226 | var body: some View { 227 | VStack { 228 | Image("login_banner") 229 | .resizable() 230 | .aspectRatio(contentMode: .fit) 231 | .frame(width: 300) 232 | .padding(20) 233 | 234 | if !isAccountInfoCompleted { 235 | accountInfoForm 236 | } else { 237 | MemberMetricsView( 238 | isAuthenticated: $isAuthenticated, 239 | firstName: firstName, 240 | lastName: lastName, 241 | dateOfBirth: dateOfBirth, 242 | email: email, 243 | password: password, 244 | username: username 245 | ) 246 | } 247 | 248 | Spacer() 249 | } 250 | .background(Color.white.opacity(0.5).edgesIgnoringSafeArea(.all)) 251 | } 252 | 253 | var accountInfoForm: some View { 254 | VStack { 255 | formField(title: "First Name", text: $firstName) 256 | formField(title: "Last Name", text: $lastName) 257 | formField(title: "Username", text: $username) 258 | .onChange(of: username) { _ in 259 | debounceUsernameCheck() 260 | } 261 | if let isAvailable = isUsernameAvailable { 262 | Image(systemName: isAvailable ? "checkmark.circle" : "xmark.circle") 263 | .foregroundColor(isAvailable ? .green : .red) 264 | } 265 | DatePicker("Date of Birth", selection: $dateOfBirth, displayedComponents: .date) 266 | .padding() 267 | .background(Color.white.opacity(0.95)) 268 | .overlay( 269 | RoundedRectangle(cornerRadius: 15) 270 | .stroke(Color.gray, lineWidth: 1) 271 | ) 272 | .clipShape(RoundedRectangle(cornerRadius: 15)) 273 | .padding(.horizontal) 274 | .accessibility(label: Text("Date of Birth")) 275 | .accessibility(hint: Text("Select your date of birth")) 276 | 277 | HStack { 278 | TextField("Email", text: $email) 279 | .padding() 280 | .background(Color.white.opacity(0.8)) 281 | .overlay( 282 | RoundedRectangle(cornerRadius: 15) 283 | .stroke(Color.gray, lineWidth: 1) 284 | ) 285 | .clipShape(RoundedRectangle(cornerRadius: 15)) 286 | .padding(.horizontal) 287 | .onChange(of: email) { _ in validateEmail() } 288 | .accessibility(label: Text("Email")) 289 | .accessibility(hint: Text("Enter your email address")) 290 | 291 | if !isEmailValid { 292 | Image(systemName: "exclamationmark.triangle.fill") 293 | .foregroundColor(.yellow) 294 | .padding(.trailing, 30) 295 | .accessibility(label: Text("Invalid email")) 296 | .accessibility(hint: Text("Please enter a valid email address")) 297 | } 298 | } 299 | 300 | HStack { 301 | SecureField("Password", text: $password) 302 | .padding() 303 | .background(Color.white.opacity(0.95)) 304 | .overlay( 305 | RoundedRectangle(cornerRadius: 15) 306 | .stroke(Color.gray, lineWidth: 1) 307 | ) 308 | .clipShape(RoundedRectangle(cornerRadius: 15)) 309 | .padding(.horizontal) 310 | .onChange(of: password) { _ in validatePassword() } 311 | .accessibility(label: Text("Password")) 312 | .accessibility(hint: Text("Enter your password")) 313 | 314 | if !passwordWarning.isEmpty { 315 | Image(systemName: "exclamationmark.triangle.fill") 316 | .foregroundColor(.yellow) 317 | .padding(.trailing, 30) 318 | .accessibility(label: Text("Password warning")) 319 | .accessibility(hint: Text(passwordWarning)) 320 | } 321 | } 322 | 323 | if !passwordWarning.isEmpty { 324 | Text(passwordWarning) 325 | .foregroundColor(.red) 326 | .font(.caption) 327 | .padding(.horizontal) 328 | } 329 | 330 | if allFieldsValid() { 331 | Button("Sign Up") { 332 | isAccountInfoCompleted = true 333 | } 334 | .padding() 335 | .background(Color.black) 336 | .foregroundColor(.white) 337 | .clipShape(RoundedRectangle(cornerRadius: 15)) 338 | .padding(.horizontal) 339 | } 340 | } 341 | } 342 | 343 | private func formField(title: String, text: Binding) -> some View { 344 | TextField(title, text: text) 345 | .padding() 346 | .background(Color.white.opacity(0.8)) 347 | .overlay( 348 | RoundedRectangle(cornerRadius: 15) 349 | .stroke(Color.gray, lineWidth: 1) 350 | ) 351 | .clipShape(RoundedRectangle(cornerRadius: 15)) 352 | .padding(.horizontal) 353 | .accessibility(label: Text(title)) 354 | .accessibility(hint: Text("Enter your \(title.lowercased())")) 355 | } 356 | 357 | private func validatePassword() { 358 | passwordWarning = "" 359 | 360 | if password.count < 8 { 361 | passwordWarning += "Password must be at least 8 characters. " 362 | } 363 | 364 | if !passwordContainsNumber() { 365 | passwordWarning += "Password must contain at least one number." 366 | } 367 | } 368 | 369 | private func passwordContainsNumber() -> Bool { 370 | let numberRange = password.rangeOfCharacter(from: .decimalDigits) 371 | return numberRange != nil 372 | } 373 | 374 | private func debounceUsernameCheck() { 375 | debounceTimer?.invalidate() 376 | debounceTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { [self] _ in 377 | self.checkUsernameAvailability() 378 | } 379 | } 380 | 381 | private func validateEmail() { 382 | let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}" 383 | let emailTest = NSPredicate(format:"SELF MATCHES %@", emailRegEx) 384 | isEmailValid = emailTest.evaluate(with: email) 385 | } 386 | 387 | func checkUsernameAvailability() { 388 | guard !username.isEmpty else { 389 | isUsernameAvailable = nil 390 | return 391 | } 392 | 393 | if let url = URL(string: "http://192.168.0.139:3000/checkUsername/\(username)") { 394 | URLSession.shared.dataTask(with: url) { data, response, error in 395 | if let data = data { 396 | if let response = try? JSONDecoder().decode([String: Bool].self, from: data) { 397 | DispatchQueue.main.async { 398 | self.isUsernameAvailable = response["isAvailable"] 399 | } 400 | } 401 | } 402 | }.resume() 403 | } 404 | } 405 | 406 | private func allFieldsValid() -> Bool { 407 | return !firstName.isEmpty && !lastName.isEmpty && isEmailValid && !password.isEmpty 408 | } 409 | } 410 | -------------------------------------------------------------------------------- /iOS/ReminderX/Views/SettingsView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import Foundation 3 | 4 | struct SettingsView: View { 5 | var userInfo: UserInfo 6 | var userMetrics: [MemberMetric] 7 | @State private var heightFeet: String = "5" 8 | @State private var heightInches: String = "6" 9 | @State private var weight: String = "70" 10 | @State private var weightUnit: WeightUnit = .lbs 11 | 12 | var body: some View { 13 | ScrollView { 14 | VStack(spacing: 15) { 15 | settingsSection(title: "Personal Information", settings: personalInformationSettings()) 16 | settingsSection(title: "Health Metrics", settings: healthMetricsSettings()) 17 | settingsSection(title: "Workout Preferences", settings: workoutPreferencesSettings()) 18 | settingsSection(title: "Nutrition", settings: nutritionSettings()) 19 | settingsSection(title: "App Settings", settings: appSettings()) 20 | 21 | Button(action: logOut) { 22 | Text("Log Out") 23 | .frame(maxWidth: .infinity) 24 | .font(.title3) 25 | .padding() 26 | .background(Color.black) 27 | .foregroundColor(.white) 28 | .clipShape(RoundedRectangle(cornerRadius: 18)) 29 | } 30 | .padding(.horizontal) 31 | } 32 | .padding(.bottom, 80) 33 | } 34 | } 35 | 36 | private func personalInformationSettings() -> [SettingItem] { 37 | [ 38 | SettingItem(iconName: "person.fill", title: "First Name", actionView: AnyView(Text(userInfo.firstName))), 39 | SettingItem(iconName: "person.fill", title: "Last Name", actionView: AnyView(Text(userInfo.lastName))), 40 | SettingItem(iconName: "envelope.fill", title: "Email", actionView: AnyView(Text(userInfo.email))), 41 | SettingItem(iconName: "calendar", title: "Date of Birth", actionView: AnyView(Text(userInfo.dateOfBirth))), 42 | ] 43 | } 44 | 45 | private func healthMetricsSettings() -> [SettingItem] { 46 | [ 47 | SettingItem(iconName: "figure.walk", title: "Gender", actionView: AnyView(Text("Male"))), 48 | SettingItem(iconName: "arrow.up.and.down", title: "Height", actionView: AnyView(HeightEntryView(heightFeet: $heightFeet, heightInches: $heightInches))), 49 | SettingItem(iconName: "scalemass.fill", title: "Weight", actionView: AnyView(WeightEntryView(weight: $weight, unit: $weightUnit))), 50 | SettingItem(iconName: "heart.fill", title: "Resting Heart Rate", actionView: AnyView(Text("70 BPM"))), 51 | ] 52 | } 53 | 54 | private func workoutPreferencesSettings() -> [SettingItem] { 55 | [ 56 | SettingItem(iconName: "flame.fill", title: "Fitness Goal", actionView: AnyView(Text("Weight Loss"))), 57 | SettingItem(iconName: "clock.fill", title: "Preferred Workout Time", actionView: AnyView(Text("Morning"))), 58 | SettingItem(iconName: "location.fill", title: "Preferred Workout Location", actionView: AnyView(Text("Outdoor"))), 59 | ] 60 | } 61 | 62 | private func nutritionSettings() -> [SettingItem] { 63 | [ 64 | SettingItem(iconName: "leaf.fill", title: "Dietary Preferences", actionView: AnyView(Text("Vegetarian"))), 65 | SettingItem(iconName: "applelogo", title: "Daily Caloric Intake", actionView: AnyView(Text("2000 kcal"))), 66 | SettingItem(iconName: "cup.and.saucer.fill", title: "Water Intake Goal", actionView: AnyView(Text("2L"))), 67 | ] 68 | } 69 | 70 | private func appSettings() -> [SettingItem] { 71 | [ 72 | SettingItem(iconName: "paintbrush.fill", title: "Theme Color", actionView: AnyView(Text("Blue"))), 73 | SettingItem(iconName: "gear", title: "Language", actionView: AnyView(Text("English"))), 74 | ] 75 | } 76 | 77 | @ViewBuilder 78 | private func settingsSection(title: String, settings: [SettingItem]) -> some View { 79 | VStack(alignment: .leading, spacing: 0) { 80 | Text(title) 81 | .font(.title3) 82 | .padding(.vertical, 5) 83 | .frame(maxWidth: .infinity, alignment: .leading) 84 | .padding(.horizontal) 85 | 86 | ForEach(settings.indices, id: \.self) { index in 87 | let isFirst = index == settings.startIndex 88 | let isLast = index == settings.index(before: settings.endIndex) 89 | SettingItemView(setting: settings[index], isFirst: isFirst, isLast: isLast) 90 | } 91 | } 92 | .padding(.horizontal) 93 | } 94 | 95 | private func changeName() { 96 | } 97 | 98 | private func changeUsername() { 99 | } 100 | 101 | private func logOut() { 102 | KeychainManager.delete(service: "YourAppService", account: "userId") 103 | NotificationCenter.default.post(name: NSNotification.Name("UserDidLogOut"), object: nil) 104 | } 105 | 106 | private func settingItemView(iconName: String, title: String) -> some View { 107 | HStack { 108 | Image(systemName: iconName) 109 | Text(title) 110 | Spacer() 111 | } 112 | .padding() 113 | .background(Color.gray.opacity(0.1)) 114 | .cornerRadius(20) 115 | } 116 | 117 | } 118 | 119 | struct SettingItemView: View { 120 | var setting: SettingItem 121 | var isFirst: Bool 122 | var isLast: Bool 123 | 124 | var body: some View { 125 | HStack { 126 | Image(systemName: setting.iconName) 127 | Text(setting.title) 128 | Spacer() 129 | setting.actionView 130 | } 131 | .padding() 132 | .background(Color.gray.opacity(0.1)) 133 | .cornerRadius(15, corners: isFirst ? [.topLeft, .topRight] : isLast ? [.bottomLeft, .bottomRight] : []) 134 | } 135 | } 136 | 137 | extension View { 138 | func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View { 139 | clipShape(RoundedCorner(radius: radius, corners: corners)) 140 | } 141 | } 142 | 143 | struct RoundedCorner: Shape { 144 | var radius: CGFloat = .infinity 145 | var corners: UIRectCorner = .allCorners 146 | 147 | func path(in rect: CGRect) -> Path { 148 | let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius)) 149 | return Path(path.cgPath) 150 | } 151 | } 152 | 153 | struct SettingItem: Identifiable { 154 | var id = UUID() 155 | var iconName: String 156 | var title: String 157 | var actionView: Content 158 | } 159 | -------------------------------------------------------------------------------- /iOS/ReminderX/Views/VideoEditorHomeVIew.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import UIKit 3 | 4 | struct WorkoutView: View { 5 | @State private var showActionSheet = false 6 | @State private var gradientRotation: Double = 0 7 | @State private var showingWorkoutBuilder = false 8 | @State private var workouts: [Workout] = [] 9 | @State private var selectedWorkout: Workout? { 10 | didSet { 11 | if let selectedWorkout = selectedWorkout { 12 | fetchExercises(for: selectedWorkout) 13 | } 14 | } 15 | } 16 | @State private var exercises: [Exercise] = [] 17 | @State private var isLoading: Bool = false 18 | @State private var showDeletionErrorAlert: Bool = false 19 | 20 | let gradientColors = [ColorSchemeManager.shared.currentColorScheme.light, ColorSchemeManager.shared.currentColorScheme.dark] 21 | 22 | var body: some View { 23 | ZStack { 24 | RoundedRectangle(cornerRadius: 0) 25 | .fill( 26 | AngularGradient( 27 | gradient: Gradient(colors: gradientColors), 28 | center: .center, 29 | startAngle: .degrees(gradientRotation), 30 | endAngle: .degrees(gradientRotation + 360) 31 | ) 32 | ) 33 | .blur(radius: 70) 34 | .edgesIgnoringSafeArea(.all) 35 | 36 | VStack(spacing: 20) { 37 | if let selectedWorkout = selectedWorkout { 38 | WorkoutPlanCardView(workout: selectedWorkout, exercises: exercises) 39 | } 40 | workoutListSection 41 | } 42 | .padding(.horizontal) 43 | .padding(.bottom, 76) 44 | if isLoading { 45 | Color.black.opacity(0.4) 46 | .edgesIgnoringSafeArea(.all) 47 | ProgressView("Deleting...") 48 | .progressViewStyle(CircularProgressViewStyle(tint: .white)) 49 | .padding() 50 | .background(Color.gray.opacity(0.8)) 51 | .cornerRadius(10) 52 | } 53 | } 54 | .navigationBarTitle("", displayMode: .inline) 55 | .sheet(isPresented: $showingWorkoutBuilder) { 56 | WorkoutBuilderView(isPresented: self.$showingWorkoutBuilder) 57 | } 58 | .alert(isPresented: $showDeletionErrorAlert) { 59 | Alert( 60 | title: Text("Deletion Failed"), 61 | message: Text("Unable to delete the workout. Please try again."), 62 | dismissButton: .default(Text("OK")) 63 | ) 64 | } 65 | .onAppear { 66 | fetchWorkouts() 67 | withAnimation(Animation.linear(duration: 8).repeatForever(autoreverses: false)) { 68 | gradientRotation = 360 69 | } 70 | } 71 | } 72 | 73 | private var workoutListSection: some View { 74 | VStack { 75 | header 76 | workoutCards 77 | } 78 | .background(RoundedRectangle(cornerRadius: 30).fill(Color.white)) 79 | .shadow(color: .gray.opacity(0.2), radius: 10, x: 5, y: 5) 80 | } 81 | 82 | private var header: some View { 83 | HStack { 84 | Text("Workouts") 85 | .font(.title) 86 | .bold() 87 | .foregroundColor(.black.opacity(0.9)) 88 | 89 | Spacer() 90 | Button(action: { 91 | self.showingWorkoutBuilder = true 92 | }) { 93 | Image(systemName: "plus") 94 | .resizable() 95 | .scaledToFit() 96 | .frame(width: 16, height: 16) 97 | .padding(13) 98 | .foregroundColor(ColorSchemeManager.shared.currentColorScheme.med.opacity(0.4)) 99 | .background(Color.white) 100 | .cornerRadius(15) 101 | } 102 | Button(action: { 103 | }) { 104 | HStack { 105 | Image(systemName: "sparkle") 106 | .resizable() 107 | .scaledToFit() 108 | .frame(width: 16, height: 16) 109 | Text("Generate") 110 | .frame(alignment: .center) 111 | .bold() 112 | } 113 | .padding(13) 114 | .foregroundColor(Color.white) 115 | .background(ColorSchemeManager.shared.currentColorScheme.med.opacity(0.5)) 116 | .cornerRadius(15) 117 | } 118 | } 119 | .padding([.top, .horizontal]) 120 | } 121 | 122 | private var workoutCards: some View { 123 | ScrollView { 124 | VStack { 125 | ForEach(workouts, id: \.workoutId) { workout in 126 | WorkoutCardView(workout: workout, onDelete: { workoutToDelete in 127 | deleteWorkout(workoutToDelete) 128 | }) 129 | .onTapGesture { 130 | self.selectedWorkout = workout 131 | } 132 | } 133 | } 134 | } 135 | } 136 | 137 | private func fetchWorkouts() { 138 | guard let memberIdData = KeychainManager.load(service: "YourAppService", account: "userId"), 139 | let memberIdString = String(data: memberIdData, encoding: .utf8), 140 | let memberId = Int(memberIdString), 141 | let authKeyData = KeychainManager.load(service: "YourAppService", account: "authKey"), 142 | let authKey = String(data: authKeyData, encoding: .utf8) else { 143 | print("Unable to retrieve member ID and/or auth key from Keychain") 144 | return 145 | } 146 | 147 | NetworkManager.fetchWorkoutsForMember(memberId: memberId, authKey: authKey) { result in 148 | DispatchQueue.main.async { 149 | switch result { 150 | case .success(let fetchedWorkouts): 151 | self.workouts = fetchedWorkouts 152 | if self.selectedWorkout == nil, let firstWorkout = fetchedWorkouts.first { 153 | self.selectedWorkout = firstWorkout 154 | } 155 | case .failure(let error): 156 | print("Error fetching workouts: \(error)") 157 | } 158 | } 159 | } 160 | } 161 | 162 | private func fetchExercises(for workout: Workout) { 163 | guard let memberIdData = KeychainManager.load(service: "YourAppService", account: "userId"), 164 | let memberIdString = String(data: memberIdData, encoding: .utf8), 165 | let memberId = Int(memberIdString), 166 | let authKeyData = KeychainManager.load(service: "YourAppService", account: "authKey"), 167 | let authKey = String(data: authKeyData, encoding: .utf8) else { 168 | print("Unable to retrieve member ID and/or auth key from Keychain") 169 | return 170 | } 171 | 172 | NetworkManager.fetchExercisesForWorkout(workoutId: workout.workoutId, authKey: authKey) { result in 173 | DispatchQueue.main.async { 174 | switch result { 175 | case .success(let loadedExercises): 176 | self.exercises = loadedExercises 177 | case .failure(let error): 178 | print("Error fetching exercises: \(error)") 179 | } 180 | } 181 | } 182 | } 183 | 184 | private func deleteWorkout(_ workout: Workout) { 185 | guard let authKeyData = KeychainManager.load(service: "YourAppService", account: "authKey"), 186 | let authKey = String(data: authKeyData, encoding: .utf8) else { 187 | print("Unable to retrieve auth key from Keychain") 188 | return 189 | } 190 | 191 | isLoading = true 192 | 193 | NetworkManager.deleteWorkout(workoutId: workout.workoutId, authKey: authKey) { success in 194 | DispatchQueue.main.async { 195 | isLoading = false 196 | if success { 197 | if let index = workouts.firstIndex(where: { $0.workoutId == workout.workoutId }) { 198 | workouts.remove(at: index) 199 | if selectedWorkout?.workoutId == workout.workoutId { 200 | selectedWorkout = workouts.first 201 | } 202 | } 203 | } else { 204 | showDeletionErrorAlert = true 205 | } 206 | } 207 | } 208 | } 209 | } 210 | 211 | struct WorkoutCardView: View { 212 | var workout: Workout 213 | @State private var showDeleteConfirmation = false 214 | var onDelete: (Workout) -> Void 215 | 216 | var body: some View { 217 | VStack { 218 | HStack { 219 | VStack(alignment: .leading, spacing: 5) { 220 | Text(workout.workoutName) 221 | .font(.subheadline) 222 | .fontWeight(.bold) 223 | .foregroundColor(.primary) 224 | 225 | Text("(\(workout.exerciseIds.count)) exercises") 226 | .font(.subheadline) 227 | .foregroundColor(.secondary) 228 | } 229 | .padding([.top, .bottom, .leading]) 230 | 231 | Spacer() 232 | 233 | Menu { 234 | Button(role: .destructive) { 235 | showDeleteConfirmation = true 236 | } label: { 237 | Label("Delete", systemImage: "trash") 238 | } 239 | Button { 240 | renameWorkout() 241 | } label: { 242 | Label("Rename", systemImage: "pencil") 243 | } 244 | } label: { 245 | Image(systemName: "ellipsis") 246 | .imageScale(.large) 247 | .foregroundColor(.black) 248 | .padding(10) 249 | } 250 | .contentShape(Rectangle()) 251 | .padding([.top, .trailing]) 252 | } 253 | .background(Color.gray.opacity(0.05)) 254 | .cornerRadius(20) 255 | } 256 | .shadow(color: Color.black.opacity(0.03), radius: 1, x: 2, y: 3) 257 | .padding(.horizontal) 258 | .alert(isPresented: $showDeleteConfirmation) { 259 | Alert( 260 | title: Text("Delete Workout"), 261 | message: Text("Are you sure you want to delete this workout? This action cannot be undone."), 262 | primaryButton: .destructive(Text("Delete")) { 263 | onDelete(workout) 264 | }, 265 | secondaryButton: .cancel() 266 | ) 267 | } 268 | } 269 | 270 | private func renameWorkout() { 271 | } 272 | } 273 | 274 | 275 | struct WorkoutPlanCardView: View { 276 | let workout: Workout 277 | let exercises: [Exercise] 278 | @State private var isShowingDetail = false 279 | 280 | var body: some View { 281 | VStack(alignment: .leading) { 282 | HStack { 283 | VStack(alignment: .leading) { 284 | Text(workout.workoutName) 285 | .font(.title) 286 | .fontWeight(.bold) 287 | } 288 | Spacer() 289 | VStack { 290 | Text("\(exercises.count) Exercises") 291 | .fontWeight(.semibold) 292 | } 293 | } 294 | .padding(.top) 295 | .padding(.horizontal) 296 | 297 | HStack { 298 | WorkoutPreviewScrollView(exercises: exercises) 299 | 300 | Button(action: { 301 | isShowingDetail = true 302 | }) { 303 | Label("Start", systemImage: "play.circle.fill") 304 | .font(.subheadline) 305 | .padding() 306 | .foregroundColor(.black) 307 | .background(Color.gray.opacity(0.1)) 308 | .cornerRadius(15) 309 | } 310 | .padding() 311 | .fullScreenCover(isPresented: $isShowingDetail) { 312 | WorkoutDetailView(workout: workout, exercises: exercises) 313 | } 314 | } 315 | } 316 | .background(RoundedRectangle(cornerRadius: 30).fill(Color.white)) 317 | .shadow(color: .gray.opacity(0.2), radius: 10, x: 5, y: 5) 318 | } 319 | } 320 | 321 | struct WorkoutPreviewScrollView: View { 322 | let exercises: [Exercise] 323 | @State private var currentIndex: Int = 0 324 | let timer = Timer.publish(every: 3, on: .main, in: .common).autoconnect() 325 | 326 | let gradientColors = [ColorSchemeManager.shared.currentColorScheme.med, ColorSchemeManager.shared.currentColorScheme.light] 327 | 328 | var body: some View { 329 | TabView(selection: $currentIndex) { 330 | ForEach(exercises.indices, id: \.self) { index in 331 | VStack(alignment: .leading, spacing: 10) { 332 | Text(exercises[index].title) 333 | .font(.headline) 334 | .fontWeight(.medium) 335 | HStack(spacing: 10) { 336 | GradientTag(text:"\(exercises[index].equipment)", gradientColors: gradientColors) 337 | GradientTag(text:"\(exercises[index].difficulty)", gradientColors: gradientColors) 338 | } 339 | } 340 | .padding() 341 | .tag(index) 342 | } 343 | } 344 | .frame(height: 110) 345 | .tabViewStyle(PageTabViewStyle()) 346 | .onReceive(timer) { _ in 347 | withAnimation { 348 | currentIndex = (currentIndex + 1) % exercises.count 349 | } 350 | } 351 | } 352 | } 353 | 354 | struct GradientTag: View { 355 | let text: String 356 | let gradientColors: [Color] 357 | 358 | var body: some View { 359 | Text(text) 360 | .font(.subheadline) 361 | .foregroundColor(gradientColors[0]) 362 | .padding(.horizontal, 7) 363 | .padding(.vertical, 4) 364 | .background( 365 | Capsule() 366 | .fill(.white) 367 | ) 368 | .overlay( 369 | Capsule() 370 | .stroke(gradientColors[0], lineWidth: 1) 371 | ) 372 | } 373 | } 374 | 375 | struct WorkoutView_Previews: PreviewProvider { 376 | static var previews: some View { 377 | WorkoutView() 378 | } 379 | } 380 | -------------------------------------------------------------------------------- /iOS/ReminderX/Views/WorkoutBuilder.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftUI 3 | 4 | struct WorkoutBuilderView: View { 5 | @Binding var isPresented: Bool 6 | @State private var exercises: [Exercise] = [] 7 | @State private var selectedExercises: [Exercise] = [] 8 | @State private var workoutName: String = "New Workout" 9 | @State private var searchText: String = "" 10 | @State private var isLoading = true 11 | @State private var isEditingWorkoutName = false 12 | @State private var currentPage = 1 13 | @State private var isLoadingMore = false 14 | @State private var hasMoreExercises = true 15 | 16 | var filteredExercises: [Exercise] { 17 | if searchText.isEmpty { 18 | return exercises 19 | } else { 20 | return exercises.filter { $0.title.localizedCaseInsensitiveContains(searchText) } 21 | } 22 | } 23 | 24 | var body: some View { 25 | VStack { 26 | WorkoutPreviewCardView(workoutName: $workoutName, selectedExercises: $selectedExercises) 27 | if !selectedExercises.isEmpty { 28 | Button(action: saveWorkout) { 29 | Text("Save Workout") 30 | .fontWeight(.bold) 31 | .foregroundColor(Color.black) 32 | .padding() 33 | .frame(maxWidth: .infinity) 34 | .background(Color.gray.opacity(0.05)) 35 | .cornerRadius(20) 36 | } 37 | .padding(.horizontal) 38 | .padding(.bottom) 39 | } 40 | 41 | RoundedRectangle(cornerRadius: 20) 42 | .fill(Color.gray.opacity(0.05)) 43 | .overlay( 44 | VStack { 45 | exercisesHeader 46 | exercisesList 47 | } 48 | ) 49 | .padding(.horizontal) 50 | } 51 | .onAppear(perform: loadExercises) 52 | } 53 | 54 | private var exercisesHeader: some View { 55 | VStack(alignment: .leading) { 56 | Text("Exercises") 57 | .font(.title) 58 | .bold() 59 | 60 | TextField("Search exercises", text: $searchText) 61 | .padding(10) 62 | .background(Color.gray.opacity(0.05)) 63 | .cornerRadius(15) 64 | .onChange(of: searchText) { _ in 65 | } 66 | } 67 | .padding(.horizontal) 68 | .padding(.top) 69 | } 70 | 71 | private func saveWorkout() { 72 | if let memberIdData = KeychainManager.load(service: "YourAppService", account: "userId"), 73 | let memberIdString = String(data: memberIdData, encoding: .utf8), 74 | let memberId = Int(memberIdString), 75 | let authKeyData = KeychainManager.load(service: "YourAppService", account: "authKey"), 76 | let authKey = String(data: authKeyData, encoding: .utf8) { 77 | 78 | let exerciseIds = selectedExercises.map { $0.id } 79 | 80 | NetworkManager.createWorkout( 81 | memberId: memberId, 82 | workoutName: workoutName, 83 | exerciseIds: exerciseIds, 84 | authKey: authKey) { result in 85 | DispatchQueue.main.async { 86 | switch result { 87 | case .success(): 88 | print("Workout successfully saved!") 89 | isPresented = false 90 | case .failure(let error): 91 | print("Failed to save workout: \(error.localizedDescription)") 92 | } 93 | } 94 | } 95 | } 96 | } 97 | 98 | private func addExercise(_ exercise: Exercise) { 99 | if !selectedExercises.contains(where: { $0.id == exercise.id }) { 100 | selectedExercises.append(exercise) 101 | } 102 | } 103 | 104 | private func deleteExercise(at offsets: IndexSet) { 105 | selectedExercises.remove(atOffsets: offsets) 106 | } 107 | 108 | private func moveExercise(from source: IndexSet, to destination: Int) { 109 | selectedExercises.move(fromOffsets: source, toOffset: destination) 110 | } 111 | 112 | private func loadExercises() { 113 | isLoadingMore = true 114 | NetworkManager.fetchExercises(page: currentPage) { newExercises in 115 | DispatchQueue.main.async { 116 | if !newExercises.isEmpty { 117 | self.exercises.append(contentsOf: newExercises) 118 | } else { 119 | self.hasMoreExercises = false 120 | } 121 | self.isLoadingMore = false 122 | } 123 | } 124 | } 125 | 126 | private func loadMoreExercises() { 127 | guard hasMoreExercises, !isLoadingMore else { return } 128 | currentPage += 1 129 | loadExercises() 130 | } 131 | 132 | private var exercisesList: some View { 133 | ScrollView { 134 | VStack(spacing: 15) { 135 | ForEach(filteredExercises) { exercise in 136 | ExerciseCard(exercise: exercise, onAdd: { addExercise(exercise) }) 137 | } 138 | 139 | if isLoadingMore { 140 | ProgressView() 141 | } 142 | } 143 | .padding(.bottom) 144 | .onReachBottom(perform: loadMoreExercises) 145 | } 146 | } 147 | } 148 | 149 | struct OnReachBottomModifier: ViewModifier { 150 | var action: () -> Void 151 | 152 | func body(content: Content) -> some View { 153 | content.background( 154 | GeometryReader { geometry in 155 | Color.clear.preference(key: ViewOffsetKey.self, value: geometry.frame(in: .global).maxY) 156 | } 157 | ) 158 | .onPreferenceChange(ViewOffsetKey.self) { maxY in 159 | DispatchQueue.main.async { 160 | actionIfNeeded(maxY: maxY) 161 | } 162 | } 163 | } 164 | 165 | private func actionIfNeeded(maxY: CGFloat) { 166 | guard let rootView = UIApplication.shared.windows.first?.rootViewController?.view else { return } 167 | let rootViewHeight = rootView.frame.size.height 168 | let threshold = maxY - rootViewHeight 169 | 170 | if threshold < 50 { 171 | action() 172 | } 173 | } 174 | } 175 | 176 | struct ViewOffsetKey: PreferenceKey { 177 | static var defaultValue: CGFloat = 0 178 | static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { 179 | value += nextValue() 180 | } 181 | } 182 | 183 | extension View { 184 | func onReachBottom(perform action: @escaping () -> Void) -> some View { 185 | self.modifier(OnReachBottomModifier(action: action)) 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /iOS/ReminderX/WorkoutDetailView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import Combine 3 | 4 | struct WorkoutDetailView: View { 5 | let workout: Workout 6 | let exercises: [Exercise] 7 | 8 | @State private var currentExerciseIndex: Int = 0 9 | @State private var repCount: Int = 4 10 | @State private var setCount: Int = 10 11 | @State private var timerValue: Int = 0 12 | @State private var timerSubscription: AnyCancellable? 13 | @Environment(\.dismiss) var dismiss 14 | 15 | let gradientColors = [ColorSchemeManager.shared.currentColorScheme.med, ColorSchemeManager.shared.currentColorScheme.light] 16 | 17 | var body: some View { 18 | ZStack { 19 | backgroundView 20 | VStack(spacing: 16) { 21 | headerView 22 | if let exercise = currentExercise() { 23 | exerciseDetailView(exercise: exercise) 24 | countControls 25 | } 26 | Spacer() 27 | navigationButtons 28 | } 29 | .padding() 30 | .background(RoundedRectangle(cornerRadius: 16).fill(Color(.systemBackground))) 31 | .shadow(color: .gray.opacity(0.3), radius: 8, x: 2, y: 2) 32 | .padding() 33 | .onAppear(perform: startTimer) 34 | .onDisappear(perform: stopTimer) 35 | } 36 | } 37 | 38 | private var backgroundView: some View { 39 | RoundedRectangle(cornerRadius: 0) 40 | .fill( 41 | AngularGradient( 42 | gradient: Gradient(colors: gradientColors), 43 | center: .center, 44 | startAngle: .degrees(0), 45 | endAngle: .degrees(360) 46 | ) 47 | ) 48 | .blur(radius: 50) 49 | .edgesIgnoringSafeArea(.all) 50 | } 51 | 52 | private var headerView: some View { 53 | VStack(spacing: 8) { 54 | Text(workout.workoutName) 55 | .font(.title) 56 | .bold() 57 | .foregroundColor(.primary) 58 | Text("Time Elapsed: \(timerValue) s") 59 | .font(.subheadline) 60 | .foregroundColor(.secondary) 61 | } 62 | .padding(.horizontal) 63 | } 64 | 65 | private func exerciseDetailView(exercise: Exercise) -> some View { 66 | VStack(alignment: .leading, spacing: 12) { 67 | Text(exercise.title) 68 | .font(.headline) 69 | .bold() 70 | HStack(spacing: 8) { 71 | GradientTag(text: exercise.equipment, gradientColors: gradientColors) 72 | GradientTag(text: exercise.difficulty, gradientColors: gradientColors) 73 | } 74 | } 75 | .padding(.horizontal) 76 | } 77 | 78 | private var countControls: some View { 79 | HStack { 80 | Spacer() 81 | CounterView(value: $repCount, label: "Reps") 82 | Spacer() 83 | CounterView(value: $setCount, label: "Sets") 84 | Spacer() 85 | } 86 | .padding(.horizontal) 87 | } 88 | 89 | private var navigationButtons: some View { 90 | HStack(spacing: 20) { 91 | Button(action: previousExercise) { 92 | Text("Previous") 93 | .font(.headline) 94 | .padding() 95 | .frame(maxWidth: .infinity) 96 | .background(Color.blue.opacity(0.1)) 97 | .cornerRadius(10) 98 | } 99 | .disabled(currentExerciseIndex == 0) 100 | 101 | Button(action: endWorkout) { 102 | Text("Finish") 103 | .font(.headline) 104 | .padding() 105 | .frame(maxWidth: .infinity) 106 | .background(Color.red.opacity(0.1)) 107 | .cornerRadius(10) 108 | } 109 | 110 | Button(action: nextExercise) { 111 | Text("Next") 112 | .font(.headline) 113 | .padding() 114 | .frame(maxWidth: .infinity) 115 | .background(Color.blue.opacity(0.1)) 116 | .cornerRadius(10) 117 | } 118 | .disabled(currentExerciseIndex == exercises.count - 1) 119 | } 120 | .padding() 121 | } 122 | 123 | func currentExercise() -> Exercise? { 124 | guard currentExerciseIndex >= 0 && currentExerciseIndex < exercises.count else { return nil } 125 | return exercises[currentExerciseIndex] 126 | } 127 | 128 | func nextExercise() { 129 | if currentExerciseIndex < exercises.count - 1 { 130 | currentExerciseIndex += 1 131 | } 132 | } 133 | 134 | func previousExercise() { 135 | if currentExerciseIndex > 0 { 136 | currentExerciseIndex -= 1 137 | } 138 | } 139 | 140 | func endWorkout() { 141 | dismiss() 142 | } 143 | 144 | private func startTimer() { 145 | timerSubscription = Timer.publish(every: 1, on: .main, in: .common) 146 | .autoconnect() 147 | .sink { _ in timerValue += 1 } 148 | } 149 | 150 | private func stopTimer() { 151 | timerSubscription?.cancel() 152 | } 153 | } 154 | 155 | struct CounterView: View { 156 | @Binding var value: Int 157 | let label: String 158 | 159 | var body: some View { 160 | VStack { 161 | Text(label) 162 | .font(.subheadline) 163 | HStack { 164 | Button(action: { if value > 1 { value -= 1 } }) { 165 | Image(systemName: "minus.circle.fill") 166 | .font(.title2) 167 | } 168 | Text("\(value)") 169 | .font(.title3) 170 | .bold() 171 | .padding(.horizontal) 172 | Button(action: { value += 1 }) { 173 | Image(systemName: "plus.circle.fill") 174 | .font(.title2) 175 | } 176 | } 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /iOS/ReminderXTests/ReminderXTests.swift: -------------------------------------------------------------------------------- 1 | 2 | ReminderXTests.swift 3 | ReminderXTests 4 | 5 | Created by Aaron McLean on 2023-03-15. 6 | 7 | 8 | import XCTest 9 | @testable import ReminderX 10 | 11 | final class ReminderXTests: XCTestCase { 12 | 13 | override func setUpWithError() throws { 14 | Put setup code here. This method is called before the invocation of each test method in the class. 15 | } 16 | 17 | override func tearDownWithError() throws { 18 | Put teardown code here. This method is called after the invocation of each test method in the class. 19 | } 20 | 21 | func testExample() throws { 22 | This is an example of a functional test case. 23 | Use XCTAssert and related functions to verify your tests produce the correct results. 24 | Any test you write for XCTest can be annotated as throws and async. 25 | Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. 26 | Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. 27 | } 28 | 29 | func testPerformanceExample() throws { 30 | This is an example of a performance test case. 31 | self.measure { 32 | Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /iOS/ReminderXUITests/ReminderXUITests.swift: -------------------------------------------------------------------------------- 1 | 2 | ReminderXUITests.swift 3 | ReminderXUITests 4 | 5 | Created by Aaron McLean on 2023-03-15. 6 | 7 | 8 | import XCTest 9 | 10 | final class ReminderXUITests: XCTestCase { 11 | 12 | override func setUpWithError() throws { 13 | Put setup code here. This method is called before the invocation of each test method in the class. 14 | 15 | In UI tests it is usually best to stop immediately when a failure occurs. 16 | continueAfterFailure = false 17 | 18 | In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 19 | } 20 | 21 | override func tearDownWithError() throws { 22 | Put teardown code here. This method is called after the invocation of each test method in the class. 23 | } 24 | 25 | func testExample() throws { 26 | UI tests must launch the application that they test. 27 | let app = XCUIApplication() 28 | app.launch() 29 | 30 | Use XCTAssert and related functions to verify your tests produce the correct results. 31 | } 32 | 33 | func testLaunchPerformance() throws { 34 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { 35 | This measures how long it takes to launch your application. 36 | measure(metrics: [XCTApplicationLaunchMetric()]) { 37 | XCUIApplication().launch() 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /iOS/ReminderXUITests/ReminderXUITestsLaunchTests.swift: -------------------------------------------------------------------------------- 1 | 2 | ReminderXUITestsLaunchTests.swift 3 | ReminderXUITests 4 | 5 | Created by Aaron McLean on 2023-03-15. 6 | 7 | 8 | import XCTest 9 | 10 | final class ReminderXUITestsLaunchTests: XCTestCase { 11 | 12 | override class var runsForEachTargetApplicationUIConfiguration: Bool { 13 | true 14 | } 15 | 16 | override func setUpWithError() throws { 17 | continueAfterFailure = false 18 | } 19 | 20 | func testLaunch() throws { 21 | let app = XCUIApplication() 22 | app.launch() 23 | 24 | Insert steps here to perform after app launch but before taking a screenshot, 25 | such as logging into a test account or navigating somewhere in the app 26 | 27 | let attachment = XCTAttachment(screenshot: app.screenshot()) 28 | attachment.name = "Launch Screen" 29 | attachment.lifetime = .keepAlways 30 | add(attachment) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /uml.mmd: -------------------------------------------------------------------------------- 1 | classDiagram 2 | class ReminderXApp { 3 | +ContentView contentView 4 | } 5 | 6 | class ContentView { 7 | +isAuthenticated: Bool 8 | +userInfo: UserInfo? 9 | +userMetrics: [MemberMetric] 10 | +body(): some View 11 | +fetchUserDataIfNeeded(): Void 12 | +fetchUserMetricsIfNeeded(): Void 13 | } 14 | 15 | class HomeView { 16 | +ColorSchemeManager colorSchemeManager 17 | +String greetingText 18 | +TripleHeightCardView tripleHeightCardView 19 | +InfoCardView infoCardView 20 | +NetworkManager networkManager 21 | +Color color 22 | +loadData() 23 | +showLoadingIndicator() 24 | +hideLoadingIndicator() 25 | +animateLoadingIndicator() 26 | } 27 | class LoginView { 28 | -String email 29 | -String password 30 | -String errorMessage 31 | +Bool isAuthenticated 32 | +body() View 33 | +loginUser() 34 | +handleSuccessfulLogin(data: Data?) 35 | } 36 | class SettingsView { 37 | -UserInfo userInfo 38 | -List~MemberMetric~ userMetrics 39 | -String heightFeet 40 | -String heightInches 41 | -String weight 42 | -WeightUnit weightUnit 43 | +body() View 44 | +personalInformationSettings() List~SettingItem~ 45 | +healthMetricsSettings() List~SettingItem~ 46 | +workoutPreferencesSettings() List~SettingItem~ 47 | +nutritionSettings() List~SettingItem~ 48 | +appSettings() List~SettingItem~ 49 | +settingsSection(title: String, settings: List~SettingItem~) View 50 | +changeName() 51 | +changeUsername() 52 | +logOut() 53 | +settingItemView(iconName: String, title: String) View 54 | } 55 | class AiView { 56 | +@State userMessage: String 57 | +@State messages: [ChatMessage] 58 | +@State isWaitingForResponse: Bool 59 | +@State showMiniCards: Bool 60 | +@State keyboardHeight: CGFloat 61 | +@State navbarVisible: Bool 62 | +@State isTyping: Bool 63 | +@State typingMessage: String 64 | +@State aiTypingMessage: String 65 | +@State lastSentMessageDate: Date 66 | +@State typingMessages: [(UUID, String)] 67 | +@State gradientRotation: Double 68 | +let columns: [GridItem] 69 | +let gradientColors: [Color] 70 | +let api: ChatGPTAPI 71 | } 72 | class ChatMessage { 73 | +id: UUID 74 | +text: String 75 | +isUser: Bool 76 | } 77 | class WorkoutBuilder { 78 | +Binding isPresented 79 | +[Exercise] exercises 80 | +[Exercise] selectedExercises 81 | +String workoutName 82 | +String searchText 83 | +Bool isLoading 84 | +Bool isEditingWorkoutName 85 | +Int currentPage 86 | +Bool isLoadingMore 87 | +Bool hasMoreExercises 88 | +View body() 89 | +View exercisesHeader() 90 | +View exercisesList() 91 | +saveWorkout() 92 | +addExercise(Exercise exercise) 93 | +deleteExercise(IndexSet offsets) 94 | +moveExercise(IndexSet source, Int destination) 95 | +loadExercises() 96 | +loadMoreExercises() 97 | +onAppear() 98 | } 99 | 100 | class Exercise { 101 | +UUID id 102 | +String title 103 | } 104 | 105 | ReminderXApp --> ContentView 106 | ContentView --> HomeView 107 | ContentView --> LoginView 108 | ContentView --> SettingsView 109 | ContentView --> AiView 110 | ContentView --> WorkoutBuilder 111 | WorkoutBuilder --> Exercise 112 | AiView --> ChatMessage 113 | 114 | class ColorSchemeManager { 115 | +shared: ColorSchemeManager 116 | +transitionDuration: Double 117 | +currentColorScheme: (dark: Color, med: Color, light: Color) 118 | +updateColorScheme(to newColorScheme: ColorSchemeOption): Void 119 | } 120 | 121 | class KeychainManager { 122 | +save(_ data: Data, service: String, account: String) : OSStatus 123 | +load(service: String, account: String) : Data? 124 | +delete(service: String, account: String) : OSStatus 125 | +loadAuthKey() : String? 126 | } 127 | 128 | class NetworkManager { 129 | +createWorkout(memberId: Int, workoutName: String, exerciseIds: [Int], authKey: String, completion: @escaping (Result) -> Void) 130 | +fetchDetailedWorkoutData(workoutId: Int, authKey: String, completion: @escaping (Result) -> Void) 131 | +fetchWorkoutsForMember(memberId: Int, authKey: String, completion: @escaping (Result<[Workout], Error>) -> Void) 132 | +fetchMemberMetrics(memberId: Int, authKey: String, completion: @escaping (Result<[MemberMetric], Error>) -> Void) 133 | +fetchUserDataAndMetrics(memberId: Int, authKey: String, completion: @escaping (UserInfo?) -> Void) 134 | +signUpUser(with data: [String: Any], completion: @escaping (Result) -> Void) 135 | +fetchExercises(page: Int, completion: @escaping ([Exercise]) -> Void) 136 | } 137 | class Extensions { 138 | +func towAWDAWDAWDAWDHex() String? 139 | +init?(hex: String) 140 | } 141 | class VisualComponents { 142 | +VisualEffectBlur 143 | +NeumorphicGraphCardView 144 | +WorkoutDetailsView 145 | +WeightEntryView 146 | +HeightEntryView 147 | +WorkoutPlanCardView 148 | +WorkoutPreviewCardView 149 | +ExerciseCardView 150 | +WorkoutCardView 151 | +WorkoutPreviewScrollView 152 | +ExerciseCard 153 | +CompressedExerciseCard 154 | +CustomTabBar 155 | } 156 | 157 | class Models { 158 | +NetworkError 159 | +MemberMetric 160 | +DetailedWorkout 161 | +ExerciseDetails 162 | +Achievement 163 | +UserInfo 164 | +Workout 165 | +Exercise 166 | +WeightUnit 167 | +SignupResponse 168 | +LoginResponse 169 | +AppViewModel 170 | +TagView 171 | +CodableColor 172 | +Reminder 173 | } 174 | 175 | class Graph { 176 | +exampleGraphData 177 | +GraphData 178 | +LabelView 179 | +CustomGraphCardView 180 | +GraphLine 181 | +LineGraph 182 | +GraphPoints 183 | } 184 | 185 | class Server { 186 | - package.json 187 | - package-lock.json 188 | - sql/ 189 | - repbook.js 190 | - API Endpoints 191 | 192 | + initializeServer(): void 193 | + connectDatabase(): void 194 | + startServer(): void 195 | + shutdownServer(): void 196 | 197 | + POST /api/members 198 | + GET /api/members 199 | + POST /api/fitness_achievements 200 | + GET /api/fitness_achievements 201 | + POST /api/members_metrics 202 | + GET /api/members_metrics 203 | + POST /api/exercises 204 | + GET /api/exercises 205 | + POST /api/gym_memberships 206 | + GET /api/gym_memberships 207 | + POST /api/user_workout_plans 208 | + GET /api/user_workout_plans 209 | + POST /api/pr_tracker 210 | + GET /api/pr_tracker 211 | + POST /api/admin 212 | + GET /api/admin 213 | } 214 | 215 | class Database { 216 | + PostgreSQL 217 | + Tables 218 | + ER Diagram 219 | 220 | + connect(): void 221 | + executeQuery(query: String): ResultSet 222 | + closeConnection(): void 223 | 224 | + members 225 | + fitness_achievements 226 | + members_metrics 227 | + exercises 228 | + gym_memberships 229 | + user_workout_plans 230 | + pr_tracker 231 | + admin 232 | + workouts 233 | } 234 | 235 | class members { 236 | + id: UUID 237 | + name: String 238 | + email: String 239 | + password_hash: String 240 | + created_at: Date 241 | + updated_at: Date 242 | } 243 | 244 | class fitness_achievements { 245 | + id: UUID 246 | + member_id: UUID 247 | + achievement_type: String 248 | + description: String 249 | + date: Date 250 | } 251 | 252 | class members_metrics { 253 | + id: UUID 254 | + member_id: UUID 255 | + metric_type: String 256 | + value: Float 257 | + recorded_at: Date 258 | } 259 | 260 | class exercises { 261 | + id: UUID 262 | + name: String 263 | + category: String 264 | + description: String 265 | } 266 | 267 | class gym_memberships { 268 | + id: UUID 269 | + member_id: UUID 270 | + gym_name: String 271 | + start_date: Date 272 | + end_date: Date 273 | } 274 | 275 | class user_workout_plans { 276 | + id: UUID 277 | + member_id: UUID 278 | + workout_name: String 279 | + created_at: Date 280 | } 281 | 282 | class pr_tracker { 283 | + id: UUID 284 | + member_id: UUID 285 | + exercise_id: UUID 286 | + pr_value: Float 287 | + date: Date 288 | } 289 | 290 | class admin { 291 | + id: UUID 292 | + username: String 293 | + password_hash: String 294 | + role: String 295 | } 296 | 297 | class workouts { 298 | + id: UUID 299 | + workout_plan_id: UUID 300 | + exercise_id: UUID 301 | + reps: Integer 302 | + sets: Integer 303 | + weight: Float 304 | } 305 | 306 | 307 | 308 | Database --> members 309 | Database --> fitness_achievements 310 | Database --> members_metrics 311 | Database --> exercises 312 | Database --> gym_memberships 313 | Database --> user_workout_plans 314 | Database --> pr_tracker 315 | Database --> admin 316 | Database --> workouts 317 | 318 | members "1" --> "*" fitness_achievements : "member_id" 319 | members "1" --> "*" members_metrics : "member_id" 320 | members "1" --> "*" gym_memberships : "member_id" 321 | members "1" --> "*" user_workout_plans : "member_id" 322 | members "1" --> "*" pr_tracker : "member_id" 323 | pr_tracker "*" --> "1" exercises : "exercise_id" 324 | user_workout_plans "1" --> "*" workouts : "workout_plan_id" 325 | workouts "*" --> "1" exercises : "exercise_id" 326 | 327 | 328 | class members 329 | class fitness_achievements 330 | class members_metrics 331 | class exercises 332 | class gym_memberships 333 | class user_workout_plans 334 | class pr_tracker 335 | class admin 336 | 337 | ReminderXApp --> ColorSchemeManager 338 | ReminderXApp --> KeychainManager 339 | ReminderXApp --> NetworkManager 340 | NetworkManager --> Server : API Calls 341 | Server --> Database : Queries 342 | Database --> members 343 | Database --> fitness_achievements 344 | Database --> members_metrics 345 | Database --> exercises 346 | Database --> gym_memberships 347 | Database --> user_workout_plans 348 | Database --> pr_tracker 349 | Database --> admin 350 | 351 | ColorSchemeManager ..> Extensions 352 | KeychainManager ..> Extensions 353 | NetworkManager ..> Models 354 | NetworkManager ..> Extensions 355 | HomeView ..> VisualComponents 356 | HomeView ..> Graph 357 | --------------------------------------------------------------------------------