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

680 lines
22 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 18:28:55 +03:00
import Text.HTML.SanitizeXSS (sanitaryURI)
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 16:12:45 +03:00
data Trait = Trait {
_traitUid :: Uid,
_traitContent :: Text }
2016-02-20 10:31:14 +03:00
2016-02-20 16:12:45 +03:00
makeFields ''Trait
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,
2016-02-20 16:12:45 +03:00
_itemPros :: [Trait],
_itemCons :: [Trait],
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 16:12:45 +03:00
traitById :: Uid -> Lens' Item Trait
traitById uid' = singular $
2016-02-20 11:22:46 +03:00
(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",
2016-02-20 16:12:45 +03:00
_itemPros = [Trait 121 "the standard lenses library",
Trait 122 "batteries included"],
_itemCons = [Trait 123 "huge"],
2016-02-20 11:27:16 +03:00
_itemLink = Nothing,
_itemKind = HackageLibrary }
let microlensItem = Item {
_itemUid = 13,
_itemName = "microlens",
2016-02-20 16:12:45 +03:00
_itemPros = [Trait 131 "very small",
Trait 132 "good for libraries"],
_itemCons = [Trait 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-20 16:12:45 +03:00
traitVar :: Path '[Uid]
traitVar = "trait" <//> 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
2016-02-20 16:12:45 +03:00
-- All item traits
Spock.get (itemVar <//> "traits") $ \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:12:45 +03:00
lucid $ renderItemTraits renderMode item
-- A single trait
Spock.get (itemVar <//> traitVar) $
\itemId traitId -> do
trait <- withGlobal $ use (itemById itemId . traitById traitId)
2016-02-20 14:18:51 +03:00
renderMode <- param' "mode"
2016-02-20 16:12:45 +03:00
lucid $ renderTrait renderMode itemId trait
2016-02-20 14:18:51 +03:00
-- 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
2016-02-20 18:28:55 +03:00
-- Item info
Spock.post (itemVar <//> "info") $ \itemId -> do
name' <- T.strip <$> param' "name"
link' <- T.strip <$> param' "link"
changedItem <- withGlobal $ do
-- TODO: actually validate the form and report errors
unless (T.null name') $
itemById itemId . name .= name'
case (T.null link', sanitiseUrl link') of
(True, _) -> itemById itemId . link .= Nothing
(_, Just l) -> itemById itemId . link .= Just l
_otherwise -> return ()
use (itemById itemId)
lucid $ renderItemInfo Normal changedItem
2016-02-20 16:12:45 +03:00
-- Trait
Spock.post (itemVar <//> traitVar) $
\itemId traitId -> do
2016-02-20 14:18:51 +03:00
content' <- param' "content"
2016-02-20 16:12:45 +03:00
changedTrait <- withGlobal $ do
itemById itemId . traitById traitId . content .= content'
use (itemById itemId . traitById traitId)
lucid $ renderTrait Editable itemId changedTrait
2016-02-20 14:18:51 +03:00
-- 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
2016-02-20 16:12:45 +03:00
let newTrait = Trait uid' content'
withGlobal $ itemById itemId . pros %= (++ [newTrait])
lucid $ renderTrait Editable itemId newTrait
2016-02-20 14:18:51 +03:00
-- Con (argument against a library)
Spock.post (itemVar <//> "con") $ \itemId -> do
content' <- param' "content"
uid' <- randomUid
2016-02-20 16:12:45 +03:00
let newTrait = Trait uid' content'
withGlobal $ itemById itemId . cons %= (++ [newTrait])
lucid $ renderTrait Editable itemId newTrait
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)
2016-02-20 18:28:55 +03:00
input_ [type_ "text", placeholder_ "new category", onInputSubmit 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 18:28:55 +03:00
input_ [type_ "text", value_ (category^.title), onInputSubmit handler]
2016-02-20 12:52:55 +03:00
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:12:45 +03:00
-- TODO: render descriptions and traits as Markdown
2016-02-20 16:03:08 +03:00
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 18:28:55 +03:00
input_ [type_ "text", value_ (category^.description), onInputSubmit handler]
2016-02-20 13:29:56 +03:00
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-20 18:28:55 +03:00
input_ [type_ "text", placeholder_ "new item", onInputSubmit 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
2016-02-20 16:12:45 +03:00
renderItemTraits Normal item
2016-02-20 16:03:08 +03:00
2016-02-20 18:28:55 +03:00
-- TODO: warn when a library isn't on Hackage but is supposed to be
-- TODO: give a link to oldest available docs when the new docs aren't there
2016-02-20 16:03:08 +03:00
renderItemInfo :: Editable -> Item -> HtmlT IO ()
renderItemInfo editable item =
2016-02-20 18:28:55 +03:00
div_ $ do
2016-02-20 16:03:08 +03:00
this <- thisNode
case editable of
2016-02-20 18:28:55 +03:00
Normal -> h3_ $ do
-- 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.
let hackageLink = "https://hackage.haskell.org/package/" <> item^.name
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 ()
textButton "edit" $
js_setItemInfoMode (this, item^.uid, Editable)
-- TODO: this should actually be InEdit
Editable -> do
let handler s = js_submitItemInfoEdit (this, item^.uid, s)
form_ [onFormSubmit handler] $ do
label_ $ do
"Package name: "
input_ [type_ "text", name_ "name",
value_ (item^.name)]
br_ []
label_ $ do
"Site (optional): "
input_ [type_ "text", name_ "link",
value_ (fromMaybe "" (item^.link))]
br_ []
input_ [type_ "submit", value_ "Submit"]
let cancelHandler = js_setItemInfoMode (this, item^.uid, Normal)
input_ [type_ "button", value_ "Cancel", onclick_ cancelHandler]
-- TODO: categories that don't directly compare libraries but just list all
-- libraries about something (e.g. Yesod plugins, or whatever)
2016-02-20 16:12:45 +03:00
renderItemTraits :: Editable -> Item -> HtmlT IO ()
renderItemTraits editable item =
div_ [class_ "traits"] $ do
2016-02-20 16:03:08 +03:00
this <- thisNode
case editable of
Normal -> textButton "edit" $
2016-02-20 16:12:45 +03:00
js_setItemTraitsMode (this, item^.uid, Editable)
2016-02-20 16:03:08 +03:00
Editable -> textButton "edit off" $
2016-02-20 16:12:45 +03:00
js_setItemTraitsMode (this, item^.uid, Normal)
div_ [class_ "traits-group"] $ do
2016-02-20 16:03:08 +03:00
p_ "Pros:"
case editable of
Normal ->
2016-02-20 16:12:45 +03:00
ul_ $ mapM_ (renderTrait Normal (item^.uid)) (item^.pros)
2016-02-20 16:03:08 +03:00
Editable -> do
listNode <- ul_ $ do
2016-02-20 16:12:45 +03:00
mapM_ (renderTrait Editable (item^.uid)) (item^.pros)
2016-02-20 16:03:08 +03:00
thisNode
let handler s = js_addPro (listNode, item^.uid, s)
2016-02-20 18:28:55 +03:00
input_ [type_ "text", placeholder_ "add pro", onInputSubmit handler]
2016-02-20 16:12:45 +03:00
div_ [class_ "traits-group"] $ do
2016-02-20 16:03:08 +03:00
p_ "Cons:"
case editable of
Normal ->
2016-02-20 16:12:45 +03:00
ul_ $ mapM_ (renderTrait Normal (item^.uid)) (item^.cons)
2016-02-20 16:03:08 +03:00
Editable -> do
listNode <- ul_ $ do
2016-02-20 16:12:45 +03:00
mapM_ (renderTrait Editable (item^.uid)) (item^.cons)
2016-02-20 16:03:08 +03:00
thisNode
let handler s = js_addCon (listNode, item^.uid, s)
2016-02-20 18:28:55 +03:00
input_ [type_ "text", placeholder_ "add con", onInputSubmit handler]
2016-02-20 16:03:08 +03:00
2016-02-20 16:12:45 +03:00
renderTrait :: Editable -> Uid -> Trait -> HtmlT IO ()
renderTrait Normal _ trait = li_ (toHtml (trait^.content))
renderTrait Editable itemId trait = li_ $ do
2016-02-20 10:31:14 +03:00
this <- thisNode
2016-02-20 16:12:45 +03:00
toHtml (trait^.content)
2016-02-20 10:31:14 +03:00
textButton "edit" $
2016-02-20 16:12:45 +03:00
js_setTraitMode (this, itemId, trait^.uid, InEdit)
renderTrait InEdit itemId trait = li_ $ do
2016-02-20 10:31:14 +03:00
this <- thisNode
2016-02-20 16:12:45 +03:00
let handler s = js_submitTraitEdit
(this, itemId, trait^.uid, s)
2016-02-20 18:28:55 +03:00
input_ [type_ "text", value_ (trait^.content), onInputSubmit handler]
2016-02-20 10:31:14 +03:00
textButton "cancel" $
2016-02-20 16:12:45 +03:00
js_setTraitMode (this, itemId, trait^.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.
2016-02-20 18:28:55 +03:00
onInputSubmit :: (JS -> JS) -> Attribute
onInputSubmit f = onkeyup_ $ format
2016-02-14 15:19:36 +03:00
"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-20 18:28:55 +03:00
onFormSubmit :: (JS -> JS) -> Attribute
onFormSubmit f = onsubmit_ $ format "{} return false;" [f "this"]
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
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,
2016-02-20 16:12:45 +03:00
js_setItemInfoMode, js_setItemTraitsMode,
js_setTraitMode,
2016-02-20 15:22:16 +03:00
-- Set methods
2016-02-20 18:28:55 +03:00
js_submitCategoryTitleEdit, js_submitCategoryDescriptionEdit,
2016-02-20 16:12:45 +03:00
js_submitTraitEdit,
2016-02-20 18:28:55 +03:00
js_submitItemInfoEdit ]
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));
}
|]
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));
}
|]
2016-02-20 16:12:45 +03:00
js_setItemTraitsMode :: JSFunction a => a
js_setItemTraitsMode = makeJSFunction "setItemTraitsMode" [text|
function setItemTraitsMode(node, itemId, mode) {
$.get("/render/item/"+itemId+"/traits", {mode: mode})
2016-02-20 10:51:36 +03:00
.done(replaceWithData(node));
2016-02-20 02:28:18 +03:00
}
|]
2016-02-20 16:12:45 +03:00
js_setTraitMode :: JSFunction a => a
js_setTraitMode = makeJSFunction "setTraitMode" [text|
function setTraitMode(node, itemId, traitId, mode) {
$.get("/render/item/"+itemId+"/trait/"+traitId, {mode: mode})
2016-02-20 10:51:36 +03:00
.done(replaceWithData(node));
2016-02-20 10:31:14 +03:00
}
|]
2016-02-20 16:12:45 +03:00
js_submitTraitEdit :: JSFunction a => a
js_submitTraitEdit = makeJSFunction "submitTraitEdit" [text|
function submitTraitEdit(node, itemId, traitId, s) {
$.post("/set/item/"+itemId+"/trait/"+traitId, {content: s})
2016-02-20 10:51:36 +03:00
.done(replaceWithData(node));
2016-02-20 10:31:14 +03:00
}
|]
2016-02-20 18:28:55 +03:00
js_submitItemInfoEdit :: JSFunction a => a
js_submitItemInfoEdit = makeJSFunction "submitItemInfoEdit" [text|
function submitItemInfoEdit(node, itemId, form) {
$.post("/set/item/"+itemId+"/info", $(form).serialize())
.done(replaceWithData(node));
}
|]
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?
2016-02-20 18:28:55 +03:00
sanitiseUrl :: Url -> Maybe Url
sanitiseUrl u
| not (sanitaryURI u) = Nothing
| "http:" `T.isPrefixOf` u = Just u
| "https:" `T.isPrefixOf` u = Just u
| otherwise = Just ("http://" <> u)