Main.hs (5028B)
1 {-# LANGUAGE MultiParamTypeClasses #-} 2 {-# LANGUAGE OverloadedStrings #-} 3 {-# LANGUAGE QuasiQuotes #-} 4 {-# LANGUAGE TemplateHaskell #-} 5 {-# LANGUAGE TypeFamilies #-} 6 7 module Main where 8 9 import Control.Monad.Logger (runStderrLoggingT) 10 import Control.Monad.Trans.Resource (runResourceT) 11 import Data.ByteString.Lazy as BS 12 import Data.List as List 13 import Data.Text as T 14 import Data.Text.Lazy qualified as T (toStrict) 15 import Data.Text.Lazy.Builder qualified as B 16 import Data.Text.Lazy.Builder.Int qualified as B 17 import Data.Time.Clock (nominalDiffTimeToSeconds) 18 import Data.Time.Clock.POSIX (posixSecondsToUTCTime, utcTimeToPOSIXSeconds) 19 import Database.Persist.Postgresql (ConnectionPool, ConnectionString, SqlBackend, runMigration, runSqlPool, withPostgresqlPool) 20 import Debug.Trace (traceShow) 21 import Point qualified (fetch, migration, save) 22 import Session qualified 23 import Track (parseBS) 24 import Yesod.Core 25 import Yesod.Form.Fields 26 import Yesod.Form.Functions 27 import Yesod.Form.Input 28 import Yesod.Form.Types 29 import Yesod.Persist 30 import Yesod.Static 31 32 staticFiles "frontend" -- this param appears to be a pathname 33 34 data Souplesse = Souplesse ConnectionPool Static 35 36 -- ref https://www.yesodweb.com/book/routing-and-handlers 37 -- for adding params (start/end) to the timeline route 38 mkYesod 39 "Souplesse" 40 [parseRoutes| 41 / CalendarR GET 42 /timeline TimelineR GET 43 /upload UploadR POST 44 /static StaticR Static getStatic 45 /points PointsR GET 46 |] 47 48 getStatic :: Souplesse -> Static 49 getStatic y = 50 let Souplesse _ s = y 51 in s 52 53 instance Yesod Souplesse 54 55 instance YesodPersist Souplesse where 56 type YesodPersistBackend Souplesse = SqlBackend 57 58 runDB action = do 59 Souplesse pool _ <- getYesod 60 runSqlPool action pool 61 62 intToText :: (Integral a) => a -> T.Text 63 intToText = T.toStrict . B.toLazyText . B.decimal 64 65 getCalendarR :: Handler Html 66 getCalendarR = do 67 let fTime = intToText . floor . utcTimeToPOSIXSeconds 68 fDur = intToText . ceiling . nominalDiffTimeToSeconds 69 (formWidget, _) <- generateFormPost uploadForm 70 sessions' <- runDB Session.recents 71 defaultLayout 72 [whamlet| 73 <h1>Calendar 74 75 <p>A calendar view goes here 76 <ul> 77 $forall s <- sessions' 78 <li> 79 <a href=@?{(TimelineR, [("start", fTime $ Session.startTime s), ("duration", fDur $ Session.duration s)])} > #{show $ Session.startTime s} #{show (Session.duration s)} 80 81 <form action="/upload" method=post enctype="multipart/form-data"> 82 ^{formWidget} 83 <input type="submit" name="send" value="send"> 84 85 <a href="/timeline">timeline 86 © 2024 Daniel Barlow 87 |] 88 89 getTimelineR :: Handler Html 90 -- what's this about? <img src=@{StaticR image_png}/>|] 91 getTimelineR = defaultLayout $ do 92 setTitle "timeline" 93 addScriptRemote "/static/frontend.js" 94 toWidgetBody 95 [julius| 96 window.addEventListener("load", function(){ 97 var app = Elm.Main.init({ 98 node: document.getElementById("elm") 99 }); 100 }) 101 |] 102 toWidget 103 [hamlet| 104 <pre id="elm"> 105 <p>Copyright © 2024 Daniel Barlow 106 |] 107 108 type Form x = Html -> MForm (HandlerFor Souplesse) (FormResult x, Widget) 109 110 data Timerange = Timerange {start :: Int, duration :: Int} deriving (Show) 111 112 getPointsR :: Handler Value 113 getPointsR = do 114 tr <- 115 runInputGet $ 116 Timerange 117 <$> ireq intField "start" 118 <*> ireq intField "duration" 119 let start' = posixSecondsToUTCTime $ fromInteger $ toInteger $ start tr 120 duration' = fromInteger $ toInteger $ duration tr 121 points <- runDB $ Point.fetch start' duration' 122 returnJson (traceShow tr points) 123 124 data FileForm = FileForm 125 { fileInfo :: FileInfo 126 } 127 128 uploadForm :: Form FileForm 129 uploadForm = 130 renderDivs $ 131 FileForm 132 <$> fileAFormReq "choose file" 133 134 instance RenderMessage Souplesse FormMessage where 135 renderMessage _ _ = defaultFormMessage 136 137 postUploadR :: Handler Html 138 postUploadR = do 139 ((result, _), _) <- runFormPostNoToken uploadForm 140 141 case result of 142 FormSuccess upload -> do 143 bs <- fileSourceByteString $ fileInfo upload 144 case Track.parseBS (fromStrict bs) of 145 Right points -> do 146 eitherPoints <- runDB $ Point.save points 147 case eitherPoints of 148 Right points' -> 149 defaultLayout [whamlet|<p>#{List.length points'} points - thanks!|] 150 Left _ -> 151 defaultLayout [whamlet|<p>overlap error |] 152 Left _ -> 153 defaultLayout 154 [whamlet|<p>parse error |] 155 FormMissing -> 156 defaultLayout [whamlet|<p>FormMissing (?)|] 157 FormFailure errors -> 158 defaultLayout 159 [whamlet| 160 <h1>Error uploading 161 <ul> 162 $forall err <- errors 163 <li>#{err} 164 165 |] 166 167 connStr :: ConnectionString 168 connStr = "host=127.0.0.1 port=5432 user=souplesse dbname=souplesse password=secret" 169 170 main :: IO () 171 main = runStderrLoggingT $ withPostgresqlPool connStr 10 $ \pool -> liftIO $ do 172 runResourceT $ flip runSqlPool pool $ do 173 runMigration Session.migration 174 runMigration Point.migration 175 static' <- static "frontend" 176 warp 3000 $ Souplesse pool static'