Add landing page with cook log integration
Build and Deploy / build-and-deploy (push) Failing after 1m15s
Build and Deploy / build-and-deploy (push) Failing after 1m15s
- New landing page (/) based on roux-landing-mockup.html layout - Shuffle card that suggests random recipes - Recently cooked shelf (driven by cook log DB) - Recently added shelf (by file modification time) - Existing recipe index moved to /recipes - Updated nav links throughout to point to /recipes - All hlint warnings fixed
This commit is contained in:
+57
-15
@@ -20,7 +20,8 @@ import Data.ByteString.Lazy (ByteString)
|
||||
import qualified Data.ByteString.Lazy as LB
|
||||
import Data.Char (digitToInt, isAlphaNum, toLower)
|
||||
import Data.IORef (IORef, atomicModifyIORef', newIORef, readIORef, writeIORef)
|
||||
import Data.List (isSuffixOf)
|
||||
import Data.List (isSuffixOf, sortOn)
|
||||
import Data.Ord (Down (Down))
|
||||
import Data.Map.Strict (Map)
|
||||
import qualified Data.Map.Strict as Map
|
||||
import Data.Text (Text)
|
||||
@@ -29,7 +30,7 @@ import Data.Text.Encoding (encodeUtf8)
|
||||
|
||||
import qualified Network.HTTP.Types as HTTP
|
||||
import qualified Network.Wai as Wai
|
||||
import System.Directory (createDirectoryIfMissing, doesFileExist, findExecutable, makeAbsolute, removeFile)
|
||||
import System.Directory (createDirectoryIfMissing, doesFileExist, findExecutable, getModificationTime, makeAbsolute, removeFile)
|
||||
import System.FSNotify (
|
||||
Event (..),
|
||||
EventIsDirectory (..),
|
||||
@@ -44,7 +45,7 @@ import System.FilePath (makeRelative, takeBaseName, takeExtension, (</>))
|
||||
import qualified Data.Aeson as A
|
||||
import qualified Data.Aeson.KeyMap as KM
|
||||
import qualified Data.ByteString as BS
|
||||
import Data.Maybe (fromMaybe, mapMaybe)
|
||||
import Data.Maybe (catMaybes, fromMaybe, mapMaybe)
|
||||
import qualified Data.Text.Encoding as TE
|
||||
import Data.Time.Calendar (Day, fromGregorianValid)
|
||||
import Data.Time.Clock (diffUTCTime, getCurrentTime, utctDay)
|
||||
@@ -200,7 +201,7 @@ router recipeDir config state changeLog db request respond =
|
||||
| Just filename <- dispatchLogPost pathInfo
|
||||
, Wai.requestMethod request == "POST" ->
|
||||
handleCookLogPost db filename request respond
|
||||
_ -> handleRequest state db request respond
|
||||
_ -> handleRequest recipeDir state db request respond
|
||||
|
||||
-- | Extract filename from /cook-log/{filename} POST path, if it matches.
|
||||
dispatchLogPost :: [Text] -> Maybe Text
|
||||
@@ -622,17 +623,29 @@ reReadRecipe dir path state changeLog = do
|
||||
putStrLn $ "[roux] updated " <> path
|
||||
|
||||
-- | Route an incoming request to the appropriate handler.
|
||||
handleRequest :: IORef (Map FilePath Idx.RecipeInfo) -> SQL.Connection -> Wai.Application
|
||||
handleRequest state db request respond = do
|
||||
handleRequest :: FilePath -> IORef (Map FilePath Idx.RecipeInfo) -> SQL.Connection -> Wai.Application
|
||||
handleRequest recipeDir state db request respond = do
|
||||
recipes <- readIORef state
|
||||
let recipeList = Map.elems recipes
|
||||
case Wai.pathInfo request of
|
||||
-- Index page (default: alphabetical)
|
||||
[] -> respond (htmlResponse (Html.indexPage Html.AlphaSort recipeList))
|
||||
-- Index page (sorted by tags) — sort mode ignored, driven client-side
|
||||
["sorted", "tags"] -> respond (htmlResponse (Html.indexPage Html.AlphaSort recipeList))
|
||||
-- Index page (sorted by course/category) — sort mode ignored, driven client-side
|
||||
["sorted", "course"] -> respond (htmlResponse (Html.indexPage Html.AlphaSort recipeList))
|
||||
-- Landing page (root)
|
||||
[] -> do
|
||||
allEntries <- CookLog.fetchAllEntries db
|
||||
let okRecipes = filter (\r -> case Idx.riRecipe r of Right _ -> True; _ -> False) recipeList
|
||||
-- Deduplicate cook entries by recipe filename, keep most recent
|
||||
let deduped = deduplicateByRecipe allEntries
|
||||
-- Look up recipe info for each recently cooked entry
|
||||
lookupInfo e = case Map.lookup (map toLower (stripCookExt (T.unpack (CookLog.ceRecipeFilename e)))) recipes of
|
||||
Just info -> Just (e, info)
|
||||
Nothing -> Nothing
|
||||
cookedWithInfo = mapMaybe lookupInfo (take 10 deduped)
|
||||
-- Get recently added recipes (by modification time)
|
||||
recentlyAdded <- getRecentlyAdded recipeDir okRecipes
|
||||
respond $ htmlResponse (Html.landingPage okRecipes cookedWithInfo recentlyAdded)
|
||||
-- Recipe index page
|
||||
["recipes"] -> respond (htmlResponse (Html.indexPage Html.AlphaSort recipeList))
|
||||
["recipes", "sorted", "tags"] -> respond (htmlResponse (Html.indexPage Html.AlphaSort recipeList))
|
||||
["recipes", "sorted", "course"] -> respond (htmlResponse (Html.indexPage Html.AlphaSort recipeList))
|
||||
-- Recipe detail page
|
||||
["recipes", rawPath] ->
|
||||
case lookupRecipe (urlDecodePath rawPath) recipes of
|
||||
@@ -665,9 +678,38 @@ handleRequest state db request respond = do
|
||||
(A.encode entries)
|
||||
_ -> respond notFound
|
||||
|
||||
-- | Handle POST /cook-log/{filename}
|
||||
-- | Normalize a recipe filename to match the recipe index key:
|
||||
-- lowercased, with @.cook@ extension stripped.
|
||||
-- | Deduplicate cook entries by recipe filename, keeping the most recent entry per recipe.
|
||||
deduplicateByRecipe :: [CookLog.CookEntry] -> [CookLog.CookEntry]
|
||||
deduplicateByRecipe entries =
|
||||
let seen = foldl' addIfNew [] entries
|
||||
in reverse seen
|
||||
where
|
||||
addIfNew acc e =
|
||||
if any ((== CookLog.ceRecipeFilename e) . CookLog.ceRecipeFilename) acc
|
||||
then acc
|
||||
else e : acc
|
||||
|
||||
-- | Get recently added recipes sorted by modification time (newest first).
|
||||
getRecentlyAdded :: FilePath -> [Idx.RecipeInfo] -> IO [Idx.RecipeInfo]
|
||||
getRecentlyAdded recipeDir recipes = do
|
||||
times <-
|
||||
mapM
|
||||
( \r -> do
|
||||
let filepath = recipeDir </> Idx.riFilename r
|
||||
result <- try (getModificationTime filepath)
|
||||
case result of
|
||||
Left (_ :: IOException) -> pure Nothing
|
||||
Right t -> pure (Just (r, t))
|
||||
)
|
||||
recipes
|
||||
let valid = catMaybes times
|
||||
sorted = sortOn (Down . snd) valid
|
||||
pure (Prelude.map fst (take 10 sorted))
|
||||
|
||||
{- | Handle POST /cook-log/{filename}
|
||||
| Normalize a recipe filename to match the recipe index key:
|
||||
lowercased, with @.cook@ extension stripped.
|
||||
-}
|
||||
normalizeRecipeKey :: Text -> Text
|
||||
normalizeRecipeKey = T.pack . map toLower . stripCookExt . T.unpack
|
||||
|
||||
|
||||
Reference in New Issue
Block a user