git.iko.soy/typeable/blog-posts-ru
on main help sign in
47 files changed+1417
since splitsnapshots
unifiedsplit
Nix_Intro/fact.nix+7 Nix_Intro/mul.nix+3 Nix_Intro/simple-derivation/default.nix+11 Nix_Intro/simple-derivation/hello.c+7 elm-vs-reflex/elm-quiz-demo/.gitignore+6 elm-vs-reflex/elm-quiz-demo/README.md+9 elm-vs-reflex/elm-quiz-demo/elm.json+25 elm-vs-reflex/elm-quiz-demo/index.html+19 elm-vs-reflex/elm-quiz-demo/main.css+90 elm-vs-reflex/elm-quiz-demo/src/Main.elm+199 elm-vs-reflex/reflex-quiz-demo/.gitignore+15 elm-vs-reflex/reflex-quiz-demo/README.md+3 elm-vs-reflex/reflex-quiz-demo/backend/backend.cabal+29 elm-vs-reflex/reflex-quiz-demo/backend/frontendJs/frontend.jsexe+1 elm-vs-reflex/reflex-quiz-demo/backend/src-bin/main.hs+6 elm-vs-reflex/reflex-quiz-demo/backend/src/Backend.hs+10 elm-vs-reflex/reflex-quiz-demo/backend/static+1 elm-vs-reflex/reflex-quiz-demo/cabal.project+3 elm-vs-reflex/reflex-quiz-demo/common/common.cabal+15 elm-vs-reflex/reflex-quiz-demo/common/src/Common/Api.hs+4 elm-vs-reflex/reflex-quiz-demo/common/src/Common/Route.hs+47 elm-vs-reflex/reflex-quiz-demo/config/common/example+1 elm-vs-reflex/reflex-quiz-demo/config/common/route+1 elm-vs-reflex/reflex-quiz-demo/config/readme.md+9 elm-vs-reflex/reflex-quiz-demo/default.nix+23 elm-vs-reflex/reflex-quiz-demo/frontend/frontend.cabal+48 elm-vs-reflex/reflex-quiz-demo/frontend/src-bin/main.hs+10 elm-vs-reflex/reflex-quiz-demo/frontend/src/Frontend.hs+280 elm-vs-reflex/reflex-quiz-demo/static/main.css+90 reflex-todo/cabal.project+4 reflex-todo/default.nix+39 reflex-todo/todo-client/CHANGELOG.md+5 reflex-todo/todo-client/LICENSE reflex-todo/todo-client/Setup.hs+2 reflex-todo/todo-client/src/GHCJS.hs+33 reflex-todo/todo-client/src/JSFFI.hs+29 reflex-todo/todo-client/src/Main.hs+225 reflex-todo/todo-client/todo-client.cabal+35 reflex-todo/todo-common/CHANGELOG.md+5 reflex-todo/todo-common/LICENSE reflex-todo/todo-common/Setup.hs+2 reflex-todo/todo-common/src/Common.hs+1 reflex-todo/todo-common/todo-common.cabal+26 reflex-todo/todo-server/CHANGELOG.md+5 reflex-todo/todo-server/Setup.hs+2 reflex-todo/todo-server/src/Main.hs+4 reflex-todo/todo-server/todo-server.cabal+28
Nix_Intro/fact.nixnew file+7
@@ -0,0 +1,7 @@
1+with import ./mul.nix;
2+let
3+ fac = n:
4+ if n == 0
5+ then 1
6+ else mul n (fac (n - 1));
7+in { inherit fac; }
Nix_Intro/mul.nixnew file+3
@@ -0,0 +1,3 @@
1+let
2+ mul = a: b: a * b;
3+in { inherit mul; }
Nix_Intro/simple-derivation/default.nixnew file+11
@@ -0,0 +1,11 @@
1+{ pkgs ? import <nixpkgs> {} }:
2+
3+derivation {
4+ name = "hello-world";
5+ builder = pkgs.writeShellScript "build-hello" ''
6+ ${pkgs.coreutils}/bin/mkdir -p $out/bin
7+ ${pkgs.gcc}/bin/gcc $src -o $out/bin/hello -O2
8+ '';
9+ src = ./hello.c;
10+ system = builtins.currentSystem;
11+}
Nix_Intro/simple-derivation/hello.cnew file+7
@@ -0,0 +1,7 @@
1+#include <stdio.h>
2+
3+int main(void)
4+{
5+ printf("Hello %s!\n", "World");
6+ return 0;
7+}
elm-vs-reflex/elm-quiz-demo/.gitignorenew file+6
@@ -0,0 +1,6 @@
1+# elm-package generated files
2+elm-stuff
3+# elm-repl generated files
4+repl-temp-*
5+# generated file
6+output.js
elm-vs-reflex/elm-quiz-demo/README.mdnew file+9
@@ -0,0 +1,9 @@
1+# Сборка и запуск
2+
3+Установите [Elm](https://elm-lang.org/) и выполните следующую команду:
4+
5+```bash
6+elm make src/Main.elm --output output.js
7+```
8+
9+Теперь откройте `index.html` в браузере.
elm-vs-reflex/elm-quiz-demo/elm.jsonnew file+25
@@ -0,0 +1,25 @@
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-community/list-extra": "8.2.4"
13+ },
14+ "indirect": {
15+ "elm/json": "1.1.3",
16+ "elm/time": "1.0.0",
17+ "elm/url": "1.0.0",
18+ "elm/virtual-dom": "1.0.2"
19+ }
20+ },
21+ "test-dependencies": {
22+ "direct": {},
23+ "indirect": {}
24+ }
25+}
elm-vs-reflex/elm-quiz-demo/index.htmlnew file+19
@@ -0,0 +1,19 @@
1+<!DOCTYPE html>
2+<html>
3+ <head>
4+ <link rel="stylesheet" type="text/css" href="main.css">
5+ <meta charset="utf-8">
6+ <title>Quiz Demo (Elm)</title>
7+ </head>
8+ <body>
9+ <script src="output.js"></script>
10+ <h1>Quiz demo using <a href="https://elm-lang.org/" target="_blank">Elm</a></h1>
11+ <div id="container"></div>
12+ <script>
13+ var app = Elm.Main.init({
14+ node: document.getElementById('container')
15+ });
16+ </script>
17+ <h3>Demo made by <a href="https://typeable.io/" target="_blank">Typeable</a<</h3>
18+ </body>
19+</html>
elm-vs-reflex/elm-quiz-demo/main.cssnew file+90
@@ -0,0 +1,90 @@
1+body {
2+ margin: 40px auto;
3+ max-width: 650px;
4+ line-height: 1.6;
5+ color: #444;
6+ padding: 0 10px;
7+}
8+
9+h1, h2, h3 {
10+ line-height:1.2;
11+}
12+
13+button {
14+ padding: 10px;
15+ cursor: pointer;
16+}
17+
18+h1, h3 {
19+ text-align: center;
20+}
21+
22+#container {
23+ width: 800px;
24+ margin: auto;
25+}
26+
27+.answers {
28+ margin-left: 10px;
29+ margin-top: 10px;
30+ margin-bottom: 20px;
31+}
32+
33+.question {
34+ font-size: 20px;
35+}
36+
37+.answer {
38+ font-weight: bold;
39+ cursor: pointer;
40+ border-top: 0.1px dashed rgba(0,0,0,0.15);
41+}
42+
43+.check-answers-button-container {
44+ height: 100px;
45+ width: 100%;
46+}
47+
48+.unfinished-quiz-notice {
49+ font-style: italic;
50+}
51+
52+.answer::before {
53+ content: "☐ ";
54+}
55+
56+.answer.answer-chosen::before {
57+ content: "🗹 ";
58+}
59+
60+.answer-chosen.answer-correct.answer-shown::before {
61+ content: "✓ ";
62+}
63+
64+.answer-chosen.answer-correct.answer-shown::after {
65+ content: " [correct!]";
66+}
67+
68+.answer-chosen.answer-incorrect.answer-shown::after {
69+ content: " [wrong!]";
70+}
71+
72+.answer-chosen.answer-incorrect.answer-shown::before {
73+ content: "✗ ";
74+}
75+
76+.answer-correct.answer-shown {
77+ color: lightgreen;
78+}
79+
80+.answer-incorrect.answer-shown {
81+ color: pink;
82+}
83+
84+.answer-correct.answer-chosen.answer-shown {
85+ color: green;
86+}
87+
88+.answer-incorrect.answer-chosen.answer-shown {
89+ color: red;
90+}
elm-vs-reflex/elm-quiz-demo/src/Main.elmnew file+199
@@ -0,0 +1,199 @@
1+module Main exposing (main)
2+
3+import Browser
4+import Html exposing (Html, button, div, text)
5+import Html.Events exposing (onClick)
6+import List.Extra exposing (updateAt)
7+import List
8+import Tuple
9+import Html.Attributes exposing (style, class)
10+import String
11+
12+-- * Типы данных, описывающие состояние виджета.
13+
14+type alias QuestionText = String
15+
16+type alias AnswerText = String
17+
18+type IsChosen = Chosen | NotChosen
19+
20+type IsCorrect = Correct | Incorrect
21+
22+type CanCheckAnswers = CanCheckAnswers | CantCheckAnswers
23+
24+type AreAnswersShown = AnswersShown | AnswersHidden
25+
26+type alias Answer =
27+ { answerText : AnswerText
28+ , isCorrect : IsCorrect
29+ }
30+
31+type Score
32+ = NoScore
33+ | Score { totalQuestions : Int, correctAnswers : Int }
34+
35+type alias Questions = List (QuestionText, List (Answer, IsChosen))
36+
37+type alias Model
38+ = { areAnswersShown : AreAnswersShown
39+ , allQuestions : Questions
40+ , canCheckAnswers : CanCheckAnswers
41+ , score : Score
42+ }
43+
44+-- ** Тип данных, описывающий payload события (Message).
45+
46+type Msg
47+ = SelectAnswer
48+ { questionNumber : Int
49+ , answerNumber : Int
50+ , isChosen : IsChosen }
51+ | CheckAnswers
52+
53+update : Msg -> Model -> Model
54+update msg = case msg of
55+ SelectAnswer { questionNumber, answerNumber, isChosen } ->
56+ updateCanCheckAnswers <<
57+ ( mapAllQuestions
58+ <| updateAt questionNumber
59+ <| Tuple.mapSecond
60+ <| updateAnswers answerNumber isChosen )
61+ CheckAnswers -> mapAnswersShown (\_ -> AnswersShown) >> updateScore
62+
63+updateCanCheckAnswers : Model -> Model
64+updateCanCheckAnswers model =
65+ { model | canCheckAnswers =
66+ if List.all hasChosenAnswer model.allQuestions
67+ then CanCheckAnswers
68+ else CantCheckAnswers }
69+
70+updateScore : Model -> Model
71+updateScore model =
72+ let
73+ hasCorrectAnswer (_, answers) =
74+ List.any isCorrectAnswerChosen answers
75+ correctAnswers =
76+ List.length <| List.filter hasCorrectAnswer model.allQuestions
77+ totalQuestions = List.length model.allQuestions
78+ in
79+ { model | score =
80+ Score { correctAnswers = correctAnswers
81+ , totalQuestions = totalQuestions } }
82+
83+updateAnswers : Int -> IsChosen -> List (Answer, IsChosen) -> List (Answer, IsChosen)
84+updateAnswers answerIx newIsChosen =
85+ List.indexedMap <| \aix ->
86+ Tuple.mapSecond <| \isChosen ->
87+ if aix /= answerIx
88+ then
89+ if newIsChosen == Chosen
90+ then NotChosen
91+ else isChosen
92+ else newIsChosen
93+
94+view : Model -> Html Msg
95+view model =
96+ div [] <|
97+ List.indexedMap (viewQuestion model.areAnswersShown) model.allQuestions ++
98+ [ div [ class "check-answers-button-container" ] [ viewFooter model ] ]
99+
100+viewQuestion : AreAnswersShown -> Int -> (QuestionText, List (Answer, IsChosen)) -> Html Msg
101+viewQuestion areShown questionIx (question, answers) =
102+ div [class "question"] <|
103+ [ text question ] ++
104+ [ div [class "answers"]
105+ <| List.indexedMap (viewAnswer areShown questionIx) answers ]
106+
107+viewAnswer : AreAnswersShown -> Int -> Int -> (Answer, IsChosen) -> Html Msg
108+viewAnswer areShown questionIx answerIx (answer, isChosen) =
109+ let
110+ events = [ onClick <|
111+ SelectAnswer { questionNumber = questionIx
112+ , answerNumber = answerIx
113+ , isChosen = toggleChosen isChosen
114+ } ]
115+ className = String.join " " <|
116+ ["answer"] ++
117+ ( if isChosen == Chosen
118+ then ["answer-chosen"]
119+ else [] ) ++
120+ ( if areShown == AnswersShown
121+ then ["answer-shown"]
122+ else ["answer-hidden"] ) ++
123+ ( if answer.isCorrect == Correct
124+ then ["answer-correct"]
125+ else ["answer-incorrect"] )
126+ attrs = [ class className ]
127+ in
128+ div (attrs ++ events) [ text answer.answerText ]
129+
130+viewFooter : Model -> Html Msg
131+viewFooter model =
132+ case model.score of
133+ NoScore ->
134+ case model.canCheckAnswers of
135+ CanCheckAnswers ->
136+ button [ onClick CheckAnswers ] [ text "Check answers" ]
137+ CantCheckAnswers ->
138+ div [ class "unfinished-quiz-notice" ]
139+ [ text "Select answers for all questions before you can get the results." ]
140+ Score { totalQuestions, correctAnswers } ->
141+ text <|
142+ "Your score: " ++ String.fromInt correctAnswers ++
143+ " of " ++ String.fromInt totalQuestions
144+
145+-- * Вспомогательные функции
146+
147+toggleChosen : IsChosen -> IsChosen
148+toggleChosen isChosen =
149+ case isChosen of
150+ Chosen -> NotChosen
151+ NotChosen -> Chosen
152+
153+hasChosenAnswer : (QuestionText, List (Answer, IsChosen)) -> Bool
154+hasChosenAnswer (_, answers) =
155+ List.any (\(_, isChosen) -> isChosen == Chosen) answers
156+
157+isCorrectAnswerChosen : (Answer, IsChosen) -> Bool
158+isCorrectAnswerChosen (answer, isChosen) =
159+ case (answer.isCorrect, isChosen) of
160+ (Correct, Chosen) -> True
161+ _ -> False
162+
163+mapAllQuestions : (Questions -> Questions) -> Model -> Model
164+mapAllQuestions f model = { model | allQuestions = f model.allQuestions }
165+
166+mapAnswersShown : (AreAnswersShown -> AreAnswersShown) -> Model -> Model
167+mapAnswersShown f model = { model | areAnswersShown = f model.areAnswersShown }
168+
169+mapCanCheckAnswers : (CanCheckAnswers -> CanCheckAnswers) -> Model -> Model
170+mapCanCheckAnswers f model = { model | canCheckAnswers = f model.canCheckAnswers }
171+
172+allQuestions : Questions
173+allQuestions =
174+ [ ( "How many arguments does update function have?"
175+ , [ ({ answerText = "1", isCorrect = Incorrect }, NotChosen)
176+ , ({ answerText = "2", isCorrect = Correct }, NotChosen)
177+ , ({ answerText = "3", isCorrect = Incorrect }, NotChosen)
178+ , ({ answerText = "4", isCorrect = Incorrect }, NotChosen)
179+ ] )
180+ , ( "Which of the following does not contain a value at any point of time?"
181+ , [ ({ answerText = "Behavior", isCorrect = Incorrect }, NotChosen)
182+ , ({ answerText = "Event", isCorrect = Correct }, NotChosen)
183+ , ({ answerText = "Dynamic", isCorrect = Incorrect }, NotChosen)
184+ ] )
185+ , ( "Which framework does not use Virtual DOM?"
186+ , [ ({ answerText = "Elm", isCorrect = Incorrect }, NotChosen)
187+ , ({ answerText = "Reflex", isCorrect = Correct }, NotChosen)
188+ ] )
189+ ]
190+
191+initialModel =
192+ { areAnswersShown = AnswersHidden
193+ , allQuestions = allQuestions
194+ , score = NoScore
195+ , canCheckAnswers = CantCheckAnswers
196+ }
197+
198+main =
199+ Browser.sandbox { init = initialModel, update = update, view = view }
elm-vs-reflex/reflex-quiz-demo/.gitignorenew file+15
@@ -0,0 +1,15 @@
1+**/dist
2+**/.cabal-sandbox
3+**/cabal.sandbox.config
4+result
5+result-*
6+*.o
7+*.hi
8+TAGS
9+tags
10+ctags
11+dist-newstyle
12+cabal.project.local
13+.ghc.environment.*
14+**/ghcid-output.txt
15+.obelisk
elm-vs-reflex/reflex-quiz-demo/README.mdnew file+3
@@ -0,0 +1,3 @@
1+# Сборка и запуск
2+
3+Установите [obelisk](https://github.com/obsidiansystems/obelisk) и выполните `ob run`.
elm-vs-reflex/reflex-quiz-demo/backend/backend.cabalnew file+29
@@ -0,0 +1,29 @@
1+name: backend
2+version: 0.1
3+cabal-version: >= 1.8
4+build-type: Simple
5+
6+library
7+ hs-source-dirs: src
8+ if impl(ghcjs)
9+ buildable: False
10+ build-depends: base
11+ , common
12+ , frontend
13+ , obelisk-backend
14+ , obelisk-route
15+ exposed-modules:
16+ Backend
17+ ghc-options: -Wall -Wredundant-constraints -Wincomplete-uni-patterns -Wincomplete-record-updates -O -fno-show-valid-hole-fits
18+
19+executable backend
20+ main-is: main.hs
21+ hs-source-dirs: src-bin
22+ ghc-options: -Wall -Wredundant-constraints -Wincomplete-uni-patterns -Wincomplete-record-updates -O -threaded -fno-show-valid-hole-fits
23+ if impl(ghcjs)
24+ buildable: False
25+ build-depends: base
26+ , backend
27+ , common
28+ , frontend
29+ , obelisk-backend
elm-vs-reflex/reflex-quiz-demo/backend/frontendJs/frontend.jsexenew file+1
@@ -0,0 +1 @@
1+../../frontend-js/bin/frontend.jsexe
elm-vs-reflex/reflex-quiz-demo/backend/src-bin/main.hsnew file+6
@@ -0,0 +1,6 @@
1+import Backend
2+import Frontend
3+import Obelisk.Backend
4+
5+main :: IO ()
6+main = runBackend backend frontend
elm-vs-reflex/reflex-quiz-demo/backend/src/Backend.hsnew file+10
@@ -0,0 +1,10 @@
1+module Backend where
2+
3+import Common.Route
4+import Obelisk.Backend
5+
6+backend :: Backend BackendRoute FrontendRoute
7+backend = Backend
8+ { _backend_run = \serve -> serve $ const $ return ()
9+ , _backend_routeEncoder = fullRouteEncoder
10+ }
elm-vs-reflex/reflex-quiz-demo/backend/staticnew file+1
@@ -0,0 +1 @@
1+../static
elm-vs-reflex/reflex-quiz-demo/cabal.projectnew file+3
@@ -0,0 +1,3 @@
1+optional-packages:
2+ *
3+write-ghc-environment-files: never
elm-vs-reflex/reflex-quiz-demo/common/common.cabalnew file+15
@@ -0,0 +1,15 @@
1+name: common
2+version: 0.1
3+cabal-version: >= 1.2
4+build-type: Simple
5+
6+library
7+ hs-source-dirs: src
8+ build-depends: base
9+ , obelisk-route
10+ , mtl
11+ , text
12+ exposed-modules:
13+ Common.Api
14+ Common.Route
15+ ghc-options: -Wall -Wredundant-constraints -Wincomplete-uni-patterns -Wincomplete-record-updates -O -fno-show-valid-hole-fits
elm-vs-reflex/reflex-quiz-demo/common/src/Common/Api.hsnew file+4
@@ -0,0 +1,4 @@
1+module Common.Api where
2+
3+commonStuff :: String
4+commonStuff = "Here is a string defined in Common.Api"
elm-vs-reflex/reflex-quiz-demo/common/src/Common/Route.hsnew file+47
@@ -0,0 +1,47 @@
1+{-# LANGUAGE EmptyCase #-}
2+{-# LANGUAGE FlexibleContexts #-}
3+{-# LANGUAGE FlexibleInstances #-}
4+{-# LANGUAGE GADTs #-}
5+{-# LANGUAGE KindSignatures #-}
6+{-# LANGUAGE LambdaCase #-}
7+{-# LANGUAGE MultiParamTypeClasses #-}
8+{-# LANGUAGE OverloadedStrings #-}
9+{-# LANGUAGE RankNTypes #-}
10+{-# LANGUAGE TemplateHaskell #-}
11+{-# LANGUAGE TypeFamilies #-}
12+module Common.Route where
13+
14+{- -- You will probably want these imports for composing Encoders.
15+import Prelude hiding (id, (.))
16+import Control.Category
17+-}
18+
19+import Data.Text (Text)
20+import Data.Functor.Identity
21+
22+import Obelisk.Route
23+import Obelisk.Route.TH
24+
25+data BackendRoute :: * -> * where
26+ -- | Used to handle unparseable routes.
27+ BackendRoute_Missing :: BackendRoute ()
28+ -- You can define any routes that will be handled specially by the backend here.
29+ -- i.e. These do not serve the frontend, but do something different, such as serving static files.
30+
31+data FrontendRoute :: * -> * where
32+ FrontendRoute_Main :: FrontendRoute ()
33+ -- This type is used to define frontend routes, i.e. ones for which the backend will serve the frontend.
34+
35+fullRouteEncoder
36+ :: Encoder (Either Text) Identity (R (FullRoute BackendRoute FrontendRoute)) PageName
37+fullRouteEncoder = mkFullRouteEncoder
38+ (FullRoute_Backend BackendRoute_Missing :/ ())
39+ (\case
40+ BackendRoute_Missing -> PathSegment "missing" $ unitEncoder mempty)
41+ (\case
42+ FrontendRoute_Main -> PathEnd $ unitEncoder mempty)
43+
44+concat <$> mapM deriveRouteComponent
45+ [ ''BackendRoute
46+ , ''FrontendRoute
47+ ]
elm-vs-reflex/reflex-quiz-demo/config/common/examplenew file+1
@@ -0,0 +1 @@
1+This string comes from config/common/example
elm-vs-reflex/reflex-quiz-demo/config/common/routenew file+1
@@ -0,0 +1 @@
1+http://localhost:8000
elm-vs-reflex/reflex-quiz-demo/config/readme.mdnew file+9
@@ -0,0 +1,9 @@
1+### Config
2+
3+Obelisk projects should contain a config folder with the following subfolders: common, frontend, and backend.
4+
5+Things that should never be transmitted to the frontend belong in backend/ (e.g., email credentials)
6+
7+Frontend-only configuration belongs in frontend/.
8+
9+Shared configuration files (e.g., the route config) belong in common/
elm-vs-reflex/reflex-quiz-demo/default.nixnew file+23
@@ -0,0 +1,23 @@
1+{ system ? builtins.currentSystem
2+, obelisk ? import ./.obelisk/impl {
3+ inherit system;
4+ iosSdkVersion = "13.2";
5+
6+ # You must accept the Android Software Development Kit License Agreement at
7+ # https://developer.android.com/studio/terms in order to build Android apps.
8+ # Uncomment and set this to `true` to indicate your acceptance:
9+ # config.android_sdk.accept_license = false;
10+
11+ # In order to use Let's Encrypt for HTTPS deployments you must accept
12+ # their terms of service at https://letsencrypt.org/repository/.
13+ # Uncomment and set this to `true` to indicate your acceptance:
14+ # terms.security.acme.acceptTerms = false;
15+ }
16+}:
17+with obelisk;
18+project ./. ({ ... }: {
19+ android.applicationId = "typeable.examples.quiz";
20+ android.displayName = "Reflex Quiz Demo";
21+ ios.bundleIdentifier = "typeable.examples.quiz";
22+ ios.bundleName = "Reflex Quiz Demo";
23+})
elm-vs-reflex/reflex-quiz-demo/frontend/frontend.cabalnew file+48
@@ -0,0 +1,48 @@
1+name: frontend
2+version: 0.1
3+cabal-version: >= 1.8
4+build-type: Simple
5+
6+library
7+ hs-source-dirs: src
8+ build-depends: base
9+ , common
10+ , obelisk-frontend
11+ , obelisk-route
12+ , jsaddle
13+ , reflex-dom
14+ , obelisk-executable-config-lookup
15+ , obelisk-generated-static
16+ , text
17+ exposed-modules:
18+ Frontend
19+ ghc-options: -Wall -Wredundant-constraints -Wincomplete-uni-patterns -Wincomplete-record-updates -O -fno-show-valid-hole-fits
20+ default-extensions:
21+ BlockArguments
22+ DataKinds
23+ FlexibleContexts
24+ GADTs
25+ LambdaCase
26+ NamedFieldPuns
27+ OverloadedStrings
28+ RecordWildCards
29+ RecursiveDo
30+ TupleSections
31+ TypeApplications
32+
33+executable frontend
34+ main-is: main.hs
35+ hs-source-dirs: src-bin
36+ build-depends: base
37+ , common
38+ , obelisk-frontend
39+ , obelisk-route
40+ , reflex-dom
41+ , obelisk-generated-static
42+ , frontend
43+ ghc-options: -threaded -O -Wall -Wredundant-constraints -Wincomplete-uni-patterns -Wincomplete-record-updates -fno-show-valid-hole-fits
44+ if impl(ghcjs)
45+ ghc-options: -dedupe
46+ cpp-options: -DGHCJS_BROWSER
47+ if os(darwin)
48+ ghc-options: -dynamic
elm-vs-reflex/reflex-quiz-demo/frontend/src-bin/main.hsnew file+10
@@ -0,0 +1,10 @@
1+import Frontend
2+import Common.Route
3+import Obelisk.Frontend
4+import Obelisk.Route.Frontend
5+import Reflex.Dom
6+
7+main :: IO ()
8+main = do
9+ let Right validFullEncoder = checkEncoder fullRouteEncoder
10+ run $ runFrontend validFullEncoder frontend
elm-vs-reflex/reflex-quiz-demo/frontend/src/Frontend.hsnew file+280
@@ -0,0 +1,280 @@
1+module Frontend where
2+
3+import Common.Route
4+import Control.Monad
5+import Control.Monad.Fix
6+import Control.Monad.State
7+import Control.Monad.Writer
8+import Data.Foldable
9+import Data.Functor
10+import qualified Data.Text as T
11+import Data.Traversable
12+import Obelisk.Frontend
13+import Obelisk.Generated.Static
14+import Obelisk.Route
15+import Reflex.Dom.Core
16+
17+-- * Типы данных, описывающие состояние виджета.
18+
19+type QuestionText = T.Text
20+
21+type AnswerText = T.Text
22+
23+data IsChosen = Chosen | NotChosen
24+ deriving (Show, Eq)
25+
26+data IsCorrect = Correct | Incorrect
27+ deriving (Show, Eq)
28+
29+data CanCheckAnswers = CanCheckAnswers | CantCheckAnswers
30+ deriving (Show, Eq)
31+
32+data AreAnswersShown = AnswersShown | AnswersHidden
33+ deriving (Show, Eq)
34+
35+data Answer = Answer
36+ { answerText :: AnswerText
37+ , isCorrect :: IsCorrect }
38+ deriving (Show, Eq)
39+
40+data Score
41+ = NoScore
42+ | Score
43+ { totalQuestions :: Int
44+ , correctAnswers :: Int }
45+ deriving (Eq, Show)
46+
47+-- ** Типы данных, описывающие payload различных событий.
48+
49+-- | Выбор варианта ответа.
50+data SelectAnswer
51+ = SelectAnswer
52+ { questionNumber :: Int
53+ , answerNumber :: Int }
54+
55+-- | Payload для события "показать ответы"
56+data CheckAnswers = CheckAnswers
57+
58+-- ** Типы данных, описывающие внутренние интерфейсы нашего виджета.
59+
60+-- | Тип, содержащий все события, с которыми мы будем работать.
61+data QuizEvents t = QuizEvents
62+ { selectAnswer :: Event t SelectAnswer
63+ , showAnswers :: Event t CheckAnswers }
64+
65+-- | Тип, содержащий все изменяющиеся (Dynamic) данные.
66+data QuizState t = QuizState
67+ { areAnswersShown :: Dynamic t AreAnswersShown
68+ , allQuestions :: [(QuestionText, [(Answer, Dynamic t IsChosen)])]
69+ , canCheckAnswers :: Dynamic t CanCheckAnswers
70+ , score :: Dynamic t Score }
71+
72+-- * Model
73+
74+-- | Функция, описывающая всю внутреннюю логику работы виджета. Она принимает
75+-- тип данных 'QuizEvents', хранящий все события и возвращает динамическое
76+-- состояние.
77+mkQuizModel :: ObeliskWidget js t route m
78+ => [(QuestionText, [Answer])]
79+ -- ^ Список вопросов с ответами
80+ -> QuizEvents t
81+ -> m (QuizState t)
82+mkQuizModel questions events = do
83+ areAnswersShown <- holdDyn AnswersHidden (showAnswers events $> AnswersShown)
84+ allQuestions <- mkAllQuestionsModel questions events
85+ canCheckAnswers <- mkCanCheckAnswersModel allQuestions
86+ score <- mkScoreModel areAnswersShown allQuestions
87+ return QuizState{..}
88+
89+mkAllQuestionsModel :: ObeliskWidget js t route m
90+ => [(QuestionText, [Answer])]
91+ -- ^ Список вопросов с ответами
92+ -> QuizEvents t
93+ -> m [(QuestionText, [(Answer, Dynamic t IsChosen)])]
94+mkAllQuestionsModel questions events = do
95+ for (enumerate questions)
96+ \(qNum, (questionText, answers)) -> do
97+ (questionText, ) <$> for (enumerate answers)
98+ \(aNum, Answer{..}) -> do
99+ let
100+ updChosenState SelectAnswer{questionNumber,answerNumber} isChosen = do
101+ guard (questionNumber == qNum)
102+ return
103+ if answerNumber == aNum
104+ then toggleChosen isChosen
105+ else NotChosen
106+ isChosenDyn <- foldDynMaybe updChosenState NotChosen
107+ (selectAnswer events)
108+ return (Answer{..}, isChosenDyn)
109+
110+mkCanCheckAnswersModel :: ObeliskWidget js t route m
111+ => [(QuestionText, [(Answer, Dynamic t IsChosen)])]
112+ -> m (Dynamic t CanCheckAnswers)
113+mkCanCheckAnswersModel allQuestions = holdUniqDyn do
114+ -- Для каждого вопроса хотя бы один ответ выбран
115+ allQuestionsAnswered <- all (Chosen `elem`) <$> do
116+ for allQuestions \(_, answers) -> do
117+ for answers \(_, dynIsChosen) -> dynIsChosen
118+ return if allQuestionsAnswered then CanCheckAnswers else CantCheckAnswers
119+
120+mkScoreModel :: ObeliskWidget js t route m
121+ => Dynamic t AreAnswersShown
122+ -> [(QuestionText, [(Answer, Dynamic t IsChosen)])]
123+ -> m (Dynamic t Score)
124+mkScoreModel areAnswersShown allQuestions = holdUniqDyn do
125+ areAnswersShown >>= \case
126+ AnswersHidden -> return NoScore
127+ AnswersShown -> do
128+ correctAnswers <- flip execStateT 0 do
129+ for_ allQuestions \(_, answers) -> do
130+ for_ answers \(answer, dynIsChosen) -> do
131+ isChosen <- lift dynIsChosen
132+ when (isChosen == Chosen && isCorrect answer == Correct) do
133+ modify (+ 1)
134+ return Score { correctAnswers, totalQuestions = length allQuestions }
135+
136+-- * Интерфейс пользователя
137+
138+quizUI :: ObeliskWidget js t route m => QuizState t -> m (QuizEvents t)
139+quizUI QuizState{..} = wrapUI do
140+ selectAnswer <- leftmost <$> for (enumerate allQuestions)
141+ \(qNum, (questionText, answers)) -> do
142+ divClass "question" do
143+ text questionText
144+ answersUI qNum areAnswersShown answers
145+ showAnswers <- footerUI canCheckAnswers score
146+ return QuizEvents{..}
147+
148+answersUI :: ObeliskWidget js t route m
149+ => Int
150+ -> Dynamic t AreAnswersShown
151+ -> [(Answer, Dynamic t IsChosen)]
152+ -> m (Event t SelectAnswer)
153+answersUI qNum areAnswersShown answers = elClass "div" "answers" do
154+ leftmost <$> for (enumerate answers)
155+ \(aNum, (Answer{answerText,isCorrect}, dynIsChosen)) -> do
156+ event <- answerUI areAnswersShown answerText isCorrect dynIsChosen
157+ return (event $> SelectAnswer { questionNumber = qNum, answerNumber = aNum })
158+
159+answerUI :: ObeliskWidget js t route m
160+ => Dynamic t AreAnswersShown
161+ -> AnswerText
162+ -> IsCorrect
163+ -> Dynamic t IsChosen
164+ -> m (Event t ())
165+answerUI areAnswersShown answerText isCorrect dynIsChosen =
166+ domEvent Click . fst <$> elDynAttr' "div" dynAttrs do
167+ text answerText
168+ where
169+ dynAttrs = do
170+ isChosen <- dynIsChosen
171+ areShown <- areAnswersShown
172+ let
173+ className = T.intercalate " " $ execWriter do
174+ tell ["answer"]
175+ when (isChosen == Chosen) $ tell ["answer-chosen"]
176+ tell [ if areShown == AnswersShown
177+ then "answer-shown"
178+ else "answer-hidden" ]
179+ tell [ if isCorrect == Correct
180+ then "answer-correct"
181+ else "answer-incorrect" ]
182+ return $ "class" =: className
183+
184+-- | Виджет, содержащий либо текст с предложением ответить на все вопросы, либо
185+-- кнопку проверки ответов, либо информацию о результатах.
186+footerUI :: ObeliskWidget js t route m
187+ => Dynamic t CanCheckAnswers -> Dynamic t Score -> m (Event t CheckAnswers)
188+footerUI canCheckAnswersDyn dynScore = wrapContainer do
189+ evt <- switchHold never <=< dyn $ do
190+ canCheckAnswers <- canCheckAnswersDyn
191+ score <- dynScore
192+ return if score /= NoScore
193+ then return never
194+ else
195+ case canCheckAnswers of
196+ CanCheckAnswers -> do
197+ domEvent Click . fst <$> do
198+ el' "button" do
199+ text "Check answers"
200+ CantCheckAnswers -> do
201+ divClass "unfinished-quiz-notice" do
202+ text "Select answers for all questions before you can get the results."
203+ return never
204+ dyn_ $ dynScore <&> \case
205+ NoScore -> blank
206+ Score{totalQuestions, correctAnswers} -> do
207+ text "Your score: "
208+ text . T.pack $ show correctAnswers
209+ text " of "
210+ text . T.pack $ show totalQuestions
211+ return (evt $> CheckAnswers)
212+ where wrapContainer = divClass "check-answers-button-container"
213+
214+-- | Функция, снабжающая наш виджет статическим контентом.
215+wrapUI :: ObeliskWidget js t route m => m a -> m a
216+wrapUI contents = do
217+ el "h1" do
218+ text "Quiz Demo using "
219+ mkLink "Reflex framework" "https://reflex-frp.org/"
220+ result <- elAttr "div" ("id" =: "container") contents
221+ el "h3" do
222+ text "Demo made by "
223+ mkLink "Typeable" "https://typeable.io/"
224+ return result
225+ where
226+ mkLink linkText url = do
227+ elAttr "a" ("href" =: url <> "target" =: "blank") do
228+ text linkText
229+
230+-- * Main widget
231+
232+questions :: [(QuestionText, [Answer])]
233+questions =
234+ [ ( "How many arguments does update function have?"
235+ , [ Answer "1" Incorrect
236+ , Answer "2" Correct
237+ , Answer "3" Incorrect
238+ , Answer "4" Incorrect
239+ ] )
240+ , ( "Which of the following does not contain a value at any point of time?"
241+ , [ Answer "Behaviour" Incorrect
242+ , Answer "Event" Correct
243+ , Answer "Dynamic" Incorrect
244+ ] )
245+ , ( "Which framework does not use Virtual DOM?"
246+ , [ Answer "Elm" Incorrect
247+ , Answer "Reflex" Correct
248+ ] )
249+ ]
250+
251+mkQuizWidget :: ObeliskWidget js t route m => [(QuestionText, [Answer])] -> m ()
252+mkQuizWidget qs =
253+ mkWidget (mkQuizModel qs) quizUI
254+
255+-- * Вспомогательные функции.
256+
257+mkWidget :: ObeliskWidget js t route m
258+ => (events -> m state) -> (state -> m events) -> m ()
259+mkWidget model ui = void (mfix (model >=> ui))
260+
261+enumerate :: [a] -> [(Int, a)]
262+enumerate = zip [0..]
263+
264+toggleChosen :: IsChosen -> IsChosen
265+toggleChosen Chosen = NotChosen
266+toggleChosen NotChosen = Chosen
267+
268+frontend :: Frontend (R FrontendRoute)
269+frontend = Frontend
270+ { _frontend_head = do
271+ el "title" $ text "Quiz Demo (Reflex)"
272+ elAttr "link"
273+ ( "href" =: static @"main.css" <>
274+ "type" =: "text/css" <>
275+ "rel" =: "stylesheet" ) blank
276+ , _frontend_body = do
277+ el "div" do
278+ mkQuizWidget questions
279+ return ()
280+ }
elm-vs-reflex/reflex-quiz-demo/static/main.cssnew file+90
@@ -0,0 +1,90 @@
1+body {
2+ margin: 40px auto;
3+ max-width: 650px;
4+ line-height: 1.6;
5+ color: #444;
6+ padding: 0 10px;
7+}
8+
9+h1, h2, h3 {
10+ line-height:1.2;
11+}
12+
13+button {
14+ padding: 10px;
15+ cursor: pointer;
16+}
17+
18+h1, h3 {
19+ text-align: center;
20+}
21+
22+#container {
23+ width: 800px;
24+ margin: auto;
25+}
26+
27+.answers {
28+ margin-left: 10px;
29+ margin-top: 10px;
30+ margin-bottom: 20px;
31+}
32+
33+.question {
34+ font-size: 20px;
35+}
36+
37+.answer {
38+ font-weight: bold;
39+ cursor: pointer;
40+ border-top: 0.1px dashed rgba(0,0,0,0.15);
41+}
42+
43+.check-answers-button-container {
44+ height: 100px;
45+ width: 100%;
46+}
47+
48+.unfinished-quiz-notice {
49+ font-style: italic;
50+}
51+
52+.answer::before {
53+ content: "☐ ";
54+}
55+
56+.answer.answer-chosen::before {
57+ content: "🗹 ";
58+}
59+
60+.answer-chosen.answer-correct.answer-shown::before {
61+ content: "✓ ";
62+}
63+
64+.answer-chosen.answer-correct.answer-shown::after {
65+ content: " [correct!]";
66+}
67+
68+.answer-chosen.answer-incorrect.answer-shown::after {
69+ content: " [wrong!]";
70+}
71+
72+.answer-chosen.answer-incorrect.answer-shown::before {
73+ content: "✗ ";
74+}
75+
76+.answer-correct.answer-shown {
77+ color: lightgreen;
78+}
79+
80+.answer-incorrect.answer-shown {
81+ color: pink;
82+}
83+
84+.answer-correct.answer-chosen.answer-shown {
85+ color: green;
86+}
87+
88+.answer-incorrect.answer-chosen.answer-shown {
89+ color: red;
90+}
reflex-todo/cabal.projectnew file+4
@@ -0,0 +1,4 @@
1+packages:
2+ todo-common/
3+ todo-client/
4+ todo-server/
reflex-todo/default.nixnew file+39
@@ -0,0 +1,39 @@
1+{ reflex-platform ? ((import <nixpkgs> {}).fetchFromGitHub {
2+ owner = "reflex-frp";
3+ repo = "reflex-platform";
4+ rev = "efc6d923c633207d18bd4d8cae3e20110a377864";
5+ sha256 = "121rmnkx8nwiy96ipfyyv6vrgysv0zpr2br46y70zf4d0y1h1lz5";
6+ })
7+}:
8+(import reflex-platform {}).project ({ pkgs, ... }:
9+let
10+ reflexDomContribSrc = builtins.fetchGit {
11+ url = "https://github.com/reflex-frp/reflex-dom-contrib.git";
12+ rev = "11db20865fd275362be9ea099ef88ded425789e7";
13+ };
14+
15+ override = self: pkg: with pkgs.haskell.lib;
16+ doJailbreak (pkg.overrideAttrs
17+ (old: {
18+ buildInputs = old.buildInputs ++ [ self.doctest self.cabal-doctest ];
19+ }));
20+
21+in {
22+ useWarp = true;
23+
24+ overrides = self: super: with pkgs.haskell.lib; rec {
25+ reflex-dom-contrib = override self
26+ (self.callCabal2nix "reflex-dom-contrib" reflexDomContribSrc { });
27+ };
28+
29+ packages = {
30+ todo-common = ./todo-common;
31+ todo-server = ./todo-server;
32+ todo-client = ./todo-client;
33+ };
34+
35+ shells = {
36+ ghc = ["todo-common" "todo-server" "todo-client"];
37+ ghcjs = ["todo-common" "todo-client"];
38+ };
39+})
reflex-todo/todo-client/CHANGELOG.mdnew file+5
@@ -0,0 +1,5 @@
1+# Revision history for todo-client
2+
3+## 0.1.0.0 -- YYYY-mm-dd
4+
5+* First version. Released on an unsuspecting world.
reflex-todo/todo-client/LICENSEnew file

No changes to the content.

reflex-todo/todo-client/Setup.hsnew file+2
@@ -0,0 +1,2 @@
1+import Distribution.Simple
2+main = defaultMain
reflex-todo/todo-client/src/GHCJS.hsnew file+33
@@ -0,0 +1,33 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+{-# LANGUAGE MonoLocalBinds #-}
3+module GHCJS where
4+
5+import Control.Monad
6+import Data.Functor (($>))
7+import Data.Text (Text)
8+import GHCJS.DOM
9+import GHCJS.DOM.Document
10+ (createElement, execCommand, getBodyUnchecked)
11+import GHCJS.DOM.Element as Element hiding (scroll)
12+import GHCJS.DOM.HTMLElement as HE (focus)
13+import GHCJS.DOM.HTMLInputElement as HIE (select, setValue)
14+import GHCJS.DOM.Node (appendChild, removeChild)
15+import GHCJS.DOM.Types hiding (Event, Text)
16+import Reflex.Dom as R
17+
18+toClipboard :: MonadJSM m => Text -> m ()
19+toClipboard txt = do
20+ doc <- currentDocumentUnchecked
21+ body <- getBodyUnchecked doc
22+ inpEl <- uncheckedCastTo HTMLInputElement <$> createElement doc
23+ ("textarea" :: Text)
24+ void $ appendChild body inpEl
25+ HE.focus inpEl
26+ HIE.setValue inpEl txt
27+ HIE.select inpEl
28+ void $ execCommand doc ("copy" :: Text) False (Nothing :: Maybe Text)
29+ void $ removeChild body inpEl
30+
31+copyByEvent :: MonadWidget t m => Text -> Event t () -> m ()
32+copyByEvent txt ev =
33+ void $ performEvent $ ev $> toClipboard txt
reflex-todo/todo-client/src/JSFFI.hsnew file+29
@@ -0,0 +1,29 @@
1+{-# LANGUAGE CPP #-}
2+{-# LANGUAGE MonoLocalBinds #-}
3+
4+module JSFFI where
5+
6+import Reflex.Dom
7+
8+#ifdef ghcjs_HOST_OS
9+
10+import Control.Monad.IO.Class
11+
12+foreign import javascript unsafe
13+ "(function() {\
14+ flatpickr($1, {\
15+ enableTime: false,\
16+ dateFormat: \"Y-m-d\"\
17+ }); \
18+ })()"
19+ addDatePicker_js :: RawInputElement GhcjsDomSpace -> IO ()
20+
21+addDatePicker :: MonadWidget t m => InputElement er GhcjsDomSpace t -> m ()
22+addDatePicker = liftIO . addDatePicker_js . _inputElement_raw
23+
24+#else
25+
26+addDatePicker :: MonadWidget t m => InputElement er GhcjsDomSpace t -> m ()
27+addDatePicker _ = pure ()
28+
29+#endif
reflex-todo/todo-client/src/Main.hsnew file+225
@@ -0,0 +1,225 @@
1+{-# LANGUAGE MonoLocalBinds #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+{-# LANGUAGE DeriveGeneric #-}
4+{-# LANGUAGE RecordWildCards #-}
5+{-# LANGUAGE RecursiveDo #-}
6+{-# LANGUAGE LambdaCase #-}
7+{-# LANGUAGE FlexibleContexts #-}
8+module Main where
9+
10+import Control.Lens
11+import Control.Monad
12+import Control.Monad.IO.Class
13+import Data.Functor (($>))
14+import Data.Maybe
15+import Data.Map as M (fromAscList, elems)
16+import Data.IntMap as IM
17+import Data.Monoid
18+import Data.Text
19+import Data.Time
20+import GHC.Generics
21+import Reflex.Dom
22+import Reflex.Dom.Contrib.Widgets.ScriptDependent
23+
24+import GHCJS
25+import JSFFI
26+
27+main :: IO ()
28+main = mainWidgetWithHead headWidget rootWidget
29+
30+headWidget :: MonadWidget t m => m ()
31+headWidget = do
32+ elAttr "meta" ("charset" =: "utf-8") blank
33+ elAttr "meta"
34+ ( "name" =: "viewport"
35+ <> "content" =: "width=device-width, initial-scale=1, shrink-to-fit=no" )
36+ blank
37+ elAttr "link"
38+ ( "rel" =: "stylesheet"
39+ <> "href" =: "https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css"
40+ <> "integrity" =: "sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh"
41+ <> "crossorigin" =: "anonymous")
42+ blank
43+ elAttr "link"
44+ ( "rel" =: "stylesheet"
45+ <> "href" =: "https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css" )
46+ blank
47+ elAttr "script"
48+ ( "src" =: "https://cdn.jsdelivr.net/npm/flatpickr")
49+ blank
50+ el "title" $ text "TODO App"
51+
52+type Todos = IntMap Todo
53+
54+data TodoState
55+ = TodoDone
56+ | TodoActive { stateEdit :: Bool }
57+ deriving (Generic, Eq, Show)
58+
59+data Todo = Todo
60+ { todoText :: Text
61+ , todoDeadline :: Day
62+ , todoState :: TodoState }
63+ deriving (Generic, Eq, Show)
64+
65+newTodo :: Text -> Day -> Todo
66+newTodo todoText todoDeadline = Todo {todoState = TodoActive False, ..}
67+
68+startEdit :: Todo -> Todo
69+startEdit todo = todo { todoState = TodoActive True }
70+
71+finishEdit :: Text -> Todo -> Todo
72+finishEdit val todo = todo { todoState = TodoActive False, todoText = val }
73+
74+toggleTodo :: Todo -> Todo
75+toggleTodo Todo{..} = Todo {todoState = toggleState todoState,..}
76+ where
77+ toggleState = \case
78+ TodoDone -> TodoActive False
79+ TodoActive _ -> TodoDone
80+
81+nextKey :: IntMap Todo -> Int
82+nextKey = maybe 0 (succ . fst . fst) . maxViewWithKey
83+
84+rootWidget :: MonadWidget t m => m ()
85+rootWidget =
86+ divClass "container" $ mdo
87+ elClass "h2" "text-center mt-3" $ text "Todos"
88+ (_, ev) <- runEventWriterT $ do
89+ todosDyn <- foldDyn appEndo mempty ev
90+ newTodoForm
91+ delimiter
92+ todoListWidget todosDyn
93+ blank
94+
95+newTodoForm :: (EventWriter t (Endo Todos) m, MonadWidget t m) => m ()
96+newTodoForm = rowWrapper $ el "form" $ divClass "input-group" $ mdo
97+ iEl <- inputElement $ def
98+ & initialAttributes .~
99+ ( "type" =: "text"
100+ <> "class" =: "form-control"
101+ <> "placeholder" =: "Todo" )
102+ & inputElementConfig_setValue .~ ("" <$ btnEv)
103+ dEl <- inputElement $ def
104+ & initialAttributes .~
105+ ( "type" =: "text"
106+ <> "class" =: "form-control"
107+ <> "placeholder" =: "Deadline"
108+ <> "style" =: "max-width: 150px" )
109+ pb <- getPostBuild
110+ widgetHoldUntilDefined "flatpickr"
111+ (pb $> "https://cdn.jsdelivr.net/npm/flatpickr")
112+ blank
113+ (addDatePicker dEl)
114+ today <- utctDay <$> liftIO getCurrentTime
115+ let
116+ dateStrDyn = value dEl
117+ dateDyn = fromMaybe today . parseTimeM True
118+ defaultTimeLocale "%Y-%m-%d" . unpack <$> dateStrDyn
119+ addNewTodo = \todo date -> Endo $ \todos ->
120+ insert (nextKey todos) (newTodo todo date) todos
121+ newTodoDyn = addNewTodo <$> value iEl <*> dateDyn
122+ btnAttr = "class" =: "btn btn-outline-secondary"
123+ <> "type" =: "button"
124+ (btnEl, _) <- divClass "input-group-append" $
125+ elAttr' "button" btnAttr $ text "Add new entry"
126+ let btnEv = domEvent Click btnEl
127+ tellEvent $ tagPromptlyDyn newTodoDyn $ domEvent Click btnEl
128+
129+todoListWidget
130+ :: (EventWriter t (Endo Todos) m, MonadWidget t m)
131+ => Dynamic t Todos -> m ()
132+todoListWidget todosDyn = rowWrapper $
133+ void $ listWithKey (M.fromAscList . IM.toAscList <$> todosDyn) todoWidget
134+
135+rowWrapper :: MonadWidget t m => m a -> m a
136+rowWrapper ma =
137+ divClass "row justify-content-md-center" $
138+ divClass "col-6" ma
139+
140+delimiter :: MonadWidget t m => m ()
141+delimiter = rowWrapper $
142+ divClass "border-top mt-3" blank
143+
144+todoWidget
145+ :: (EventWriter t (Endo Todos) m, MonadWidget t m)
146+ => Int -> Dynamic t Todo -> m ()
147+todoWidget ix todoDyn' = do
148+ todoDyn <- holdUniqDyn todoDyn'
149+ dyn_ $ ffor todoDyn $ \td@Todo{..} -> case todoState of
150+ TodoDone -> todoDone ix todoText todoDeadline
151+ TodoActive False -> todoActive ix todoText todoDeadline
152+ TodoActive True -> todoEditable ix todoText
153+
154+todoActive
155+ :: (EventWriter t (Endo Todos) m, MonadWidget t m)
156+ => Int -> Text -> Day -> m ()
157+todoActive ix todoText deadline = divClass "d-flex border-bottom" $ do
158+ elClass "p" "p-2 flex-grow-1 my-auto" $ do
159+ text todoText
160+ elClass "span" "badge badge-secondary mx-2" $
161+ text $ pack $ formatTime defaultTimeLocale "%F" deadline
162+ divClass "p-2 btn-group" $ do
163+ (copyEl, _) <- elAttr' "button"
164+ ( "class" =: "btn btn-outline-secondary"
165+ <> "type" =: "button" ) $ text "Copy"
166+ (doneEl, _) <- elAttr' "button"
167+ ( "class" =: "btn btn-outline-secondary"
168+ <> "type" =: "button" ) $ text "Done"
169+ (editEl, _) <- elAttr' "button"
170+ ( "class" =: "btn btn-outline-secondary"
171+ <> "type" =: "button" ) $ text "Edit"
172+ (delEl, _) <- elAttr' "button"
173+ ( "class" =: "btn btn-outline-secondary"
174+ <> "type" =: "button" ) $ text "Drop"
175+ copyByEvent todoText $ domEvent Click copyEl
176+ tellEvent $ Endo <$> leftmost
177+ [ update (Just . toggleTodo) ix <$ domEvent Click doneEl
178+ , update (Just . startEdit) ix <$ domEvent Click editEl
179+ , delete ix <$ domEvent Click delEl
180+ ]
181+
182+todoDone
183+ :: (EventWriter t (Endo Todos) m, MonadWidget t m)
184+ => Int -> Text -> Day -> m ()
185+todoDone ix todoText deadline = divClass "d-flex border-bottom" $ do
186+ elClass "p" "p-2 flex-grow-1 my-auto" $ do
187+ el "del" $ text todoText
188+ elClass "span" "badge badge-secondary mx-2" $
189+ text $ pack $ formatTime defaultTimeLocale "%F" deadline
190+ divClass "p-2 btn-group" $ do
191+ (doneEl, _) <- elAttr' "button"
192+ ( "class" =: "btn btn-outline-secondary"
193+ <> "type" =: "button" ) $ text "Undo"
194+ (delEl, _) <- elAttr' "button"
195+ ( "class" =: "btn btn-outline-secondary"
196+ <> "type" =: "button" ) $ text "Drop"
197+ tellEvent $ Endo <$> leftmost
198+ [ update (Just . toggleTodo) ix <$ domEvent Click doneEl
199+ , delete ix <$ domEvent Click delEl
200+ ]
201+
202+
203+todoEditable
204+ :: (EventWriter t (Endo Todos) m, MonadWidget t m)
205+ => Int -> Text -> m ()
206+todoEditable ix todoText = divClass "d-flex border-bottom" $ do
207+ updTodoDyn <- divClass "p-2 flex-grow-1 my-auto" $
208+ editTodoForm todoText
209+ divClass "p-2 btn-group" $ do
210+ (doneEl, _) <- elAttr' "button"
211+ ( "class" =: "btn btn-outline-secondary"
212+ <> "type" =: "button" ) $ text "Finish edit"
213+ let updTodos = \todo -> Endo $ update (Just . finishEdit todo) ix
214+ tellEvent $
215+ tagPromptlyDyn (updTodos <$> updTodoDyn) (domEvent Click doneEl)
216+
217+editTodoForm :: MonadWidget t m => Text -> m (Dynamic t Text)
218+editTodoForm todo = do
219+ editIEl <- inputElement $ def
220+ & initialAttributes .~
221+ ( "type" =: "text"
222+ <> "class" =: "form-control"
223+ <> "placeholder" =: "Todo")
224+ & inputElementConfig_initialValue .~ todo
225+ pure $ value editIEl
reflex-todo/todo-client/todo-client.cabalnew file+35
@@ -0,0 +1,35 @@
1+cabal-version: >=1.10
2+-- Initial package description 'todo-client.cabal' generated by 'cabal
3+-- init'. For further documentation, see
4+-- http://haskell.org/cabal/users-guide/
5+
6+name: todo-client
7+version: 0.1.0.0
8+-- synopsis:
9+-- description:
10+-- bug-reports:
11+-- license:
12+license-file: LICENSE
13+author: Anisimov Nikita
14+maintainer: anisimoff.nikita@gmail.com
15+-- copyright:
16+-- category:
17+build-type: Simple
18+extra-source-files: CHANGELOG.md
19+
20+executable todo-client
21+ main-is: Main.hs
22+ other-modules: GHCJS
23+ , JSFFI
24+ -- other-extensions:
25+ build-depends: base
26+ , containers
27+ , ghcjs-dom
28+ , lens
29+ , reflex-dom
30+ , reflex-dom-contrib
31+ , text
32+ , time
33+ , todo-common
34+ hs-source-dirs: src
35+ default-language: Haskell2010
reflex-todo/todo-common/CHANGELOG.mdnew file+5
@@ -0,0 +1,5 @@
1+# Revision history for todo-common
2+
3+## 0.1.0.0 -- YYYY-mm-dd
4+
5+* First version. Released on an unsuspecting world.
reflex-todo/todo-common/LICENSEnew file

No changes to the content.

reflex-todo/todo-common/Setup.hsnew file+2
@@ -0,0 +1,2 @@
1+import Distribution.Simple
2+main = defaultMain
reflex-todo/todo-common/src/Common.hsnew file+1
@@ -0,0 +1 @@
1+module Common where
reflex-todo/todo-common/todo-common.cabalnew file+26
@@ -0,0 +1,26 @@
1+cabal-version: >=1.10
2+-- Initial package description 'todo-common.cabal' generated by 'cabal
3+-- init'. For further documentation, see
4+-- http://haskell.org/cabal/users-guide/
5+
6+name: todo-common
7+version: 0.1.0.0
8+-- synopsis:
9+-- description:
10+-- bug-reports:
11+-- license:
12+license-file: LICENSE
13+author: Anisimov Nikita
14+maintainer: anisimoff.nikita@gmail.com
15+-- copyright:
16+-- category:
17+build-type: Simple
18+extra-source-files: CHANGELOG.md
19+
20+library
21+ exposed-modules: Common
22+ -- other-modules:
23+ -- other-extensions:
24+ build-depends: base
25+ hs-source-dirs: src
26+ default-language: Haskell2010
reflex-todo/todo-server/CHANGELOG.mdnew file+5
@@ -0,0 +1,5 @@
1+# Revision history for todo-server
2+
3+## 0.1.0.0 -- YYYY-mm-dd
4+
5+* First version. Released on an unsuspecting world.
reflex-todo/todo-server/Setup.hsnew file+2
@@ -0,0 +1,2 @@
1+import Distribution.Simple
2+main = defaultMain
reflex-todo/todo-server/src/Main.hsnew file+4
@@ -0,0 +1,4 @@
1+module Main where
2+
3+main :: IO ()
4+main = putStrLn "Hello, Haskell!"
reflex-todo/todo-server/todo-server.cabalnew file+28
@@ -0,0 +1,28 @@
1+cabal-version: >=1.10
2+-- Initial package description 'todo-server.cabal' generated by 'cabal
3+-- init'. For further documentation, see
4+-- http://haskell.org/cabal/users-guide/
5+
6+name: todo-server
7+version: 0.1.0.0
8+-- synopsis:
9+-- description:
10+-- bug-reports:
11+-- license:
12+license-file: LICENSE
13+author: Anisimov Nikita
14+maintainer: anisimoff.nikita@gmail.com
15+-- copyright:
16+-- category:
17+build-type: Simple
18+extra-source-files: CHANGELOG.md
19+
20+executable todo-server
21+ main-is: Main.hs
22+ -- other-modules:
23+ -- other-extensions:
24+ build-depends: base
25+ , reflex-dom
26+ , todo-common
27+ hs-source-dirs: src
28+ default-language: Haskell2010