{-# LANGUAGE AllowAmbiguousTypes #-} | |
{-# LANGUAGE CPP #-} | |
{-# LANGUAGE DataKinds #-} | |
{-# LANGUAGE DuplicateRecordFields #-} | |
{-# LANGUAGE ExtendedDefaultRules #-} | |
{-# LANGUAGE FlexibleContexts #-} | |
{-# LANGUAGE FlexibleInstances #-} | |
{-# LANGUAGE ImpredicativeTypes #-} | |
{-# LANGUAGE InstanceSigs #-} | |
{-# LANGUAGE LambdaCase #-} | |
{-# LANGUAGE MultiParamTypeClasses #-} | |
{-# LANGUAGE OverloadedStrings #-} | |
{-# LANGUAGE RecordWildCards #-} | |
{-# LANGUAGE ScopedTypeVariables #-} | |
{-# LANGUAGE TypeApplications #-} | |
{-# LANGUAGE TypeFamilies #-} | |
{-# LANGUAGE TypeOperators #-} | |
{-# OPTIONS_GHC -fno-warn-type-defaults #-} | |
module View where | |
import Control.Lens hiding (view) | |
import Control.Lens.Unsound (lensProduct) | |
import Data.Coerce (Coercible) | |
import Data.Maybe (fromMaybe, isNothing) | |
import Data.String (IsString) | |
import Data.Text as T | |
import Shpadoinkle (Html, MonadJSM, text) | |
import qualified Shpadoinkle.Html as H | |
import Shpadoinkle.Lens ((<%), (<+)) | |
import Shpadoinkle.Router (navigate, toHydration) | |
import Shpadoinkle.Widgets.Form.Dropdown as Dropdown (Dropdown (..), | |
Theme (..), | |
defConfig, | |
dropdown) | |
import qualified Shpadoinkle.Widgets.Form.Input as Input | |
import Shpadoinkle.Widgets.Table as Table | |
import Shpadoinkle.Widgets.Types (Consideration, Considered, | |
ConsideredChoice, | |
Control (..), Field, | |
Hygiene (..), Input (..), | |
Pick (..), Present, | |
Selected, Status (..), | |
Toggle (..), Validated (..), | |
fullset, fuzzySearch, | |
getValid, humanize, | |
validate, withOptions') | |
import Types | |
default (Text, []) | |
toEditForm :: SpaceCraft -> SpaceCraftUpdate 'Edit | |
toEditForm sc = SpaceCraftUpdate | |
{ _sku = pure $ sc ^. sku | |
, _description = pure $ sc ^. description | |
, _serial = pure $ sc ^. serial | |
, _squadron = (sc ^. squadron) `withOptions'` fullset | |
, _operable = (sc ^. operable) `withOptions'` fullset | |
} | |
formGroup :: [Html m a] -> Html m a | |
formGroup = H.div "form-group row" | |
textControl | |
:: forall t m a | |
. Eq t => IsString t => Coercible Text t => MonadJSM m | |
=> (forall v. Lens' (a v) (Field v Text Input (Maybe t))) | |
-> Text -> a 'Errors -> a 'Edit -> Html m (a 'Edit) | |
textControl l msg errs ef = formGroup | |
[ H.label [ H.for' hName, H.class' "col-sm-2 col-form-label" ] [ text msg ] | |
, H.div "col-sm-10" $ | |
[ ef <% l . mapping (fromMaybe "" `iso` noEmpty) $ Input.text | |
[ H.name' hName | |
, H.className ("form-control":controlClass (errs ^. l) (ef ^. l .hygiene)) | |
] | |
] | |
<> invalid (errs ^. l) (ef ^. l . hygiene) | |
] where hName = toHtmlName msg | |
noEmpty "" = Nothing | |
noEmpty x = Just x | |
intControl | |
:: forall n m a | |
. MonadJSM m => Integral n => Show n | |
=> (forall v. Lens' (a v) (Field v Text Input n)) | |
-> Text -> a 'Errors -> a 'Edit -> Html m (a 'Edit) | |
intControl l msg errs ef = formGroup | |
[ H.label [ H.for' hName, H.class' "col-sm-2 col-form-label" ] [ text msg ] | |
, H.div "col-sm-10" $ | |
[ ef <% l $ Input.integral @m | |
$ [ H.name' hName, H.step "1", H.min "0" | |
, H.className ("form-control":controlClass (errs ^. l) (ef ^. l .hygiene)) | |
] | |
] | |
<> invalid (errs ^. l) (ef ^. l . hygiene) | |
] where hName = toHtmlName msg | |
selectControl | |
:: forall p x m a | |
. MonadJSM m => Control (Dropdown p) | |
=> Considered p ~ Maybe => Consideration ConsideredChoice p | |
=> Present (Selected p x) => Present x => Ord x | |
=> (forall v. Lens' (a v) (Field v Text (Dropdown p) x)) | |
-> Text -> a 'Errors -> a 'Edit -> Html m (a 'Edit) | |
selectControl l msg errs ef = formGroup | |
[ H.label [ H.for' (toHtmlName msg) | |
, H.class' "col-sm-2 col-form-label" ] [ text msg ] | |
, H.div "col-sm-10" $ | |
[ ef <% l $ dropdown bootstrap defConfig ] | |
<> invalid (errs ^. l) (ef ^. l . hygiene) | |
] | |
where | |
bootstrap Dropdown {..} = Dropdown.Theme | |
{ _wrapper = H.div | |
[ H.className [ ("dropdown", True) | |
, ("show", _toggle == Open) ] | |
] | |
, _header = pure . H.button | |
[ H.className ([ "btn", "btn-secondary", "dropdown-toggle" ] :: [Text]) | |
, H.type' "button" | |
] | |
, _list = H.div | |
[ H.className [ ("dropdown-menu", True) | |
, ("show", _toggle == Open) ] | |
] | |
, _item = H.a [ H.className "dropdown-item" | |
, H.textProperty "style" "cursor:pointer" ] | |
} | |
controlClass :: Validated e a -> Hygiene -> [Text] | |
controlClass (Invalid _ _) Dirty = ["is-invalid"] | |
controlClass (Validated _) Dirty = ["is-valid"] | |
controlClass _ Clean = [] | |
invalid :: Validated Text a -> Hygiene -> [ Html m b ] | |
invalid (Invalid err errs) Dirty = (\e -> H.div "invalid-feedback" [ text e ]) <$> err:errs | |
invalid _ _ = [] | |
toHtmlName :: Text -> Text | |
toHtmlName = toLower . replace " " "-" | |
editForm :: (CRUDSpaceCraft m, MonadJSM m) => Maybe SpaceCraftId -> SpaceCraftUpdate 'Edit -> Html m (SpaceCraftUpdate 'Edit) | |
editForm mid ef = H.div_ | |
[ intControl @SKU sku "SKU" errs ef | |
, textControl @Description description "Description" errs ef | |
, intControl @SerialNumber serial "Serial Number" errs ef | |
, selectControl @'One @Squadron squadron "Squadron" errs ef | |
, selectControl @'AtleastOne @Operable operable "Operable" errs ef | |
, H.div "d-flex flex-row justify-content-end" | |
[ H.button | |
[ H.onClick' (ef <$ navigate @SPA (RList mempty)) | |
, H.class' "btn btn-secondary" | |
] [ "Cancel" ] | |
, H.button | |
[ H.onClick' $ case isValid of | |
Nothing -> return ef | |
Just up -> do | |
case mid of Nothing -> () <$ createSpaceCraft up | |
Just sid -> updateSpaceCraft sid up | |
ef <$ navigate @SPA (RList mempty) | |
, H.class' "btn btn-primary" | |
, H.disabled $ isNothing isValid | |
] [ "Save" ] | |
] | |
] where errs = validate ef | |
isValid = getValid errs | |
start :: (Monad m, CRUDSpaceCraft m) => Route -> m Frontend | |
start = \case | |
RList s -> MList . Roster (SortCol SKUT ASC) s <$> listSpaceCraft | |
REcho t -> return $ MEcho t | |
RNew -> return $ MDetail Nothing emptyEditForm | |
RExisting i -> do | |
mcraft <- getSpaceCraft i | |
return $ case mcraft of | |
Just craft -> MDetail (Just i) $ toEditForm craft | |
_ -> M404 | |
tableCfg :: Table.Theme m [SpaceCraft] | |
tableCfg = mempty | |
{ tableProps = [ H.class' "table table-striped table-bordered" ] | |
, tdProps = \case | |
ToolsT -> [ H.width 1 ] | |
_ -> "align-middle" | |
} | |
fuzzy :: [SpaceCraft -> Text] | |
fuzzy = flip (^.) <$> | |
[ sku . to humanize | |
, description . to humanize | |
, serial . to humanize | |
, squadron . to humanize | |
, operable . to humanize | |
] | |
view :: (MonadJSM m, CRUDSpaceCraft m) => Frontend -> Html m Frontend | |
view fe = case fe of | |
MList r -> MList <$> H.div "container-fluid" | |
[ H.div "row justify-content-between align-items-center" | |
[ H.h2_ [ "Space Craft Roster" ] | |
, H.div [ H.class' "input-group" | |
, H.textProperty "style" ("width:300px" :: Text) | |
] | |
[ r <% search $ Input.search [ H.class' "form-control", H.placeholder "Search" ] | |
, H.div "input-group-append mr-3" | |
[ H.button [ H.onClick' (r <$ navigate @SPA RNew), H.class' "btn btn-primary" ] [ "Register" ] | |
] | |
] | |
] | |
, r <+ lensProduct table sort $ Table.viewWith tableCfg | |
(r ^. table . to (fuzzySearch fuzzy $ r ^. search . value)) | |
(r ^. sort) | |
] | |
MDetail sid form -> MDetail sid <$> H.div "row" | |
[ H.div "col-sm-8 offset-sm-2" | |
[ H.h2_ [ text $ maybe "Register New Space Craft" (const "Edit Space Craft") sid | |
] | |
, editForm sid form | |
] | |
] | |
MEcho t -> H.div_ | |
[ maybe (text "Erie silence") text t | |
, H.a [ H.onClick' (fe <$ navigate @SPA (RList $ Input Clean "")) ] [ "Go To Space Craft Roster" ] | |
] | |
M404 -> text "404" | |
template :: Frontend -> Html m a -> Html m a | |
template fe stage = H.html_ | |
[ H.head_ | |
[ H.link' | |
[ H.rel "stylesheet" | |
, H.href "https://cdn.usebootstrap.com/bootstrap/4.3.1/css/bootstrap.min.css" | |
] | |
, H.meta [ H.charset "ISO-8859-1" ] [] | |
, toHydration fe | |
, H.script [ H.src "/all.js" ] [] | |
] | |
, H.body_ | |
[ stage | |
] | |
] |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment