refactor pages

This commit is contained in:
Alexander Foremny
2024-06-04 14:36:26 +02:00
parent 03b019ca96
commit ed753b0410
7 changed files with 361 additions and 202 deletions

64
frontend/app/Api.hs Normal file
View File

@@ -0,0 +1,64 @@
{-# OPTIONS_GHC -fno-warn-incomplete-patterns #-}
module Api
( fetchSchema,
fetchPosts,
)
where
#ifndef ghcjs_HOST_OS
import Data.String
import Network.HTTP.Simple
#else
import Data.ByteString.Char8 qualified as B
import Data.Maybe
import Data.String
import JavaScript.Web.XMLHttpRequest
import Miso.String qualified as J
#endif
import Data.Aeson qualified as A
import Data.ByteString.Lazy.Char8 qualified as LB
import Data.Function
import Miso
import Schema
fetchSchema :: JSM (Either String Schema)
fetchSchema =
A.eitherDecode <$> fetch (fromString "http://localhost:8081/posts.schema.json")
fetchPosts :: JSM (Either String [A.Value])
fetchPosts =
A.eitherDecode
<$> fetch
( fromString "http://localhost:8081"
& setRequestMethod "POST"
& setRequestBodyLBS "SELECT posts FROM posts"
)
fetch :: Request -> JSM LB.ByteString
fetch req = LB.fromStrict . getResponseBody <$> httpBS req
#ifdef ghcjs_HOST_OS
httpBS :: Request -> JSM (Response B.ByteString)
httpBS req = xhrByteString req
instance IsString Request where
fromString uri =
Request
{ reqMethod = GET,
reqURI = J.pack uri,
reqLogin = Nothing,
reqHeaders = [],
reqWithCredentials = False,
reqData = NoData
}
setRequestMethod :: B.ByteString -> Request -> Request
setRequestMethod "POST" req = req {reqMethod = POST}
setRequestBodyLBS :: LB.ByteString -> Request -> Request
setRequestBodyLBS body req = req {reqData = StringData (J.pack (LB.unpack body))}
getResponseBody :: Response B.ByteString -> B.ByteString
getResponseBody = fromMaybe "" . contents
#endif

View File

@@ -1,81 +1,31 @@
module Main where module Main where
#ifndef ghcjs_HOST_OS #ifndef ghcjs_HOST_OS
import Data.Maybe
import Data.String
import Language.Javascript.JSaddle.Warp as JSaddle import Language.Javascript.JSaddle.Warp as JSaddle
import Network.HTTP.Simple
#else
import Data.ByteString.Char8 qualified as B
import Data.Maybe
import Data.String
import JavaScript.Web.XMLHttpRequest
import Miso.String qualified as J
#endif #endif
import Data.Aeson qualified as A import Data.Bifunctor
import Data.Aeson.Key qualified as AK
import Data.Aeson.KeyMap qualified as AM
import Data.ByteString.Lazy.Char8 qualified as LB
import Data.Default import Data.Default
import Data.Function import Data.Function
import Data.List
import Data.Map qualified as M
import Data.Text qualified as T
import Form qualified as F
import GHC.Generics (Generic) import GHC.Generics (Generic)
import Miso import Miso
import Miso.String (toMisoString) import Miso.String (toMisoString)
import NeatInterpolation qualified as Q import NeatInterpolation qualified as Q
import Page (Page, initialPage, updatePage, viewPage)
import Page qualified as Page
import Route (parseURI)
data Model = Model data Model = Model
{ schema :: Maybe (Either String Schema), { page :: Maybe (Either String Page)
posts :: Maybe (Either String [A.Value]),
input :: Maybe A.Value
} }
deriving (Show, Eq, Generic, Default) deriving (Show, Eq, Generic, Default)
data Schema = Schema
{ id :: String,
schema :: String,
title :: String,
type_ :: SchemaType
}
deriving (Show, Eq)
instance A.FromJSON Schema where
parseJSON =
A.withObject
"Schema"
( \v ->
Schema
<$> v A..: "$id"
<*> v A..: "$schema"
<*> v A..: "title"
<*> A.parseJSON (A.Object v)
)
data SchemaType = Object (M.Map String String)
deriving (Show, Eq)
instance A.FromJSON SchemaType where
parseJSON =
A.withObject
"SchemaType"
( \v ->
v A..: "type" >>= \case
("object" :: String) -> Object <$> v A..: "properties"
)
data Action data Action
= NoOp = NoOp
| Init | Init URI
| FetchSchema | HandleURI URI
| SetSchema (Either String Schema) | HandlePage Page.Action
| FetchPosts | SetPage (Either String Page)
| SetPosts (Either String [A.Value])
| FormChanged A.Value
| FormSubmitted A.Value
deriving (Show, Eq) deriving (Show, Eq)
#ifndef ghcjs_HOST_OS #ifndef ghcjs_HOST_OS
@@ -87,90 +37,47 @@ runApp app = app
#endif #endif
main :: IO () main :: IO ()
main = runApp $ startApp App {..} main = runApp $ do
uri <- getCurrentURI
startApp App {initialAction = Init uri, ..}
where where
initialAction = Init
model = def model = def
update = updateModel update = updateModel
view = viewModel view = viewModel
events = defaultEvents events = defaultEvents
subs = [] subs = [uriSub HandleURI]
mountPoint = Nothing mountPoint = Nothing
logLevel = Off logLevel = Off
updateModel :: Action -> Model -> Effect Action Model updateModel :: Action -> Model -> Effect Action Model
updateModel action m = updateModel NoOp m = noEff m
case action of updateModel (Init uri) m =
NoOp -> noEff m
Init -> batchEff m [pure FetchSchema, pure FetchPosts]
FetchSchema -> m <# do SetSchema <$> fetchSchema
SetSchema schema ->
let setSchema :: Either String Schema -> Model -> Model
setSchema schema m = m {schema = Just schema}
in noEff (setSchema schema m)
FetchPosts -> m <# do SetPosts <$> fetchPosts
SetPosts posts ->
let setPosts :: Either String [A.Value] -> Model -> Model
setPosts posts m = m {posts = Just posts}
in noEff (setPosts posts m)
FormChanged (Just -> input) -> noEff m {input}
FormSubmitted output ->
m <# do m <# do
const NoOp <$> consoleLog (toMisoString (A.encode output)) SetPage <$> initialPage (parseURI uri)
updateModel (HandleURI uri) m =
fetchSchema :: JSM (Either String Schema) m <# do
fetchSchema = let route = parseURI uri
A.eitherDecode <$> fetch (fromString "http://localhost:8081/posts.schema.json") SetPage <$> initialPage route
updateModel (SetPage page) m = noEff m {page = Just page}
fetchPosts :: JSM (Either String [A.Value]) updateModel (HandlePage action) m =
fetchPosts = case m.page of
A.eitherDecode Just (Right page) ->
<$> fetch updatePage action page
( fromString "http://localhost:8081" & bimap HandlePage (\page -> m {page = Just (Right page)})
& setRequestMethod "POST" _ -> noEff m
& setRequestBodyLBS "SELECT posts FROM posts"
)
fetch :: Request -> JSM LB.ByteString
fetch req = LB.fromStrict . getResponseBody <$> httpBS req
#ifdef ghcjs_HOST_OS
httpBS :: Request -> JSM (Response B.ByteString)
httpBS req = xhrByteString req
instance IsString Request where
fromString uri =
Request
{ reqMethod = GET,
reqURI = J.pack uri,
reqLogin = Nothing,
reqHeaders = [],
reqWithCredentials = False,
reqData = NoData
}
setRequestMethod :: B.ByteString -> Request -> Request
setRequestMethod "POST" req = req {reqMethod = POST}
setRequestBodyLBS :: LB.ByteString -> Request -> Request
setRequestBodyLBS body req = req {reqData = StringData (J.pack (LB.unpack body))}
getResponseBody :: Response B.ByteString -> B.ByteString
getResponseBody = fromMaybe "" . contents
#endif
viewModel :: Model -> View Action viewModel :: Model -> View Action
viewModel model = viewModel model =
let input = fromMaybe (A.Object AM.empty) model.input div_ [] $
in div_ [] $
[ viewCss, [ viewCss,
viewHeader, viewHeader,
nav_ [] [viewCollections], nav_ [] [viewCollections],
main_ [] $ main_ [] $
[ maybe (text "..") (either err viewSchema) model.schema, [ HandlePage
maybe (text "..") (either err viewPosts) model.posts, <$> maybe
maybe (text "..") (either err (viewForm input)) model.schema, (text "..")
viewInput input (either err viewPage)
model.page
] ]
] ]
@@ -244,13 +151,13 @@ header section:first-child {
) )
] ]
err :: String -> View Action err :: String -> View action
err = text . toMisoString . ("err! " <>) err = text . toMisoString . ("err! " <>)
viewHeader :: View Action viewHeader :: View Action
viewHeader = viewHeader =
header_ [] $ header_ [] $
[ section_ [] [h1_ [] [text "acms"]], [ section_ [] [h1_ [] [a_ [href_ "#"] [text "acms"]]],
section_ [] [viewBranch] section_ [] [viewBranch]
] ]
@@ -262,69 +169,8 @@ viewCollections :: View Action
viewCollections = viewCollections =
section_ [] $ section_ [] $
[ span_ [] [text "collections"], [ span_ [] [text "collections"],
ol_ [] [li_ [] [a_ [href_ "#"] [text "posts"]]]
]
viewSchema :: Schema -> View Action
viewSchema schema =
case schema.type_ of
Object properties ->
ol_ [] $ ol_ [] $
( \(k, v) -> [ li_ [] [a_ [href_ "#collection/posts"] [text "posts"]],
li_ [] $ li_ [] [a_ [href_ "#collection/posts1"] [text "posts1"]]
[ text (toMisoString k), ]
text ":",
text (toMisoString v)
] ]
)
<$> (M.toList properties)
viewForm :: A.Value -> Schema -> View Action
viewForm input =
fmap (either FormChanged FormSubmitted)
. flip F.runForm input
. schemaForm
viewInput :: A.Value -> View Action
viewInput input =
pre_ [] [text (toMisoString (A.encode input))]
schemaForm :: Schema -> F.Form A.Value A.Value
schemaForm schema =
fmap mergeJson . sequence $
case schema.type_ of
Object properties ->
( \(AK.fromString -> k, "string") ->
A.Object . AM.singleton k
<$> ( F.mapValues (getO k) (setO k) $
jsonString (AK.toString k)
)
)
<$> (M.toList properties)
mergeJson :: [A.Value] -> A.Value
mergeJson = foldl' mergeObject (A.Object AM.empty)
mergeObject :: A.Value -> A.Value -> A.Value
mergeObject (A.Object kvs) (A.Object kvs') = A.Object (AM.union kvs kvs')
fromJson :: A.Value -> T.Text
fromJson (A.String x) = x
fromJson _ = ""
toJson :: T.Text -> A.Value -> A.Value
toJson x _ = A.String x
getO :: AK.Key -> A.Value -> A.Value
getO k (A.Object kvs) = fromMaybe A.Null (AM.lookup k kvs)
setO :: AK.Key -> A.Value -> A.Value -> A.Value
setO k v (A.Object kvs) = A.Object (AM.insert k v kvs)
jsonString :: String -> F.Form A.Value A.Value
jsonString = fmap A.String . F.mapValues fromJson toJson . F.string
viewPosts :: [A.Value] -> View Action
viewPosts posts = ol_ [] (viewPost <$> posts)
where
viewPost post = pre_ [] [text (toMisoString (A.encode post))]

