diff options
Diffstat (limited to 'frontend/app')
-rw-r--r-- | frontend/app/Api.hs | 64 | ||||
-rw-r--r-- | frontend/app/Main.hs | 244 | ||||
-rw-r--r-- | frontend/app/Page.hs | 43 | ||||
-rw-r--r-- | frontend/app/Page/ListCollection.hs | 70 | ||||
-rw-r--r-- | frontend/app/Route.hs | 30 | ||||
-rw-r--r-- | frontend/app/Schema.hs | 101 |
6 files changed, 353 insertions, 199 deletions
diff --git a/frontend/app/Api.hs b/frontend/app/Api.hs new file mode 100644 index 0000000..2a26c66 --- /dev/null +++ b/frontend/app/Api.hs @@ -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 diff --git a/frontend/app/Main.hs b/frontend/app/Main.hs index 9151705..7345a98 100644 --- a/frontend/app/Main.hs +++ b/frontend/app/Main.hs @@ -1,81 +1,31 @@ module Main where #ifndef ghcjs_HOST_OS -import Data.Maybe -import Data.String 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 -import Data.Aeson qualified as A -import Data.Aeson.Key qualified as AK -import Data.Aeson.KeyMap qualified as AM -import Data.ByteString.Lazy.Char8 qualified as LB +import Data.Bifunctor import Data.Default 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 Miso import Miso.String (toMisoString) import NeatInterpolation qualified as Q +import Page (Page, initialPage, updatePage, viewPage) +import Page qualified as Page +import Route (parseURI) data Model = Model - { schema :: Maybe (Either String Schema), - posts :: Maybe (Either String [A.Value]), - input :: Maybe A.Value + { page :: Maybe (Either String Page) } 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 = NoOp - | Init - | FetchSchema - | SetSchema (Either String Schema) - | FetchPosts - | SetPosts (Either String [A.Value]) - | FormChanged A.Value - | FormSubmitted A.Value + | Init URI + | HandleURI URI + | HandlePage Page.Action + | SetPage (Either String Page) deriving (Show, Eq) #ifndef ghcjs_HOST_OS @@ -87,92 +37,49 @@ runApp app = app #endif main :: IO () -main = runApp $ startApp App {..} +main = runApp $ do + uri <- getCurrentURI + startApp App {initialAction = Init uri, ..} where - initialAction = Init model = def update = updateModel view = viewModel events = defaultEvents - subs = [] + subs = [uriSub HandleURI] mountPoint = Nothing logLevel = Off updateModel :: Action -> Model -> Effect Action Model -updateModel action m = - case action of - 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 - const NoOp <$> consoleLog (toMisoString (A.encode output)) - -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 +updateModel NoOp m = noEff m +updateModel (Init uri) m = + m <# do + SetPage <$> initialPage (parseURI uri) +updateModel (HandleURI uri) m = + m <# do + let route = parseURI uri + SetPage <$> initialPage route +updateModel (SetPage page) m = noEff m {page = Just page} +updateModel (HandlePage action) m = + case m.page of + Just (Right page) -> + updatePage action page + & bimap HandlePage (\page -> m {page = Just (Right page)}) + _ -> noEff m viewModel :: Model -> View Action viewModel model = - let input = fromMaybe (A.Object AM.empty) model.input - in div_ [] $ - [ viewCss, - viewHeader, - nav_ [] [viewCollections], - main_ [] $ - [ maybe (text "..") (either err viewSchema) model.schema, - maybe (text "..") (either err viewPosts) model.posts, - maybe (text "..") (either err (viewForm input)) model.schema, - viewInput input - ] + div_ [] $ + [ viewCss, + viewHeader, + nav_ [] [viewCollections], + main_ [] $ + [ HandlePage + <$> maybe + (text "..") + (either err viewPage) + model.page ] + ] viewCss :: View Action viewCss = @@ -244,13 +151,13 @@ header section:first-child { ) ] -err :: String -> View Action +err :: String -> View action err = text . toMisoString . ("err! " <>) viewHeader :: View Action viewHeader = header_ [] $ - [ section_ [] [h1_ [] [text "acms"]], + [ section_ [] [h1_ [] [a_ [href_ "#"] [text "acms"]]], section_ [] [viewBranch] ] @@ -262,69 +169,8 @@ viewCollections :: View Action viewCollections = section_ [] $ [ span_ [] [text "collections"], - ol_ [] [li_ [] [a_ [href_ "#"] [text "posts"]]] - ] - -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) - -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))] + [ li_ [] [a_ [href_ "#collection/posts"] [text "posts"]], + li_ [] [a_ [href_ "#collection/posts1"] [text "posts1"]] + ] + ] diff --git a/frontend/app/Page.hs b/frontend/app/Page.hs new file mode 100644 index 0000000..84a551f --- /dev/null +++ b/frontend/app/Page.hs @@ -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 diff --git a/frontend/app/Page/ListCollection.hs b/frontend/app/Page/ListCollection.hs new file mode 100644 index 0000000..102973e --- /dev/null +++ b/frontend/app/Page/ListCollection.hs @@ -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))] diff --git a/frontend/app/Route.hs b/frontend/app/Route.hs new file mode 100644 index 0000000..36e1462 --- /dev/null +++ b/frontend/app/Route.hs @@ -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) diff --git a/frontend/app/Schema.hs b/frontend/app/Schema.hs new file mode 100644 index 0000000..e2d2e15 --- /dev/null +++ b/frontend/app/Schema.hs @@ -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 |