From b90171e1f1862885a1efdfb915869eb5fad13b01 Mon Sep 17 00:00:00 2001 From: Akshay Date: Sun, 22 Nov 2020 15:50:07 +0530 Subject: dump basic typing test PWA --- elm.json | 24 +++++ shell.nix | 11 ++ src/Main.elm | 336 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 371 insertions(+) create mode 100644 elm.json create mode 100644 shell.nix create mode 100644 src/Main.elm diff --git a/elm.json b/elm.json new file mode 100644 index 0000000..4c4956d --- /dev/null +++ b/elm.json @@ -0,0 +1,24 @@ +{ + "type": "application", + "source-directories": [ + "src" + ], + "elm-version": "0.19.1", + "dependencies": { + "direct": { + "elm/browser": "1.0.2", + "elm/core": "1.0.5", + "elm/html": "1.0.0", + "elm/time": "1.0.0" + }, + "indirect": { + "elm/json": "1.1.3", + "elm/url": "1.0.0", + "elm/virtual-dom": "1.0.2" + } + }, + "test-dependencies": { + "direct": {}, + "indirect": {} + } +} diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..b5f20b4 --- /dev/null +++ b/shell.nix @@ -0,0 +1,11 @@ +{ pkgs ? import {} }: + +pkgs.mkShell { + buildInputs = [ + pkgs.elmPackages.elm + pkgs.elmPackages.elm-language-server + pkgs.elmPackages.elm-format + pkgs.nodePackages.elm-oracle + pkgs.elmPackages.elm-test + ]; +} diff --git a/src/Main.elm b/src/Main.elm new file mode 100644 index 0000000..9a35631 --- /dev/null +++ b/src/Main.elm @@ -0,0 +1,336 @@ +module Main exposing (..) + +import Array exposing (..) +import Browser +import Html exposing (..) +import Html.Attributes exposing (..) +import Html.Events exposing (onInput) +import Task +import Time + + +main = + Browser.element + { init = init + , view = view + , update = update + , subscriptions = subscriptions + } + + +type WordStatus + = Correct + | Wrong + | Todo + | CurrentWord + + +type alias Word = + { content : String + , status : WordStatus + } + + +type alias Model = + { begin : Maybe Time.Posix + , end : Maybe Time.Posix + , words : Array Word + , accuracy : Maybe Float + , length : Int + , currentWord : Int + , inputBox : String + } + + +type Msg + = Started Time.Posix + | Finished Time.Posix + | CorrectInput + | CorrectSoFar + | NextWord + | WrongInput + | InputChanged String + | Redo + + +subscriptions : Model -> Sub Msg +subscriptions model = + Sub.none + + +generateWords : Array Word +generateWords = + "this is some sample text for the demoz" + |> String.split " " + |> Array.fromList + |> Array.map (\w -> Word w Todo) + + +init : () -> ( Model, Cmd Msg ) +init _ = + let + words = + generateWords + in + ( Model Nothing Nothing words Nothing (Array.length words) 0 "" + , Cmd.none + ) + + +setStartTime : Cmd Msg +setStartTime = + Task.perform Started Time.now + + +setEndTime : Cmd Msg +setEndTime = + Task.perform Finished Time.now + + +firstWord : Model -> Bool +firstWord model = + String.length model.inputBox == 1 && model.currentWord == 0 + + +lastWord : Model -> Bool +lastWord model = + model.currentWord == model.length - 1 + + +wordEnded : String -> Bool +wordEnded s = + String.endsWith " " s + + +handleWordEnded : Model -> ( Model, Cmd Msg ) +handleWordEnded model = + let + s = + model.inputBox + + current = + Array.get model.currentWord model.words |> Maybe.withDefault (Word "" Todo) + + currentContent = + current.content + + prevWordStatus = + if s == currentContent then + Correct + + else + Wrong + + newWords = + Array.set model.currentWord { current | status = prevWordStatus } model.words + + newModel = + { model | inputBox = "", words = newWords, currentWord = model.currentWord + 1 } + + cmd = + if lastWord model then + setEndTime + + else + Cmd.none + in + ( newModel, cmd ) + + +isNothing : Maybe a -> Bool +isNothing p = + case p of + Nothing -> + True + + Just a -> + False + + +isJust : Maybe a -> Bool +isJust p = + not (isNothing p) + + +flip : (a -> b -> c) -> (b -> a -> c) +flip f = + \x y -> f y x + + +update : Msg -> Model -> ( Model, Cmd Msg ) +update msg model = + case msg of + Started t -> + ( { model | begin = Just t } + , Cmd.none + ) + + Finished t -> + ( { model | end = Just t } + , Cmd.none + ) + + InputChanged s -> + let + cmd = + if firstWord model && isNothing model.begin then + setStartTime + + else if s == currentContents model && lastWord model then + setEndTime + + else + Cmd.none + in + ( { model | inputBox = s } + , cmd + ) + + NextWord -> + if isJust model.end then + ( model, Cmd.none ) + + else + handleWordEnded model + + _ -> + ( model + , Cmd.none + ) + + +displayTime : Time.Posix -> String +displayTime t = + let + hh = + Time.toHour Time.utc t + + mm = + Time.toMinute Time.utc t + + ss = + Time.toSecond Time.utc t + in + String.join ":" (List.map String.fromInt [ hh, mm, ss ]) + + +wordStyle : WordStatus -> Attribute msg +wordStyle w = + case w of + Correct -> + style "color" "green" + + Wrong -> + style "color" "red" + + Todo -> + style "color" "black" + + CurrentWord -> + style "color" "magenta" + + +toPara : Array Word -> Int -> String -> Html Msg +toPara words current content = + words + |> Array.map (\w -> span [ wordStyle w.status ] [ text (w.content ++ " ") ]) + |> Array.set current (span [ wordStyle CurrentWord ] [ text (content ++ " ") ]) + |> Array.toList + |> p [] + + +currentContents : Model -> String +currentContents model = + Array.get model.currentWord model.words + |> Maybe.map .content + |> Maybe.withDefault "" + + +diffDuration : Time.Posix -> Time.Posix -> Float +diffDuration t1 t2 = + let + m1 = + Time.posixToMillis t1 + + m2 = + Time.posixToMillis t2 + in + toFloat (m2 - m1) / 1000 + + +viewWpm : Model -> String +viewWpm model = + let + t1 = + model.begin + + t2 = + model.end + + duration = + Maybe.map2 diffDuration t1 t2 + + correctWords = + wordCount model.words ((==) Correct) + + wpm = + Maybe.map (String.fromInt << truncate << (*) 60 << (/) (toFloat correctWords)) duration + in + Maybe.withDefault "XX" wpm + + +wordCount : Array Word -> (WordStatus -> Bool) -> Int +wordCount words predicate = + words |> Array.map .status |> Array.filter predicate |> Array.length + + +viewProgress : Model -> String +viewProgress model = + String.fromInt model.currentWord ++ "/" ++ String.fromInt model.length + + +viewAccuracy : Model -> String +viewAccuracy model = + let + wordsAttempted = + toFloat <| wordCount model.words ((/=) Todo) + + correctCount = + toFloat <| wordCount model.words ((==) Correct) + + accuracy = + if wordsAttempted == 0.0 then + Nothing + + else + Just <| correctCount / wordsAttempted * 100 + in + case accuracy of + Nothing -> + "XX" + + Just a -> + String.fromInt <| truncate a + + +view : Model -> Html Msg +view model = + div [] + [ p [] [ text (String.fromInt model.currentWord) ] + , toPara model.words model.currentWord (currentContents model) + + -- , p [] [ text (Maybe.withDefault "XX" (Maybe.map displayTime model.begin)) ] + -- , p [] [ text (Maybe.withDefault "XX" (Maybe.map displayTime model.end)) ] + , p [] [ text ("WPM: " ++ viewWpm model) ] + , p [] [ text ("ACC: " ++ viewAccuracy model) ] + , input [ onInput handleInputChanged, value model.inputBox ] [] + ] + + +handleInputChanged : String -> Msg +handleInputChanged s = + if wordEnded s then + NextWord + + else + InputChanged s -- cgit v1.2.3