43
frontend/app/Page.hs Normal file
View File

@@ -0,0 +1,43 @@
module Page
( Page (..),
Action,
initialPage,
updatePage,
viewPage,
)
where
import Data.Bifunctor
import Data.Default
import Data.Function
import Miso
import Page.ListCollection qualified as ListCollection
import Route (Route)
import Route qualified as Route
data Page
= Home
| ListCollection ListCollection.Model
deriving (Show, Eq)
instance Default Page where
def = Home
data Action
= HandleListCollection ListCollection.Action
deriving (Show, Eq)
initialPage :: Route -> JSM (Either String Page)
initialPage Route.Home = pure (Right Home)
initialPage (Route.ListCollection c) =
fmap ListCollection <$> ListCollection.initialModel c
updatePage :: Action -> Page -> Effect Action Page
updatePage (HandleListCollection action) (ListCollection m) =
ListCollection.updateModel action m
& bimap HandleListCollection ListCollection
updatePage (HandleListCollection _) p = noEff p
viewPage :: Page -> View Action
viewPage Home = text "home"
viewPage (ListCollection m) = HandleListCollection <$> ListCollection.viewModel m

View File

@@ -0,0 +1,70 @@
module Page.ListCollection
( Model,
initialModel,
Action,
updateModel,
viewModel,
)
where
import Api
import Data.Aeson qualified as A
import Data.Aeson.KeyMap qualified as AM
import Form qualified as F
import Miso
import Miso.String (toMisoString)
import Schema
data Model = Model
{ collection :: String,
input :: A.Value,
schema :: Schema,
posts :: [A.Value]
}
deriving (Show, Eq)
initialModel :: String -> JSM (Either String Model)
initialModel collection = do
schema' <- fetchSchema
posts' <- fetchPosts
pure do
schema <- schema'
posts <- posts'
pure $ Model {input = A.Object AM.empty, ..}
data Action
= NoOp
| FormChanged A.Value
| FormSubmitted A.Value
deriving (Eq, Show)
updateModel :: Action -> Model -> Effect Action Model
updateModel NoOp m = noEff m
updateModel (FormChanged input) m = noEff m {input}
updateModel (FormSubmitted output) m =
m <# do
const NoOp <$> consoleLog (toMisoString (A.encode output))
viewModel :: Model -> View Action
viewModel m =
div_ [] $
[ viewSchema m.schema,
viewPosts m.posts,
viewForm m.input m.schema,
viewInput m.input
]
viewForm :: A.Value -> Schema -> View Action
viewForm input =
fmap (either FormChanged FormSubmitted)
. flip F.runForm input
. schemaForm
viewInput :: A.Value -> View Action
viewInput input =
pre_ [] [text (toMisoString (A.encode input))]
viewPosts :: [A.Value] -> View Action
viewPosts posts = ol_ [] (viewPost <$> posts)
where
viewPost post = pre_ [] [text (toMisoString (A.encode post))]

