├── 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 |
8 |
9 | |
10 |
11 |
12 | |
13 |
14 |
15 | |
16 |
17 |
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 |
--------------------------------------------------------------------------------