diff options
author | Akshay <[email protected]> | 2020-11-22 10:20:07 +0000 |
---|---|---|
committer | Akshay <[email protected]> | 2020-11-22 10:20:07 +0000 |
commit | b90171e1f1862885a1efdfb915869eb5fad13b01 (patch) | |
tree | 57cbef5b586c7dc11e20348a72ed1c2a4dac7636 | |
parent | 767b3310aa1589109ed211919f1d2e2984827757 (diff) |
dump basic typing test PWA
-rw-r--r-- | elm.json | 24 | ||||
-rw-r--r-- | shell.nix | 11 | ||||
-rw-r--r-- | src/Main.elm | 336 |
3 files changed, 371 insertions, 0 deletions
diff --git a/elm.json b/elm.json new file mode 100644 index 0000000..4c4956d --- /dev/null +++ b/elm.json | |||
@@ -0,0 +1,24 @@ | |||
1 | { | ||
2 | "type": "application", | ||
3 | "source-directories": [ | ||
4 | "src" | ||
5 | ], | ||
6 | "elm-version": "0.19.1", | ||
7 | "dependencies": { | ||
8 | "direct": { | ||
9 | "elm/browser": "1.0.2", | ||
10 | "elm/core": "1.0.5", | ||
11 | "elm/html": "1.0.0", | ||
12 | "elm/time": "1.0.0" | ||
13 | }, | ||
14 | "indirect": { | ||
15 | "elm/json": "1.1.3", | ||
16 | "elm/url": "1.0.0", | ||
17 | "elm/virtual-dom": "1.0.2" | ||
18 | } | ||
19 | }, | ||
20 | "test-dependencies": { | ||
21 | "direct": {}, | ||
22 | "indirect": {} | ||
23 | } | ||
24 | } | ||
diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..b5f20b4 --- /dev/null +++ b/shell.nix | |||
@@ -0,0 +1,11 @@ | |||
1 | { pkgs ? import <nixpkgs> {} }: | ||
2 | |||
3 | pkgs.mkShell { | ||
4 | buildInputs = [ | ||
5 | pkgs.elmPackages.elm | ||
6 | pkgs.elmPackages.elm-language-server | ||
7 | pkgs.elmPackages.elm-format | ||
8 | pkgs.nodePackages.elm-oracle | ||
9 | pkgs.elmPackages.elm-test | ||
10 | ]; | ||
11 | } | ||
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 @@ | |||
1 | module Main exposing (..) | ||
2 | |||
3 | import Array exposing (..) | ||
4 | import Browser | ||
5 | import Html exposing (..) | ||
6 | import Html.Attributes exposing (..) | ||
7 | import Html.Events exposing (onInput) | ||
8 | import Task | ||
9 | import Time | ||
10 | |||
11 | |||
12 | main = | ||
13 | Browser.element | ||
14 | { init = init | ||
15 | , view = view | ||
16 | , update = update | ||
17 | , subscriptions = subscriptions | ||
18 | } | ||
19 | |||
20 | |||
21 | type WordStatus | ||
22 | = Correct | ||
23 | | Wrong | ||
24 | | Todo | ||
25 | | CurrentWord | ||
26 | |||
27 | |||
28 | type alias Word = | ||
29 | { content : String | ||
30 | , status : WordStatus | ||
31 | } | ||
32 | |||
33 | |||
34 | type alias Model = | ||
35 | { begin : Maybe Time.Posix | ||
36 | , end : Maybe Time.Posix | ||
37 | , words : Array Word | ||
38 | , accuracy : Maybe Float | ||
39 | , length : Int | ||
40 | , currentWord : Int | ||
41 | , inputBox : String | ||
42 | } | ||
43 | |||
44 | |||
45 | type Msg | ||
46 | = Started Time.Posix | ||
47 | | Finished Time.Posix | ||
48 | | CorrectInput | ||
49 | | CorrectSoFar | ||
50 | | NextWord | ||
51 | | WrongInput | ||
52 | | InputChanged String | ||
53 | | Redo | ||
54 | |||
55 | |||
56 | subscriptions : Model -> Sub Msg | ||
57 | subscriptions model = | ||
58 | Sub.none | ||
59 | |||
60 | |||
61 | generateWords : Array Word | ||
62 | generateWords = | ||
63 | "this is some sample text for the demoz" | ||
64 | |> String.split " " | ||
65 | |> Array.fromList | ||
66 | |> Array.map (\w -> Word w Todo) | ||
67 | |||
68 | |||
69 | init : () -> ( Model, Cmd Msg ) | ||
70 | init _ = | ||
71 | let | ||
72 | words = | ||
73 | generateWords | ||
74 | in | ||
75 | ( Model Nothing Nothing words Nothing (Array.length words) 0 "" | ||
76 | , Cmd.none | ||
77 | ) | ||
78 | |||
79 | |||
80 | setStartTime : Cmd Msg | ||
81 | setStartTime = | ||
82 | Task.perform Started Time.now | ||
83 | |||
84 | |||
85 | setEndTime : Cmd Msg | ||
86 | setEndTime = | ||
87 | Task.perform Finished Time.now | ||
88 | |||
89 | |||
90 | firstWord : Model -> Bool | ||
91 | firstWord model = | ||
92 | String.length model.inputBox == 1 && model.currentWord == 0 | ||
93 | |||
94 | |||
95 | lastWord : Model -> Bool | ||
96 | lastWord model = | ||
97 | model.currentWord == model.length - 1 | ||
98 | |||
99 | |||
100 | wordEnded : String -> Bool | ||
101 | wordEnded s = | ||
102 | String.endsWith " " s | ||
103 | |||
104 | |||
105 | handleWordEnded : Model -> ( Model, Cmd Msg ) | ||
106 | handleWordEnded model = | ||
107 | let | ||
108 | s = | ||
109 | model.inputBox | ||
110 | |||
111 | current = | ||
112 | Array.get model.currentWord model.words |> Maybe.withDefault (Word "" Todo) | ||
113 | |||
114 | currentContent = | ||
115 | current.content | ||
116 | |||
117 | prevWordStatus = | ||
118 | if s == currentContent then | ||
119 | Correct | ||
120 | |||
121 | else | ||
122 | Wrong | ||
123 | |||
124 | newWords = | ||
125 | Array.set model.currentWord { current | status = prevWordStatus } model.words | ||
126 | |||
127 | newModel = | ||
128 | { model | inputBox = "", words = newWords, currentWord = model.currentWord + 1 } | ||
129 | |||
130 | cmd = | ||
131 | if lastWord model then | ||
132 | setEndTime | ||
133 | |||
134 | else | ||
135 | Cmd.none | ||
136 | in | ||
137 | ( newModel, cmd ) | ||
138 | |||
139 | |||
140 | isNothing : Maybe a -> Bool | ||
141 | isNothing p = | ||
142 | case p of | ||
143 | Nothing -> | ||
144 | True | ||
145 | |||
146 | Just a -> | ||
147 | False | ||
148 | |||
149 | |||
150 | isJust : Maybe a -> Bool | ||
151 | isJust p = | ||
152 | not (isNothing p) | ||
153 | |||
154 | |||
155 | flip : (a -> b -> c) -> (b -> a -> c) | ||
156 | flip f = | ||
157 | \x y -> f y x | ||
158 | |||
159 | |||
160 | update : Msg -> Model -> ( Model, Cmd Msg ) | ||
161 | update msg model = | ||
162 | case msg of | ||
163 | Started t -> | ||
164 | ( { model | begin = Just t } | ||
165 | , Cmd.none | ||
166 | ) | ||
167 | |||
168 | Finished t -> | ||
169 | ( { model | end = Just t } | ||
170 | , Cmd.none | ||
171 | ) | ||
172 | |||
173 | InputChanged s -> | ||
174 | let | ||
175 | cmd = | ||
176 | if firstWord model && isNothing model.begin then | ||
177 | setStartTime | ||
178 | |||
179 | else if s == currentContents model && lastWord model then | ||
180 | setEndTime | ||
181 | |||
182 | else | ||
183 | Cmd.none | ||
184 | in | ||
185 | ( { model | inputBox = s } | ||
186 | , cmd | ||
187 | ) | ||
188 | |||
189 | NextWord -> | ||
190 | if isJust model.end then | ||
191 | ( model, Cmd.none ) | ||
192 | |||
193 | else | ||
194 | handleWordEnded model | ||
195 | |||
196 | _ -> | ||
197 | ( model | ||
198 | , Cmd.none | ||
199 | ) | ||
200 | |||
201 | |||
202 | displayTime : Time.Posix -> String | ||
203 | displayTime t = | ||
204 | let | ||
205 | hh = | ||
206 | Time.toHour Time.utc t | ||
207 | |||
208 | mm = | ||
209 | Time.toMinute Time.utc t | ||
210 | |||
211 | ss = | ||
212 | Time.toSecond Time.utc t | ||
213 | in | ||
214 | String.join ":" (List.map String.fromInt [ hh, mm, ss ]) | ||
215 | |||
216 | |||
217 | wordStyle : WordStatus -> Attribute msg | ||
218 | wordStyle w = | ||
219 | case w of | ||
220 | Correct -> | ||
221 | style "color" "green" | ||
222 | |||
223 | Wrong -> | ||
224 | style "color" "red" | ||
225 | |||
226 | Todo -> | ||
227 | style "color" "black" | ||
228 | |||
229 | CurrentWord -> | ||
230 | style "color" "magenta" | ||
231 | |||
232 | |||
233 | toPara : Array Word -> Int -> String -> Html Msg | ||
234 | toPara words current content = | ||
235 | words | ||
236 | |> Array.map (\w -> span [ wordStyle w.status ] [ text (w.content ++ " ") ]) | ||
237 | |> Array.set current (span [ wordStyle CurrentWord ] [ text (content ++ " ") ]) | ||
238 | |> Array.toList | ||
239 | |> p [] | ||
240 | |||
241 | |||
242 | currentContents : Model -> String | ||
243 | currentContents model = | ||
244 | Array.get model.currentWord model.words | ||
245 | |> Maybe.map .content | ||
246 | |> Maybe.withDefault "" | ||
247 | |||
248 | |||
249 | diffDuration : Time.Posix -> Time.Posix -> Float | ||
250 | diffDuration t1 t2 = | ||
251 | let | ||
252 | m1 = | ||
253 | Time.posixToMillis t1 | ||
254 | |||
255 | m2 = | ||
256 | Time.posixToMillis t2 | ||
257 | in | ||
258 | toFloat (m2 - m1) / 1000 | ||
259 | |||
260 | |||
261 | viewWpm : Model -> String | ||
262 | viewWpm model = | ||
263 | let | ||
264 | t1 = | ||
265 | model.begin | ||
266 | |||
267 | t2 = | ||
268 | model.end | ||
269 | |||
270 | duration = | ||
271 | Maybe.map2 diffDuration t1 t2 | ||
272 | |||
273 | correctWords = | ||
274 | wordCount model.words ((==) Correct) | ||
275 | |||
276 | wpm = | ||
277 | Maybe.map (String.fromInt << truncate << (*) 60 << (/) (toFloat correctWords)) duration | ||
278 | in | ||
279 | Maybe.withDefault "XX" wpm | ||
280 | |||
281 | |||
282 | wordCount : Array Word -> (WordStatus -> Bool) -> Int | ||
283 | wordCount words predicate = | ||
284 | words |> Array.map .status |> Array.filter predicate |> Array.length | ||
285 | |||
286 | |||
287 | viewProgress : Model -> String | ||
288 | viewProgress model = | ||
289 | String.fromInt model.currentWord ++ "/" ++ String.fromInt model.length | ||
290 | |||
291 | |||
292 | viewAccuracy : Model -> String | ||
293 | viewAccuracy model = | ||
294 | let | ||
295 | wordsAttempted = | ||
296 | toFloat <| wordCount model.words ((/=) Todo) | ||
297 | |||
298 | correctCount = | ||
299 | toFloat <| wordCount model.words ((==) Correct) | ||
300 | |||
301 | accuracy = | ||
302 | if wordsAttempted == 0.0 then | ||
303 | Nothing | ||
304 | |||
305 | else | ||
306 | Just <| correctCount / wordsAttempted * 100 | ||
307 | in | ||
308 | case accuracy of | ||
309 | Nothing -> | ||
310 | "XX" | ||
311 | |||
312 | Just a -> | ||
313 | String.fromInt <| truncate a | ||
314 | |||
315 | |||
316 | view : Model -> Html Msg | ||
317 | view model = | ||
318 | div [] | ||
319 | [ p [] [ text (String.fromInt model.currentWord) ] | ||
320 | , toPara model.words model.currentWord (currentContents model) | ||
321 | |||
322 | -- , p [] [ text (Maybe.withDefault "XX" (Maybe.map displayTime model.begin)) ] | ||
323 | -- , p [] [ text (Maybe.withDefault "XX" (Maybe.map displayTime model.end)) ] | ||
324 | , p [] [ text ("WPM: " ++ viewWpm model) ] | ||
325 | , p [] [ text ("ACC: " ++ viewAccuracy model) ] | ||
326 | , input [ onInput handleInputChanged, value model.inputBox ] [] | ||
327 | ] | ||
328 | |||
329 | |||
330 | handleInputChanged : String -> Msg | ||
331 | handleInputChanged s = | ||
332 | if wordEnded s then | ||
333 | NextWord | ||
334 | |||
335 | else | ||
336 | InputChanged s | ||