@@ -57,6 +57,8 @@ module El = struct
57
57
(* * Elements that are dynamically created (ids only) *)
58
58
let exercise_list_id = " learnocaml-main-exercise-list"
59
59
let exercise_bar = " learnocaml-main-exercise-bar"
60
+ let exercise_pane = " learnocaml-main-exercise-pane"
61
+ let filter_box = " learnocaml-filter-box"
60
62
let tutorial_id = " learnocaml-main-tutorial"
61
63
let lesson_id = " learnocaml-main-lesson"
62
64
let toplevel_id = " learnocaml-main-toplevel"
@@ -75,151 +77,221 @@ let get_url token dynamic_url static_url id =
75
77
| Some _ -> dynamic_url ^ Url. urlencode id ^ " /"
76
78
| None -> api_server ^ " /" ^ static_url ^ Url. urlencode id
77
79
78
- let exercises_display index display construct_exo =
79
- let open Tyxml_js.Html5 in
80
- let rec display_by_legacy lvl acc contents =
81
- match contents with
82
- | Exercise.Index. Exercises exercises ->
83
- List. fold_left
84
- (fun acc (exercise_id , meta_opt ) ->
85
- match meta_opt with
86
- | None -> acc
87
- | Some meta -> construct_exo exercise_id meta :: acc)
88
- acc exercises
89
- | Exercise.Index. Groups groups ->
90
- let h = match lvl with 1 -> h1 | 2 -> h2 | _ -> h3 in
91
- List. fold_left
92
- (fun acc (_ , Exercise.Index. { title ; contents } ) ->
93
- display_by_legacy (succ lvl)
94
- (h ~a: [ a_class [ " pack" ] ] [ txt title ] :: acc)
95
- contents)
96
- acc groups
97
- in
98
- let display_by_stars index =
99
- let cat1,cat2,cat3,cat4 = " 1 star" ," 2 stars" ," 3 stars" ," 4 stars" in
100
- let add acc id meta =
101
- let stars = meta.Exercise.Meta. stars in
102
- let key = if stars < = 1. then cat1 else
103
- if stars < = 2. then cat2 else
104
- if stars < = 3. then cat3 else cat4 in
105
- SMap. update key
106
- (function None -> Some [(id,meta)]
107
- | Some l -> Some ((id,meta) :: l))
108
- acc
80
+ type exercise_ordering = By_category | By_prereq | By_difficulty
81
+
82
+ let (exercise_filter_signal: string option React.signal ), set_exercise_filter =
83
+ React.S. create None
84
+
85
+ let (exercise_sort_signal: exercise_ordering React.signal ), set_exercise_sort =
86
+ React.S. create By_category
87
+
88
+ let make_exercises_to_display_signal index =
89
+ let get_index exo_sort exo_filter =
90
+ let index =
91
+ match exo_sort with
92
+ | By_category -> index
93
+ | By_prereq ->
94
+ Exercise.Index. Exercises
95
+ Exercise.Graph. (compute_graph index |>
96
+ fold (fun acc node ->
97
+ let id = node_exercise node in
98
+ (id, Some (Exercise.Index. find index id)) :: acc
99
+ ) [] |>
100
+ List. rev)
101
+ | By_difficulty ->
102
+ let module IntMap = Map. Make (Int ) in
103
+ let starmap =
104
+ Exercise.Index. fold_exercises (fun map id meta ->
105
+ IntMap. update
106
+ (int_of_float (Float. ceil meta.Exercise.Meta. stars))
107
+ (function None -> Some [id, Some meta]
108
+ | Some l -> Some ((id, Some meta) :: l))
109
+ map)
110
+ IntMap. empty index
111
+ in
112
+ let groups =
113
+ IntMap. fold (fun stars exercises acc ->
114
+ let title =
115
+ if stars = 1 then [% i" 1 star" ]
116
+ else Printf. sprintf [% if " %d stars" ] stars
117
+ in
118
+ (title,
119
+ {Exercise.Index. title;
120
+ contents = Exercise.Index. Exercises (List. rev exercises)})
121
+ :: acc)
122
+ starmap []
123
+ in
124
+ Exercise.Index. Groups (List. rev groups)
125
+ in
126
+ let index =
127
+ match exo_filter with
128
+ | None -> index
129
+ | Some filt_str ->
130
+ Exercise.Index. filter (fun _ meta ->
131
+ let re = Re. (compile (no_case (str filt_str))) in
132
+ List. exists (Re. execp re)
133
+ (meta.Exercise.Meta. title ::
134
+ Option. to_list meta.Exercise.Meta. short_description @
135
+ List. map fst meta.Exercise.Meta. author @
136
+ List. map snd meta.Exercise.Meta. author @
137
+ meta.Exercise.Meta. focus @
138
+ meta.Exercise.Meta. requirements))
139
+ index
109
140
in
110
- let by_stars = Exercise.Index. fold_exercises add SMap. empty index |> SMap. bindings in
111
- List. map ( fun ( star_cat , exos ) ->
112
- h1 ~a: [ a_class [ " pack " ] ] [ txt star_cat ] ::
113
- List. map ( fun ( id , meta ) -> construct_exo id meta) exos
114
- ) by_stars
115
- |> List. flatten
141
+ if index = Exercise.Index. Exercises [] then
142
+ Exercise.Index. Groups
143
+ [ " empty_group " ,
144
+ { Exercise.Index. title = [ % i " No exercises found " ];
145
+ Exercise.Index. contents = Exercise.Index. Exercises [] ; }]
146
+ else index
116
147
in
117
- match display with
118
- | `By_legacy -> List. rev (display_by_legacy 1 [] index)
119
- | `By_deps ->
120
- Exercise.Graph. (compute_graph index |>
121
- fold (fun acc node ->
122
- let id = node_exercise node in
123
- construct_exo id (Exercise.Index. find index id) :: acc
124
- ) [] )
125
- | `By_stars -> display_by_stars index
126
-
127
- let exercises_tab token : tab_handler =
128
- fun select_f (get ,set ,_ ) () ->
148
+ React.S. l2 get_index exercise_sort_signal exercise_filter_signal
149
+
150
+ let retain_signals = ref (React.S. const () )
151
+ (* Used to register signals as GC roots *)
152
+
153
+ let exercises_tab token : tab_handler =
154
+ fun _ _ () ->
129
155
let open Tyxml_js.Html5 in
130
156
show_loading [% i" Loading exercises" ] @@ fun () ->
131
157
Lwt_js. sleep 0.5 >> = fun () ->
132
158
retrieve (Learnocaml_api. Exercise_index token)
133
159
>> = fun (index , deadlines ) ->
134
- let display =
135
- match get " display" with
136
- | "legacy" -> `By_legacy
137
- | "deps" -> `By_deps
138
- | "stars" -> `By_stars
139
- | exception Not_found | _ -> `By_legacy
160
+ let exercises_to_display_signal =
161
+ make_exercises_to_display_signal index
140
162
in
141
- let format_exercise_list all_exercise_states =
142
- let format_exercise exercise_id {Exercise.Meta. kind; title; short_description; stars; _ } =
143
- let pct_init =
144
- match SMap. find exercise_id all_exercise_states with
145
- | exception Not_found -> None
146
- | { Answer. grade ; _ } -> grade in
147
- let pct_signal, pct_signal_set = React.S. create pct_init in
148
- Learnocaml_local_storage. (listener (exercise_state exercise_id)) :=
149
- Some (function
150
- | Some { Answer. grade ; _ } -> pct_signal_set grade
151
- | None -> pct_signal_set None ) ;
152
- let pct_text_signal =
153
- React.S. map
154
- (function
155
- | None -> " --"
156
- | Some 0 -> " 0%"
157
- | Some pct -> string_of_int pct ^ " %" )
158
- pct_signal in
159
- let time_left = match List. assoc_opt exercise_id deadlines with
160
- | None -> " "
161
- | Some 0. -> [% i" Exercise closed" ]
162
- | Some f -> Printf. sprintf [% if " Time left: %s" ]
163
- (string_of_seconds (int_of_float f))
164
- in
165
- let status_classes_signal =
166
- React.S. map
167
- (function
168
- | None -> [ " stats" ]
169
- | Some 0 -> [ " stats" ; " failure" ]
170
- | Some pct when pct > = 100 -> [ " stats" ; " success" ]
171
- | Some _ -> [ " stats" ; " partial" ])
172
- pct_signal in
173
- a ~a: [ a_href (get_url token " /exercises/" " exercise.html#id=" exercise_id) ;
174
- a_class [ " exercise" ] ] [
175
- div ~a: [ a_class [ " descr" ] ] (
176
- h1 [ txt title ] ::
177
- begin match short_description with
178
- | None -> []
179
- | Some text -> [ txt text ]
180
- end
181
- );
182
- div ~a: [ a_class [ " time-left" ] ] [H. txt time_left];
183
- div ~a: [ Tyxml_js.R.Html5. a_class status_classes_signal ] [
184
- stars_div stars;
185
- div ~a: [ a_class [ " length" ] ] [
186
- match kind with
187
- | Exercise.Meta. Project -> txt [% i" project" ]
188
- | Exercise.Meta. Problem -> txt [% i" problem" ]
189
- | Exercise.Meta. Exercise -> txt [% i" exercise" ] ] ;
190
- div ~a: [ a_class [ " score" ] ] [
191
- Tyxml_js.R.Html5. txt pct_text_signal
192
- ]
193
- ] ]
194
- in exercises_display index display format_exercise
163
+ let all_exercise_states =
164
+ Learnocaml_local_storage. (retrieve all_exercise_states)
165
+ in
166
+ let format_exercise exercise_id {Exercise.Meta. kind; title; short_description; stars; _ } =
167
+ let pct_init =
168
+ match SMap. find exercise_id all_exercise_states with
169
+ | exception Not_found -> None
170
+ | { Answer. grade ; _ } -> grade in
171
+ let pct_signal, pct_signal_set = React.S. create pct_init in
172
+ Learnocaml_local_storage. (listener (exercise_state exercise_id)) :=
173
+ Some (function
174
+ | Some { Answer. grade ; _ } -> pct_signal_set grade
175
+ | None -> pct_signal_set None ) ;
176
+ let pct_text_signal =
177
+ React.S. map
178
+ (function
179
+ | None -> " --"
180
+ | Some 0 -> " 0%"
181
+ | Some pct -> string_of_int pct ^ " %" )
182
+ pct_signal in
183
+ let time_left = match List. assoc_opt exercise_id deadlines with
184
+ | None -> " "
185
+ | Some 0. -> [% i" Exercise closed" ]
186
+ | Some f -> Printf. sprintf [% if " Time left: %s" ]
187
+ (string_of_seconds (int_of_float f))
188
+ in
189
+ let status_classes_signal =
190
+ React.S. map
191
+ (function
192
+ | None -> [ " stats" ]
193
+ | Some 0 -> [ " stats" ; " failure" ]
194
+ | Some pct when pct > = 100 -> [ " stats" ; " success" ]
195
+ | Some _ -> [ " stats" ; " partial" ])
196
+ pct_signal in
197
+ a ~a: [ a_href (get_url token " /exercises/" " exercise.html#id=" exercise_id) ;
198
+ a_class [ " exercise" ] ] [
199
+ div ~a: [ a_class [ " descr" ] ] (
200
+ h1 [ txt title ] ::
201
+ begin match short_description with
202
+ | None -> []
203
+ | Some text -> [ txt text ]
204
+ end
205
+ );
206
+ div ~a: [ a_class [ " time-left" ] ] [H. txt time_left];
207
+ div ~a: [ Tyxml_js.R.Html5. a_class status_classes_signal ] [
208
+ stars_div stars;
209
+ div ~a: [ a_class [ " length" ] ] [
210
+ match kind with
211
+ | Exercise.Meta. Project -> txt [% i" project" ]
212
+ | Exercise.Meta. Problem -> txt [% i" problem" ]
213
+ | Exercise.Meta. Exercise -> txt [% i" exercise" ] ] ;
214
+ div ~a: [ a_class [ " score" ] ] [
215
+ Tyxml_js.R.Html5. txt pct_text_signal
216
+ ]
217
+ ] ]
218
+ in
219
+ let rec format_exercise_list index =
220
+ match index with
221
+ | Exercise.Index. Exercises el ->
222
+ H. ul @@
223
+ List. map
224
+ (fun (id , meta ) -> H. li [format_exercise id (Option. get meta)])
225
+ el
226
+ | Exercise.Index. Groups gl ->
227
+ H. ul @@
228
+ List. map (fun (id , grp ) ->
229
+ let clas =
230
+ " group-title" ::
231
+ match gl with [] | [_] -> [] | _ -> [" collapsed" ]
232
+ in
233
+ let title =
234
+ H. div ~a: [a_id id; a_class clas]
235
+ [H. txt grp.Exercise.Index. title];
236
+ in
237
+ let exos = format_exercise_list grp.Exercise.Index. contents in
238
+ Manip.Ev. onclick title
239
+ (fun _ ->
240
+ ignore (Manip. toggleClass title " collapsed" );
241
+ false );
242
+ H. li [title; exos])
243
+ gl
244
+ in
245
+ let exercise_list_signal =
246
+ React.S. l1 format_exercise_list exercises_to_display_signal
195
247
in
196
- let list_div =
197
- match format_exercise_list Learnocaml_local_storage. (retrieve all_exercise_states) with
198
- | [] -> H. div [H. txt [% i" No open exercises at the moment" ]]
199
- | l ->
200
- let btns =
201
- H. div ~a: [ a_id El.Dyn. exercise_bar ] @@
202
- List. map (fun (id , active , name , callback ) ->
203
- let btn = button ~a: [a_id id] [ txt name ] in
204
- if active then Manip. addClass btn " active" ;
205
- Manip.Ev. onclick btn
206
- (fun _ -> ignore @@ callback () ; true );
207
- btn)
208
- [
209
- " by_legacy" , display = `By_legacy , [% i" By legacy" ],
210
- (fun () -> set " display" " legacy" ; select_f ~clear_cache: true () );
211
- " by_deps" , display = `By_deps , [% i" By order" ],
212
- (fun () -> set " display" " deps" ; select_f ~clear_cache: true () );
213
- " by_deps" , display = `By_stars , [% i" By stars" ],
214
- (fun () -> set " display" " stars" ; select_f ~clear_cache: true () )
215
- ]
248
+ let btns_sigs =
249
+ List. map (fun (id , sort , name ) ->
250
+ let btn = button ~a: [a_id id] [ txt name ] in
251
+ Manip.Ev. onclick btn
252
+ (fun _ -> set_exercise_sort sort; true );
253
+ let signal =
254
+ React.S. map (fun s ->
255
+ (if sort = s then Manip. addClass else Manip. removeClass)
256
+ btn " active"
257
+ ) exercise_sort_signal
258
+ in
259
+ btn, signal)
260
+ [
261
+ " by_category" , By_category , [% i" By category" ];
262
+ " by_prereq" , By_prereq , [% i" By prerequisites" ];
263
+ " by_difficulty" , By_difficulty , [% i" By difficulty" ];
264
+ ]
265
+ in
266
+ let btns, btns_sigs = List. split btns_sigs in
267
+ let btns =
268
+ btns @
269
+ [
270
+ let input_field =
271
+ H. input ~a: [H. a_input_type `Search ] ()
216
272
in
217
- H. div ~a: [H. a_id El.Dyn. exercise_list_id]
218
- (btns :: l)
273
+ Manip.Ev. oninput input_field (fun _ev ->
274
+ set_exercise_filter (Some (Manip. value input_field));
275
+ true );
276
+ H. div ~a: [H. a_class [" filter-box" ]] [input_field];
277
+ ]
278
+ in
279
+ let exercise_list_html =
280
+ H. div ~a: [H. a_id El.Dyn. exercise_list_id] btns
281
+ in
282
+ let pane_div =
283
+ H. div ~a: [H. a_id El.Dyn. exercise_pane]
284
+ [H. div ~a: [H. a_id El.Dyn. exercise_bar] btns; exercise_list_html]
219
285
in
220
286
Manip. removeChildren El. content;
221
- Manip. appendChild El. content list_div;
222
- Lwt. return list_div
287
+ Manip. appendChild El. content pane_div;
288
+ let list_update_signal =
289
+ React.S. map (fun l -> Manip. replaceChildren exercise_list_html [l])
290
+ exercise_list_signal
291
+ in
292
+ retain_signals :=
293
+ React.S. merge (fun () () -> () ) () (list_update_signal :: btns_sigs);
294
+ Lwt. return pane_div
223
295
224
296
let playground_tab token : tab_handler =
225
297
fun _ _ () ->
0 commit comments