Added site itself, in more or less working shape
Getty Ritter
8 years ago
1 | module Main where | |
2 | ||
3 | import Control.Monad.IO.Class | |
4 | import Database.SQLite.Simple (Connection, open) | |
5 | import Data.Monoid ((<>)) | |
6 | import Data.Text (pack) | |
7 | import Network.HTTP.Types.Status (status404) | |
8 | import Network.Wai.Middleware.Static (staticPolicy, hasPrefix) | |
9 | import System.Environment (lookupEnv) | |
10 | import Web.Spock | |
11 | ||
12 | import Render | |
13 | import Types | |
14 | ||
15 | respondWith :: [Entry] -> ActionCtxT ctx IO () | |
16 | respondWith es = do | |
17 | pref <- preferredFormat | |
18 | case pref of | |
19 | PrefJSON -> json es | |
20 | _ -> html (rPage (rEntries es)) | |
21 | ||
22 | db :: DB a -> ActionCtxT Connection IO a | |
23 | db mote = do | |
24 | conn <- getContext | |
25 | liftIO (mote conn) | |
26 | ||
27 | main :: IO () | |
28 | main = do | |
29 | dbLoc <- maybe "test.db" id `fmap` lookupEnv "DB_LOC" | |
30 | port <- maybe 8080 read `fmap` lookupEnv "PORT" | |
31 | conn <- open dbLoc | |
32 | runSpock port $ spockT id $ prehook (return conn) $ do | |
33 | middleware $ staticPolicy (mempty <> hasPrefix "static") | |
34 | get root $ do | |
35 | html (rPage "welcome to the dictionary yo") | |
36 | get "add" $ do | |
37 | ls <- db allLanguages | |
38 | html (rPage (rAdd ls)) | |
39 | get "search" $ do | |
40 | html (rPage rSearch) | |
41 | get "word" $ do | |
42 | es <- db getAllWords | |
43 | respondWith es | |
44 | post "word" $ do | |
45 | ps <- params | |
46 | case paramsToEntry ps of | |
47 | Nothing -> setStatus status404 | |
48 | Just e -> do | |
49 | i <- db (addWord e) | |
50 | redirect (pack ("/word/" ++ show i)) | |
51 | get ("word" <//> var) $ \ w -> do | |
52 | eM <- db (getWordById w) | |
53 | case eM of | |
54 | Just e -> respondWith [e] | |
55 | Nothing -> setStatus status404 | |
56 | get ("word" <//> "search" <//> var) $ \ t -> do | |
57 | db (searchAll t) >>= respondWith | |
58 | get ("word" <//> "search" <//> "by-word" <//> var) $ \ t -> do | |
59 | db (searchWord t) >>= respondWith | |
60 | get ("word" <//> "search" <//> "by-meaning" <//> var) $ \ t -> do | |
61 | db (searchMeaning t) >>= respondWith |
1 | module Render where | |
2 | ||
3 | import Control.Monad (when) | |
4 | import Data.Text (Text, pack) | |
5 | import Data.Text.Lazy (toStrict) | |
6 | import Lucid | |
7 | ||
8 | import Types | |
9 | ||
10 | rPage :: Html () -> Text | |
11 | rPage content = toStrict $ renderText $ doctypehtml_ $ do | |
12 | head_ $ do | |
13 | meta_ [ httpEquiv_ "Content-Type" | |
14 | , content_ "text/html; encoding=utf8;" | |
15 | ] | |
16 | link_ [ href_ "/static/main.css" | |
17 | , rel_ "stylesheet" | |
18 | , type_ "text/css" | |
19 | ] | |
20 | title_ "infinite negative dictionary" | |
21 | body_ $ do | |
22 | div_ [ class_ "header" ] $ do | |
23 | h1_ "Infinite Negative Dictionary" | |
24 | div_ [ class_ "menu" ] $ do | |
25 | menu | |
26 | div_ [ class_ "main" ] $ do | |
27 | content | |
28 | div_ [ class_ "footer" ] $ do | |
29 | "[what do I need here? copyright?]" | |
30 | ||
31 | menu :: Html () | |
32 | menu = ul_ $ do | |
33 | li_ $ a_ [ href_ "/" ] "index" | |
34 | li_ $ a_ [ href_ "/word" ] "browse" | |
35 | li_ $ a_ [ href_ "/add" ] "add" | |
36 | li_ $ a_ [ href_ "/search" ] "search" | |
37 | ||
38 | rEntries :: [Entry] -> Html () | |
39 | rEntries = mapM_ rEntry | |
40 | ||
41 | rEntry :: Entry -> Html () | |
42 | rEntry e = div_ [ class_ "entry" ] $ do | |
43 | div_ [ class_ "word" ] $ toHtml (eWord e) | |
44 | div_ [ class_ "translit" ] $ toHtml (eTranslit e) | |
45 | div_ [ class_ "meaning" ] $ toHtml (eMeaning e) | |
46 | when (eNotes e /= "") $ | |
47 | div_ [ class_ "notes" ] $ toHtml (eNotes e) | |
48 | ||
49 | rSearch :: Html () | |
50 | rSearch = form_ [ name_ "search" ] $ do | |
51 | input_ [ type_ "text", name_ "query" ] | |
52 | ||
53 | rAdd :: [Language] -> Html () | |
54 | rAdd ls = form_ [ name_ "add", action_ "/word", method_ "POST" ] $ do | |
55 | div_ $ do | |
56 | label_ "Language: " | |
57 | select_ [ name_ "lang" ] $ sequence_ | |
58 | [ option_ [ value_ (pack (show i)) ] $ toHtml n | |
59 | | Language { langId = i, langName = n } <- ls | |
60 | ] | |
61 | div_ $ do | |
62 | label_ "Word: " | |
63 | input_ [ type_ "text", name_ "word" ] | |
64 | div_ $ do | |
65 | label_ "Transliteration: " | |
66 | input_ [ type_ "text", name_ "translit" ] | |
67 | div_ $ do | |
68 | label_ "Meaning: " | |
69 | textarea_ [ rows_ "4", cols_ "100", name_ "meaning" ] "" | |
70 | div_ $ do | |
71 | label_ "Notes: " | |
72 | textarea_ [ rows_ "4", cols_ "100", name_ "notes" ] "" | |
73 | div_ $ input_ [ type_ "submit" ] |
1 | module Types where | |
2 | ||
3 | import Database.SQLite.Simple | |
4 | import Data.Aeson | |
5 | import Data.Monoid ((<>)) | |
6 | import Data.Text (Text, unpack) | |
7 | import Control.Error (headMay) | |
8 | import Text.Read (readMaybe) | |
9 | ||
10 | type DB a = Connection -> IO a | |
11 | ||
12 | -- | Language | |
13 | ||
14 | data Language = Language | |
15 | { langId :: Integer | |
16 | , langName :: Text | |
17 | } deriving (Eq, Show) | |
18 | ||
19 | instance FromRow Language where | |
20 | fromRow = Language <$> field <*> field | |
21 | ||
22 | instance ToRow Language where | |
23 | toRow l = toRow (Only (langName l)) | |
24 | ||
25 | instance ToJSON Language where | |
26 | toJSON l = object [ "id" .= langId l | |
27 | , "name" .= langName l | |
28 | ] | |
29 | ||
30 | instance FromJSON Language where | |
31 | parseJSON = withObject "Language object" $ \ o -> | |
32 | Language <$> o .: "id" | |
33 | <*> o .: "name" | |
34 | ||
35 | allLanguages :: DB [Language] | |
36 | allLanguages conn = | |
37 | query_ conn "SELECT id, name FROM languages" | |
38 | ||
39 | -- | Entry | |
40 | ||
41 | data Entry = Entry | |
42 | { eID :: Integer | |
43 | , eWord :: Text | |
44 | , eTranslit :: Text | |
45 | , eMeaning :: Text | |
46 | , eNotes :: Text | |
47 | , eLanguage :: Language | |
48 | } deriving (Eq, Show) | |
49 | ||
50 | instance FromRow Entry where | |
51 | fromRow = Entry <$> field | |
52 | <*> field | |
53 | <*> field | |
54 | <*> field | |
55 | <*> field | |
56 | <*> (Language <$> field <*> field) | |
57 | ||
58 | instance ToRow Entry where | |
59 | toRow e = toRow ( eWord e | |
60 | , eTranslit e | |
61 | , eMeaning e | |
62 | , eNotes e | |
63 | , langId (eLanguage e) | |
64 | ) | |
65 | ||
66 | instance ToJSON Entry where | |
67 | toJSON e = object [ "id" .= eID e | |
68 | , "word" .= eWord e | |
69 | , "translit" .= eTranslit e | |
70 | , "meaning" .= eMeaning e | |
71 | , "notes" .= eNotes e | |
72 | , "language" .= eLanguage e | |
73 | ] | |
74 | ||
75 | instance FromJSON Entry where | |
76 | parseJSON = withObject "Word object" $ \ o -> | |
77 | Entry <$> o .: "id" | |
78 | <*> o .: "word" | |
79 | <*> o .: "translit" | |
80 | <*> o .: "meaning" | |
81 | <*> o .: "notes" | |
82 | <*> o .: "language" | |
83 | ||
84 | paramsToEntry :: [(Text,Text)] -> Maybe Entry | |
85 | paramsToEntry ps = Entry <$> pure 0 | |
86 | <*> lookup "word" ps | |
87 | <*> lookup "translit" ps | |
88 | <*> lookup "meaning" ps | |
89 | <*> lookup "notes" ps | |
90 | <*> (flip Language "???" <$> langNum) | |
91 | where langNum = lookup "lang" ps >>= (readMaybe . unpack) | |
92 | ||
93 | -- | |
94 | ||
95 | addWord :: Entry -> DB Integer | |
96 | addWord entry conn = do | |
97 | let q = "INSERT INTO words (word, trans, meaning, notes, language) \ | |
98 | \ VALUES (?,?,?,?,?)" | |
99 | execute conn q entry | |
100 | fromIntegral <$> lastInsertRowId conn | |
101 | ||
102 | getWordById :: Integer -> DB (Maybe Entry) | |
103 | getWordById i conn = do | |
104 | let q = "SELECT w.id, w.word, w.trans, w.meaning, w.notes, l.id, l.name \ | |
105 | \ FROM words w, languages l \ | |
106 | \ WHERE w.id = ? AND w.language = l.id" | |
107 | headMay <$> query conn q (Only i) | |
108 | ||
109 | getAllWords :: DB [Entry] | |
110 | getAllWords conn = do | |
111 | let q = "SELECT w.id, w.word, w.trans, w.meaning, w.notes, l.id, l.name \ | |
112 | \ FROM words w, languages l \ | |
113 | \ WHERE w.language = l.id" | |
114 | query_ conn q | |
115 | ||
116 | searchText :: Text -> Text | |
117 | searchText t = "%" <> t <> "%" | |
118 | ||
119 | searchWord :: Text -> DB [Entry] | |
120 | searchWord t conn = do | |
121 | let q = "SELECT w.id, w.word, w.trans, w.meaning, w.notes, l.id, l.name \ | |
122 | \ FROM words w, languages l \ | |
123 | \ WHERE w.language = l.id AND \ | |
124 | \ (w.word LIKE ? OR w.trans LIKE ?)" | |
125 | query conn q (searchText t, searchText t) | |
126 | ||
127 | searchMeaning :: Text -> DB [Entry] | |
128 | searchMeaning t conn = do | |
129 | let q = "SELECT w.id, w.word, w.trans, w.meaning, w.notes, l.id, l.name \ | |
130 | \ FROM words w, languages l \ | |
131 | \ WHERE w.language = l.id AND \ | |
132 | \ (w.meaning LIKE ? OR w.notes LIKE ?)" | |
133 | query conn q (searchText t, searchText t) | |
134 | ||
135 | searchAll :: Text -> DB [Entry] | |
136 | searchAll t conn = do | |
137 | let q = "SELECT w.id, w.word, w.trans, w.meaning, w.notes, l.id, l.name \ | |
138 | \ FROM words w, languages l \ | |
139 | \ WHERE w.language = l.id AND \ | |
140 | \ (w.word LIKE ? OR w.trans LIKE ? OR w.meaning LIKE ? OR w.notes LIKE ?)" | |
141 | query conn q (searchText t, searchText t, searchText t, searchText t) |
1 | @font-face { | |
2 | font-family: trinako; | |
3 | src: url("/static/trinako_sans.otf"); | |
4 | } | |
5 | ||
6 | body { | |
7 | background-color: #eee; | |
8 | font-family: "Arial", "Helvetica", sans-serif; | |
9 | } | |
10 | ||
11 | .header { | |
12 | text-align: center; | |
13 | letter-spacing: 0.2em; | |
14 | } | |
15 | ||
16 | .menu { | |
17 | text-align: center; | |
18 | } | |
19 | ||
20 | .menu li { | |
21 | display: inline; | |
22 | list-style-type: none; | |
23 | padding-left: 20px; | |
24 | padding-right: 40px; | |
25 | } | |
26 | ||
27 | .main { | |
28 | border-top-style: solid; | |
29 | border-top-width: 1px; | |
30 | border-bottom-style: solid; | |
31 | border-bottom-width: 1px; | |
32 | width: 50%; | |
33 | background-color: #f7f7f7; | |
34 | padding: 40px; | |
35 | margin-left: auto; | |
36 | margin-right: auto; | |
37 | margin-bottom: 20px; | |
38 | } | |
39 | ||
40 | .word { | |
41 | font-family: trinako; | |
42 | float: left; | |
43 | margin-left: 2em; | |
44 | font-weight: bold; | |
45 | margin-bottom: 10px; | |
46 | } | |
47 | ||
48 | .translit { | |
49 | float: right; | |
50 | padding-right: 50%; | |
51 | font-style: italic; | |
52 | margin-bottom: 10px; | |
53 | } | |
54 | ||
55 | .meaning { | |
56 | clear: left; | |
57 | padding-top: 10px; | |
58 | padding-left: 40px; | |
59 | padding-right: 40px; | |
60 | border-top-style: solid; | |
61 | border-top-width: 1px; | |
62 | border-top-color: #aaa; | |
63 | } | |
64 | ||
65 | .notes { | |
66 | font-style: italic; | |
67 | } | |
68 | ||
69 | .entry { | |
70 | background-color: #fff; | |
71 | clear: left; | |
72 | margin-top: 15px; | |
73 | margin-bottom: 15px; | |
74 | padding-left: 20px; | |
75 | padding-right: 20px; | |
76 | padding-top: 10px; | |
77 | padding-bottom: 10px; | |
78 | } | |
79 | ||
80 | .footer { | |
81 | text-align: center; | |
82 | width: 400px; | |
83 | margin-left: auto; | |
84 | margin-right: auto; | |
85 | } | |
86 | ||
87 | form { | |
88 | width: 80%; | |
89 | margin: 0 auto; | |
90 | } | |
91 | ||
92 | label, input { | |
93 | display: inline-block; | |
94 | } | |
95 | ||
96 | label { | |
97 | clear: right; | |
98 | width: 30%; | |
99 | text-align: right; | |
100 | margin-bottom: 10px; | |
101 | } | |
102 | ||
103 | label + input { | |
104 | width: 30%; | |
105 | margin: 0 30% 0 4%; | |
106 | } | |
107 | ||
108 | input + input { | |
109 | float: right; | |
110 | } | |
111 | ||
112 | select { | |
113 | width: 50%; | |
114 | margin-left: 4%; | |
115 | } | |
116 | ||
117 | textarea { | |
118 | float: right; | |
119 | }⏎ |
Binary diff not shown