diff --git a/src/app/learnocaml_index_main.ml b/src/app/learnocaml_index_main.ml index d2011b9b9..883e65574 100644 --- a/src/app/learnocaml_index_main.ml +++ b/src/app/learnocaml_index_main.ml @@ -56,12 +56,20 @@ module El = struct module Dyn = struct (** Elements that are dynamically created (ids only) *) let exercise_list_id = "learnocaml-main-exercise-list" + let exercise_bar = "learnocaml-main-exercise-bar" + let exercise_pane = "learnocaml-main-exercise-pane" + let filter_box = "learnocaml-filter-box" let tutorial_id = "learnocaml-main-tutorial" let lesson_id = "learnocaml-main-lesson" let toplevel_id = "learnocaml-main-toplevel" end end +type tab_handler = + (?clear_cache:bool -> unit -> unit Lwt.t) -> + (string -> string) * (string -> string -> unit) * (string -> unit) -> + unit -> Html_types.div H.elt t + let show_loading msg = show_loading ~id:El.loading_id H.[ul [li [txt msg]]] let get_url token dynamic_url static_url id = @@ -69,95 +77,244 @@ let get_url token dynamic_url static_url id = | Some _ -> dynamic_url ^ Url.urlencode id ^ "/" | None -> api_server ^ "/" ^ static_url ^ Url.urlencode id -let exercises_tab token _ _ () = - show_loading [%i"Loading exercises"] @@ fun () -> - Lwt_js.sleep 0.5 >>= fun () -> - retrieve (Learnocaml_api.Exercise_index token) - >>= fun (index, deadlines) -> - let format_exercise_list all_exercise_states = - let rec format_contents lvl acc contents = - let open Tyxml_js.Html5 in - match contents with - | Exercise.Index.Exercises exercises -> - List.fold_left - (fun acc (exercise_id, meta_opt) -> - match meta_opt with None -> acc | Some meta -> - let {Exercise.Meta.kind; title; short_description; stars; _ } = - meta +type exercise_ordering = By_category | By_skill | By_difficulty + +let (exercise_filter_signal: string option React.signal), set_exercise_filter = + React.S.create None + +let (exercise_sort_signal: exercise_ordering React.signal), set_exercise_sort = + React.S.create By_category + +let make_exercises_to_display_signal index = + let get_index exo_sort exo_filter = + let index = + match exo_sort with + | By_category -> index + | By_skill -> + let module StrMap = Map.Make(struct + type t = string + (* Case-insensitive ordering *) + let compare a b = + String.(compare (lowercase_ascii a) (lowercase_ascii b)) + end) + in + let by_skill = + Exercise.Index.fold_exercises (fun map id meta -> + List.fold_left (fun acc skill -> + StrMap.update skill + (function None -> Some [id, Some meta] + | Some l -> Some ((id, Some meta) :: l)) + acc) + map + meta.Exercise.Meta.focus) + StrMap.empty index + in + let groups = + StrMap.fold (fun skill exercises acc -> + (skill, + {Exercise.Index.title = skill; + contents = Exercise.Index.Exercises (List.rev exercises)}) + :: acc) + by_skill [] + in + Exercise.Index.Groups (List.rev groups) + | By_difficulty -> + let module IntMap = Map.Make(Int) in + let starmap = + Exercise.Index.fold_exercises (fun map id meta -> + IntMap.update + (int_of_float (Float.ceil meta.Exercise.Meta.stars)) + (function None -> Some [id, Some meta] + | Some l -> Some ((id, Some meta) :: l)) + map) + IntMap.empty index + in + let groups = + IntMap.fold (fun stars exercises acc -> + let title = + if stars = 1 then [%i"1 star"] + else Printf.sprintf [%if"%d stars"] stars + in + (title, + {Exercise.Index.title; + contents = Exercise.Index.Exercises (List.rev exercises)}) + :: acc) + starmap [] + in + Exercise.Index.Groups (List.rev groups) + in + let index = + match exo_filter with + | None -> index + | Some filt_str -> + Exercise.Index.filter (fun _ meta -> + let re = Re.(compile (no_case (str filt_str))) in + List.exists (Re.execp re) + (meta.Exercise.Meta.title :: + Option.to_list meta.Exercise.Meta.short_description @ + List.map fst meta.Exercise.Meta.author @ + List.map snd meta.Exercise.Meta.author @ + meta.Exercise.Meta.focus @ + meta.Exercise.Meta.requirements)) + index + in + if index = Exercise.Index.Exercises [] then + Exercise.Index.Groups + ["empty_group", + { Exercise.Index.title = [%i"No exercises found"]; + Exercise.Index.contents = Exercise.Index.Exercises []; }] + else index + in + React.S.l2 get_index exercise_sort_signal exercise_filter_signal + +let retain_signals = ref (React.S.const ()) +(* Used to register signals as GC roots *) + +let exercises_tab token : tab_handler = + fun _ _ () -> + let open Tyxml_js.Html5 in + show_loading [%i"Loading exercises"] @@ fun () -> + Lwt_js.sleep 0.5 >>= fun () -> + retrieve (Learnocaml_api.Exercise_index token) + >>= fun (index, deadlines) -> + let exercises_to_display_signal = + make_exercises_to_display_signal index + in + let all_exercise_states = + Learnocaml_local_storage.(retrieve all_exercise_states) + in + let format_exercise exercise_id {Exercise.Meta.kind; title; short_description; stars; _ } = + let pct_init = + match SMap.find exercise_id all_exercise_states with + | exception Not_found -> None + | { Answer.grade ; _ } -> grade in + let pct_signal, pct_signal_set = React.S.create pct_init in + Learnocaml_local_storage.(listener (exercise_state exercise_id)) := + Some (function + | Some { Answer.grade ; _ } -> pct_signal_set grade + | None -> pct_signal_set None) ; + let pct_text_signal = + React.S.map + (function + | None -> "--" + | Some 0 -> "0%" + | Some pct -> string_of_int pct ^ "%") + pct_signal in + let time_left = match List.assoc_opt exercise_id deadlines with + | None -> "" + | Some 0. -> [%i"Exercise closed"] + | Some f -> Printf.sprintf [%if"Time left: %s"] + (string_of_seconds (int_of_float f)) + in + let status_classes_signal = + React.S.map + (function + | None -> [ "stats" ] + | Some 0 -> [ "stats" ; "failure" ] + | Some pct when pct >= 100 -> [ "stats" ; "success" ] + | Some _ -> [ "stats" ; "partial" ]) + pct_signal in + a ~a:[ a_href (get_url token "/exercises/" "exercise.html#id=" exercise_id) ; + a_class [ "exercise" ] ] [ + div ~a:[ a_class [ "descr" ] ] ( + h1 [ txt title ] :: + begin match short_description with + | None -> [] + | Some text -> [ txt text ] + end + ); + div ~a:[ a_class [ "time-left" ] ] [H.txt time_left]; + div ~a:[ Tyxml_js.R.Html5.a_class status_classes_signal ] [ + stars_div stars; + div ~a:[ a_class [ "length" ] ] [ + match kind with + | Exercise.Meta.Project -> txt [%i"project"] + | Exercise.Meta.Problem -> txt [%i"problem"] + | Exercise.Meta.Exercise -> txt [%i"exercise"] ] ; + div ~a:[ a_class [ "score" ] ] [ + Tyxml_js.R.Html5.txt pct_text_signal + ] + ] ] + in + let rec format_exercise_list index = + match index with + | Exercise.Index.Exercises el -> + H.ul @@ + List.map + (fun (id, meta) -> H.li [format_exercise id (Option.get meta)]) + el + | Exercise.Index.Groups gl -> + H.ul @@ + List.map (fun (id, grp) -> + let clas = + "group-title" :: + match gl with [] | [_] -> [] | _ -> ["collapsed"] in - let pct_init = - match SMap.find exercise_id all_exercise_states with - | exception Not_found -> None - | { Answer.grade ; _ } -> grade in - let pct_signal, pct_signal_set = React.S.create pct_init in - Learnocaml_local_storage.(listener (exercise_state exercise_id)) := - Some (function - | Some { Answer.grade ; _ } -> pct_signal_set grade - | None -> pct_signal_set None) ; - let pct_text_signal = - React.S.map - (function - | None -> "--" - | Some 0 -> "0%" - | Some pct -> string_of_int pct ^ "%") - pct_signal in - let time_left = match List.assoc_opt exercise_id deadlines with - | None -> "" - | Some 0. -> [%i"Exercise closed"] - | Some f -> Printf.sprintf [%if"Time left: %s"] - (string_of_seconds (int_of_float f)) + let title = + H.div ~a:[a_id id; a_class clas] + [H.txt grp.Exercise.Index.title]; in - let status_classes_signal = - React.S.map - (function - | None -> [ "stats" ] - | Some 0 -> [ "stats" ; "failure" ] - | Some pct when pct >= 100 -> [ "stats" ; "success" ] - | Some _ -> [ "stats" ; "partial" ]) - pct_signal in - a ~a:[ a_href (get_url token "/exercises/" "exercise.html#id=" exercise_id) ; - a_class [ "exercise" ] ] [ - div ~a:[ a_class [ "descr" ] ] ( - h1 [ txt title ] :: - begin match short_description with - | None -> [] - | Some text -> [ txt text ] - end - ); - div ~a:[ a_class [ "time-left" ] ] [H.txt time_left]; - div ~a:[ Tyxml_js.R.Html5.a_class status_classes_signal ] [ - stars_div stars; - div ~a:[ a_class [ "length" ] ] [ - match kind with - | Exercise.Meta.Project -> txt [%i"project"] - | Exercise.Meta.Problem -> txt [%i"problem"] - | Exercise.Meta.Exercise -> txt [%i"exercise"] ] ; - div ~a:[ a_class [ "score" ] ] [ - Tyxml_js.R.Html5.txt pct_text_signal - ] - ] ] :: - acc) - acc exercises - | Exercise.Index.Groups groups -> - let h = match lvl with 1 -> h1 | 2 -> h2 | _ -> h3 in - List.fold_left - (fun acc (_, Exercise.Index.{ title ; contents }) -> - format_contents (succ lvl) - (h ~a:[ a_class [ "pack" ] ] [ txt title ] :: acc) - contents) - acc groups in - List.rev (format_contents 1 [] index) in - let list_div = - match format_exercise_list - Learnocaml_local_storage.(retrieve all_exercise_states) - with - | [] -> H.div [H.txt [%i"No open exercises at the moment"]] - | l -> H.div ~a:[H.a_id El.Dyn.exercise_list_id] l - in - Manip.appendChild El.content list_div; - Lwt.return list_div + let exos = format_exercise_list grp.Exercise.Index.contents in + Manip.Ev.onclick title + (fun _ -> + ignore (Manip.toggleClass title "collapsed"); + false); + H.li [title; exos]) + gl + in + let exercise_list_signal = + React.S.l1 format_exercise_list exercises_to_display_signal + in + let btns_sigs = + List.map (fun (id, sort, name) -> + let btn = button ~a:[a_id id] [ txt name ] in + Manip.Ev.onclick btn + (fun _ -> set_exercise_sort sort; true); + let signal = + React.S.map (fun s -> + (if sort = s then Manip.addClass else Manip.removeClass) + btn "active" + ) exercise_sort_signal + in + btn, signal) + [ + "by_category", By_category, [%i"By category"]; + "by_skill", By_skill, [%i"By skill"]; + "by_difficulty", By_difficulty, [%i"By difficulty"]; + ] + in + let btns, btns_sigs = List.split btns_sigs in + let btns = + btns @ + [ + let input_field = + H.input ~a:[H.a_input_type `Search] () + in + Manip.Ev.oninput input_field (fun _ev -> + set_exercise_filter (Some (Manip.value input_field)); + true); + H.div ~a:[H.a_class ["filter-box"]] [input_field]; + ] + in + let exercise_list_html = + H.div ~a:[H.a_id El.Dyn.exercise_list_id] btns + in + let pane_div = + H.div ~a:[H.a_id El.Dyn.exercise_pane] + [H.div ~a:[H.a_id El.Dyn.exercise_bar] btns; exercise_list_html] + in + Manip.removeChildren El.content; + Manip.appendChild El.content pane_div; + let list_update_signal = + React.S.map (fun l -> Manip.replaceChildren exercise_list_html [l]) + exercise_list_signal + in + retain_signals := + React.S.merge (fun () () -> ()) () (list_update_signal :: btns_sigs); + Lwt.return pane_div -let playground_tab token _ _ () = +let playground_tab token : tab_handler = + fun _ _ () -> show_loading [%i"Loading playground"] @@ fun () -> Lwt_js.sleep 0.5 >>= fun () -> retrieve (Learnocaml_api.Playground_index ()) @@ -183,7 +340,8 @@ let playground_tab token _ _ () = Manip.appendChild El.content list_div; Lwt.return list_div -let lessons_tab select (arg, set_arg, _delete_arg) () = +let lessons_tab : tab_handler = + fun select (arg, set_arg, _delete_arg) () -> show_loading [%i"Loading lessons"] @@ fun () -> Lwt_js.sleep 0.5 >>= fun () -> retrieve (Learnocaml_api.Lesson_index ()) >>= fun index -> @@ -301,7 +459,8 @@ let lessons_tab select (arg, set_arg, _delete_arg) () = end >>= fun () -> Lwt.return lesson_div -let tutorial_tab select (arg, set_arg, _delete_arg) () = +let tutorial_tab : tab_handler = + fun select (arg, set_arg, _delete_arg) () -> let open Tutorial in let navigation_div = Tyxml_js.Html5.(div ~a: [ a_class [ "navigation" ] ] []) in @@ -489,7 +648,8 @@ let tutorial_tab select (arg, set_arg, _delete_arg) () = init_toplevel_pane toplevel_launch top toplevel_buttons_group toplevel_button ; Lwt.return tutorial_div -let toplevel_tab select _ () = +let toplevel_tab : tab_handler = + fun select _ () -> let container = Tyxml_js.Html5.(div ~a: [ a_class [ "toplevel-pane" ] ]) [] in let buttons_div = @@ -509,7 +669,8 @@ let toplevel_tab select _ () = init_toplevel_pane (Lwt.return top) top toplevel_buttons_group button ; Lwt.return div -let teacher_tab token a b () = +let teacher_tab token : tab_handler = + fun a b () -> show_loading [%i"Loading student info"] @@ fun () -> Learnocaml_teacher_tab.teacher_tab token a b () >>= fun div -> Lwt.return div @@ -680,7 +841,7 @@ let () = in let init_tabs token = let get_opt o = Js.Optdef.get o (fun () -> false) in - let tabs = + let tabs : (string * (string * tab_handler)) list = (if get_opt config##.enableTutorials then [ "tutorials", ([%i"Tutorials"], tutorial_tab) ] else []) @ (if get_opt config##.enableLessons @@ -707,7 +868,7 @@ let () = let btn = Tyxml_js.Html5.(button [ txt name]) in let div = ref None in let args = ref [] in - let rec select () = + let rec select ?(clear_cache=false) () = let th () = Lwt.pause () >>= fun () -> begin match !current_btn with @@ -715,13 +876,15 @@ let () = | Some btn -> Manip.removeClass btn "active" end ; Manip.removeChildren El.content ; - List.iter (fun (n, _) -> delete_arg n) !(!current_args) ; + List.iter (fun (n, _) -> + if n <> "display" then delete_arg n) + !(!current_args); begin match !div with - | Some div -> + | Some div when not clear_cache -> List.iter (fun (n, v) -> set_arg n v) !args ; Manip.appendChild El.content div ; Lwt.return_unit - | None -> + | _ -> let arg name = arg name in let set_arg name value = diff --git a/src/state/learnocaml_data.ml b/src/state/learnocaml_data.ml index 09f6b1b7b..9b94d7fda 100644 --- a/src/state/learnocaml_data.ml +++ b/src/state/learnocaml_data.ml @@ -1018,7 +1018,7 @@ module Exercise = struct List.fold_left (fun exs skill -> List.fold_left (fun exs id -> (ex_node exercises id, [Skill skill]) :: exs) - exs (SMap.find skill focus) + exs (try SMap.find skill focus with Not_found -> []) ) exs ex_meta.Meta.requirements in let exs = merge_children exs in @@ -1077,6 +1077,27 @@ module Exercise = struct in compute [] graph + let fold f acc graph = + let visited_nodes = Hashtbl.create 17 in + let rec fold_nodes acc graph = + let rec fold_children acc node = + if Hashtbl.mem visited_nodes node.name + then acc + else let acc = + List.fold_left + fold_children acc + (List.map fst node.children) + in + Hashtbl.add visited_nodes node.name (); + f acc node + in + match graph with + | [] -> acc + | node :: nodes -> + fold_nodes (fold_children acc node) nodes + in + fold_nodes acc graph + let dump_dot fmt nodes = let print_kind fmt = function | Skill s -> Format.fprintf fmt "(S %s)" s diff --git a/src/state/learnocaml_data.mli b/src/state/learnocaml_data.mli index df4224a27..b80bb7ec5 100644 --- a/src/state/learnocaml_data.mli +++ b/src/state/learnocaml_data.mli @@ -345,6 +345,9 @@ module Exercise: sig exercise. *) val compute_exercise_set : node -> string list + (** Fold function that handles every exercise's dependency before handling exercise itself *) + val fold : ('a -> node -> 'a) -> 'a -> node list -> 'a + (** Dumps the graph as a `dot` representation, into the given formatter. *) val dump_dot : Format.formatter -> node list -> unit diff --git a/static/css/learnocaml_main.css b/static/css/learnocaml_main.css index 27539cbb3..a18ee7105 100644 --- a/static/css/learnocaml_main.css +++ b/static/css/learnocaml_main.css @@ -418,16 +418,58 @@ body { /* -- Exercises activity --------------------------------------------------- */ -#learnocaml-main-exercise-list > .exercise + .exercise { +#learnocaml-main-exercise-bar { + display: flex; + justify-content: space-between; + width: 100%; +} + +#learnocaml-main-exercise-bar button { + flex-grow: 1; + background: linear-gradient(to bottom, #9bd 0%, #5581ff 100%); + color: #fff; + text-shadow: 0 0 10px #000; +} + +#learnocaml-main-exercise-bar button.active { + background: linear-gradient(to bottom, #f29100 0%, #ec670f 100%); +} + +#learnocaml-main-exercise-list .collapsed + ul { + display: none; +} + +.filter-box input { + padding-left: 22px; + padding-right: 22px; +} +.filter-box::before { + position: absolute; + margin: 3px; + content: "🔍"; + width: 16px; +} + +#learnocaml-main-exercise-list ul { + list-style-type: none; + margin: 0; + padding: 0; +} + +#learnocaml-main-exercise-list ul ul .group-title { + padding-left: 2em; +} + +#learnocaml-main-exercise-list .exercise + .exercise { border-top: 1px #333 solid; } -#learnocaml-main-exercise-list > .exercise { +#learnocaml-main-exercise-list .exercise { position: relative; background: linear-gradient(to top, #aaa 0px, #eee 10px, #ddd 100%); color: black; text-decoration: none; } -#learnocaml-main-exercise-list > .exercise:hover::after { +#learnocaml-main-exercise-list .exercise:hover::after { position: absolute; background: #acf; opacity: 0.3; @@ -435,20 +477,20 @@ body { left:0; right:0; bottom:0; top:0; content:""; } -#learnocaml-main-exercise-list > .exercise > .descr { +#learnocaml-main-exercise-list .exercise > .descr { padding: 10px; flex: 1 1 auto; } -#learnocaml-main-exercise-list > .exercise > .descr > h1 { +#learnocaml-main-exercise-list .exercise > .descr > h1 { margin: 0 0 10px 0; padding: 0; font-size: 18px; font-weight: bold; } -#learnocaml-main-exercise-list > .exercise > .descr > p { +#learnocaml-main-exercise-list .exercise > .descr > p { margin: 10px 0 0 0; padding: 0; text-align: justify; } -#learnocaml-main-exercise-list > .pack { +#learnocaml-main-exercise-list .group-title { padding: 10px; margin: 0; background: #222; @@ -456,7 +498,15 @@ body { font-weight: bold; font-size: 20px; line-height: 26px; + cursor: pointer; } +#learnocaml-main-exercise-list .group-title.collapsed::before { + content: "▶ "; +} +#learnocaml-main-exercise-list .group-title:not(.collapsed)::before { + content: "â–Œ "; +} + @media (min-width: 1000px) { #learnocaml-main-exercise-list { position: relative; @@ -470,13 +520,13 @@ body { } } @media (min-width: 550px) { - #learnocaml-main-exercise-list > .exercise { + #learnocaml-main-exercise-list .exercise { display: flex; flex-direction: row; position: relative; z-index: 999; } - #learnocaml-main-exercise-list > .exercise > .stats { + #learnocaml-main-exercise-list .exercise > .stats { flex: 0; display: flex; flex-direction: column; @@ -488,65 +538,65 @@ body { linear-gradient(to bottom, #bbb 0px, #ccc 36px, rgba(0,0,0,0) 36px), linear-gradient(to top, #999 0px, #ccc 10px); } - #learnocaml-main-exercise-list > .exercise > .stats > .stars { + #learnocaml-main-exercise-list .exercise > .stats > .stars { min-height: 20px; flex: 0 1 auto; } - #learnocaml-main-exercise-list > .exercise > .stats > .length { + #learnocaml-main-exercise-list .exercise > .stats > .length { flex: 1 1 auto; } - #learnocaml-main-exercise-list > .exercise > .stats > .score { + #learnocaml-main-exercise-list .exercise > .stats > .score { font-size: 20px; color: #666; flex: 0 1 auto; } - #learnocaml-main-exercise-list > .exercise > .stats.success { + #learnocaml-main-exercise-list .exercise > .stats.success { background: linear-gradient(to bottom, #7b6 0px, #8c7 36px, rgba(0,0,0,0) 36px), linear-gradient(to top, #483 0px, #8c7 10px); } - #learnocaml-main-exercise-list > .exercise > .stats.success > .score { + #learnocaml-main-exercise-list .exercise > .stats.success > .score { color: #080; } - #learnocaml-main-exercise-list > .exercise > .stats.failure { + #learnocaml-main-exercise-list .exercise > .stats.failure { background: linear-gradient(to bottom, #f44 0px, #f55 36px, rgba(0,0,0,0) 36px), linear-gradient(to top, #844 0px, #f55 10px); } - #learnocaml-main-exercise-list > .exercise > .stats.failure > .score { + #learnocaml-main-exercise-list .exercise > .stats.failure > .score { color: #800; } - #learnocaml-main-exercise-list > .exercise > .stats.partial { + #learnocaml-main-exercise-list .exercise > .stats.partial { background: linear-gradient(to bottom, #fc4 0px, #fd5 36px, rgba(0,0,0,0) 36px), linear-gradient(to top, #874 0px, #fd5 10px); } - #learnocaml-main-exercise-list > .exercise > .stats.partial > .score { + #learnocaml-main-exercise-list .exercise > .stats.partial > .score { color: #C80; } } @media (max-width: 549px) { - #learnocaml-main-exercise-list > .exercise { + #learnocaml-main-exercise-list .exercise { display: block; position: relative; z-index: 999; } - #learnocaml-main-exercise-list > .exercise > .stats > *, - #learnocaml-main-exercise-list > .exercise > .descr > * { + #learnocaml-main-exercise-list .exercise > .stats > *, + #learnocaml-main-exercise-list .exercise > .descr > * { position: relative; z-index: 1000; } - #learnocaml-main-exercise-list > .exercise > .descr > p { + #learnocaml-main-exercise-list .exercise > .descr > p { margin: 0 0 30px 0; } - #learnocaml-main-exercise-list > .exercise > .stats > .stars { + #learnocaml-main-exercise-list .exercise > .stats > .stars { position: absolute; line-height: 20px; height: 30px; bottom: 0; left: 10px; vertical-align: bottom; } - #learnocaml-main-exercise-list > .exercise > .stats > .length { + #learnocaml-main-exercise-list .exercise > .stats > .length { position: absolute; display: inline-block; line-height: 20px; @@ -554,7 +604,7 @@ body { bottom: 0; left: 90px; vertical-align: bottom; } - #learnocaml-main-exercise-list > .exercise > .stats > .score { + #learnocaml-main-exercise-list .exercise > .stats > .score { width: 70px; text-align: center; font-size: 20px; @@ -562,30 +612,30 @@ body { bottom: 15px; right: 10px; color: #666; } - #learnocaml-main-exercise-list > .exercise > .stats::after { + #learnocaml-main-exercise-list .exercise > .stats::after { background: linear-gradient(to top, #999 0px, #ccc 10px, rgba(0,0,0,0) 100%); bottom: 0; right: 0; top: 0; left: 0; content:""; position: absolute; z-index: 999; } - #learnocaml-main-exercise-list > .exercise > .stats.success::after { + #learnocaml-main-exercise-list .exercise > .stats.success::after { background: linear-gradient(to top, #483 0px, #8c7 10px, #8c7 100%); } - #learnocaml-main-exercise-list > .exercise > .stats.success > .score { + #learnocaml-main-exercise-list .exercise > .stats.success > .score { color: #080; } - #learnocaml-main-exercise-list > .exercise > .stats.failure::after { + #learnocaml-main-exercise-list .exercise > .stats.failure::after { background: linear-gradient(to top, #844 0px, #f55 10px); } - #learnocaml-main-exercise-list > .exercise > .stats.failure > .score { + #learnocaml-main-exercise-list .exercise > .stats.failure > .score { color: #800; } - #learnocaml-main-exercise-list > .exercise > .stats.partial::after { + #learnocaml-main-exercise-list .exercise > .stats.partial::after { background: linear-gradient(to top, #874 0px, #fd5 10px); color: #C80; } - #learnocaml-main-exercise-list > .exercise > .stats.partial > .score { + #learnocaml-main-exercise-list .exercise > .stats.partial > .score { color: #C80; } } diff --git a/translations/fr.po b/translations/fr.po index 6ce93eb5e..201aad75a 100644 --- a/translations/fr.po +++ b/translations/fr.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: learn-ocaml ~dev\n" -"PO-Revision-Date: 2023-08-28 09:10+0200\n" +"PO-Revision-Date: 2023-10-24 16:44+0200\n" "Last-Translator: Erik Martin-Dorel \n" "Language-Team: OCSF\n" "Language: french\n" @@ -86,22 +86,22 @@ msgid "ERROR" msgstr "ERREUR" #: File "src/app/learnocaml_common.ml", line 140, characters 58-66 160, 30-38 -#: 455, 12-20 953, 19-27 "src/app/learnocaml_index_main.ml", 583, 17-25 +#: 455, 12-20 953, 19-27 "src/app/learnocaml_index_main.ml", 737, 17-25 msgid "Cancel" msgstr "Annuler" #: File "src/app/learnocaml_common.ml", line 447, characters 26-41 948, 32-47 -#: "src/app/learnocaml_index_main.ml", 578, +#: "src/app/learnocaml_index_main.ml", 732, msgid "REQUEST ERROR" msgstr "ERREUR DE REQUÊTE" #: File "src/app/learnocaml_common.ml", line 448, characters 22-59 949, 30-67 -#: "src/app/learnocaml_index_main.ml", 579, 28-65 +#: "src/app/learnocaml_index_main.ml", 733, 28-65 msgid "Could not retrieve data from server" msgstr "Échec lors du tĂ©lĂ©chargement des donnĂ©es du serveur" #: File "src/app/learnocaml_common.ml", line 451, characters 12-19 495, 13-20 -#: 952, 19-26 "src/app/learnocaml_index_main.ml", 582, 17-24 +#: 952, 19-26 "src/app/learnocaml_index_main.ml", 736, 17-24 msgid "Retry" msgstr "RĂ©essayer" @@ -138,17 +138,17 @@ msgid "No description available for this exercise." msgstr "Aucune description pour cet exercice." #: File "src/app/learnocaml_common.ml", line 643, characters 32-41 -#: "src/app/learnocaml_index_main.ml", 132, 54-63 +#: "src/app/learnocaml_index_main.ml", 224, 46-55 msgid "project" msgstr "projet" #: File "src/app/learnocaml_common.ml", line 644, characters 32-41 -#: "src/app/learnocaml_index_main.ml", 133, 54-63 +#: "src/app/learnocaml_index_main.ml", 225, 46-55 msgid "problem" msgstr "problĂšme" #: File "src/app/learnocaml_common.ml", line 645, characters 33-43 -#: "src/app/learnocaml_index_main.ml", 134, 55-65 +#: "src/app/learnocaml_index_main.ml", 226, 47-57 msgid "exercise" msgstr "exercice" @@ -173,7 +173,7 @@ msgid "Editor" msgstr "Éditeur" #: File "src/app/learnocaml_common.ml", line 824, characters 41-51 -#: "src/app/learnocaml_index_main.ml", 692, 30-40 +#: "src/app/learnocaml_index_main.ml", 846, 30-40 msgid "Toplevel" msgstr "Toplevel" @@ -204,7 +204,7 @@ msgid "Stats" msgstr "Statistiques" #: File "src/app/learnocaml_common.ml", line 836, characters 37-48 -#: "src/app/learnocaml_index_main.ml", 689, 29-40 +#: "src/app/learnocaml_index_main.ml", 843, 29-40 #: "src/app/learnocaml_teacher_tab.ml", 375, 21-32 #: "src/app/learnocaml_exercise_main.ml", 204, 23-34 msgid "Exercises" @@ -232,12 +232,12 @@ msgid "This will discard all your edits. Are you sure?" msgstr "Toutes vos modifications seront perdues. Êtes-vous sĂ»r·e ?" #: File "src/app/learnocaml_common.ml", line 944, characters 28-45 1180, -#: "src/app/learnocaml_index_main.ml", 574, +#: "src/app/learnocaml_index_main.ml", 728, msgid "TOKEN NOT FOUND" msgstr "TOKEN NON TROUVÉ" #: File "src/app/learnocaml_common.ml", line 945, characters 17-60 1181, -#: "src/app/learnocaml_index_main.ml", 575, +#: "src/app/learnocaml_index_main.ml", 729, msgid "The entered token couldn't be recognised." msgstr "Le token entrĂ© n'a pas Ă©tĂ© reconnu." @@ -290,7 +290,7 @@ msgid "Show" msgstr "Montrer" #: File "src/app/learnocaml_common.ml", line 1171, characters 18-36 -#: "src/app/learnocaml_index_main.ml", 629, 31-49 +#: "src/app/learnocaml_index_main.ml", 783, 31-49 msgid "Enter your token" msgstr "Entrez votre token" @@ -412,174 +412,194 @@ msgstr "" msgid "The toplevel has been reset.\n" msgstr "Le toplevel a Ă©tĂ© redĂ©marrĂ©.\n" -#: File "src/app/learnocaml_index_main.ml", line 73, characters 18-37 +#: File "src/app/learnocaml_index_main.ml", line 128, characters 39-47 +msgid "1 star" +msgstr "1 Ă©toile" + +#: File "src/app/learnocaml_index_main.ml", line 129, characters 42-52 +msgid "%d stars" +msgstr "%d Ă©toiles" + +#: File "src/app/learnocaml_index_main.ml", line 157, characters 37-57 +msgid "No exercises found" +msgstr "Aucun exercice trouvĂ©" + +#: File "src/app/learnocaml_index_main.ml", line 169, characters 21-40 msgid "Loading exercises" msgstr "Chargement des exercices" -#: File "src/app/learnocaml_index_main.ml", line 106, characters 32-49 +#: File "src/app/learnocaml_index_main.ml", line 198, characters 24-41 msgid "Exercise closed" msgstr "Exercice fermĂ©" -#: File "src/app/learnocaml_index_main.ml", line 107, characters 47-62 +#: File "src/app/learnocaml_index_main.ml", line 199, characters 39-54 msgid "Time left: %s" msgstr "Temps restant: %s" -#: File "src/app/learnocaml_index_main.ml", line 154, characters 28-61 -msgid "No open exercises at the moment" -msgstr "Aucun exercice n'est encore ouvert" +#: File "src/app/learnocaml_index_main.ml", line 274, characters 41-54 +msgid "By category" +msgstr "Par catĂ©gorie" + +#: File "src/app/learnocaml_index_main.ml", line 275, characters 35-45 +msgid "By skill" +msgstr "Par compĂ©tence" + +#: File "src/app/learnocaml_index_main.ml", line 276, characters 45-60 +msgid "By difficulty" +msgstr "Par difficultĂ©" -#: File "src/app/learnocaml_index_main.ml", line 161, characters 18-38 +#: File "src/app/learnocaml_index_main.ml", line 311, characters 18-38 msgid "Loading playground" msgstr "Chargement du bac-Ă -sable" -#: File "src/app/learnocaml_index_main.ml", line 187, characters 18-35 +#: File "src/app/learnocaml_index_main.ml", line 338, characters 18-35 msgid "Loading lessons" msgstr "Chargement des cours" -#: File "src/app/learnocaml_index_main.ml", line 220, characters 37-61 +#: File "src/app/learnocaml_index_main.ml", line 371, characters 37-61 msgid "Running OCaml examples" msgstr "Lancement des exemples d'OCaml" -#: File "src/app/learnocaml_index_main.ml", line 261, characters 39-45 450, +#: File "src/app/learnocaml_index_main.ml", line 412, characters 39-45 602, msgid "Prev" msgstr "Prec." -#: File "src/app/learnocaml_index_main.ml", line 277, characters 40-46 467, +#: File "src/app/learnocaml_index_main.ml", line 428, characters 40-46 619, msgid "Next" msgstr "Suiv." -#: File "src/app/learnocaml_index_main.ml", line 334, characters 18-37 +#: File "src/app/learnocaml_index_main.ml", line 486, characters 18-37 msgid "Loading tutorials" msgstr "Chargement des tutoriels" -#: File "src/app/learnocaml_index_main.ml", line 500, characters 18-35 +#: File "src/app/learnocaml_index_main.ml", line 653, characters 18-35 msgid "Launching OCaml" msgstr "DĂ©marrage d'OCaml" -#: File "src/app/learnocaml_index_main.ml", line 513, characters 18-40 +#: File "src/app/learnocaml_index_main.ml", line 667, characters 18-40 msgid "Loading student info" msgstr "Chargement des informations sur les Ă©tudiants" -#: File "src/app/learnocaml_index_main.ml", line 533, characters 22-46 +#: File "src/app/learnocaml_index_main.ml", line 687, characters 22-46 msgid "Your Learn-OCaml token" msgstr "Votre token Learn-OCaml" -#: File "src/app/learnocaml_index_main.ml", lines 534-535, characters 18-70 +#: File "src/app/learnocaml_index_main.ml", lines 688-689, characters 18-70 msgid "Your token is displayed below. It identifies you and allows to share your workspace between devices." msgstr "Votre token est affichĂ© ci-dessous. Il vous identifie et permet de partager un mĂȘme espace de travail entre plusieurs machines." -#: File "src/app/learnocaml_index_main.ml", line 536, characters 18-41 +#: File "src/app/learnocaml_index_main.ml", line 690, characters 18-41 msgid "Please write it down." msgstr "Notez-le !" -#: File "src/app/learnocaml_index_main.ml", line 617, characters 7-21 +#: File "src/app/learnocaml_index_main.ml", line 771, characters 7-21 msgid "Connected as" msgstr "ConnectĂ© en tant que" -#: File "src/app/learnocaml_index_main.ml", line 619, characters 7-19 +#: File "src/app/learnocaml_index_main.ml", line 773, characters 7-19 msgid "Activities" msgstr "ActivitĂ©s" -#: File "src/app/learnocaml_index_main.ml", line 621, characters 9-33 +#: File "src/app/learnocaml_index_main.ml", line 775, characters 9-33 msgid "Welcome to Learn OCaml" msgstr "Bienvenue sur Learn OCaml" -#: File "src/app/learnocaml_index_main.ml", line 622, characters 31-49 +#: File "src/app/learnocaml_index_main.ml", line 776, characters 31-49 msgid "First connection" msgstr "PremiĂšre connexion" -#: File "src/app/learnocaml_index_main.ml", line 623, characters 36-66 +#: File "src/app/learnocaml_index_main.ml", line 777, characters 36-66 msgid "New user? Create a new token" msgstr "Vous ĂȘtes nouveau ? Obtenez un nouveau token" -#: File "src/app/learnocaml_index_main.ml", line 624, characters 38-57 +#: File "src/app/learnocaml_index_main.ml", line 778, characters 38-57 msgid "Choose a nickname" msgstr "Choisissez un identifiant" -#: File "src/app/learnocaml_index_main.ml", line 625, characters 38-46 +#: File "src/app/learnocaml_index_main.ml", line 779, characters 38-46 msgid "Secret" msgstr "Secret" -#: File "src/app/learnocaml_index_main.ml", line 626, characters 24-42 +#: File "src/app/learnocaml_index_main.ml", line 780, characters 24-42 msgid "Create new token" msgstr "Nouveau token" -#: File "src/app/learnocaml_index_main.ml", line 627, characters 24-40 +#: File "src/app/learnocaml_index_main.ml", line 781, characters 24-40 msgid "Returning user" msgstr "Utilisateur existant" -#: File "src/app/learnocaml_index_main.ml", line 628, characters 29-64 +#: File "src/app/learnocaml_index_main.ml", line 782, characters 29-64 msgid "Already have a token? Click here!" msgstr "Vous avez dĂ©jĂ  un token ? Par ici !" -#: File "src/app/learnocaml_index_main.ml", line 630, characters 31-40 +#: File "src/app/learnocaml_index_main.ml", line 784, characters 31-40 msgid "Connect" msgstr "Se connecter" -#: File "src/app/learnocaml_index_main.ml", line 638, characters 9-19 640, +#: File "src/app/learnocaml_index_main.ml", line 792, characters 9-19 794, #: "src/app/learnocaml_teacher_tab.ml", 612, 22-32 msgid "Nickname" msgstr "Pseudonyme" -#: File "src/app/learnocaml_index_main.ml", line 647, characters 7-48 +#: File "src/app/learnocaml_index_main.ml", line 801, characters 7-48 msgid "Send feedback to Learn-OCaml developers" msgstr "Envoyer un commentaire aux dĂ©veloppeurs Learn-OCaml" -#: File "src/app/learnocaml_index_main.ml", line 676, characters 38-59 +#: File "src/app/learnocaml_index_main.ml", line 830, characters 38-59 msgid "Choose an activity." msgstr "SĂ©lectionnez une activitĂ©." -#: File "src/app/learnocaml_index_main.ml", line 685, characters 31-42 +#: File "src/app/learnocaml_index_main.ml", line 839, characters 31-42 msgid "Tutorials" msgstr "Tutoriels" -#: File "src/app/learnocaml_index_main.ml", line 687, characters 29-38 +#: File "src/app/learnocaml_index_main.ml", line 841, characters 29-38 msgid "Lessons" msgstr "Cours" -#: File "src/app/learnocaml_index_main.ml", line 694, characters 32-44 +#: File "src/app/learnocaml_index_main.ml", line 848, characters 32-44 #: "src/app/learnocaml_playground_main.ml", 77, 23-35 msgid "Playground" msgstr "Bac-Ă -sable" -#: File "src/app/learnocaml_index_main.ml", line 697, characters 28-35 +#: File "src/app/learnocaml_index_main.ml", line 851, characters 28-35 msgid "Teach" msgstr "Enseignement" -#: File "src/app/learnocaml_index_main.ml", line 795, characters 15-69 +#: File "src/app/learnocaml_index_main.ml", line 951, characters 15-69 msgid "Be sure to write down your token before logging out:" msgstr "Assurez-vous d'avoir notĂ© votre token :" -#: File "src/app/learnocaml_index_main.ml", lines 797-799, characters 15-26 +#: File "src/app/learnocaml_index_main.ml", lines 953-955, characters 15-26 msgid "WARNING: the data could not be synchronised with the server. Logging out will lose your local changes, be sure you exported a backup." msgstr "ATTENTION: l'espace de travail n'a pas pu ĂȘtre synchronisĂ© avec le serveur. En vous dĂ©connectant, vous perdrez tous les changements locaux, Ă  moins d'avoir exportĂ© votre espace de travail au prĂ©alable." -#: File "src/app/learnocaml_index_main.ml", line 801, characters 22-30 45-53 -#: 823, 9-17 +#: File "src/app/learnocaml_index_main.ml", line 957, characters 22-30 45-53 +#: 979, 9-17 msgid "Logout" msgstr "DĂ©connexion" -#: File "src/app/learnocaml_index_main.ml", line 814, characters 9-21 +#: File "src/app/learnocaml_index_main.ml", line 970, characters 9-21 msgid "Show token" msgstr "Afficher le token" -#: File "src/app/learnocaml_index_main.ml", line 817, characters 9-25 +#: File "src/app/learnocaml_index_main.ml", line 973, characters 9-25 msgid "Sync workspace" msgstr "Synchroniser" -#: File "src/app/learnocaml_index_main.ml", line 820, characters 9-25 +#: File "src/app/learnocaml_index_main.ml", line 976, characters 9-25 msgid "Export to file" msgstr "Exporter vers un fichier" -#: File "src/app/learnocaml_index_main.ml", line 821, characters 9-17 +#: File "src/app/learnocaml_index_main.ml", line 977, characters 9-17 msgid "Import" msgstr "Importer" -#: File "src/app/learnocaml_index_main.ml", line 822, characters 9-36 +#: File "src/app/learnocaml_index_main.ml", line 978, characters 9-36 msgid "Download all source files" msgstr "TĂ©lĂ©charger tous les fichiers sources" -#: File "src/app/learnocaml_index_main.ml", line 828, characters 38-44 +#: File "src/app/learnocaml_index_main.ml", line 984, characters 38-44 msgid "Menu" msgstr "Menu" @@ -1184,6 +1204,18 @@ msgstr "" msgid "Unexpected error:\n" msgstr "Erreur inattendue:\n" +#~ msgid "By prerequisites" +#~ msgstr "Par prĂ©requis" + +#~ msgid "No open exercises at the moment" +#~ msgstr "Aucun exercice n'est encore ouvert" + +#~ msgid "By legacy" +#~ msgstr "Par groupe" + +#~ msgid "By order" +#~ msgstr "Par ordre" + #~ msgid "Draft not available." #~ msgstr "Brouillon non disponible."