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

429 lines
13 KiB
Haskell
Raw Normal View History

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