{-# LANGUAGE GHC2021 #-} {-# LANGUAGE OverloadedStrings #-} -- | Now page: loads data/now.yaml and renders the active-projects view -- and the recently-shipped archive for /current.html. Page-level -- "Last updated" stamp is exposed as a context field; relative time -- ("4 days ago") is computed at build time from getCurrentTime. module Now ( nowCtx ) where import Data.Aeson (FromJSON (..), withObject, (.:), (.:?), (.!=)) import Data.Char (toUpper) import Data.List (nub, sortBy) import Data.Maybe (fromMaybe) import Data.Ord (Down (..), comparing) import Data.Time.Calendar (Day, diffDays) import Data.Time.Clock (UTCTime (..), getCurrentTime) import Data.Time.Format (defaultTimeLocale, formatTime, parseTimeM) import qualified Data.Text as T import qualified Data.Text.Encoding as TE import qualified Data.Yaml as Y import Hakyll hiding (escapeHtml) import Contexts (siteCtx) import Utils (escapeHtml) -- --------------------------------------------------------------------------- -- Entry types -- --------------------------------------------------------------------------- data NowEntry = NowEntry { neTitle :: String , neSection :: String , neStatus :: String , neUpdated :: String , neLink :: Maybe String , neNote :: Maybe String , nePriority :: Int } instance FromJSON NowEntry where parseJSON = withObject "NowEntry" $ \o -> NowEntry <$> o .: "title" <*> o .: "section" <*> o .: "status" <*> o .: "updated" <*> o .:? "link" <*> o .:? "note" <*> o .:? "priority" .!= 0 data NowShipped = NowShipped { nsTitle :: String , nsCompleted :: String , nsLink :: Maybe String , nsNote :: Maybe String } instance FromJSON NowShipped where parseJSON = withObject "NowShipped" $ \o -> NowShipped <$> o .: "title" <*> o .: "completed" <*> o .:? "link" <*> o .:? "note" data NowDoc = NowDoc { nLastUpdated :: String , nEntries :: [NowEntry] , nShipped :: [NowShipped] } instance FromJSON NowDoc where parseJSON = withObject "NowDoc" $ \o -> NowDoc <$> o .: "last-updated" <*> o .:? "entries" .!= [] <*> o .:? "shipped" .!= [] -- --------------------------------------------------------------------------- -- Helpers -- --------------------------------------------------------------------------- -- | Section ordering follows first-appearance in entries. Reorder the -- YAML to reorder the page; no separate ordering key required. sectionOrder :: [NowEntry] -> [String] sectionOrder = nub . map neSection -- | Status ordering — "how close to shipping." Lower rank sorts first. -- Statuses not listed sort below all known ones (rank 99) so a typo -- surfaces visibly at the bottom of its section instead of silently -- ranking next-to-the-top. statusRanks :: [(String, Int)] statusRanks = [ ("in-review", 1) , ("revising", 2) , ("drafting", 3) , ("building", 4) , ("early-stage", 5) , ("paused", 6) ] statusRank :: String -> Int statusRank s = fromMaybe 99 (lookup s statusRanks) -- | Three-tier sort key for active entries: -- 1. priority — manual override; higher floats up (default 0) -- 2. statusRank — how close to shipping (lower is closer) -- 3. updated — recency tiebreaker within the same rank -- Sectioning is applied to the *unsorted* list so section ordering -- continues to follow YAML source order; sorting happens within each -- section's filtered slice. entrySortKey :: NowEntry -> (Down Int, Int, Down String) entrySortKey e = ( Down (nePriority e) , statusRank (neStatus e) , Down (neUpdated e) ) -- | "early-stage" → "Early Stage", "research" → "Research". titleCaseWords :: String -> String titleCaseWords = unwords . map cap . wordsOnDash where cap [] = [] cap (x:xs) = toUpper x : xs wordsOnDash s = case break (== '-') s of (a, []) -> [a] (a, _:rest) -> a : wordsOnDash rest -- --------------------------------------------------------------------------- -- HTML rendering -- --------------------------------------------------------------------------- renderStatusChip :: String -> String renderStatusChip s = concat [ "" , escapeHtml (titleCaseWords s) , "" ] -- | Active-entry card. Reuses the .item-card / .item-card-* classes from -- item-card.css so the Now page picks up the existing typographic -- register; the .now-* classes layer status-chip + spacing on top. renderEntry :: NowEntry -> String renderEntry e = concat [ "
" ++ escapeHtml n ++ "
") (neNote e) , "" ++ escapeHtml n ++ "
") (nsNote s) , "