packages feed

Shpadoinkle-examples-0.0.0.2: TODOMVC.hs

{-# LANGUAGE DeriveGeneric              #-}
{-# LANGUAGE ExtendedDefaultRules       #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedLabels           #-}
{-# LANGUAGE OverloadedStrings          #-}
{-# OPTIONS_GHC -fno-warn-type-defaults #-}


module Main where


import           Control.Lens                  hiding (view)
import           Data.Generics.Labels          ()
import           Data.String
import           Data.Text                     hiding (count, filter, length)
import           GHC.Generics
import           Prelude                       hiding (div, unwords)
import           Shpadoinkle
import           Shpadoinkle.Backend.ParDiff
import           Shpadoinkle.Html
import           Shpadoinkle.Html.LocalStorage
import           Shpadoinkle.Html.Memo
import           Shpadoinkle.Lens


default (Text)


newtype Description = Description { unDescription :: Text } deriving (Generic, Show, Read, Eq, IsString)
newtype TaskId      = TaskId      { unTaskId      :: Int  } deriving (Generic, Show, Read, Eq, Ord, Num)


data Completed  = Complete | Incomplete
  deriving (Generic, Show, Read, Eq)


data Visibility = All | Active | Completed
  deriving (Generic, Show, Read, Eq, Ord, Enum, Bounded)


data Task = Task
  { description :: Description
  , completed   :: Completed
  , taskId      :: TaskId
  } deriving (Generic, Show, Read, Eq)


data Model = Model
  { tasks      :: [Task]
  , editing    :: Maybe TaskId
  , visibility :: Visibility
  , current    :: Description
  } deriving (Generic, Show, Read, Eq)


emptyModel :: Model
emptyModel = Model [] Nothing All (Description "")


appendItem :: Model -> Model
appendItem m = if current m /= "" then m
  & #tasks   %~ (Task (current m) Incomplete newId :)
  & #current .~ "" else m
  where newId = Prelude.maximum (0 : (taskId <$> tasks m)) + 1


toggleCompleted :: TaskId -> Model -> Model
toggleCompleted tid m = m & #tasks . mapped %~ \t ->
  if taskId t == tid then t & #completed %~ negC else t
  where negC Complete   = Incomplete
        negC Incomplete = Complete


toggleEditing :: Maybe TaskId -> Model -> Model
toggleEditing t = #editing .~ t


updateTaskDescription :: TaskId -> Description -> Model -> Model
updateTaskDescription tid desc m = m & #tasks . mapped %~ \t ->
  if taskId t == tid then t & #description .~ desc else t


removeTask :: TaskId -> Model -> Model
removeTask tid m = m & #tasks %~ filter ((/= tid) . taskId)


toggleAll :: Model -> Model
toggleAll m = m & #tasks . traverse . #completed .~ c
  where c = if Prelude.all ((== Complete) . completed) $ tasks m then Incomplete else Complete


count :: Completed -> [Task] -> Int
count c = length . filter ((== c) . completed)


clearComplete :: Model -> Model
clearComplete = #tasks %~ filter ((== Incomplete) . completed)


toVisible :: Visibility -> [Task] -> [Task]
toVisible v = case v of
  All       -> id
  Active    -> filter $ (== Incomplete) . completed
  Completed -> filter $ (== Complete)   . completed


filterHtml :: Visibility -> Visibility -> Html m Visibility
filterHtml = memo2 $ \cur item -> li_
  [ a (href "#" : onClick item
        : [class' ("selected", cur == item)]) [ text . pack $ show item ]
  ]


htmlIfTasks :: Model -> [Html m a] -> [Html m a]
htmlIfTasks m h' = if Prelude.null (tasks m) then [] else h'


taskView :: Model -> Task -> Html m Model
taskView m = memo $ \(Task (Description d) c tid) ->
  li [ id' . pack . show $ unTaskId tid
     , class' [ ("completed", c == Complete)
              , ("editing", Just tid == editing m)
              ]
     ]
  [ div "view"
    [ input' [ type' "checkbox"
             , class' "toggle"
             , onChange $ toggleCompleted tid m
             , checked $ c == Complete
             ]
    , label [ onDblclick $ toggleEditing (Just tid) m ] [ text d ]
    , button' [ class' "destroy", onClick $ removeTask tid m ]
    ]
  , form [ onSubmit $ toggleEditing Nothing m ]
    [ input' [ class' "edit"
             , value d
             , onInput $ ($ m) . updateTaskDescription tid . Description
             , autofocus True
             , onBlur $ toggleEditing Nothing m
             ]
    ]
  ]


listFooter :: Functor m => Model -> Html m Model
listFooter model = footer "footer" $
  [ Shpadoinkle.Html.span "todo-count" $ let co = count Incomplete $ tasks model in
    [ strong_ [ text . pack $ show co ]
    , text $ " item" <> (if co == 1 then "" else "s") <> " left"
    ]
  , ul "filters" $ generalize #visibility .
      filterHtml (visibility model) <$> [minBound..maxBound]
  ] ++ (if count Complete (tasks model) == 0 then [] else
  [ button [ class' "clear-completed", onClick $ clearComplete model ] [ "Clear completed" ]
  ])



info :: Html m a
info = footer "info"
  [ p_ [ "Double-click to edit a todo" ]
  , p_ [ "Credits ", a [ href "https://twitter.com/fresheyeball" ] [ "Isaac Shapira" ] ]
  , p_ [ "Part of ", a [ href "http://todomvc.com" ] [ "TodoMVC" ] ]
  ]


newTaskForm :: Model -> Html m Model
newTaskForm model = form [ class' "todo-form", onSubmit (appendItem model) ]
  [ input' [ class' "new-todo"
           , value . unDescription $ current model
           , onInput $ ($ model) . (#current .~) . Description
           , placeholder "What needs to be done?" ]
  ]


todoList :: Model -> Html m Model
todoList model = ul "todo-list" $ taskView model <$> visibility model `toVisible` tasks model


toggleAllBtn :: Model -> [Html m Model]
toggleAllBtn model =
  [ input' [ id' "toggle-all", class' "toggle-all", type' "checkbox", onChange $ toggleAll model ]
  , label [ for' "toggle-all" ] [ "Mark all as complete" ]
  ]


view :: Functor m => Model -> Html m Model
view model = div_
  [ section "todoapp" $
    header "header"
      [ h1_ [ "todos" ], newTaskForm model ]
    : htmlIfTasks model
    [ section "main" $ toggleAllBtn model <> [ todoList model ]
    , listFooter model
    ]
  , info
  ]


app :: JSM ()
app = do
  model <- manageLocalStorage "todo" emptyModel
  initial <- readTVarIO model
  addStyle "https://cdn.jsdelivr.net/npm/todomvc-common@1.0.5/base.css"
  addStyle "https://cdn.jsdelivr.net/npm/todomvc-app-css@2.2.0/index.css"
  shpadoinkle id runParDiff initial model view getBody


main :: IO ()
main = do
  putStrLn "running app"
  runJSorWarp 8080 app