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-20 01:01:14 +03:00
|
|
|
|
ScopedTypeVariables,
|
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-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-02 14:50:30 +03:00
|
|
|
|
|
|
|
|
|
|
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-14 15:19:36 +03:00
|
|
|
|
type UID = Int
|
|
|
|
|
|
2016-02-20 01:28:49 +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 {
|
|
|
|
|
_proConId :: UID,
|
|
|
|
|
_proConText :: Text }
|
|
|
|
|
|
|
|
|
|
makeLenses ''ProCon
|
|
|
|
|
|
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,
|
2016-02-20 10:31:14 +03:00
|
|
|
|
_pros :: [ProCon],
|
|
|
|
|
_cons :: [ProCon],
|
2016-02-14 15:19:36 +03:00
|
|
|
|
_link :: Maybe Text,
|
|
|
|
|
_kind :: ItemKind }
|
2016-02-02 20:41:29 +03:00
|
|
|
|
|
|
|
|
|
makeLenses ''Item
|
|
|
|
|
|
2016-02-20 10:31:14 +03:00
|
|
|
|
proConById :: UID -> Lens' Item ProCon
|
|
|
|
|
proConById uid = singular $
|
|
|
|
|
(pros.each . filtered ((== uid) . view proConId)) `failing`
|
|
|
|
|
(cons.each . filtered ((== uid) . view proConId))
|
|
|
|
|
|
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 {
|
|
|
|
|
_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 14:10:54 +03:00
|
|
|
|
emptyState :: S
|
|
|
|
|
emptyState = S {
|
|
|
|
|
_categories = [] }
|
|
|
|
|
|
|
|
|
|
sampleState :: S
|
|
|
|
|
sampleState = S {
|
|
|
|
|
_categories = [
|
|
|
|
|
Category {
|
2016-02-20 01:28:49 +03:00
|
|
|
|
_catId = 1,
|
2016-02-14 14:10:54 +03:00
|
|
|
|
_title = "lenses",
|
|
|
|
|
_items = [
|
|
|
|
|
Item {
|
2016-02-20 01:28:49 +03:00
|
|
|
|
_itemId = 2,
|
2016-02-14 15:19:36 +03:00
|
|
|
|
_name = "lens",
|
2016-02-20 10:31:14 +03:00
|
|
|
|
_pros = [ProCon 3 "the standard lenses library",
|
|
|
|
|
ProCon 4 "batteries included"],
|
|
|
|
|
_cons = [ProCon 5 "huge"],
|
2016-02-14 15:19:36 +03:00
|
|
|
|
_link = Nothing,
|
|
|
|
|
_kind = HackageLibrary },
|
2016-02-14 14:10:54 +03:00
|
|
|
|
Item {
|
2016-02-20 10:31:14 +03:00
|
|
|
|
_itemId = 6,
|
2016-02-14 15:19:36 +03:00
|
|
|
|
_name = "microlens",
|
2016-02-20 10:31:14 +03:00
|
|
|
|
_pros = [ProCon 7 "very small",
|
|
|
|
|
ProCon 8 "good for libraries"],
|
|
|
|
|
_cons = [ProCon 9 "doesn't have advanced features"],
|
2016-02-14 15:19:36 +03:00
|
|
|
|
_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"
|
2016-02-20 01:28:49 +03:00
|
|
|
|
uid <- randomUID
|
2016-02-02 19:29:23 +03:00
|
|
|
|
let newCategory = Category {
|
2016-02-20 01:28:49 +03:00
|
|
|
|
_catId = uid,
|
2016-02-14 14:03:39 +03:00
|
|
|
|
_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-20 01:28:49 +03:00
|
|
|
|
uid <- randomUID
|
2016-02-14 14:03:39 +03:00
|
|
|
|
let newItem = Item {
|
2016-02-20 01:28:49 +03:00
|
|
|
|
_itemId = uid,
|
2016-02-14 15:19:36 +03:00
|
|
|
|
_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-20 10:31:14 +03:00
|
|
|
|
lucid $ renderItem Normal 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-20 10:31:14 +03:00
|
|
|
|
content <- param' "content"
|
|
|
|
|
uid <- randomUID
|
2016-02-20 10:36:50 +03:00
|
|
|
|
let newThing = ProCon uid content
|
|
|
|
|
withS $ do
|
|
|
|
|
itemById itemId' . pros %= (++ [newThing])
|
|
|
|
|
lucid $ renderProCon Editable itemId' newThing
|
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-20 10:31:14 +03:00
|
|
|
|
content <- param' "content"
|
|
|
|
|
uid <- randomUID
|
2016-02-20 10:36:50 +03:00
|
|
|
|
let newThing = ProCon uid content
|
|
|
|
|
withS $ do
|
|
|
|
|
itemById itemId' . cons %= (++ [newThing])
|
|
|
|
|
lucid $ renderProCon Editable itemId' newThing
|
2016-02-17 19:43:35 +03:00
|
|
|
|
|
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-20 02:28:18 +03:00
|
|
|
|
-- Return rendered item the way it should normally look.
|
|
|
|
|
Spock.get ("/item" <//> var <//> "render-normal") $ \itemId' -> do
|
|
|
|
|
item <- withS $ use (itemById itemId')
|
2016-02-20 10:31:14 +03:00
|
|
|
|
lucid $ renderItem Normal item
|
2016-02-20 02:28:18 +03:00
|
|
|
|
|
2016-02-20 10:31:14 +03:00
|
|
|
|
-- Return rendered item the way it should look when it's editable.
|
2016-02-20 02:28:18 +03:00
|
|
|
|
Spock.get ("/item" <//> var <//> "render-edit") $ \itemId' -> do
|
|
|
|
|
item <- withS $ use (itemById itemId')
|
|
|
|
|
lucid $ renderItem Editable item
|
|
|
|
|
|
2016-02-20 10:31:14 +03:00
|
|
|
|
-- Return rendered pro/con the way it should normally look.
|
|
|
|
|
Spock.get ("/item" <//> var <//> "pro-con" <//> var <//> "render-normal") $
|
|
|
|
|
\itemId' proConId' -> do
|
|
|
|
|
thing <- withS $ use (itemById itemId' . proConById proConId')
|
|
|
|
|
lucid $ renderProCon Editable itemId' thing
|
|
|
|
|
|
|
|
|
|
-- Return rendered pro/con the way it should look when it's being edited.
|
|
|
|
|
Spock.get ("/item" <//> var <//> "pro-con" <//> var <//> "render-edit") $
|
|
|
|
|
\itemId' proConId' -> do
|
|
|
|
|
thing <- withS $ use (itemById itemId' . proConById proConId')
|
|
|
|
|
lucid $ renderProCon InEdit itemId' thing
|
|
|
|
|
|
|
|
|
|
-- Change a pro/con.
|
|
|
|
|
Spock.post ("/item" <//> var <//> "pro-con" <//> var <//> "set") $
|
|
|
|
|
\itemId' proConId' -> do
|
|
|
|
|
content <- param' "content"
|
|
|
|
|
changedThing <- withS $ do
|
|
|
|
|
itemById itemId' . proConById proConId' . proConText .= content
|
|
|
|
|
use (itemById itemId' . proConById proConId')
|
|
|
|
|
lucid $ renderProCon Editable itemId' changedThing
|
|
|
|
|
|
2016-02-20 01:01:14 +03:00
|
|
|
|
renderRoot :: S -> HtmlT IO ()
|
2016-02-02 19:41:28 +03:00
|
|
|
|
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-20 01:51:33 +03:00
|
|
|
|
categoriesNode <- div_ [id_ "categories"] $ do
|
2016-02-02 19:41:28 +03:00
|
|
|
|
mapM_ renderCategory (s ^. categories)
|
2016-02-20 01:51:33 +03:00
|
|
|
|
thisNode
|
2016-02-19 22:12:23 +03:00
|
|
|
|
input_ [type_ "text", placeholder_ "new category",
|
2016-02-20 01:51:33 +03:00
|
|
|
|
submitFunc (js_addCategory (categoriesNode, js_this_value))]
|
2016-02-02 14:50:30 +03:00
|
|
|
|
|
2016-02-20 01:01:14 +03:00
|
|
|
|
renderCategoryHeading :: Category -> HtmlT IO ()
|
2016-02-17 19:43:35 +03:00
|
|
|
|
renderCategoryHeading category =
|
|
|
|
|
h2_ $ do
|
2016-02-20 01:01:14 +03:00
|
|
|
|
headerNode <- thisNode
|
2016-02-17 19:43:35 +03:00
|
|
|
|
-- TODO: make category headings anchor links
|
|
|
|
|
toHtml (category^.title)
|
2016-02-20 00:13:20 +03:00
|
|
|
|
textButton "edit" $
|
2016-02-20 01:01:14 +03:00
|
|
|
|
js_startCategoryHeadingEdit (headerNode, category^.catId)
|
2016-02-17 19:43:35 +03:00
|
|
|
|
|
2016-02-20 01:01:14 +03:00
|
|
|
|
renderCategoryHeadingEdit :: Category -> HtmlT IO ()
|
2016-02-17 19:43:35 +03:00
|
|
|
|
renderCategoryHeadingEdit category =
|
|
|
|
|
h2_ $ do
|
2016-02-20 01:01:14 +03:00
|
|
|
|
headerNode <- thisNode
|
2016-02-19 22:34:51 +03:00
|
|
|
|
let handler = js_submitCategoryHeadingEdit
|
2016-02-20 01:01:14 +03:00
|
|
|
|
(headerNode, category^.catId, js_this_value)
|
2016-02-17 19:43:35 +03:00
|
|
|
|
input_ [type_ "text", value_ (category^.title), submitFunc handler]
|
2016-02-20 01:01:14 +03:00
|
|
|
|
textButton "cancel" $
|
|
|
|
|
js_cancelCategoryHeadingEdit (headerNode, category^.catId)
|
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 01:28:49 +03:00
|
|
|
|
div_ [id_ (tshow (category^.catId))] $ do
|
2016-02-17 19:43:35 +03:00
|
|
|
|
renderCategoryHeading category
|
2016-02-20 01:51:33 +03:00
|
|
|
|
itemsNode <- div_ [class_ "items"] $ do
|
2016-02-20 10:31:14 +03:00
|
|
|
|
mapM_ (renderItem Normal) (category^.items)
|
2016-02-20 01:51:33 +03:00
|
|
|
|
thisNode
|
|
|
|
|
let handler = js_addLibrary (itemsNode, 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-20 02:28:18 +03:00
|
|
|
|
renderItem
|
|
|
|
|
:: Editable -- ^ Show edit buttons?
|
|
|
|
|
-> Item
|
|
|
|
|
-> HtmlT IO ()
|
|
|
|
|
renderItem editable item =
|
2016-02-20 01:28:49 +03:00
|
|
|
|
div_ [class_ "item", id_ (tshow (item^.itemId))] $ do
|
2016-02-20 01:01:14 +03:00
|
|
|
|
itemNode <- thisNode
|
2016-02-20 02:28:18 +03:00
|
|
|
|
h3_ $ do
|
|
|
|
|
itemHeader
|
|
|
|
|
case editable of
|
2016-02-20 10:31:14 +03:00
|
|
|
|
Normal -> textButton "edit" $
|
2016-02-20 02:28:18 +03:00
|
|
|
|
js_enableItemEdit (itemNode, item^.itemId)
|
|
|
|
|
Editable -> textButton "edit off" $
|
|
|
|
|
js_disableItemEdit (itemNode, item^.itemId)
|
2016-02-14 14:03:39 +03:00
|
|
|
|
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:"
|
2016-02-20 02:28:18 +03:00
|
|
|
|
case editable of
|
2016-02-20 10:31:14 +03:00
|
|
|
|
Normal ->
|
|
|
|
|
ul_ $ mapM_ (renderProCon Normal (item^.itemId)) (item^.pros)
|
2016-02-20 02:28:18 +03:00
|
|
|
|
Editable -> do
|
2016-02-20 10:36:50 +03:00
|
|
|
|
listNode <- ul_ $ do
|
|
|
|
|
mapM_ (renderProCon Editable (item^.itemId)) (item^.pros)
|
|
|
|
|
thisNode
|
|
|
|
|
let handler = js_addPros (listNode, item^.itemId, js_this_value)
|
2016-02-20 02:28:18 +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:"
|
2016-02-20 02:28:18 +03:00
|
|
|
|
case editable of
|
2016-02-20 10:31:14 +03:00
|
|
|
|
Normal ->
|
|
|
|
|
ul_ $ mapM_ (renderProCon Normal (item^.itemId)) (item^.cons)
|
2016-02-20 02:28:18 +03:00
|
|
|
|
Editable -> do
|
2016-02-20 10:36:50 +03:00
|
|
|
|
listNode <- ul_ $ do
|
|
|
|
|
mapM_ (renderProCon Editable (item^.itemId)) (item^.cons)
|
|
|
|
|
thisNode
|
|
|
|
|
let handler = js_addCons (listNode, item^.itemId, js_this_value)
|
2016-02-20 02:28:18 +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-20 10:31:14 +03:00
|
|
|
|
renderProCon :: Editable -> UID -> ProCon -> HtmlT IO ()
|
|
|
|
|
renderProCon Normal _ thing = li_ (toHtml (thing^.proConText))
|
|
|
|
|
renderProCon Editable itemId' thing = li_ $ do
|
|
|
|
|
this <- thisNode
|
|
|
|
|
toHtml (thing^.proConText)
|
|
|
|
|
textButton "edit" $
|
|
|
|
|
js_startProConEdit (this, itemId', thing^.proConId)
|
|
|
|
|
renderProCon InEdit itemId' thing = li_ $ do
|
|
|
|
|
this <- thisNode
|
|
|
|
|
let handler = js_submitProConEdit
|
|
|
|
|
(this, itemId', thing^.proConId, js_this_value)
|
|
|
|
|
input_ [type_ "text", value_ (thing^.proConText), submitFunc handler]
|
|
|
|
|
textButton "cancel" $
|
|
|
|
|
js_cancelProConEdit (this, itemId', thing^.proConId)
|
|
|
|
|
|
2016-02-02 14:50:30 +03:00
|
|
|
|
-- Utils
|
|
|
|
|
|
2016-02-20 01:01:14 +03:00
|
|
|
|
includeJS :: Monad m => Text -> HtmlT m ()
|
2016-02-14 14:03:39 +03:00
|
|
|
|
includeJS url = with (script_ "") [src_ url]
|
|
|
|
|
|
2016-02-20 01:01:14 +03:00
|
|
|
|
includeCSS :: Monad m => Text -> 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 02:30:04 +03:00
|
|
|
|
submitFunc :: JS -> Attribute
|
2016-02-14 15:19:36 +03:00
|
|
|
|
submitFunc f = onkeyup_ $ format
|
|
|
|
|
"if (event.keyCode == 13) {\
|
|
|
|
|
\ {}\
|
|
|
|
|
\ this.value = ''; }"
|
|
|
|
|
[f]
|
|
|
|
|
|
2016-02-19 22:12:23 +03:00
|
|
|
|
-- Javascript
|
|
|
|
|
|
2016-02-20 02:30:04 +03:00
|
|
|
|
js_this_value :: JS
|
2016-02-20 00:13:20 +03:00
|
|
|
|
js_this_value = "this.value"
|
2016-02-19 22:12:23 +03:00
|
|
|
|
|
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 02:30:04 +03:00
|
|
|
|
instance Format.Params a => JSFunction (a -> JS) where
|
2016-02-19 22:12:23 +03:00
|
|
|
|
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-20 02:28:18 +03:00
|
|
|
|
js_addPros, js_addCons,
|
2016-02-20 10:31:14 +03:00
|
|
|
|
js_enableItemEdit, js_disableItemEdit,
|
|
|
|
|
js_startProConEdit, js_submitProConEdit, js_cancelProConEdit ]
|
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-19 22:27:27 +03:00
|
|
|
|
$.post("/category/add", {title: s})
|
2016-02-19 22:12:23 +03:00
|
|
|
|
.done(function(data) {
|
2016-02-20 01:51:33 +03:00
|
|
|
|
$(node).append(data);
|
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-19 22:27:27 +03:00
|
|
|
|
$.post("/category/"+catId+"/library/add", {name: s})
|
|
|
|
|
.done(function(data) {
|
2016-02-20 01:51:33 +03:00
|
|
|
|
$(node).append(data);
|
2016-02-19 22:27:27 +03:00
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|]
|
|
|
|
|
|
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|
|
2016-02-20 01:01:14 +03:00
|
|
|
|
function startCategoryHeadingEdit(node, 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) {
|
2016-02-20 01:01:14 +03:00
|
|
|
|
$(node).replaceWith(data);
|
2016-02-19 22:12:23 +03:00
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|]
|
|
|
|
|
|
|
|
|
|
{- |
|
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|
|
2016-02-20 01:01:14 +03:00
|
|
|
|
function cancelCategoryHeadingEdit(node, 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) {
|
2016-02-20 01:01:14 +03:00
|
|
|
|
$(node).replaceWith(data);
|
2016-02-19 22:12:23 +03:00
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|]
|
|
|
|
|
|
|
|
|
|
{- |
|
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|
|
2016-02-20 01:01:14 +03:00
|
|
|
|
function submitCategoryHeadingEdit(node, 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) {
|
2016-02-20 01:01:14 +03:00
|
|
|
|
$(node).replaceWith(data);
|
2016-02-19 22:12:23 +03:00
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|]
|
|
|
|
|
|
|
|
|
|
-- | Add pros to some item.
|
|
|
|
|
js_addPros :: JSFunction a => a
|
|
|
|
|
js_addPros = makeJSFunction "addPros" [text|
|
2016-02-20 01:01:14 +03:00
|
|
|
|
function addPros(node, 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) {
|
2016-02-20 10:36:50 +03:00
|
|
|
|
$(node).append(data);
|
2016-02-19 22:12:23 +03:00
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|]
|
|
|
|
|
|
|
|
|
|
-- | Add cons to some item.
|
|
|
|
|
js_addCons :: JSFunction a => a
|
|
|
|
|
js_addCons = makeJSFunction "addCons" [text|
|
2016-02-20 01:01:14 +03:00
|
|
|
|
function addCons(node, itemId, s) {
|
2016-02-19 22:27:27 +03:00
|
|
|
|
$.post("/item/"+itemId+"/cons/add", {content: s})
|
2016-02-20 02:28:18 +03:00
|
|
|
|
.done(function(data) {
|
2016-02-20 10:36:50 +03:00
|
|
|
|
$(node).append(data);
|
2016-02-20 02:28:18 +03:00
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|]
|
|
|
|
|
|
|
|
|
|
-- | Add “[edit]” buttons to everything in an item.
|
|
|
|
|
js_enableItemEdit :: JSFunction a => a
|
|
|
|
|
js_enableItemEdit = makeJSFunction "enableItemEdit" [text|
|
|
|
|
|
function enableItemEdit (node, itemId) {
|
|
|
|
|
$.get("/item/"+itemId+"/render-edit")
|
2016-02-19 22:12:23 +03:00
|
|
|
|
.done(function(data) {
|
2016-02-20 01:01:14 +03:00
|
|
|
|
$(node).replaceWith(data);
|
2016-02-19 22:12:23 +03:00
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|]
|
|
|
|
|
|
2016-02-20 02:28:18 +03:00
|
|
|
|
-- | Remove “[edit]” buttons from everything in an item.
|
|
|
|
|
js_disableItemEdit :: JSFunction a => a
|
|
|
|
|
js_disableItemEdit = makeJSFunction "disableItemEdit" [text|
|
|
|
|
|
function disableItemEdit (node, itemId) {
|
|
|
|
|
$.get("/item/"+itemId+"/render-normal")
|
|
|
|
|
.done(function(data) {
|
|
|
|
|
$(node).replaceWith(data);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|]
|
|
|
|
|
|
2016-02-20 10:31:14 +03:00
|
|
|
|
js_startProConEdit :: JSFunction a => a
|
|
|
|
|
js_startProConEdit = makeJSFunction "startProConEdit" [text|
|
|
|
|
|
function startProConEdit(node, itemId, thingId) {
|
|
|
|
|
$.get("/item/"+itemId+"/pro-con/"+thingId+"/render-edit")
|
|
|
|
|
.done(function(data) {
|
|
|
|
|
$(node).replaceWith(data);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|]
|
|
|
|
|
|
|
|
|
|
js_cancelProConEdit :: JSFunction a => a
|
|
|
|
|
js_cancelProConEdit = makeJSFunction "cancelProConEdit" [text|
|
|
|
|
|
function cancelProConEdit(node, itemId, thingId) {
|
|
|
|
|
$.get("/item/"+itemId+"/pro-con/"+thingId+"/render-normal")
|
|
|
|
|
.done(function(data) {
|
|
|
|
|
$(node).replaceWith(data);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|]
|
|
|
|
|
|
|
|
|
|
js_submitProConEdit :: JSFunction a => a
|
|
|
|
|
js_submitProConEdit = makeJSFunction "submitProConEdit" [text|
|
|
|
|
|
function submitProConEdit(node, itemId, thingId, s) {
|
|
|
|
|
$.post("/item/"+itemId+"/pro-con/"+thingId+"/set", {content: s})
|
|
|
|
|
.done(function(data) {
|
|
|
|
|
$(node).replaceWith(data);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|]
|
|
|
|
|
|
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 01:28:49 +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
|