2016-02-02 14:50:30 +03:00
|
|
|
{-# LANGUAGE
|
|
|
|
OverloadedStrings,
|
2016-02-02 19:29:23 +03:00
|
|
|
TemplateHaskell,
|
|
|
|
RecordWildCards,
|
|
|
|
RankNTypes,
|
2016-02-19 22:12:23 +03:00
|
|
|
FlexibleInstances,
|
|
|
|
QuasiQuotes,
|
2016-02-02 14:50:30 +03:00
|
|
|
NoImplicitPrelude
|
|
|
|
#-}
|
|
|
|
|
|
|
|
|
2016-02-02 12:35:39 +03:00
|
|
|
module Main (main) where
|
|
|
|
|
2016-02-02 14:50:30 +03:00
|
|
|
|
|
|
|
-- General
|
2016-02-02 19:29:23 +03:00
|
|
|
import BasePrelude hiding (Category)
|
2016-02-14 15:19:36 +03:00
|
|
|
-- Monads and monad transformers
|
|
|
|
import Control.Monad.State
|
2016-02-02 19:29:23 +03:00
|
|
|
-- Lenses
|
|
|
|
import Lens.Micro.Platform
|
2016-02-02 14:50:30 +03:00
|
|
|
-- Text
|
|
|
|
import Data.Text (Text)
|
2016-02-02 19:29:23 +03:00
|
|
|
import qualified Data.Text as T
|
2016-02-02 14:50:30 +03:00
|
|
|
import qualified Data.Text.Lazy as TL
|
2016-02-19 22:12:23 +03:00
|
|
|
import qualified Data.Text.Lazy.Builder as TL
|
2016-02-02 19:29:23 +03:00
|
|
|
import Data.Text.Format hiding (format)
|
|
|
|
import qualified Data.Text.Format as Format
|
2016-02-19 22:12:23 +03:00
|
|
|
import qualified Data.Text.Format.Params as Format
|
|
|
|
import NeatInterpolation
|
2016-02-02 14:50:30 +03:00
|
|
|
-- Web
|
2016-02-02 19:29:23 +03:00
|
|
|
import Lucid hiding (for_)
|
2016-02-19 22:12:23 +03:00
|
|
|
import Web.Spock hiding (get, text)
|
2016-02-14 15:19:36 +03:00
|
|
|
import qualified Web.Spock as Spock
|
2016-02-02 19:29:23 +03:00
|
|
|
import Network.Wai.Middleware.Static
|
2016-02-02 14:50:30 +03:00
|
|
|
|
|
|
|
|
2016-02-14 15:19:36 +03:00
|
|
|
-- | Unique id, used for categories and items.
|
|
|
|
type UID = Int
|
|
|
|
|
2016-02-14 14:03:39 +03:00
|
|
|
data ItemKind = HackageLibrary | Library | Unknown
|
|
|
|
|
|
|
|
data Item = Item {
|
2016-02-14 15:19:36 +03:00
|
|
|
_itemId :: UID,
|
|
|
|
_name :: Text,
|
|
|
|
_pros :: [Text],
|
|
|
|
_cons :: [Text],
|
|
|
|
_link :: Maybe Text,
|
|
|
|
_kind :: ItemKind }
|
2016-02-02 20:41:29 +03:00
|
|
|
|
|
|
|
makeLenses ''Item
|
|
|
|
|
2016-02-02 19:29:23 +03:00
|
|
|
data Category = Category {
|
2016-02-14 15:19:36 +03:00
|
|
|
_catId :: UID,
|
2016-02-14 14:03:39 +03:00
|
|
|
_title :: Text,
|
|
|
|
_items :: [Item] }
|
2016-02-02 19:29:23 +03:00
|
|
|
|
|
|
|
makeLenses ''Category
|
|
|
|
|
|
|
|
data S = S {
|
2016-02-14 15:19:36 +03:00
|
|
|
_nextId :: UID,
|
2016-02-02 19:29:23 +03:00
|
|
|
_categories :: [Category] }
|
|
|
|
|
|
|
|
makeLenses ''S
|
|
|
|
|
2016-02-17 22:47:52 +03:00
|
|
|
categoryById :: UID -> Lens' S Category
|
|
|
|
categoryById uid = singular $
|
|
|
|
categories.each . filtered ((== uid) . view catId)
|
2016-02-14 15:19:36 +03:00
|
|
|
|
2016-02-17 22:47:52 +03:00
|
|
|
itemById :: UID -> Lens' S Item
|
|
|
|
itemById uid = singular $
|
|
|
|
categories.each . items.each . filtered ((== uid) . view itemId)
|
2016-02-14 14:03:39 +03:00
|
|
|
|
2016-02-14 15:19:36 +03:00
|
|
|
newId :: IORef S -> IO UID
|
2016-02-14 14:03:39 +03:00
|
|
|
newId s = do
|
2016-02-14 15:19:36 +03:00
|
|
|
uid <- view nextId <$> readIORef s
|
2016-02-14 14:03:39 +03:00
|
|
|
modifyIORef s (nextId %~ succ)
|
2016-02-14 15:19:36 +03:00
|
|
|
return uid
|
2016-02-02 19:29:23 +03:00
|
|
|
|
2016-02-14 14:10:54 +03:00
|
|
|
emptyState :: S
|
|
|
|
emptyState = S {
|
|
|
|
_nextId = 0,
|
|
|
|
_categories = [] }
|
|
|
|
|
|
|
|
sampleState :: S
|
|
|
|
sampleState = S {
|
2016-02-14 15:19:36 +03:00
|
|
|
_nextId = 3,
|
2016-02-14 14:10:54 +03:00
|
|
|
_categories = [
|
|
|
|
Category {
|
|
|
|
_catId = 0,
|
|
|
|
_title = "lenses",
|
|
|
|
_items = [
|
|
|
|
Item {
|
2016-02-14 15:19:36 +03:00
|
|
|
_itemId = 1,
|
|
|
|
_name = "lens",
|
|
|
|
_pros = ["the standard lenses library", "batteries included"],
|
|
|
|
_cons = ["huge"],
|
|
|
|
_link = Nothing,
|
|
|
|
_kind = HackageLibrary },
|
2016-02-14 14:10:54 +03:00
|
|
|
Item {
|
2016-02-14 15:19:36 +03:00
|
|
|
_itemId = 2,
|
|
|
|
_name = "microlens",
|
|
|
|
_pros = ["very small", "good for libraries"],
|
|
|
|
_cons = ["doesn't have advanced features"],
|
|
|
|
_link = Nothing,
|
|
|
|
_kind = HackageLibrary }
|
2016-02-14 14:10:54 +03:00
|
|
|
] }
|
|
|
|
] }
|
|
|
|
|
2016-02-02 12:35:39 +03:00
|
|
|
main :: IO ()
|
2016-02-02 14:50:30 +03:00
|
|
|
main = runSpock 8080 $ spockT id $ do
|
2016-02-02 19:29:23 +03:00
|
|
|
middleware (staticPolicy (addBase "static"))
|
2016-02-14 14:10:54 +03:00
|
|
|
stateVar <- liftIO $ newIORef sampleState
|
2016-02-19 22:12:23 +03:00
|
|
|
let withS :: MonadIO m => State S a -> m a
|
|
|
|
withS f = liftIO $ atomicModifyIORef' stateVar (swap . runState f)
|
2016-02-14 14:19:32 +03:00
|
|
|
|
2016-02-19 22:34:51 +03:00
|
|
|
-- Render the main page.
|
2016-02-14 15:19:36 +03:00
|
|
|
Spock.get root $ do
|
2016-02-02 19:29:23 +03:00
|
|
|
s <- liftIO $ readIORef stateVar
|
2016-02-02 19:41:28 +03:00
|
|
|
lucid $ renderRoot s
|
2016-02-14 14:19:32 +03:00
|
|
|
|
2016-02-14 15:19:36 +03:00
|
|
|
-- The “/add” methods return rendered parts of the structure (added
|
|
|
|
-- categories, changed items, etc) so that the Javascript part could take
|
|
|
|
-- them and inject into the page. We don't want to duplicate rendering on
|
|
|
|
-- server side and on client side.
|
|
|
|
|
2016-02-19 22:27:27 +03:00
|
|
|
-- (category|item)/action
|
|
|
|
-- (category|item)/id/action
|
|
|
|
-- (category|item)/id/thing/action
|
|
|
|
|
2016-02-19 22:34:51 +03:00
|
|
|
-- Create a new category, with its title submitted via a POST request.
|
2016-02-19 22:27:27 +03:00
|
|
|
Spock.post "/category/add" $ do
|
2016-02-14 14:03:39 +03:00
|
|
|
title' <- param' "title"
|
|
|
|
id' <- liftIO (newId stateVar)
|
2016-02-02 19:29:23 +03:00
|
|
|
let newCategory = Category {
|
2016-02-14 14:03:39 +03:00
|
|
|
_catId = id',
|
|
|
|
_title = title',
|
|
|
|
_items = [] }
|
2016-02-14 15:19:36 +03:00
|
|
|
withS $
|
|
|
|
categories %= (++ [newCategory])
|
2016-02-02 19:41:28 +03:00
|
|
|
lucid $ renderCategory newCategory
|
2016-02-14 14:19:32 +03:00
|
|
|
|
2016-02-19 22:34:51 +03:00
|
|
|
-- Create a new library in the specified category, with the library name
|
|
|
|
-- and category id submitted via a POST request.
|
2016-02-19 22:27:27 +03:00
|
|
|
Spock.post ("/category" <//> var <//> "library/add") $ \catId' -> do
|
2016-02-14 14:03:39 +03:00
|
|
|
name' <- param' "name"
|
2016-02-14 15:19:36 +03:00
|
|
|
id' <- liftIO (newId stateVar)
|
2016-02-14 14:03:39 +03:00
|
|
|
let newItem = Item {
|
2016-02-14 15:19:36 +03:00
|
|
|
_itemId = id',
|
|
|
|
_name = name',
|
|
|
|
_pros = [],
|
|
|
|
_cons = [],
|
|
|
|
_link = Nothing,
|
|
|
|
_kind = HackageLibrary }
|
2016-02-02 19:29:23 +03:00
|
|
|
-- TODO: maybe do something if the category doesn't exist (e.g. has been
|
|
|
|
-- already deleted)
|
2016-02-14 15:19:36 +03:00
|
|
|
withS $
|
|
|
|
categoryById catId' . items %= (++ [newItem])
|
2016-02-02 20:41:29 +03:00
|
|
|
lucid $ renderItem newItem
|
2016-02-02 19:29:23 +03:00
|
|
|
|
2016-02-19 22:34:51 +03:00
|
|
|
-- Add a pro (argument in favor of a library).
|
2016-02-19 22:27:27 +03:00
|
|
|
Spock.post ("/item" <//> var <//> "pros/add") $ \itemId' -> do
|
2016-02-14 15:19:36 +03:00
|
|
|
content <- param' "content"
|
2016-02-17 19:43:35 +03:00
|
|
|
changedItem <- withS $ do
|
2016-02-14 15:19:36 +03:00
|
|
|
itemById itemId' . pros %= (++ [content])
|
2016-02-17 22:47:52 +03:00
|
|
|
use (itemById itemId')
|
2016-02-17 19:43:35 +03:00
|
|
|
lucid $ renderItem changedItem
|
2016-02-14 15:19:36 +03:00
|
|
|
|
2016-02-19 22:34:51 +03:00
|
|
|
-- Add a con (argument against a library).
|
2016-02-19 22:27:27 +03:00
|
|
|
Spock.post ("/item" <//> var <//> "cons/add") $ \itemId' -> do
|
2016-02-14 15:19:36 +03:00
|
|
|
content <- param' "content"
|
2016-02-17 19:43:35 +03:00
|
|
|
changedItem <- withS $ do
|
2016-02-14 15:19:36 +03:00
|
|
|
itemById itemId' . cons %= (++ [content])
|
2016-02-17 22:47:52 +03:00
|
|
|
use (itemById itemId')
|
2016-02-17 19:43:35 +03:00
|
|
|
lucid $ renderItem changedItem
|
|
|
|
|
2016-02-19 22:34:51 +03:00
|
|
|
-- Set the title of a category (returns rendered new title).
|
2016-02-19 22:27:27 +03:00
|
|
|
Spock.post ("/category" <//> var <//> "title/set") $ \catId' -> do
|
2016-02-17 19:43:35 +03:00
|
|
|
title' <- param' "title"
|
|
|
|
changedCategory <- withS $ do
|
|
|
|
categoryById catId' . title .= title'
|
2016-02-17 22:47:52 +03:00
|
|
|
use (categoryById catId')
|
2016-02-17 19:43:35 +03:00
|
|
|
lucid $ renderCategoryHeading changedCategory
|
|
|
|
|
2016-02-19 22:34:51 +03:00
|
|
|
-- Return rendered title of a category.
|
2016-02-19 22:27:27 +03:00
|
|
|
Spock.get ("/category" <//> var <//> "title/render-normal") $ \catId' -> do
|
2016-02-17 22:47:52 +03:00
|
|
|
category <- withS $ use (categoryById catId')
|
2016-02-19 22:27:27 +03:00
|
|
|
lucid $ renderCategoryHeading category
|
2016-02-17 19:43:35 +03:00
|
|
|
|
2016-02-19 22:34:51 +03:00
|
|
|
-- Return rendered title of a category the way it should look when the
|
|
|
|
-- category is being edited.
|
2016-02-19 22:27:27 +03:00
|
|
|
Spock.get ("/category" <//> var <//> "title/render-edit") $ \catId' -> do
|
2016-02-17 22:47:52 +03:00
|
|
|
category <- withS $ use (categoryById catId')
|
2016-02-19 22:27:27 +03:00
|
|
|
lucid $ renderCategoryHeadingEdit category
|
2016-02-14 15:19:36 +03:00
|
|
|
|
2016-02-02 19:41:28 +03:00
|
|
|
renderRoot :: S -> Html ()
|
|
|
|
renderRoot s = do
|
2016-02-14 14:03:39 +03:00
|
|
|
includeJS "https://ajax.googleapis.com/ajax/libs/jquery/2.2.0/jquery.min.js"
|
|
|
|
includeCSS "/css.css"
|
2016-02-19 22:34:51 +03:00
|
|
|
-- Include definitions of all Javascript functions that we have defined in
|
|
|
|
-- this file.
|
2016-02-19 22:12:23 +03:00
|
|
|
script_ $ T.unlines (map snd (allJSFunctions :: [(Text, Text)]))
|
2016-02-02 19:29:23 +03:00
|
|
|
div_ [id_ "categories"] $ do
|
2016-02-02 19:41:28 +03:00
|
|
|
mapM_ renderCategory (s ^. categories)
|
2016-02-19 22:12:23 +03:00
|
|
|
input_ [type_ "text", placeholder_ "new category",
|
2016-02-20 00:13:20 +03:00
|
|
|
submitFunc (js_addCategory [js_this_value])]
|
2016-02-02 14:50:30 +03:00
|
|
|
|
2016-02-17 19:43:35 +03:00
|
|
|
renderCategoryHeading :: Category -> Html ()
|
|
|
|
renderCategoryHeading category =
|
|
|
|
h2_ $ do
|
|
|
|
-- TODO: make category headings anchor links
|
|
|
|
toHtml (category^.title)
|
2016-02-20 00:13:20 +03:00
|
|
|
textButton "edit" $
|
|
|
|
js_startCategoryHeadingEdit [category^.catId]
|
2016-02-17 19:43:35 +03:00
|
|
|
|
|
|
|
renderCategoryHeadingEdit :: Category -> Html ()
|
|
|
|
renderCategoryHeadingEdit category =
|
|
|
|
h2_ $ do
|
2016-02-19 22:34:51 +03:00
|
|
|
let handler = js_submitCategoryHeadingEdit
|
2016-02-20 00:13:20 +03:00
|
|
|
(category^.catId, js_this_value)
|
2016-02-17 19:43:35 +03:00
|
|
|
input_ [type_ "text", value_ (category^.title), submitFunc handler]
|
2016-02-19 22:34:51 +03:00
|
|
|
textButton "cancel" $ js_cancelCategoryHeadingEdit [category^.catId]
|
2016-02-17 19:43:35 +03:00
|
|
|
|
2016-02-02 19:41:28 +03:00
|
|
|
renderCategory :: Category -> Html ()
|
2016-02-14 14:03:39 +03:00
|
|
|
renderCategory category =
|
|
|
|
div_ [id_ (format "cat{}" [category^.catId])] $ do
|
2016-02-17 19:43:35 +03:00
|
|
|
renderCategoryHeading category
|
2016-02-14 14:03:39 +03:00
|
|
|
-- Note: if you change anything here, look at js.js/addLibrary to see
|
|
|
|
-- whether it has to be updated.
|
|
|
|
div_ [class_ "items"] $
|
|
|
|
mapM_ renderItem (category^.items)
|
2016-02-20 00:13:20 +03:00
|
|
|
let handler = js_addLibrary (category^.catId, js_this_value)
|
2016-02-14 15:19:36 +03:00
|
|
|
input_ [type_ "text", placeholder_ "new item", submitFunc handler]
|
2016-02-02 19:41:28 +03:00
|
|
|
|
2016-02-14 14:22:47 +03:00
|
|
|
-- TODO: when the link for a HackageLibrary isn't empty, show it separately
|
|
|
|
-- (as “site”), don't replace the Hackage link
|
2016-02-02 20:41:29 +03:00
|
|
|
renderItem :: Item -> Html ()
|
2016-02-14 14:03:39 +03:00
|
|
|
renderItem item =
|
2016-02-14 15:19:36 +03:00
|
|
|
div_ [class_ "item", id_ (format "item{}" [item^.itemId])] $ do
|
2016-02-14 14:03:39 +03:00
|
|
|
h3_ itemHeader
|
|
|
|
div_ [class_ "pros-cons"] $ do
|
2016-02-14 14:19:32 +03:00
|
|
|
div_ [class_ "pros"] $ do
|
2016-02-14 14:03:39 +03:00
|
|
|
p_ "Pros:"
|
|
|
|
ul_ $ mapM_ (li_ . toHtml) (item^.pros)
|
2016-02-20 00:13:20 +03:00
|
|
|
let handler = js_addPros (item^.itemId, js_this_value)
|
2016-02-14 15:19:36 +03:00
|
|
|
input_ [type_ "text", placeholder_ "add pros", submitFunc handler]
|
2016-02-14 14:19:32 +03:00
|
|
|
div_ [class_ "cons"] $ do
|
2016-02-14 14:03:39 +03:00
|
|
|
p_ "Cons:"
|
|
|
|
ul_ $ mapM_ (li_ . toHtml) (item^.cons)
|
2016-02-20 00:13:20 +03:00
|
|
|
let handler = js_addCons (item^.itemId, js_this_value)
|
2016-02-14 15:19:36 +03:00
|
|
|
input_ [type_ "text", placeholder_ "add cons", submitFunc handler]
|
2016-02-02 20:41:29 +03:00
|
|
|
where
|
2016-02-14 14:03:39 +03:00
|
|
|
hackageLink = format "https://hackage.haskell.org/package/{}"
|
|
|
|
[item^.name]
|
|
|
|
itemHeader = case (item^.link, item^.kind) of
|
|
|
|
(Just l, _) ->
|
|
|
|
a_ [href_ l] (toHtml (item^.name))
|
|
|
|
(Nothing, HackageLibrary) ->
|
|
|
|
a_ [href_ hackageLink] (toHtml (item^.name))
|
|
|
|
_otherwise -> toHtml (item^.name)
|
2016-02-02 20:41:29 +03:00
|
|
|
|
2016-02-02 14:50:30 +03:00
|
|
|
-- Utils
|
|
|
|
|
2016-02-14 14:03:39 +03:00
|
|
|
includeJS :: Text -> Html ()
|
|
|
|
includeJS url = with (script_ "") [src_ url]
|
|
|
|
|
|
|
|
includeCSS :: Text -> Html ()
|
2016-02-14 14:22:47 +03:00
|
|
|
includeCSS url = link_ [rel_ "stylesheet", type_ "text/css", href_ url]
|
2016-02-02 19:29:23 +03:00
|
|
|
|
2016-02-14 15:19:36 +03:00
|
|
|
submitFunc :: Text -> Attribute
|
|
|
|
submitFunc f = onkeyup_ $ format
|
|
|
|
"if (event.keyCode == 13) {\
|
|
|
|
\ {}\
|
|
|
|
\ this.value = ''; }"
|
|
|
|
[f]
|
|
|
|
|
2016-02-19 22:12:23 +03:00
|
|
|
-- Javascript
|
|
|
|
|
2016-02-20 00:13:20 +03:00
|
|
|
js_this_value :: Text
|
|
|
|
js_this_value = "this.value"
|
2016-02-19 22:12:23 +03:00
|
|
|
|
|
|
|
class JSFunction a where
|
|
|
|
makeJSFunction
|
|
|
|
:: Text -- Name
|
|
|
|
-> Text -- Definition
|
|
|
|
-> a
|
|
|
|
|
|
|
|
-- This generates function name
|
|
|
|
instance JSFunction Text where
|
|
|
|
makeJSFunction fName _ = fName
|
|
|
|
|
|
|
|
-- This generates function definition and direct dependencies
|
|
|
|
instance JSFunction (Text, Text) where
|
|
|
|
makeJSFunction fName fDef = (fName, fDef)
|
|
|
|
|
|
|
|
-- This generates a function that takes arguments and produces a Javascript
|
|
|
|
-- function call
|
|
|
|
instance Format.Params a => JSFunction (a -> Text) where
|
|
|
|
makeJSFunction fName _ = \args -> do
|
|
|
|
let argsText = map (TL.toStrict . TL.toLazyText) (Format.buildParams args)
|
|
|
|
fName <> "(" <> T.intercalate "," argsText <> ");"
|
|
|
|
|
|
|
|
allJSFunctions :: JSFunction a => [a]
|
|
|
|
allJSFunctions = [
|
|
|
|
js_addLibrary, js_addCategory,
|
2016-02-19 22:34:51 +03:00
|
|
|
js_startCategoryHeadingEdit, js_submitCategoryHeadingEdit, js_cancelCategoryHeadingEdit,
|
2016-02-19 22:12:23 +03:00
|
|
|
js_addPros, js_addCons,
|
|
|
|
js_setItemHtml, js_setCategoryHeadingHtml ]
|
|
|
|
|
|
|
|
-- | Create a new category.
|
|
|
|
js_addCategory :: JSFunction a => a
|
|
|
|
js_addCategory = makeJSFunction "addCategory" [text|
|
|
|
|
function addCategory(s) {
|
2016-02-19 22:27:27 +03:00
|
|
|
$.post("/category/add", {title: s})
|
2016-02-19 22:12:23 +03:00
|
|
|
.done(function(data) {
|
|
|
|
$("#categories").append(data);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|]
|
|
|
|
|
2016-02-19 22:27:27 +03:00
|
|
|
-- | Add a new library to some category.
|
|
|
|
js_addLibrary :: JSFunction a => a
|
|
|
|
js_addLibrary = makeJSFunction "addLibrary" [text|
|
|
|
|
function addLibrary(catId, s) {
|
|
|
|
$.post("/category/"+catId+"/library/add", {name: s})
|
|
|
|
.done(function(data) {
|
|
|
|
$("#cat"+catId+" > .items").append(data);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|]
|
|
|
|
|
2016-02-19 22:12:23 +03:00
|
|
|
{- |
|
|
|
|
Start category heading editing (this happens when you click on “[edit]”).
|
|
|
|
|
|
|
|
This turns the heading into an editbox, and adds a [cancel] link.
|
|
|
|
-}
|
2016-02-19 22:34:51 +03:00
|
|
|
js_startCategoryHeadingEdit :: JSFunction a => a
|
|
|
|
js_startCategoryHeadingEdit = makeJSFunction "startCategoryHeadingEdit" [text|
|
|
|
|
function startCategoryHeadingEdit(catId) {
|
2016-02-19 22:27:27 +03:00
|
|
|
$.get("/category/"+catId+"/title/render-edit")
|
2016-02-19 22:12:23 +03:00
|
|
|
.done(function(data) {
|
|
|
|
setCategoryHeadingHtml(catId, data);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|]
|
|
|
|
|
|
|
|
{- |
|
2016-02-19 22:27:27 +03:00
|
|
|
Cancel category heading editing.
|
2016-02-19 22:12:23 +03:00
|
|
|
|
|
|
|
This turns the heading with the editbox back into a simple text heading.
|
|
|
|
-}
|
2016-02-19 22:34:51 +03:00
|
|
|
js_cancelCategoryHeadingEdit :: JSFunction a => a
|
|
|
|
js_cancelCategoryHeadingEdit = makeJSFunction "cancelCategoryHeadingEdit" [text|
|
|
|
|
function cancelCategoryHeadingEdit(catId) {
|
2016-02-19 22:27:27 +03:00
|
|
|
$.get("/category/"+catId+"/title/render-normal")
|
2016-02-19 22:12:23 +03:00
|
|
|
.done(function(data) {
|
|
|
|
setCategoryHeadingHtml(catId, data);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|]
|
|
|
|
|
|
|
|
{- |
|
2016-02-19 22:27:27 +03:00
|
|
|
Finish category heading editing (this happens when you submit the field).
|
2016-02-19 22:12:23 +03:00
|
|
|
|
|
|
|
This turns the heading with the editbox back into a simple text heading.
|
|
|
|
-}
|
2016-02-19 22:34:51 +03:00
|
|
|
js_submitCategoryHeadingEdit :: JSFunction a => a
|
|
|
|
js_submitCategoryHeadingEdit = makeJSFunction "submitCategoryHeadingEdit" [text|
|
|
|
|
function submitCategoryHeadingEdit(catId, s) {
|
2016-02-19 22:27:27 +03:00
|
|
|
$.post("/category/"+catId+"/title/set", {title: s})
|
2016-02-19 22:12:23 +03:00
|
|
|
.done(function(data) {
|
|
|
|
setCategoryHeadingHtml(catId, data);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|]
|
|
|
|
|
|
|
|
-- | Add pros to some item.
|
|
|
|
js_addPros :: JSFunction a => a
|
|
|
|
js_addPros = makeJSFunction "addPros" [text|
|
|
|
|
function addPros(itemId, s) {
|
2016-02-19 22:27:27 +03:00
|
|
|
$.post("/item/"+itemId+"/pros/add", {content: s})
|
2016-02-19 22:12:23 +03:00
|
|
|
.done(function(data) {
|
|
|
|
setItemHtml(itemId, data);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|]
|
|
|
|
|
|
|
|
-- | Add cons to some item.
|
|
|
|
js_addCons :: JSFunction a => a
|
|
|
|
js_addCons = makeJSFunction "addCons" [text|
|
|
|
|
function addCons(itemId, s) {
|
2016-02-19 22:27:27 +03:00
|
|
|
$.post("/item/"+itemId+"/cons/add", {content: s})
|
2016-02-19 22:12:23 +03:00
|
|
|
.done(function(data) {
|
|
|
|
setItemHtml(itemId, data);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|]
|
|
|
|
|
|
|
|
-- | Reload an item.
|
|
|
|
js_setItemHtml :: JSFunction a => a
|
|
|
|
js_setItemHtml = makeJSFunction "setItemHtml" [text|
|
|
|
|
function setItemHtml(itemId, data) {
|
|
|
|
$("#item"+itemId).replaceWith(data);
|
|
|
|
}
|
|
|
|
|]
|
|
|
|
|
|
|
|
-- | Reload a category heading.
|
|
|
|
js_setCategoryHeadingHtml :: JSFunction a => a
|
|
|
|
js_setCategoryHeadingHtml = makeJSFunction "setCategoryHeadingHtml" [text|
|
|
|
|
function setCategoryHeadingHtml(catId, data) {
|
|
|
|
$("#cat"+catId+" > h2").replaceWith(data);
|
|
|
|
}
|
|
|
|
|]
|
|
|
|
|
2016-02-17 19:43:35 +03:00
|
|
|
-- A text button looks like “[cancel]”
|
|
|
|
textButton
|
|
|
|
:: Text -- ^ Button text
|
|
|
|
-> Text -- ^ Onclick handler
|
|
|
|
-> Html ()
|
2016-02-20 00:09:49 +03:00
|
|
|
textButton caption handler =
|
|
|
|
span_ [class_ "textButton"] $
|
|
|
|
a_ [href_ "javascript:void(0)", onclick_ handler] (toHtml caption)
|
2016-02-17 19:43:35 +03:00
|
|
|
|
2016-02-02 14:50:30 +03:00
|
|
|
lucid :: Html a -> ActionT IO a
|
|
|
|
lucid = html . TL.toStrict . renderText
|
2016-02-02 19:29:23 +03:00
|
|
|
|
|
|
|
-- | Format a string (a bit 'Text.Printf.printf' but with different syntax).
|
2016-02-19 22:12:23 +03:00
|
|
|
format :: Format.Params ps => Format -> ps -> Text
|
2016-02-02 19:29:23 +03:00
|
|
|
format f ps = TL.toStrict (Format.format f ps)
|