← Back to task

Commit 240a0557

commit 240a055709657f04b838d18325a647644fea90df
Author: Ben Sima <ben@bensima.com>
Date:   Wed Nov 26 06:01:07 2025

    Add task list view with mobile-friendly cards
    
    Task-Id: t-1o2g8gugkr1.2

diff --git a/Omni/Jr/Web.hs b/Omni/Jr/Web.hs
index 0a00f54d..f0036d15 100644
--- a/Omni/Jr/Web.hs
+++ b/Omni/Jr/Web.hs
@@ -1,5 +1,6 @@
 {-# LANGUAGE DataKinds #-}
 {-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TypeOperators #-}
 {-# LANGUAGE NoImplicitPrelude #-}
 
 -- : dep warp
@@ -15,16 +16,21 @@ where
 import Alpha
 import qualified Lucid
 import qualified Network.Wai.Handler.Warp as Warp
+import qualified Omni.Task.Core as TaskCore
 import Servant
 import qualified Servant.HTML.Lucid as Lucid
 
 defaultPort :: Warp.Port
 defaultPort = 8080
 
-type API = Get '[Lucid.HTML] HomePage
+type API =
+  Get '[Lucid.HTML] HomePage
+    :<|> "tasks" :> Get '[Lucid.HTML] TaskListPage
 
 newtype HomePage = HomePage ()
 
+newtype TaskListPage = TaskListPage [TaskCore.Task]
+
 instance Lucid.ToHtml HomePage where
   toHtmlRaw = Lucid.toHtml
   toHtml (HomePage ()) =
@@ -38,12 +44,84 @@ instance Lucid.ToHtml HomePage where
           ]
       Lucid.body_ <| do
         Lucid.h1_ "Jr Web UI"
+        Lucid.p_ <| Lucid.a_ [Lucid.href_ "/tasks"] "View Tasks"
+
+instance Lucid.ToHtml TaskListPage where
+  toHtmlRaw = Lucid.toHtml
+  toHtml (TaskListPage tasks) =
+    Lucid.doctypehtml_ <| do
+      Lucid.head_ <| do
+        Lucid.title_ "Tasks - Jr Web UI"
+        Lucid.meta_ [Lucid.charset_ "utf-8"]
+        Lucid.meta_
+          [ Lucid.name_ "viewport",
+            Lucid.content_ "width=device-width, initial-scale=1"
+          ]
+        Lucid.style_ styles
+      Lucid.body_ <| do
+        Lucid.h1_ "Tasks"
+        Lucid.div_ [Lucid.class_ "task-list"] <| do
+          traverse_ renderTask tasks
+    where
+      styles :: Text
+      styles =
+        "* { box-sizing: border-box; } \
+        \body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; \
+        \       margin: 0; padding: 16px; background: #f5f5f5; } \
+        \h1 { margin: 0 0 16px 0; } \
+        \.task-list { display: flex; flex-direction: column; gap: 8px; } \
+        \.task-card { background: white; border-radius: 8px; padding: 16px; \
+        \             box-shadow: 0 1px 3px rgba(0,0,0,0.1); } \
+        \.task-header { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-bottom: 8px; } \
+        \.task-id { font-family: monospace; color: #0066cc; text-decoration: none; \
+        \           font-size: 14px; padding: 4px 0; } \
+        \.task-id:hover { text-decoration: underline; } \
+        \.badge { display: inline-block; padding: 4px 8px; border-radius: 4px; \
+        \         font-size: 12px; font-weight: 500; } \
+        \.badge-open { background: #fef3c7; color: #92400e; } \
+        \.badge-inprogress { background: #dbeafe; color: #1e40af; } \
+        \.badge-review { background: #ede9fe; color: #6b21a8; } \
+        \.badge-approved { background: #d1fae5; color: #065f46; } \
+        \.badge-done { background: #d1fae5; color: #065f46; } \
+        \.priority { font-size: 12px; color: #6b7280; } \
+        \.task-title { font-size: 16px; margin: 0; }"
+
+      renderTask :: (Monad m) => TaskCore.Task -> Lucid.HtmlT m ()
+      renderTask t =
+        Lucid.div_ [Lucid.class_ "task-card"] <| do
+          Lucid.div_ [Lucid.class_ "task-header"] <| do
+            Lucid.a_
+              [ Lucid.class_ "task-id",
+                Lucid.href_ ("/tasks/" <> TaskCore.taskId t)
+              ]
+              (Lucid.toHtml (TaskCore.taskId t))
+            statusBadge (TaskCore.taskStatus t)
+            Lucid.span_ [Lucid.class_ "priority"] (Lucid.toHtml (tshow (TaskCore.taskPriority t)))
+          Lucid.p_ [Lucid.class_ "task-title"] (Lucid.toHtml (TaskCore.taskTitle t))
+
+      statusBadge :: (Monad m) => TaskCore.Status -> Lucid.HtmlT m ()
+      statusBadge status =
+        let (cls, label) = case status of
+              TaskCore.Open -> ("badge badge-open", "Open")
+              TaskCore.InProgress -> ("badge badge-inprogress", "In Progress")
+              TaskCore.Review -> ("badge badge-review", "Review")
+              TaskCore.Approved -> ("badge badge-approved", "Approved")
+              TaskCore.Done -> ("badge badge-done", "Done")
+         in Lucid.span_ [Lucid.class_ cls] label
 
 api :: Proxy API
 api = Proxy
 
 server :: Server API
-server = pure (HomePage ())
+server = homeHandler :<|> taskListHandler
+  where
+    homeHandler :: Servant.Handler HomePage
+    homeHandler = pure (HomePage ())
+
+    taskListHandler :: Servant.Handler TaskListPage
+    taskListHandler = do
+      tasks <- liftIO TaskCore.loadTasks
+      pure (TaskListPage tasks)
 
 app :: Application
 app = serve api server