30
frontend/app/Route.hs Normal file
View File

@@ -0,0 +1,30 @@
module Route
( Route (..),
parseURI,
)
where
import Data.Attoparsec.Text qualified as P
import Data.Default
import Data.Text qualified as T
import Miso
data Route
= Home
| ListCollection String
deriving (Show, Eq)
instance Default Route where
def = Home
parseURI :: URI -> Route
parseURI uri =
either (const def) id $
P.parseOnly
( P.choice
[ ListCollection <$> (P.string "#collection/" *> P.many1 P.anyChar),
pure Home
]
<* P.endOfInput
)
(T.pack uri.uriFragment)

101
frontend/app/Schema.hs Normal file
View File

@@ -0,0 +1,101 @@
{-# OPTIONS_GHC -fno-warn-incomplete-patterns #-}
{-# OPTIONS_GHC -fno-warn-incomplete-uni-patterns #-}
module Schema
( Schema,
viewSchema,
schemaForm,
)
where
import Data.Aeson qualified as A
import Data.Aeson.Key qualified as AK
import Data.Aeson.KeyMap qualified as AM
import Data.List
import Data.Map qualified as M
import Data.Maybe
import Data.Text qualified as T
import Form qualified as F
import Miso
import Miso.String (toMisoString)
data Schema = Schema
{ id :: String,
schema :: String,
title :: String,
type_ :: SchemaType
}
deriving (Show, Eq)
instance A.FromJSON Schema where
parseJSON =
A.withObject
"Schema"
( \v ->
Schema
<$> v A..: "$id"
<*> v A..: "$schema"
<*> v A..: "title"
<*> A.parseJSON (A.Object v)
)
data SchemaType = Object (M.Map String String)
deriving (Show, Eq)
instance A.FromJSON SchemaType where
parseJSON =
A.withObject
"SchemaType"
( \v ->
v A..: "type" >>= \case
("object" :: String) -> Object <$> v A..: "properties"
)
viewSchema :: Schema -> View action
viewSchema schema =
case schema.type_ of
Object properties ->
ol_ [] $
( \(k, v) ->
li_ [] $
[ text (toMisoString k),
text ":",
text (toMisoString v)
]
)
<$> (M.toList properties)
schemaForm :: Schema -> F.Form A.Value A.Value
schemaForm schema =
fmap mergeJson . sequence $
case schema.type_ of
Object properties ->
( \(AK.fromString -> k, "string") ->
A.Object . AM.singleton k
<$> ( F.mapValues (getO k) (setO k) $
jsonString (AK.toString k)
)
)
<$> (M.toList properties)
mergeJson :: [A.Value] -> A.Value
mergeJson = foldl' mergeObject (A.Object AM.empty)
mergeObject :: A.Value -> A.Value -> A.Value
mergeObject (A.Object kvs) (A.Object kvs') = A.Object (AM.union kvs kvs')
fromJson :: A.Value -> T.Text
fromJson (A.String x) = x
fromJson _ = ""
toJson :: T.Text -> A.Value -> A.Value
toJson x _ = A.String x
getO :: AK.Key -> A.Value -> A.Value
getO k (A.Object kvs) = fromMaybe A.Null (AM.lookup k kvs)
setO :: AK.Key -> A.Value -> A.Value -> A.Value
setO k v (A.Object kvs) = A.Object (AM.insert k v kvs)
jsonString :: String -> F.Form A.Value A.Value
jsonString = fmap A.String . F.mapValues fromJson toJson . F.string

View File

@@ -12,23 +12,28 @@ executable frontend
main-is: Main.hs main-is: Main.hs
hs-source-dirs: app hs-source-dirs: app
other-modules: other-modules:
Api
Form Form
Form.Input Form.Input
Form.Internal Form.Internal
Page
Page.ListCollection
Route
Schema
default-language: GHC2021 default-language: GHC2021
default-extensions: default-extensions:
CPP OverloadedStrings RecordWildCards DeriveAnyClass CPP OverloadedStrings RecordWildCards DeriveAnyClass
DuplicateRecordFields LambdaCase OverloadedRecordDot DuplicateRecordFields LambdaCase OverloadedRecordDot
NoFieldSelectors ViewPatterns QuasiQuotes NoFieldSelectors ViewPatterns QuasiQuotes BlockArguments
ghc-options: ghc-options:
-Wall -fno-warn-name-shadowing -fno-warn-ambiguous-fields -Wall -fno-warn-name-shadowing -fno-warn-ambiguous-fields
-fno-warn-incomplete-patterns -fno-warn-orphans -fno-warn-orphans
-fno-warn-incomplete-uni-patterns
build-depends: build-depends:
aeson, aeson,
attoparsec,
base, base,
bytestring, bytestring,
containers, containers,