diff --git a/examples/haskell-miso.org/ChangeLog.md b/examples/haskell-miso.org/ChangeLog.md new file mode 100644 index 00000000..a86c27d3 --- /dev/null +++ b/examples/haskell-miso.org/ChangeLog.md @@ -0,0 +1,5 @@ +# Revision history for haskell-miso + +## 0.1.0.0 -- YYYY-mm-dd + +* First version. Released on an unsuspecting world. diff --git a/examples/haskell-miso.org/LICENSE b/examples/haskell-miso.org/LICENSE new file mode 100644 index 00000000..70b828a0 --- /dev/null +++ b/examples/haskell-miso.org/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2017, David Johnson + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of David Johnson nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/examples/haskell-miso.org/Setup.hs b/examples/haskell-miso.org/Setup.hs new file mode 100644 index 00000000..9a994af6 --- /dev/null +++ b/examples/haskell-miso.org/Setup.hs @@ -0,0 +1,2 @@ +import Distribution.Simple +main = defaultMain diff --git a/examples/haskell-miso.org/client/Main.hs b/examples/haskell-miso.org/client/Main.hs new file mode 100644 index 00000000..cf34e5a6 --- /dev/null +++ b/examples/haskell-miso.org/client/Main.hs @@ -0,0 +1,39 @@ +{-# LANGUAGE RecordWildCards #-} +module Main where + +import Common +import Data.Proxy +import Network.URI + +import Miso +import Miso.String + +instance HasURI Model where + lensURI = makeLens getter setter + where + getter = uri + setter = \m u -> m { uri = u } + +main :: IO () +main = do + currentURI <- getCurrentURI + miso App { model = Model currentURI, ..} + where + initialAction = NoOp + update = updateModel + events = defaultEvents + subs = [ uriSub HandleURI ] + view m = + either (const $ the404 m) id $ + runRoute (Proxy :: Proxy ClientRoutes) handlers m + +updateModel :: Action -> Model -> Effect Model Action +updateModel (HandleURI u) m = m { uri = u } <# do + pure NoOp +updateModel (ChangeURI u) m = m <# do + pushURI u + pure NoOp +updateModel Alert m@Model{..} = m <# do + alert $ pack (show uri) + pure NoOp +updateModel NoOp m = noEff m diff --git a/examples/haskell-miso.org/client/default.nix b/examples/haskell-miso.org/client/default.nix new file mode 100644 index 00000000..285ac714 --- /dev/null +++ b/examples/haskell-miso.org/client/default.nix @@ -0,0 +1,12 @@ +{ mkDerivation, aeson, base, containers, miso, servant, stdenv, network-uri }: +mkDerivation { + pname = "haskell-miso"; + version = "0.1.0.0"; + src = ../.; + isLibrary = false; + isExecutable = true; + executableHaskellDepends = [ aeson base containers miso servant network-uri ]; + homepage = "https://haskell-miso.org"; + description = "https://haskell-miso.org"; + license = stdenv.lib.licenses.bsd3; +} diff --git a/examples/haskell-miso.org/default.nix b/examples/haskell-miso.org/default.nix new file mode 100644 index 00000000..095760e3 --- /dev/null +++ b/examples/haskell-miso.org/default.nix @@ -0,0 +1,16 @@ +{ pkgs ? import {} }: +let + inherit (pkgs) runCommand closurecompiler; + inherit (pkgs.haskell.packages) ghcjs ghc802; + miso-ghc = ghc802.callPackage ./../../miso-ghc.nix { }; + miso-ghcjs = ghcjs.callPackage ./../../miso-ghcjs.nix { }; + client = ghcjs.callPackage ./client { miso = miso-ghcjs; }; + server = ghc802.callPackage ./server { miso = miso-ghc; }; +in + runCommand "haskell-miso.org" { inherit client server; } '' + mkdir -p $out/{bin,static} + cp ${client}/bin/client.jsexe/all.js $out/static/all.js + cp ${server}/bin/* $out/bin + '' + + diff --git a/examples/haskell-miso.org/haskell-miso.cabal b/examples/haskell-miso.org/haskell-miso.cabal new file mode 100644 index 00000000..b203b08f --- /dev/null +++ b/examples/haskell-miso.org/haskell-miso.cabal @@ -0,0 +1,60 @@ +name: haskell-miso +version: 0.1.0.0 +synopsis: https://haskell-miso.org +description: Website for the Miso web framework +homepage: https://haskell-miso.org +license: BSD3 +license-file: LICENSE +author: David Johnson +maintainer: djohnson.m@gmail.com +copyright: David Johnson (c) 2017 +category: Web +build-type: Simple +extra-source-files: ChangeLog.md +cabal-version: >=1.10 + +executable server + main-is: + Main.hs + if impl(ghcjs) + buildable: False + else + ghc-options: + -O2 -threaded -Wall -rtsopts + hs-source-dirs: + server, shared + build-depends: + aeson, + base < 5, + containers, + http-types, + lucid, + miso, + mtl, + network-uri, + servant, + servant-lucid, + servant-server, + wai, + wai-extra, + warp + default-language: + Haskell2010 + +executable client + main-is: + Main.hs + if !impl(ghcjs) + buildable: False + else + hs-source-dirs: + client, shared + build-depends: + aeson, + base < 5, + containers, + network-uri, + miso, + servant + default-language: + Haskell2010 diff --git a/examples/haskell-miso.org/nix/config.nix b/examples/haskell-miso.org/nix/config.nix new file mode 100644 index 00000000..6d4ea08b --- /dev/null +++ b/examples/haskell-miso.org/nix/config.nix @@ -0,0 +1,24 @@ +{ pkgs, config, ... }: +{ + imports = [ ./module.nix ]; + nixpkgs.config.packageOverrides = pkgs: { + haskell-miso = import ./../default.nix {}; + }; + services = { + haskell-miso.enable = true; + nginx = { + enable = true; + virtualHosts = { + "haskell-miso.org" = { + forceSSL = true; + enableACME = true; + locations = { + "/" = { + proxyPass = "http://localhost:3002"; + }; + }; + }; + }; + }; + }; +} diff --git a/examples/haskell-miso.org/nix/module.nix b/examples/haskell-miso.org/nix/module.nix new file mode 100644 index 00000000..39e82886 --- /dev/null +++ b/examples/haskell-miso.org/nix/module.nix @@ -0,0 +1,25 @@ +{ options, lib, config, pkgs, modulesPath }: +let + cfg = config.services.haskell-miso; +in { + options.services.haskell-miso.enable = lib.mkEnableOption "Enable the haskell-miso.org service"; + config = lib.mkIf cfg.enable { + systemd.services.haskell-miso = { + path = with pkgs; [ haskell-miso bash ]; + wantedBy = [ "multi-user.target" ]; + script = '' + ./bin/server +RTS -N -A4M -RTS + ''; + description = '' + https://haskell-miso.org + ''; + serviceConfig = { + WorkingDirectory=pkgs.haskell-miso; + KillSignal="INT"; + Type = "simple"; + Restart = "on-abort"; + RestartSec = "10"; + }; + }; + }; +} diff --git a/examples/haskell-miso.org/server/Main.hs b/examples/haskell-miso.org/server/Main.hs new file mode 100644 index 00000000..a8c19412 --- /dev/null +++ b/examples/haskell-miso.org/server/Main.hs @@ -0,0 +1,123 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE PolyKinds #-} +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE TypeOperators #-} +{-# LANGUAGE TypeApplications #-} +module Main where + +import Common +import Data.Proxy +import qualified Lucid as L +import Lucid.Base +import Network.HTTP.Types +import Network.Wai +import Network.Wai.Handler.Warp +import Network.Wai.Middleware.Gzip +import Network.Wai.Middleware.RequestLogger +import Servant +import qualified System.IO as IO + +import Miso +import Miso.String + +main :: IO () +main = do + IO.hPutStrLn IO.stderr "Running on port 3002..." + run 3002 $ logStdout (compress app) + where + compress = gzip def { gzipFiles = GzipCompress } + +app :: Application +app = serve (Proxy @ API) (static :<|> serverHandlers :<|> handle404) + where + static = serveDirectory "static" + +-- | Wrapper for setting HTML doctype and header +newtype Wrapper a = Wrapper a + deriving (Show, Eq) + +-- | Convert client side routes into server-side web handlers +type ServerRoutes = ToServerRoutes ClientRoutes Wrapper Action + +-- | API type +type API = ("static" :> Raw) + :<|> ServerRoutes + :<|> Raw + +handle404 :: Application +handle404 _ respond = respond $ responseLBS + status404 + [("Content-Type", "text/html")] $ + renderBS $ toHtml $ Wrapper $ the404 Model { uri = goHome } + +instance L.ToHtml a => L.ToHtml (Wrapper a) where + toHtmlRaw = L.toHtml + toHtml (Wrapper x) = + L.doctypehtml_ $ do + L.head_ $ do + L.meta_ [L.charset_ "utf-8"] + L.meta_ [ L.httpEquiv_ "X-UA-Compatible" + , L.content_ "IE=edge" + ] + L.meta_ [ L.name_ "viewport" + , L.content_ "width=device-width, initial-scale=1" + ] + L.meta_ [ L.name_ "description" + , L.content_ "Miso is a small isomorphic Haskell front-end framework featuring a virtual-dom, diffing / patching algorithm, event delegation, event batching, SVG, Server-sent events, Websockets, type-safe servant-style routing and an extensible Subscription-based subsystem. Inspired by Elm, Redux and Bobril. Miso is pure by default, but side effects (like XHR) can be introduced into the system via the Effect data type. Miso makes heavy use of the GHCJS FFI and therefore has minimal dependencies." + ] + cssRef animateRef + cssRef bulmaRef + cssRef fontAwesomeRef + jsRef "https://buttons.github.io/buttons.js" + L.script_ analytics + jsRef "static/all.js" + L.body_ (L.toHtml x) + where + jsRef href = + L.with (L.script_ mempty) + [ makeAttribute "src" href + , makeAttribute "async" mempty + , makeAttribute "defer" mempty + ] + cssRef href = + L.with (L.link_ mempty) [ + L.rel_ "stylesheet" + , L.type_ "text/css" + , L.href_ href + ] + +fontAwesomeRef :: MisoString +fontAwesomeRef = "https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" + +animateRef :: MisoString +animateRef = "https://cdnjs.cloudflare.com/ajax/libs/animate.css/3.5.2/animate.min.css" + +bulmaRef :: MisoString +bulmaRef = "https://cdnjs.cloudflare.com/ajax/libs/bulma/0.4.3/css/bulma.min.css" + +analytics :: MisoString +analytics = + "(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){\ + \(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),\ + \m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)\ + \})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');\ + \ga('create', 'UA-102668481-1', 'auto');\ + \ga('send', 'pageview');" + +serverHandlers :: + Handler (Wrapper (View Action)) + :<|> Handler (Wrapper (View Action)) + :<|> Handler (Wrapper (View Action)) + :<|> Handler (Wrapper (View Action)) +serverHandlers = examplesHandler + :<|> docsHandler + :<|> communityHandler + :<|> homeHandler + where + send f u = pure $ Wrapper $ f Model {uri = u} + homeHandler = send home goHome + examplesHandler = send examples goExamples + docsHandler = send docs goDocs + communityHandler = send community goCommunity + diff --git a/examples/haskell-miso.org/server/default.nix b/examples/haskell-miso.org/server/default.nix new file mode 100644 index 00000000..0ab92391 --- /dev/null +++ b/examples/haskell-miso.org/server/default.nix @@ -0,0 +1,18 @@ +{ mkDerivation, aeson, base, containers, lucid, miso, mtl +, network-uri, servant, servant-lucid, servant-server, stdenv, time +, wai-extra, warp +}: +mkDerivation { + pname = "haskell-miso"; + version = "0.1.0.0"; + src = ../.; + isLibrary = false; + isExecutable = true; + executableHaskellDepends = [ + aeson base containers lucid miso mtl network-uri servant + servant-lucid servant-server time wai-extra warp + ]; + homepage = "https://haskell-miso.org"; + description = "https://haskell-miso.org"; + license = stdenv.lib.licenses.bsd3; +} diff --git a/examples/haskell-miso.org/shared/Common.hs b/examples/haskell-miso.org/shared/Common.hs new file mode 100644 index 00000000..d7d70535 --- /dev/null +++ b/examples/haskell-miso.org/shared/Common.hs @@ -0,0 +1,655 @@ +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} +{-# LANGUAGE DataKinds #-} +module Common where + +import Data.Bool +import qualified Data.Map as M +import Data.Monoid +import Data.Proxy +import Network.URI +import Servant.API + +import Miso +import Miso.String + +-- | We can pretty much share everything +-- +-- model, action, view, router, links, events map +-- decoders are all shareable + +-- | Model +data Model = Model + { uri :: URI + } deriving (Show, Eq) + +-- | Event Actions +data Action + = Alert + | ChangeURI URI + | HandleURI URI + | NoOp + deriving (Show, Eq) + +-- | Router +type ClientRoutes = Examples + :<|> Docs + :<|> Community + :<|> Home + +-- | Handlers +handlers :: + (Model -> View Action) + :<|> ((Model -> View Action) + :<|> ((Model -> View Action) :<|> (Model -> View Action))) +handlers = examples + :<|> docs + :<|> community + :<|> home + +-- | Client Routes +type Examples = "examples" :> View Action +type Docs = "docs" :> View Action +type Community = "community" :> View Action +type Home = View Action + +-- | Views +community :: Model -> View Action +community = template v + where + v = div_ [ class_ $ pack "animated fadeIn" ] [ img_ [ + width_ $ pack "100" + , class_ $ pack "animated bounceInDown" + , src_ misoSrc + ] [ ] + , h1_ [ class_ $ pack "title animated pulse" + , style_ $ M.fromList [(pack "font-size", pack "82px") + ,(pack "font-weight", pack "100") + ] + ] [ text "community" ] + , h2_ [ class_ $ pack "subtitle animated pulse" ] [ + a_ [ href_ $ pack "https://haskell-miso-slack.herokuapp.com/" + , target_ $ pack "_blank" + ] + [ text "Slack" ] + , text " / " + , a_ [ href_ $ pack "https://www.irccloud.com/invite?channel=%23haskell-miso&hostname=irc.freenode.net&port=6697&ssl=1" + , target_ $ pack "_blank" + ] [ text "#haskell-miso" ] + ] + ] + +docs :: Model -> View Action +docs = template v + where + v = div_ [ class_ $ pack "animated fadeIn" ] [ img_ [ + width_ $ pack "100" + , class_ $ pack "animated bounceInDown" + , src_ misoSrc + ] [ ] + , h1_ [ class_ $ pack "title animated pulse" + , style_ $ M.fromList [(pack "font-size", pack "82px") + ,(pack "font-weight", pack "100") + ] + ] [ text "docs" ] + , h2_ [ class_ $ pack "subtitle animated pulse" ] [ + a_ [ href_ $ pack "https://haddocks.haskell-miso.org/" + , target_ $ pack "_blank" + ] + [ text "Haddocks" ] + , text " / " + , a_ [ href_ $ pack "https://github.com/dmjio/miso/blob/master/README.md" + , target_ $ pack "_blank" + ] + [ text "README" ] + ] + ] + +misoSrc :: MisoString +misoSrc = pack "https://camo.githubusercontent.com/d6641458f09e24e8fef783de8278886949085960/68747470733a2f2f656d6f6a6970656469612d75732e73332e616d617a6f6e6177732e636f6d2f7468756d62732f3234302f6170706c652f39362f737465616d696e672d626f776c5f31663335632e706e67" + +examples :: Model -> View Action +examples = template v + where + v = + div_ [ class_ $ pack "animated fadeIn" ] [ img_ [ + width_ $ pack "100" + , class_ $ pack "animated bounceInDown" + , src_ misoSrc + ] [ ] + , h1_ [ class_ $ pack "title animated pulse" + , style_ $ M.fromList [(pack "font-size", pack "82px") + ,(pack "font-weight", pack "100") + ] + ] [ text "examples" ] + , h2_ [ class_ $ pack "subtitle animated pulse" ] [ + a_ [ target_ $ pack "_blank" + , href_ $ pack "https://todo-mvc.haskell-miso.org/" + ] [ text "TodoMVC" ] + , text " / " + , a_ [ target_ $ pack "_blank" + , href_ $ pack "https://mario.haskell-miso.org/" ] + [ text "Mario" ] + ] + ] + +home :: Model -> View Action +home = template v + where + v = div_ [class_ $ pack "animated fadeIn"] [ img_ [ + width_ $ pack "100" + , class_ $ pack "animated bounceInDown" + , src_ misoSrc + ] [ ] + , h1_ [ class_ $ pack "title animated pulse" + , style_ $ M.fromList [(pack "font-size", pack "82px") + ,(pack "font-weight", pack "100") + ] + ] [ text "miso" ] + , h2_ [ class_ $ pack "subtitle animated pulse" ] [ + text "A tasty " + , a_ [ href_ $pack "https://www.haskell.org/" + , target_ $ pack "_blank"][ + strong_ [] [text . pack $ "Haskell" ]] + , text $ pack " front-end framework" + ] + ] + + +-- nav :: View Action +-- nav = +-- nav_ [ class_$pack "navbar is-bold" ] [ +-- div_ [class_$pack"navbar-menu"][ +-- div_[class_$pack"navbar-start"][ +-- a_[class_$pack"navbar-item" +-- , onClick (ChangeURI goHome) +-- ][ text$pack"Home" ], +-- a_[class_$pack"navbar-item" +-- , onClick (ChangeURI goExamples) +-- ][ text$pack"Examples" ], +-- a_[class_$pack"navbar-item" +-- , onClick (ChangeURI goDocs) +-- ][ text$pack"Docs" ], +-- a_[ class_$pack"navbar-item" +-- , onClick (ChangeURI goCommunity) +-- ][ text$pack"Community" ] +-- ] +-- ], +-- div_ [class_$pack"navbar-end"][ +-- div_ [class_$pack"navbar-item"][ +-- div_[class_$pack"field is-grouped"][ +-- p_ [class_$pack"control is-info"][ +-- a_[ id_$pack"twitter" +-- , class_$pack"button" +-- , prop (pack "data-social-network") (pack "Twitter") +-- , prop (pack "data-social-action") (pack "tweet") +-- , prop (pack "data-social-target") (pack "https://haskell-miso.org") +-- , target_$pack"blank" +-- , href_$pack "https://twitter.com/intent/tweet?text=Miso: a tasty Haskell front-end framework&url=https://haskell-miso.org&via=dmjio" +-- ] [ +-- span_ [ class_$pack"icon"] [ +-- i_ [class_$pack"fa fa-twitter"] [] +-- ], span_ [] [text$pack"Tweet"] +-- ], +-- p_ [class_$pack"control"][ +-- a_ [class_$pack"button is-primary" +-- ,href_$pack"https://github.com/dmjio/miso" +-- ][ +-- span_ [ class_$pack"icon"] [ +-- i_ [class_$pack"fa fa-github"] [] +-- ], span_ [] [text$pack"Github"] +-- ] +-- ] +-- ] +-- ] +-- ] +-- ] +-- ] + +template :: View Action -> Model -> View Action +template content Model{..} = + div_ [ ] [ +-- newNav + hero content uri + , middle + , footer + ] + +middle = + section_ [class_ $ pack "hero" ] [ + div_ [class_ $ pack "hero-body"] [ + div_ [class_ $ pack "container"] [ + nav_ [class_ $ pack "columns"] [ + a_ [ class_ $ pack "column has-text-centered" + , href_ $ pack "https://medium.com/@localvoid/how-to-win-in-web-framework-benchmarks-8bc31af76ce7" + , target_ $ pack "_blank" + ] [ + span_ [class_ $ pack "icon is-large"] [ + i_ [class_ $ pack "fa fa-flash"] [ ] + ], + p_ [class_ $ pack "title is-4"] [ + strong_ [] [ text $ pack "Fast"] + ], + p_ [class_ $ pack "subtitle"] [ + text $ pack "Virtual DOM diffing algorithm" + ] + ] + + , a_ [ class_ $ pack "column has-text-centered" + , href_ $ pack "/uhoh" + ] [ + span_ [class_ $ pack "icon is-large"] [ + i_ [class_ $ pack "fa fa-refresh"] [ ] + ], + p_ [class_ $ pack "title is-4"] [ + strong_ [] [ text $ pack "Isomorphic"] + ], + p_ [class_ $ pack "subtitle"] + [ text $ pack "Seamless web experience, try to 404 (click here)" ] + ], + a_ [ class_ $ pack "column has-text-centered" + , target_ $ pack "_blank" + , href_ $ pack "http://chimera.labs.oreilly.com/books/1230000000929/index.html" + ] [ + span_ [class_ $ pack "icon is-large"] [ + i_ [class_ $ pack "fa fa-gears"] [ ] + ], p_ [class_ $ pack "title is-4"] [ + strong_ [] [ text $ pack "Concurrent" ] + ], + p_ [class_ $ pack "subtitle"] [ + text $ pack "Type-safe and polymorphic, GHC Haskell" + ] + ], + a_ [class_ $ pack "column has-text-centered" + , href_ $ pack "https://github.com/ghcjs/ghcjs/blob/master/doc/foreign-function-interface.md" + , target_ $ pack "_blank" + ] [ + span_ [class_ $ pack "icon is-large"] [ + i_ [class_ $ pack "fa fa-code-fork"] [ ] + ], p_ [class_ $ pack "title is-4"] [ + strong_ [] [ text $ pack "Interoperable" ] + ], + p_ [class_ $ pack "subtitle"] [ + text $ pack "via the GHCJS FFI" + ] + ] + ] + ] + ] + ] + + +cols :: View action +cols = section_[][div_ [ class_ $ pack "container" ] [ + div_ [class_ $ pack "columns" ] [ + div_ [ class_ $ pack "column" ] [ + h1_ [class_ $ pack "title" ] [ + span_ [class_$pack"icon is-large"] [i_[class_$pack"fa fa-flash"][]] + , text $ pack "Fast" + ] + , h2_ [class_ $ pack "subtitle" ] [ + text $ pack "Mutable virtual dom implementation" + ] + ] + , div_ [ class_ $ pack "column" ] [ + text $ pack "Second column" + ] + , div_ [ class_ $ pack "column" ] [ + text $ pack "Third column" + ] + , div_ [ class_ $ pack "column" ] [ + text $ pack "Fourth column" + ] + ]]] + +the404 :: Model -> View Action +the404 = template v + where + v = div_ [] [ img_ [ + width_ $ pack "100" + , class_ $ pack "animated bounceOutUp" + , src_ misoSrc + ] [] + , h1_ [ class_ $ pack "title" + , style_ $ M.fromList [(pack "font-size", pack "82px") + ,(pack "font-weight", pack "100") + ] + ] [ text "404" ] + , h2_ [ class_ $ pack "subtitle animated pulse" ] [ + text "No soup for you! " + , a_ [ onClick $ ChangeURI goHome ] [ text " - Go Home" ] + ] + ] + +-- | Links +goHome, goExamples, goDocs, goCommunity :: URI +( goHome, goExamples, goDocs, goCommunity ) = + ( safeLink routes homeProxy + , safeLink routes examplesProxy + , safeLink routes docsProxy + , safeLink routes communityProxy + ) + +homeProxy :: Proxy Home +homeProxy = Proxy +examplesProxy :: Proxy Examples +examplesProxy = Proxy +docsProxy :: Proxy Docs +docsProxy = Proxy +communityProxy :: Proxy Community +communityProxy = Proxy +routes :: Proxy ClientRoutes +routes = Proxy + +-- | Github stars +starMiso :: View action +starMiso = a_ [ + class_ (pack "github-button") + , href_ (pack "https://github.com/dmjio/miso") + , prop (pack "data-icon") "octicon-star" + , prop (pack "data-size") "large" + , prop (pack "data-show-count") "true" + , prop (pack "aria-label") "Star dmjio/miso on GitHub" + ] [ text "Star" ] + +forkMiso :: View action +forkMiso = a_ [ + class_ (pack "github-button") + , href_ (pack "https://github.com/dmjio/miso/fork") + , prop (pack "data-icon") "octicon-repo-forked" + , prop (pack "data-size") "large" + , prop (pack "data-show-count") "true" + , prop (pack "aria-label") "Fork dmjio/miso on GitHub" + ] [ text "Fork" ] + +-- | Hero +hero :: View Action -> URI -> View Action +hero content uri' = + section_ [ class_ $ pack "hero is-medium is-primary is-bold has-text-centered" ] [ + div_ [ class_ $pack"hero-head" ] [ + header_ [class_$pack"nav"] [ + div_ [class_$pack"container"] [ + div_ [class_$pack"nav-left"][ + a_ [class_$pack"nav-item"][ + ] + ], + span_ [class_$pack"nav-toggle"] [ + span_[][] + , span_[][] + , span_[][] + ], + div_ [ class_$pack"nav-right nav-menu"] [ + a_ [ class_$ pack "nav-item " <> do pack $ bool mempty "is-active" (uri' == goHome) + , onClick (ChangeURI goHome) ] [ text$pack"Home" ], + a_ [class_$ pack "nav-item " <> do pack $ bool mempty "is-active" (uri' == goExamples) + , onClick (ChangeURI goExamples) + ] [ text$pack"Examples" ], + a_ [class_$ pack "nav-item " <> do pack $ bool mempty "is-active" (uri' == goDocs) + , onClick (ChangeURI goDocs) + ] [ text$pack"Docs" ], + a_ [class_$ pack "nav-item " <> do pack $ bool mempty "is-active" (uri' == goCommunity) + , onClick (ChangeURI goCommunity) + ] [ text$pack"Community" ] + + ]]]] + , div_ [ class_ $ pack "hero-body" ] [ + div_ [ class_ $ pack "container" ] [ + content + ] + ] + ] + +-- | Footer +footer :: View action +footer = + footer_ [ class_ $ pack "footer" ] [ + div_ [ class_ $ pack "container" ] [ + div_ [ class_ $ pack "content has-text-centered" ] [ + p_ [] [ + strong_ [] [ text "Miso" ] + , text " by " + , a_ [ href_ $ pack "https://github.com/dmjio/miso" ] + [ text "dmjio" ] + , text ". BSD3" + , a_ [ href_ $ pack "https://opensource.org/licenses/BSD-3-Clause" ] + [ text " licensed." ] + ] + , p_ [] [ text "The source code for this website is located " + , a_ [ href_ $ pack "https://github.com/dmjio/miso/tree/master/examples/haskell-miso.org" ] [ text$pack" here."] + ] + , p_ [] [ + a_ [ class_ $ pack "icon" + , href_ $ pack "https://github.com/dmjio/miso" + , target_ (pack "blank") + ] [span_ [class_$pack"icon is-large"] + [i_[class_$pack"fa fa-github"][]]] + ] + ] + ] + ] + + + +newNav = + div_ [ class_$pack "container" ] [ + nav_ [class_$pack "navbar is-transparent"] [ + div_ [class_$pack "navbar-brand"] [ + a_ [class_$pack "navbar-item" + ,href_$pack "https://haskell-miso.org"] [ + text "miso", + a_ [class_$pack "navbar-item is-hidden-desktop" + ,href_$pack "https://github.com/dmjio/miso" + ,target_$pack "_blank"] [ + span_ [class_$pack "icon", + style_ $ M.singleton (pack "color") (pack "#333") + ] [ i_ [class_$pack "fa fa-github"] [ ] + ] + ] + , a_ [class_$pack "navbar-item is-hidden-desktop" + ,href_$pack "https://twitter.com/dmjio" + ,target_$pack "_blank"] [ + span_ [ class_$pack "icon" + , style_ $ M.singleton (pack "color") (pack "#55acee") + ] [ + i_ [class_$pack "fa fa-twitter"] [ ] + ] + ] + , div_ [ class_$pack "navbar-burger burger" + , prop (pack "data-target") (pack "navMenuIndex")] [ + span_ [] [ ] + ,span_ [] [ ] + ,span_ [] [ ] + ] + ] + , div_ [ id_ $pack"navMenuIndex" + , class_$pack "navbar-menu"] [ + div_ [class_$pack "navbar-start"] [ + a_ [class_$pack "navbar-item is-active" + ,href_$pack "https://haskell-miso.org"] [ + text $ pack "Home"] + , div_ [class_$pack "navbar-item has-dropdown is-hoverable"] [ + a_ [class_$pack "navbar-link" + ,href_$pack "/documentation/overview/start/"] [ + text $ pack "Docs"] + , div_ [class_$pack "navbar-dropdown is-boxed"] [ + a_ [class_$pack "navbar-item " + ,href_$pack "/documentation/overview/start/"] [ + text $ pack "Overview" + ] + , a_ [class_$pack "navbar-item " + ,href_$pack "http://bulma.io/documentation/modifiers/syntax/"] [ + text $ pack "Modifiers" + ] + , a_ [class_$pack "navbar-item " + ,href_$pack "http://bulma.io/documentation/grid/columns/"] [ + text $ pack "Grid" + ] + , a_ [ class_$pack "navbar-item " + ,href_$pack "http://bulma.io/documentation/form/general/"] [ + text $ pack "Form" + ] + , a_ [class_$pack "navbar-item " + , href_$pack "http://bulma.io/documentation/elements/box/"] [ + text $ pack "Elements" + ] + , a_ [class_$pack "navbar-item " + ,href_$pack "http://bulma.io/documentation/components/breadcrumb/" + ] [ text $ pack "Components"] + , a_ [class_$pack "navbar-item " + ,href_$pack "http://bulma.io/documentation/layout/container/" + ] [ text $ pack "Layout"] + , hr_ [class_$pack "navbar-divider"] [ + div_ [class_$pack "navbar-item"] [ + div_ [] [ + p_ [class_$pack "has-text-info is-size-6-desktop"] [ + strong_ [] [ text $ pack "0.4.4"]] + , small_ [] [ + a_ [class_$pack "view-all-versions",href_$pack "/versions"] [ + text $ pack "View all versions" + ] + ] + ] + ] + ] + ] + , div_ [class_$pack "navbar-item has-dropdown is-hoverable"] [ + a_ [class_$pack "navbar-link ", href_$pack "http://bulma.io/blog/"] [ + text $ pack "Blog" + ] + , div_ [id_ $pack"blogDropdown" + ,class_$pack "navbar-dropdown is-boxed" + ,prop (pack "data-style_") (pack "width: 18rem;")] [ + a_ [class_$pack "navbar-item" + ,href_$pack "/2017/07/24/access-previous-bulma-versions/"] [ + div_ [class_$pack "navbar-content"] [ + p_ [] [ small_ [class_$pack "has-text-info"] [ + text $ pack "24 Jul 2017"] + ] + , p_ [] [ text $ pack "Access previous Bulma versions"] + ] + ] + , a_ [ class_$pack "navbar-item" + ,href_$pack "/2017/03/10/new-field-element/"] [ + div_ [class_$pack "navbar-content"] [ + p_ [] [ + small_ [class_$pack "has-text-info"] [ + text $ pack "10 Mar 2017" + ] + ] + , p_ [] [ + text $ pack "New field element (for better controls)" + ] + ] + ] + , a_ [class_$pack "navbar-item" + ,href_$pack "/2016/04/11/metro-ui-css-grid-with-bulma-tiles/"] [ + div_ [class_$pack "navbar-content"] [ + p_ [] [ + small_ [class_$pack "has-text-info"] [ + text $ pack "11 Apr 2016" + ] + ] + , p_ [] [ + text $ pack "Metro UI CSS grid with Bulma tiles" + ] + ] + ] + , a_ [class_$pack "navbar-item",href_$pack "http://bulma.io/blog/"] [ + text $ pack "More posts" + ] + , hr_ [class_$pack "navbar-divider"] [ + div_ [class_$pack "navbar-item"] [ + div_ [class_$pack "navbar-content"] [ + div_ [class_$pack "level is-mobile"] [ + div_ [class_$pack "level-left"] [ + div_ [class_$pack "level-item"] [ + strong_ [] [ text $ pack "Stay up to date!"] + ] + ] + , div_ [class_$pack "level-right"] [ + div_ [class_$pack "level-item"] [ + a_ [class_$pack "button is-rss is-small" + ,href_$pack "http://bulma.io/atom.xml"] [ + span_ [class_$pack "icon is-small"] [ + i_ [class_$pack "fa fa-rss"] [ ] + ] + , span_ [] [ + text $ pack "Subscribe"] + ] + ] + ] + ] + ] + ] + ] + ] + , div_ [class_$pack "navbar-item has-dropdown is-hoverable"] [ + div_ [class_$pack "navbar-link"] [ + text $ pack "More"] + , div_ [id_ $pack"moreDropdown" + ,class_$pack "navbar-dropdown is-boxed" ] [ + a_ [class_$pack "navbar-item ",href_$pack "http://bulma.io/extensions/"] [ + div_ [class_$pack "level is-mobile"] [ + div_ [class_$pack "level-left"] [ + div_ [class_$pack "level-item"] [ + p_ [] [ strong_ [] [ + text $ pack "Extensions" + ] + , br_ [] [ + small_ [] [ + text $ pack "Side projects to enhance Bulma"] + ] + ] + ] + , div_ [class_$pack "level-right"] [ + div_ [class_$pack "level-item"] [ + span_ [class_$pack "icon has-text-info"] [ + i_ [class_$pack "fa fa-plug"] [ ] + ] + ] + ] + ] + ] + ] + ] + ] + , div_ [class_$pack "navbar-end"] [ + a_ [class_$pack "navbar-item" + ,href_$pack "https://github.com/dmjio/miso" + ,target_$pack "_blank"] [ + text $ pack "Github" + ] + , a_ [class_$pack "navbar-item" + ,href_$pack "https://twitter.com/dmjio" + ,target_$pack "_blank"] [ + text $ pack "Twitter" + ] + , div_ [class_$pack "navbar-item"] [ + div_ [class_$pack "field is-grouped"] [ + p_ [class_$pack "control"] [ + a_ [ id_ $pack"twitter" + , class_$pack "button" + , prop (pack "data-social-network_") (pack "Twitter") + , prop (pack "data-social-action_") (pack "tweet") + , prop (pack "data-social-target") (pack "http://bulma.io") + ,target_$pack "_blank" + , href_$pack "https://twitter.com/intent/tweet?text=Miso: a tasty Haskell front-end framework&url=https://haskell-miso.org&via=dmjio"] [ + span_ [class_$pack "icon"] [ + i_ [class_$pack "fa fa-twitter"] [ ] + ] + , span_ [] [ text $ pack "Tweet"] ] + ], p_ [class_$pack "control"] [ + starMiso + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] + diff --git a/ghc-src/Miso.hs b/ghc-src/Miso.hs index 1398efe0..bd593fc8 100644 --- a/ghc-src/Miso.hs +++ b/ghc-src/Miso.hs @@ -15,7 +15,9 @@ module Miso ( module Miso.Event , module Miso.Html + , module Miso.TypeLevel ) where import Miso.Event import Miso.Html +import Miso.TypeLevel diff --git a/ghc-src/Miso/Html/Internal.hs b/ghc-src/Miso/Html/Internal.hs index 5330e009..fd6057b2 100644 --- a/ghc-src/Miso/Html/Internal.hs +++ b/ghc-src/Miso/Html/Internal.hs @@ -39,8 +39,6 @@ module Miso.Html.Internal ( -- * Handling events , on , onWithOptions - -- * String - , module Miso.String ) where import Data.Aeson @@ -64,7 +62,6 @@ data VTree action where VNode :: { vType :: Text -- ^ Element type (i.e. "div", "a", "p") , vNs :: NS -- ^ HTML or SVG , vProps :: Props -- ^ Fields present on DOM Node - , vCss :: CSS -- ^ Styles , vKey :: Maybe Key -- ^ Key used for child swap patch , vChildren :: V.Vector (VTree action) -- ^ Child nodes } -> VTree action @@ -77,7 +74,8 @@ instance Show (VTree action) where -- | Converting `VTree` to Lucid's `L.Html` instance L.ToHtml (VTree action) where toHtmlRaw = L.toHtml - toHtml (VText x) = L.toHtml x + toHtml (VText x) | T.null x = L.toHtml (" " :: MisoString) + | otherwise = L.toHtml x toHtml VNode{..} = let ele = L.makeElement (toTag vType) kids in L.with ele as @@ -130,7 +128,6 @@ data NS node :: NS -> MisoString -> Maybe Key -> [Attribute action] -> [View action] -> View action node vNs vType vKey as xs = let vProps = Props $ M.fromList [ (k,v) | P k v <- as ] - vCss = CSS $ M.fromList [ (k,v) | C k v <- as ] vChildren = V.fromList $ map runView xs in View VNode {..} @@ -161,15 +158,13 @@ instance ToKey Word where toKey = Key . T.pack . show -- | Properties newtype Props = Props (M.Map MisoString Value) - --- | CSS -newtype CSS = CSS (M.Map MisoString MisoString) + deriving (Show, Eq) -- | `View` Attributes to annotate DOM, converted into Events, Props, Attrs and CSS data Attribute action - = C MisoString MisoString - | P MisoString Value + = P MisoString Value | E () + deriving (Show, Eq) -- | DMJ: this used to get set on preventDefault on Options... if options are dynamic now what -- | Useful for `drop` events @@ -212,7 +207,7 @@ onWithOptions _ _ _ _ = E () -- -- style_ :: M.Map MisoString MisoString -> Attribute action -style_ = C "style" . M.foldrWithKey go mempty +style_ map' = P "style" $ String (M.foldrWithKey go mempty map') where go :: MisoString -> MisoString -> MisoString -> MisoString go k v xs = mconcat [ k, ":", v, ";" ] <> xs diff --git a/ghc-src/Miso/TypeLevel.hs b/ghc-src/Miso/TypeLevel.hs new file mode 100644 index 00000000..84be6911 --- /dev/null +++ b/ghc-src/Miso/TypeLevel.hs @@ -0,0 +1,19 @@ +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE PolyKinds #-} +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE TypeOperators #-} +module Miso.TypeLevel ( ToServerRoutes ) where + +import Miso.Html +import Servant.API +import Servant.HTML.Lucid + +-- | Convert client route type to a server web handler type +type family ToServerRoutes (layout :: k) (wrapper :: * -> *) (action :: *) :: k where + ToServerRoutes (a :<|> b) wrapper action = + ToServerRoutes a wrapper action :<|> + ToServerRoutes b wrapper action + ToServerRoutes (a :> b) wrapper action = + a :> ToServerRoutes b wrapper action + ToServerRoutes (View _) wrapper action = + Get '[HTML] (wrapper (View action)) diff --git a/ghcjs-src/Miso.hs b/ghcjs-src/Miso.hs index c4b4ebce..59b98098 100644 --- a/ghcjs-src/Miso.hs +++ b/ghcjs-src/Miso.hs @@ -13,7 +13,8 @@ -- Portability : non-portable ---------------------------------------------------------------------------- module Miso - ( startApp + ( miso + , startApp , module Miso.Effect , module Miso.Event , module Miso.Html @@ -28,6 +29,7 @@ import Data.IORef import Data.List import Data.Sequence ((|>)) import qualified Data.Sequence as S +import qualified JavaScript.Object.Internal as OI import JavaScript.Web.AnimationFrame import Miso.Concurrent @@ -39,39 +41,40 @@ import Miso.Html import Miso.Router import Miso.Subscription import Miso.Types +import Miso.FFI --- | Runs a miso application -startApp :: Eq model => App model action -> IO () -startApp App {..} = do - let initialView = view model - -- init empty Model - modelRef <- newIORef model - -- init empty actions - actionsMVar <- newMVar S.empty +-- | Helper function to abstract out common functionality between `startApp` and `miso` +common + :: Eq model + => App model action + -> model + -> ((action -> IO ()) -> IO (IORef VTree)) + -> IO b +common App {..} m getView = do -- init Notifier Notify {..} <- newNotify -- init EventWriter EventWriter {..} <- newEventWriter notify + -- init empty Model + modelRef <- newIORef m + -- init empty actions + actionsMVar <- newMVar S.empty -- init Subs forM_ subs $ \sub -> sub (readIORef modelRef) writeEvent -- init event application thread void . forkIO . forever $ do - newAction <- getEvent + action <- getEvent modifyMVar_ actionsMVar $! \actions -> - pure (actions |> newAction) + pure (actions |> action) -- Hack to get around `BlockedIndefinitelyOnMVar` exception -- that occurs when no event handlers are present on a template -- and `notify` is no longer in scope void . forkIO . forever $ threadDelay (1000000 * 86400) >> notify - -- Create virtual dom, perform initial diff - initialVTree <- flip runView writeEvent initialView - Nothing `diff` (Just initialVTree) - viewRef <- newIORef initialVTree + -- Retrieves reference view + viewRef <- getView writeEvent -- Begin listening for events in the virtual dom delegator viewRef events - -- Process initial action of application - writeEvent initialAction -- Program loop, blocking on SkipChan forever $ wait >> do -- Apply actions to model @@ -93,6 +96,31 @@ startApp App {..} = do Just oldVTree `diff` Just newVTree atomicWriteIORef viewRef newVTree +-- | Runs an isomorphic miso application +-- Assumes the pre-rendered DOM is already present +miso :: (HasURI model, Eq model) => App model action -> IO () +miso app@App{..} = do + uri <- getCurrentURI + let modelWithUri = setURI uri model + common app model $ \writeEvent -> do + let initialView = view modelWithUri + VTree (OI.Object iv) <- flip runView writeEvent initialView + -- Initial diff can be bypassed, just copy DOM into VTree + copyDOMIntoVTree iv + let initialVTree = VTree (OI.Object iv) + -- Create virtual dom, perform initial diff + newIORef initialVTree + +-- | Runs a miso application +startApp :: Eq model => App model action -> IO () +startApp app@App {..} = + common app model $ \writeEvent -> do + let initialView = view model + initialVTree <- flip runView writeEvent initialView + Nothing `diff` (Just initialVTree) + newIORef initialVTree + +-- | Helper foldEffects :: (action -> IO ()) -> (action -> model -> Effect model action) diff --git a/ghcjs-src/Miso/Router.hs b/ghcjs-src/Miso/Router.hs index a9101358..d987e158 100644 --- a/ghcjs-src/Miso/Router.hs +++ b/ghcjs-src/Miso/Router.hs @@ -190,7 +190,10 @@ routeLoc loc r m = case r of p:paths -> if p == T.pack (symbolVal sym) then routeLoc (loc { locPath = paths }) a m else Left Fail - RPage a -> Right a + RPage a -> + case locPath loc of + [] -> Right a + _ -> Left Fail -- | Convert a 'URI' to a 'Location'. uriToLocation :: URI -> Location diff --git a/ghcjs-src/Miso/Subscription/History.hs b/ghcjs-src/Miso/Subscription/History.hs index 98fc725b..a8622a58 100644 --- a/ghcjs-src/Miso/Subscription/History.hs +++ b/ghcjs-src/Miso/Subscription/History.hs @@ -43,7 +43,7 @@ getURI :: IO URI getURI = do URI <$> pure mempty <*> pure Nothing - <*> do unpack <$> getPathName + <*> do Prelude.drop 1 . unpack <$> getPathName <*> do unpack <$> getSearch <*> pure mempty diff --git a/jsbits/isomorphic.js b/jsbits/isomorphic.js index a4a31106..72a94bcf 100644 --- a/jsbits/isomorphic.js +++ b/jsbits/isomorphic.js @@ -3,10 +3,17 @@ function copyDOMIntoVTree (vtree) { } function walk (vtree, node) { - var i = 0; + var i = 0, vdomChild, domChild; vtree.domRef = node; while (i < vtree.children.length) { - walk(vtree.children[i], node.children[i]); + vdomChild = vtree.children[i]; + domChild = node.childNodes[i]; + if (vdomChild.type === "vtext") { + vdomChild.domRef = domChild; + i++; + continue; + } + walk(vdomChild, domChild); i++; } } diff --git a/miso-ghc.nix b/miso-ghc.nix index 0faf3bd8..c3a5bcfc 100644 --- a/miso-ghc.nix +++ b/miso-ghc.nix @@ -1,5 +1,5 @@ { mkDerivation, aeson, base, bytestring, containers, lucid -, stdenv, text, vector, BoundedChan, servant +, stdenv, text, vector, BoundedChan, servant, servant-lucid }: mkDerivation { pname = "miso"; @@ -9,6 +9,7 @@ mkDerivation { isExecutable = true; libraryHaskellDepends = [ aeson base bytestring containers lucid text vector BoundedChan servant + servant-lucid ]; homepage = "http://github.com/dmjio/miso"; description = "A tasty Haskell front-end framework"; diff --git a/miso.cabal b/miso.cabal index e03c3436..098f1064 100644 --- a/miso.cabal +++ b/miso.cabal @@ -204,8 +204,11 @@ library Miso.FFI Miso.Delegate else + exposed-modules: + Miso.TypeLevel build-depends: lucid, + servant-lucid, vector hs-source-dirs: ghc-src