diff options
author | Gregor Kleen <gkleen@yggdrasil.li> | 2017-03-14 01:06:28 +0100 |
---|---|---|
committer | Gregor Kleen <gkleen@yggdrasil.li> | 2017-03-14 01:06:28 +0100 |
commit | d84b462a711ce95593ff05a7581e722562c3835a (patch) | |
tree | 41e5af455fea925b2680b29718b24ba2876e803a | |
download | bar-d84b462a711ce95593ff05a7581e722562c3835a.tar bar-d84b462a711ce95593ff05a7581e722562c3835a.tar.gz bar-d84b462a711ce95593ff05a7581e722562c3835a.tar.bz2 bar-d84b462a711ce95593ff05a7581e722562c3835a.tar.xz bar-d84b462a711ce95593ff05a7581e722562c3835a.zip |
Implement old bar.hs
41 files changed, 1608 insertions, 0 deletions
diff --git a/.dir-locals.el b/.dir-locals.el new file mode 100644 index 0000000..a44395f --- /dev/null +++ b/.dir-locals.el | |||
@@ -0,0 +1,4 @@ | |||
1 | ((haskell-mode . ((haskell-indent-spaces . 4) | ||
2 | (haskell-process-use-ghci . t))) | ||
3 | (hamlet-mode . ((hamlet/basic-offset . 4) | ||
4 | (haskell-process-use-ghci . t)))) | ||
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5747830 --- /dev/null +++ b/.gitignore | |||
@@ -0,0 +1,23 @@ | |||
1 | dist* | ||
2 | static/tmp/ | ||
3 | static/combined/ | ||
4 | config/client_session_key.aes | ||
5 | *.hi | ||
6 | *.o | ||
7 | *.sqlite3 | ||
8 | *.sqlite3-shm | ||
9 | *.sqlite3-wal | ||
10 | .hsenv* | ||
11 | cabal-dev/ | ||
12 | .stack-work/ | ||
13 | yesod-devel/ | ||
14 | .cabal-sandbox | ||
15 | cabal.sandbox.config | ||
16 | .DS_Store | ||
17 | *.swp | ||
18 | *.keter | ||
19 | **/result | ||
20 | **/client_session_key.aes | ||
21 | **/.gup | ||
22 | static/webshim/ | ||
23 | static/jquery.js | ||
diff --git a/Application.hs b/Application.hs new file mode 100644 index 0000000..048a316 --- /dev/null +++ b/Application.hs | |||
@@ -0,0 +1,180 @@ | |||
1 | {-# OPTIONS_GHC -fno-warn-orphans #-} | ||
2 | module Application | ||
3 | ( getApplicationDev | ||
4 | , appMain | ||
5 | , develMain | ||
6 | , makeFoundation | ||
7 | , makeLogWare | ||
8 | -- * for DevelMain | ||
9 | , getApplicationRepl | ||
10 | , shutdownApp | ||
11 | -- * for GHCI | ||
12 | , handler | ||
13 | , db | ||
14 | ) where | ||
15 | |||
16 | import Control.Monad.Logger (liftLoc, runLoggingT) | ||
17 | import Database.Persist.Postgresql (createPostgresqlPool, pgConnStr, | ||
18 | pgPoolSize, runSqlPool) | ||
19 | import Import | ||
20 | import Language.Haskell.TH.Syntax (qLocation) | ||
21 | import Network.Wai (Middleware) | ||
22 | import Network.Wai.Handler.Warp (Settings, defaultSettings, | ||
23 | defaultShouldDisplayException, | ||
24 | runSettings, setHost, | ||
25 | setOnException, setPort, getPort) | ||
26 | import Network.Wai.Middleware.RequestLogger (Destination (Logger), | ||
27 | IPAddrSource (..), | ||
28 | OutputFormat (..), destination, | ||
29 | mkRequestLogger, outputFormat) | ||
30 | import System.Log.FastLogger (defaultBufSize, newStdoutLoggerSet, | ||
31 | toLogStr) | ||
32 | |||
33 | -- Import all relevant handler modules here. | ||
34 | -- Don't forget to add new modules to your cabal file! | ||
35 | import Handler.InventoryListing | ||
36 | import Handler.UpdateItem | ||
37 | import Handler.OpenItem | ||
38 | import Handler.DeleteItem | ||
39 | import Handler.Item | ||
40 | |||
41 | -- This line actually creates our YesodDispatch instance. It is the second half | ||
42 | -- of the call to mkYesodData which occurs in Foundation.hs. Please see the | ||
43 | -- comments there for more details. | ||
44 | mkYesodDispatch "App" resourcesApp | ||
45 | |||
46 | -- | This function allocates resources (such as a database connection pool), | ||
47 | -- performs initialization and returns a foundation datatype value. This is also | ||
48 | -- the place to put your migrate statements to have automatic database | ||
49 | -- migrations handled by Yesod. | ||
50 | makeFoundation :: AppSettings -> IO App | ||
51 | makeFoundation appSettings = do | ||
52 | -- Some basic initializations: HTTP connection manager, logger, and static | ||
53 | -- subsite. | ||
54 | appHttpManager <- newManager | ||
55 | appLogger <- newStdoutLoggerSet defaultBufSize >>= makeYesodLogger | ||
56 | |||
57 | -- We need a log function to create a connection pool. We need a connection | ||
58 | -- pool to create our foundation. And we need our foundation to get a | ||
59 | -- logging function. To get out of this loop, we initially create a | ||
60 | -- temporary foundation without a real connection pool, get a log function | ||
61 | -- from there, and then create the real foundation. | ||
62 | let mkFoundation appConnPool = App { appStatic = eStatic, ..} | ||
63 | -- The App {..} syntax is an example of record wild cards. For more | ||
64 | -- information, see: | ||
65 | -- https://ocharles.org.uk/blog/posts/2014-12-04-record-wildcards.html | ||
66 | tempFoundation = mkFoundation $ error "connPool forced in tempFoundation" | ||
67 | logFunc = messageLoggerSource tempFoundation appLogger | ||
68 | |||
69 | -- Create the database connection pool | ||
70 | pool <- flip runLoggingT logFunc $ createPostgresqlPool | ||
71 | (pgConnStr $ appDatabaseConf appSettings) | ||
72 | (pgPoolSize $ appDatabaseConf appSettings) | ||
73 | |||
74 | -- Perform database migration using our application's logging settings. | ||
75 | runLoggingT (runSqlPool (runMigration migrateAll) pool) logFunc | ||
76 | |||
77 | -- Return the foundation | ||
78 | return $ mkFoundation pool | ||
79 | |||
80 | -- | Convert our foundation to a WAI Application by calling @toWaiAppPlain@ and | ||
81 | -- applying some additional middlewares. | ||
82 | makeApplication :: App -> IO Application | ||
83 | makeApplication foundation = do | ||
84 | logWare <- makeLogWare foundation | ||
85 | -- Create the WAI application and apply middlewares | ||
86 | appPlain <- toWaiAppPlain foundation | ||
87 | return $ logWare $ defaultMiddlewaresNoLogging appPlain | ||
88 | |||
89 | makeLogWare :: App -> IO Middleware | ||
90 | makeLogWare foundation = | ||
91 | mkRequestLogger def | ||
92 | { outputFormat = | ||
93 | if appDetailedRequestLogging $ appSettings foundation | ||
94 | then Detailed True | ||
95 | else Apache | ||
96 | (if appIpFromHeader $ appSettings foundation | ||
97 | then FromFallback | ||
98 | else FromSocket) | ||
99 | , destination = Logger $ loggerSet $ appLogger foundation | ||
100 | } | ||
101 | |||
102 | |||
103 | -- | Warp settings for the given foundation value. | ||
104 | warpSettings :: App -> Settings | ||
105 | warpSettings foundation = | ||
106 | setPort (appPort $ appSettings foundation) | ||
107 | $ setHost (appHost $ appSettings foundation) | ||
108 | $ setOnException (\_req e -> | ||
109 | when (defaultShouldDisplayException e) $ messageLoggerSource | ||
110 | foundation | ||
111 | (appLogger foundation) | ||
112 | $(qLocation >>= liftLoc) | ||
113 | "yesod" | ||
114 | LevelError | ||
115 | (toLogStr $ "Exception from Warp: " ++ show e)) | ||
116 | defaultSettings | ||
117 | |||
118 | -- | For yesod devel, return the Warp settings and WAI Application. | ||
119 | getApplicationDev :: IO (Settings, Application) | ||
120 | getApplicationDev = do | ||
121 | settings <- getAppSettings | ||
122 | foundation <- makeFoundation settings | ||
123 | wsettings <- getDevSettings $ warpSettings foundation | ||
124 | app <- makeApplication foundation | ||
125 | return (wsettings, app) | ||
126 | |||
127 | getAppSettings :: IO AppSettings | ||
128 | getAppSettings = loadYamlSettings [configSettingsYml] [] useEnv | ||
129 | |||
130 | -- | main function for use by yesod devel | ||
131 | develMain :: IO () | ||
132 | develMain = develMainHelper getApplicationDev | ||
133 | |||
134 | -- | The @main@ function for an executable running this site. | ||
135 | appMain :: IO () | ||
136 | appMain = do | ||
137 | -- Get the settings from all relevant sources | ||
138 | settings <- loadYamlSettingsArgs | ||
139 | -- fall back to compile-time values, set to [] to require values at runtime | ||
140 | [configSettingsYmlValue] | ||
141 | |||
142 | -- allow environment variables to override | ||
143 | useEnv | ||
144 | |||
145 | -- Generate the foundation from the settings | ||
146 | foundation <- makeFoundation settings | ||
147 | |||
148 | -- Generate a WAI Application from the foundation | ||
149 | app <- makeApplication foundation | ||
150 | |||
151 | -- Run the application with Warp | ||
152 | runSettings (warpSettings foundation) app | ||
153 | |||
154 | |||
155 | -------------------------------------------------------------- | ||
156 | -- Functions for DevelMain.hs (a way to run the app from GHCi) | ||
157 | -------------------------------------------------------------- | ||
158 | getApplicationRepl :: IO (Int, App, Application) | ||
159 | getApplicationRepl = do | ||
160 | settings <- getAppSettings | ||
161 | foundation <- makeFoundation settings | ||
162 | wsettings <- getDevSettings $ warpSettings foundation | ||
163 | app1 <- makeApplication foundation | ||
164 | return (getPort wsettings, foundation, app1) | ||
165 | |||
166 | shutdownApp :: App -> IO () | ||
167 | shutdownApp _ = return () | ||
168 | |||
169 | |||
170 | --------------------------------------------- | ||
171 | -- Functions for use in development with GHCi | ||
172 | --------------------------------------------- | ||
173 | |||
174 | -- | Run a handler | ||
175 | handler :: Handler a -> IO a | ||
176 | handler h = getAppSettings >>= makeFoundation >>= flip unsafeHandler h | ||
177 | |||
178 | -- | Run DB queries | ||
179 | db :: ReaderT SqlBackend (HandlerT App IO) a -> IO a | ||
180 | db = handler . runDB | ||
diff --git a/Foundation.hs b/Foundation.hs new file mode 100644 index 0000000..85512a3 --- /dev/null +++ b/Foundation.hs | |||
@@ -0,0 +1,141 @@ | |||
1 | module Foundation where | ||
2 | |||
3 | import Import.NoFoundation | ||
4 | import Database.Persist.Sql (ConnectionPool, runSqlPool) | ||
5 | import Text.Hamlet (hamletFile) | ||
6 | |||
7 | import Yesod.Core.Types (Logger) | ||
8 | import qualified Yesod.Core.Unsafe as Unsafe | ||
9 | |||
10 | -- | The foundation datatype for your application. This can be a good place to | ||
11 | -- keep settings and values requiring initialization before your application | ||
12 | -- starts running, such as database connections. Every handler will have | ||
13 | -- access to the data present here. | ||
14 | data App = App | ||
15 | { appSettings :: AppSettings | ||
16 | , appStatic :: EmbeddedStatic | ||
17 | , appConnPool :: ConnectionPool -- ^ Database connection pool. | ||
18 | , appHttpManager :: Manager | ||
19 | , appLogger :: Logger | ||
20 | } | ||
21 | |||
22 | data MenuItem = MenuItem | ||
23 | { menuItemLabel :: Text | ||
24 | , menuItemRoute :: Route App | ||
25 | } | ||
26 | |||
27 | -- This is where we define all of the routes in our application. For a full | ||
28 | -- explanation of the syntax, please see: | ||
29 | -- http://www.yesodweb.com/book/routing-and-handlers | ||
30 | -- | ||
31 | -- Note that this is really half the story; in Application.hs, mkYesodDispatch | ||
32 | -- generates the rest of the code. Please see the following documentation | ||
33 | -- for an explanation for this split: | ||
34 | -- http://www.yesodweb.com/book/scaffolding-and-the-site-template#scaffolding-and-the-site-template_foundation_and_application_modules | ||
35 | -- | ||
36 | -- This function also generates the following type synonyms: | ||
37 | -- type Handler = HandlerT App IO | ||
38 | -- type Widget = WidgetT App IO () | ||
39 | mkYesodData "App" $(parseRoutesFile "config/routes") | ||
40 | |||
41 | -- | A convenient synonym for creating forms. | ||
42 | type Form x = Html -> MForm (HandlerT App IO) (FormResult x, Widget) | ||
43 | |||
44 | -- Please see the documentation for the Yesod typeclass. There are a number | ||
45 | -- of settings which can be configured by overriding methods here. | ||
46 | instance Yesod App where | ||
47 | -- Controls the base of generated URLs. For more information on modifying, | ||
48 | -- see: https://github.com/yesodweb/yesod/wiki/Overriding-approot | ||
49 | approot = ApprootRequest $ \app req -> | ||
50 | case appRoot $ appSettings app of | ||
51 | Nothing -> getApprootText guessApproot app req | ||
52 | Just root -> root | ||
53 | |||
54 | -- Store session data on the client in encrypted cookies, | ||
55 | -- default session idle timeout is 120 minutes | ||
56 | makeSessionBackend _ = Just <$> defaultClientSessionBackend | ||
57 | 120 -- timeout in minutes | ||
58 | "client_session_key.aes" | ||
59 | |||
60 | -- Yesod Middleware allows you to run code before and after each handler function. | ||
61 | |||
62 | -- The defaultYesodMiddleware adds the response header "Vary: Accept, Accept-Language" and performs authorization checks. | ||
63 | -- Some users may also want to add the defaultCsrfMiddleware, which: | ||
64 | -- a) Sets a cookie with a CSRF token in it. | ||
65 | -- b) Validates that incoming write requests include that token in either a header or POST parameter. | ||
66 | -- To add it, chain it together with the defaultMiddleware: yesodMiddleware = defaultYesodMiddleware . defaultCsrfMiddleware | ||
67 | -- For details, see the CSRF documentation in the Yesod.Core.Handler module of the yesod-core package. | ||
68 | yesodMiddleware = defaultYesodMiddleware | ||
69 | |||
70 | defaultLayout widget = do | ||
71 | msgs <- getMessages | ||
72 | |||
73 | mCurrentRoute <- getCurrentRoute | ||
74 | |||
75 | -- Define the menu items of the header. | ||
76 | let menuItems = | ||
77 | [ MenuItem "Inventory" InventoryListingR | ||
78 | ] | ||
79 | |||
80 | -- We break up the default layout into two components: | ||
81 | -- default-layout is the contents of the body tag, and | ||
82 | -- default-layout-wrapper is the entire page. Since the final | ||
83 | -- value passed to hamletToRepHtml cannot be a widget, this allows | ||
84 | -- you to use normal widget features in default-layout. | ||
85 | |||
86 | pc <- widgetToPageContent $ do | ||
87 | addScript $ StaticR jquery_js | ||
88 | addScript $ StaticR webshim_polyfiller_js | ||
89 | $(widgetFile "default-layout") | ||
90 | withUrlRenderer $(hamletFile "templates/default-layout-wrapper.hamlet") | ||
91 | |||
92 | |||
93 | -- This function creates static content files in the static folder | ||
94 | -- and names them based on a hash of their content. This allows | ||
95 | -- expiration dates to be set far in the future without worry of | ||
96 | -- users receiving stale content. | ||
97 | addStaticContent = embedStaticContent appStatic StaticR Right | ||
98 | |||
99 | -- What messages should be logged. The following includes all messages when | ||
100 | -- in development, and warnings and errors in production. | ||
101 | shouldLog app _source level = | ||
102 | appShouldLogAll (appSettings app) | ||
103 | || level == LevelWarn | ||
104 | || level == LevelError | ||
105 | |||
106 | makeLogger = return . appLogger | ||
107 | |||
108 | -- Provide proper Bootstrap styling for default displays, like | ||
109 | -- error pages | ||
110 | defaultMessageWidget title body = $(widgetFile "default-message-widget") | ||
111 | |||
112 | -- How to run database actions. | ||
113 | instance YesodPersist App where | ||
114 | type YesodPersistBackend App = SqlBackend | ||
115 | runDB action = do | ||
116 | master <- getYesod | ||
117 | runSqlPool action $ appConnPool master | ||
118 | instance YesodPersistRunner App where | ||
119 | getDBRunner = defaultGetDBRunner appConnPool | ||
120 | |||
121 | -- This instance is required to use forms. You can modify renderMessage to | ||
122 | -- achieve customized and internationalized form validation messages. | ||
123 | instance RenderMessage App FormMessage where | ||
124 | renderMessage _ _ = defaultFormMessage | ||
125 | |||
126 | -- Useful when writing code that is re-usable outside of the Handler context. | ||
127 | -- An example is background jobs that send email. | ||
128 | -- This can also be useful for writing code that works across multiple Yesod applications. | ||
129 | instance HasHttpManager App where | ||
130 | getHttpManager = appHttpManager | ||
131 | |||
132 | unsafeHandler :: App -> Handler a -> IO a | ||
133 | unsafeHandler = Unsafe.fakeHandlerGetLogger appLogger | ||
134 | |||
135 | -- Note: Some functionality previously present in the scaffolding has been | ||
136 | -- moved to documentation in the Wiki. Following are some hopefully helpful | ||
137 | -- links: | ||
138 | -- | ||
139 | -- https://github.com/yesodweb/yesod/wiki/Sending-email | ||
140 | -- https://github.com/yesodweb/yesod/wiki/Serve-static-files-from-a-separate-domain | ||
141 | -- https://github.com/yesodweb/yesod/wiki/i18n-messages-in-the-scaffolding | ||
diff --git a/Handler/Common.hs b/Handler/Common.hs new file mode 100644 index 0000000..38fb1ce --- /dev/null +++ b/Handler/Common.hs | |||
@@ -0,0 +1,78 @@ | |||
1 | {-# LANGUAGE ApplicativeDo #-} | ||
2 | |||
3 | module Handler.Common | ||
4 | ( inventoryListing | ||
5 | , itemForm | ||
6 | , InventoryState(..) | ||
7 | , FormState(..) | ||
8 | ) where | ||
9 | |||
10 | import Import | ||
11 | |||
12 | import Data.Unique | ||
13 | |||
14 | import qualified Data.Text as Text (pack) | ||
15 | |||
16 | import Control.Lens | ||
17 | |||
18 | import Handler.Common.Types | ||
19 | |||
20 | dayFormat :: Day -> String | ||
21 | dayFormat = formatTime defaultTimeLocale "%e. %b %y" | ||
22 | |||
23 | itemForm :: Maybe Item -- ^ Update existing item or insert new? | ||
24 | -> Html -> MForm Handler (FormResult Item, Widget) | ||
25 | itemForm proto identView = do | ||
26 | today <- utctDay <$> liftIO getCurrentTime | ||
27 | |||
28 | (kindRes, kindView) <- mreq textField "" $ itemKind <$> proto | ||
29 | (boughtRes, boughtWidget) <- dayForm (maybe (Just $ Just today) Just $ fmap itemBought proto) "Unknown" | ||
30 | (expiresRes, expiresWidget) <- dayForm (fmap itemExpires proto) "Never" | ||
31 | (openedRes, openedWidget) <- dayForm (fmap itemOpened proto) "Never" | ||
32 | |||
33 | let itemRes = do | ||
34 | itemKind <- kindRes | ||
35 | itemBought <- boughtRes | ||
36 | itemExpires <- expiresRes | ||
37 | itemOpened <- openedRes | ||
38 | return Item{ itemNormKind = normalizeKind itemKind, ..} | ||
39 | |||
40 | return . (itemRes, ) $ | ||
41 | [whamlet| | ||
42 | $newline never | ||
43 | #{identView} | ||
44 | <div .td>^{fvInput kindView} | ||
45 | <div .td>^{boughtWidget} | ||
46 | <div .td>^{expiresWidget} | ||
47 | <div .td>^{openedWidget} | ||
48 | |] | ||
49 | where | ||
50 | dayForm :: Maybe (Maybe Day) -> String -> MForm Handler (FormResult (Maybe Day), Widget) | ||
51 | dayForm proto label = do | ||
52 | today <- utctDay <$> liftIO getCurrentTime | ||
53 | |||
54 | checkboxId <- ("check" <>) . show . hashUnique <$> liftIO newUnique | ||
55 | |||
56 | (fmap (fromMaybe False) -> isNothingRes, isNothingView) <- | ||
57 | mopt checkBoxField ("" { fsId = Just $ Text.pack checkboxId }) . Just . Just . fromMaybe True $ fmap isNothing proto | ||
58 | (dayRes, dayView) <- | ||
59 | mreq dayField "" . Just . fromMaybe today $ join proto | ||
60 | |||
61 | let res = (bool Just (const Nothing) <$> isNothingRes) <*> dayRes | ||
62 | return . (res, ) $ do | ||
63 | [whamlet| | ||
64 | $newline never | ||
65 | <div .table> | ||
66 | <div .tr> | ||
67 | <label for=#{checkboxId} .checkbox .td> | ||
68 | ^{fvInput isNothingView} | ||
69 | <span> | ||
70 | #{label} | ||
71 | <div .tr> | ||
72 | <div .td .dayInput>^{fvInput dayView} | ||
73 | |] | ||
74 | |||
75 | inventoryListing :: InventoryState -> Widget | ||
76 | inventoryListing InventoryState{..} = do | ||
77 | setTitle "Bar Inventory" | ||
78 | $(widgetFile "inventoryListing") | ||
diff --git a/Handler/Common/Types.hs b/Handler/Common/Types.hs new file mode 100644 index 0000000..ca7cb8d --- /dev/null +++ b/Handler/Common/Types.hs | |||
@@ -0,0 +1,23 @@ | |||
1 | {-# LANGUAGE FunctionalDependencies #-} | ||
2 | |||
3 | module Handler.Common.Types where | ||
4 | |||
5 | import Import | ||
6 | |||
7 | import Control.Lens | ||
8 | |||
9 | data InventoryState = InventoryState | ||
10 | { stock :: [Entity Item] | ||
11 | , formState :: Maybe FormState | ||
12 | } | ||
13 | |||
14 | data FormState = InsertForm | ||
15 | { fsInsertForm :: Widget | ||
16 | , fsInsertEncoding :: Enctype | ||
17 | } | ||
18 | | UpdateForm | ||
19 | { fsUpdateItem :: ItemId | ||
20 | , fsUpdateForm :: Widget | ||
21 | , fsUpdateEncoding :: Enctype | ||
22 | } | ||
23 | makeLensesWith abbreviatedFields ''FormState | ||
diff --git a/Handler/DeleteItem.hs b/Handler/DeleteItem.hs new file mode 100644 index 0000000..ee6d9d3 --- /dev/null +++ b/Handler/DeleteItem.hs | |||
@@ -0,0 +1,10 @@ | |||
1 | module Handler.DeleteItem where | ||
2 | |||
3 | import Import | ||
4 | |||
5 | postDeleteItemR :: ItemId -> Handler TypedContent | ||
6 | postDeleteItemR itemId = do | ||
7 | runDB $ delete itemId | ||
8 | selectRep $ do | ||
9 | provideJson () | ||
10 | provideRep (redirect $ InventoryListingR :: Handler Html) | ||
diff --git a/Handler/InventoryListing.hs b/Handler/InventoryListing.hs new file mode 100644 index 0000000..e3c062e --- /dev/null +++ b/Handler/InventoryListing.hs | |||
@@ -0,0 +1,26 @@ | |||
1 | module Handler.InventoryListing where | ||
2 | |||
3 | import Import | ||
4 | import Handler.Common | ||
5 | |||
6 | getInventoryListingR, postInventoryListingR :: Handler TypedContent | ||
7 | getInventoryListingR = postInventoryListingR | ||
8 | postInventoryListingR = do | ||
9 | ((insertResult, fsInsertForm), fsInsertEncoding) <- runFormPost $ itemForm Nothing | ||
10 | |||
11 | mapM_ (addMessage "formError" . toHtml) =<< case insertResult of | ||
12 | FormSuccess newItem -> [] <$ runDB (insert newItem) | ||
13 | FormFailure errors -> return errors | ||
14 | _ -> return [] | ||
15 | |||
16 | (sortOn entityVal -> stock) <- runDB $ selectList [] [] | ||
17 | |||
18 | selectRep $ do | ||
19 | provideJson (stock :: [Entity Item]) | ||
20 | provideRep . defaultLayout $ inventoryListing InventoryState | ||
21 | { formState = Just InsertForm{..} | ||
22 | , .. | ||
23 | } | ||
24 | |||
25 | putInventoryListingR :: Handler Value | ||
26 | putInventoryListingR = returnJson =<< runDB . insertEntity =<< (requireCheckJsonBody :: Handler Item) | ||
diff --git a/Handler/Item.hs b/Handler/Item.hs new file mode 100644 index 0000000..87030bb --- /dev/null +++ b/Handler/Item.hs | |||
@@ -0,0 +1,31 @@ | |||
1 | module Handler.Item where | ||
2 | |||
3 | import Import | ||
4 | |||
5 | getItemR :: ItemId -> Handler TypedContent | ||
6 | getItemR itemId = do | ||
7 | eLookup <- runDB $ fmap (Entity itemId) <$> get itemId | ||
8 | case eLookup of | ||
9 | Nothing -> notFound | ||
10 | Just entity -> selectRep $ do | ||
11 | provideJson entity | ||
12 | provideRep (redirect $ InventoryListingR :#: itemId :: Handler Html) | ||
13 | |||
14 | putItemR :: ItemId -> Handler Value | ||
15 | putItemR itemId = do | ||
16 | Item{..} <- requireCheckJsonBody | ||
17 | returnJson . Entity itemId =<< runDB | ||
18 | (updateGet itemId [ ItemKind =. itemKind | ||
19 | , ItemNormKind =. itemNormKind | ||
20 | , ItemBought =. itemBought | ||
21 | , ItemExpires =. itemExpires | ||
22 | , ItemOpened =. itemOpened | ||
23 | ]) | ||
24 | |||
25 | patchItemR :: ItemId -> Handler Value | ||
26 | patchItemR itemId = do | ||
27 | diffs <- (requireCheckJsonBody :: Handler ItemDiffs) | ||
28 | returnJson . Entity itemId =<< runDB (updateGet itemId $ toUpdate diffs) | ||
29 | |||
30 | deleteItemR :: ItemId -> Handler () | ||
31 | deleteItemR = runDB . delete | ||
diff --git a/Handler/OpenItem.hs b/Handler/OpenItem.hs new file mode 100644 index 0000000..468c6ec --- /dev/null +++ b/Handler/OpenItem.hs | |||
@@ -0,0 +1,12 @@ | |||
1 | module Handler.OpenItem where | ||
2 | |||
3 | import Import | ||
4 | |||
5 | postOpenItemR :: ItemId -> Handler TypedContent | ||
6 | postOpenItemR itemId = do | ||
7 | today <- utctDay <$> liftIO getCurrentTime | ||
8 | result <- fmap (Entity itemId) . runDB $ updateGet itemId [ ItemOpened =. Just today | ||
9 | ] | ||
10 | selectRep $ do | ||
11 | provideJson result | ||
12 | provideRep (redirect $ InventoryListingR :#: itemId :: Handler Html) | ||
diff --git a/Handler/UpdateItem.hs b/Handler/UpdateItem.hs new file mode 100644 index 0000000..353572b --- /dev/null +++ b/Handler/UpdateItem.hs | |||
@@ -0,0 +1,33 @@ | |||
1 | module Handler.UpdateItem where | ||
2 | |||
3 | import Import | ||
4 | |||
5 | import Handler.Common | ||
6 | |||
7 | getUpdateItemR, postUpdateItemR :: ItemId -> Handler TypedContent | ||
8 | getUpdateItemR = postUpdateItemR | ||
9 | postUpdateItemR fsUpdateItem = do | ||
10 | Just entity <- fmap (Entity fsUpdateItem) <$> runDB (get fsUpdateItem) | ||
11 | |||
12 | ((updateResult, fsUpdateForm), fsUpdateEncoding) <- runFormPost . itemForm . Just $ entityVal entity | ||
13 | |||
14 | mapM_ (addMessage "formError" . toHtml) =<< case updateResult of | ||
15 | FormSuccess Item{..} -> [] <$ runDB (update fsUpdateItem [ ItemKind =. itemKind | ||
16 | , ItemNormKind =. normalizeKind itemKind | ||
17 | , ItemBought =. itemBought | ||
18 | , ItemExpires =. itemExpires | ||
19 | , ItemOpened =. itemOpened | ||
20 | ]) | ||
21 | FormFailure errors -> return errors | ||
22 | _ -> return [] | ||
23 | |||
24 | selectRep $ do | ||
25 | provideRep $ case updateResult of | ||
26 | FormSuccess _ -> redirect $ InventoryListingR :#: fsUpdateItem :: Handler Html | ||
27 | _ -> do | ||
28 | (sortOn entityVal -> stock) <- runDB $ selectList [] [] | ||
29 | defaultLayout $ inventoryListing InventoryState | ||
30 | { formState = Just UpdateForm{..} | ||
31 | , .. | ||
32 | } | ||
33 | provideJson () | ||
diff --git a/Import.hs b/Import.hs new file mode 100644 index 0000000..a102001 --- /dev/null +++ b/Import.hs | |||
@@ -0,0 +1,6 @@ | |||
1 | module Import | ||
2 | ( module Import | ||
3 | ) where | ||
4 | |||
5 | import Foundation as Import | ||
6 | import Import.NoFoundation as Import | ||
diff --git a/Import/NoFoundation.hs b/Import/NoFoundation.hs new file mode 100644 index 0000000..6872d0a --- /dev/null +++ b/Import/NoFoundation.hs | |||
@@ -0,0 +1,13 @@ | |||
1 | {-# LANGUAGE CPP #-} | ||
2 | module Import.NoFoundation | ||
3 | ( module Import | ||
4 | ) where | ||
5 | |||
6 | import ClassyPrelude.Yesod as Import | ||
7 | import Model as Import | ||
8 | import Settings as Import | ||
9 | import Settings.StaticFiles as Import | ||
10 | import Yesod.Auth as Import | ||
11 | import Yesod.Core.Types as Import (loggerSet) | ||
12 | import Yesod.Default.Config2 as Import | ||
13 | import Yesod.EmbeddedStatic as Import | ||
diff --git a/Model.hs b/Model.hs new file mode 100644 index 0000000..7b33f6e --- /dev/null +++ b/Model.hs | |||
@@ -0,0 +1,96 @@ | |||
1 | {-# LANGUAGE FlexibleInstances #-} | ||
2 | |||
3 | module Model where | ||
4 | |||
5 | import ClassyPrelude.Yesod | ||
6 | import Database.Persist.Quasi | ||
7 | |||
8 | import Control.Monad.Writer | ||
9 | |||
10 | import Data.Text (Text) | ||
11 | import qualified Data.Text as Text | ||
12 | |||
13 | import Data.Aeson | ||
14 | |||
15 | -- You can define all of your database entities in the entities file. | ||
16 | -- You can find more information on persistent and how to declare entities | ||
17 | -- at: | ||
18 | -- http://www.yesodweb.com/book/persistent/ | ||
19 | share [mkPersist sqlSettings, mkMigrate "migrateAll"] | ||
20 | $(persistFileWith lowerCaseSettings "config/models") | ||
21 | |||
22 | instance Ord Item where | ||
23 | x `compare` y = mconcat | ||
24 | [ (isNothing $ itemOpened x) `compare` (isNothing $ itemOpened y) | ||
25 | , itemOpened x `compare` itemOpened y | ||
26 | , (isNothing $ itemExpires x) `compare` (isNothing $ itemExpires y) | ||
27 | , itemExpires x `compare` itemExpires x | ||
28 | , itemKind x `compare` itemKind x | ||
29 | , itemBought x `compare` itemBought x | ||
30 | ] | ||
31 | |||
32 | instance ToJSON Item where | ||
33 | toJSON Item{..} = object $ | ||
34 | [ "kind" .= itemKind | ||
35 | ] ++ maybe [] (\x -> ["bought" .= x]) itemBought | ||
36 | ++ maybe [] (\x -> ["expires" .= x]) itemExpires | ||
37 | ++ maybe [] (\x -> ["opened" .= x]) itemOpened | ||
38 | |||
39 | instance FromJSON Item where | ||
40 | parseJSON = withObject "Item" $ \obj -> do | ||
41 | itemKind <- obj .: "kind" | ||
42 | let | ||
43 | itemNormKind = normalizeKind itemKind | ||
44 | itemBought <- obj .:? "bought" | ||
45 | itemExpires <- obj .:? "expires" | ||
46 | itemOpened <- obj .:? "opened" | ||
47 | return Item{..} | ||
48 | |||
49 | instance ToJSON (Entity Item) where | ||
50 | toJSON = entityIdToJSON | ||
51 | |||
52 | instance FromJSON (Entity Item) where | ||
53 | parseJSON = entityIdFromJSON | ||
54 | |||
55 | instance ToJSON Reference where | ||
56 | toJSON Reference{..} = String referenceKind | ||
57 | |||
58 | instance FromJSON Reference where | ||
59 | parseJSON = withText "Reference" $ \referenceKind -> do | ||
60 | let | ||
61 | referenceNormKind = normalizeKind referenceKind | ||
62 | return Reference{..} | ||
63 | |||
64 | instance ToJSON (Entity Reference) where | ||
65 | toJSON = keyValueEntityToJSON | ||
66 | |||
67 | instance FromJSON (Entity Reference) where | ||
68 | parseJSON = keyValueEntityFromJSON | ||
69 | |||
70 | normalizeKind :: Text -> Text | ||
71 | normalizeKind = Text.strip . Text.toCaseFold | ||
72 | |||
73 | data ItemDiff = DiffKind Text | ||
74 | | DiffBought (Maybe Day) | ||
75 | | DiffExpires (Maybe Day) | ||
76 | | DiffOpened (Maybe Day) | ||
77 | |||
78 | newtype ItemDiffs = ItemDiffs [ItemDiff] | ||
79 | |||
80 | instance FromJSON ItemDiffs where | ||
81 | parseJSON = withObject "ItemDiff" $ \obj -> fmap ItemDiffs . execWriterT $ do | ||
82 | tell =<< maybe [] (pure . DiffKind) <$> lift (obj .:? "kind") | ||
83 | tell =<< maybe [] (pure . DiffBought) <$> lift (obj .:! "bought") | ||
84 | tell =<< maybe [] (pure . DiffExpires) <$> lift (obj .:! "expires") | ||
85 | tell =<< maybe [] (pure . DiffOpened) <$> lift (obj .:! "opened") | ||
86 | |||
87 | toUpdate :: ItemDiffs -> [Update Item] | ||
88 | toUpdate (ItemDiffs ds) = do | ||
89 | x <- ds | ||
90 | case x of | ||
91 | DiffKind t -> [ ItemKind =. t | ||
92 | , ItemNormKind =. normalizeKind t | ||
93 | ] | ||
94 | DiffBought d -> [ ItemBought =. d ] | ||
95 | DiffExpires d -> [ ItemExpires =. d ] | ||
96 | DiffOpened d -> [ ItemOpened =. d ] | ||
diff --git a/Settings.hs b/Settings.hs new file mode 100644 index 0000000..76aa2f3 --- /dev/null +++ b/Settings.hs | |||
@@ -0,0 +1,107 @@ | |||
1 | {-# Language CPP #-} | ||
2 | -- | Settings are centralized, as much as possible, into this file. This | ||
3 | -- includes database connection settings, static file locations, etc. | ||
4 | -- In addition, you can configure a number of different aspects of Yesod | ||
5 | -- by overriding methods in the Yesod typeclass. That instance is | ||
6 | -- declared in the Foundation.hs file. | ||
7 | module Settings where | ||
8 | |||
9 | import ClassyPrelude.Yesod | ||
10 | import qualified Control.Exception as Exception | ||
11 | import Data.Aeson (Result (..), fromJSON, withObject, (.!=), | ||
12 | (.:?)) | ||
13 | import Data.FileEmbed (embedFile) | ||
14 | import Data.Yaml (decodeEither') | ||
15 | import Database.Persist.Postgresql (PostgresConf) | ||
16 | import Language.Haskell.TH.Syntax (Exp, Q) | ||
17 | import Network.Wai.Handler.Warp (HostPreference) | ||
18 | import Yesod.Default.Config2 (applyEnvValue, configSettingsYml) | ||
19 | import Yesod.Default.Util (WidgetFileSettings, widgetFileNoReload, | ||
20 | widgetFileReload) | ||
21 | |||
22 | #ifdef DEVELOPMENT | ||
23 | #define DEV_BOOL True | ||
24 | #else | ||
25 | #define DEV_BOOL False | ||
26 | #endif | ||
27 | |||
28 | -- | Runtime settings to configure this application. These settings can be | ||
29 | -- loaded from various sources: defaults, environment variables, config files, | ||
30 | -- theoretically even a database. | ||
31 | data AppSettings = AppSettings | ||
32 | { appStaticDir :: String | ||
33 | -- ^ Directory from which to serve static files. | ||
34 | , appDatabaseConf :: PostgresConf | ||
35 | -- ^ Configuration settings for accessing the database. | ||
36 | , appRoot :: Maybe Text | ||
37 | -- ^ Base for all generated URLs. If @Nothing@, determined | ||
38 | -- from the request headers. | ||
39 | , appHost :: HostPreference | ||
40 | -- ^ Host/interface the server should bind to. | ||
41 | , appPort :: Int | ||
42 | -- ^ Port to listen on | ||
43 | , appIpFromHeader :: Bool | ||
44 | -- ^ Get the IP address from the header when logging. Useful when sitting | ||
45 | -- behind a reverse proxy. | ||
46 | |||
47 | , appDetailedRequestLogging :: Bool | ||
48 | -- ^ Use detailed request logging system | ||
49 | , appShouldLogAll :: Bool | ||
50 | -- ^ Should all log messages be displayed? | ||
51 | , appReloadTemplates :: Bool | ||
52 | -- ^ Use the reload version of templates | ||
53 | } | ||
54 | |||
55 | instance FromJSON AppSettings where | ||
56 | parseJSON = withObject "AppSettings" $ \o -> do | ||
57 | let defaultDev = DEV_BOOL | ||
58 | appStaticDir <- o .: "static-dir" | ||
59 | appDatabaseConf <- o .: "database" | ||
60 | appRoot <- o .:? "approot" | ||
61 | appHost <- fromString <$> o .: "host" | ||
62 | appPort <- o .: "port" | ||
63 | appIpFromHeader <- o .: "ip-from-header" | ||
64 | |||
65 | appDetailedRequestLogging <- o .:? "detailed-logging" .!= defaultDev | ||
66 | appShouldLogAll <- o .:? "should-log-all" .!= defaultDev | ||
67 | appReloadTemplates <- o .:? "reload-templates" .!= defaultDev | ||
68 | |||
69 | return AppSettings {..} | ||
70 | |||
71 | -- | Settings for 'widgetFile', such as which template languages to support and | ||
72 | -- default Hamlet settings. | ||
73 | -- | ||
74 | -- For more information on modifying behavior, see: | ||
75 | -- | ||
76 | -- https://github.com/yesodweb/yesod/wiki/Overriding-widgetFile | ||
77 | widgetFileSettings :: WidgetFileSettings | ||
78 | widgetFileSettings = def | ||
79 | |||
80 | -- | How static files should be combined. | ||
81 | combineSettings :: CombineSettings | ||
82 | combineSettings = def | ||
83 | |||
84 | -- The rest of this file contains settings which rarely need changing by a | ||
85 | -- user. | ||
86 | |||
87 | widgetFile :: String -> Q Exp | ||
88 | widgetFile = (if appReloadTemplates compileTimeAppSettings | ||
89 | then widgetFileReload | ||
90 | else widgetFileNoReload) | ||
91 | widgetFileSettings | ||
92 | |||
93 | -- | Raw bytes at compile time of @config/settings.yml@ | ||
94 | configSettingsYmlBS :: ByteString | ||
95 | configSettingsYmlBS = $(embedFile configSettingsYml) | ||
96 | |||
97 | -- | @config/settings.yml@, parsed to a @Value@. | ||
98 | configSettingsYmlValue :: Value | ||
99 | configSettingsYmlValue = either Exception.throw id | ||
100 | $ decodeEither' configSettingsYmlBS | ||
101 | |||
102 | -- | A version of @AppSettings@ parsed at compile time from @config/settings.yml@. | ||
103 | compileTimeAppSettings :: AppSettings | ||
104 | compileTimeAppSettings = | ||
105 | case fromJSON $ applyEnvValue False mempty configSettingsYmlValue of | ||
106 | Error e -> error e | ||
107 | Success settings -> settings | ||
diff --git a/Settings/StaticFiles.hs b/Settings/StaticFiles.hs new file mode 100644 index 0000000..bd29ca3 --- /dev/null +++ b/Settings/StaticFiles.hs | |||
@@ -0,0 +1,13 @@ | |||
1 | module Settings.StaticFiles where | ||
2 | |||
3 | import Settings (appStaticDir, compileTimeAppSettings) | ||
4 | import Yesod.EmbeddedStatic (mkEmbeddedStatic, embedDir) | ||
5 | import Prelude (Bool(..), ($)) | ||
6 | |||
7 | #ifdef DEVELOPMENT | ||
8 | #define DEV_BOOL True | ||
9 | #else | ||
10 | #define DEV_BOOL False | ||
11 | #endif | ||
12 | |||
13 | mkEmbeddedStatic DEV_BOOL "eStatic" [embedDir $ appStaticDir compileTimeAppSettings] | ||
@@ -0,0 +1,3 @@ | |||
1 | #!/usr/bin/env zsh | ||
2 | |||
3 | gup -u bar.nix static/jquery.js static/webshim | ||
diff --git a/app/DevelMain.hs b/app/DevelMain.hs new file mode 100644 index 0000000..b327943 --- /dev/null +++ b/app/DevelMain.hs | |||
@@ -0,0 +1,99 @@ | |||
1 | -- | Running your app inside GHCi. | ||
2 | -- | ||
3 | -- To start up GHCi for usage with Yesod, first make sure you are in dev mode: | ||
4 | -- | ||
5 | -- > cabal configure -fdev | ||
6 | -- | ||
7 | -- Note that @yesod devel@ automatically sets the dev flag. | ||
8 | -- Now launch the repl: | ||
9 | -- | ||
10 | -- > cabal repl --ghc-options="-O0 -fobject-code" | ||
11 | -- | ||
12 | -- To start your app, run: | ||
13 | -- | ||
14 | -- > :l DevelMain | ||
15 | -- > DevelMain.update | ||
16 | -- | ||
17 | -- You can also call @DevelMain.shutdown@ to stop the app | ||
18 | -- | ||
19 | -- You will need to add the foreign-store package to your .cabal file. | ||
20 | -- It is very light-weight. | ||
21 | -- | ||
22 | -- If you don't use cabal repl, you will need | ||
23 | -- to run the following in GHCi or to add it to | ||
24 | -- your .ghci file. | ||
25 | -- | ||
26 | -- :set -DDEVELOPMENT | ||
27 | -- | ||
28 | -- There is more information about this approach, | ||
29 | -- on the wiki: https://github.com/yesodweb/yesod/wiki/ghci | ||
30 | |||
31 | module DevelMain where | ||
32 | |||
33 | import Prelude | ||
34 | import Application (getApplicationRepl, shutdownApp) | ||
35 | |||
36 | import Control.Exception (finally) | ||
37 | import Control.Monad ((>=>)) | ||
38 | import Control.Concurrent | ||
39 | import Data.IORef | ||
40 | import Foreign.Store | ||
41 | import Network.Wai.Handler.Warp | ||
42 | import GHC.Word | ||
43 | |||
44 | -- | Start or restart the server. | ||
45 | -- newStore is from foreign-store. | ||
46 | -- A Store holds onto some data across ghci reloads | ||
47 | update :: IO () | ||
48 | update = do | ||
49 | mtidStore <- lookupStore tidStoreNum | ||
50 | case mtidStore of | ||
51 | -- no server running | ||
52 | Nothing -> do | ||
53 | done <- storeAction doneStore newEmptyMVar | ||
54 | tid <- start done | ||
55 | _ <- storeAction (Store tidStoreNum) (newIORef tid) | ||
56 | return () | ||
57 | -- server is already running | ||
58 | Just tidStore -> restartAppInNewThread tidStore | ||
59 | where | ||
60 | doneStore :: Store (MVar ()) | ||
61 | doneStore = Store 0 | ||
62 | |||
63 | -- shut the server down with killThread and wait for the done signal | ||
64 | restartAppInNewThread :: Store (IORef ThreadId) -> IO () | ||
65 | restartAppInNewThread tidStore = modifyStoredIORef tidStore $ \tid -> do | ||
66 | killThread tid | ||
67 | withStore doneStore takeMVar | ||
68 | readStore doneStore >>= start | ||
69 | |||
70 | |||
71 | -- | Start the server in a separate thread. | ||
72 | start :: MVar () -- ^ Written to when the thread is killed. | ||
73 | -> IO ThreadId | ||
74 | start done = do | ||
75 | (port, site, app) <- getApplicationRepl | ||
76 | forkIO (finally (runSettings (setPort port defaultSettings) app) | ||
77 | -- Note that this implies concurrency | ||
78 | -- between shutdownApp and the next app that is starting. | ||
79 | -- Normally this should be fine | ||
80 | (putMVar done () >> shutdownApp site)) | ||
81 | |||
82 | -- | kill the server | ||
83 | shutdown :: IO () | ||
84 | shutdown = do | ||
85 | mtidStore <- lookupStore tidStoreNum | ||
86 | case mtidStore of | ||
87 | -- no server running | ||
88 | Nothing -> putStrLn "no Yesod app running" | ||
89 | Just tidStore -> do | ||
90 | withStore tidStore $ readIORef >=> killThread | ||
91 | putStrLn "Yesod app is shutdown" | ||
92 | |||
93 | tidStoreNum :: Word32 | ||
94 | tidStoreNum = 1 | ||
95 | |||
96 | modifyStoredIORef :: Store (IORef a) -> (a -> IO a) -> IO () | ||
97 | modifyStoredIORef store f = withStore store $ \ref -> do | ||
98 | v <- readIORef ref | ||
99 | f v >>= writeIORef ref | ||
diff --git a/app/devel.hs b/app/devel.hs new file mode 100644 index 0000000..979103f --- /dev/null +++ b/app/devel.hs | |||
@@ -0,0 +1,6 @@ | |||
1 | {-# LANGUAGE PackageImports #-} | ||
2 | import "bar" Application (develMain) | ||
3 | import Prelude (IO) | ||
4 | |||
5 | main :: IO () | ||
6 | main = develMain | ||
diff --git a/app/main.hs b/app/main.hs new file mode 100644 index 0000000..4ffa93d --- /dev/null +++ b/app/main.hs | |||
@@ -0,0 +1,5 @@ | |||
1 | import Prelude (IO) | ||
2 | import Application (appMain) | ||
3 | |||
4 | main :: IO () | ||
5 | main = appMain | ||
diff --git a/bar.cabal b/bar.cabal new file mode 100644 index 0000000..264339d --- /dev/null +++ b/bar.cabal | |||
@@ -0,0 +1,128 @@ | |||
1 | name: bar | ||
2 | version: 0.0.0 | ||
3 | cabal-version: >= 1.8 | ||
4 | build-type: Simple | ||
5 | |||
6 | Flag dev | ||
7 | Description: Turn on development settings, like auto-reload templates. | ||
8 | Default: False | ||
9 | |||
10 | Flag library-only | ||
11 | Description: Build for use with "yesod devel" | ||
12 | Default: False | ||
13 | |||
14 | library | ||
15 | hs-source-dirs: ., app | ||
16 | exposed-modules: Application | ||
17 | Foundation | ||
18 | Import | ||
19 | Import.NoFoundation | ||
20 | Model | ||
21 | Settings | ||
22 | Settings.StaticFiles | ||
23 | Handler.Common | ||
24 | Handler.Common.Types | ||
25 | Handler.InventoryListing | ||
26 | Handler.UpdateItem | ||
27 | Handler.OpenItem | ||
28 | Handler.DeleteItem | ||
29 | Handler.Item | ||
30 | |||
31 | if flag(dev) || flag(library-only) | ||
32 | cpp-options: -DDEVELOPMENT | ||
33 | ghc-options: -Wall -fwarn-tabs -O0 | ||
34 | else | ||
35 | ghc-options: -Wall -fwarn-tabs -O2 | ||
36 | |||
37 | extensions: TemplateHaskell | ||
38 | QuasiQuotes | ||
39 | OverloadedStrings | ||
40 | NoImplicitPrelude | ||
41 | MultiParamTypeClasses | ||
42 | TypeFamilies | ||
43 | GADTs | ||
44 | GeneralizedNewtypeDeriving | ||
45 | FlexibleContexts | ||
46 | FlexibleInstances | ||
47 | EmptyDataDecls | ||
48 | NoMonomorphismRestriction | ||
49 | DeriveDataTypeable | ||
50 | ViewPatterns | ||
51 | TupleSections | ||
52 | RecordWildCards | ||
53 | CPP | ||
54 | |||
55 | build-depends: | ||
56 | -- Due to a bug in GHC 8.0.1, we block its usage | ||
57 | -- See: https://ghc.haskell.org/trac/ghc/ticket/12130 | ||
58 | base >= 4.8.2.0 && < 4.9 | ||
59 | || >= 4.9.1.0 && < 5 | ||
60 | |||
61 | , yesod >= 1.4.3 && < 1.5 | ||
62 | , yesod-core >= 1.4.30 && < 1.5 | ||
63 | , yesod-auth >= 1.4.0 && < 1.5 | ||
64 | , yesod-static >= 1.4.0.3 && < 1.6 | ||
65 | , yesod-form >= 1.4.0 && < 1.5 | ||
66 | , classy-prelude >= 0.10.2 | ||
67 | , classy-prelude-conduit >= 0.10.2 | ||
68 | -- version 1.0 had a bug in reexporting Handler, causing trouble | ||
69 | , classy-prelude-yesod >= 0.10.2 && < 1.0 | ||
70 | || >= 1.1 | ||
71 | , bytestring >= 0.9 && < 0.11 | ||
72 | , text >= 0.11 && < 2.0 | ||
73 | , persistent >= 2.0 && < 2.7 | ||
74 | , persistent-postgresql >= 2.1.1 && < 2.7 | ||
75 | , persistent-template >= 2.0 && < 2.7 | ||
76 | , template-haskell | ||
77 | , shakespeare >= 2.0 && < 2.1 | ||
78 | , hjsmin >= 0.1 && < 0.3 | ||
79 | , monad-control >= 0.3 && < 1.1 | ||
80 | , wai-extra >= 3.0 && < 3.1 | ||
81 | , yaml >= 0.8 && < 0.9 | ||
82 | , http-conduit >= 2.1 && < 2.3 | ||
83 | , directory >= 1.1 && < 1.4 | ||
84 | , warp >= 3.0 && < 3.3 | ||
85 | , data-default | ||
86 | , aeson >= 0.6 && < 1.1 | ||
87 | , conduit >= 1.0 && < 2.0 | ||
88 | , monad-logger >= 0.3 && < 0.4 | ||
89 | , fast-logger >= 2.2 && < 2.5 | ||
90 | , wai-logger >= 2.2 && < 2.4 | ||
91 | , file-embed | ||
92 | , safe | ||
93 | , unordered-containers | ||
94 | , containers | ||
95 | , vector | ||
96 | , time | ||
97 | , case-insensitive | ||
98 | , wai | ||
99 | , mtl | ||
100 | , lens | ||
101 | |||
102 | executable bar | ||
103 | if flag(library-only) | ||
104 | Buildable: False | ||
105 | |||
106 | main-is: main.hs | ||
107 | hs-source-dirs: app | ||
108 | build-depends: base, bar | ||
109 | |||
110 | ghc-options: -threaded -rtsopts -with-rtsopts=-N | ||
111 | |||
112 | extensions: TemplateHaskell | ||
113 | QuasiQuotes | ||
114 | OverloadedStrings | ||
115 | NoImplicitPrelude | ||
116 | MultiParamTypeClasses | ||
117 | TypeFamilies | ||
118 | GADTs | ||
119 | GeneralizedNewtypeDeriving | ||
120 | FlexibleContexts | ||
121 | FlexibleInstances | ||
122 | EmptyDataDecls | ||
123 | NoMonomorphismRestriction | ||
124 | DeriveDataTypeable | ||
125 | ViewPatterns | ||
126 | TupleSections | ||
127 | RecordWildCards | ||
128 | CPP | ||
@@ -0,0 +1,30 @@ | |||
1 | { mkDerivation, aeson, base, bytestring, case-insensitive | ||
2 | , classy-prelude, classy-prelude-conduit, classy-prelude-yesod | ||
3 | , conduit, containers, data-default, directory, fast-logger | ||
4 | , file-embed, hjsmin, http-conduit, lens, monad-control | ||
5 | , monad-logger, mtl, persistent, persistent-postgresql | ||
6 | , persistent-template, safe, shakespeare, stdenv, template-haskell | ||
7 | , text, time, unordered-containers, vector, wai, wai-extra | ||
8 | , wai-logger, warp, yaml, yesod, yesod-auth, yesod-core, yesod-form | ||
9 | , yesod-static | ||
10 | }: | ||
11 | mkDerivation { | ||
12 | pname = "bar"; | ||
13 | version = "0.0.0"; | ||
14 | src = ./.; | ||
15 | isLibrary = true; | ||
16 | isExecutable = true; | ||
17 | libraryHaskellDepends = [ | ||
18 | aeson base bytestring case-insensitive classy-prelude | ||
19 | classy-prelude-conduit classy-prelude-yesod conduit containers | ||
20 | data-default directory fast-logger file-embed hjsmin http-conduit | ||
21 | lens monad-control monad-logger mtl persistent | ||
22 | persistent-postgresql persistent-template safe shakespeare | ||
23 | template-haskell text time unordered-containers vector wai | ||
24 | wai-extra wai-logger warp yaml yesod yesod-auth yesod-core | ||
25 | yesod-form yesod-static | ||
26 | ]; | ||
27 | executableHaskellDepends = [ base ]; | ||
28 | doHaddock = false; | ||
29 | license = stdenv.lib.licenses.unfree; | ||
30 | } | ||
diff --git a/bar.nix.gup b/bar.nix.gup new file mode 100644 index 0000000..f0f9c74 --- /dev/null +++ b/bar.nix.gup | |||
@@ -0,0 +1,5 @@ | |||
1 | #!/usr/bin/env zsh | ||
2 | |||
3 | gup -u ${2:r}.cabal | ||
4 | cd ${2:h} | ||
5 | cabal2nix --no-haddock ./. >! ${1} | ||
diff --git a/config/models b/config/models new file mode 100644 index 0000000..aa335df --- /dev/null +++ b/config/models | |||
@@ -0,0 +1,12 @@ | |||
1 | Item | ||
2 | kind Text | ||
3 | normKind Text | ||
4 | bought Day Maybe | ||
5 | expires Day Maybe | ||
6 | opened Day Maybe | ||
7 | deriving Show Eq | ||
8 | Reference | ||
9 | normKind Text | ||
10 | kind Text | ||
11 | UniqueKind normKind | ||
12 | deriving Show Eq Ord \ No newline at end of file | ||
diff --git a/config/routes b/config/routes new file mode 100644 index 0000000..54d6593 --- /dev/null +++ b/config/routes | |||
@@ -0,0 +1,7 @@ | |||
1 | /static StaticR EmbeddedStatic appStatic | ||
2 | |||
3 | / InventoryListingR GET POST PUT | ||
4 | /inv/#ItemId/edit UpdateItemR GET POST | ||
5 | /inv/#ItemId/open OpenItemR POST | ||
6 | /inv/#ItemId/delete DeleteItemR POST | ||
7 | /inv/#ItemId ItemR GET PUT PATCH DELETE | ||
diff --git a/config/settings.yml b/config/settings.yml new file mode 100644 index 0000000..fcae60c --- /dev/null +++ b/config/settings.yml | |||
@@ -0,0 +1,26 @@ | |||
1 | # Values formatted like "_env:ENV_VAR_NAME:default_value" can be overridden by the specified environment variable. | ||
2 | # See https://github.com/yesodweb/yesod/wiki/Configuration#overriding-configuration-values-with-environment-variables | ||
3 | |||
4 | static-dir: "_env:STATIC_DIR:static" | ||
5 | host: "_env:HOST:*4" # any IPv4 host | ||
6 | port: "_env:PORT:3000" # NB: The port `yesod devel` uses is distinct from this value. Set the `yesod devel` port from the command line. | ||
7 | ip-from-header: "_env:IP_FROM_HEADER:false" | ||
8 | |||
9 | # Default behavior: determine the application root from the request headers. | ||
10 | # Uncomment to set an explicit approot | ||
11 | approot: "_env:APPROOT:" | ||
12 | |||
13 | # Optional values with the following production defaults. | ||
14 | # In development, they default to the inverse. | ||
15 | # | ||
16 | # detailed-logging: false | ||
17 | # should-log-all: false | ||
18 | # reload-templates: false | ||
19 | |||
20 | database: | ||
21 | user: "_env:PGUSER:bar" | ||
22 | password: "_env:PGPASS:" | ||
23 | host: "_env:PGHOST:" | ||
24 | port: "_env:PGPORT:" | ||
25 | database: "_env:PGDATABASE:bar" | ||
26 | poolsize: "_env:PGPOOLSIZE:10" | ||
diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..1e631cf --- /dev/null +++ b/default.nix | |||
@@ -0,0 +1,29 @@ | |||
1 | argumentPackages@{ ... }: | ||
2 | |||
3 | let | ||
4 | defaultPackages = (import <nixpkgs> {}).haskellPackages; | ||
5 | haskellPackages = defaultPackages // argumentPackages; | ||
6 | pkgs = (import <nixpkgs> {}).pkgs // argumentPackages; | ||
7 | |||
8 | webshim = with pkgs; stdenv.mkDerivation rec { | ||
9 | name = "webshim-${version}"; | ||
10 | version = "1.16.0"; | ||
11 | src = fetchFromGitHub { | ||
12 | owner = "aFarkas"; | ||
13 | repo = "webshim"; | ||
14 | rev = "1.16.0"; | ||
15 | sha256 = "14pk7hljqipzp0n7vpgcfxr3w4bla57cwyd7bmwmmxrm2zn62cyh"; | ||
16 | }; | ||
17 | |||
18 | installPhase = '' | ||
19 | mkdir -p $out/js | ||
20 | cp -r $src/js-webshim/dev/* $out/js/ | ||
21 | ''; | ||
22 | }; | ||
23 | in pkgs.stdenv.lib.overrideDerivation (haskellPackages.callPackage ./bar.nix {}) (oldAttrs: { | ||
24 | postUnpack = '' | ||
25 | rm -rf bar/static/jquery.js bar/static/webshim | ||
26 | ln -vs ${pkgs.jquery}/js/jquery.js bar/static | ||
27 | ln -vs ${webshim}/js bar/static/webshim | ||
28 | ''; | ||
29 | }) | ||
diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..b6595aa --- /dev/null +++ b/shell.nix | |||
@@ -0,0 +1,18 @@ | |||
1 | { nixpkgs ? import <nixpkgs> {}, compiler ? "default" }: | ||
2 | |||
3 | let | ||
4 | inherit (nixpkgs) pkgs; | ||
5 | |||
6 | haskellPackages = if compiler == "default" | ||
7 | then pkgs.haskellPackages | ||
8 | else pkgs.haskell.packages.${compiler}; | ||
9 | |||
10 | drv = haskellPackages.callPackage ./bar.nix {}; | ||
11 | in | ||
12 | pkgs.stdenv.lib.overrideDerivation drv.env (oldAttrs: { | ||
13 | buildInputs = oldAttrs.buildInputs ++ (with pkgs; [ cabal2nix gup haskellPackages.hlint haskellPackages.stack haskellPackages.yesod-bin ]); | ||
14 | shellHook = '' | ||
15 | ${oldAttrs.shellHook} | ||
16 | export PROMPT_INFO="${oldAttrs.name}" | ||
17 | ''; | ||
18 | }) | ||
diff --git a/stack.yaml b/stack.yaml new file mode 100644 index 0000000..776a6b4 --- /dev/null +++ b/stack.yaml | |||
@@ -0,0 +1,73 @@ | |||
1 | # This file was automatically generated by 'stack init' | ||
2 | # | ||
3 | # Some commonly used options have been documented as comments in this file. | ||
4 | # For advanced use and comprehensive documentation of the format, please see: | ||
5 | # http://docs.haskellstack.org/en/stable/yaml_configuration/ | ||
6 | |||
7 | # Resolver to choose a 'specific' stackage snapshot or a compiler version. | ||
8 | # A snapshot resolver dictates the compiler version and the set of packages | ||
9 | # to be used for project dependencies. For example: | ||
10 | # | ||
11 | # resolver: lts-3.5 | ||
12 | # resolver: nightly-2015-09-21 | ||
13 | # resolver: ghc-7.10.2 | ||
14 | # resolver: ghcjs-0.1.0_ghc-7.10.2 | ||
15 | # resolver: | ||
16 | # name: custom-snapshot | ||
17 | # location: "./custom-snapshot.yaml" | ||
18 | resolver: lts-8.5 | ||
19 | |||
20 | # User packages to be built. | ||
21 | # Various formats can be used as shown in the example below. | ||
22 | # | ||
23 | # packages: | ||
24 | # - some-directory | ||
25 | # - https://example.com/foo/bar/baz-0.0.2.tar.gz | ||
26 | # - location: | ||
27 | # git: https://github.com/commercialhaskell/stack.git | ||
28 | # commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a | ||
29 | # - location: https://github.com/commercialhaskell/stack/commit/e7b331f14bcffb8367cd58fbfc8b40ec7642100a | ||
30 | # extra-dep: true | ||
31 | # subdirs: | ||
32 | # - auto-update | ||
33 | # - wai | ||
34 | # | ||
35 | # A package marked 'extra-dep: true' will only be built if demanded by a | ||
36 | # non-dependency (i.e. a user package), and its test suites and benchmarks | ||
37 | # will not be run. This is useful for tweaking upstream packages. | ||
38 | packages: | ||
39 | - '.' | ||
40 | # Dependency packages to be pulled from upstream that are not in the resolver | ||
41 | # (e.g., acme-missiles-0.3) | ||
42 | extra-deps: [] | ||
43 | |||
44 | # Override default flag values for local packages and extra-deps | ||
45 | flags: {} | ||
46 | |||
47 | # Extra package databases containing global packages | ||
48 | extra-package-dbs: [] | ||
49 | |||
50 | # Control whether we use the GHC we find on the path | ||
51 | system-ghc: true | ||
52 | # | ||
53 | # Require a specific version of stack, using version ranges | ||
54 | # require-stack-version: -any # Default | ||
55 | # require-stack-version: ">=1.3" | ||
56 | # | ||
57 | # Override the architecture used by stack, especially useful on Windows | ||
58 | # arch: i386 | ||
59 | # arch: x86_64 | ||
60 | # | ||
61 | # Extra directories used by stack for building | ||
62 | # extra-include-dirs: [/path/to/dir] | ||
63 | # extra-lib-dirs: [/path/to/dir] | ||
64 | # | ||
65 | # Allow a newer minor version of GHC than the snapshot specifies | ||
66 | # compiler-check: newer-minor | ||
67 | nix: | ||
68 | #enable: true | ||
69 | packages: | ||
70 | - postgresql | ||
71 | - zlib | ||
72 | - haskellPackages.yesod-bin | ||
73 | - haskellPackages.stack | ||
diff --git a/static/jquery.js.gup b/static/jquery.js.gup new file mode 100644 index 0000000..b6db680 --- /dev/null +++ b/static/jquery.js.gup | |||
@@ -0,0 +1,3 @@ | |||
1 | #!/usr/bin/env zsh | ||
2 | |||
3 | curl -Ls "https://cdn.jsdelivr.net/jquery/3.1.1/jquery.js" >$1 \ No newline at end of file | ||
diff --git a/static/webshim.gup b/static/webshim.gup new file mode 100644 index 0000000..f9e205c --- /dev/null +++ b/static/webshim.gup | |||
@@ -0,0 +1,6 @@ | |||
1 | #!/usr/bin/env zsh | ||
2 | |||
3 | mkdir -p ${1} | ||
4 | |||
5 | version="1.16.0" | ||
6 | curl -Ls "https://github.com/aFarkas/webshim/archive/${version}.tar.gz" | tar -C ${1} -xz --strip-components=3 webshim-${version}/js-webshim/dev/ | ||
diff --git a/templates/default-layout-wrapper.hamlet b/templates/default-layout-wrapper.hamlet new file mode 100644 index 0000000..dd317f1 --- /dev/null +++ b/templates/default-layout-wrapper.hamlet | |||
@@ -0,0 +1,15 @@ | |||
1 | $newline never | ||
2 | $doctype 5 | ||
3 | <html lang="en"> | ||
4 | <head> | ||
5 | <meta charset="UTF-8"> | ||
6 | |||
7 | <title>#{pageTitle pc} | ||
8 | <meta name="description" content=""> | ||
9 | <meta name="author" content=""> | ||
10 | |||
11 | <meta name="viewport" content="width=device-width,initial-scale=1"> | ||
12 | |||
13 | ^{pageHead pc} | ||
14 | <body> | ||
15 | ^{pageBody pc} | ||
diff --git a/templates/default-layout.cassius b/templates/default-layout.cassius new file mode 100644 index 0000000..492cde8 --- /dev/null +++ b/templates/default-layout.cassius | |||
@@ -0,0 +1,77 @@ | |||
1 | .table | ||
2 | display: table | ||
3 | .table div | ||
4 | vertical-align: middle | ||
5 | .td | ||
6 | display: table-cell | ||
7 | text-align: center | ||
8 | padding: 0.25em | ||
9 | .tr | ||
10 | display: table-row | ||
11 | .tc | ||
12 | display: table-caption | ||
13 | padding: 0.25em | ||
14 | .th | ||
15 | display: table-cell | ||
16 | font-variant: small-caps | ||
17 | font-weight: bold | ||
18 | text-align: center | ||
19 | padding: 0.25em | ||
20 | .kind | ||
21 | display: table-cell | ||
22 | text-align: left | ||
23 | padding: 0.25em | ||
24 | .table .table .td, .table .table .tc, .table .table .th, .table .table .kind | ||
25 | padding: 0 | ||
26 | .error | ||
27 | background-color: #fdd | ||
28 | text-align: center | ||
29 | color: #c00 | ||
30 | list-style-type: none | ||
31 | button | ||
32 | width: 6em | ||
33 | display:inline-text | ||
34 | .day hr | ||
35 | width: 2em | ||
36 | border: 1px solid #ddd | ||
37 | border-style: solid none solid none | ||
38 | .sepBelow > div, .sepAbove > div | ||
39 | border: 2px none #ddd | ||
40 | .sepBelow > div | ||
41 | border-bottom-style: solid | ||
42 | .sepAbove > div | ||
43 | border-top-style: solid | ||
44 | .color:nth-child(even) | ||
45 | background-color: #f0f0f0 | ||
46 | .color:nth-child(odd) | ||
47 | background-color: #fff | ||
48 | body > div | ||
49 | margin: 0 auto | ||
50 | .table > h1 | ||
51 | display: table-caption | ||
52 | nav ul | ||
53 | display:block | ||
54 | text-align: center | ||
55 | li | ||
56 | display:inline-block | ||
57 | font-variant: small-caps | ||
58 | font-size: 1.5em | ||
59 | font-weight: bold | ||
60 | a | ||
61 | text-decoration:none | ||
62 | color:#aaa | ||
63 | a:hover | ||
64 | color:inherit | ||
65 | li.active | ||
66 | a | ||
67 | color:inherit | ||
68 | li::before | ||
69 | content:" | " | ||
70 | color: #ddd | ||
71 | li:first-child::before | ||
72 | content:"" | ||
73 | label.checkbox | ||
74 | input | ||
75 | vertical-align: middle | ||
76 | span | ||
77 | vertical-align: middle | ||
diff --git a/templates/default-layout.hamlet b/templates/default-layout.hamlet new file mode 100644 index 0000000..14c8cd5 --- /dev/null +++ b/templates/default-layout.hamlet | |||
@@ -0,0 +1,12 @@ | |||
1 | <nav> | ||
2 | <ul> | ||
3 | $forall MenuItem{..} <- menuItems | ||
4 | <li :Just menuItemRoute == mCurrentRoute:.active> | ||
5 | <a href=@{menuItemRoute}>#{menuItemLabel} | ||
6 | |||
7 | $if not $ null msgs | ||
8 | <ul #messages> | ||
9 | $forall (status, msg) <- msgs | ||
10 | <li .#{status}>#{msg} | ||
11 | |||
12 | ^{widget} | ||
diff --git a/templates/default-layout.julius b/templates/default-layout.julius new file mode 100644 index 0000000..5ef6b42 --- /dev/null +++ b/templates/default-layout.julius | |||
@@ -0,0 +1,7 @@ | |||
1 | webshims.setOptions("forms-ext", { | ||
2 | "widgets": { | ||
3 | "classes": "hide-dropdownbtn" | ||
4 | } | ||
5 | }); | ||
6 | webshims.activeLang("en-GB"); | ||
7 | webshims.polyfill("forms forms-ext"); | ||
diff --git a/templates/default-message-widget.hamlet b/templates/default-message-widget.hamlet new file mode 100644 index 0000000..2eee196 --- /dev/null +++ b/templates/default-message-widget.hamlet | |||
@@ -0,0 +1,6 @@ | |||
1 | <div .container> | ||
2 | <div .row> | ||
3 | <div .col-md-2> | ||
4 | <div .col-md-8> | ||
5 | <h1>#{title} | ||
6 | ^{body} | ||
diff --git a/templates/homepage.hamlet b/templates/homepage.hamlet new file mode 100644 index 0000000..fb79965 --- /dev/null +++ b/templates/homepage.hamlet | |||
@@ -0,0 +1,139 @@ | |||
1 | <div .masthead> | ||
2 | <div .container> | ||
3 | <div .row> | ||
4 | <h1 .header> | ||
5 | Yesod—a modern framework for blazing fast websites | ||
6 | <h2> | ||
7 | Fast, stable & spiced with great community | ||
8 | <a href="http://www.yesodweb.com/book/" .btn.btn-info.btn-lg> | ||
9 | Read the Book | ||
10 | |||
11 | <div .container> | ||
12 | <!-- Starting | ||
13 | ================================================== --> | ||
14 | <div .bs-docs-section> | ||
15 | <div .row> | ||
16 | <div .col-lg-12> | ||
17 | <div .page-header> | ||
18 | <h1 #start>Starting | ||
19 | |||
20 | <p> | ||
21 | Now that you have a working project you should use the | ||
22 | <a href=http://www.yesodweb.com/book/>Yesod book</a> to learn more. | ||
23 | <p> | ||
24 | You can also use this scaffolded site to explore some concepts, and best practices. | ||
25 | |||
26 | <ul .list-group> | ||
27 | |||
28 | <li .list-group-item> | ||
29 | This page was generated by the <tt>#{handlerName}</tt> handler in | ||
30 | <tt>Handler/Home.hs</tt>. | ||
31 | |||
32 | <li .list-group-item> | ||
33 | The <tt>#{handlerName}</tt> handler is set to generate your | ||
34 | site's home screen in Routes file | ||
35 | <tt>config/routes | ||
36 | |||
37 | <li .list-group-item> | ||
38 | We can link to other handlers, like the <a href="@{ProfileR}">Profile</a>. | ||
39 | Try it out as an anonymous user and see the access denied. | ||
40 | Then, try to <a href="@{AuthR LoginR}">login</a> with the dummy authentication added | ||
41 | while in development. | ||
42 | |||
43 | <li .list-group-item> | ||
44 | The HTML you are seeing now is actually composed by a number of <em>widgets</em>, # | ||
45 | most of them are brought together by the <tt>defaultLayout</tt> function which # | ||
46 | is defined in the <tt>Foundation.hs</tt> module, and used by <tt>#{handlerName}</tt>. # | ||
47 | All the files for templates and wigdets are in <tt>templates</tt>. | ||
48 | |||
49 | <li .list-group-item> | ||
50 | A Widget's Html, Css and Javascript are separated in three files with the | ||
51 | <tt>.hamlet</tt>, <tt>.lucius</tt> and <tt>.julius</tt> extensions. | ||
52 | |||
53 | <li .list-group-item ##{aDomId}> | ||
54 | If you had javascript enabled then you wouldn't be seeing this. | ||
55 | |||
56 | <hr> | ||
57 | |||
58 | <!-- Forms | ||
59 | ================================================== --> | ||
60 | <div .bs-docs-section> | ||
61 | <div .row> | ||
62 | <div .col-lg-12> | ||
63 | <div .page-header> | ||
64 | <h1 #forms>Forms | ||
65 | |||
66 | <p> | ||
67 | This is an example of a form. Read the | ||
68 | <a href="http://www.yesodweb.com/book/forms">Forms chapter</a> # | ||
69 | on the yesod book to learn more about them. | ||
70 | |||
71 | <div .row> | ||
72 | <div .col-lg-6> | ||
73 | <div .bs-callout bs-callout-info well> | ||
74 | <form .form-horizontal method=post action=@{HomeR}#forms enctype=#{formEnctype}> | ||
75 | ^{formWidget} | ||
76 | |||
77 | <button .btn.btn-primary type="submit"> | ||
78 | Upload it! | ||
79 | |||
80 | |||
81 | <div .col-lg-4.col-lg-offset-1> | ||
82 | <div .bs-callout.bs-callout-info.upload-response> | ||
83 | |||
84 | $maybe (FileForm info con) <- submission | ||
85 | Your file type is <em>#{fileContentType info}</em>. You say it has: <em>#{con}</em> | ||
86 | |||
87 | $nothing | ||
88 | File upload result will be here... | ||
89 | |||
90 | |||
91 | <hr> | ||
92 | |||
93 | <!-- JSON | ||
94 | ================================================== --> | ||
95 | <div .bs-docs-section> | ||
96 | <div .row> | ||
97 | <div .col-lg-12> | ||
98 | <div .page-header> | ||
99 | <h1 #json>JSON | ||
100 | |||
101 | <p> | ||
102 | Yesod has JSON support baked-in. | ||
103 | The form below makes an AJAX request with Javascript, | ||
104 | then updates the page with your submission. | ||
105 | (see <tt>Handler/Comment.hs</tt>, <tt>templates/homepage.julius</tt>, | ||
106 | and <tt>Handler/Home.hs</tt> for the implementation). | ||
107 | |||
108 | <div .row> | ||
109 | <div .col-lg-6> | ||
110 | <div .bs-callout.bs-callout-info.well> | ||
111 | <form .form-horizontal ##{commentFormId}> | ||
112 | <div .field> | ||
113 | <textarea rows="2" ##{commentTextareaId} placeholder="Your comment here..." required></textarea> | ||
114 | |||
115 | <button .btn.btn-primary type=submit> | ||
116 | Create comment | ||
117 | |||
118 | <div .col-lg-4.col-lg-offset-1> | ||
119 | <div .bs-callout.bs-callout-info> | ||
120 | <small> | ||
121 | Your comments will appear here. You can also open the | ||
122 | console log to see the raw response from the server. | ||
123 | <ul ##{commentListId}> | ||
124 | |||
125 | <hr> | ||
126 | |||
127 | <!-- Testing | ||
128 | ================================================== --> | ||
129 | <div .bs-docs-section> | ||
130 | <div .row> | ||
131 | <div .col-lg-12> | ||
132 | <div .page-header> | ||
133 | <h1 #test>Testing | ||
134 | |||
135 | <p> | ||
136 | And last but not least, Testing. In <tt>test/Spec.hs</tt> you will find a # | ||
137 | test suite that performs tests on this page. | ||
138 | <p> | ||
139 | You can run your tests by doing: <code>stack test</code> | ||
diff --git a/templates/homepage.julius b/templates/homepage.julius new file mode 100644 index 0000000..865882e --- /dev/null +++ b/templates/homepage.julius | |||
@@ -0,0 +1,34 @@ | |||
1 | document.getElementById(#{toJSON aDomId}).innerHTML = "This text was added by the Javascript part of the homepage widget."; | ||
2 | |||
3 | $(function() { | ||
4 | $("##{rawJS commentFormId}").submit(function(event) { | ||
5 | event.preventDefault(); | ||
6 | |||
7 | var message = $("##{rawJS commentTextareaId}").val(); | ||
8 | // (Browsers that enforce the "required" attribute on the textarea won't see this alert) | ||
9 | if (!message) { | ||
10 | alert("Please fill out the comment form first."); | ||
11 | return; | ||
12 | } | ||
13 | |||
14 | // Make an AJAX request to the server to create a new comment | ||
15 | $.ajax({ | ||
16 | url: '@{CommentR}', | ||
17 | type: 'POST', | ||
18 | contentType: "application/json", | ||
19 | data: JSON.stringify({ | ||
20 | message: message, | ||
21 | }), | ||
22 | success: function (data) { | ||
23 | var newNode = $("<li></li>"); | ||
24 | newNode.text(data.message); | ||
25 | console.log(data); | ||
26 | $("##{rawJS commentListId}").append(newNode); | ||
27 | }, | ||
28 | error: function (data) { | ||
29 | console.log("Error creating comment: " + data); | ||
30 | }, | ||
31 | }); | ||
32 | |||
33 | }); | ||
34 | }); | ||
diff --git a/templates/homepage.lucius b/templates/homepage.lucius new file mode 100644 index 0000000..3197fd5 --- /dev/null +++ b/templates/homepage.lucius | |||
@@ -0,0 +1,13 @@ | |||
1 | h2##{aDomId} { | ||
2 | color: #990 | ||
3 | } | ||
4 | |||
5 | li { | ||
6 | line-height: 2em; | ||
7 | font-size: 16px | ||
8 | } | ||
9 | |||
10 | ##{commentTextareaId} { | ||
11 | width: 400px; | ||
12 | height: 100px; | ||
13 | } | ||
diff --git a/templates/inventoryListing.hamlet b/templates/inventoryListing.hamlet new file mode 100644 index 0000000..7c2c06b --- /dev/null +++ b/templates/inventoryListing.hamlet | |||
@@ -0,0 +1,49 @@ | |||
1 | <div .table> | ||
2 | <div .tr .sepBelow> | ||
3 | <div .th>Description | ||
4 | <div .th>Bought | ||
5 | <div .th>Expires | ||
6 | <div .th>Opened | ||
7 | <div .th>Actions | ||
8 | $if isJust (preview insertForm =<< formState) | ||
9 | $with Just InsertForm{..} <- formState | ||
10 | <form .tr .sepBelow action=@{InventoryListingR} method=post enctype=#{fsInsertEncoding}> | ||
11 | ^{fsInsertForm} | ||
12 | <div .td> | ||
13 | <button type=submit> | ||
14 | Insert | ||
15 | $forall Entity itemId Item{..} <- stock | ||
16 | $if Just itemId == (preview updateItem =<< formState) | ||
17 | $with Just UpdateForm{..} <- formState | ||
18 | <form .tr .color action=@{UpdateItemR fsUpdateItem}##{toPathPiece fsUpdateItem} method=post enctype=#{fsUpdateEncoding} ##{toPathPiece fsUpdateItem}> | ||
19 | ^{fsUpdateForm} | ||
20 | <div .td> | ||
21 | <button type=submit> | ||
22 | Save Changes | ||
23 | $else | ||
24 | <div .tr .color ##{toPathPiece itemId}> | ||
25 | <div .kind>#{itemKind} | ||
26 | <div .td .day> | ||
27 | $maybe bought <- itemBought | ||
28 | #{dayFormat bought} | ||
29 | $nothing | ||
30 | <hr> | ||
31 | <div .td .day> | ||
32 | $maybe expires <- itemExpires | ||
33 | #{dayFormat expires} | ||
34 | $nothing | ||
35 | <hr> | ||
36 | <div .td .day> | ||
37 | $maybe opened <- itemOpened | ||
38 | #{dayFormat opened} | ||
39 | $nothing | ||
40 | <form method=post action=@{OpenItemR itemId}> | ||
41 | <button type=submit> | ||
42 | Open | ||
43 | <div .td> | ||
44 | <form method=get action=@{UpdateItemR itemId}##{toPathPiece itemId}> | ||
45 | <button type=submit> | ||
46 | Edit | ||
47 | <form method=post action=@{DeleteItemR itemId}> | ||
48 | <button type=submit> | ||
49 | Delete | ||
diff --git a/templates/profile.hamlet b/templates/profile.hamlet new file mode 100644 index 0000000..2420de6 --- /dev/null +++ b/templates/profile.hamlet | |||
@@ -0,0 +1,10 @@ | |||
1 | <div .ui.container> | ||
2 | |||
3 | <h1> | ||
4 | Access granted! | ||
5 | |||
6 | <p> | ||
7 | This page is protected and access is allowed only for authenticated users. | ||
8 | |||
9 | <p> | ||
10 | Your data is protected with us <strong><span class="username">#{userIdent user}</span></strong>! | ||