To generate our mood tracker view, we need … mood data, i.e., the mood Model type. If we are recording our mood each day, then Haskell’s Map type is one way to represent moods over time.
data Model = Model
{ modelDays :: Map Date Mood
}
data Mood = Bad | Neutral | Good
deriving stock (Show, Read)
Now we want to associate our Route type from Add Routes with this Model. This can be done as follows:
-
When genericaly deriving routes, use the
WithModeloption to associate a model for that route. -
Use the same1 model in the
IsRouteinstance for subroutes (here,Date). -
Change
EmaSite’ssiteInputmethod to return the model; andsiteOutputto use the new model
To achieve (1), we would change the deriving clause for our Route to the following:
deriveGeneric ''Route
deriveIsRoute ''Route [t|'[ WithModel Model ]|]
To achieve (2):
instance IsRoute Date where
type RouteModel Date = Model -- ^ We changed `()` to `Model`
routePrism (Model _moods) = toPrism_ $
prism'
( \(Date (y, m, d)) ->
formatTime defaultTimeLocale "%Y-%m-%d.html" $
fromGregorian y m d
)
( fmap (Date . toGregorian)
. parseTimeM False defaultTimeLocale "%Y-%m-%d.html"
)
routeUniverse (Model moods) = Map.keys moods -- ^ We implemented this
Notice how this time we are able to properly define routeUniverse (it is used during static site generation, to determine which routes to generate on disk), because the model value is available. routePrism also gets the model as an argument, but in this case we have no need for it (in theory, we could check that a date exists before decoding successfully).
Finally, (3) is where we get to produce (siteInput) and consume (siteOutput) the model when rendering the site. The next section explains this in detail.
Use Model
We are yet to use our model to do anything meaningful. The most meaningful thing to do here is to render HTML for our routes. Change the siteOutput to following (we use blaze-html library):
instance EmaSite Route where
siteInput _ _ = pure $ pure $ Model mempty -- Empty model for now
siteOutput rp model r =
pure . Ema.AssetGenerated Ema.Html . RU.renderHtml $ do
H.docType
H.html ! A.lang "en" $ do
H.head $ do
H.title "Mood tracker"
H.body $ case r of
Route_Index -> do
H.h1 "Mood tracker"
-- Just list the moods
forM_ (Map.toList $ modelDays model) $ \(date, mood) -> do
H.li $ do
let url = Ema.routeUrl rp $ Route_Date date
H.a ! A.href (H.toValue url) $ show date
": "
show mood
Route_Date d -> do
H.h1 (show d)
H.pre $ show $ Map.lookup d (modelDays model)
This should render both / (Route_Index) and, say, /date/2020-01-01.html (Route_Date ...) in your browser. However, it won’t have any moods since our Model is empty per the siteInput definition! Let’s fix that.
Represent Model using CSV
Ultimately the value for our Model will come from elsewhere, such as a CSV file on disk. Let’s use cassava to parse this CSV and load it into our Model.
First, add a sample CSV file under ./data/moods.csv containing:
2022-04-23,Good
2022-04-24,Neutral
Now change the siteInput function to replace mempty with the contents of this CSV file loaded as Model:
import Data.Csv qualified as Csv
instance EmaSite Route where
siteInput _ _ = do
s <- readFileLBS "data/moods.csv"
case toList <$> Csv.decode Csv.NoHeader s of
Left err -> throw $ userError err
Right moods ->
pure $ pure $ Model $ Map.fromList moods
Note that this will require that you define cassava’s FromField instances on Date and Mood types. A simple implementation is provided below:
instance Csv.FromField Date where
parseField f = do
s <- Csv.parseField @String f
case parseTimeM False defaultTimeLocale "%Y-%m-%d" s of
Left err -> fail err
Right date ->
pure $ Date $ toGregorian date
instance Csv.FromField Mood where
parseField f = do
s <- Csv.parseField @String f
case readEither @Mood s of
Left err -> fail $ toString err
Right v -> pure v
The result is that our site’s index page will display the moods in the CSV file, along with the link to the particular day routes (Route_Date).
This is great so far—we can track how we feel in moods.csv and get an app-like “view” of it. But, we don’t have Hot Reload. Changing data/moods.csv ought to update our site. The final step of our tutorial series will explain this.
Next, we will enable hot-reload on the mood model.