summaryrefslogtreecommitdiff
path: root/Omni/Jr/Web.hs
blob: f0036d15be3d0c92265a29d99a48a8f6f9d5d2fb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
{-# 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