Initial commit.
This commit is contained in:
commit
2d2d926416
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
.spago/
|
||||
app/index.js
|
||||
node_modules/
|
||||
output/
|
||||
package-lock.json
|
11
app/index.html
Normal file
11
app/index.html
Normal file
@ -0,0 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>This is a simple pushstate example</title>
|
||||
</head>
|
||||
<body>
|
||||
<script src="./index.js" type="module"></script>
|
||||
</body>
|
||||
</html>
|
32
makefile
Normal file
32
makefile
Normal file
@ -0,0 +1,32 @@
|
||||
all: build
|
||||
|
||||
build:
|
||||
spago build
|
||||
|
||||
bundle: install-esbuild
|
||||
PATH=$$PATH:node_modules/.bin spago bundle-app
|
||||
mv index.js app/
|
||||
|
||||
repl:
|
||||
spago repl
|
||||
|
||||
spagobuild:
|
||||
spago build
|
||||
|
||||
install-esbuild:
|
||||
@echo "install ebbuild"
|
||||
[ -f node_modules/.bin/esbuild ] || npm install esbuild
|
||||
|
||||
HTTPD_ACCESS_LOGS ?= /tmp/access.log
|
||||
HTTPD_ADDR ?= 127.0.0.1
|
||||
HTTPD_PORT ?= 35000
|
||||
DIR ?= app
|
||||
serve:
|
||||
darkhttpd $(DIR) --addr $(HTTPD_ADDR) --port $(HTTPD_PORT) --log $(HTTPD_ACCESS_LOGS)
|
||||
|
||||
# NPM can serve it, but it's slow for nothing.
|
||||
#serve:
|
||||
# npm run serve
|
||||
|
||||
# You can add your specific instructions there.
|
||||
-include makefile.user
|
27
spago.dhall
Normal file
27
spago.dhall
Normal file
@ -0,0 +1,27 @@
|
||||
{ name = "halogen-project"
|
||||
, dependencies =
|
||||
[ "aff"
|
||||
, "arrays"
|
||||
, "bifunctors"
|
||||
, "const"
|
||||
, "control"
|
||||
, "effect"
|
||||
, "either"
|
||||
, "foreign"
|
||||
, "halogen"
|
||||
, "halogen-store"
|
||||
, "maybe"
|
||||
, "prelude"
|
||||
, "routing"
|
||||
, "routing-duplex"
|
||||
, "strings"
|
||||
, "tailrec"
|
||||
, "transformers"
|
||||
, "web-dom"
|
||||
, "web-events"
|
||||
, "web-html"
|
||||
, "web-uievents"
|
||||
]
|
||||
, packages = ./packages.dhall
|
||||
, sources = [ "src/**/*.purs", "test/**/*.purs" ]
|
||||
}
|
669
src/Main.purs
Normal file
669
src/Main.purs
Normal file
@ -0,0 +1,669 @@
|
||||
{-
|
||||
|
||||
This Gist has:
|
||||
* PushState routing
|
||||
* Interception of all clicks on <a> hrefs or their children
|
||||
* Login / sign out
|
||||
- You "land" on the 'Secrets' page, but cannot see it until you "Login"
|
||||
|
||||
You can preview the appearance of this Gist on TryPureScript, but note
|
||||
that the PushState routing will not work within its <iframe> environment
|
||||
|
||||
-}
|
||||
|
||||
module Main where
|
||||
|
||||
import Prelude hiding ((/))
|
||||
|
||||
import Control.Alt ((<|>))
|
||||
import Control.Monad.Except (ExceptT(..), runExceptT, throwError)
|
||||
import Control.Monad.Maybe.Trans (MaybeT(..), runMaybeT)
|
||||
import Control.Monad.Rec.Class as Rec
|
||||
import Control.Monad.State (class MonadState)
|
||||
import Control.Monad.Trans.Class (lift)
|
||||
import Data.Array as Array
|
||||
import Data.Bifunctor (lmap)
|
||||
import Data.Const (Const)
|
||||
import Data.Either (Either(..), either, note)
|
||||
import Data.Generic.Rep (class Generic)
|
||||
import Data.Maybe (Maybe(..), isJust, isNothing, maybe)
|
||||
import Data.Show.Generic (genericShow)
|
||||
import Data.String as String
|
||||
import Effect (Effect)
|
||||
import Effect.Aff (Aff, launchAff_)
|
||||
import Effect.Aff as Aff
|
||||
import Effect.Aff.Class (class MonadAff)
|
||||
import Effect.Class (class MonadEffect)
|
||||
import Foreign as F
|
||||
import Halogen as H
|
||||
import Halogen.Aff as HA
|
||||
import Halogen.HTML as HH
|
||||
import Halogen.HTML.Events as HE
|
||||
import Halogen.HTML.Properties as HP
|
||||
import Halogen.Store.Connect as HSC
|
||||
import Halogen.Store.Monad as HSM
|
||||
import Halogen.Store.Select as HSS
|
||||
import Halogen.VDom.Driver ( runUI )
|
||||
import Routing.Duplex (RouteDuplex')
|
||||
import Routing.Duplex as RD
|
||||
import Routing.Duplex.Generic ( noArgs, sum )
|
||||
import Routing.Duplex.Generic.Syntax ((/))
|
||||
import Routing.Duplex.Parser (RouteError)
|
||||
import Routing.PushState as RPS
|
||||
import Type.Proxy (Proxy(Proxy))
|
||||
import Web.DOM.Document as Document
|
||||
import Web.DOM.Element as Element
|
||||
import Web.DOM.Node as Node
|
||||
import Web.DOM.ParentNode (QuerySelector(..))
|
||||
import Web.DOM.Text as Text
|
||||
import Web.Event.Event (Event)
|
||||
import Web.Event.Event as Event
|
||||
import Web.Event.EventTarget (EventTarget)
|
||||
import Web.HTML as HTML
|
||||
import Web.HTML.HTMLAnchorElement (HTMLAnchorElement)
|
||||
import Web.HTML.HTMLAnchorElement as HTMLAnchorElement
|
||||
import Web.HTML.HTMLElement (HTMLElement)
|
||||
import Web.HTML.HTMLElement as HTMLElement
|
||||
import Web.HTML.HTMLHyperlinkElementUtils as Utils
|
||||
import Web.HTML.Location as Location
|
||||
import Web.HTML.Window as Window
|
||||
import Web.UIEvent.MouseEvent as MouseEvent
|
||||
|
||||
----------
|
||||
-- `Main`
|
||||
|
||||
main :: Effect Unit
|
||||
main = HA.runHalogenAff do
|
||||
|
||||
HA.awaitLoad
|
||||
|
||||
let
|
||||
selectHTML :: String -> ExceptT String Aff HTMLElement
|
||||
selectHTML target = ExceptT do
|
||||
maybeElement <- HA.selectElement (QuerySelector target)
|
||||
pure $ maybe (Left ("'" <> target <> "' element not found")) Right maybeElement
|
||||
|
||||
either (throwError <<< Aff.error) pure =<< runExceptT do
|
||||
|
||||
html <- selectHTML "html"
|
||||
body <- selectHTML "body"
|
||||
H.liftEffect $ Node.removeChild (HTMLElement.toNode body) (HTMLElement.toNode html)
|
||||
|
||||
-- This section is to to add a stylesheet (see bottom of Gist) to the header
|
||||
-- within a TryPureScript <iframe>, as I use the site a lot for small demos.
|
||||
headNode <- HTMLElement.toNode <$> selectHTML "head"
|
||||
doc <- ExceptT $ map (note "document not found") do
|
||||
H.liftEffect $ Node.ownerDocument headNode
|
||||
H.liftEffect do
|
||||
styleElem <- Element.toNode <$> Document.createElement "style" doc
|
||||
styleTextNode <- Text.toNode <$> Document.createTextNode stylesheet doc
|
||||
Node.appendChild styleTextNode styleElem *> Node.appendChild styleElem headNode
|
||||
|
||||
nav <- H.liftEffect RPS.makeInterface
|
||||
|
||||
let
|
||||
initialStore :: Store
|
||||
initialStore = { currentUser: Nothing, loggerHist: [], nav }
|
||||
|
||||
routerInput :: Unit
|
||||
routerInput = unit
|
||||
|
||||
lift do
|
||||
componentAff <- runAppM initialStore componentRouter
|
||||
halogenIO <- runUI componentAff routerInput html
|
||||
|
||||
void $ H.liftEffect $ nav # RPS.matchesWith routeParse \maybeOld new ->
|
||||
when ( maybeOld /= Just new ) $
|
||||
launchAff_ do
|
||||
void $ halogenIO.query $ H.mkTell $ Router_Navigate new
|
||||
|
||||
|
||||
-- Store: App-wide state
|
||||
|
||||
type Store =
|
||||
{ currentUser :: Maybe Profile
|
||||
, loggerHist :: Array String
|
||||
, nav :: RPS.PushStateInterface
|
||||
}
|
||||
|
||||
data StoreAction
|
||||
= LoginUser Profile
|
||||
| LogoutUser
|
||||
| UpdateHistory String
|
||||
|
||||
reduce :: Store -> StoreAction -> Store
|
||||
reduce store = case _ of
|
||||
LoginUser profile ->
|
||||
store { currentUser = Just profile }
|
||||
|
||||
LogoutUser ->
|
||||
store { currentUser = Nothing }
|
||||
|
||||
UpdateHistory msg ->
|
||||
store { loggerHist = msg `prependMessage` store.loggerHist }
|
||||
where
|
||||
prependMessage :: String -> Array String -> Array String
|
||||
prependMessage x xs
|
||||
| Array.length xs < 10 = x `Array.cons` xs
|
||||
| otherwise = x `Array.cons` (Array.take 9 xs)
|
||||
|
||||
|
||||
updateLocalState
|
||||
:: forall m r i
|
||||
. MonadState { currentUser :: Maybe Profile | r } m
|
||||
=> { context :: Maybe Profile | i }
|
||||
-> m Unit
|
||||
updateLocalState { context: newUser } = do
|
||||
oldUser <- H.gets _.currentUser
|
||||
when (oldUser /= newUser) do
|
||||
H.modify_ _ { currentUser = newUser }
|
||||
|
||||
|
||||
-- | AppM: Main application Monad
|
||||
|
||||
newtype AppM a = AppM (HSM.StoreT StoreAction Store Aff a)
|
||||
|
||||
runAppM :: forall q i o. Store -> H.Component q i o AppM -> Aff (H.Component q i o Aff)
|
||||
runAppM initialStore = HSM.runStoreT initialStore reduce <<< H.hoist (\(AppM m) -> m )
|
||||
|
||||
derive newtype instance Functor AppM
|
||||
derive newtype instance Apply AppM
|
||||
derive newtype instance Applicative AppM
|
||||
derive newtype instance Bind AppM
|
||||
derive newtype instance Monad AppM
|
||||
derive newtype instance MonadEffect AppM
|
||||
derive newtype instance MonadAff AppM
|
||||
derive newtype instance HSM.MonadStore StoreAction Store AppM
|
||||
|
||||
instance MonadNavigate AppM where
|
||||
navigate :: Route -> AppM Unit
|
||||
navigate r = do
|
||||
{ nav } <- HSM.getStore
|
||||
H.liftEffect $ nav.pushState (F.unsafeToForeign {}) (routePrint r)
|
||||
|
||||
logoutUser :: AppM Unit
|
||||
logoutUser = do
|
||||
HSM.updateStore LogoutUser
|
||||
navigate Home
|
||||
|
||||
instance MonadUser AppM where
|
||||
loginUser :: Username -> AppM (Maybe Profile)
|
||||
loginUser username = do
|
||||
HSM.updateStore $ LoginUser { username }
|
||||
pure (Just { username })
|
||||
|
||||
instance MonadLog AppM where
|
||||
log :: String -> AppM Unit
|
||||
log msg = do
|
||||
HSM.updateStore $ UpdateHistory msg
|
||||
|
||||
|
||||
-- Capability classes
|
||||
|
||||
class Monad m <= MonadNavigate m where
|
||||
navigate :: Route -> m Unit
|
||||
logoutUser :: m Unit
|
||||
|
||||
instance MonadNavigate m => MonadNavigate ( H.HalogenM state action slots msg m ) where
|
||||
navigate = H.lift <<< navigate
|
||||
logoutUser = H.lift logoutUser
|
||||
|
||||
class Monad m <= MonadUser m where
|
||||
loginUser :: Username -> m (Maybe Profile)
|
||||
|
||||
instance MonadUser m => MonadUser ( H.HalogenM state action slots msg m ) where
|
||||
loginUser = H.lift <<< loginUser
|
||||
|
||||
class Monad m <= MonadLog m where
|
||||
log :: String -> m Unit
|
||||
|
||||
instance MonadLog m => MonadLog ( H.HalogenM state action slots msg m ) where
|
||||
log = H.lift <<< log
|
||||
|
||||
|
||||
-- Data.Route
|
||||
|
||||
data Route
|
||||
= Home
|
||||
| Secrets
|
||||
|
||||
derive instance Generic Route _
|
||||
derive instance Eq Route
|
||||
derive instance Ord Route
|
||||
instance Show Route where show = genericShow
|
||||
|
||||
routePrint :: Route -> String
|
||||
routePrint = RD.print codec
|
||||
|
||||
routeParse :: String -> Either String Route
|
||||
routeParse = lmap show <<< RD.parse codec
|
||||
|
||||
codec :: RouteDuplex' Route
|
||||
codec = RD.root $ sum
|
||||
{ "Home": noArgs
|
||||
, "Secrets": "secrets" / noArgs
|
||||
}
|
||||
|
||||
|
||||
-- Data.Profile
|
||||
|
||||
type Profile = { username :: Username }
|
||||
|
||||
type Username = String
|
||||
|
||||
parseUsername :: String -> Maybe Username
|
||||
parseUsername = case _ of
|
||||
"" -> Nothing
|
||||
str -> Just str
|
||||
|
||||
printUsername :: Username -> String
|
||||
printUsername u = u
|
||||
|
||||
|
||||
-- Component.Router
|
||||
data Router_Query :: Type -> Type
|
||||
data Router_Query a = Router_Navigate Route a
|
||||
|
||||
type ConnectedInput = HSC.Connected (Maybe Profile) Unit
|
||||
|
||||
type Router_State =
|
||||
{ route :: Maybe Route
|
||||
, loggerHist :: Array String
|
||||
, currentUser :: Maybe Profile
|
||||
}
|
||||
|
||||
data Router_Action
|
||||
= Router_Initialize
|
||||
| Router_Receive { input :: Unit, context :: (Maybe Profile) }
|
||||
| Router_AnchorClick Event
|
||||
| Router_LogMsg (Array String)
|
||||
|
||||
type Router_ChildSlots =
|
||||
( home :: Home_Slot
|
||||
, secrets :: Secrets_Slot
|
||||
)
|
||||
|
||||
type RouterM = H.HalogenM Router_State Router_Action Router_ChildSlots Void AppM
|
||||
type RouterHTML = H.ComponentHTML Router_Action Router_ChildSlots AppM
|
||||
|
||||
componentRouter :: H.Component Router_Query Unit Void AppM
|
||||
componentRouter =
|
||||
HSC.connect (HSS.selectEq _.currentUser) $
|
||||
H.mkComponent { initialState, render, eval }
|
||||
where
|
||||
eval :: H.HalogenQ _ _ _ ~> H.HalogenM _ _ _ _ _
|
||||
eval = H.mkEval $ H.defaultEval
|
||||
{ handleAction = handleAction
|
||||
, handleQuery = handleQuery
|
||||
, initialize = Just Router_Initialize
|
||||
, receive = Just <<< Router_Receive
|
||||
}
|
||||
|
||||
initialState :: ConnectedInput -> Router_State
|
||||
initialState = \{ context: currentUser } ->
|
||||
{ route: Just Secrets
|
||||
, loggerHist: []
|
||||
, currentUser
|
||||
}
|
||||
|
||||
handleAction
|
||||
:: Router_Action
|
||||
-> RouterM Unit
|
||||
handleAction = case _ of
|
||||
Router_Initialize -> do
|
||||
HSM.getStore >>= \r -> H.modify_ _ { loggerHist = r.loggerHist }
|
||||
loggerHistEmitter <- HSM.emitSelected (HSS.selectEq _.loggerHist)
|
||||
void $ H.subscribe (Router_LogMsg <$> loggerHistEmitter)
|
||||
|
||||
Router_Receive input ->
|
||||
updateLocalState input
|
||||
|
||||
Router_AnchorClick event -> do
|
||||
withClickedAnchor_ event codec case _ of
|
||||
LocalRoute route -> do
|
||||
H.liftEffect $ Event.preventDefault event
|
||||
log $ "Local anchor link clicked for route: " <> show route
|
||||
navigate route
|
||||
|
||||
LocalRouteError { anchorPath: path, routeError: _ } -> do
|
||||
log $ "Local anchor link clicked for route that fails to parse: " <> show path
|
||||
H.liftEffect $ Event.preventDefault event
|
||||
|
||||
ForeignRoute { anchorHost: host, anchorHref: _ } -> do
|
||||
log $ "Anchor clicked for link to foreign host: " <> show host <> " (not exiting)"
|
||||
H.liftEffect $ Event.preventDefault event
|
||||
|
||||
Router_LogMsg newLog -> do
|
||||
currentLog <- H.gets _.loggerHist
|
||||
when (newLog /= currentLog) do
|
||||
H.modify_ _ { loggerHist = newLog }
|
||||
|
||||
handleQuery :: forall a. Router_Query a -> RouterM ( Maybe a )
|
||||
handleQuery = case _ of
|
||||
Router_Navigate new a -> do
|
||||
{ currentUser: _, route: maybeOld } <- H.get
|
||||
|
||||
when (maybeOld /= Just new) do
|
||||
H.modify_ _ { route = Just new }
|
||||
log $ displayRouteChange maybeOld new
|
||||
|
||||
pure ( Just a )
|
||||
|
||||
where
|
||||
labelFromMain txt = "[ main | matchesWith | Address bar path ] " <> txt
|
||||
|
||||
labelNavigate txt = "[ Router | Query = Navigate ] " <> txt
|
||||
|
||||
displayRouteChange :: Maybe Route -> Route -> String
|
||||
displayRouteChange maybeOld new =
|
||||
String.joinWith "\n"
|
||||
[ labelFromMain $ case maybeOld of
|
||||
Nothing -> "was set on load to " <> routePrint new
|
||||
Just old -> "was set from " <> routePrint old <> " to " <> routePrint new
|
||||
, labelNavigate $ case maybeOld of
|
||||
Nothing -> "Setting local state initially to " <> show new
|
||||
Just old -> "Setting local state from " <> show old <> " to " <> show new
|
||||
]
|
||||
|
||||
render :: Router_State -> RouterHTML
|
||||
render st@{ currentUser } =
|
||||
HH.body
|
||||
[ css ("body "<>""), HE.onClick (Router_AnchorClick <<< MouseEvent.toEvent) ]
|
||||
[ HH.nav
|
||||
[ css "nav" ]
|
||||
[ HH.ul
|
||||
[ css "nav-ul" ]
|
||||
[ renderNavItem Home "🏡" "Home"
|
||||
, whenElem (isJust currentUser) \_ ->
|
||||
renderNavItem Secrets "🕵" "Secrets"
|
||||
]
|
||||
]
|
||||
, HH.main [ css "main" ]
|
||||
[ renderChildren st
|
||||
, HH.hr_
|
||||
, HH.div [ css "logger" ]
|
||||
[ HH.h2_ [ HH.text "Logger Output:" ]
|
||||
, HH.ul_ (map (\x -> HH.li_ [ HH.text x ]) st.loggerHist)
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
renderNavItem :: forall w i. Route -> String -> String -> HH.HTML w i
|
||||
renderNavItem r i label =
|
||||
HH.li
|
||||
[ css "nav-li" ]
|
||||
[ HH.text i
|
||||
, HH.a
|
||||
[ safeHref r, HP.style "margin-left: 0.25rem;" ]
|
||||
[ HH.text label ]
|
||||
]
|
||||
|
||||
renderChildren :: Router_State -> RouterHTML
|
||||
renderChildren = case _ of
|
||||
{ route: Nothing } ->
|
||||
HH.slot_ (Proxy :: _ "home") unit componentHome unit
|
||||
|
||||
{ route: Just Home } ->
|
||||
HH.slot_ (Proxy :: _ "home") unit componentHome unit
|
||||
|
||||
{ currentUser, route: Just Secrets } ->
|
||||
authorize currentUser do
|
||||
HH.slot_ (Proxy :: _ "secrets") unit componentSecrets unit
|
||||
|
||||
where
|
||||
authorize :: Maybe Profile -> HH.HTML _ _ -> HH.HTML _ _
|
||||
authorize mProfile html = case mProfile of
|
||||
Nothing -> HH.slot_ (Proxy :: _ "home") unit componentHome unit
|
||||
Just _ -> html
|
||||
|
||||
|
||||
data AnchorClickResult
|
||||
= LocalRoute Route
|
||||
| LocalRouteError { anchorPath :: String, routeError :: RouteError }
|
||||
| ForeignRoute { anchorHost :: String, anchorHref :: String }
|
||||
|
||||
withClickedAnchor_
|
||||
:: Event
|
||||
-> RouteDuplex' Route
|
||||
-> (AnchorClickResult -> RouterM Unit)
|
||||
-> RouterM Unit
|
||||
withClickedAnchor_ event codec' parsedRouteAction =
|
||||
void $ runMaybeT do
|
||||
target <- hoistMaybe $ Event.target event
|
||||
anchor <- getAnchorIfTarget target <|> walkupDomToAnchor target
|
||||
let
|
||||
utils = HTMLAnchorElement.toHTMLHyperlinkElementUtils anchor
|
||||
siteHost <- H.liftEffect $ Location.host =<< Window.location =<< HTML.window
|
||||
anchorHost <- H.liftEffect $ Utils.host utils
|
||||
anchorPath <- H.liftEffect $ Utils.pathname utils
|
||||
case RD.parse codec' anchorPath of
|
||||
Left routeError ->
|
||||
lift $ parsedRouteAction $ LocalRouteError { anchorPath, routeError }
|
||||
Right localRoute
|
||||
| siteHost == anchorHost -> do
|
||||
lift $ parsedRouteAction $ LocalRoute localRoute
|
||||
| otherwise -> do
|
||||
anchorHref <- H.liftEffect $ Utils.href utils
|
||||
lift $ parsedRouteAction $ ForeignRoute { anchorHost, anchorHref }
|
||||
where
|
||||
getAnchorIfTarget :: EventTarget -> MaybeT RouterM HTMLAnchorElement
|
||||
getAnchorIfTarget eventTarget =
|
||||
hoistMaybe (HTMLAnchorElement.fromEventTarget eventTarget)
|
||||
|
||||
walkupDomToAnchor :: EventTarget -> MaybeT RouterM HTMLAnchorElement
|
||||
walkupDomToAnchor eventTarget = do
|
||||
targetElement <- hoistMaybe $ Element.fromEventTarget eventTarget
|
||||
targetElement # Rec.tailRecM \currentElement ->
|
||||
case HTMLAnchorElement.fromElement currentElement of
|
||||
Nothing -> do
|
||||
parent <- MaybeT $ H.liftEffect $ Node.parentElement (Element.toNode currentElement)
|
||||
pure $ Rec.Loop parent
|
||||
Just anchor -> do
|
||||
pure $ Rec.Done anchor
|
||||
|
||||
|
||||
hoistMaybe :: forall m a. Monad m => Maybe a -> MaybeT m a
|
||||
hoistMaybe = MaybeT <<< pure
|
||||
|
||||
-- Component.HTML.Util
|
||||
|
||||
css :: forall r i. String -> HH.IProp ( class :: String | r ) i
|
||||
css = HP.class_ <<< HH.ClassName
|
||||
|
||||
safeHref :: forall r i. Route -> HH.IProp ( href :: String | r) i
|
||||
safeHref = HP.href <<< routePrint
|
||||
|
||||
whenElem :: forall w i. Boolean -> (Unit -> HH.HTML w i) -> HH.HTML w i
|
||||
whenElem cond f = if cond then f unit else HH.text ""
|
||||
|
||||
maybeElem :: forall w i a. Maybe a -> (a -> HH.HTML w i) -> HH.HTML w i
|
||||
maybeElem (Just x) f = f x
|
||||
maybeElem _ _ = HH.text ""
|
||||
|
||||
----------
|
||||
-- Page.Home
|
||||
-- (visible to both logged-in/-out users)
|
||||
|
||||
type Home_State =
|
||||
{ currentUser :: Maybe Profile
|
||||
}
|
||||
|
||||
data Home_Action
|
||||
= Home_Initialize
|
||||
| Home_Receive { context :: Maybe Profile, input :: Unit }
|
||||
| Home_Login Event
|
||||
|
||||
type Home_Slot = H.Slot (Const Void) Void Unit
|
||||
|
||||
type HomeM = H.HalogenM Home_State Home_Action () Void AppM
|
||||
type HomeHTML = H.ComponentHTML Home_Action () AppM
|
||||
|
||||
componentHome :: H.Component (Const Void) Unit Void AppM
|
||||
componentHome =
|
||||
HSC.connect (HSS.selectEq _.currentUser) $
|
||||
H.mkComponent { initialState, render, eval }
|
||||
where
|
||||
initialState :: ConnectedInput -> Home_State
|
||||
initialState =
|
||||
\{ context: currentUser } ->
|
||||
{ currentUser
|
||||
}
|
||||
|
||||
eval = H.mkEval H.defaultEval
|
||||
{ handleAction = handleAction
|
||||
, initialize = Just Home_Initialize
|
||||
, receive = Just <<< Home_Receive
|
||||
}
|
||||
|
||||
render :: Home_State -> HomeHTML
|
||||
render { currentUser } =
|
||||
HH.div
|
||||
[ css "page" ]
|
||||
[ HH.h1_ [ HH.text "Home page" ]
|
||||
, HH.p_ [ HH.text message ]
|
||||
, whenElem (isNothing currentUser) \_ ->
|
||||
HH.button
|
||||
[ HP.type_ HP.ButtonButton, HE.onClick (Home_Login <<< MouseEvent.toEvent) ]
|
||||
[ HH.text "Login" ]
|
||||
]
|
||||
where
|
||||
message :: String
|
||||
message = case currentUser of
|
||||
Nothing -> "Welcome, guest. Log in?"
|
||||
Just { username } -> "WELCOME HOME, " <> username <> "!"
|
||||
|
||||
handleAction :: Home_Action -> HomeM Unit
|
||||
handleAction = case _ of
|
||||
Home_Initialize ->
|
||||
log $ "NOTE: If you're in TryPureScript's <iframe>, PushState routing will not work"
|
||||
Home_Receive i ->
|
||||
updateLocalState i
|
||||
Home_Login event -> do
|
||||
H.liftEffect $ Event.preventDefault event
|
||||
loginUser "Alice" >>= case _ of
|
||||
Nothing ->
|
||||
pure unit
|
||||
Just { username } ->
|
||||
log $ username <> " has logged in!"
|
||||
|
||||
|
||||
-- Page.Secrets
|
||||
-- (visible to logged-in users)
|
||||
|
||||
type Secrets_State =
|
||||
{ currentUser :: Maybe Profile
|
||||
}
|
||||
|
||||
data Secrets_Action
|
||||
= Secrets_Initialize
|
||||
| Secrets_Receive { context :: (Maybe Profile), input :: Unit }
|
||||
| Secrets_Logout Event
|
||||
|
||||
type Secrets_Slot = H.Slot (Const Void) Void Unit
|
||||
|
||||
type SecretsM = H.HalogenM Secrets_State Secrets_Action () Void AppM
|
||||
type SecretsHTML = H.ComponentHTML Secrets_Action () AppM
|
||||
|
||||
componentSecrets :: H.Component (Const Void) Unit Void AppM
|
||||
componentSecrets =
|
||||
HSC.connect (HSS.selectEq _.currentUser) $
|
||||
H.mkComponent { initialState, render, eval }
|
||||
where
|
||||
initialState :: ConnectedInput -> Secrets_State
|
||||
initialState =
|
||||
\{ context: currentUser } ->
|
||||
{ currentUser
|
||||
}
|
||||
|
||||
eval =
|
||||
H.mkEval H.defaultEval
|
||||
{ handleAction = handleAction
|
||||
, initialize = Just Secrets_Initialize
|
||||
, receive = Just <<< Secrets_Receive
|
||||
}
|
||||
|
||||
render :: Secrets_State -> SecretsHTML
|
||||
render { currentUser } =
|
||||
maybeElem currentUser \{ username } -> do
|
||||
HH.div
|
||||
[ css "page" ]
|
||||
[ HH.h1_ [ HH.text (username <> "'s page of secrets") ]
|
||||
, HH.p_
|
||||
[ HH.text "The answer is always: ", HH.code [ css "code" ] [ HH.text "traverse" ] ]
|
||||
, HH.button
|
||||
[ HE.onClick (Secrets_Logout <<< MouseEvent.toEvent) ]
|
||||
[ HH.text "Sign out" ]
|
||||
]
|
||||
|
||||
handleAction :: Secrets_Action -> SecretsM Unit
|
||||
handleAction = case _ of
|
||||
Secrets_Initialize ->
|
||||
H.gets _.currentUser >>= case _ of
|
||||
Nothing -> logoutUser
|
||||
Just _ -> pure unit
|
||||
|
||||
Secrets_Receive input ->
|
||||
updateLocalState input
|
||||
|
||||
Secrets_Logout event -> do
|
||||
H.liftEffect $ Event.preventDefault event
|
||||
log "Signing out" *> logoutUser
|
||||
|
||||
|
||||
-- CSS
|
||||
|
||||
stylesheet :: String
|
||||
stylesheet =
|
||||
"""
|
||||
.body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-family: Helvetica Neue, Helvetica, Arial, sans-serif;
|
||||
justify-content: space-between;
|
||||
height: calc(100vh - 18px);
|
||||
}
|
||||
|
||||
.main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
height: calc(100vh - 100px);
|
||||
}
|
||||
|
||||
.page {
|
||||
flex: auto;
|
||||
padding: 0.5rem 1.5rem;
|
||||
}
|
||||
|
||||
.logger {
|
||||
background: #282c34;
|
||||
flex-basis: 275px;
|
||||
color: #e06c75;
|
||||
font-family: 'Consolas';
|
||||
padding: 5px 20px 5px 20px;
|
||||
}
|
||||
|
||||
.code {
|
||||
color: #d63384;
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
list-style: none;
|
||||
padding: 0.5rem 1.0rem;
|
||||
border-bottom: 1px solid black;
|
||||
}
|
||||
|
||||
.nav-ul {
|
||||
list-style-type: none;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
|
||||
.nav-li {
|
||||
display: inline-block;
|
||||
padding: .5em 1em;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
"""
|
Loading…
Reference in New Issue
Block a user