{-# LANGUAGE DataKinds #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TypeOperators #-} {-# LANGUAGE NoImplicitPrelude #-} -- : dep warp -- : dep servant-server -- : dep lucid -- : dep servant-lucid module Omni.Jr.Web ( run, defaultPort, ) 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 :<|> "tasks" :> Get '[Lucid.HTML] TaskListPage newtype HomePage = HomePage () newtype TaskListPage = TaskListPage [TaskCore.Task] instance Lucid.ToHtml HomePage where toHtmlRaw = Lucid.toHtml toHtml (HomePage ()) = Lucid.doctypehtml_ <| do Lucid.head_ <| do Lucid.title_ "Jr Web UI" Lucid.meta_ [Lucid.charset_ "utf-8"] Lucid.meta_ [ Lucid.name_ "viewport", Lucid.content_ "width=device-width, initial-scale=1" ] 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 = 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 run :: Warp.Port -> IO () run port = do putText <| "Starting Jr web server on port " <> tshow port Warp.run port app