1
1
mirror of https://github.com/aelve/guide.git synced 2024-12-24 21:35:06 +03:00
guide/src/Main.hs

632 lines
20 KiB
Haskell
Raw Normal View History

{-# LANGUAGE
OverloadedStrings,
TemplateHaskell,
RecordWildCards,
RankNTypes,
2016-02-19 22:12:23 +03:00
FlexibleInstances,
2016-02-20 13:39:16 +03:00
FlexibleContexts,
2016-02-19 22:12:23 +03:00
QuasiQuotes,
2016-02-20 01:01:14 +03:00
ScopedTypeVariables,
2016-02-20 11:22:46 +03:00
MultiParamTypeClasses,
FunctionalDependencies,
2016-02-20 14:18:51 +03:00
DataKinds,
NoImplicitPrelude
#-}
2016-02-02 12:35:39 +03:00
module Main (main) where
-- General
import BasePrelude hiding (Category)
2016-02-14 15:19:36 +03:00
-- Monads and monad transformers
import Control.Monad.State
-- Lenses
import Lens.Micro.Platform
-- Text
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Lazy as TL
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-20 01:01:14 +03:00
-- Randomness
import System.Random
-- Web
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
import Network.Wai.Middleware.Static
2016-02-20 12:52:55 +03:00
import Web.PathPieces
2016-02-20 15:34:29 +03:00
type Url = Text
-- | Unique id, used for many things categories, items, and anchor ids.
-- Note that in HTML 5 using numeric ids for divs, spans, etc is okay.
2016-02-20 11:22:46 +03:00
type Uid = Int
2016-02-14 15:19:36 +03:00
2016-02-20 11:22:46 +03:00
randomUid :: MonadIO m => m Uid
randomUid = liftIO $ randomRIO (0, 10^(9::Int))
2016-02-20 10:31:14 +03:00
data ProCon = ProCon {
2016-02-20 11:22:46 +03:00
_proConUid :: Uid,
_proConContent :: Text }
2016-02-20 10:31:14 +03:00
2016-02-20 11:22:46 +03:00
makeFields ''ProCon
2016-02-20 10:31:14 +03:00
2016-02-14 14:03:39 +03:00
data ItemKind = HackageLibrary | Library | Unknown
data Item = Item {
2016-02-20 11:22:46 +03:00
_itemUid :: Uid,
_itemName :: Text,
_itemPros :: [ProCon],
_itemCons :: [ProCon],
2016-02-20 15:34:29 +03:00
_itemLink :: Maybe Url,
2016-02-20 11:22:46 +03:00
_itemKind :: ItemKind }
2016-02-20 11:22:46 +03:00
makeFields ''Item
2016-02-20 11:22:46 +03:00
proConById :: Uid -> Lens' Item ProCon
proConById uid' = singular $
(pros.each . filtered ((== uid') . view uid)) `failing`
(cons.each . filtered ((== uid') . view uid))
2016-02-20 10:31:14 +03:00
data Category = Category {
2016-02-20 11:22:46 +03:00
_categoryUid :: Uid,
_categoryTitle :: Text,
2016-02-20 13:29:56 +03:00
_categoryDescription :: Text,
2016-02-20 11:22:46 +03:00
_categoryItems :: [Item] }
2016-02-20 11:22:46 +03:00
makeFields ''Category
2016-02-20 14:25:16 +03:00
data GlobalState = GlobalState {
_categories :: [Category] }
2016-02-20 14:25:16 +03:00
makeLenses ''GlobalState
2016-02-20 14:25:16 +03:00
categoryById :: Uid -> Lens' GlobalState Category
2016-02-20 11:22:46 +03:00
categoryById uid' = singular $
categories.each . filtered ((== uid') . view uid)
2016-02-14 15:19:36 +03:00
2016-02-20 14:25:16 +03:00
itemById :: Uid -> Lens' GlobalState Item
2016-02-20 11:22:46 +03:00
itemById uid' = singular $
categories.each . items.each . filtered ((== uid') . view uid)
2016-02-14 14:03:39 +03:00
2016-02-20 14:25:16 +03:00
emptyState :: GlobalState
emptyState = GlobalState {
2016-02-14 14:10:54 +03:00
_categories = [] }
2016-02-20 14:25:16 +03:00
sampleState :: GlobalState
2016-02-20 11:27:16 +03:00
sampleState = do
let lensItem = Item {
_itemUid = 12,
_itemName = "lens",
_itemPros = [ProCon 121 "the standard lenses library",
ProCon 122 "batteries included"],
_itemCons = [ProCon 123 "huge"],
_itemLink = Nothing,
_itemKind = HackageLibrary }
let microlensItem = Item {
_itemUid = 13,
_itemName = "microlens",
_itemPros = [ProCon 131 "very small",
ProCon 132 "good for libraries"],
_itemCons = [ProCon 133 "doesn't have advanced features"],
2016-02-20 15:02:59 +03:00
_itemLink = Just "https://github.com/aelve/microlens",
2016-02-20 11:27:16 +03:00
_itemKind = HackageLibrary }
let lensesCategory = Category {
_categoryUid = 1,
_categoryTitle = "lenses",
2016-02-20 13:29:56 +03:00
_categoryDescription = "Lenses are first-class composable accessors.",
2016-02-20 11:27:16 +03:00
_categoryItems = [lensItem, microlensItem] }
2016-02-20 14:25:16 +03:00
GlobalState {_categories = [lensesCategory]}
2016-02-14 14:10:54 +03:00
2016-02-20 14:18:51 +03:00
itemVar :: Path '[Uid]
itemVar = "item" <//> var
categoryVar :: Path '[Uid]
categoryVar = "category" <//> var
2016-02-02 12:35:39 +03:00
main :: IO ()
main = runSpock 8080 $ spockT id $ do
middleware (staticPolicy (addBase "static"))
2016-02-14 14:10:54 +03:00
stateVar <- liftIO $ newIORef sampleState
2016-02-20 14:25:16 +03:00
let withGlobal :: MonadIO m => State GlobalState a -> m a
withGlobal f = liftIO $ atomicModifyIORef' stateVar (swap . runState f)
2016-02-14 14:19:32 +03:00
2016-02-20 14:18:51 +03:00
-- Main page
2016-02-14 15:19:36 +03:00
Spock.get root $ do
s <- liftIO $ readIORef stateVar
lucid $ renderRoot s
2016-02-14 14:19:32 +03:00
2016-02-20 14:18:51 +03:00
-- Render methods
Spock.subcomponent "render" $ do
-- Title of a category
Spock.get (categoryVar <//> "title") $ \catId -> do
2016-02-20 14:25:16 +03:00
category <- withGlobal $ use (categoryById catId)
2016-02-20 14:18:51 +03:00
renderMode <- param' "mode"
lucid $ renderCategoryTitle renderMode category
-- Description of a category
Spock.get (categoryVar <//> "description") $ \catId -> do
2016-02-20 14:25:16 +03:00
category <- withGlobal $ use (categoryById catId)
2016-02-20 14:18:51 +03:00
renderMode <- param' "mode"
lucid $ renderCategoryDescription renderMode category
-- Item
Spock.get itemVar $ \itemId -> do
2016-02-20 16:03:08 +03:00
item <- withGlobal $ use (itemById itemId)
lucid $ renderItem item
-- Item info
Spock.get (itemVar <//> "info") $ \itemId -> do
item <- withGlobal $ use (itemById itemId)
renderMode <- param' "mode"
lucid $ renderItemInfo renderMode item
-- Item pros-cons
Spock.get (itemVar <//> "pros-cons") $ \itemId -> do
2016-02-20 14:25:16 +03:00
item <- withGlobal $ use (itemById itemId)
2016-02-20 14:18:51 +03:00
renderMode <- param' "mode"
2016-02-20 16:03:08 +03:00
lucid $ renderItemProsCons renderMode item
2016-02-20 14:18:51 +03:00
-- Pro/con
Spock.get (itemVar <//> "pro-con" <//> var) $
\itemId thingId -> do
2016-02-20 14:25:16 +03:00
thing <- withGlobal $ use (itemById itemId . proConById thingId)
2016-02-20 14:18:51 +03:00
renderMode <- param' "mode"
lucid $ renderProCon renderMode itemId thing
-- The add/set methods return rendered parts of the structure (added
2016-02-14 15:19:36 +03:00
-- 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-20 14:18:51 +03:00
-- Set methods
Spock.subcomponent "set" $ do
-- Title of a category
Spock.post (categoryVar <//> "title") $ \catId -> do
content' <- param' "content"
2016-02-20 14:25:16 +03:00
changedCategory <- withGlobal $ do
2016-02-20 14:18:51 +03:00
categoryById catId . title .= content'
use (categoryById catId)
lucid $ renderCategoryTitle Editable changedCategory
-- Description of a category
Spock.post (categoryVar <//> "description") $ \catId -> do
content' <- param' "content"
2016-02-20 14:25:16 +03:00
changedCategory <- withGlobal $ do
2016-02-20 14:18:51 +03:00
categoryById catId . description .= content'
use (categoryById catId)
lucid $ renderCategoryDescription Editable changedCategory
-- Pro/con
Spock.post (itemVar <//> "pro-con" <//> var) $
\itemId thingId -> do
content' <- param' "content"
2016-02-20 14:25:16 +03:00
changedThing <- withGlobal $ do
2016-02-20 14:18:51 +03:00
itemById itemId . proConById thingId . content .= content'
use (itemById itemId . proConById thingId)
lucid $ renderProCon Editable itemId changedThing
-- Add methods
Spock.subcomponent "add" $ do
-- New category
Spock.post "category" $ do
2016-02-20 15:24:22 +03:00
content' <- param' "content"
2016-02-20 14:18:51 +03:00
uid' <- randomUid
let newCategory = Category {
_categoryUid = uid',
2016-02-20 15:24:22 +03:00
_categoryTitle = content',
2016-02-20 14:18:51 +03:00
_categoryDescription = "<write a description here>",
_categoryItems = [] }
2016-02-20 14:25:16 +03:00
withGlobal $ categories %= (++ [newCategory])
2016-02-20 14:18:51 +03:00
lucid $ renderCategory newCategory
-- New library in a category
Spock.post (categoryVar <//> "library") $ \catId -> do
name' <- param' "name"
uid' <- randomUid
let newItem = Item {
_itemUid = uid',
_itemName = name',
_itemPros = [],
_itemCons = [],
_itemLink = Nothing,
_itemKind = HackageLibrary }
-- TODO: maybe do something if the category doesn't exist (e.g. has been
-- already deleted)
2016-02-20 14:25:16 +03:00
withGlobal $ categoryById catId . items %= (++ [newItem])
2016-02-20 16:03:08 +03:00
lucid $ renderItem newItem
2016-02-20 14:18:51 +03:00
-- Pro (argument in favor of a library)
Spock.post (itemVar <//> "pro") $ \itemId -> do
content' <- param' "content"
uid' <- randomUid
let newThing = ProCon uid' content'
2016-02-20 14:25:16 +03:00
withGlobal $ itemById itemId . pros %= (++ [newThing])
2016-02-20 14:18:51 +03:00
lucid $ renderProCon Editable itemId newThing
-- Con (argument against a library)
Spock.post (itemVar <//> "con") $ \itemId -> do
content' <- param' "content"
uid' <- randomUid
let newThing = ProCon uid' content'
2016-02-20 14:25:16 +03:00
withGlobal $ itemById itemId . cons %= (++ [newThing])
2016-02-20 14:18:51 +03:00
lucid $ renderProCon Editable itemId newThing
2016-02-20 10:31:14 +03:00
2016-02-20 14:25:16 +03:00
renderRoot :: GlobalState -> HtmlT IO ()
2016-02-20 13:39:16 +03:00
renderRoot globalState = 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-20 14:38:44 +03:00
script_ $ T.unlines (map snd (allJSFunctions :: [(Text, JS)]))
2016-02-20 01:51:33 +03:00
categoriesNode <- div_ [id_ "categories"] $ do
2016-02-20 13:39:16 +03:00
mapM_ renderCategory (globalState ^. categories)
2016-02-20 01:51:33 +03:00
thisNode
2016-02-20 13:39:16 +03:00
let handler s = js_addCategory (categoriesNode, s)
input_ [type_ "text", placeholder_ "new category", submitFunc handler]
2016-02-20 13:29:56 +03:00
renderCategoryTitle :: Editable -> Category -> HtmlT IO ()
renderCategoryTitle editable category =
2016-02-20 15:47:24 +03:00
h2_ [id_ (tshow (category^.uid))] $ do
a_ [class_ "anchor", href_ ("#" <> tshow (category^.uid))] "#"
2016-02-20 13:29:56 +03:00
titleNode <- thisNode
2016-02-20 12:52:55 +03:00
case editable of
Editable -> do
toHtml (category^.title)
textButton "edit" $
2016-02-20 15:22:16 +03:00
js_setCategoryTitleMode (titleNode, category^.uid, InEdit)
2016-02-20 12:52:55 +03:00
InEdit -> do
2016-02-20 13:39:16 +03:00
let handler s = js_submitCategoryTitleEdit
(titleNode, category^.uid, s)
2016-02-20 12:52:55 +03:00
input_ [type_ "text", value_ (category^.title), submitFunc handler]
textButton "cancel" $
2016-02-20 15:22:16 +03:00
js_setCategoryTitleMode (titleNode, category^.uid, Editable)
2016-02-20 13:29:56 +03:00
2016-02-20 16:03:08 +03:00
-- TODO: render descriptions and pros/cons as Markdown
2016-02-20 13:29:56 +03:00
renderCategoryDescription :: Editable -> Category -> HtmlT IO ()
renderCategoryDescription editable category =
p_ $ do
descrNode <- thisNode
case editable of
Editable -> do
toHtml (category^.description)
textButton "edit" $
2016-02-20 15:22:16 +03:00
js_setCategoryDescriptionMode (descrNode, category^.uid, InEdit)
2016-02-20 13:29:56 +03:00
InEdit -> do
2016-02-20 13:39:16 +03:00
let handler s = js_submitCategoryDescriptionEdit
2016-02-20 14:38:44 +03:00
(descrNode, category^.uid, s)
2016-02-20 13:29:56 +03:00
input_ [type_ "text", value_ (category^.description), submitFunc handler]
textButton "cancel" $
2016-02-20 15:22:16 +03:00
js_setCategoryDescriptionMode (descrNode, category^.uid, Editable)
2016-02-17 19:43:35 +03:00
2016-02-20 01:01:14 +03:00
renderCategory :: Category -> HtmlT IO ()
2016-02-14 14:03:39 +03:00
renderCategory category =
div_ $ do
2016-02-20 13:29:56 +03:00
renderCategoryTitle Editable category
renderCategoryDescription Editable category
2016-02-20 01:51:33 +03:00
itemsNode <- div_ [class_ "items"] $ do
2016-02-20 16:03:08 +03:00
mapM_ renderItem (category^.items)
2016-02-20 01:51:33 +03:00
thisNode
2016-02-20 13:39:16 +03:00
let handler s = js_addLibrary (itemsNode, category^.uid, s)
2016-02-14 15:19:36 +03:00
input_ [type_ "text", placeholder_ "new item", submitFunc handler]
2016-02-20 16:03:08 +03:00
renderItem :: Item -> HtmlT IO ()
renderItem item =
div_ [class_ "item"] $ do
2016-02-20 16:03:08 +03:00
renderItemInfo Normal item
renderItemProsCons Normal item
renderItemInfo :: Editable -> Item -> HtmlT IO ()
renderItemInfo editable item =
h3_ $ do
this <- thisNode
-- If the library is on Hackage, the title links to its Hackage page;
-- otherwise, it doesn't link anywhere. Even if the link field is
-- present, it's going to be rendered as “(site)”, not linked in the
-- title.
case item^.kind of
HackageLibrary -> a_ [href_ hackageLink] (toHtml (item^.name))
_otherwise -> toHtml (item^.name)
case item^.link of
Just l -> " (" >> a_ [href_ l] "site" >> ")"
Nothing -> return ()
case editable of
Normal -> textButton "edit" $
js_setItemInfoMode (this, item^.uid, Editable)
-- TODO: change to an actual button, “Submit”, etc.
Editable -> textButton "edit off" $
js_setItemInfoMode (this, item^.uid, Normal)
where
2016-02-20 15:02:59 +03:00
hackageLink = "https://hackage.haskell.org/package/" <> item^.name
2016-02-20 16:03:08 +03:00
renderItemProsCons :: Editable -> Item -> HtmlT IO ()
renderItemProsCons editable item =
div_ [class_ "pros-cons"] $ do
this <- thisNode
case editable of
Normal -> textButton "edit" $
js_setItemProsConsMode (this, item^.uid, Editable)
Editable -> textButton "edit off" $
js_setItemProsConsMode (this, item^.uid, Normal)
div_ [class_ "pros"] $ do
p_ "Pros:"
case editable of
Normal ->
ul_ $ mapM_ (renderProCon Normal (item^.uid)) (item^.pros)
Editable -> do
listNode <- ul_ $ do
mapM_ (renderProCon Editable (item^.uid)) (item^.pros)
thisNode
let handler s = js_addPro (listNode, item^.uid, s)
input_ [type_ "text", placeholder_ "add pro", submitFunc handler]
div_ [class_ "cons"] $ do
p_ "Cons:"
case editable of
Normal ->
ul_ $ mapM_ (renderProCon Normal (item^.uid)) (item^.cons)
Editable -> do
listNode <- ul_ $ do
mapM_ (renderProCon Editable (item^.uid)) (item^.cons)
thisNode
let handler s = js_addCon (listNode, item^.uid, s)
input_ [type_ "text", placeholder_ "add con", submitFunc handler]
2016-02-20 11:22:46 +03:00
renderProCon :: Editable -> Uid -> ProCon -> HtmlT IO ()
renderProCon Normal _ proCon = li_ (toHtml (proCon^.content))
renderProCon Editable itemId proCon = li_ $ do
2016-02-20 10:31:14 +03:00
this <- thisNode
2016-02-20 11:22:46 +03:00
toHtml (proCon^.content)
2016-02-20 10:31:14 +03:00
textButton "edit" $
2016-02-20 15:22:16 +03:00
js_setProConMode (this, itemId, proCon^.uid, InEdit)
2016-02-20 11:22:46 +03:00
renderProCon InEdit itemId thing = li_ $ do
2016-02-20 10:31:14 +03:00
this <- thisNode
2016-02-20 13:39:16 +03:00
let handler s = js_submitProConEdit
(this, itemId, thing^.uid, s)
2016-02-20 11:22:46 +03:00
input_ [type_ "text", value_ (thing^.content), submitFunc handler]
2016-02-20 10:31:14 +03:00
textButton "cancel" $
2016-02-20 15:22:16 +03:00
js_setProConMode (this, itemId, thing^.uid, Editable)
2016-02-20 10:31:14 +03:00
-- Utils
2016-02-20 15:34:29 +03:00
includeJS :: Monad m => Url -> HtmlT m ()
2016-02-14 14:03:39 +03:00
includeJS url = with (script_ "") [src_ url]
2016-02-20 15:34:29 +03:00
includeCSS :: Monad m => Url -> HtmlT m ()
2016-02-14 14:22:47 +03:00
includeCSS url = link_ [rel_ "stylesheet", type_ "text/css", href_ url]
2016-02-20 13:39:16 +03:00
-- The function is passed a JS expression that refers to text being submitted.
submitFunc :: (JS -> JS) -> Attribute
2016-02-14 15:19:36 +03:00
submitFunc f = onkeyup_ $ format
"if (event.keyCode == 13) {\
\ {}\
\ this.value = ''; }"
2016-02-20 13:39:16 +03:00
[f "this.value"]
2016-02-14 15:19:36 +03:00
2016-02-19 22:12:23 +03:00
-- Javascript
2016-02-20 02:28:18 +03:00
-- TODO: try to make them more type-safe somehow?
2016-02-19 22:12:23 +03:00
class JSFunction a where
makeJSFunction
:: Text -- Name
2016-02-20 02:30:04 +03:00
-> JS -- Definition
2016-02-19 22:12:23 +03:00
-> a
-- This generates function name
instance JSFunction Text where
makeJSFunction fName _ = fName
-- This generates function definition and direct dependencies
2016-02-20 02:30:04 +03:00
instance JSFunction (Text, JS) where
2016-02-19 22:12:23 +03:00
makeJSFunction fName fDef = (fName, fDef)
-- This generates a function that takes arguments and produces a Javascript
-- function call
instance JSParams a => JSFunction (a -> JS) where
makeJSFunction fName _ = \args ->
fName <> "(" <> T.intercalate "," (jsParams args) <> ");"
2016-02-19 22:12:23 +03:00
2016-02-20 16:03:08 +03:00
-- TODO: rename pros/cons to traits
2016-02-19 22:12:23 +03:00
allJSFunctions :: JSFunction a => [a]
allJSFunctions = [
2016-02-20 15:22:16 +03:00
-- Utilities
2016-02-20 10:51:36 +03:00
js_replaceWithData, js_appendData,
2016-02-20 15:22:16 +03:00
-- Add methods
2016-02-19 22:12:23 +03:00
js_addLibrary, js_addCategory,
2016-02-20 10:39:15 +03:00
js_addPro, js_addCon,
2016-02-20 15:22:16 +03:00
-- Render-as-editable methods
2016-02-20 16:03:08 +03:00
js_setCategoryTitleMode, js_setCategoryDescriptionMode,
js_setItemInfoMode, js_setItemProsConsMode,
2016-02-20 15:22:16 +03:00
js_setProConMode,
-- Set methods
js_submitCategoryTitleEdit,
js_submitProConEdit,
js_submitCategoryDescriptionEdit ]
2016-02-19 22:12:23 +03:00
2016-02-20 10:51:36 +03:00
js_replaceWithData :: JSFunction a => a
js_replaceWithData = makeJSFunction "replaceWithData" [text|
function replaceWithData(node) {
return function(data) {$(node).replaceWith(data); }; }
|]
js_appendData :: JSFunction a => a
js_appendData = makeJSFunction "appendData" [text|
function appendData(node) {
return function(data) {$(node).append(data); }; }
|]
2016-02-19 22:12:23 +03:00
-- | Create a new category.
js_addCategory :: JSFunction a => a
js_addCategory = makeJSFunction "addCategory" [text|
2016-02-20 01:51:33 +03:00
function addCategory(node, s) {
2016-02-20 15:24:22 +03:00
$.post("/add/category", {content: s})
2016-02-20 10:51:36 +03:00
.done(appendData(node));
2016-02-19 22:12:23 +03:00
}
|]
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|
2016-02-20 01:51:33 +03:00
function addLibrary(node, catId, s) {
2016-02-20 14:18:51 +03:00
$.post("/add/category/"+catId+"/library", {name: s})
2016-02-20 10:51:36 +03:00
.done(appendData(node));
2016-02-19 22:27:27 +03:00
}
|]
2016-02-20 15:22:16 +03:00
js_setCategoryTitleMode :: JSFunction a => a
js_setCategoryTitleMode = makeJSFunction "setCategoryTitleMode" [text|
function setCategoryTitleMode(node, catId, mode) {
$.get("/render/category/"+catId+"/title", {mode: mode})
2016-02-20 10:51:36 +03:00
.done(replaceWithData(node));
2016-02-19 22:12:23 +03:00
}
|]
{- |
2016-02-20 13:29:56 +03:00
Finish category title editing (this happens when you submit the field).
This turns the title with the editbox back into a simple text title.
-}
js_submitCategoryTitleEdit :: JSFunction a => a
js_submitCategoryTitleEdit = makeJSFunction "submitCategoryTitleEdit" [text|
function submitCategoryTitleEdit(node, catId, s) {
2016-02-20 14:18:51 +03:00
$.post("/set/category/"+catId+"/title", {content: s})
2016-02-20 13:29:56 +03:00
.done(replaceWithData(node));
}
|]
2016-02-20 15:22:16 +03:00
js_setCategoryDescriptionMode :: JSFunction a => a
js_setCategoryDescriptionMode = makeJSFunction "setCategoryDescriptionMode" [text|
function setCategoryDescriptionMode(node, catId, mode) {
$.get("/render/category/"+catId+"/description", {mode: mode})
2016-02-20 13:29:56 +03:00
.done(replaceWithData(node));
}
|]
{- |
Finish category description editing (this happens when you submit the field).
2016-02-19 22:12:23 +03:00
2016-02-20 13:29:56 +03:00
This turns the description with the editbox back into a simple text description.
2016-02-19 22:12:23 +03:00
-}
2016-02-20 13:29:56 +03:00
js_submitCategoryDescriptionEdit :: JSFunction a => a
js_submitCategoryDescriptionEdit = makeJSFunction "submitCategoryDescriptionEdit" [text|
function submitCategoryDescriptionEdit(node, catId, s) {
2016-02-20 14:18:51 +03:00
$.post("/set/category/"+catId+"/description", {content: s})
2016-02-20 10:51:36 +03:00
.done(replaceWithData(node));
2016-02-19 22:12:23 +03:00
}
|]
2016-02-20 10:39:15 +03:00
-- | Add a pro to some item.
js_addPro :: JSFunction a => a
js_addPro = makeJSFunction "addPro" [text|
function addPro(node, itemId, s) {
2016-02-20 14:18:51 +03:00
$.post("/add/item/"+itemId+"/pro", {content: s})
2016-02-20 10:51:36 +03:00
.done(appendData(node));
2016-02-19 22:12:23 +03:00
}
|]
2016-02-20 10:39:15 +03:00
-- | Add a con to some item.
js_addCon :: JSFunction a => a
js_addCon = makeJSFunction "addCon" [text|
function addCon(node, itemId, s) {
2016-02-20 14:18:51 +03:00
$.post("/add/item/"+itemId+"/con", {content: s})
2016-02-20 10:51:36 +03:00
.done(appendData(node));
2016-02-20 02:28:18 +03:00
}
|]
2016-02-20 16:03:08 +03:00
js_setItemInfoMode :: JSFunction a => a
js_setItemInfoMode = makeJSFunction "setItemInfoMode" [text|
function setItemInfoMode(node, itemId, mode) {
$.get("/render/item/"+itemId+"/info", {mode: mode})
.done(replaceWithData(node));
}
|]
js_setItemProsConsMode :: JSFunction a => a
js_setItemProsConsMode = makeJSFunction "setItemProsConsMode" [text|
function setItemProsConsMode(node, itemId, mode) {
$.get("/render/item/"+itemId+"/pros-cons", {mode: mode})
2016-02-20 10:51:36 +03:00
.done(replaceWithData(node));
2016-02-20 02:28:18 +03:00
}
|]
2016-02-20 15:22:16 +03:00
js_setProConMode :: JSFunction a => a
js_setProConMode = makeJSFunction "setProConMode" [text|
function setProConMode(node, itemId, thingId, mode) {
$.get("/render/item/"+itemId+"/pro-con/"+thingId, {mode: mode})
2016-02-20 10:51:36 +03:00
.done(replaceWithData(node));
2016-02-20 10:31:14 +03:00
}
|]
js_submitProConEdit :: JSFunction a => a
js_submitProConEdit = makeJSFunction "submitProConEdit" [text|
function submitProConEdit(node, itemId, thingId, s) {
2016-02-20 14:18:51 +03:00
$.post("/set/item/"+itemId+"/pro-con/"+thingId, {content: s})
2016-02-20 10:51:36 +03:00
.done(replaceWithData(node));
2016-02-20 10:31:14 +03:00
}
|]
2016-02-20 02:28:18 +03:00
-- When adding a function, don't forget to add it to 'allJSFunctions'!
2016-02-20 02:30:04 +03:00
type JS = Text
2016-02-20 02:28:18 +03:00
2016-02-17 19:43:35 +03:00
-- A text button looks like “[cancel]”
textButton
2016-02-20 01:01:14 +03:00
:: Text -- ^ Button text
2016-02-20 02:30:04 +03:00
-> JS -- ^ Onclick handler
2016-02-20 01:01:14 +03:00
-> HtmlT IO ()
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-20 01:01:14 +03:00
type JQuerySelector = Text
thisNode :: HtmlT IO JQuerySelector
thisNode = do
2016-02-20 11:22:46 +03:00
uid' <- randomUid
span_ [id_ (tshow uid')] mempty
return (T.pack (show (format ":has(> #{})" [uid'])))
2016-02-20 01:01:14 +03:00
lucid :: HtmlT IO a -> ActionT IO a
lucid h = do
htmlText <- liftIO (renderTextT h)
html (TL.toStrict htmlText)
-- | 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
format f ps = TL.toStrict (Format.format f ps)
tshow :: Show a => a -> Text
tshow = T.pack . show
2016-02-20 02:28:18 +03:00
2016-02-20 10:31:14 +03:00
data Editable = Normal | Editable | InEdit
2016-02-20 12:52:55 +03:00
instance PathPiece Editable where
fromPathPiece "normal" = Just Normal
fromPathPiece "editable" = Just Editable
fromPathPiece "in-edit" = Just InEdit
fromPathPiece _ = Nothing
toPathPiece Normal = "normal"
toPathPiece Editable = "editable"
toPathPiece InEdit = "in-edit"
class LiftJS a where liftJS :: a -> Text
instance LiftJS Text where liftJS = id
instance LiftJS Integer where liftJS = tshow
instance LiftJS Int where liftJS = tshow
instance LiftJS Editable where liftJS = tshow . toPathPiece
class JSParams a where
jsParams :: a -> [Text]
instance JSParams () where
jsParams () = []
instance LiftJS a => JSParams [a] where
jsParams = map liftJS
instance (LiftJS a, LiftJS b) => JSParams (a,b) where
jsParams (a,b) = [liftJS a, liftJS b]
instance (LiftJS a, LiftJS b, LiftJS c) => JSParams (a,b,c) where
jsParams (a,b,c) = [liftJS a, liftJS b, liftJS c]
instance (LiftJS a, LiftJS b, LiftJS c, LiftJS d) => JSParams (a,b,c,d) where
jsParams (a,b,c,d) = [liftJS a, liftJS b, liftJS c, liftJS d]
2016-02-20 15:24:22 +03:00
-- TODO: why not compare Haskellers too?