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,
|
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,
|
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-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-20 01:01:14 +03:00
|
|
|
|
-- Randomness
|
|
|
|
|
import System.Random
|
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-20 12:52:55 +03:00
|
|
|
|
import Web.PathPieces
|
2016-02-02 14:50:30 +03:00
|
|
|
|
|
|
|
|
|
|
2016-02-20 15:34:29 +03:00
|
|
|
|
type Url = Text
|
|
|
|
|
|
2016-02-20 01:28:49 +03:00
|
|
|
|
-- | 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 01:28:49 +03:00
|
|
|
|
|
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-02 20:41:29 +03:00
|
|
|
|
|
2016-02-20 11:22:46 +03:00
|
|
|
|
makeFields ''Item
|
2016-02-02 20:41:29 +03:00
|
|
|
|
|
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
|
|
|
|
|
2016-02-02 19:29:23 +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-02 19:29:23 +03:00
|
|
|
|
|
2016-02-20 11:22:46 +03:00
|
|
|
|
makeFields ''Category
|
2016-02-02 19:29:23 +03:00
|
|
|
|
|
2016-02-20 14:25:16 +03:00
|
|
|
|
data GlobalState = GlobalState {
|
2016-02-02 19:29:23 +03:00
|
|
|
|
_categories :: [Category] }
|
|
|
|
|
|
2016-02-20 14:25:16 +03:00
|
|
|
|
makeLenses ''GlobalState
|
2016-02-02 19:29:23 +03:00
|
|
|
|
|
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 ()
|
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-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
|
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-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-02 14:50:30 +03:00
|
|
|
|
|
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 =
|
2016-02-20 15:38:29 +03:00
|
|
|
|
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-02 19:41:28 +03:00
|
|
|
|
|
2016-02-20 16:03:08 +03:00
|
|
|
|
renderItem :: Item -> HtmlT IO ()
|
|
|
|
|
renderItem item =
|
2016-02-20 15:38:29 +03:00
|
|
|
|
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)
|
2016-02-02 20:41:29 +03:00
|
|
|
|
where
|
2016-02-20 15:02:59 +03:00
|
|
|
|
hackageLink = "https://hackage.haskell.org/package/" <> item^.name
|
2016-02-02 20:41:29 +03:00
|
|
|
|
|
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
|
|
|
|
|
2016-02-02 14:50:30 +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-02 19:29:23 +03:00
|
|
|
|
|
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
|
2016-02-20 15:13:46 +03:00
|
|
|
|
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)
|
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)
|
2016-02-20 01:28:49 +03:00
|
|
|
|
|
|
|
|
|
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"
|
2016-02-20 15:13:46 +03:00
|
|
|
|
|
|
|
|
|
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?
|