[Pharo] Personal fitness tracker web app.
Introducing... MFTCards! :fire:
Changed files
- src/MyFitnessTracker/MFTCard.class.st
- src/MyFitnessTracker/MFTExercise.class.st
- src/MyFitnessTracker/MFTFooterComponent.class.st
- src/MyFitnessTracker/MFTPublicLandingPage.class.st
- src/MyFitnessTracker/MFTSession.class.st
- src/MyFitnessTracker/MFTUser.class.st
- src/MyFitnessTracker/MFTUserLandingPage.class.st
- src/MyFitnessTracker/MFTWeightsListPage.class.st
- src/MyFitnessTracker/MFTWorkoutsListPage.class.st
src/MyFitnessTracker/MFTCard.class.st
@@ -0,0 +1,268 @@
1
Added:
Class {
2
Added:
#name : #MFTCard,
3
Added:
#superclass : #SBSComponent,
4
Added:
#instVars : [
5
Added:
'title',
6
Added:
'type'
7
Added:
],
8
Added:
#category : #'MyFitnessTracker-Components'
9
Added:
}
10
Added:
11
Added:
{ #category : #'accessing - structure variables' }
12
Added:
MFTCard class >> type: aType [
13
Added:
14
Added:
^ self new
15
Added:
type: aType;
16
Added:
yourself
17
Added:
]
18
Added:
19
Added:
{ #category : #'accessing - structure variables' }
20
Added:
MFTCard class >> type: aType title: aString [
21
Added:
22
Added:
^ self new
23
Added:
type: aType;
24
Added:
title: aString;
25
Added:
yourself
26
Added:
]
27
Added:
28
Added:
{ #category : #accessing }
29
Added:
MFTCard >> defaultTitle [
30
Added:
31
Added:
^ 'Default Card'
32
Added:
]
33
Added:
34
Added:
{ #category : #initialization }
35
Added:
MFTCard >> initialize [
36
Added:
37
Added:
super initialize.
38
Added:
title := self defaultTitle.
39
Added:
type := #default
40
Added:
]
41
Added:
42
Added:
{ #category : #rendering }
43
Added:
MFTCard >> renderCardOn: html type: aType [
44
Added:
45
Added:
html card
46
Added:
marginTopAndBottom: 3;
47
Added:
with: [
48
Added:
html cardHeader: self title.
49
Added:
self
50
Added:
perform: ('render' , aType asCamelCase , 'CardOn:') asSymbol
51
Added:
with: html ]
52
Added:
]
53
Added:
54
Added:
{ #category : #rendering }
55
Added:
MFTCard >> renderContentOn: html [
56
Added:
57
Added:
self renderCardOn: html type: type
58
Added:
]
59
Added:
60
Added:
{ #category : #rendering }
61
Added:
MFTCard >> renderDefaultCardOn: html [
62
Added:
63
Added:
html cardBody: [
64
Added:
html paragraph: 'I am the default MFTCard representation.' ]
65
Added:
]
66
Added:
67
Added:
{ #category : #rendering }
68
Added:
MFTCard >> renderMainPublicCardOn: html [
69
Added:
70
Added:
html jumbotron
71
Added:
class: 'p-3 bg-light';
72
Added:
with: [
73
Added:
html displayHeading
74
Added:
level: 1;
75
Added:
with: 'Welcome to My Fitness Tracker!'.
76
Added:
html big textMuted with: 'Free Software never looked so fit.'.
77
Added:
html form: [
78
Added:
html formButton
79
Added:
bePrimary;
80
Added:
beLarge;
81
Added:
class: 'mt-3';
82
Added:
callback: [
83
Added:
self session pageManager activePage:
84
Added:
(self session pageManager mainPages at: 2) ];
85
Added:
with: 'Log a workout' ] ]
86
Added:
]
87
Added:
88
Added:
{ #category : #rendering }
89
Added:
MFTCard >> renderMainUserCardOn: html [
90
Added:
91
Added:
| lastLoginDate subtitle |
92
Added:
lastLoginDate := DateAndTime fromString:
93
Added:
self session user lastLoginDateAndTime.
94
Added:
subtitle := {
95
Added:
'Last logged in'.
96
Added:
(lastLoginDate dayOfWeekName , ',').
97
Added:
lastLoginDate asDate asString.
98
Added:
'at'.
99
Added:
lastLoginDate asTimeUTC asString.
100
Added:
'UTC' } joinUsing: ' '.
101
Added:
html card: [
102
Added:
html jumbotron
103
Added:
class: 'p-3 bg-light';
104
Added:
with: [
105
Added:
html displayHeading
106
Added:
level: 1;
107
Added:
with: [
108
Added:
html
109
Added:
text: 'Welcome, ';
110
Added:
code: self session user username;
111
Added:
text: '!' ].
112
Added:
html big textMuted with: subtitle ] ]
113
Added:
]
114
Added:
115
Added:
{ #category : #rendering }
116
Added:
MFTCard >> renderPromisesCardOn: html [
117
Added:
118
Added:
html cardBody: [
119
Added:
html unorderedList: [
120
Added:
html listItem:
121
Added:
'Every single feature shall always be free of charge.'.
122
Added:
html listItem:
123
Added:
'No user data shall ever be sold, under any circumstances.'.
124
Added:
html listItem: 'No user data shall ever be tracked.' ] ]
125
Added:
]
126
Added:
127
Added:
{ #category : #rendering }
128
Added:
MFTCard >> renderWeightsCardOn: html [
129
Added:
130
Added:
html cardBody: [
131
Added:
html paragraph: 'Great job logging your weight '
132
Added:
, self session user weights size asString , ' times!'.
133
Added:
html form
134
Added:
class: 'my-3';
135
Added:
with: [
136
Added:
html formButton
137
Added:
bePrimary;
138
Added:
beSmall;
139
Added:
callback: [
140
Added:
self session pageManager activePage:
141
Added:
(self session pageManager mainPages at: 3) ];
142
Added:
with: 'Log a weight' ].
143
Added:
self renderWeightsGraphOn: html ]
144
Added:
]
145
Added:
146
Added:
{ #category : #rendering }
147
Added:
MFTCard >> renderWeightsGraphOn: html [
148
Added:
149
Added:
| labels data |
150
Added:
labels := self session user weights collect: [ :weight |
151
Added:
weight date yyyymmdd ].
152
Added:
data := self session user weights collect: #value.
153
Added:
html canvas
154
Added:
id: 'weightChart';
155
Added:
width: 4;
156
Added:
height: 1.
157
Added:
html script url: 'https://cdn.jsdelivr.net/npm/chart.js'.
158
Added:
html script:
159
Added:
'var ctx = document.getElementById("weightChart").getContext("2d");
160
Added:
var weightChart = new Chart(ctx, {
161
Added:
type: "line",
162
Added:
data: {
163
Added:
labels: ' , labels asJavascript , ',
164
Added:
datasets: [{
165
Added:
label: "weight",
166
Added:
data: ' , data asJavascript , ',
167
Added:
backgroundColor: "rgba(255, 99, 132, 0.2)",
168
Added:
borderColor: "rgba(255, 99, 132, 1)",
169
Added:
borderWidth: 1
170
Added:
}]
171
Added:
},
172
Added:
options: {
173
Added:
scales: {
174
Added:
yAxes: [{
175
Added:
ticks: {
176
Added:
beginAtZero: true
177
Added:
}
178
Added:
}]
179
Added:
}
180
Added:
}
181
Added:
});'
182
Added:
]
183
Added:
184
Added:
{ #category : #rendering }
185
Added:
MFTCard >> renderWorkoutsCardOn: html [
186
Added:
187
Added:
html cardBody: [
188
Added:
html paragraph:
189
Added:
'Great job logging ' , self session user workouts size asString
190
Added:
, ' workouts!'.
191
Added:
html form
192
Added:
class: 'my-3';
193
Added:
with: [
194
Added:
html formButton
195
Added:
bePrimary;
196
Added:
beSmall;
197
Added:
callback: [
198
Added:
self session pageManager activePage:
199
Added:
(self session pageManager mainPages at: 2) ];
200
Added:
with: 'Log a workout' ].
201
Added:
self renderWorkoutsGraphOn: html ]
202
Added:
]
203
Added:
204
Added:
{ #category : #rendering }
205
Added:
MFTCard >> renderWorkoutsGraphOn: html [
206
Added:
207
Added:
| exercises labels data repTally |
208
Added:
exercises := self session user workouts flatCollect: #exercises.
209
Added:
repTally := Dictionary new.
210
Added:
exercises do: [ :exercise |
211
Added:
repTally at: exercise englishName ifAbsentPut: [ 0 ].
212
Added:
repTally
213
Added:
at: exercise englishName
214
Added:
put: (repTally at: exercise englishName) + exercise repsBySet sum ].
215
Added:
labels := repTally keys.
216
Added:
data := repTally values.
217
Added:
html div
218
Added:
style: 'max-width: 24rem';
219
Added:
with: [
220
Added:
html canvas
221
Added:
id: 'workoutChart';
222
Added:
width: 4;
223
Added:
height: 1.
224
Added:
html script url: 'https://cdn.jsdelivr.net/npm/chart.js'.
225
Added:
html script:
226
Added:
'var ctx = document.getElementById("workoutChart").getContext("2d");
227
Added:
var workoutChart = new Chart(ctx, {
228
Added:
type: "doughnut",
229
Added:
data: {
230
Added:
labels: ' , labels asJavascript , ',
231
Added:
datasets: [{
232
Added:
data: ' , data asJavascript , '
233
Added:
}]
234
Added:
},
235
Added:
options: {
236
Added:
title: {
237
Added:
display: true,
238
Added:
text: "Reps by exercise"
239
Added:
}
240
Added:
}
241
Added:
});' ]
242
Added:
]
243
Added:
244
Added:
{ #category : #accessing }
245
Added:
MFTCard >> title [
246
Added:
247
Added:
^ title = self defaultTitle
248
Added:
ifTrue: [ ^ type asCapitalizedPhrase ]
249
Added:
ifFalse: [ ^ title ]
250
Added:
]
251
Added:
252
Added:
{ #category : #accessing }
253
Added:
MFTCard >> title: aString [
254
Added:
255
Added:
title := aString
256
Added:
]
257
Added:
258
Added:
{ #category : #accessing }
259
Added:
MFTCard >> type [
260
Added:
261
Added:
^ type
262
Added:
]
263
Added:
264
Added:
{ #category : #accessing }
265
Added:
MFTCard >> type: anObject [
266
Added:
267
Added:
type := anObject
268
Added:
]
src/MyFitnessTracker/MFTExercise.class.st
@@ -237,7 +237,9 @@
237
237
{ #category : #'as yet unclassified' }
238
238
MFTExercise >> repsBySet [
239
239
240
Removed:
^ sets collect: #reps
240
Added:
^ (sets collect: #reps)
241
Added:
ifNotEmpty: [ ^ sets collect: #reps ]
242
Added:
ifEmpty: [ ^ { 0 } ]
241
243
]
242
244
243
245
{ #category : #accessing }
src/MyFitnessTracker/MFTFooterComponent.class.st
@@ -12,5 +12,5 @@
12
12
with: [
13
13
html card: [
14
14
html cardHeader:
15
Removed:
'Copyright ' , Date today year asString , ' Marius PETER' ] ]
15
Added:
'Copyright 2023-' , Date today year asString , ' Marius PETER' ] ]
16
16
]
src/MyFitnessTracker/MFTPublicLandingPage.class.st
@@ -15,134 +15,9 @@
15
15
MFTPublicLandingPage >> renderContentOn: html [
16
16
17
17
html container: [
18
Removed:
self renderMainCardOn: html.
19
Removed:
self renderWorkoutsGraphChartOn: html.
20
Removed:
self renderWeightsGraphChartOn: html.
21
Removed:
self renderPromisesCardOn: html ]
22
Removed:
]
23
Removed:
24
Removed:
{ #category : #rendering }
25
Removed:
MFTPublicLandingPage >> renderMainCardOn: html [
26
Removed:
27
Removed:
html card: [
28
Removed:
html jumbotron
29
Removed:
class: 'p-3 bg-light';
30
Removed:
with: [
31
Removed:
html displayHeading
32
Removed:
level: 1;
33
Removed:
with: 'Welcome to My Fitness Tracker!'.
34
Removed:
html big textMuted with: 'Free Software never looked so fit.'.
35
Removed:
html form: [ "TODO"
36
Removed:
html formButton
37
Removed:
bePrimary;
38
Removed:
beLarge;
39
Removed:
class: 'mt-3';
40
Removed:
callback: [
41
Removed:
self session pageManager activePage:
42
Removed:
(self session pageManager mainPages at: 2) ];
43
Removed:
with: 'Log a workout' ] ] ]
44
Removed:
]
45
Removed:
46
Removed:
{ #category : #rendering }
47
Removed:
MFTPublicLandingPage >> renderPromisesCardOn: html [
48
Removed:
49
Removed:
html card
50
Removed:
marginTopAndBottom: 3;
51
Removed:
with: [
52
Removed:
html cardHeader: 'My promise to all account owners...'.
53
Removed:
html cardBody: [
54
Removed:
html unorderedList: [
55
Removed:
html listItem: 'All features shall always be entirely free.'.
56
Removed:
html listItem: 'No user data shall ever be sold.'.
57
Removed:
html listItem: '.' ] ] ]
58
Removed:
]
59
Removed:
60
Removed:
{ #category : #rendering }
61
Removed:
MFTPublicLandingPage >> renderWeightsGraphChartOn: html [
62
Removed:
63
Removed:
html card with: [
64
Removed:
html cardHeader: 'Weight'.
65
Removed:
html cardBody: [ self renderWeightsGraphOn: html ] ]
66
Removed:
]
67
Removed:
68
Removed:
{ #category : #rendering }
69
Removed:
MFTPublicLandingPage >> renderWeightsGraphOn: html [
70
Removed:
71
Removed:
| labels data |
72
Removed:
labels := self session weights collect: #date.
73
Removed:
data := #( 10 15 40 25 ).
74
Removed:
html canvas
75
Removed:
id: 'weightChart';
76
Removed:
width: 100;
77
Removed:
height: 30.
78
Removed:
html script url: 'https://cdn.jsdelivr.net/npm/chart.js'.
79
Removed:
html script:
80
Removed:
'var ctx = document.getElementById("weightChart").getContext("2d");
81
Removed:
var weightChart = new Chart(ctx, {
82
Removed:
type: "bar",
83
Removed:
data: {
84
Removed:
labels: ' , labels asJavascript , ',
85
Removed:
datasets: [{
86
Removed:
label: "# of Votes",
87
Removed:
data: ' , data asJavascript , ',
88
Removed:
backgroundColor: "rgba(255, 99, 132, 0.2)",
89
Removed:
borderColor: "rgba(255, 99, 132, 1)",
90
Removed:
borderWidth: 1
91
Removed:
}]
92
Removed:
},
93
Removed:
options: {
94
Removed:
scales: {
95
Removed:
yAxes: [{
96
Removed:
ticks: {
97
Removed:
beginAtZero: true
98
Removed:
}
99
Removed:
}]
100
Removed:
}
101
Removed:
}
102
Removed:
});'
103
Removed:
]
104
Removed:
105
Removed:
{ #category : #rendering }
106
Removed:
MFTPublicLandingPage >> renderWorkoutsGraphChartOn: html [
107
Removed:
108
Removed:
html card with: [
109
Removed:
html cardHeader: 'Workouts'.
110
Removed:
html cardBody: [ self renderWorkoutsGraphOn: html ] ]
111
Removed:
]
112
Removed:
113
Removed:
{ #category : #rendering }
114
Removed:
MFTPublicLandingPage >> renderWorkoutsGraphOn: html [
115
Removed:
116
Removed:
| labels data |
117
Removed:
labels := #( 'January' 'February' 'March' 'April' ).
118
Removed:
data := #( 10 15 40 25 ).
119
Removed:
html canvas
120
Removed:
id: 'workoutChart';
121
Removed:
width: 100;
122
Removed:
height: 30.
123
Removed:
html script url: 'https://cdn.jsdelivr.net/npm/chart.js'.
124
Removed:
html script:
125
Removed:
'var ctx = document.getElementById("workoutChart").getContext("2d");
126
Removed:
var workoutChart = new Chart(ctx, {
127
Removed:
type: "bar",
128
Removed:
data: {
129
Removed:
labels: ' , labels asJavascript , ',
130
Removed:
datasets: [{
131
Removed:
label: "# of Votes",
132
Removed:
data: ' , data asJavascript , ',
133
Removed:
backgroundColor: "rgba(255, 99, 132, 0.2)",
134
Removed:
borderColor: "rgba(255, 99, 132, 1)",
135
Removed:
borderWidth: 1
136
Removed:
}]
137
Removed:
},
138
Removed:
options: {
139
Removed:
scales: {
140
Removed:
yAxes: [{
141
Removed:
ticks: {
142
Removed:
beginAtZero: true
143
Removed:
}
144
Removed:
}]
145
Removed:
}
146
Removed:
}
147
Removed:
});'
18
Added:
html render: (MFTCard type: #mainPublic).
19
Added:
html render: (MFTCard type: #workouts).
20
Added:
html render: (MFTCard type: #weights).
21
Added:
html render:
22
Added:
(MFTCard type: #promises title: 'My promises to all users') ]
148
23
]
src/MyFitnessTracker/MFTSession.class.st
@@ -25,8 +25,8 @@
25
25
MFTSession >> initialize [
26
26
27
27
super initialize.
28
Removed:
user := MFTUser new.
29
Removed:
pageManager := MFTPageManager new.
28
Added:
user := MFTUser newDefault.
29
Added:
pageManager := MFTPageManager new
30
30
]
31
31
32
32
{ #category : #login }
src/MyFitnessTracker/MFTUser.class.st
@@ -8,7 +8,9 @@
8
8
'realFirstName',
9
9
'realLastName',
10
10
'isLoggedIn',
11
Removed:
'lastLoginDateAndTime'
11
Added:
'lastLoginDateAndTime',
12
Added:
'workouts',
13
Added:
'weights'
12
14
],
13
15
#category : #'MyFitnessTracker-Model'
14
16
}
@@ -26,6 +28,19 @@
26
28
]
27
29
28
30
{ #category : #'instance creation' }
31
Added:
MFTUser class >> newDefault [
32
Added:
33
Added:
^ self new
34
Added:
username: 'mrs_default';
35
Added:
realFirstName: 'Deborah';
36
Added:
realLastName: 'Default';
37
Added:
dateOfBirth: Date today;
38
Added:
isLoggedIn: false;
39
Added:
workouts: MFTWorkoutsListPage placeholder workouts;
40
Added:
weights: MFTWeightsListPage placeholder weights
41
Added:
]
42
Added:
43
Added:
{ #category : #'instance creation' }
29
44
MFTUser class >> newFromDictionary: aDictionary [
30
45
31
46
^ self new
@@ -67,8 +82,9 @@
67
82
realFirstName := String new.
68
83
realLastName := String new.
69
84
dateOfBirth := Date new.
70
Removed:
71
Removed:
isLoggedIn := false
85
Added:
isLoggedIn := false.
86
Added:
workouts := OrderedCollection new.
87
Added:
weights := OrderedCollection new
72
88
]
73
89
74
90
{ #category : #accessing }
@@ -147,4 +163,28 @@
147
163
MFTUser >> username: anObject [
148
164
149
165
username := anObject
166
Added:
]
167
Added:
168
Added:
{ #category : #accessing }
169
Added:
MFTUser >> weights [
170
Added:
171
Added:
^ weights
172
Added:
]
173
Added:
174
Added:
{ #category : #accessing }
175
Added:
MFTUser >> weights: anObject [
176
Added:
177
Added:
weights := anObject
178
Added:
]
179
Added:
180
Added:
{ #category : #accessing }
181
Added:
MFTUser >> workouts [
182
Added:
183
Added:
^ workouts
184
Added:
]
185
Added:
186
Added:
{ #category : #accessing }
187
Added:
MFTUser >> workouts: anObject [
188
Added:
189
Added:
workouts := anObject
150
190
]
src/MyFitnessTracker/MFTUserLandingPage.class.st
@@ -15,26 +15,9 @@
15
15
MFTUserLandingPage >> renderContentOn: html [
16
16
17
17
html container: [
18
Removed:
self renderMainCardOn: html.
19
Removed:
self renderWorkoutsSummaryCardOn: html ]
20
Removed:
]
21
Removed:
22
Removed:
{ #category : #rendering }
23
Removed:
MFTUserLandingPage >> renderMainCardOn: html [
24
Removed:
25
Removed:
html card: [
26
Removed:
html jumbotron
27
Removed:
class: 'p-3 bg-light';
28
Removed:
with: [
29
Removed:
html displayHeading
30
Removed:
level: 1;
31
Removed:
with: [
32
Removed:
html
33
Removed:
text: 'Welcome, ';
34
Removed:
code: self session user username;
35
Removed:
text: '!' ].
36
Removed:
html big textMuted with:
37
Removed:
'Last logged in ' , self session user lastLoginDateAndTime ] ]
18
Added:
html render: (MFTCard type: #mainUser).
19
Added:
html render: (MFTCard type: #workouts).
20
Added:
html render: MFTCard new ]
38
21
]
39
22
40
23
{ #category : #rendering }
src/MyFitnessTracker/MFTWeightsListPage.class.st
@@ -24,7 +24,7 @@
24
24
25
25
| newWeight |
26
26
newWeight := self call: MFTWeightDialog new.
27
Removed:
newWeight ifNotNil: [ weights addFirst: newWeight ]
27
Added:
newWeight ifNotNil: [ self session user weights addLast: newWeight ]
28
28
]
29
29
30
30
{ #category : #accessing }
@@ -125,7 +125,13 @@
125
125
]
126
126
127
127
{ #category : #accessing }
128
Removed:
MFTWeightsListPage >> weights: aWeight [
128
Added:
MFTWeightsListPage >> weights [
129
129
130
Removed:
weights := aWeight
130
Added:
^ weights
131
Added:
]
132
Added:
133
Added:
{ #category : #accessing }
134
Added:
MFTWeightsListPage >> weights: aMFTWeightCollection [
135
Added:
136
Added:
weights := aMFTWeightCollection
131
137
]
src/MyFitnessTracker/MFTWorkoutsListPage.class.st
@@ -184,11 +184,14 @@
184
184
html accordionButton
185
185
dataToggle: 'collapse';
186
186
dataTarget: '#collapse' , i asString;
187
Removed:
with:
188
Removed:
workout date yyyymmdd , (workout primaryMuscle englishName
189
Removed:
ifNotEmpty: [
190
Removed:
' | ' , workout primaryMuscle englishName asString ]
191
Removed:
ifEmpty: [ '' ]) ].
187
Added:
with: [
188
Added:
html
189
Added:
text:
190
Added:
workout date yyyymmdd , ' ('
191
Added:
, workout date dayOfWeekName asString , ') ';
192
Added:
space.
193
Added:
workout primaryMuscle englishName ifNotEmpty: [
194
Added:
html strong: workout primaryMuscle englishName asString ] ] ].
192
195
html accordionCollapse
193
196
id: 'collapse' , i asString;
194
197
class: 'collapse';
@@ -206,7 +209,13 @@
206
209
]
207
210
208
211
{ #category : #accessing }
209
Removed:
MFTWorkoutsListPage >> workouts: aListOfWorkouts [
212
Added:
MFTWorkoutsListPage >> workouts [
210
213
211
Removed:
workouts := aListOfWorkouts
214
Added:
^ workouts
215
Added:
]
216
Added:
217
Added:
{ #category : #accessing }
218
Added:
MFTWorkoutsListPage >> workouts: aMFTWorkoutCollection [
219
Added:
220
Added:
workouts := aMFTWorkoutCollection
212
221
]