|
|
|
@ -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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
"""
|