From b951faa6b4771693f08b4002c771a508904d97a1 Mon Sep 17 00:00:00 2001 From: "\"Vincent Ambo ext:(%22)" Date: Wed, 22 Feb 2012 22:03:31 +0100 Subject: * initial checkin --- res/.DS_Store | Bin 0 -> 6148 bytes res/admin.css | 49 ++++++ res/apple-touch-icon.png | Bin 0 -> 9756 bytes res/blogstyle.css | 99 +++++++++++ res/drama.wav | Bin 0 -> 208300 bytes res/favicon.ico | Bin 0 -> 4354 bytes res/loginBoxTop.png | Bin 0 -> 606 bytes res/signin.gif | Bin 0 -> 1850 bytes res/twtbtn.png | Bin 0 -> 8058 bytes src/.DS_Store | Bin 0 -> 6148 bytes src/Blog.hs | 33 ++++ src/Server.hs | 98 +++++++++++ tools/.DS_Store | Bin 0 -> 6148 bytes tools/convertdb/.DS_Store | Bin 0 -> 6148 bytes tools/convertdb/Makefile | 13 ++ tools/convertdb/convertdb.go | 121 +++++++++++++ tools/convertdb/couch.go | 403 +++++++++++++++++++++++++++++++++++++++++++ tools/music/Makefile | 10 ++ tools/music/gettitle | 4 + tools/music/iTunes.8 | Bin 0 -> 37608 bytes tools/music/iTunes.go | 79 +++++++++ tools/music/music | Bin 0 -> 2843022 bytes tools/music/start | 1 + 23 files changed, 910 insertions(+) create mode 100644 res/.DS_Store create mode 100644 res/admin.css create mode 100644 res/apple-touch-icon.png create mode 100644 res/blogstyle.css create mode 100644 res/drama.wav create mode 100644 res/favicon.ico create mode 100644 res/loginBoxTop.png create mode 100644 res/signin.gif create mode 100644 res/twtbtn.png create mode 100644 src/.DS_Store create mode 100644 src/Blog.hs create mode 100644 src/Server.hs create mode 100644 tools/.DS_Store create mode 100644 tools/convertdb/.DS_Store create mode 100644 tools/convertdb/Makefile create mode 100644 tools/convertdb/convertdb.go create mode 100644 tools/convertdb/couch.go create mode 100644 tools/music/Makefile create mode 100755 tools/music/gettitle create mode 100644 tools/music/iTunes.8 create mode 100644 tools/music/iTunes.go create mode 100755 tools/music/music create mode 100755 tools/music/start diff --git a/res/.DS_Store b/res/.DS_Store new file mode 100644 index 0000000000..5008ddfcf5 Binary files /dev/null and b/res/.DS_Store differ diff --git a/res/admin.css b/res/admin.css new file mode 100644 index 0000000000..7a4d418975 --- /dev/null +++ b/res/admin.css @@ -0,0 +1,49 @@ +@charset "UTF-8"; +/* CSS Document */ + + +body { + padding-top: 20px; + font-family: 'PT Sans', sans-serif; + background-image: linear-gradient(bottom, rgb(245,245,245) 66%, rgb(239,239,239) 83%); + background-image: -o-linear-gradient(bottom, rgb(245,245,245) 66%, rgb(239,239,239) 83%); + background-image: -webkit-linear-gradient(bottom, rgb(245,245,245) 66%, rgb(239,239,239) 83%); + background-image: -webkit-gradient( + linear, + left bottom, + left top, + color-stop(0.66, rgb(245,245,245)), + color-stop(0.83, rgb(239,239,239)) + ); + background-repeat: no-repeat; + background-color: rgb(245,245,245); +} + +.loginBox { + width: 400px; + margin: 0 auto; +} + +.loginBoxTop { + width: 380px; + height: 28px; + color: #FFFFFF; + font-size: 12px; + padding-left: 20px; + padding-top: 11px; + background: url(/res/loginBoxTop.png); +} + +.loginBoxMiddle { + background-color: #F3F3F3; + border-top: 0px hidden; + border:1px solid #D2D2D2; + border-bottom-left-radius: 12px; + border-bottom-right-radius: 12px; + ; + font-size:12px; + height:auto; + min-height:200px; + padding-left:20px; + width:378px; +} \ No newline at end of file diff --git a/res/apple-touch-icon.png b/res/apple-touch-icon.png new file mode 100644 index 0000000000..22ba058cdd Binary files /dev/null and b/res/apple-touch-icon.png differ diff --git a/res/blogstyle.css b/res/blogstyle.css new file mode 100644 index 0000000000..8c065c53ce --- /dev/null +++ b/res/blogstyle.css @@ -0,0 +1,99 @@ +@charset "UTF-8"; +/* CSS Document */ + +@font-face { + font-family: 'PT Sans'; + font-style: normal; + font-weight: normal; + src: local('PT Sans'), local('PTSans-Regular'), url('http://themes.googleusercontent.com/static/fonts/ptsans/v2/7YGmE4Ls5b94ct65u07hVQLUuEpTyoUstqEm5AMlJo4.woff') format('woff'); +} + +body { + padding-top: 20px; + font-family: 'PT Sans', sans-serif; + background-image: linear-gradient(bottom, rgb(245,245,245) 66%, rgb(239,239,239) 83%); + background-image: -o-linear-gradient(bottom, rgb(245,245,245) 66%, rgb(239,239,239) 83%); + background-image: -moz-linear-gradient(bottom, rgb(245,245,245) 66%, rgb(239,239,239) 83%); + background-image: -webkit-linear-gradient(bottom, rgb(245,245,245) 66%, rgb(239,239,239) 83%); + background-image: -ms-linear-gradient(bottom, rgb(245,245,245) 66%, rgb(239,239,239) 83%); + background-image: -webkit-gradient( + linear, + left bottom, + left top, + color-stop(0.66, rgb(245,245,245)), + color-stop(0.83, rgb(239,239,239)) + ); +} + +.mainshell { + width: 98%; + margin: auto; +} + +.gradBox { + width: 98%; + margin: auto; +} + +.myclear { + clear: both; +} + +.centerbox { + text-align:center; +} + +.innerBox { + width: 100%; + margin-top: 20px; +} + +.innerBoxTop { + height: 28px; + color: #000000; + font-weight: bold; + font-size: 16px; + padding-left: 20px; + padding-top: 11px; + border: 1px solid #b6b6b6; + border-top-left-radius: 6px; + border-top-right-radius: 6px; + -moz-border-radius-topleft: 6px; + -moz-border-radius-topright: 6px; + -webkit-border-top-left-radius: 6px; + -webkit-border-top-right-radius: 6px; + border-bottom: 1px solid #dcdcdc; + background-image: linear-gradient(bottom, rgb(246,246,246) 31%, rgb(234,234,234) 83%); + background-image: -o-linear-gradient(bottom, rgb(246,246,246) 31%, rgb(234,234,234) 83%); + background-image: -moz-linear-gradient(bottom, rgb(246,246,246) 31%, rgb(234,234,234) 83%); + background-image: -webkit-linear-gradient(bottom, rgb(246,246,246) 31%, rgb(234,234,234) 83%); + background-image: -ms-linear-gradient(bottom, rgb(246,246,246) 31%, rgb(234,234,234) 83%); + + background-image: -webkit-gradient( + linear, + left bottom, + left top, + color-stop(0.31, rgb(246,246,246)), + color-stop(0.83, rgb(234,234,234)) + ); +} + +.innerBoxMiddle { + border: 1px solid #b6b6b6; + border-bottom-left-radius: 6px; + border-bottom-right-radius: 6px; + -moz-border-radius-bottomleft: 6px; + -moz-border-radius-bottomright: 6px; + -webkit-border-bottom-left-radius: 6px; + -webkit-border-bottom-right-radius: 6px; + border-top: 0px hidden; + background-color: #FFFFFF; + min-height: 200px; + height: auto; + padding-top: 21px; + padding-right: 2px; +} + +.innerBoxComments { + padding-left: 20px +} \ No newline at end of file diff --git a/res/drama.wav b/res/drama.wav new file mode 100644 index 0000000000..20d326c5f4 Binary files /dev/null and b/res/drama.wav differ diff --git a/res/favicon.ico b/res/favicon.ico new file mode 100644 index 0000000000..2958dd3afc Binary files /dev/null and b/res/favicon.ico differ diff --git a/res/loginBoxTop.png b/res/loginBoxTop.png new file mode 100644 index 0000000000..8a0ee3ba8d Binary files /dev/null and b/res/loginBoxTop.png differ diff --git a/res/signin.gif b/res/signin.gif new file mode 100644 index 0000000000..bbe282bae0 Binary files /dev/null and b/res/signin.gif differ diff --git a/res/twtbtn.png b/res/twtbtn.png new file mode 100644 index 0000000000..3a54c73c4c Binary files /dev/null and b/res/twtbtn.png differ diff --git a/src/.DS_Store b/src/.DS_Store new file mode 100644 index 0000000000..db284a629a Binary files /dev/null and b/src/.DS_Store differ diff --git a/src/Blog.hs b/src/Blog.hs new file mode 100644 index 0000000000..2a62bb7680 --- /dev/null +++ b/src/Blog.hs @@ -0,0 +1,33 @@ +{-# LANGUAGE OverloadedStrings, ScopedTypeVariables #-} +module Blog where + +import Text.Blaze (toValue, preEscapedString) +import Text.Blaze.Html5 (Html, (!), a, form, input, p, toHtml, label) +import Text.Blaze.Html5.Attributes (action, enctype, href, name, size, type_, value) +import qualified Text.Blaze.Html5 as H +import qualified Text.Blaze.Html5.Attributes as A + +blogTemplate :: String -> String -> String -> Html +blogTemplate t h o = H.docTypeHtml $ do + H.head $ do + H.title $ (toHtml t) + H.link ! A.rel "alternate" ! A.type_ "application/rss+xml" ! A.title "RSS-Feed" ! A.href "/rss" + H.link ! A.rel "stylesheet" ! A.type_ "text/css" ! A.href "/res/blogstyle.css" ! A.media "all" + H.meta ! A.httpEquiv "content-type" ! A.content "text/html;charset=UTF-8" +{- H.style ! A.type_ "text/css" ! A.title "iOS iMessage" ! A.media "screen and (max-device-width: 1024px)" $ "#cosx{display:none;} #cios{display:block;}" -} + H.body $ do + H.div ! A.class_ "mainshell" $ H.div ! A.class_ "gradBox" $ H.div ! A.class_ "header" $ do + H.a ! A.href "/" ! A.style "text-decoration:none;color:black;font-size:x-large;font-weight:bold;" $ + (toHtml t) + H.br + H.span ! A.id "cosx" ! A.style "display:block;" $ H.b $ contactInfo "imessage:tazjin@me.com" + H.span ! A.id "cios" ! A.style "display:none;" $ H.b $ contactInfo "sms:tazjin@me.com" + where + contactInfo (imu :: String) = do + toHtml h + H.a ! A.href "mailto:hej@tazj.in" $ "Mail" + ", " + H.a ! A.href "http://twitter.com/#!/tazjin" ! A.target "_blank" $ "Twitter" + toHtml o + H.a ! A.href (toValue imu) ! A.target "_blank" $ "iMessage" + "." \ No newline at end of file diff --git a/src/Server.hs b/src/Server.hs new file mode 100644 index 0000000000..aa41a2173d --- /dev/null +++ b/src/Server.hs @@ -0,0 +1,98 @@ +{-# LANGUAGE OverloadedStrings, ScopedTypeVariables, DeriveDataTypeable #-} + +module Main where + +import Control.Monad (msum, mzero) +import Data.Data (Data, Typeable) +import Data.Monoid (mempty) +import Data.ByteString.Char8 (ByteString) +import Data.Text hiding (map, length, zip, head) +import Data.Time +import Database.CouchDB +import Happstack.Server +import Text.Blaze (toValue, preEscapedString) +import Text.Blaze.Html5 (Html, (!), a, form, input, p, toHtml, label) +import Text.Blaze.Html5.Attributes (action, enctype, href, name, size, type_, value) +import qualified Text.Blaze.Html5 as H +import qualified Text.Blaze.Html5.Attributes as A +import Text.JSON.Generic + +import Blog + +tmpPolicy :: BodyPolicy +tmpPolicy = (defaultBodyPolicy "./tmp/" 0 1000 1000) + +data BlogLang = EN | DE + +data Comment = Comment{ + cauthor :: String, + ctext :: String, + cdate :: Integer +} deriving (Show, Data, Typeable) + +data Entry = Entry{ + _id :: String, + year :: Int, + month :: Int, + day :: Int, + lang :: String, + title :: String, + author :: String, + text :: String, + mtext :: String, + comments :: [Comment] +} deriving (Show, Data, Typeable) + +instance Show BlogLang where + show EN = "en" + show DE = "de" + +--TazBlog version +version = ("2.2b" :: String) + +main :: IO() +main = do + putStrLn ("TazBlog " ++ version ++ " in Haskell starting") + simpleHTTP nullConf tazBlog + +tazBlog :: ServerPart Response +tazBlog = do + msum [ dir "en" $ blogHandler EN + , dir "de" $ blogHandler DE + , do nullDir; + ok $ showIndex DE + , do dir " " $ nullDir; + seeOther ("https://plus.google.com/115916629925754851590" :: String) (toResponse ()) + , dir "res" $ serveDirectory DisableBrowsing [] "../res" + , serveDirectory DisableBrowsing [] "../res" + ] + +blogHandler :: BlogLang -> ServerPart Response +blogHandler lang = + msum [ path $ \(year :: Int) -> path $ \(month :: Int) -> path $ + \(day :: Int) -> path $ \(id_ :: String) -> showEntry lang year month day id_ + ] + +showEntry :: BlogLang -> Int -> Int -> Int -> String -> ServerPart Response +showEntry EN y m d i = undefined +showEntry DE y m d i = undefined + +showIndex :: BlogLang -> Response +showIndex lang = toResponse $ renderBlogHeader lang + +renderBlogHeader :: BlogLang -> Html +renderBlogHeader DE = blogTemplate "Tazjins Blog" "Wer mich kontaktieren will: " " oder " +renderBlogHeader EN = blogTemplate "Tazjin's Blog" "Get in touch with me: " " or " + +-- http://tazj.in/2012/02/10.155234 + +-- CouchDB View Setup +latestDEView = "function(doc){ if(doc.lang == \"de\"){ emit([doc.year, doc.month, doc.day, doc.id_], doc); } }" +latestENView = "function(doc){ if(doc.lang == \"en\"){ emit([doc.year, doc.month, doc.day, doc.id_]], doc); } }" + +latestDE = ViewMap "latestDE" latestDEView +latestEN = ViewMap "latestEN" latestENView + +setupBlogViews :: IO () -- taking *reservations* DB name as parameter because we'll have multiple stores +setupBlogViews = runCouchDB' $ + newView "tazblog" "entries" [latestDE, latestEN] diff --git a/tools/.DS_Store b/tools/.DS_Store new file mode 100644 index 0000000000..714b6567ae Binary files /dev/null and b/tools/.DS_Store differ diff --git a/tools/convertdb/.DS_Store b/tools/convertdb/.DS_Store new file mode 100644 index 0000000000..5de3023c9b Binary files /dev/null and b/tools/convertdb/.DS_Store differ diff --git a/tools/convertdb/Makefile b/tools/convertdb/Makefile new file mode 100644 index 0000000000..eba288ec1d --- /dev/null +++ b/tools/convertdb/Makefile @@ -0,0 +1,13 @@ +all: convertdb + +convertdb: couch.8 convertdb.8 + 8l -o convertdb convertdb.8 + +convertdb.8: convertdb.go + 8g convertdb.go + +couch.8: couch.go + 8g couch.go + +clean: + rm -rf *.8 convertdb diff --git a/tools/convertdb/convertdb.go b/tools/convertdb/convertdb.go new file mode 100644 index 0000000000..adef31910b --- /dev/null +++ b/tools/convertdb/convertdb.go @@ -0,0 +1,121 @@ +package main + +import ( + "strconv" + "fmt" + "io/ioutil" + "json" + "./couch" + "os" + "time" +) + +//old +type OldComment struct { + Author string + Text string + Date string +} + +type OldEntry struct { + Id string + Title string + Author string + Text string + Mtext string + Comments []OldComment +} + +//new +type Comment struct { + Author string `json:"cauthor"` + Text string `json:"ctext"` + Date int64 `json:"cdate"` +} + +type Entry struct { + Id string `json:"_id"` + Year int `json:"year"` + Month int `json:"month"` + Day int + Lang string `json:"lang"` + Title string `json:"title"` + Author string `json:"author"` + Text string `json:"text"` + Mtext string `json:"mtext"` + Comments []Comment `json:"comments"` +} + +func main() { + getAllByYear("2011", 8, 12) + getAllByYear("2012", 1, 2) +} + +func getAllByYear(year string, minm, maxm int){ + db, _ := couch.NewDatabase("127.0.0.1", "5984", "tazblog") + for i:=minm;i<=maxm;i++{ + dirList, err := ioutil.ReadDir(fmt.Sprintf("data/%s/%02d/", year, i)) + if err != nil { + fmt.Println(err.String()) + os.Exit(1) + } + for d:=len(dirList)-1; d>-1; d--{ + content, cErr := ioutil.ReadFile(fmt.Sprintf("data/%s/%02d/%s", year, i, dirList[d].Name)) + if cErr != nil { + fmt.Println(cErr) + os.Exit(1) + } + var oEntry OldEntry + jErr := json.Unmarshal(content, &oEntry) + if jErr != nil { + fmt.Println(jErr.String()) + os.Exit(1) + } + nEntry := convertEntry(oEntry, fmt.Sprintf("data/%s/%02d/%s", year, i, dirList[d].Name)) + eId, _, err := db.Insert(nEntry) + if err != nil { + fmt.Println(err.String()) + os.Exit(1) + } + fmt.Println("Inserted " + eId) + } + } +} + +func convertEntry(oEntry OldEntry, p string) Entry{ + var nEntry Entry + nComments := make([]Comment, len(oEntry.Comments)) + for i:=0;i= 300 { + b := []byte{} + r.Body.Read(b) + return r.StatusCode, os.NewError("server said: " + r.Status) + } + decoder := json.NewDecoder(r.Body) + if err = decoder.Decode(out); err != nil { + return 0, err + } + r.Body.Close() + return r.StatusCode, nil +} + +type Database struct { + Host string + Port string + Name string +} + +func (p Database) BaseURL() string { + return fmt.Sprintf("http://%s:%s", p.Host, p.Port) +} + +func (p Database) DBURL() string { + return fmt.Sprintf("%s/%s", p.BaseURL(), p.Name) +} + +// Test whether CouchDB is running (ignores Database.Name) +func (p Database) Running() bool { + u := fmt.Sprintf("%s/%s", p.BaseURL(), "_all_dbs") + s := url_to_buf(u) + if len(s) > 0 { + return true + } + return false +} + +type database_info struct { + Db_name string + // other stuff too, ignore for now +} + +// Test whether specified database exists in specified CouchDB instance +func (p Database) Exists() bool { + di := new(database_info) + if err := json.Unmarshal(url_to_buf(p.DBURL()), di); err != nil { + return false + } + if di.Db_name != p.Name { + return false + } + return true +} + +func (p Database) create_database() os.Error { + ir := response{} + if _, err := p.interact("PUT", p.DBURL(), def_hdrs, nil, &ir); err != nil { + return err + } + if !ir.Ok { + return os.NewError("Create database operation returned not-OK") + } + return nil +} + +// Deletes the given database and all documents +func (p Database) DeleteDatabase() os.Error { + ir := response{} + if _, err := p.interact("DELETE", p.DBURL(), def_hdrs, nil, &ir); err != nil { + return err + } + if !ir.Ok { + return os.NewError("Delete database operation returned not-OK") + } + return nil +} + +func NewDatabase(host, port, name string) (Database, os.Error) { + db := Database{host, port, name} + if !db.Running() { + return db, os.NewError("CouchDB not running") + } + if !db.Exists() { + if err := db.create_database(); err != nil { + return db, err + } + } + return db, nil +} + +// Strip _id and _rev from d, returning them separately if they exist +func clean_JSON(d interface{}) (json_buf []byte, id, rev string, err os.Error) { + json_buf, err = json.Marshal(d) + if err != nil { + return + } + m := map[string]interface{}{} + err = json.Unmarshal(json_buf, &m) + if err != nil { + return + } + id_rev := new(IdAndRev) + err = json.Unmarshal(json_buf, &id_rev) + if err != nil { + return + } + if _, ok := m["_id"]; ok { + id = id_rev.Id + m["_id"] = nil, false + } + if _, ok := m["_rev"]; ok { + rev = id_rev.Rev + m["_rev"] = nil, false + } + json_buf, err = json.Marshal(m) + return +} + +type response struct { + Ok bool + Id string + Rev string + Error string + Reason string +} + +// Inserts document to CouchDB, returning id and rev on success. +// Document may specify both "_id" and "_rev" fields (will overwrite existing) +// or just "_id" (will use that id, but not overwrite existing) +// or neither (will use autogenerated id) +func (p Database) Insert(d interface{}) (string, string, os.Error) { + json_buf, id, rev, err := clean_JSON(d) + if err != nil { + return "", "", err + } + if id != "" && rev != "" { + new_rev, err2 := p.Edit(d) + return id, new_rev, err2 + } else if id != "" { + return p.insert_with(json_buf, id) + } else if id == "" { + return p.insert(json_buf) + } + return "", "", os.NewError("invalid Document") +} + +// Private implementation of simple autogenerated-id insert +func (p Database) insert(json_buf []byte) (string, string, os.Error) { + ir := response{} + if _, err := p.interact("POST", p.DBURL(), def_hdrs, json_buf, &ir); err != nil { + return "", "", err + } + if !ir.Ok { + return "", "", os.NewError(fmt.Sprintf("%s: %s", ir.Error, ir.Reason)) + } + return ir.Id, ir.Rev, nil +} + +// Inserts the given document (shouldn't contain "_id" or "_rev" tagged fields) +// using the passed 'id' as the _id. Will fail if the id already exists. +func (p Database) InsertWith(d interface{}, id string) (string, string, os.Error) { + json_buf, err := json.Marshal(d) + if err != nil { + return "", "", err + } + return p.insert_with(json_buf, id) +} + +// Private implementation of insert with given id +func (p Database) insert_with(json_buf []byte, id string) (string, string, os.Error) { + u := fmt.Sprintf("%s/%s", p.DBURL(), url.QueryEscape(id)) + ir := response{} + if _, err := p.interact("PUT", u, def_hdrs, json_buf, &ir); err != nil { + return "", "", err + } + if !ir.Ok { + return "", "", os.NewError(fmt.Sprintf("%s: %s", ir.Error, ir.Reason)) + } + return ir.Id, ir.Rev, nil +} + +// Edits the given document, returning the new revision. +// d must contain "_id" and "_rev" tagged fields. +func (p Database) Edit(d interface{}) (string, os.Error) { + json_buf, err := json.Marshal(d) + if err != nil { + return "", err + } + id_rev := new(IdAndRev) + err = json.Unmarshal(json_buf, id_rev) + if err != nil { + return "", err + } + if id_rev.Id == "" { + return "", os.NewError("Id not specified in interface") + } + if id_rev.Rev == "" { + return "", os.NewError("Rev not specified in interface (try InsertWith)") + } + u := fmt.Sprintf("%s/%s", p.DBURL(), url.QueryEscape(id_rev.Id)) + ir := response{} + if _, err = p.interact("PUT", u, def_hdrs, json_buf, &ir); err != nil { + return "", err + } + return ir.Rev, nil +} + +// Edits the given document, returning the new revision. +// d should not contain "_id" or "_rev" tagged fields. If it does, they will +// be overwritten with the passed values. +func (p Database) EditWith(d interface{}, id, rev string) (string, os.Error) { + if id == "" || rev == "" { + return "", os.NewError("EditWith: must specify both id and rev") + } + json_buf, err := json.Marshal(d) + if err != nil { + return "", err + } + m := map[string]interface{}{} + err = json.Unmarshal(json_buf, &m) + if err != nil { + return "", err + } + m["_id"] = id + m["_rev"] = rev + return p.Edit(m) +} + +// Unmarshals the document matching id to the given interface, returning rev. +func (p Database) Retrieve(id string, d interface{}) (string, os.Error) { + if id == "" { + return "", os.NewError("no id specified") + } + json_buf := url_to_buf(fmt.Sprintf("%s/%s", p.DBURL(), id)) + id_rev := new(IdAndRev) + if err := json.Unmarshal(json_buf, &id_rev); err != nil { + return "", err + } + if id_rev.Id != id { + return "", os.NewError("invalid id specified") + } + return id_rev.Rev, json.Unmarshal(json_buf, d) +} + +// Deletes document given by id and rev. +func (p Database) Delete(id, rev string) os.Error { + headers := map[string][]string{ + "If-Match": []string{rev}, + } + u := fmt.Sprintf("%s/%s", p.DBURL(), id) + ir := response{} + if _, err := p.interact("DELETE", u, headers, nil, &ir); err != nil { + return err + } + if !ir.Ok { + return os.NewError(fmt.Sprintf("%s: %s", ir.Error, ir.Reason)) + } + return nil +} + +type Row struct { + Id *string +} + +type keyed_view_response struct { + Total_rows uint64 + Offset uint64 + Rows []Row +} + +// Return array of document ids as returned by the given view/options combo. +// view should be eg. "_design/my_foo/_view/my_bar" +// options should be eg. { "limit": 10, "key": "baz" } +func (p Database) QueryIds(view string, options map[string]interface{}) ([]string, os.Error) { + kvr := new(keyed_view_response) + + if err := p.Query(view, options, kvr); err != nil { + fmt.Println("Query error: " + err.String()) + return make([]string, 0), err + } + ids := make([]string, len(kvr.Rows)) + i := 0 + for _, row := range kvr.Rows { + if row.Id != nil { + ids[i] = *row.Id + i++ + } + } + return ids[:i], nil +} + +func (p Database) Query(view string, options map[string]interface{}, results interface{}) os.Error { + if view == "" { + return os.NewError("empty view") + } + + var parameters string + for k, v := range options { + switch t := v.(type) { + case string: + parameters += fmt.Sprintf(`%s=%s&`, k, url.QueryEscape(t)) + case int: + parameters += fmt.Sprintf(`%s=%d&`, k, t) + case bool: + parameters += fmt.Sprintf(`%s=%v&`, k, t) + default: + // TODO more types are supported + panic(fmt.Sprintf("unsupported value-type %T in Query", t)) + } + } + full_url := fmt.Sprintf("%s/%s?%s", p.DBURL(), view, parameters) + json_buf := url_to_buf(full_url) + + if err := json.Unmarshal(json_buf, results); err != nil { + return err + } + return nil +} diff --git a/tools/music/Makefile b/tools/music/Makefile new file mode 100644 index 0000000000..488c7eb1b0 --- /dev/null +++ b/tools/music/Makefile @@ -0,0 +1,10 @@ +all: music + +music: iTunes.8 + 8l -o music iTunes.8 + +iTunes.8: iTunes.go + 8g iTunes.go + +clean: + rm -rf *.8 music \ No newline at end of file diff --git a/tools/music/gettitle b/tools/music/gettitle new file mode 100755 index 0000000000..0bd4cc6979 --- /dev/null +++ b/tools/music/gettitle @@ -0,0 +1,4 @@ +#!/bin/bash + +osascript -e 'tell application "iTunes" to get name of current track' +osascript -e 'tell application "iTunes" to get artist of current track' diff --git a/tools/music/iTunes.8 b/tools/music/iTunes.8 new file mode 100644 index 0000000000..ac5ce58ff1 Binary files /dev/null and b/tools/music/iTunes.8 differ diff --git a/tools/music/iTunes.go b/tools/music/iTunes.go new file mode 100644 index 0000000000..5eb530f6b3 --- /dev/null +++ b/tools/music/iTunes.go @@ -0,0 +1,79 @@ +/* This program is free software. It comes without any warranty, to + * the extent permitted by applicable law. You can redistribute it + * and/or modify it under the terms of the Do What The Fuck You Want + * To Do Public License, Version 3, as published by Vincent Ambo. See + * included COPYING file for more details. */ + +package main + +import( "fmt" + "exec" + "strings" + "http" + "url" + "flag" + "os" + "time" +) + +var authkey, host, c_artist, c_title string + +func init(){ + flag.StringVar(&authkey, "key", "none", "http auth key") + flag.StringVar(&host, "host", "http://localhost:8080", "host") +} + +func main(){ + flag.Parse() + fmt.Println("Music updater launching. Update occurs once per minute.") + go updaterThread() + + var cc string + for { + fmt.Println("Type \"exit\" to quit") + fmt.Scanf("%s", &cc) + switch(cc) { + case "exit": + os.Exit(1) + default: + fmt.Println("Type \"exit\" to quit") + + } + } +} + +func updaterThread(){ + rValues := make(url.Values) + rValues.Add("artist", "") + rValues.Add("title", "") + rValues.Add("key", authkey) + + for { + title, artist := getTrack() + if (title != c_title) || (artist != c_artist) { + fmt.Println("Updating to: " + title + " - " + artist) + c_artist = artist; c_title = title + rValues.Set("artist", artist) + rValues.Set("title", title) + _, err := http.PostForm(fmt.Sprint(host + "/setsong"), rValues) + if err != nil { + fmt.Println(err.String()) + } + } + time.Sleep(60000000000) + } +} + +func getTrack() (title, artist string){ + a, err := exec.Command("./gettitle").Output() + if err != nil { + fmt.Println("err: " + err.String()) + title = "" + artist = "" + } else { + trackInfo := strings.Split(string(a), "\n") + title = trackInfo[0] + artist = trackInfo[1] + } + return +} \ No newline at end of file diff --git a/tools/music/music b/tools/music/music new file mode 100755 index 0000000000..f91b62fe8a Binary files /dev/null and b/tools/music/music differ diff --git a/tools/music/start b/tools/music/start new file mode 100755 index 0000000000..b9f1358e34 --- /dev/null +++ b/tools/music/start @@ -0,0 +1 @@ +./music -host "http://tazj.in" -key "4058ef41bbca252a7b7e675a61dbf935" -- cgit 1.4.1 From fd9dc7e706c450ae6d017a92cbb90bc2774c3d2f Mon Sep 17 00:00:00 2001 From: "\"Vincent Ambo ext:(%22)" Date: Wed, 22 Feb 2012 22:05:11 +0100 Subject: * removing binaries --- tools/music/iTunes.8 | Bin 37608 -> 0 bytes tools/music/music | Bin 2843022 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 tools/music/iTunes.8 delete mode 100755 tools/music/music diff --git a/tools/music/iTunes.8 b/tools/music/iTunes.8 deleted file mode 100644 index ac5ce58ff1..0000000000 Binary files a/tools/music/iTunes.8 and /dev/null differ diff --git a/tools/music/music b/tools/music/music deleted file mode 100755 index f91b62fe8a..0000000000 Binary files a/tools/music/music and /dev/null differ -- cgit 1.4.1 From 47c8d9a96d8532931c4415230e9385470062d907 Mon Sep 17 00:00:00 2001 From: "\"Vincent Ambo ext:(%22)" Date: Wed, 22 Feb 2012 22:07:28 +0100 Subject: * .hgignore file --- .hgignore | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .hgignore diff --git a/.hgignore b/.hgignore new file mode 100644 index 0000000000..ab263ce4cb --- /dev/null +++ b/.hgignore @@ -0,0 +1,10 @@ +syntax: glob +.DS_Store +reServe +convertdb +music +*.o +*.hi +*.esproj +*.sublime* +*.8 -- cgit 1.4.1 From 2fa129e7e3107dee64d855df4260dbc9f2188a83 Mon Sep 17 00:00:00 2001 From: "\"Vincent Ambo ext:(%22)" Date: Thu, 23 Feb 2012 03:30:14 +0100 Subject: * blog footer, language handling, emptyTest --- res/.DS_Store | Bin 6148 -> 6148 bytes res/blogstyle.css | 4 +++ src/.DS_Store | Bin 6148 -> 6148 bytes src/Blog.hs | 76 ++++++++++++++++++++++++++++++++++++++++------ src/Server.hs | 89 ++++++++++++++++++++++++++++-------------------------- 5 files changed, 116 insertions(+), 53 deletions(-) diff --git a/res/.DS_Store b/res/.DS_Store index 5008ddfcf5..6bbb484b85 100644 Binary files a/res/.DS_Store and b/res/.DS_Store differ diff --git a/res/blogstyle.css b/res/blogstyle.css index 8c065c53ce..63330a9775 100644 --- a/res/blogstyle.css +++ b/res/blogstyle.css @@ -43,6 +43,10 @@ body { text-align:center; } +.rightbox { + text-align:right; +} + .innerBox { width: 100%; margin-top: 20px; diff --git a/src/.DS_Store b/src/.DS_Store index db284a629a..570a12de90 100644 Binary files a/src/.DS_Store and b/src/.DS_Store differ diff --git a/src/Blog.hs b/src/Blog.hs index 2a62bb7680..983bae2366 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -1,33 +1,89 @@ {-# LANGUAGE OverloadedStrings, ScopedTypeVariables #-} + module Blog where +import Data.Monoid (mempty) import Text.Blaze (toValue, preEscapedString) import Text.Blaze.Html5 (Html, (!), a, form, input, p, toHtml, label) import Text.Blaze.Html5.Attributes (action, enctype, href, name, size, type_, value) import qualified Text.Blaze.Html5 as H import qualified Text.Blaze.Html5.Attributes as A -blogTemplate :: String -> String -> String -> Html -blogTemplate t h o = H.docTypeHtml $ do + + + +repoURL = ("" :: String) + +{- + +
+Proudly made with +Google Go and without PHP, Java, Perl, MySQL and Python. +
Idee zum simplen Blog von +Fefe +
Version 2.1.3  +Impressum +
+ + +
\"\"
+ +" + +-} + +blogTemplate :: String -> String -> String -> String -> String -> Html -- -> Html +blogTemplate title ctext1 ortext lang version = H.docTypeHtml $ do --add body H.head $ do - H.title $ (toHtml t) + H.title $ (toHtml title) H.link ! A.rel "alternate" ! A.type_ "application/rss+xml" ! A.title "RSS-Feed" ! A.href "/rss" H.link ! A.rel "stylesheet" ! A.type_ "text/css" ! A.href "/res/blogstyle.css" ! A.media "all" H.meta ! A.httpEquiv "content-type" ! A.content "text/html;charset=UTF-8" -{- H.style ! A.type_ "text/css" ! A.title "iOS iMessage" ! A.media "screen and (max-device-width: 1024px)" $ "#cosx{display:none;} #cios{display:block;}" -} + --H.style ! A.type_ "text/css" ! A.title "iOS iMessage" ! A.media "screen and (max-device-width: 1024px)" $ "#cosx{display:none;}" H.body $ do - H.div ! A.class_ "mainshell" $ H.div ! A.class_ "gradBox" $ H.div ! A.class_ "header" $ do + H.div ! A.class_ "mainshell" $ H.div ! A.class_ "gradBox" $ do + H.div ! A.class_ "header" $ do H.a ! A.href "/" ! A.style "text-decoration:none;color:black;font-size:x-large;font-weight:bold;" $ - (toHtml t) + (toHtml title) H.br H.span ! A.id "cosx" ! A.style "display:block;" $ H.b $ contactInfo "imessage:tazjin@me.com" - H.span ! A.id "cios" ! A.style "display:none;" $ H.b $ contactInfo "sms:tazjin@me.com" + -- H.span ! A.id "cios" ! A.style "display:none;" $ H.b $ contactInfo "sms:tazjin@me.com" + H.div ! A.class_ "myclear" $ mempty + emptyTest lang + showFooter lang version + H.div ! A.class_ "centerbox" $ + H.img ! A.src "http://getpunchd.com/img/june/idiots.png" ! A.alt "" where contactInfo (imu :: String) = do - toHtml h + toHtml ctext1 H.a ! A.href "mailto:hej@tazj.in" $ "Mail" ", " H.a ! A.href "http://twitter.com/#!/tazjin" ! A.target "_blank" $ "Twitter" - toHtml o + toHtml ortext H.a ! A.href (toValue imu) ! A.target "_blank" $ "iMessage" - "." \ No newline at end of file + "." + +emptyTest :: String -> Html +emptyTest lang = H.div ! A.class_ "innerBox" $ do + H.div ! A.class_ "innerBoxTop" $ "Test" + H.div ! A.class_ "innerBoxMiddle" $ getTestText lang + H.div ! A.class_ "myclear" $ mempty + where + getTestText "de" = toHtml ("Das ist doch schonmal was." :: String) + getTestText "en" = toHtml ("This is starting to look like something." :: String) + +showFooter :: String -> String -> Html +showFooter l v = H.div ! A.class_ "rightbox" ! A.style "text-align:right;" $ do + toHtml ("Proudly made with " :: String) + H.a ! A.href "http://haskell.org" $ "Haskell" + toHtml (", " :: String) + H.a ! A.href "http://couchdb.apache.org/" $ "CouchDB" + toHtml (" and without PHP, Java, Perl, MySQL and Python." :: String) + H.br + H.a ! A.href (toValue repoURL) $ toHtml $ ("Version " :: String) ++ v + preEscapedString " " + H.a ! A.href "/notice" $ toHtml $ noticeText l + where + noticeText :: String -> String + noticeText "en" = "site notice" + noticeText "de" = "Impressum" diff --git a/src/Server.hs b/src/Server.hs index aa41a2173d..eefc9b1e7a 100644 --- a/src/Server.hs +++ b/src/Server.hs @@ -2,76 +2,79 @@ module Main where -import Control.Monad (msum, mzero) -import Data.Data (Data, Typeable) -import Data.Monoid (mempty) -import Data.ByteString.Char8 (ByteString) -import Data.Text hiding (map, length, zip, head) -import Data.Time -import Database.CouchDB -import Happstack.Server +import Control.Monad (msum, mzero) +import Data.Data (Data, Typeable) +import Data.Monoid (mempty) +import Data.ByteString.Char8 (ByteString) +import Data.Text hiding (map, length, zip, head) +import Data.Time +import Database.CouchDB +import Happstack.Server import Text.Blaze (toValue, preEscapedString) import Text.Blaze.Html5 (Html, (!), a, form, input, p, toHtml, label) import Text.Blaze.Html5.Attributes (action, enctype, href, name, size, type_, value) import qualified Text.Blaze.Html5 as H import qualified Text.Blaze.Html5.Attributes as A -import Text.JSON.Generic +import Text.JSON.Generic -import Blog +import Blog tmpPolicy :: BodyPolicy tmpPolicy = (defaultBodyPolicy "./tmp/" 0 1000 1000) -data BlogLang = EN | DE data Comment = Comment{ - cauthor :: String, - ctext :: String, - cdate :: Integer + cauthor :: String, + ctext :: String, + cdate :: Integer } deriving (Show, Data, Typeable) data Entry = Entry{ - _id :: String, - year :: Int, - month :: Int, - day :: Int, - lang :: String, - title :: String, - author :: String, - text :: String, - mtext :: String, - comments :: [Comment] + _id :: String, + year :: Int, + month :: Int, + day :: Int, + lang :: BlogLang, + title :: String, + author :: String, + text :: String, + mtext :: String, + comments :: [Comment] } deriving (Show, Data, Typeable) +data BlogLang = EN | DE deriving (Data, Typeable) + instance Show BlogLang where - show EN = "en" - show DE = "de" + show EN = "en" + show DE = "de" --TazBlog version version = ("2.2b" :: String) main :: IO() main = do - putStrLn ("TazBlog " ++ version ++ " in Haskell starting") - simpleHTTP nullConf tazBlog + putStrLn ("TazBlog " ++ version ++ " in Haskell starting") + simpleHTTP nullConf tazBlog tazBlog :: ServerPart Response tazBlog = do - msum [ dir "en" $ blogHandler EN - , dir "de" $ blogHandler DE - , do nullDir; - ok $ showIndex DE - , do dir " " $ nullDir; - seeOther ("https://plus.google.com/115916629925754851590" :: String) (toResponse ()) - , dir "res" $ serveDirectory DisableBrowsing [] "../res" - , serveDirectory DisableBrowsing [] "../res" - ] + msum [ dir "en" $ blogHandler EN + , dir "de" $ blogHandler DE + , do nullDir + ok $ showIndex DE + , do dir " " $ nullDir + seeOther ("https://plus.google.com/115916629925754851590" :: String) (toResponse ()) + , dir "res" $ serveDirectory DisableBrowsing [] "../res" + , serveDirectory DisableBrowsing [] "../res" + ] blogHandler :: BlogLang -> ServerPart Response blogHandler lang = - msum [ path $ \(year :: Int) -> path $ \(month :: Int) -> path $ - \(day :: Int) -> path $ \(id_ :: String) -> showEntry lang year month day id_ - ] + msum [ path $ \(year :: Int) -> path $ \(month :: Int) -> path $ --single entry + \(day :: Int) -> path $ \(id_ :: String) -> showEntry lang year month day id_ + , do nullDir + ok $ showIndex lang + ] showEntry :: BlogLang -> Int -> Int -> Int -> String -> ServerPart Response showEntry EN y m d i = undefined @@ -81,8 +84,8 @@ showIndex :: BlogLang -> Response showIndex lang = toResponse $ renderBlogHeader lang renderBlogHeader :: BlogLang -> Html -renderBlogHeader DE = blogTemplate "Tazjins Blog" "Wer mich kontaktieren will: " " oder " -renderBlogHeader EN = blogTemplate "Tazjin's Blog" "Get in touch with me: " " or " +renderBlogHeader DE = blogTemplate "Tazjins Blog" "Wer mich kontaktieren will: " " oder " "de" version +renderBlogHeader EN = blogTemplate "Tazjin's Blog" "Get in touch with me: " " or " "en" version -- http://tazj.in/2012/02/10.155234 @@ -93,6 +96,6 @@ latestENView = "function(doc){ if(doc.lang == \"en\"){ emit([doc.year, doc.month latestDE = ViewMap "latestDE" latestDEView latestEN = ViewMap "latestEN" latestENView -setupBlogViews :: IO () -- taking *reservations* DB name as parameter because we'll have multiple stores +setupBlogViews :: IO () setupBlogViews = runCouchDB' $ newView "tazblog" "entries" [latestDE, latestEN] -- cgit 1.4.1 From 47dbfe900e57738dc44c4bfe06c624e725102a03 Mon Sep 17 00:00:00 2001 From: "\"Vincent Ambo ext:(%22)" Date: Thu, 23 Feb 2012 03:32:40 +0100 Subject: * removing .DS_Store files --- res/.DS_Store | Bin 6148 -> 0 bytes src/.DS_Store | Bin 6148 -> 0 bytes tools/.DS_Store | Bin 6148 -> 0 bytes tools/convertdb/.DS_Store | Bin 6148 -> 0 bytes 4 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 res/.DS_Store delete mode 100644 src/.DS_Store delete mode 100644 tools/.DS_Store delete mode 100644 tools/convertdb/.DS_Store diff --git a/res/.DS_Store b/res/.DS_Store deleted file mode 100644 index 6bbb484b85..0000000000 Binary files a/res/.DS_Store and /dev/null differ diff --git a/src/.DS_Store b/src/.DS_Store deleted file mode 100644 index 570a12de90..0000000000 Binary files a/src/.DS_Store and /dev/null differ diff --git a/tools/.DS_Store b/tools/.DS_Store deleted file mode 100644 index 714b6567ae..0000000000 Binary files a/tools/.DS_Store and /dev/null differ diff --git a/tools/convertdb/.DS_Store b/tools/convertdb/.DS_Store deleted file mode 100644 index 5de3023c9b..0000000000 Binary files a/tools/convertdb/.DS_Store and /dev/null differ -- cgit 1.4.1 From a4119e1cfd3f599cf67012535a5a55bcbf4800c6 Mon Sep 17 00:00:00 2001 From: "\"Vincent Ambo ext:(%22)" Date: Thu, 23 Feb 2012 13:20:29 +0100 Subject: * displaying blog entries * changed convertDB for BlogLang JSON representation --- src/Blog.hs | 86 +++++++++++++++++++++++++++++--------------- src/Server.hs | 61 +++++++++++++++---------------- tools/convertdb/convertdb.go | 15 +++----- 3 files changed, 91 insertions(+), 71 deletions(-) diff --git a/src/Blog.hs b/src/Blog.hs index 983bae2366..bb74cbb16b 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -1,7 +1,9 @@ -{-# LANGUAGE OverloadedStrings, ScopedTypeVariables #-} +{-# LANGUAGE OverloadedStrings, ScopedTypeVariables, DeriveDataTypeable #-} module Blog where +--import Control.Monad(when) +import Data.Data (Data, Typeable) import Data.Monoid (mempty) import Text.Blaze (toValue, preEscapedString) import Text.Blaze.Html5 (Html, (!), a, form, input, p, toHtml, label) @@ -10,30 +12,37 @@ import qualified Text.Blaze.Html5 as H import qualified Text.Blaze.Html5.Attributes as A +data Comment = Comment{ + cauthor :: String, + ctext :: String, + cdate :: Integer +} deriving (Show, Data, Typeable) +data Entry = Entry{ + _id :: String, + year :: Int, + month :: Int, + day :: Int, + lang :: BlogLang, + title :: String, + author :: String, + text :: String, + mtext :: String, + comments :: [Comment] +} deriving (Show, Data, Typeable) -repoURL = ("" :: String) +data BlogError = NoEntries | NotFound | DBError -{- - -
-Proudly made with -Google Go and without PHP, Java, Perl, MySQL and Python. -
Idee zum simplen Blog von -Fefe -
Version 2.1.3  -Impressum -
- - -
\"\"
- -" +data BlogLang = EN | DE deriving (Data, Typeable) --} +instance Show BlogLang where + show EN = "en" + show DE = "de" -blogTemplate :: String -> String -> String -> String -> String -> Html -- -> Html -blogTemplate title ctext1 ortext lang version = H.docTypeHtml $ do --add body +repoURL = ("https://bitbucket.org/tazjin/tazblog-haskell" :: String) + +blogTemplate :: String -> String -> String -> String -> BlogLang -> Html -> Html +blogTemplate title ctext1 ortext version lang body = H.docTypeHtml $ do --add body H.head $ do H.title $ (toHtml title) H.link ! A.rel "alternate" ! A.type_ "application/rss+xml" ! A.title "RSS-Feed" ! A.href "/rss" @@ -49,7 +58,7 @@ blogTemplate title ctext1 ortext lang version = H.docTypeHtml $ do --add body H.span ! A.id "cosx" ! A.style "display:block;" $ H.b $ contactInfo "imessage:tazjin@me.com" -- H.span ! A.id "cios" ! A.style "display:none;" $ H.b $ contactInfo "sms:tazjin@me.com" H.div ! A.class_ "myclear" $ mempty - emptyTest lang + body showFooter lang version H.div ! A.class_ "centerbox" $ H.img ! A.src "http://getpunchd.com/img/june/idiots.png" ! A.alt "" @@ -63,16 +72,32 @@ blogTemplate title ctext1 ortext lang version = H.docTypeHtml $ do --add body H.a ! A.href (toValue imu) ! A.target "_blank" $ "iMessage" "." -emptyTest :: String -> Html +renderEntry :: Entry -> Html +renderEntry entry = H.div ! A.class_ "innerBox" $ do + H.div ! A.class_ "innerBoxTop" $ toHtml $ title entry + H.div ! A.class_ "innerBoxMiddle" $ do + H.article $ H.ul $ H.li $ do + preEscapedString $ text entry + preEscapedString $ mtext entry + H.div ! A.class_ "innerBoxComments" $ do + H.div ! A.name "cHead" ! A.style "font-size:large;font-weight:bold;" $ toHtml cHead + H.ul $ H.li $ toHtml noC + where + getTexts :: BlogLang -> (String, String) + getTexts EN = ("Comments:", " No comments yet") + getTexts DE = ("Kommentare:", " Keine Kommentare") + (cHead,noC) = getTexts (lang entry) + +emptyTest :: BlogLang -> Html emptyTest lang = H.div ! A.class_ "innerBox" $ do H.div ! A.class_ "innerBoxTop" $ "Test" H.div ! A.class_ "innerBoxMiddle" $ getTestText lang H.div ! A.class_ "myclear" $ mempty where - getTestText "de" = toHtml ("Das ist doch schonmal was." :: String) - getTestText "en" = toHtml ("This is starting to look like something." :: String) + getTestText DE = toHtml ("Das ist doch schonmal was." :: String) + getTestText EN = toHtml ("This is starting to look like something." :: String) -showFooter :: String -> String -> Html +showFooter :: BlogLang -> String -> Html showFooter l v = H.div ! A.class_ "rightbox" ! A.style "text-align:right;" $ do toHtml ("Proudly made with " :: String) H.a ! A.href "http://haskell.org" $ "Haskell" @@ -84,6 +109,11 @@ showFooter l v = H.div ! A.class_ "rightbox" ! A.style "text-align:right;" $ do preEscapedString " " H.a ! A.href "/notice" $ toHtml $ noticeText l where - noticeText :: String -> String - noticeText "en" = "site notice" - noticeText "de" = "Impressum" + noticeText :: BlogLang -> String + noticeText EN = "site notice" + noticeText DE = "Impressum" + + +-- Error pages +showError :: BlogError -> Html +showError _ = undefined diff --git a/src/Server.hs b/src/Server.hs index eefc9b1e7a..310c2ea122 100644 --- a/src/Server.hs +++ b/src/Server.hs @@ -1,15 +1,15 @@ -{-# LANGUAGE OverloadedStrings, ScopedTypeVariables, DeriveDataTypeable #-} +{-# LANGUAGE OverloadedStrings, ScopedTypeVariables #-} module Main where import Control.Monad (msum, mzero) -import Data.Data (Data, Typeable) import Data.Monoid (mempty) import Data.ByteString.Char8 (ByteString) import Data.Text hiding (map, length, zip, head) import Data.Time import Database.CouchDB import Happstack.Server +import Network.CGI (liftIO) import Text.Blaze (toValue, preEscapedString) import Text.Blaze.Html5 (Html, (!), a, form, input, p, toHtml, label) import Text.Blaze.Html5.Attributes (action, enctype, href, name, size, type_, value) @@ -23,31 +23,6 @@ tmpPolicy :: BodyPolicy tmpPolicy = (defaultBodyPolicy "./tmp/" 0 1000 1000) -data Comment = Comment{ - cauthor :: String, - ctext :: String, - cdate :: Integer -} deriving (Show, Data, Typeable) - -data Entry = Entry{ - _id :: String, - year :: Int, - month :: Int, - day :: Int, - lang :: BlogLang, - title :: String, - author :: String, - text :: String, - mtext :: String, - comments :: [Comment] -} deriving (Show, Data, Typeable) - -data BlogLang = EN | DE deriving (Data, Typeable) - -instance Show BlogLang where - show EN = "en" - show DE = "de" - --TazBlog version version = ("2.2b" :: String) @@ -71,24 +46,44 @@ tazBlog = do blogHandler :: BlogLang -> ServerPart Response blogHandler lang = msum [ path $ \(year :: Int) -> path $ \(month :: Int) -> path $ --single entry - \(day :: Int) -> path $ \(id_ :: String) -> showEntry lang year month day id_ + \(day :: Int) -> path $ \(id_ :: String) -> showEntry year month day id_ , do nullDir ok $ showIndex lang ] -showEntry :: BlogLang -> Int -> Int -> Int -> String -> ServerPart Response -showEntry EN y m d i = undefined -showEntry DE y m d i = undefined +showEntry :: Int -> Int -> Int -> String -> ServerPart Response +showEntry y m d i = do + entryJS <- liftIO $ runCouchDB' $ getDoc (db "tazblog") (doc i) + let entry = maybeDoc entryJS + ok $ tryEntry entry + +tryEntry :: Maybe Entry -> Response +tryEntry Nothing = toResponse $ showError NotFound +tryEntry (Just entry) = toResponse $ renderBlog eLang $ renderEntry entry + where + eLang = lang entry showIndex :: BlogLang -> Response showIndex lang = toResponse $ renderBlogHeader lang +renderBlog :: BlogLang -> Html -> Html +renderBlog DE body = blogTemplate "Tazjins Blog" "Wer mich kontaktieren will: " " oder " version DE body +renderBlog EN body = blogTemplate "Tazjin's Blog" "Get in touch with me: " " or " version EN body + renderBlogHeader :: BlogLang -> Html -renderBlogHeader DE = blogTemplate "Tazjins Blog" "Wer mich kontaktieren will: " " oder " "de" version -renderBlogHeader EN = blogTemplate "Tazjin's Blog" "Get in touch with me: " " or " "en" version +renderBlogHeader DE = blogTemplate "Tazjins Blog" "Wer mich kontaktieren will: " " oder " version DE (emptyTest DE) +renderBlogHeader EN = blogTemplate "Tazjin's Blog" "Get in touch with me: " " or " version EN (emptyTest EN) -- http://tazj.in/2012/02/10.155234 +-- CouchDB functions +maybeDoc :: Data a => Maybe (Doc, Rev, JSValue) -> Maybe a +maybeDoc (Just(_,_,v)) = Just( stripResult $ fromJSON v) +maybeDoc Nothing = Nothing + +stripResult :: Result a -> a +stripResult (Ok z) = z +stripResult (Error s) = error $ "JSON error: " ++ s -- CouchDB View Setup latestDEView = "function(doc){ if(doc.lang == \"de\"){ emit([doc.year, doc.month, doc.day, doc.id_], doc); } }" latestENView = "function(doc){ if(doc.lang == \"en\"){ emit([doc.year, doc.month, doc.day, doc.id_]], doc); } }" diff --git a/tools/convertdb/convertdb.go b/tools/convertdb/convertdb.go index adef31910b..f7b94176b4 100644 --- a/tools/convertdb/convertdb.go +++ b/tools/convertdb/convertdb.go @@ -26,7 +26,7 @@ type OldEntry struct { Comments []OldComment } -//new +//old type Comment struct { Author string `json:"cauthor"` Text string `json:"ctext"` @@ -37,7 +37,7 @@ type Entry struct { Id string `json:"_id"` Year int `json:"year"` Month int `json:"month"` - Day int + Day int `json:"day"` Lang string `json:"lang"` Title string `json:"title"` Author string `json:"author"` @@ -100,17 +100,12 @@ func convertEntry(oEntry OldEntry, p string) Entry{ nEntry.Mtext = oEntry.Mtext nEntry.Text = oEntry.Text nEntry.Comments = nComments - nEntry.Lang = "de" + nEntry.Lang = "DE" return nEntry } -//http://tazj.in/2012/02/10.155234 -func parseEntryTime(year, month, day int, ids string) string { - x := fmt.Sprintf() -} - -func parseDumbTime(Year, Month, Day int, ) int64 { +func parseDumbTime(ct string) int64 { x, err := time.Parse("[Am 02.01.2006 um 15:04 Uhr]", ct) if err != nil { fmt.Println(err.String()) @@ -118,4 +113,4 @@ func parseDumbTime(Year, Month, Day int, ) int64 { } return x.Seconds() -} +} \ No newline at end of file -- cgit 1.4.1 From d04d693eb9047d0ce10ae1244f4de1678d0d117a Mon Sep 17 00:00:00 2001 From: "\"Vincent Ambo ext:(%22)" Date: Thu, 23 Feb 2012 14:46:51 +0100 Subject: * comment parsing --- src/Blog.hs | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/src/Blog.hs b/src/Blog.hs index bb74cbb16b..1caa021d54 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -5,6 +5,8 @@ module Blog where --import Control.Monad(when) import Data.Data (Data, Typeable) import Data.Monoid (mempty) +import Data.Time +import System.Locale (defaultTimeLocale) import Text.Blaze (toValue, preEscapedString) import Text.Blaze.Html5 (Html, (!), a, form, input, p, toHtml, label) import Text.Blaze.Html5.Attributes (action, enctype, href, name, size, type_, value) @@ -80,13 +82,28 @@ renderEntry entry = H.div ! A.class_ "innerBox" $ do preEscapedString $ text entry preEscapedString $ mtext entry H.div ! A.class_ "innerBoxComments" $ do - H.div ! A.name "cHead" ! A.style "font-size:large;font-weight:bold;" $ toHtml cHead - H.ul $ H.li $ toHtml noC + H.div ! A.name "cHead" ! A.style "font-size:large;font-weight:bold;" $ toHtml $ cHead (lang entry) + H.ul $ renderComments (comments entry) (lang entry) where - getTexts :: BlogLang -> (String, String) - getTexts EN = ("Comments:", " No comments yet") - getTexts DE = ("Kommentare:", " Keine Kommentare") - (cHead,noC) = getTexts (lang entry) + cHead EN = ("Comments:" :: String) + cHead DE = ("Kommentare:" :: String) + +renderComments :: [Comment] -> BlogLang -> Html +renderComments [] DE = H.li $ toHtml (" Keine Kommentare" :: String) +renderComments [] EN = H.li $ toHtml (" No comments yet" :: String) +renderComments comments _ = sequence_ $ map showComment comments + where + showComment :: Comment -> Html + showComment c = H.li $ do + H.a ! A.name (toValue $ cdate c) ! A.href (toValue $ "#" ++ show c) $ + H.i $ toHtml $ (cauthor c ++ ": ") + preEscapedString $ ctext c + getTime :: Integer -> Maybe UTCTime + getTime = parseTime defaultTimeLocale "%s" (show ) + showTime (Just t) = formatTime defaultTimeLocale "[Am %d.%m.%y um %H:%M Uhr]" t + showTime Nothing = "???" -- this can not happen?? + +--[Am %d.%m.%y um %H:%M Uhr] emptyTest :: BlogLang -> Html emptyTest lang = H.div ! A.class_ "innerBox" $ do -- cgit 1.4.1 From 066762051abe5739e956aeb5f369c58c02703010 Mon Sep 17 00:00:00 2001 From: "\"Vincent Ambo ext:(%22)" Date: Fri, 24 Feb 2012 00:03:16 +0100 Subject: * rendering comments --- res/blogstyle.css | 9 +++++++++ src/Blog.hs | 13 ++++++++----- src/Server.hs | 4 ++-- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/res/blogstyle.css b/res/blogstyle.css index 63330a9775..5a27f55093 100644 --- a/res/blogstyle.css +++ b/res/blogstyle.css @@ -52,6 +52,15 @@ body { margin-top: 20px; } +.tt { + font-family: "courier new",courier,monospace; + font-size: 13px; +} + +.cl { + text-decoration:none;color:black; +} + .innerBoxTop { height: 28px; color: #000000; diff --git a/src/Blog.hs b/src/Blog.hs index 1caa021d54..80d6c45871 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -91,17 +91,20 @@ renderEntry entry = H.div ! A.class_ "innerBox" $ do renderComments :: [Comment] -> BlogLang -> Html renderComments [] DE = H.li $ toHtml (" Keine Kommentare" :: String) renderComments [] EN = H.li $ toHtml (" No comments yet" :: String) -renderComments comments _ = sequence_ $ map showComment comments +renderComments comments lang = sequence_ $ map showComment comments where showComment :: Comment -> Html showComment c = H.li $ do - H.a ! A.name (toValue $ cdate c) ! A.href (toValue $ "#" ++ show c) $ + H.a ! A.name (toValue $ cdate c) ! A.href (toValue $ "#" ++ (show $ cdate c)) ! A.class_ "cl" $ H.i $ toHtml $ (cauthor c ++ ": ") preEscapedString $ ctext c + H.p ! A.class_ "tt" $ toHtml (timeString $ cdate c) getTime :: Integer -> Maybe UTCTime - getTime = parseTime defaultTimeLocale "%s" (show ) - showTime (Just t) = formatTime defaultTimeLocale "[Am %d.%m.%y um %H:%M Uhr]" t - showTime Nothing = "???" -- this can not happen?? + getTime t = parseTime defaultTimeLocale "%s" (show t) + showTime DE (Just t) = formatTime defaultTimeLocale "[Am %d.%m.%y um %H:%M Uhr]" t + showTime EN (Just t) = formatTime defaultTimeLocale "[On %D at %H:%M Uhr]" t + showTime _ Nothing = "[???]" -- this can not happen?? + timeString = (showTime lang) . getTime --[Am %d.%m.%y um %H:%M Uhr] diff --git a/src/Server.hs b/src/Server.hs index 310c2ea122..a95b72cd7b 100644 --- a/src/Server.hs +++ b/src/Server.hs @@ -85,8 +85,8 @@ stripResult :: Result a -> a stripResult (Ok z) = z stripResult (Error s) = error $ "JSON error: " ++ s -- CouchDB View Setup -latestDEView = "function(doc){ if(doc.lang == \"de\"){ emit([doc.year, doc.month, doc.day, doc.id_], doc); } }" -latestENView = "function(doc){ if(doc.lang == \"en\"){ emit([doc.year, doc.month, doc.day, doc.id_]], doc); } }" +latestDEView = "function(doc){ if(doc.lang == \"DE\"){ emit([doc.year, doc.month, doc.day, doc.id_], doc); } }" +latestENView = "function(doc){ if(doc.lang == \"EN\"){ emit([doc.year, doc.month, doc.day, doc.id_]], doc); } }" latestDE = ViewMap "latestDE" latestDEView latestEN = ViewMap "latestEN" latestENView -- cgit 1.4.1 From a29a34d41f3101344b46f51a8e5dc5afecf82218 Mon Sep 17 00:00:00 2001 From: "\"Vincent Ambo ext:(%22)" Date: Fri, 24 Feb 2012 05:20:36 +0100 Subject: * I fixed the front page --- res/blogstyle.css | 3 ++- src/Blog.hs | 21 ++++++++++++++++++--- src/Server.hs | 35 +++++++++++++++++++++++++---------- 3 files changed, 45 insertions(+), 14 deletions(-) diff --git a/res/blogstyle.css b/res/blogstyle.css index 5a27f55093..d830ee2e23 100644 --- a/res/blogstyle.css +++ b/res/blogstyle.css @@ -37,6 +37,7 @@ body { .myclear { clear: both; + height: 20px; } .centerbox { @@ -101,7 +102,7 @@ body { -webkit-border-bottom-right-radius: 6px; border-top: 0px hidden; background-color: #FFFFFF; - min-height: 200px; + min-height: 500px; height: auto; padding-top: 21px; padding-right: 2px; diff --git a/src/Blog.hs b/src/Blog.hs index 80d6c45871..e22a8d9efe 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -4,6 +4,7 @@ module Blog where --import Control.Monad(when) import Data.Data (Data, Typeable) +import Data.List (intersperse) import Data.Monoid (mempty) import Data.Time import System.Locale (defaultTimeLocale) @@ -61,6 +62,7 @@ blogTemplate title ctext1 ortext version lang body = H.docTypeHtml $ do --add bo -- H.span ! A.id "cios" ! A.style "display:none;" $ H.b $ contactInfo "sms:tazjin@me.com" H.div ! A.class_ "myclear" $ mempty body + H.div ! A.class_ "myclear" $ mempty showFooter lang version H.div ! A.class_ "centerbox" $ H.img ! A.src "http://getpunchd.com/img/june/idiots.png" ! A.alt "" @@ -74,6 +76,21 @@ blogTemplate title ctext1 ortext version lang body = H.docTypeHtml $ do --add bo H.a ! A.href (toValue imu) ! A.target "_blank" $ "iMessage" "." +renderEntries :: [Entry] -> Int -> String-> Html +renderEntries entries num topText = H.div ! A.class_ "innerBox" $ do + H.div ! A.class_ "innerBoxTop" $ toHtml topText + H.div ! A.class_ "innerBoxMiddle" $ do + H.ul $ + sequence_ $ take num $ reverse $ map showEntry entries + where + showEntry :: Entry -> Html + showEntry e = H.li $ do + entryLink e + preEscapedString $ " " ++ (text e) ++ "
 
" + entryLink e = H.a ! A.href (toValue $ concat $ intersperse "/" $ linkElems e) $ + toHtml ("[" ++ show(length $ comments e) ++ "]") + linkElems e = [show(lang e), show(year e), show(month e), show(day e), _id e] + renderEntry :: Entry -> Html renderEntry entry = H.div ! A.class_ "innerBox" $ do H.div ! A.class_ "innerBoxTop" $ toHtml $ title entry @@ -102,12 +119,10 @@ renderComments comments lang = sequence_ $ map showComment comments getTime :: Integer -> Maybe UTCTime getTime t = parseTime defaultTimeLocale "%s" (show t) showTime DE (Just t) = formatTime defaultTimeLocale "[Am %d.%m.%y um %H:%M Uhr]" t - showTime EN (Just t) = formatTime defaultTimeLocale "[On %D at %H:%M Uhr]" t + showTime EN (Just t) = formatTime defaultTimeLocale "[On %D at %H:%M]" t showTime _ Nothing = "[???]" -- this can not happen?? timeString = (showTime lang) . getTime ---[Am %d.%m.%y um %H:%M Uhr] - emptyTest :: BlogLang -> Html emptyTest lang = H.div ! A.class_ "innerBox" $ do H.div ! A.class_ "innerBoxTop" $ "Test" diff --git a/src/Server.hs b/src/Server.hs index a95b72cd7b..8b513595e0 100644 --- a/src/Server.hs +++ b/src/Server.hs @@ -36,7 +36,7 @@ tazBlog = do msum [ dir "en" $ blogHandler EN , dir "de" $ blogHandler DE , do nullDir - ok $ showIndex DE + showIndex DE , do dir " " $ nullDir seeOther ("https://plus.google.com/115916629925754851590" :: String) (toResponse ()) , dir "res" $ serveDirectory DisableBrowsing [] "../res" @@ -48,7 +48,7 @@ blogHandler lang = msum [ path $ \(year :: Int) -> path $ \(month :: Int) -> path $ --single entry \(day :: Int) -> path $ \(id_ :: String) -> showEntry year month day id_ , do nullDir - ok $ showIndex lang + showIndex lang ] showEntry :: Int -> Int -> Int -> String -> ServerPart Response @@ -63,20 +63,35 @@ tryEntry (Just entry) = toResponse $ renderBlog eLang $ renderEntry entry where eLang = lang entry -showIndex :: BlogLang -> Response -showIndex lang = toResponse $ renderBlogHeader lang +showIndex :: BlogLang -> ServerPart Response +showIndex lang = do + entries <- getLatest lang [] + ok $ toResponse $ renderBlog lang $ renderEntries entries 6 (topText lang) + where + topText EN = "Latest entries" + topText DE = "Aktuelle Einträge" + renderBlog :: BlogLang -> Html -> Html renderBlog DE body = blogTemplate "Tazjins Blog" "Wer mich kontaktieren will: " " oder " version DE body renderBlog EN body = blogTemplate "Tazjin's Blog" "Get in touch with me: " " or " version EN body -renderBlogHeader :: BlogLang -> Html -renderBlogHeader DE = blogTemplate "Tazjins Blog" "Wer mich kontaktieren will: " " oder " version DE (emptyTest DE) -renderBlogHeader EN = blogTemplate "Tazjin's Blog" "Get in touch with me: " " or " version EN (emptyTest EN) - -- http://tazj.in/2012/02/10.155234 -- CouchDB functions +getLatest :: BlogLang -> [(String, JSValue)] -> ServerPart [Entry] +getLatest lang arg = do + queryResult <- queryDB view arg + let entries = map (stripResult . fromJSON . snd) queryResult + return entries + where + view = case lang of + EN -> "latestEN" + DE -> "latestDE" + +queryDB :: JSON a => String -> [(String, JSValue)] -> ServerPart [(Doc, a)] +queryDB view arg = liftIO $ runCouchDB' $ queryView (db "tazblog") (doc "entries") (doc view) arg + maybeDoc :: Data a => Maybe (Doc, Rev, JSValue) -> Maybe a maybeDoc (Just(_,_,v)) = Just( stripResult $ fromJSON v) maybeDoc Nothing = Nothing @@ -85,8 +100,8 @@ stripResult :: Result a -> a stripResult (Ok z) = z stripResult (Error s) = error $ "JSON error: " ++ s -- CouchDB View Setup -latestDEView = "function(doc){ if(doc.lang == \"DE\"){ emit([doc.year, doc.month, doc.day, doc.id_], doc); } }" -latestENView = "function(doc){ if(doc.lang == \"EN\"){ emit([doc.year, doc.month, doc.day, doc.id_]], doc); } }" +latestDEView = "function(doc){ if(doc.lang == 'DE'){ emit([doc.year, doc.month, doc.day, doc.id_], doc); } }" +latestENView = "function(doc){ if(doc.lang == 'EN'){ emit([doc.year, doc.month, doc.day, doc.id_], doc); } }" latestDE = ViewMap "latestDE" latestDEView latestEN = ViewMap "latestEN" latestENView -- cgit 1.4.1 From 35a5557e17aed86b4a655b4d4e6fe25d1466fd86 Mon Sep 17 00:00:00 2001 From: "\"Vincent Ambo ext:(%22)" Date: Fri, 24 Feb 2012 16:06:33 +0100 Subject: * localization moved to Locales.hs --- src/Blog.hs | 51 ++++++++++++--------------------------------------- src/Locales.hs | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/Server.hs | 15 ++++----------- 3 files changed, 73 insertions(+), 50 deletions(-) create mode 100644 src/Locales.hs diff --git a/src/Blog.hs b/src/Blog.hs index e22a8d9efe..f14b5df5ec 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -2,7 +2,6 @@ module Blog where ---import Control.Monad(when) import Data.Data (Data, Typeable) import Data.List (intersperse) import Data.Monoid (mempty) @@ -14,6 +13,7 @@ import Text.Blaze.Html5.Attributes (action, enctype, href, name, size, import qualified Text.Blaze.Html5 as H import qualified Text.Blaze.Html5.Attributes as A +import Locales data Comment = Comment{ cauthor :: String, @@ -36,18 +36,10 @@ data Entry = Entry{ data BlogError = NoEntries | NotFound | DBError -data BlogLang = EN | DE deriving (Data, Typeable) - -instance Show BlogLang where - show EN = "en" - show DE = "de" - -repoURL = ("https://bitbucket.org/tazjin/tazblog-haskell" :: String) - -blogTemplate :: String -> String -> String -> String -> BlogLang -> Html -> Html -blogTemplate title ctext1 ortext version lang body = H.docTypeHtml $ do --add body +blogTemplate :: BlogLang -> Html -> Html +blogTemplate lang body = H.docTypeHtml $ do --add body H.head $ do - H.title $ (toHtml title) + H.title $ (toHtml $ blogTitle lang) H.link ! A.rel "alternate" ! A.type_ "application/rss+xml" ! A.title "RSS-Feed" ! A.href "/rss" H.link ! A.rel "stylesheet" ! A.type_ "text/css" ! A.href "/res/blogstyle.css" ! A.media "all" H.meta ! A.httpEquiv "content-type" ! A.content "text/html;charset=UTF-8" @@ -56,9 +48,9 @@ blogTemplate title ctext1 ortext version lang body = H.docTypeHtml $ do --add bo H.div ! A.class_ "mainshell" $ H.div ! A.class_ "gradBox" $ do H.div ! A.class_ "header" $ do H.a ! A.href "/" ! A.style "text-decoration:none;color:black;font-size:x-large;font-weight:bold;" $ - (toHtml title) + (toHtml $ blogTitle lang) H.br - H.span ! A.id "cosx" ! A.style "display:block;" $ H.b $ contactInfo "imessage:tazjin@me.com" + H.span ! A.id "cosx" ! A.style "display:block;" $ H.b $ contactInfo iMessage -- H.span ! A.id "cios" ! A.style "display:none;" $ H.b $ contactInfo "sms:tazjin@me.com" H.div ! A.class_ "myclear" $ mempty body @@ -68,11 +60,11 @@ blogTemplate title ctext1 ortext version lang body = H.docTypeHtml $ do --add bo H.img ! A.src "http://getpunchd.com/img/june/idiots.png" ! A.alt "" where contactInfo (imu :: String) = do - toHtml ctext1 - H.a ! A.href "mailto:hej@tazj.in" $ "Mail" + toHtml $ contactText lang + H.a ! A.href (toValue mailTo) $ "Mail" ", " - H.a ! A.href "http://twitter.com/#!/tazjin" ! A.target "_blank" $ "Twitter" - toHtml ortext + H.a ! A.href (toValue twitter) ! A.target "_blank" $ "Twitter" + toHtml $ orString lang H.a ! A.href (toValue imu) ! A.target "_blank" $ "iMessage" "." @@ -101,13 +93,9 @@ renderEntry entry = H.div ! A.class_ "innerBox" $ do H.div ! A.class_ "innerBoxComments" $ do H.div ! A.name "cHead" ! A.style "font-size:large;font-weight:bold;" $ toHtml $ cHead (lang entry) H.ul $ renderComments (comments entry) (lang entry) - where - cHead EN = ("Comments:" :: String) - cHead DE = ("Kommentare:" :: String) renderComments :: [Comment] -> BlogLang -> Html -renderComments [] DE = H.li $ toHtml (" Keine Kommentare" :: String) -renderComments [] EN = H.li $ toHtml (" No comments yet" :: String) +renderComments [] lang = H.li $ toHtml $ noComments lang renderComments comments lang = sequence_ $ map showComment comments where showComment :: Comment -> Html @@ -118,20 +106,10 @@ renderComments comments lang = sequence_ $ map showComment comments H.p ! A.class_ "tt" $ toHtml (timeString $ cdate c) getTime :: Integer -> Maybe UTCTime getTime t = parseTime defaultTimeLocale "%s" (show t) - showTime DE (Just t) = formatTime defaultTimeLocale "[Am %d.%m.%y um %H:%M Uhr]" t - showTime EN (Just t) = formatTime defaultTimeLocale "[On %D at %H:%M]" t + showTime lang (Just t) = formatTime defaultTimeLocale (cTimeFormat lang) t showTime _ Nothing = "[???]" -- this can not happen?? timeString = (showTime lang) . getTime -emptyTest :: BlogLang -> Html -emptyTest lang = H.div ! A.class_ "innerBox" $ do - H.div ! A.class_ "innerBoxTop" $ "Test" - H.div ! A.class_ "innerBoxMiddle" $ getTestText lang - H.div ! A.class_ "myclear" $ mempty - where - getTestText DE = toHtml ("Das ist doch schonmal was." :: String) - getTestText EN = toHtml ("This is starting to look like something." :: String) - showFooter :: BlogLang -> String -> Html showFooter l v = H.div ! A.class_ "rightbox" ! A.style "text-align:right;" $ do toHtml ("Proudly made with " :: String) @@ -143,11 +121,6 @@ showFooter l v = H.div ! A.class_ "rightbox" ! A.style "text-align:right;" $ do H.a ! A.href (toValue repoURL) $ toHtml $ ("Version " :: String) ++ v preEscapedString " " H.a ! A.href "/notice" $ toHtml $ noticeText l - where - noticeText :: BlogLang -> String - noticeText EN = "site notice" - noticeText DE = "Impressum" - -- Error pages showError :: BlogError -> Html diff --git a/src/Locales.hs b/src/Locales.hs new file mode 100644 index 0000000000..266f4e752d --- /dev/null +++ b/src/Locales.hs @@ -0,0 +1,57 @@ +{-# LANGUAGE ScopedTypeVariables, DeriveDataTypeable #-} + +module Locales where + +import Data.Data (Data, Typeable) + +{- to add a language simply define it's abbreviation and show instance then + - translate the appropriate strings and add CouchDB views in Server.hs -} + +data BlogLang = EN | DE deriving (Data, Typeable) + +instance Show BlogLang where + show EN = "en" + show DE = "de" + +version = ("2.2b" :: String) + +allLang = [EN, DE] + +blogTitle DE = "Tazjins Blog" +blogTitle EN = "Tazjin's Blog" + +-- index site headline +topText DE = "Aktuelle Einträge" +topText EN = "Latest entries" + +-- contact information +contactText DE = "Wer mich kontaktieren will: " +contactText EN = "Get in touch with me: " + +orString DE = " oder " +orString EN = " or " + +-- footer +noticeText EN = "site notice" +noticeText DE = "Impressum" + +-- comments +noComments DE = " Keine Kommentare" +noComments EN = " No comments yet" + +cHead DE = "Kommentare:" +cHead EN = "Comments:" + +cTimeFormat DE = "[Am %d.%m.%y um %H:%M Uhr]" +cTimeFormat EN = "[On %D at %H:%M]" + +-- right side text (this is inserted AS IS. Escape HTML!) +rightText DE = "English version available here" +rightText EN = "Deutsche Version hier verfügbar" + +-- static information +repoURL = "https://bitbucket.org/tazjin/tazblog-haskell" +mailTo = "mailto:hej@tazj.in" +twitter = "http://twitter.com/#!/tazjin" +iMessage = "imessage:tazjin@me.com" +iMessage' = "sms:tazjin@me.com" \ No newline at end of file diff --git a/src/Server.hs b/src/Server.hs index 8b513595e0..764d3c9055 100644 --- a/src/Server.hs +++ b/src/Server.hs @@ -18,14 +18,11 @@ import qualified Text.Blaze.Html5.Attributes as A import Text.JSON.Generic import Blog +import Locales tmpPolicy :: BodyPolicy tmpPolicy = (defaultBodyPolicy "./tmp/" 0 1000 1000) - ---TazBlog version -version = ("2.2b" :: String) - main :: IO() main = do putStrLn ("TazBlog " ++ version ++ " in Haskell starting") @@ -33,8 +30,8 @@ main = do tazBlog :: ServerPart Response tazBlog = do - msum [ dir "en" $ blogHandler EN - , dir "de" $ blogHandler DE + msum [ dir (show DE) $ blogHandler DE + , dir (show EN) $ blogHandler EN , do nullDir showIndex DE , do dir " " $ nullDir @@ -68,13 +65,9 @@ showIndex lang = do entries <- getLatest lang [] ok $ toResponse $ renderBlog lang $ renderEntries entries 6 (topText lang) where - topText EN = "Latest entries" - topText DE = "Aktuelle Einträge" - renderBlog :: BlogLang -> Html -> Html -renderBlog DE body = blogTemplate "Tazjins Blog" "Wer mich kontaktieren will: " " oder " version DE body -renderBlog EN body = blogTemplate "Tazjin's Blog" "Get in touch with me: " " or " version EN body +renderBlog lang body = blogTemplate lang body -- http://tazj.in/2012/02/10.155234 -- cgit 1.4.1 From 0f0d874aa7bc194b48a47c29fab06513d093d306 Mon Sep 17 00:00:00 2001 From: "\"Vincent Ambo ext:(%22)" Date: Fri, 24 Feb 2012 17:01:36 +0100 Subject: * entries by month --- src/Locales.hs | 40 ++++++++++++++++++++++++++++++++++++++++ src/Server.hs | 28 ++++++++++++++++++++++++---- 2 files changed, 64 insertions(+), 4 deletions(-) diff --git a/src/Locales.hs b/src/Locales.hs index 266f4e752d..fb94351959 100644 --- a/src/Locales.hs +++ b/src/Locales.hs @@ -24,6 +24,46 @@ blogTitle EN = "Tazjin's Blog" topText DE = "Aktuelle Einträge" topText EN = "Latest entries" +getMonth :: BlogLang -> Int -> Int -> String +getMonth l y m = monthName l m ++ show y + where + monthName :: BlogLang -> Int -> String + monthName DE m = case m of + 1 -> "Januar " + 2 -> "Februar " + 3 -> "März " + 4 -> "April " + 5 -> "Mai " + 6 -> "Juni " + 7 -> "Juli " + 8 -> "August " + 9 -> "September " + 10 -> "Oktober " + 11 -> "November" + 12 -> "Dezember" + monthName EN m = case m of + 1 -> "January " + 2 -> "February " + 3 -> "March " + 4 -> "April " + 5 -> "May " + 6 -> "June " + 7 -> "July " + 8 -> "August " + 9 -> "September " + 10 -> "October " + 11 -> "November " + 12 -> "December " + +entireMonth DE = "Ganzer Monat" +entireMonth EN = "Entire month" + +prevMonth DE = "Früher" +prevMonth EN = "Earlier" + +nextMonth DE = "Später" +nextMonth EN = "Later" + -- contact information contactText DE = "Wer mich kontaktieren will: " contactText EN = "Get in touch with me: " diff --git a/src/Server.hs b/src/Server.hs index 764d3c9055..b50de2debc 100644 --- a/src/Server.hs +++ b/src/Server.hs @@ -44,6 +44,10 @@ blogHandler :: BlogLang -> ServerPart Response blogHandler lang = msum [ path $ \(year :: Int) -> path $ \(month :: Int) -> path $ --single entry \(day :: Int) -> path $ \(id_ :: String) -> showEntry year month day id_ + , path $ \(year :: Int) -> path $ \(month :: Int) -> path $ + \(day :: Int) -> showDay year month day lang + , path $ \(year :: Int ) -> path $ \(month :: Int) -> showMonth year month lang + , path $ \(year :: Int ) -> showYear year lang , do nullDir showIndex lang ] @@ -56,18 +60,29 @@ showEntry y m d i = do tryEntry :: Maybe Entry -> Response tryEntry Nothing = toResponse $ showError NotFound -tryEntry (Just entry) = toResponse $ renderBlog eLang $ renderEntry entry +tryEntry (Just entry) = toResponse $ blogTemplate eLang $ renderEntry entry where eLang = lang entry showIndex :: BlogLang -> ServerPart Response showIndex lang = do entries <- getLatest lang [] - ok $ toResponse $ renderBlog lang $ renderEntries entries 6 (topText lang) + ok $ toResponse $ blogTemplate lang $ renderEntries entries 6 (topText lang) + +showDay :: Int -> Int -> Int -> BlogLang -> ServerPart Response +showDay y m d lang = undefined + +showMonth :: Int -> Int -> BlogLang -> ServerPart Response +showMonth y m lang = do + entries <- getLatest lang $ makeQuery startkey endkey + ok $ toResponse $ blogTemplate lang $ renderEntries entries (length entries) $ getMonth lang y m where + startkey = JSArray [toJSON y, toJSON m] + endkey = JSArray [toJSON y, toJSON m, JSObject (toJSObject [] )] + +showYear :: Int -> BlogLang -> ServerPart Response +showYear y lang = undefined -renderBlog :: BlogLang -> Html -> Html -renderBlog lang body = blogTemplate lang body -- http://tazj.in/2012/02/10.155234 @@ -82,6 +97,10 @@ getLatest lang arg = do EN -> "latestEN" DE -> "latestDE" +makeQuery :: JSON a => a -> a -> [(String, JSValue)] +makeQuery qsk qek = [("startkey", (showJSON qsk)) + ,("endkey", (showJSON qek))] + queryDB :: JSON a => String -> [(String, JSValue)] -> ServerPart [(Doc, a)] queryDB view arg = liftIO $ runCouchDB' $ queryView (db "tazblog") (doc "entries") (doc view) arg @@ -92,6 +111,7 @@ maybeDoc Nothing = Nothing stripResult :: Result a -> a stripResult (Ok z) = z stripResult (Error s) = error $ "JSON error: " ++ s + -- CouchDB View Setup latestDEView = "function(doc){ if(doc.lang == 'DE'){ emit([doc.year, doc.month, doc.day, doc.id_], doc); } }" latestENView = "function(doc){ if(doc.lang == 'EN'){ emit([doc.year, doc.month, doc.day, doc.id_], doc); } }" -- cgit 1.4.1 From fed422f8724f9e0bfa7d6749721f2586803212a7 Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Thu, 1 Mar 2012 18:10:34 +0100 Subject: *updated .hgignore for geany files --- .hgignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.hgignore b/.hgignore index ab263ce4cb..558cd176c6 100644 --- a/.hgignore +++ b/.hgignore @@ -8,3 +8,4 @@ music *.esproj *.sublime* *.8 +*.geany -- cgit 1.4.1 From da8833bf343ddb0083cc14ef616eddd442896af5 Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Fri, 2 Mar 2012 09:12:09 +0100 Subject: * changes D: --- src/Blog.hs | 14 +++--- src/Locales.hs | 13 +++--- src/Main.hs | 132 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/Server.hs | 124 ----------------------------------------------------- 4 files changed, 148 insertions(+), 135 deletions(-) create mode 100644 src/Main.hs delete mode 100644 src/Server.hs diff --git a/src/Blog.hs b/src/Blog.hs index f14b5df5ec..61c8bc3f02 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -36,10 +36,14 @@ data Entry = Entry{ data BlogError = NoEntries | NotFound | DBError -blogTemplate :: BlogLang -> Html -> Html -blogTemplate lang body = H.docTypeHtml $ do --add body + +intersperse' :: a -> [a] -> [a] +intersperse' sep l = sep : intersperse sep l + +blogTemplate :: BlogLang -> String -> Html -> Html +blogTemplate lang t_append body = H.docTypeHtml $ do --add body H.head $ do - H.title $ (toHtml $ blogTitle lang) + H.title $ (toHtml $ blogTitle lang t_append) H.link ! A.rel "alternate" ! A.type_ "application/rss+xml" ! A.title "RSS-Feed" ! A.href "/rss" H.link ! A.rel "stylesheet" ! A.type_ "text/css" ! A.href "/res/blogstyle.css" ! A.media "all" H.meta ! A.httpEquiv "content-type" ! A.content "text/html;charset=UTF-8" @@ -48,7 +52,7 @@ blogTemplate lang body = H.docTypeHtml $ do --add body H.div ! A.class_ "mainshell" $ H.div ! A.class_ "gradBox" $ do H.div ! A.class_ "header" $ do H.a ! A.href "/" ! A.style "text-decoration:none;color:black;font-size:x-large;font-weight:bold;" $ - (toHtml $ blogTitle lang) + toHtml $ blogTitle lang "" H.br H.span ! A.id "cosx" ! A.style "display:block;" $ H.b $ contactInfo iMessage -- H.span ! A.id "cios" ! A.style "display:none;" $ H.b $ contactInfo "sms:tazjin@me.com" @@ -79,7 +83,7 @@ renderEntries entries num topText = H.div ! A.class_ "innerBox" $ do showEntry e = H.li $ do entryLink e preEscapedString $ " " ++ (text e) ++ "
 
" - entryLink e = H.a ! A.href (toValue $ concat $ intersperse "/" $ linkElems e) $ + entryLink e = H.a ! A.href (toValue $ concat $ intersperse' "/" $ linkElems e) $ toHtml ("[" ++ show(length $ comments e) ++ "]") linkElems e = [show(lang e), show(year e), show(month e), show(day e), _id e] diff --git a/src/Locales.hs b/src/Locales.hs index fb94351959..c3d11bc887 100644 --- a/src/Locales.hs +++ b/src/Locales.hs @@ -4,7 +4,7 @@ module Locales where import Data.Data (Data, Typeable) -{- to add a language simply define it's abbreviation and show instance then +{- to add a language simply define its abbreviation and Show instance then - translate the appropriate strings and add CouchDB views in Server.hs -} data BlogLang = EN | DE deriving (Data, Typeable) @@ -17,8 +17,9 @@ version = ("2.2b" :: String) allLang = [EN, DE] -blogTitle DE = "Tazjins Blog" -blogTitle EN = "Tazjin's Blog" +blogTitle :: BlogLang -> String -> String +blogTitle DE s = "Tazjins Blog" ++ s +blogTitle EN s = "Tazjin's Blog" ++ s -- index site headline topText DE = "Aktuelle Einträge" @@ -39,8 +40,8 @@ getMonth l y m = monthName l m ++ show y 8 -> "August " 9 -> "September " 10 -> "Oktober " - 11 -> "November" - 12 -> "Dezember" + 11 -> "November " + 12 -> "Dezember " monthName EN m = case m of 1 -> "January " 2 -> "February " @@ -94,4 +95,4 @@ repoURL = "https://bitbucket.org/tazjin/tazblog-haskell" mailTo = "mailto:hej@tazj.in" twitter = "http://twitter.com/#!/tazjin" iMessage = "imessage:tazjin@me.com" -iMessage' = "sms:tazjin@me.com" \ No newline at end of file +iMessage' = "sms:tazjin@me.com" diff --git a/src/Main.hs b/src/Main.hs new file mode 100644 index 0000000000..27dfc621bd --- /dev/null +++ b/src/Main.hs @@ -0,0 +1,132 @@ +{-# LANGUAGE OverloadedStrings, ScopedTypeVariables #-} + +module Main where + +import Control.Monad (msum, mzero) +import Data.Monoid (mempty) +import Data.ByteString.Char8 (ByteString) +import Data.Text hiding (map, length, zip, head) +import Data.Time +import Database.CouchDB +import Happstack.Server +import Network.CGI (liftIO) +import Text.Blaze (toValue, preEscapedString) +import Text.Blaze.Html5 (Html, (!), a, form, input, p, toHtml, label) +import Text.Blaze.Html5.Attributes (action, enctype, href, name, size, type_, value) +import qualified Text.Blaze.Html5 as H +import qualified Text.Blaze.Html5.Attributes as A +import Text.JSON.Generic + +import Blog +import Locales + +tmpPolicy :: BodyPolicy +tmpPolicy = (defaultBodyPolicy "./tmp/" 0 1000 1000) + +main :: IO() +main = do + putStrLn ("TazBlog " ++ version ++ " in Haskell starting") + simpleHTTP nullConf tazBlog + +tazBlog :: ServerPart Response +tazBlog = do + msum [ dir (show DE) $ blogHandler DE + , dir (show EN) $ blogHandler EN + , do nullDir + showIndex DE + , do dir " " $ nullDir + seeOther ("https://plus.google.com/115916629925754851590" :: String) (toResponse ()) + , dir "res" $ serveDirectory DisableBrowsing [] "../res" + , serveDirectory DisableBrowsing [] "../res" + ] + +blogHandler :: BlogLang -> ServerPart Response +blogHandler lang = + msum [ path $ \(year :: Int) -> path $ \(month :: Int) -> path $ --single entry + \(day :: Int) -> path $ \(id_ :: String) -> showEntry year month day id_ + , path $ \(year :: Int) -> path $ \(month :: Int) -> path $ + \(day :: Int) -> showDay year month day lang + , path $ \(year :: Int ) -> path $ \(month :: Int) -> showMonth year month lang + , path $ \(year :: Int ) -> showYear year lang + , do nullDir + showIndex lang + ] + +showEntry :: Int -> Int -> Int -> String -> ServerPart Response +showEntry y m d i = do + entryJS <- liftIO $ runCouchDB' $ getDoc (db "tazblog") (doc i) + let entry = maybeDoc entryJS + ok $ tryEntry entry + +tryEntry :: Maybe Entry -> Response +tryEntry Nothing = toResponse $ showError NotFound +tryEntry (Just entry) = toResponse $ blogTemplate eLang eTitle $ renderEntry entry + where + eTitle = ": " ++ title entry + eLang = lang entry + +showIndex :: BlogLang -> ServerPart Response +showIndex lang = do + entries <- getLatest lang [] + ok $ toResponse $ blogTemplate lang "" $ renderEntries entries 6 (topText lang) + +showDay :: Int -> Int -> Int -> BlogLang -> ServerPart Response +showDay y m d lang = undefined + +showMonth :: Int -> Int -> BlogLang -> ServerPart Response +showMonth y m lang = do + entries <- getLatest lang $ makeQuery startkey endkey + ok $ toResponse $ blogTemplate lang month + $ renderEntries entries (length entries) month + where + month = getMonth lang y m + startkey = JSArray [toJSON y, toJSON m] + endkey = JSArray [toJSON y, toJSON m, JSObject (toJSObject [] )] + +showYear :: Int -> BlogLang -> ServerPart Response +showYear y lang = undefined + + +-- http://tazj.in/2012/02/10.155234 + +-- CouchDB functions +getLatest :: BlogLang -> [(String, JSValue)] -> ServerPart [Entry] +getLatest lang arg = do + queryResult <- queryDB view arg + let entries = map (stripResult . fromJSON . snd) queryResult + return entries + where + view = case lang of + EN -> "latestEN" + DE -> "latestDE" + +makeQuery :: JSON a => a -> a -> [(String, JSValue)] +makeQuery qsk qek = [("startkey", (showJSON qsk)) + ,("endkey", (showJSON qek))] + +queryDB :: JSON a => String -> [(String, JSValue)] -> ServerPart [(Doc, a)] +queryDB view arg = liftIO $ runCouchDB' $ queryView (db "tazblog") (doc "entries") (doc view) arg + +maybeDoc :: Data a => Maybe (Doc, Rev, JSValue) -> Maybe a +maybeDoc (Just(_,_,v)) = Just( stripResult $ fromJSON v) +maybeDoc Nothing = Nothing + +stripResult :: Result a -> a +stripResult (Ok z) = z +stripResult (Error s) = error $ "JSON error: " ++ s + +-- CouchDB View Setup +latestDEView = "function(doc){ if(doc.lang == 'DE'){ emit([doc.year, doc.month, doc.day, doc._id], doc); } }" +latestENView = "function(doc){ if(doc.lang == 'EN'){ emit([doc.year, doc.month, doc.day, doc._id], doc); } }" +countDEView = "function(doc){ if(doc.lang == 'DE'){ emit([doc.year, doc.month, doc.day, doc._id], 1); } }" +countENView = "function(doc){ if(doc.lang == 'EN'){ emit([doc.year, doc.month, doc.day, doc._id], 1); } }" +countReduce = "function(keys, values, rereduce) { return sum(values); }" + +latestDE = ViewMap "latestDE" latestDEView +latestEN = ViewMap "latestEN" latestENView +countDE = ViewMapReduce "countDE" countDEView countReduce +countEN = ViewMapReduce "countEN" countENView countReduce + +setupBlogViews :: IO () +setupBlogViews = runCouchDB' $ + newView "tazblog" "entries" [latestDE, latestEN, countDE, countEN] diff --git a/src/Server.hs b/src/Server.hs deleted file mode 100644 index b50de2debc..0000000000 --- a/src/Server.hs +++ /dev/null @@ -1,124 +0,0 @@ -{-# LANGUAGE OverloadedStrings, ScopedTypeVariables #-} - -module Main where - -import Control.Monad (msum, mzero) -import Data.Monoid (mempty) -import Data.ByteString.Char8 (ByteString) -import Data.Text hiding (map, length, zip, head) -import Data.Time -import Database.CouchDB -import Happstack.Server -import Network.CGI (liftIO) -import Text.Blaze (toValue, preEscapedString) -import Text.Blaze.Html5 (Html, (!), a, form, input, p, toHtml, label) -import Text.Blaze.Html5.Attributes (action, enctype, href, name, size, type_, value) -import qualified Text.Blaze.Html5 as H -import qualified Text.Blaze.Html5.Attributes as A -import Text.JSON.Generic - -import Blog -import Locales - -tmpPolicy :: BodyPolicy -tmpPolicy = (defaultBodyPolicy "./tmp/" 0 1000 1000) - -main :: IO() -main = do - putStrLn ("TazBlog " ++ version ++ " in Haskell starting") - simpleHTTP nullConf tazBlog - -tazBlog :: ServerPart Response -tazBlog = do - msum [ dir (show DE) $ blogHandler DE - , dir (show EN) $ blogHandler EN - , do nullDir - showIndex DE - , do dir " " $ nullDir - seeOther ("https://plus.google.com/115916629925754851590" :: String) (toResponse ()) - , dir "res" $ serveDirectory DisableBrowsing [] "../res" - , serveDirectory DisableBrowsing [] "../res" - ] - -blogHandler :: BlogLang -> ServerPart Response -blogHandler lang = - msum [ path $ \(year :: Int) -> path $ \(month :: Int) -> path $ --single entry - \(day :: Int) -> path $ \(id_ :: String) -> showEntry year month day id_ - , path $ \(year :: Int) -> path $ \(month :: Int) -> path $ - \(day :: Int) -> showDay year month day lang - , path $ \(year :: Int ) -> path $ \(month :: Int) -> showMonth year month lang - , path $ \(year :: Int ) -> showYear year lang - , do nullDir - showIndex lang - ] - -showEntry :: Int -> Int -> Int -> String -> ServerPart Response -showEntry y m d i = do - entryJS <- liftIO $ runCouchDB' $ getDoc (db "tazblog") (doc i) - let entry = maybeDoc entryJS - ok $ tryEntry entry - -tryEntry :: Maybe Entry -> Response -tryEntry Nothing = toResponse $ showError NotFound -tryEntry (Just entry) = toResponse $ blogTemplate eLang $ renderEntry entry - where - eLang = lang entry - -showIndex :: BlogLang -> ServerPart Response -showIndex lang = do - entries <- getLatest lang [] - ok $ toResponse $ blogTemplate lang $ renderEntries entries 6 (topText lang) - -showDay :: Int -> Int -> Int -> BlogLang -> ServerPart Response -showDay y m d lang = undefined - -showMonth :: Int -> Int -> BlogLang -> ServerPart Response -showMonth y m lang = do - entries <- getLatest lang $ makeQuery startkey endkey - ok $ toResponse $ blogTemplate lang $ renderEntries entries (length entries) $ getMonth lang y m - where - startkey = JSArray [toJSON y, toJSON m] - endkey = JSArray [toJSON y, toJSON m, JSObject (toJSObject [] )] - -showYear :: Int -> BlogLang -> ServerPart Response -showYear y lang = undefined - - --- http://tazj.in/2012/02/10.155234 - --- CouchDB functions -getLatest :: BlogLang -> [(String, JSValue)] -> ServerPart [Entry] -getLatest lang arg = do - queryResult <- queryDB view arg - let entries = map (stripResult . fromJSON . snd) queryResult - return entries - where - view = case lang of - EN -> "latestEN" - DE -> "latestDE" - -makeQuery :: JSON a => a -> a -> [(String, JSValue)] -makeQuery qsk qek = [("startkey", (showJSON qsk)) - ,("endkey", (showJSON qek))] - -queryDB :: JSON a => String -> [(String, JSValue)] -> ServerPart [(Doc, a)] -queryDB view arg = liftIO $ runCouchDB' $ queryView (db "tazblog") (doc "entries") (doc view) arg - -maybeDoc :: Data a => Maybe (Doc, Rev, JSValue) -> Maybe a -maybeDoc (Just(_,_,v)) = Just( stripResult $ fromJSON v) -maybeDoc Nothing = Nothing - -stripResult :: Result a -> a -stripResult (Ok z) = z -stripResult (Error s) = error $ "JSON error: " ++ s - --- CouchDB View Setup -latestDEView = "function(doc){ if(doc.lang == 'DE'){ emit([doc.year, doc.month, doc.day, doc.id_], doc); } }" -latestENView = "function(doc){ if(doc.lang == 'EN'){ emit([doc.year, doc.month, doc.day, doc.id_], doc); } }" - -latestDE = ViewMap "latestDE" latestDEView -latestEN = ViewMap "latestEN" latestENView - -setupBlogViews :: IO () -setupBlogViews = runCouchDB' $ - newView "tazblog" "entries" [latestDE, latestEN] -- cgit 1.4.1 From 907eecf8c7827047073039403bfe74b3654c6e13 Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Sat, 3 Mar 2012 03:35:20 +0100 Subject: * getMonthCount function added --- src/Main.hs | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/Main.hs b/src/Main.hs index 27dfc621bd..4099168139 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -44,10 +44,7 @@ blogHandler :: BlogLang -> ServerPart Response blogHandler lang = msum [ path $ \(year :: Int) -> path $ \(month :: Int) -> path $ --single entry \(day :: Int) -> path $ \(id_ :: String) -> showEntry year month day id_ - , path $ \(year :: Int) -> path $ \(month :: Int) -> path $ - \(day :: Int) -> showDay year month day lang , path $ \(year :: Int ) -> path $ \(month :: Int) -> showMonth year month lang - , path $ \(year :: Int ) -> showYear year lang , do nullDir showIndex lang ] @@ -70,9 +67,6 @@ showIndex lang = do entries <- getLatest lang [] ok $ toResponse $ blogTemplate lang "" $ renderEntries entries 6 (topText lang) -showDay :: Int -> Int -> Int -> BlogLang -> ServerPart Response -showDay y m d lang = undefined - showMonth :: Int -> Int -> BlogLang -> ServerPart Response showMonth y m lang = do entries <- getLatest lang $ makeQuery startkey endkey @@ -83,10 +77,6 @@ showMonth y m lang = do startkey = JSArray [toJSON y, toJSON m] endkey = JSArray [toJSON y, toJSON m, JSObject (toJSObject [] )] -showYear :: Int -> BlogLang -> ServerPart Response -showYear y lang = undefined - - -- http://tazj.in/2012/02/10.155234 -- CouchDB functions @@ -105,7 +95,7 @@ makeQuery qsk qek = [("startkey", (showJSON qsk)) ,("endkey", (showJSON qek))] queryDB :: JSON a => String -> [(String, JSValue)] -> ServerPart [(Doc, a)] -queryDB view arg = liftIO $ runCouchDB' $ queryView (db "tazblog") (doc "entries") (doc view) arg +queryDB view arg = liftIO . runCouchDB' $ queryView (db "tazblog") (doc "entries") (doc view) arg maybeDoc :: Data a => Maybe (Doc, Rev, JSValue) -> Maybe a maybeDoc (Just(_,_,v)) = Just( stripResult $ fromJSON v) @@ -115,12 +105,25 @@ stripResult :: Result a -> a stripResult (Ok z) = z stripResult (Error s) = error $ "JSON error: " ++ s +getMonthCount :: Int -> Int -> ServerPart Int +getMonthCount y m = do + count <- queryDB "countDE" $ makeQuery startkey endkey + let x = map (stripResult . fromJSON . snd) count + return $ stripCount x + where + startkey = JSArray [toJSON ("count" :: String), toJSON y, toJSON m] + endkey = JSArray [toJSON ("count" :: String), toJSON y, toJSON m, JSObject (toJSObject [] )] + stripCount :: [Int] -> Int + stripCount [x] = x + stripCount [] = 0 + + -- CouchDB View Setup latestDEView = "function(doc){ if(doc.lang == 'DE'){ emit([doc.year, doc.month, doc.day, doc._id], doc); } }" latestENView = "function(doc){ if(doc.lang == 'EN'){ emit([doc.year, doc.month, doc.day, doc._id], doc); } }" countDEView = "function(doc){ if(doc.lang == 'DE'){ emit([doc.year, doc.month, doc.day, doc._id], 1); } }" countENView = "function(doc){ if(doc.lang == 'EN'){ emit([doc.year, doc.month, doc.day, doc._id], 1); } }" -countReduce = "function(keys, values, rereduce) { return sum(values); }" +countReduce = "function(keys, values, rereduce) { return sum(values); }" latestDE = ViewMap "latestDE" latestDEView latestEN = ViewMap "latestEN" latestENView -- cgit 1.4.1 From 9e4b35613cf2f11318a1dba2fa47861872d7a8f2 Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Sat, 3 Mar 2012 03:42:44 +0100 Subject: * removed an unnecessary tempVar --- src/Main.hs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Main.hs b/src/Main.hs index 4099168139..fb1cf07a5d 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -108,8 +108,7 @@ stripResult (Error s) = error $ "JSON error: " ++ s getMonthCount :: Int -> Int -> ServerPart Int getMonthCount y m = do count <- queryDB "countDE" $ makeQuery startkey endkey - let x = map (stripResult . fromJSON . snd) count - return $ stripCount x + return . stripCount $ map (stripResult . fromJSON . snd) count where startkey = JSArray [toJSON ("count" :: String), toJSON y, toJSON m] endkey = JSArray [toJSON ("count" :: String), toJSON y, toJSON m, JSObject (toJSObject [] )] -- cgit 1.4.1 From 65a5443e2d3f523dc3a61e6cdf2a9a588e6ac368 Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Sat, 3 Mar 2012 03:47:11 +0100 Subject: * getMonthCount now takes a BlogLang, as is obviously required --- src/Main.hs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Main.hs b/src/Main.hs index fb1cf07a5d..3c45375976 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -105,9 +105,9 @@ stripResult :: Result a -> a stripResult (Ok z) = z stripResult (Error s) = error $ "JSON error: " ++ s -getMonthCount :: Int -> Int -> ServerPart Int -getMonthCount y m = do - count <- queryDB "countDE" $ makeQuery startkey endkey +getMonthCount :: BlogLang -> Int -> Int -> ServerPart Int +getMonthCount lang y m = do + count <- queryDB (view lang) $ makeQuery startkey endkey return . stripCount $ map (stripResult . fromJSON . snd) count where startkey = JSArray [toJSON ("count" :: String), toJSON y, toJSON m] @@ -115,13 +115,15 @@ getMonthCount y m = do stripCount :: [Int] -> Int stripCount [x] = x stripCount [] = 0 + view DE = "countDE" + view EN = "countEN" -- CouchDB View Setup latestDEView = "function(doc){ if(doc.lang == 'DE'){ emit([doc.year, doc.month, doc.day, doc._id], doc); } }" latestENView = "function(doc){ if(doc.lang == 'EN'){ emit([doc.year, doc.month, doc.day, doc._id], doc); } }" -countDEView = "function(doc){ if(doc.lang == 'DE'){ emit([doc.year, doc.month, doc.day, doc._id], 1); } }" -countENView = "function(doc){ if(doc.lang == 'EN'){ emit([doc.year, doc.month, doc.day, doc._id], 1); } }" +countDEView = "function(doc){ if(doc.lang == 'DE'){ emit(['count', doc.year, doc.month, doc.day, doc._id], 1); } }" +countENView = "function(doc){ if(doc.lang == 'EN'){ emit(['count', doc.year, doc.month, doc.day, doc._id], 1); } }" countReduce = "function(keys, values, rereduce) { return sum(values); }" latestDE = ViewMap "latestDE" latestDEView -- cgit 1.4.1 From 7114876693cf5b2ce9ac4b7d7c5121aba4be94e2 Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Sat, 3 Mar 2012 04:02:30 +0100 Subject: * limiting amount of queries with couchDB's limit parameter --- src/Blog.hs | 6 +++--- src/Main.hs | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Blog.hs b/src/Blog.hs index 61c8bc3f02..0709dd3a3b 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -72,12 +72,12 @@ blogTemplate lang t_append body = H.docTypeHtml $ do --add body H.a ! A.href (toValue imu) ! A.target "_blank" $ "iMessage" "." -renderEntries :: [Entry] -> Int -> String-> Html -renderEntries entries num topText = H.div ! A.class_ "innerBox" $ do +renderEntries :: [Entry] -> String-> Html +renderEntries entries topText = H.div ! A.class_ "innerBox" $ do H.div ! A.class_ "innerBoxTop" $ toHtml topText H.div ! A.class_ "innerBoxMiddle" $ do H.ul $ - sequence_ $ take num $ reverse $ map showEntry entries + sequence_ $ reverse $ map showEntry entries where showEntry :: Entry -> Html showEntry e = H.li $ do diff --git a/src/Main.hs b/src/Main.hs index 3c45375976..60da994646 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -64,14 +64,14 @@ tryEntry (Just entry) = toResponse $ blogTemplate eLang eTitle $ renderEntry ent showIndex :: BlogLang -> ServerPart Response showIndex lang = do - entries <- getLatest lang [] - ok $ toResponse $ blogTemplate lang "" $ renderEntries entries 6 (topText lang) + entries <- getLatest lang [("limit", toJSON (6 :: Int)), ("descending", toJSON True)] + ok $ toResponse $ blogTemplate lang "" $ renderEntries entries (topText lang) showMonth :: Int -> Int -> BlogLang -> ServerPart Response showMonth y m lang = do entries <- getLatest lang $ makeQuery startkey endkey ok $ toResponse $ blogTemplate lang month - $ renderEntries entries (length entries) month + $ renderEntries entries month where month = getMonth lang y m startkey = JSArray [toJSON y, toJSON m] -- cgit 1.4.1 From 485e27147574106d5925ea9ab880739d8e1c4f6e Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Sat, 3 Mar 2012 04:05:19 +0100 Subject: * added TODO file --- TODO | 1 + src/Blog.hs | 2 +- src/Main.hs | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 TODO diff --git a/TODO b/TODO new file mode 100644 index 0000000000..2c6b5c9312 --- /dev/null +++ b/TODO @@ -0,0 +1 @@ +* create entirely new CouchDB views to return the blog IDs in descending order diff --git a/src/Blog.hs b/src/Blog.hs index 0709dd3a3b..7b39fe5c99 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -77,7 +77,7 @@ renderEntries entries topText = H.div ! A.class_ "innerBox" $ do H.div ! A.class_ "innerBoxTop" $ toHtml topText H.div ! A.class_ "innerBoxMiddle" $ do H.ul $ - sequence_ $ reverse $ map showEntry entries + sequence_ . reverse $ map showEntry entries where showEntry :: Entry -> Html showEntry e = H.li $ do diff --git a/src/Main.hs b/src/Main.hs index 60da994646..debf02e3cc 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -64,7 +64,7 @@ tryEntry (Just entry) = toResponse $ blogTemplate eLang eTitle $ renderEntry ent showIndex :: BlogLang -> ServerPart Response showIndex lang = do - entries <- getLatest lang [("limit", toJSON (6 :: Int)), ("descending", toJSON True)] + entries <- getLatest lang [("limit", toJSON (7 :: Int)), ("descending", toJSON True)] ok $ toResponse $ blogTemplate lang "" $ renderEntries entries (topText lang) showMonth :: Int -> Int -> BlogLang -> ServerPart Response -- cgit 1.4.1 From 96093c9009554cd63431022635fccf54e47438e2 Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Sat, 3 Mar 2012 16:39:15 +0100 Subject: * Pagination (finally!) * slight CSS change --- res/blogstyle.css | 3 ++- src/Blog.hs | 25 +++++++++++++++++++------ src/Locales.hs | 12 ++++++++---- src/Main.hs | 21 ++++++++++++++------- 4 files changed, 43 insertions(+), 18 deletions(-) diff --git a/res/blogstyle.css b/res/blogstyle.css index d830ee2e23..6315ffd06f 100644 --- a/res/blogstyle.css +++ b/res/blogstyle.css @@ -42,6 +42,7 @@ body { .centerbox { text-align:center; + min-height: 45px; } .rightbox { @@ -110,4 +111,4 @@ body { .innerBoxComments { padding-left: 20px -} \ No newline at end of file +} diff --git a/src/Blog.hs b/src/Blog.hs index 7b39fe5c99..575ec04fcf 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -72,12 +72,15 @@ blogTemplate lang t_append body = H.docTypeHtml $ do --add body H.a ! A.href (toValue imu) ! A.target "_blank" $ "iMessage" "." -renderEntries :: [Entry] -> String-> Html -renderEntries entries topText = H.div ! A.class_ "innerBox" $ do - H.div ! A.class_ "innerBoxTop" $ toHtml topText - H.div ! A.class_ "innerBoxMiddle" $ do - H.ul $ - sequence_ . reverse $ map showEntry entries +renderEntries :: Bool -> [Entry] -> String -> Maybe Html -> Html +renderEntries showAll entries topText footerLinks = + H.div ! A.class_ "innerBox" $ do + H.div ! A.class_ "innerBoxTop" $ toHtml topText + H.div ! A.class_ "innerBoxMiddle" $ do + H.ul $ if' showAll + (sequence_ $ map showEntry entries) + (sequence_ . take 6 $ map showEntry entries) + getFooterLinks footerLinks where showEntry :: Entry -> Html showEntry e = H.li $ do @@ -86,6 +89,8 @@ renderEntries entries topText = H.div ! A.class_ "innerBox" $ do entryLink e = H.a ! A.href (toValue $ concat $ intersperse' "/" $ linkElems e) $ toHtml ("[" ++ show(length $ comments e) ++ "]") linkElems e = [show(lang e), show(year e), show(month e), show(day e), _id e] + getFooterLinks (Just h) = h + getFooterLinks Nothing = mempty renderEntry :: Entry -> Html renderEntry entry = H.div ! A.class_ "innerBox" $ do @@ -114,6 +119,14 @@ renderComments comments lang = sequence_ $ map showComment comments showTime _ Nothing = "[???]" -- this can not happen?? timeString = (showTime lang) . getTime +showLinks :: Maybe Int -> BlogLang -> Html +showLinks (Just i) lang = H.div ! A.class_ "centerbox" $ do + H.a ! A.href (toValue $ "/?page=" ++ show (i+1)) $ toHtml $ backText lang + toHtml (" -- " :: String) + H.a ! A.href (toValue $ "/?page=" ++ show (i-1)) $ toHtml $ nextText lang +showLinks Nothing lang = H.div ! A.class_ "centerbox" $ + H.a ! A.href "/?page=2" $ toHtml $ backText lang + showFooter :: BlogLang -> String -> Html showFooter l v = H.div ! A.class_ "rightbox" ! A.style "text-align:right;" $ do toHtml ("Proudly made with " :: String) diff --git a/src/Locales.hs b/src/Locales.hs index c3d11bc887..6fb5849b8c 100644 --- a/src/Locales.hs +++ b/src/Locales.hs @@ -17,6 +17,10 @@ version = ("2.2b" :: String) allLang = [EN, DE] +if' :: Bool -> a -> a -> a +if' True x _ = x +if' False _ y = y + blogTitle :: BlogLang -> String -> String blogTitle DE s = "Tazjins Blog" ++ s blogTitle EN s = "Tazjin's Blog" ++ s @@ -59,11 +63,11 @@ getMonth l y m = monthName l m ++ show y entireMonth DE = "Ganzer Monat" entireMonth EN = "Entire month" -prevMonth DE = "Früher" -prevMonth EN = "Earlier" +backText DE = "Früher" +backText EN = "Earlier" -nextMonth DE = "Später" -nextMonth EN = "Later" +nextText DE = "Später" +nextText EN = "Later" -- contact information contactText DE = "Wer mich kontaktieren will: " diff --git a/src/Main.hs b/src/Main.hs index debf02e3cc..89f6179237 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -2,10 +2,11 @@ module Main where -import Control.Monad (msum, mzero) +import Control.Applicative (optional) +import Control.Monad (msum) import Data.Monoid (mempty) import Data.ByteString.Char8 (ByteString) -import Data.Text hiding (map, length, zip, head) +import Data.Text hiding (map, length, zip, head, drop) import Data.Time import Database.CouchDB import Happstack.Server @@ -64,14 +65,20 @@ tryEntry (Just entry) = toResponse $ blogTemplate eLang eTitle $ renderEntry ent showIndex :: BlogLang -> ServerPart Response showIndex lang = do - entries <- getLatest lang [("limit", toJSON (7 :: Int)), ("descending", toJSON True)] - ok $ toResponse $ blogTemplate lang "" $ renderEntries entries (topText lang) - + entries <- getLatest lang [("descending", showJSON True)] + (page :: Maybe Int) <- optional $ lookRead "page" + ok $ toResponse $ blogTemplate lang "" $ + renderEntries False (eDrop page entries) (topText lang) (Just $ showLinks page lang) + where + eDrop :: Maybe Int -> [a] -> [a] + eDrop (Just i) = drop ((i-1) * 6) + eDrop Nothing = drop 0 + showMonth :: Int -> Int -> BlogLang -> ServerPart Response showMonth y m lang = do - entries <- getLatest lang $ makeQuery startkey endkey + entries <- getLatest lang $ ("descending", showJSON True) : makeQuery startkey endkey ok $ toResponse $ blogTemplate lang month - $ renderEntries entries month + $ renderEntries True entries month Nothing where month = getMonth lang y m startkey = JSArray [toJSON y, toJSON m] -- cgit 1.4.1 From f113778e17be9124615ccc1ba684cb4a832f9408 Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Mon, 5 Mar 2012 05:27:20 +0100 Subject: * began work on commentBox --- src/Blog.hs | 14 ++++++++++++++ src/Locales.hs | 3 +++ 2 files changed, 17 insertions(+) diff --git a/src/Blog.hs b/src/Blog.hs index 575ec04fcf..62de9be0f4 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -102,6 +102,20 @@ renderEntry entry = H.div ! A.class_ "innerBox" $ do H.div ! A.class_ "innerBoxComments" $ do H.div ! A.name "cHead" ! A.style "font-size:large;font-weight:bold;" $ toHtml $ cHead (lang entry) H.ul $ renderComments (comments entry) (lang entry) + renderCommentBox $ lang entry + +renderCommentBox :: BlogLang -> Html +renderCommentBox lang = do + H.div ! A.name "cHead" $ toHtml $ cwHead lang + H.form $ do + H.p $ H.label $ do + toHtml ("Name:" :: String) + H.input +{- +
+

+
+-} renderComments :: [Comment] -> BlogLang -> Html renderComments [] lang = H.li $ toHtml $ noComments lang diff --git a/src/Locales.hs b/src/Locales.hs index 6fb5849b8c..01852cbdb0 100644 --- a/src/Locales.hs +++ b/src/Locales.hs @@ -87,6 +87,9 @@ noComments EN = " No comments yet" cHead DE = "Kommentare:" cHead EN = "Comments:" +cwHead DE = "Kommentieren:" +cwHead EN = "Comment:" + cTimeFormat DE = "[Am %d.%m.%y um %H:%M Uhr]" cTimeFormat EN = "[On %D at %H:%M]" -- cgit 1.4.1 From 0418692f07e057e2aa847fb2f5f24e6021a6073e Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Tue, 6 Mar 2012 00:50:53 +0100 Subject: * finished comment field --- res/blogstyle.css | 15 +++++++++++++++ src/Blog.hs | 24 +++++++++++------------- src/Locales.hs | 3 +++ src/Main.hs | 6 ++++++ 4 files changed, 35 insertions(+), 13 deletions(-) diff --git a/res/blogstyle.css b/res/blogstyle.css index 6315ffd06f..d8e878b012 100644 --- a/res/blogstyle.css +++ b/res/blogstyle.css @@ -63,6 +63,11 @@ body { text-decoration:none;color:black; } +.cHead { + font-size:large; + font-weight:bold; +} + .innerBoxTop { height: 28px; color: #000000; @@ -112,3 +117,13 @@ body { .innerBoxComments { padding-left: 20px } + + +label span { + width: 6%; + float: left; +} + +label input { + display: block; +} diff --git a/src/Blog.hs b/src/Blog.hs index 62de9be0f4..9c35c1ec74 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -100,22 +100,20 @@ renderEntry entry = H.div ! A.class_ "innerBox" $ do preEscapedString $ text entry preEscapedString $ mtext entry H.div ! A.class_ "innerBoxComments" $ do - H.div ! A.name "cHead" ! A.style "font-size:large;font-weight:bold;" $ toHtml $ cHead (lang entry) + H.div ! A.class_ "cHead" $ toHtml $ cHead (lang entry) -- ! A.style "font-size:large;font-weight:bold;" H.ul $ renderComments (comments entry) (lang entry) - renderCommentBox $ lang entry + renderCommentBox (lang entry) (_id entry) -renderCommentBox :: BlogLang -> Html -renderCommentBox lang = do - H.div ! A.name "cHead" $ toHtml $ cwHead lang - H.form $ do +renderCommentBox :: BlogLang -> String -> Html +renderCommentBox cLang cId = do + H.div ! A.class_ "cHead" $ toHtml $ cwHead cLang + H.form ! A.method "POST" ! A.action (toValue $ "/" ++ (show cLang) ++ "/postcomment/" ++ cId) $ do H.p $ H.label $ do - toHtml ("Name:" :: String) - H.input -{- -
-

-
--} + H.span $ "Name:" --toHtml ("Name:" :: String) + H.input ! A.name "cname" + H.p $ H.label $ do + H.span $ toHtml $ cSingle cLang -- toHtml (cSingle lang) + H.textarea ! A.name "ctext" ! A.cols "50" ! A.rows "13" $ mempty renderComments :: [Comment] -> BlogLang -> Html renderComments [] lang = H.li $ toHtml $ noComments lang diff --git a/src/Locales.hs b/src/Locales.hs index 01852cbdb0..20b4cb8476 100644 --- a/src/Locales.hs +++ b/src/Locales.hs @@ -90,6 +90,9 @@ cHead EN = "Comments:" cwHead DE = "Kommentieren:" cwHead EN = "Comment:" +cSingle DE = "Kommentar:" --input label +cSingle EN = "Comment:" + cTimeFormat DE = "[Am %d.%m.%y um %H:%M Uhr]" cTimeFormat EN = "[On %D at %H:%M]" diff --git a/src/Main.hs b/src/Main.hs index 89f6179237..f3d3db32d0 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -46,6 +46,9 @@ blogHandler lang = msum [ path $ \(year :: Int) -> path $ \(month :: Int) -> path $ --single entry \(day :: Int) -> path $ \(id_ :: String) -> showEntry year month day id_ , path $ \(year :: Int ) -> path $ \(month :: Int) -> showMonth year month lang + , do + decodeBody tmpPolicy + dir "postcomment" $ path $ \(id_ :: String) -> addComment id_ , do nullDir showIndex lang ] @@ -84,6 +87,9 @@ showMonth y m lang = do startkey = JSArray [toJSON y, toJSON m] endkey = JSArray [toJSON y, toJSON m, JSObject (toJSObject [] )] +addComment :: String -> ServerPart Response +addComment id_ = undefined + -- http://tazj.in/2012/02/10.155234 -- CouchDB functions -- cgit 1.4.1 From d4fa02deed25fe9a4e5ad1a9088242ed1506c0ea Mon Sep 17 00:00:00 2001 From: "\"Vincent Ambo ext:(%22)" Date: Tue, 6 Mar 2012 17:28:30 +0100 Subject: * using Text from Data.Text (stict) instead of String for text in entries and comments --- src/Blog.hs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/Blog.hs b/src/Blog.hs index 62de9be0f4..263060c2cb 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -5,9 +5,11 @@ module Blog where import Data.Data (Data, Typeable) import Data.List (intersperse) import Data.Monoid (mempty) +import Data.Text (Text) +import qualified Data.Text as T import Data.Time import System.Locale (defaultTimeLocale) -import Text.Blaze (toValue, preEscapedString) +import Text.Blaze (toValue, preEscapedText) import Text.Blaze.Html5 (Html, (!), a, form, input, p, toHtml, label) import Text.Blaze.Html5.Attributes (action, enctype, href, name, size, type_, value) import qualified Text.Blaze.Html5 as H @@ -36,6 +38,8 @@ data Entry = Entry{ data BlogError = NoEntries | NotFound | DBError +blogText :: (a -> String) -> a -> Text +blogText f = T.pack . f intersperse' :: a -> [a] -> [a] intersperse' sep l = sep : intersperse sep l @@ -85,7 +89,7 @@ renderEntries showAll entries topText footerLinks = showEntry :: Entry -> Html showEntry e = H.li $ do entryLink e - preEscapedString $ " " ++ (text e) ++ "
 
" + preEscapedText $ T.concat [" ", blogText text e, "
 
"] entryLink e = H.a ! A.href (toValue $ concat $ intersperse' "/" $ linkElems e) $ toHtml ("[" ++ show(length $ comments e) ++ "]") linkElems e = [show(lang e), show(year e), show(month e), show(day e), _id e] @@ -97,8 +101,8 @@ renderEntry entry = H.div ! A.class_ "innerBox" $ do H.div ! A.class_ "innerBoxTop" $ toHtml $ title entry H.div ! A.class_ "innerBoxMiddle" $ do H.article $ H.ul $ H.li $ do - preEscapedString $ text entry - preEscapedString $ mtext entry + preEscapedText $ blogText text entry + preEscapedText $ blogText mtext entry H.div ! A.class_ "innerBoxComments" $ do H.div ! A.name "cHead" ! A.style "font-size:large;font-weight:bold;" $ toHtml $ cHead (lang entry) H.ul $ renderComments (comments entry) (lang entry) @@ -125,7 +129,7 @@ renderComments comments lang = sequence_ $ map showComment comments showComment c = H.li $ do H.a ! A.name (toValue $ cdate c) ! A.href (toValue $ "#" ++ (show $ cdate c)) ! A.class_ "cl" $ H.i $ toHtml $ (cauthor c ++ ": ") - preEscapedString $ ctext c + preEscapedText $ blogText ctext c H.p ! A.class_ "tt" $ toHtml (timeString $ cdate c) getTime :: Integer -> Maybe UTCTime getTime t = parseTime defaultTimeLocale "%s" (show t) @@ -150,7 +154,7 @@ showFooter l v = H.div ! A.class_ "rightbox" ! A.style "text-align:right;" $ do toHtml (" and without PHP, Java, Perl, MySQL and Python." :: String) H.br H.a ! A.href (toValue repoURL) $ toHtml $ ("Version " :: String) ++ v - preEscapedString " " + preEscapedText " " H.a ! A.href "/notice" $ toHtml $ noticeText l -- Error pages -- cgit 1.4.1 From 8c90ebdb499be2a8bc171dad8236728399755f0c Mon Sep 17 00:00:00 2001 From: "\"Vincent Ambo ext:(%22)" Date: Tue, 6 Mar 2012 19:39:54 +0100 Subject: * TEXT EVERYWHERE, WHERE MY STRINGS AT? --- src/Blog.hs | 24 ++++++++++++------------ src/Locales.hs | 43 ++++++++++++++++++++++++++++--------------- src/Main.hs | 5 +++-- 3 files changed, 43 insertions(+), 29 deletions(-) diff --git a/src/Blog.hs b/src/Blog.hs index 263060c2cb..649329cfb3 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -44,7 +44,7 @@ blogText f = T.pack . f intersperse' :: a -> [a] -> [a] intersperse' sep l = sep : intersperse sep l -blogTemplate :: BlogLang -> String -> Html -> Html +blogTemplate :: BlogLang -> Text -> Html -> Html blogTemplate lang t_append body = H.docTypeHtml $ do --add body H.head $ do H.title $ (toHtml $ blogTitle lang t_append) @@ -63,20 +63,20 @@ blogTemplate lang t_append body = H.docTypeHtml $ do --add body H.div ! A.class_ "myclear" $ mempty body H.div ! A.class_ "myclear" $ mempty - showFooter lang version + showFooter lang $ T.pack version H.div ! A.class_ "centerbox" $ H.img ! A.src "http://getpunchd.com/img/june/idiots.png" ! A.alt "" where - contactInfo (imu :: String) = do + contactInfo (imu :: Text) = do toHtml $ contactText lang H.a ! A.href (toValue mailTo) $ "Mail" ", " H.a ! A.href (toValue twitter) ! A.target "_blank" $ "Twitter" - toHtml $ orString lang + toHtml $ orText lang H.a ! A.href (toValue imu) ! A.target "_blank" $ "iMessage" "." -renderEntries :: Bool -> [Entry] -> String -> Maybe Html -> Html +renderEntries :: Bool -> [Entry] -> Text -> Maybe Html -> Html renderEntries showAll entries topText footerLinks = H.div ! A.class_ "innerBox" $ do H.div ! A.class_ "innerBoxTop" $ toHtml topText @@ -113,7 +113,7 @@ renderCommentBox lang = do H.div ! A.name "cHead" $ toHtml $ cwHead lang H.form $ do H.p $ H.label $ do - toHtml ("Name:" :: String) + toHtml ("Name:" :: Text) H.input {-
@@ -140,20 +140,20 @@ renderComments comments lang = sequence_ $ map showComment comments showLinks :: Maybe Int -> BlogLang -> Html showLinks (Just i) lang = H.div ! A.class_ "centerbox" $ do H.a ! A.href (toValue $ "/?page=" ++ show (i+1)) $ toHtml $ backText lang - toHtml (" -- " :: String) + toHtml (" -- " :: Text) H.a ! A.href (toValue $ "/?page=" ++ show (i-1)) $ toHtml $ nextText lang showLinks Nothing lang = H.div ! A.class_ "centerbox" $ H.a ! A.href "/?page=2" $ toHtml $ backText lang -showFooter :: BlogLang -> String -> Html +showFooter :: BlogLang -> Text -> Html showFooter l v = H.div ! A.class_ "rightbox" ! A.style "text-align:right;" $ do - toHtml ("Proudly made with " :: String) + toHtml ("Proudly made with " :: Text) H.a ! A.href "http://haskell.org" $ "Haskell" - toHtml (", " :: String) + toHtml (", " :: Text) H.a ! A.href "http://couchdb.apache.org/" $ "CouchDB" - toHtml (" and without PHP, Java, Perl, MySQL and Python." :: String) + toHtml (" and without PHP, Java, Perl, MySQL and Python." :: Text) H.br - H.a ! A.href (toValue repoURL) $ toHtml $ ("Version " :: String) ++ v + H.a ! A.href (toValue repoURL) $ toHtml $ T.concat ["Version ", v] preEscapedText " " H.a ! A.href "/notice" $ toHtml $ noticeText l diff --git a/src/Locales.hs b/src/Locales.hs index 01852cbdb0..f629dbe6f3 100644 --- a/src/Locales.hs +++ b/src/Locales.hs @@ -1,8 +1,10 @@ -{-# LANGUAGE ScopedTypeVariables, DeriveDataTypeable #-} +{-# LANGUAGE ScopedTypeVariables, DeriveDataTypeable, OverloadedStrings #-} module Locales where import Data.Data (Data, Typeable) +import Data.Text (Text) +import qualified Data.Text as T {- to add a language simply define its abbreviation and Show instance then - translate the appropriate strings and add CouchDB views in Server.hs -} @@ -13,7 +15,7 @@ instance Show BlogLang where show EN = "en" show DE = "de" -version = ("2.2b" :: String) +version = "2.2b" allLang = [EN, DE] @@ -21,18 +23,18 @@ if' :: Bool -> a -> a -> a if' True x _ = x if' False _ y = y -blogTitle :: BlogLang -> String -> String -blogTitle DE s = "Tazjins Blog" ++ s -blogTitle EN s = "Tazjin's Blog" ++ s +blogTitle :: BlogLang -> Text -> Text +blogTitle DE s = T.concat ["Tazjins Blog", s] +blogTitle EN s = T.concat ["Tazjin's Blog", s] -- index site headline topText DE = "Aktuelle Einträge" topText EN = "Latest entries" -getMonth :: BlogLang -> Int -> Int -> String -getMonth l y m = monthName l m ++ show y +getMonth :: BlogLang -> Int -> Int -> Text +getMonth l y m = T.append (monthName l m) $ T.pack $ show y where - monthName :: BlogLang -> Int -> String + monthName :: BlogLang -> Int -> Text monthName DE m = case m of 1 -> "Januar " 2 -> "Februar " @@ -60,46 +62,57 @@ getMonth l y m = monthName l m ++ show y 11 -> "November " 12 -> "December " +entireMonth :: BlogLang -> Text entireMonth DE = "Ganzer Monat" entireMonth EN = "Entire month" +backText :: BlogLang -> Text backText DE = "Früher" backText EN = "Earlier" +nextText :: BlogLang -> Text nextText DE = "Später" nextText EN = "Later" -- contact information +contactText :: BlogLang -> Text contactText DE = "Wer mich kontaktieren will: " contactText EN = "Get in touch with me: " -orString DE = " oder " -orString EN = " or " +orText :: BlogLang -> Text +orText DE = " oder " +orText EN = " or " -- footer +noticeText :: BlogLang -> Text noticeText EN = "site notice" noticeText DE = "Impressum" -- comments +noComments :: BlogLang -> Text noComments DE = " Keine Kommentare" noComments EN = " No comments yet" +cHead :: BlogLang -> Text cHead DE = "Kommentare:" cHead EN = "Comments:" +cwHead :: BlogLang -> Text cwHead DE = "Kommentieren:" cwHead EN = "Comment:" +cTimeFormat :: BlogLang -> String --formatTime expects a String cTimeFormat DE = "[Am %d.%m.%y um %H:%M Uhr]" cTimeFormat EN = "[On %D at %H:%M]" -- right side text (this is inserted AS IS. Escape HTML!) +rightText :: BlogLang -> Text rightText DE = "English version available here" rightText EN = "Deutsche Version hier verfügbar" -- static information -repoURL = "https://bitbucket.org/tazjin/tazblog-haskell" -mailTo = "mailto:hej@tazj.in" -twitter = "http://twitter.com/#!/tazjin" -iMessage = "imessage:tazjin@me.com" -iMessage' = "sms:tazjin@me.com" +repoURL :: Text = "https://bitbucket.org/tazjin/tazblog-haskell" +mailTo :: Text = "mailto:hej@tazj.in" +twitter :: Text = "http://twitter.com/#!/tazjin" +iMessage :: Text = "imessage:tazjin@me.com" +iMessage' :: Text = "sms:tazjin@me.com" diff --git a/src/Main.hs b/src/Main.hs index 89f6179237..c851d9a052 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -6,7 +6,8 @@ import Control.Applicative (optional) import Control.Monad (msum) import Data.Monoid (mempty) import Data.ByteString.Char8 (ByteString) -import Data.Text hiding (map, length, zip, head, drop) +import Data.Text (Text) +import qualified Data.Text as T import Data.Time import Database.CouchDB import Happstack.Server @@ -60,7 +61,7 @@ tryEntry :: Maybe Entry -> Response tryEntry Nothing = toResponse $ showError NotFound tryEntry (Just entry) = toResponse $ blogTemplate eLang eTitle $ renderEntry entry where - eTitle = ": " ++ title entry + eTitle = T.pack $ ": " ++ title entry eLang = lang entry showIndex :: BlogLang -> ServerPart Response -- cgit 1.4.1 From 6220988fc5fa89a3f581c446fddd103beabc32cd Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Tue, 6 Mar 2012 21:24:58 +0100 Subject: * guarding showLinks against negative numbers --- src/Blog.hs | 48 +++++++++++++++++++++++++++--------------------- src/Locales.hs | 3 ++- 2 files changed, 29 insertions(+), 22 deletions(-) diff --git a/src/Blog.hs b/src/Blog.hs index 9c35c1ec74..82939641af 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -5,9 +5,11 @@ module Blog where import Data.Data (Data, Typeable) import Data.List (intersperse) import Data.Monoid (mempty) +import Data.Text (Text) +import qualified Data.Text as T import Data.Time import System.Locale (defaultTimeLocale) -import Text.Blaze (toValue, preEscapedString) +import Text.Blaze (toValue, preEscapedText) import Text.Blaze.Html5 (Html, (!), a, form, input, p, toHtml, label) import Text.Blaze.Html5.Attributes (action, enctype, href, name, size, type_, value) import qualified Text.Blaze.Html5 as H @@ -34,13 +36,15 @@ data Entry = Entry{ comments :: [Comment] } deriving (Show, Data, Typeable) -data BlogError = NoEntries | NotFound | DBError +blogText :: (a -> String) -> a -> Text +blogText f = T.pack . f +data BlogError = NoEntries | NotFound | DBError intersperse' :: a -> [a] -> [a] intersperse' sep l = sep : intersperse sep l -blogTemplate :: BlogLang -> String -> Html -> Html +blogTemplate :: BlogLang -> Text -> Html -> Html blogTemplate lang t_append body = H.docTypeHtml $ do --add body H.head $ do H.title $ (toHtml $ blogTitle lang t_append) @@ -59,20 +63,20 @@ blogTemplate lang t_append body = H.docTypeHtml $ do --add body H.div ! A.class_ "myclear" $ mempty body H.div ! A.class_ "myclear" $ mempty - showFooter lang version + showFooter lang $ T.pack version H.div ! A.class_ "centerbox" $ H.img ! A.src "http://getpunchd.com/img/june/idiots.png" ! A.alt "" where - contactInfo (imu :: String) = do + contactInfo (imu :: Text) = do toHtml $ contactText lang H.a ! A.href (toValue mailTo) $ "Mail" ", " H.a ! A.href (toValue twitter) ! A.target "_blank" $ "Twitter" - toHtml $ orString lang + toHtml $ orText lang H.a ! A.href (toValue imu) ! A.target "_blank" $ "iMessage" "." -renderEntries :: Bool -> [Entry] -> String -> Maybe Html -> Html +renderEntries :: Bool -> [Entry] -> Text -> Maybe Html -> Html renderEntries showAll entries topText footerLinks = H.div ! A.class_ "innerBox" $ do H.div ! A.class_ "innerBoxTop" $ toHtml topText @@ -85,7 +89,7 @@ renderEntries showAll entries topText footerLinks = showEntry :: Entry -> Html showEntry e = H.li $ do entryLink e - preEscapedString $ " " ++ (text e) ++ "
 
" + preEscapedText $ T.concat [" ", blogText text e, "
 
"] entryLink e = H.a ! A.href (toValue $ concat $ intersperse' "/" $ linkElems e) $ toHtml ("[" ++ show(length $ comments e) ++ "]") linkElems e = [show(lang e), show(year e), show(month e), show(day e), _id e] @@ -97,8 +101,8 @@ renderEntry entry = H.div ! A.class_ "innerBox" $ do H.div ! A.class_ "innerBoxTop" $ toHtml $ title entry H.div ! A.class_ "innerBoxMiddle" $ do H.article $ H.ul $ H.li $ do - preEscapedString $ text entry - preEscapedString $ mtext entry + preEscapedText $ blogText text entry + preEscapedText $ blogText mtext entry H.div ! A.class_ "innerBoxComments" $ do H.div ! A.class_ "cHead" $ toHtml $ cHead (lang entry) -- ! A.style "font-size:large;font-weight:bold;" H.ul $ renderComments (comments entry) (lang entry) @@ -123,7 +127,7 @@ renderComments comments lang = sequence_ $ map showComment comments showComment c = H.li $ do H.a ! A.name (toValue $ cdate c) ! A.href (toValue $ "#" ++ (show $ cdate c)) ! A.class_ "cl" $ H.i $ toHtml $ (cauthor c ++ ": ") - preEscapedString $ ctext c + preEscapedText $ blogText ctext c H.p ! A.class_ "tt" $ toHtml (timeString $ cdate c) getTime :: Integer -> Maybe UTCTime getTime t = parseTime defaultTimeLocale "%s" (show t) @@ -132,23 +136,25 @@ renderComments comments lang = sequence_ $ map showComment comments timeString = (showTime lang) . getTime showLinks :: Maybe Int -> BlogLang -> Html -showLinks (Just i) lang = H.div ! A.class_ "centerbox" $ do - H.a ! A.href (toValue $ "/?page=" ++ show (i+1)) $ toHtml $ backText lang - toHtml (" -- " :: String) - H.a ! A.href (toValue $ "/?page=" ++ show (i-1)) $ toHtml $ nextText lang +showLinks (Just i) lang + | ( i > 1) = H.div ! A.class_ "centerbox" $ do + H.a ! A.href (toValue $ "/?page=" ++ show (i+1)) $ toHtml $ backText lang + toHtml (" -- " :: Text) + H.a ! A.href (toValue $ "/?page=" ++ show (i-1)) $ toHtml $ nextText lang + | ( i <= 1 ) = showLinks Nothing lang showLinks Nothing lang = H.div ! A.class_ "centerbox" $ H.a ! A.href "/?page=2" $ toHtml $ backText lang -showFooter :: BlogLang -> String -> Html +showFooter :: BlogLang -> Text -> Html showFooter l v = H.div ! A.class_ "rightbox" ! A.style "text-align:right;" $ do - toHtml ("Proudly made with " :: String) + toHtml ("Proudly made with " :: Text) H.a ! A.href "http://haskell.org" $ "Haskell" - toHtml (", " :: String) + toHtml (", " :: Text) H.a ! A.href "http://couchdb.apache.org/" $ "CouchDB" - toHtml (" and without PHP, Java, Perl, MySQL and Python." :: String) + toHtml (" and without PHP, Java, Perl, MySQL and Python." :: Text) H.br - H.a ! A.href (toValue repoURL) $ toHtml $ ("Version " :: String) ++ v - preEscapedString " " + H.a ! A.href (toValue repoURL) $ toHtml $ T.append "Version " v + preEscapedText " " H.a ! A.href "/notice" $ toHtml $ noticeText l -- Error pages diff --git a/src/Locales.hs b/src/Locales.hs index 9b9002ab24..0f53951641 100644 --- a/src/Locales.hs +++ b/src/Locales.hs @@ -1,4 +1,4 @@ -{-# LANGUAGE ScopedTypeVariables, DeriveDataTypeable #-} +{-# LANGUAGE ScopedTypeVariables, DeriveDataTypeable, OverloadedStrings #-} module Locales where @@ -101,6 +101,7 @@ cwHead :: BlogLang -> Text cwHead DE = "Kommentieren:" cwHead EN = "Comment:" +cSingle :: BlogLang -> Text cSingle DE = "Kommentar:" --input label cSingle EN = "Comment:" -- cgit 1.4.1 From cd3a5f2cb5f73c6aff16a153864d56faca59e30b Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Tue, 6 Mar 2012 23:34:04 +0100 Subject: * links on right side --- TODO | 2 +- src/Blog.hs | 37 +++++++++++++++++++++++++++++-------- src/Locales.hs | 7 +++++-- src/Main.hs | 17 +++++++++-------- 4 files changed, 44 insertions(+), 19 deletions(-) diff --git a/TODO b/TODO index 2c6b5c9312..7c2dcaa91e 100644 --- a/TODO +++ b/TODO @@ -1 +1 @@ -* create entirely new CouchDB views to return the blog IDs in descending order +* handle BlogErrors diff --git a/src/Blog.hs b/src/Blog.hs index 82939641af..8905bc11ca 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -39,8 +39,6 @@ data Entry = Entry{ blogText :: (a -> String) -> a -> Text blogText f = T.pack . f -data BlogError = NoEntries | NotFound | DBError - intersperse' :: a -> [a] -> [a] intersperse' sep l = sep : intersperse sep l @@ -55,11 +53,12 @@ blogTemplate lang t_append body = H.docTypeHtml $ do --add body H.body $ do H.div ! A.class_ "mainshell" $ H.div ! A.class_ "gradBox" $ do H.div ! A.class_ "header" $ do - H.a ! A.href "/" ! A.style "text-decoration:none;color:black;font-size:x-large;font-weight:bold;" $ toHtml $ blogTitle lang "" - H.br - H.span ! A.id "cosx" ! A.style "display:block;" $ H.b $ contactInfo iMessage - -- H.span ! A.id "cios" ! A.style "display:none;" $ H.b $ contactInfo "sms:tazjin@me.com" + H.a ! A.href "/" ! A.style "text-decoration:none;color:black;font-size:x-large;font-weight:bold;" $ + H.p ! A.style "clear: both;" $ do + H.span ! A.style "float: left;" ! A.id "cosx" $ H.b $ contactInfo iMessage + -- H.span ! A.id "cios" ! A.style "display:none;" $ H.b $ contactInfo "sms:tazjin@me.com" + H.span ! A.style "float:right;" $ preEscapedText $ rightText lang H.div ! A.class_ "myclear" $ mempty body H.div ! A.class_ "myclear" $ mempty @@ -157,6 +156,28 @@ showFooter l v = H.div ! A.class_ "rightbox" ! A.style "text-align:right;" $ do preEscapedText " " H.a ! A.href "/notice" $ toHtml $ noticeText l +showSiteNotice :: Html +showSiteNotice = H.docTypeHtml $ do + H.title $ "Impressum" + H.h2 $ preEscapedText "Impressum und ViSdP" + H.i $ "[German law demands this]" + H.br + H.p $ do + toHtml ("Vincent Ambo" :: Text) + H.br + toHtml ("Benfleetstr. 8" :: Text) + H.br + toHtml ("50858 Köln" :: Text) + H.p $ H.a ! A.href "/" ! A.style "color:black" $ "Back" + +{- +Impressum + +

Impressum und ViSdP

+ +[German law demands this]

Vincent Ambo
Benfleetstr. 8
50858 Köln

Back +-} + -- Error pages -showError :: BlogError -> Html -showError _ = undefined +showError :: BlogError -> BlogLang -> Html +showError NotFound l = undefined diff --git a/src/Locales.hs b/src/Locales.hs index 0f53951641..047beb8aad 100644 --- a/src/Locales.hs +++ b/src/Locales.hs @@ -15,6 +15,9 @@ instance Show BlogLang where show EN = "en" show DE = "de" +data BlogError = NotFound | DBError + + version = "2.2b" allLang = [EN, DE] @@ -111,8 +114,8 @@ cTimeFormat EN = "[On %D at %H:%M]" -- right side text (this is inserted AS IS. Escape HTML!) rightText :: BlogLang -> Text -rightText DE = "English version available here" -rightText EN = "Deutsche Version hier verfügbar" +rightText DE = "English version available here." +rightText EN = "Deutsche Version hier verfügbar." -- static information repoURL :: Text = "https://bitbucket.org/tazjin/tazblog-haskell" diff --git a/src/Main.hs b/src/Main.hs index 5bc2ef2ce4..e0714c95e5 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -39,13 +39,14 @@ tazBlog = do , do dir " " $ nullDir seeOther ("https://plus.google.com/115916629925754851590" :: String) (toResponse ()) , dir "res" $ serveDirectory DisableBrowsing [] "../res" + , dir "notice" $ ok $ toResponse showSiteNotice , serveDirectory DisableBrowsing [] "../res" ] blogHandler :: BlogLang -> ServerPart Response blogHandler lang = msum [ path $ \(year :: Int) -> path $ \(month :: Int) -> path $ --single entry - \(day :: Int) -> path $ \(id_ :: String) -> showEntry year month day id_ + \(day :: Int) -> path $ \(id_ :: String) -> showEntry lang id_ , path $ \(year :: Int ) -> path $ \(month :: Int) -> showMonth year month lang , do decodeBody tmpPolicy @@ -54,15 +55,15 @@ blogHandler lang = showIndex lang ] -showEntry :: Int -> Int -> Int -> String -> ServerPart Response -showEntry y m d i = do - entryJS <- liftIO $ runCouchDB' $ getDoc (db "tazblog") (doc i) +showEntry :: BlogLang -> String -> ServerPart Response +showEntry lang id_ = do + entryJS <- liftIO $ runCouchDB' $ getDoc (db "tazblog") (doc id_) let entry = maybeDoc entryJS - ok $ tryEntry entry + ok $ tryEntry entry lang -tryEntry :: Maybe Entry -> Response -tryEntry Nothing = toResponse $ showError NotFound -tryEntry (Just entry) = toResponse $ blogTemplate eLang eTitle $ renderEntry entry +tryEntry :: Maybe Entry -> BlogLang -> Response +tryEntry Nothing lang = toResponse $ showError NotFound lang +tryEntry (Just entry) _ = toResponse $ blogTemplate eLang eTitle $ renderEntry entry where eTitle = T.pack $ ": " ++ title entry eLang = lang entry -- cgit 1.4.1 From bc25b9d1e05ed7c73dd30ae7df10836c894bd855 Mon Sep 17 00:00:00 2001 From: "\"Vincent Ambo ext:(%22)" Date: Wed, 7 Mar 2012 12:59:44 +0100 Subject: * one step closer to adding comments * generic Doc update function * redirect / to appropriate full link --- src/Blog.hs | 5 ++++- src/Locales.hs | 4 ++++ src/Main.hs | 40 ++++++++++++++++++++++++++++++++++++++-- 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/Blog.hs b/src/Blog.hs index 8905bc11ca..201851cba0 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -14,6 +14,7 @@ import Text.Blaze.Html5 (Html, (!), a, form, input, p, toHtml, label) import Text.Blaze.Html5.Attributes (action, enctype, href, name, size, type_, value) import qualified Text.Blaze.Html5 as H import qualified Text.Blaze.Html5.Attributes as A +import System.Locale (defaultTimeLocale) import Locales @@ -53,8 +54,9 @@ blogTemplate lang t_append body = H.docTypeHtml $ do --add body H.body $ do H.div ! A.class_ "mainshell" $ H.div ! A.class_ "gradBox" $ do H.div ! A.class_ "header" $ do + H.a ! A.href (toValue $ "/" ++ show lang) ! + A.style "text-decoration:none;color:black;font-size:x-large;font-weight:bold;" $ toHtml $ blogTitle lang "" - H.a ! A.href "/" ! A.style "text-decoration:none;color:black;font-size:x-large;font-weight:bold;" $ H.p ! A.style "clear: both;" $ do H.span ! A.style "float: left;" ! A.id "cosx" $ H.b $ contactInfo iMessage -- H.span ! A.id "cios" ! A.style "display:none;" $ H.b $ contactInfo "sms:tazjin@me.com" @@ -117,6 +119,7 @@ renderCommentBox cLang cId = do H.p $ H.label $ do H.span $ toHtml $ cSingle cLang -- toHtml (cSingle lang) H.textarea ! A.name "ctext" ! A.cols "50" ! A.rows "13" $ mempty + H.p $ H.input ! A.type_ "submit" ! A.value (toValue $ cSend cLang) renderComments :: [Comment] -> BlogLang -> Html renderComments [] lang = H.li $ toHtml $ noComments lang diff --git a/src/Locales.hs b/src/Locales.hs index 047beb8aad..56bc42d10b 100644 --- a/src/Locales.hs +++ b/src/Locales.hs @@ -112,6 +112,10 @@ cTimeFormat :: BlogLang -> String --formatTime expects a String cTimeFormat DE = "[Am %d.%m.%y um %H:%M Uhr]" cTimeFormat EN = "[On %D at %H:%M]" +cSend :: BlogLang -> Text +cSend DE = "Absenden" +cSend EN = "Submit" + -- right side text (this is inserted AS IS. Escape HTML!) rightText :: BlogLang -> Text rightText DE = "English version available here." diff --git a/src/Main.hs b/src/Main.hs index e0714c95e5..769e2180a3 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -2,7 +2,7 @@ module Main where -import Control.Applicative (optional) +import Control.Applicative ((<$>), (<*>), optional, pure) import Control.Monad (msum) import Data.Monoid (mempty) import Data.ByteString.Char8 (ByteString) @@ -18,6 +18,7 @@ import Text.Blaze.Html5.Attributes (action, enctype, href, name, size, import qualified Text.Blaze.Html5 as H import qualified Text.Blaze.Html5.Attributes as A import Text.JSON.Generic +import System.Locale (defaultTimeLocale) import Blog import Locales @@ -38,6 +39,7 @@ tazBlog = do showIndex DE , do dir " " $ nullDir seeOther ("https://plus.google.com/115916629925754851590" :: String) (toResponse ()) + , path $ \(id_ :: Int) -> getEntryLink id_ , dir "res" $ serveDirectory DisableBrowsing [] "../res" , dir "notice" $ ok $ toResponse showSiteNotice , serveDirectory DisableBrowsing [] "../res" @@ -68,6 +70,16 @@ tryEntry (Just entry) _ = toResponse $ blogTemplate eLang eTitle $ renderEntry e eTitle = T.pack $ ": " ++ title entry eLang = lang entry +getEntryLink :: Int -> ServerPart Response +getEntryLink id_ = do + entryJS <- liftIO $ runCouchDB' $ getDoc (db "tazblog") (doc $ show id_) + let entry = maybeDoc entryJS + seeOther (makeLink entry) (toResponse()) + where + makeLink :: Maybe Entry -> String + makeLink (Just e) = concat $ intersperse' "/" [show $ lang e, show $ year e, show $ month e, show $ day e, show id_] + makeLink Nothing = "/" + showIndex :: BlogLang -> ServerPart Response showIndex lang = do entries <- getLatest lang [("descending", showJSON True)] @@ -90,10 +102,23 @@ showMonth y m lang = do endkey = JSArray [toJSON y, toJSON m, JSObject (toJSObject [] )] addComment :: String -> ServerPart Response -addComment id_ = undefined +addComment id_ = do + rda <- liftIO $ currentSeconds >>= return + nComment <- Comment <$> look "cname" + <*> look "ctext" + <*> pure rda + rev <- updateDBDoc (doc id_) $ insertComment nComment + liftIO $ putStrLn $ show rev + seeOther ("/" ++ id_) (toResponse()) -- http://tazj.in/2012/02/10.155234 +currentSeconds :: IO Integer +currentSeconds = do + now <- getCurrentTime + let s = read (formatTime defaultTimeLocale "%s" now) :: Integer + return s + -- CouchDB functions getLatest :: BlogLang -> [(String, JSValue)] -> ServerPart [Entry] getLatest lang arg = do @@ -105,6 +130,11 @@ getLatest lang arg = do EN -> "latestEN" DE -> "latestDE" +insertComment :: Comment -> JSValue -> IO JSValue +insertComment c jEntry = return $ toJSON $ e { comments = c : (comments e)} + where + e = convertJSON jEntry :: Entry + makeQuery :: JSON a => a -> a -> [(String, JSValue)] makeQuery qsk qek = [("startkey", (showJSON qsk)) ,("endkey", (showJSON qek))] @@ -116,10 +146,16 @@ maybeDoc :: Data a => Maybe (Doc, Rev, JSValue) -> Maybe a maybeDoc (Just(_,_,v)) = Just( stripResult $ fromJSON v) maybeDoc Nothing = Nothing +updateDBDoc :: JSON a => Doc -> (a -> IO a) -> ServerPart (Maybe Rev) +updateDBDoc docn f = liftIO $ runCouchDB' $ getAndUpdateDoc (db "tazblog") docn f + stripResult :: Result a -> a stripResult (Ok z) = z stripResult (Error s) = error $ "JSON error: " ++ s +convertJSON :: Data a => JSValue -> a +convertJSON = stripResult . fromJSON + getMonthCount :: BlogLang -> Int -> Int -> ServerPart Int getMonthCount lang y m = do count <- queryDB (view lang) $ makeQuery startkey endkey -- cgit 1.4.1 From b2bb90beff55d1aae2026f66840e6331734512d8 Mon Sep 17 00:00:00 2001 From: "\"Vincent Ambo ext:(%22)" Date: Wed, 7 Mar 2012 13:40:47 +0100 Subject: * comment adding fixed * JSON Encoding is broken in the current Hackage version of CouchDB, thus it is necessary to build it manually and to apply this fix: https://github.com/tbh/haskell-couchdb/commit/fafd63a43607ab8d6306fa46a264f93d4921a26c --- src/Blog.hs | 6 +++++- src/Main.hs | 6 ++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Blog.hs b/src/Blog.hs index 201851cba0..0b32b7b30b 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -14,7 +14,6 @@ import Text.Blaze.Html5 (Html, (!), a, form, input, p, toHtml, label) import Text.Blaze.Html5.Attributes (action, enctype, href, name, size, type_, value) import qualified Text.Blaze.Html5 as H import qualified Text.Blaze.Html5.Attributes as A -import System.Locale (defaultTimeLocale) import Locales @@ -40,9 +39,14 @@ data Entry = Entry{ blogText :: (a -> String) -> a -> Text blogText f = T.pack . f + +-- custom list functions intersperse' :: a -> [a] -> [a] intersperse' sep l = sep : intersperse sep l +replace :: Eq a => a -> a -> [a] -> [a] +replace x y = map (\z -> if z == x then y else z) + blogTemplate :: BlogLang -> Text -> Html -> Html blogTemplate lang t_append body = H.docTypeHtml $ do --add body H.head $ do diff --git a/src/Main.hs b/src/Main.hs index 769e2180a3..d84e1d5332 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -40,6 +40,7 @@ tazBlog = do , do dir " " $ nullDir seeOther ("https://plus.google.com/115916629925754851590" :: String) (toResponse ()) , path $ \(id_ :: Int) -> getEntryLink id_ + , path $ \(year :: Int) -> path $ \(month :: Int) -> path $ \(id_ :: String) -> formatOldLink year month id_ , dir "res" $ serveDirectory DisableBrowsing [] "../res" , dir "notice" $ ok $ toResponse showSiteNotice , serveDirectory DisableBrowsing [] "../res" @@ -57,6 +58,11 @@ blogHandler lang = showIndex lang ] +formatOldLink :: Int -> Int -> String -> ServerPart Response +formatOldLink y m id_ = + flip seeOther (toResponse ()) $ + concat $ intersperse' "/" ["de", show y, show m, replace '.' '/' id_] + showEntry :: BlogLang -> String -> ServerPart Response showEntry lang id_ = do entryJS <- liftIO $ runCouchDB' $ getDoc (db "tazblog") (doc id_) -- cgit 1.4.1 From c880a6092c3c2c81095d8ae1a45c06687bc40677 Mon Sep 17 00:00:00 2001 From: "\"Vincent Ambo ext:(%22)" Date: Wed, 7 Mar 2012 14:51:45 +0100 Subject: * login page --- res/admin.css | 5 +++-- src/Blog.hs | 25 +++++++++++++++++++------ src/Main.hs | 3 +++ 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/res/admin.css b/res/admin.css index 7a4d418975..5225fe1033 100644 --- a/res/admin.css +++ b/res/admin.css @@ -40,10 +40,11 @@ body { border:1px solid #D2D2D2; border-bottom-left-radius: 12px; border-bottom-right-radius: 12px; - ; + text-align: center; font-size:12px; height:auto; + padding-left: 10px; + padding-right: 10px; min-height:200px; - padding-left:20px; width:378px; } \ No newline at end of file diff --git a/src/Blog.hs b/src/Blog.hs index 0b32b7b30b..87397ac821 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -177,13 +177,26 @@ showSiteNotice = H.docTypeHtml $ do toHtml ("50858 Köln" :: Text) H.p $ H.a ! A.href "/" ! A.style "color:black" $ "Back" -{- -Impressum +{- Administration pages -} -

Impressum und ViSdP

- -[German law demands this]

Vincent Ambo
Benfleetstr. 8
50858 Köln

Back --} +adminTemplate :: Html -> Text -> Html +adminTemplate body title = H.docTypeHtml $ do + H.head $ do + H.link ! A.rel "stylesheet" ! A.type_ "text/css" ! A.href "/res/admin.css" ! A.media "all" + H.meta ! A.httpEquiv "content-type" ! A.content "text/html;charset=UTF-8" + H.title $ toHtml $ T.append "TazBlog Admin: " title + H.body + body + +adminLogin :: Html +adminLogin = H.div ! A.class_ "loginBox" $ do + H.div ! A.class_ "loginBoxTop" $ "TazBlog Admin: Login" + H.div ! A.class_ "loginBoxMiddle" $ H.form ! A.action "/login" ! A.method "post" $ do + H.p $ "Account ID" + H.p $ H.input ! A.type_ "text" ! A.style "font-size: 2;" + ! A.name "account" ! A.value "tazjin" ! A.readonly "1" + H.p $ "Passwort" + H.p $ H.input ! A.type_ "password" ! A.style "font-size: 2;" ! A.name "pass" -- Error pages showError :: BlogError -> BlogLang -> Html diff --git a/src/Main.hs b/src/Main.hs index d84e1d5332..4a42aae85d 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -43,6 +43,9 @@ tazBlog = do , path $ \(year :: Int) -> path $ \(month :: Int) -> path $ \(id_ :: String) -> formatOldLink year month id_ , dir "res" $ serveDirectory DisableBrowsing [] "../res" , dir "notice" $ ok $ toResponse showSiteNotice + , do adminSession <- lookCookieValue "session" + ok $ toResponse ("Eingeloggt" :: String) + , dir "admin" $ ok $ toResponse $ adminTemplate adminLogin "Login" , serveDirectory DisableBrowsing [] "../res" ] -- cgit 1.4.1 From 7b8f95241325ef18ef62a0765838c2b69c924530 Mon Sep 17 00:00:00 2001 From: "\"Vincent Ambo ext:(%22)" Date: Wed, 7 Mar 2012 17:31:42 +0100 Subject: * initial work on AcidState session storage (http://happstack.com/docs/crashcourse/AcidState.html) --- src/Main.hs | 40 +++++++++++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/src/Main.hs b/src/Main.hs index 4a42aae85d..54b16fdfbc 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -1,28 +1,54 @@ -{-# LANGUAGE OverloadedStrings, ScopedTypeVariables #-} +{-# LANGUAGE OverloadedStrings, ScopedTypeVariables, GeneralizedNewtypeDeriving, + DeriveDataTypeable, FlexibleContexts, MultiParamTypeClasses, TemplateHaskell, + TypeFamilies, RecordWildCards #-} module Main where import Control.Applicative ((<$>), (<*>), optional, pure) import Control.Monad (msum) -import Data.Monoid (mempty) +import Control.Monad.State (get, put) +import Control.Monad.Reader (ask) +import Data.Acid +import Data.Acid.Advanced +import Data.Acid.Local import Data.ByteString.Char8 (ByteString) +import Data.Data (Data, Typeable) +import Data.Monoid (mempty) import Data.Text (Text) import qualified Data.Text as T import Data.Time +import Data.SafeCopy (base, deriveSafeCopy) import Database.CouchDB import Happstack.Server import Network.CGI (liftIO) -import Text.Blaze (toValue, preEscapedString) -import Text.Blaze.Html5 (Html, (!), a, form, input, p, toHtml, label) -import Text.Blaze.Html5.Attributes (action, enctype, href, name, size, type_, value) -import qualified Text.Blaze.Html5 as H -import qualified Text.Blaze.Html5.Attributes as A import Text.JSON.Generic import System.Locale (defaultTimeLocale) import Blog import Locales +{-session handling functions-} + +data SessionState = SessionState { sessions :: [(String, Integer)] } -- id/date + deriving (Eq, Ord, Read, Show, Data, Typeable) + +$(deriveSafeCopy 0 'base ''SessionState) + +initialSession :: SessionState +initialSession = SessionState [] + +addSession :: (String, Integer) -> Update SessionState [(String, Integer)] +addSession newS = do + s@SessionState{..} <- get + let newSessions = newS : sessions + put $ s{ sessions = newSessions } + return newSessions + +querySessions :: Query SessionState [(String, Integer)] +querySessions = sessions <$> ask + +$(makeAcidic ''SessionState ['addSession, 'querySessions]) + tmpPolicy :: BodyPolicy tmpPolicy = (defaultBodyPolicy "./tmp/" 0 1000 1000) -- cgit 1.4.1 From bbdfa3eae29e124772257e5aaecb3ee042514769 Mon Sep 17 00:00:00 2001 From: "\"Vincent Ambo ext:(%22)" Date: Thu, 8 Mar 2012 11:42:10 +0100 Subject: * initializing Acid sessions * guardSession --- src/Main.hs | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/Main.hs b/src/Main.hs index 54b16fdfbc..b0b06068a9 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -5,7 +5,8 @@ module Main where import Control.Applicative ((<$>), (<*>), optional, pure) -import Control.Monad (msum) +import Control.Exception (bracket) +import Control.Monad (msum, mzero, when, unless) import Control.Monad.State (get, put) import Control.Monad.Reader (ask) import Data.Acid @@ -49,16 +50,31 @@ querySessions = sessions <$> ask $(makeAcidic ''SessionState ['addSession, 'querySessions]) +guardSession :: AcidState SessionState -> ServerPartT IO () +guardSession acid = do + sID <- lookCookieValue "session" + sDate <- readCookieValue "sdate" + cSessions <- query' acid QuerySessions + cDate <- liftIO $ currentSeconds + when (not $ elem (sID, sDate) cSessions) + mzero + when (32400 > (cDate - sDate)) + mzero + +{- Server -} + tmpPolicy :: BodyPolicy tmpPolicy = (defaultBodyPolicy "./tmp/" 0 1000 1000) main :: IO() main = do putStrLn ("TazBlog " ++ version ++ " in Haskell starting") - simpleHTTP nullConf tazBlog + bracket (openLocalState initialSession) + (createCheckpointAndClose) + (\acid -> simpleHTTP nullConf $ tazBlog acid) -tazBlog :: ServerPart Response -tazBlog = do +tazBlog :: AcidState SessionState -> ServerPart Response +tazBlog acid = do msum [ dir (show DE) $ blogHandler DE , dir (show EN) $ blogHandler EN , do nullDir -- cgit 1.4.1 From 4eacefe854f806f67792a8d0b6c9a99c58797d25 Mon Sep 17 00:00:00 2001 From: "\"Vincent Ambo ext:(%22)" Date: Fri, 9 Mar 2012 17:57:53 +0100 Subject: * broken version of Acid State stuff * AccountState containing data of type Account * hashString functions --- src/Blog.hs | 7 ++++- src/Main.hs | 93 +++++++++++++++++++++++++++++++++++++++++++++++-------------- 2 files changed, 78 insertions(+), 22 deletions(-) diff --git a/src/Blog.hs b/src/Blog.hs index 87397ac821..aa1882073e 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -23,6 +23,11 @@ data Comment = Comment{ cdate :: Integer } deriving (Show, Data, Typeable) +data Author = Author { + username :: String, + password :: String +} deriving (Show, Data, Typeable) + data Entry = Entry{ _id :: String, year :: Int, @@ -196,7 +201,7 @@ adminLogin = H.div ! A.class_ "loginBox" $ do H.p $ H.input ! A.type_ "text" ! A.style "font-size: 2;" ! A.name "account" ! A.value "tazjin" ! A.readonly "1" H.p $ "Passwort" - H.p $ H.input ! A.type_ "password" ! A.style "font-size: 2;" ! A.name "pass" + H.p $ H.input ! A.type_ "password" ! A.style "font-size: 2;" ! A.name "password" -- Error pages showError :: BlogError -> BlogLang -> Html diff --git a/src/Main.hs b/src/Main.hs index b0b06068a9..7990b8811a 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -9,10 +9,12 @@ import Control.Exception (bracket) import Control.Monad (msum, mzero, when, unless) import Control.Monad.State (get, put) import Control.Monad.Reader (ask) +import qualified Crypto.Hash.SHA512 as SHA import Data.Acid import Data.Acid.Advanced import Data.Acid.Local -import Data.ByteString.Char8 (ByteString) +import qualified Data.ByteString.Base64 as B64 (encode) +import Data.ByteString.Char8 (ByteString, pack) import Data.Data (Data, Typeable) import Data.Monoid (mempty) import Data.Text (Text) @@ -23,21 +25,30 @@ import Database.CouchDB import Happstack.Server import Network.CGI (liftIO) import Text.JSON.Generic +import System.Environment(getEnv) import System.Locale (defaultTimeLocale) import Blog import Locales -{-session handling functions-} - data SessionState = SessionState { sessions :: [(String, Integer)] } -- id/date deriving (Eq, Ord, Read, Show, Data, Typeable) -$(deriveSafeCopy 0 'base ''SessionState) - initialSession :: SessionState initialSession = SessionState [] +$(deriveSafeCopy 0 'base ''SessionState) + + +data AccountState = AccountState { accounts :: [Account] } + deriving (Read, Show, Data, Typeable) + +data Account = Account { account :: String + , password :: ByteString + } deriving (Read, Show, Data, Typeable) + +{-session handling functions-} + addSession :: (String, Integer) -> Update SessionState [(String, Integer)] addSession newS = do s@SessionState{..} <- get @@ -49,6 +60,42 @@ querySessions :: Query SessionState [(String, Integer)] querySessions = sessions <$> ask $(makeAcidic ''SessionState ['addSession, 'querySessions]) +$(makeAcidic ''AccountState []) +{- various functions -} + +hashString :: String -> ByteString +hashString = B64.encode . SHA.hash . pack + +{- Server -} + +tmpPolicy :: BodyPolicy +tmpPolicy = (defaultBodyPolicy "./tmp/" 0 1000 1000) + +main :: IO() +main = do + putStrLn ("TazBlog " ++ version ++ " in Haskell starting") + tbDir <- getEnv "TAZBLOG" + bracket (openLocalStateFrom (tbDir ++ "/State/SessionState") initialAccounts) + (createCheckpointAndClose) + (\sessionAcid -> bracket (openLocalStateFrom (tbDir ++ "/State/AccountState") ) + (createCheckpointAndClose) + (\accountAcid -> simpleHTTP nullConf $ + tazBlog sessionAcid accountAcid)) + + + + + +initialAccounts :: AccountState +initialAccounts = [] + +askAccount :: IO Account +askAccount = do + putStrLn "Enter name for the account:" + n <- getLine + putStrLn "Enter password for the account:" + p <- getLine + return $ Account n $ hashString p guardSession :: AcidState SessionState -> ServerPartT IO () guardSession acid = do @@ -61,18 +108,6 @@ guardSession acid = do when (32400 > (cDate - sDate)) mzero -{- Server -} - -tmpPolicy :: BodyPolicy -tmpPolicy = (defaultBodyPolicy "./tmp/" 0 1000 1000) - -main :: IO() -main = do - putStrLn ("TazBlog " ++ version ++ " in Haskell starting") - bracket (openLocalState initialSession) - (createCheckpointAndClose) - (\acid -> simpleHTTP nullConf $ tazBlog acid) - tazBlog :: AcidState SessionState -> ServerPart Response tazBlog acid = do msum [ dir (show DE) $ blogHandler DE @@ -85,9 +120,10 @@ tazBlog acid = do , path $ \(year :: Int) -> path $ \(month :: Int) -> path $ \(id_ :: String) -> formatOldLink year month id_ , dir "res" $ serveDirectory DisableBrowsing [] "../res" , dir "notice" $ ok $ toResponse showSiteNotice - , do adminSession <- lookCookieValue "session" - ok $ toResponse ("Eingeloggt" :: String) + , do dir "admin" $ guardSession acid + adminHandler , dir "admin" $ ok $ toResponse $ adminTemplate adminLogin "Login" + , dir "dologin" $ processLogin acid , serveDirectory DisableBrowsing [] "../res" ] @@ -103,6 +139,9 @@ blogHandler lang = showIndex lang ] +adminHandler :: ServerPart Response +adminHandler = undefined + formatOldLink :: Int -> Int -> String -> ServerPart Response formatOldLink y m id_ = flip seeOther (toResponse ()) $ @@ -162,6 +201,14 @@ addComment id_ = do liftIO $ putStrLn $ show rev seeOther ("/" ++ id_) (toResponse()) +processLogin :: AcidState SessionState -> ServerPart Response +processLogin acid = do + decodeBody tmpPolicy + account <- look "account" + password <- look "password" + ok $ toResponse ("Shut up" :: String) + + -- http://tazj.in/2012/02/10.155234 currentSeconds :: IO Integer @@ -170,7 +217,8 @@ currentSeconds = do let s = read (formatTime defaultTimeLocale "%s" now) :: Integer return s --- CouchDB functions +{- CouchDB functions -} + getLatest :: BlogLang -> [(String, JSValue)] -> ServerPart [Entry] getLatest lang arg = do queryResult <- queryDB view arg @@ -221,7 +269,7 @@ getMonthCount lang y m = do view EN = "countEN" --- CouchDB View Setup +{- CouchDB View Setup -} latestDEView = "function(doc){ if(doc.lang == 'DE'){ emit([doc.year, doc.month, doc.day, doc._id], doc); } }" latestENView = "function(doc){ if(doc.lang == 'EN'){ emit([doc.year, doc.month, doc.day, doc._id], doc); } }" countDEView = "function(doc){ if(doc.lang == 'DE'){ emit(['count', doc.year, doc.month, doc.day, doc._id], 1); } }" @@ -236,3 +284,6 @@ countEN = ViewMapReduce "countEN" countENView countReduce setupBlogViews :: IO () setupBlogViews = runCouchDB' $ newView "tazblog" "entries" [latestDE, latestEN, countDE, countEN] + + + -- cgit 1.4.1 From 6d9144a7ee6030f1c46320a0bc3a3d37982d171f Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Mon, 12 Mar 2012 04:32:15 +0100 Subject: * added Acid-State conversion program (this is untested, but it compiles) --- tools/acid-migrate/Acid.hs | 258 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 258 insertions(+) create mode 100644 tools/acid-migrate/Acid.hs diff --git a/tools/acid-migrate/Acid.hs b/tools/acid-migrate/Acid.hs new file mode 100644 index 0000000000..ab81cdceeb --- /dev/null +++ b/tools/acid-migrate/Acid.hs @@ -0,0 +1,258 @@ +{-# LANGUAGE DeriveDataTypeable, GeneralizedNewtypeDeriving, RecordWildCards, +TemplateHaskell, TypeFamilies, OverloadedStrings, ScopedTypeVariables #-} + +module Main where +import Control.Applicative ((<$>), optional) +import Control.Exception (bracket) +import Control.Monad (msum, mzero) +import Control.Monad.IO.Class (MonadIO) +import Control.Monad.Reader (ask) +import Control.Monad.State (get, put) +import Control.Monad.Trans (liftIO) +import Data.Acid +import Data.Acid.Advanced +import Data.Acid.Local +import Data.ByteString (ByteString) +import Data.Data (Data, Typeable) +import Data.IxSet (Indexable(..), IxSet(..), (@=), Proxy(..), getOne, ixFun, ixSet) +import Data.SafeCopy (SafeCopy, base, deriveSafeCopy) +import Data.Text (Text, pack) +import Data.Text.Lazy (toStrict) +import Data.Time +import Happstack.Server hiding (Session) + +import qualified Crypto.Hash.SHA512 as SHA (hash) +import qualified Data.ByteString.Char8 as B +import qualified Data.ByteString.Base64 as B64 (encode) +import qualified Data.IxSet as IxSet +import qualified Data.Text as Text + + +{-CouchDB imports-} + +import Database.CouchDB +import Database.CouchDB.JSON +import Text.JSON +import Data.List (intersperse) +import System.Locale (defaultTimeLocale) + +-- data types and acid-state setup + +newtype EntryId = EntryId { unEntryId :: Integer } + deriving (Eq, Ord, Data, Enum, Typeable, SafeCopy) + +data BlogLang = EN | DE + deriving (Eq, Ord, Data, Typeable) + +instance Show BlogLang where + show DE = "de" + show EN = "en" + +$(deriveSafeCopy 0 'base ''BlogLang) + +data Comment = Comment {  + cauthor :: Text, + ctext :: Text, + cdate :: UTCTime +} deriving (Eq, Ord, Data, Typeable) + +$(deriveSafeCopy 0 'base ''Comment) + +data Entry = Entry { + entryId :: EntryId, + lang :: BlogLang, + author :: Text, + title :: Text, + btext :: Text, + mtext :: Text, + edate :: UTCTime, + tags :: [Text], + comments :: [Comment] +} deriving (Eq, Ord, Data, Typeable) + +$(deriveSafeCopy 0 'base ''Entry) + +-- ixSet requires different datatypes for field indexes, so let's define some +newtype Author = Author Text deriving (Eq, Ord, Data, Typeable, SafeCopy) +newtype Title = Title Text deriving (Eq, Ord, Data, Typeable, SafeCopy) +newtype BText = BText Text deriving (Eq, Ord, Data, Typeable, SafeCopy) -- standard text +newtype MText = MText Text deriving (Eq, Ord, Data, Typeable, SafeCopy) -- "read more" text +newtype Tag = Tag Text deriving (Eq, Ord, Data, Typeable, SafeCopy) +newtype EDate = EDate UTCTime deriving (Eq, Ord, Data, Typeable, SafeCopy) +newtype SDate = SDate UTCTime deriving (Eq, Ord, Data, Typeable, SafeCopy) +newtype Username = Username Text deriving (Eq, Ord, Data, Typeable, SafeCopy) +newtype SessionID = SessionID Text deriving (Eq, Ord, Data, Typeable, SafeCopy) + +instance Indexable Entry where + empty = ixSet [ ixFun $ \e -> [ entryId e] + , ixFun $ (:[]) . lang + , ixFun $ \e -> [ Author $ author e ] + , ixFun $ \e -> [ Title $ title e] + , ixFun $ \e -> [ BText $ btext e] + , ixFun $ \e -> [ MText $ mtext e] + , ixFun $ \e -> [ EDate $ edate e] + , ixFun $ \e -> map Tag (tags e) + , ixFun $ comments + ] + +data User = User { + username :: Text, + password :: ByteString +} deriving (Eq, Ord, Data, Typeable) + +$(deriveSafeCopy 0 'base ''User) + +data Session = Session { + sessionID :: Text, + user :: User, + sdate :: UTCTime +} deriving (Eq, Ord, Data, Typeable) + +$(deriveSafeCopy 0 'base ''Session) + +instance Indexable User where + empty = ixSet [ ixFun $ \u -> [Username $ username u] + , ixFun $ (:[]) . password + ] + +instance Indexable Session where + empty = ixSet [ ixFun $ \s -> [SessionID $ sessionID s] + , ixFun $ (:[]) . user + , ixFun $ \s -> [SDate $ sdate s] + ] + +data Blog = Blog { + blogSessions :: IxSet Session, + blogUsers :: IxSet User, + blogEntries :: IxSet Entry +} deriving (Data, Typeable) + +$(deriveSafeCopy 0 'base ''Blog) + +initialBlogState :: Blog +initialBlogState = + Blog { blogSessions = empty + , blogUsers = empty + , blogEntries = empty } + +-- acid-state database functions (purity is necessary!) + +insertEntry :: Entry -> Update Blog Entry +insertEntry e = + do b@Blog{..} <- get + put $ b { blogEntries = IxSet.insert e blogEntries } + return e + +updateEntry :: Entry -> Update Blog Entry +updateEntry e = + do b@Blog{..} <- get + put $ b { blogEntries = IxSet.updateIx (entryId e) e blogEntries} + return e + +getPost :: EntryId -> Query Blog (Maybe Entry) +getPost eid = + do b@Blog{..} <- ask + return $ getOne $ blogEntries @= eid + +latestPosts :: Query Blog [Entry] +latestPosts = + do b@Blog{..} <- ask + return $ IxSet.toDescList (Proxy :: Proxy UTCTime) $ blogEntries + +addSession :: Text -> User -> UTCTime -> Update Blog Session +addSession sId u t = + do b@Blog{..} <- get + let s = Session sId u t + put $ b { blogSessions = IxSet.insert s blogSessions} + return s + +addUser :: Text -> String -> Update Blog User +addUser un pw = + do b@Blog{..} <- get + let u = User un $ hashString pw + put $ b { blogUsers = IxSet.insert u blogUsers} + return u + +-- various functions +hashString :: String -> ByteString +hashString = B64.encode . SHA.hash . B.pack + +$(makeAcidic ''Blog + [ 'insertEntry + , 'updateEntry + , 'getPost + , 'latestPosts + , 'addSession + , 'addUser + ]) + +-- CouchDB database functions +instance JSON Comment where + showJSON = undefined + readJSON val = do + obj <- jsonObject val + scauthor <- jsonField "cauthor" obj + scdate <- jsonField "cdate" obj + sctext <- jsonField "cdate" obj + return $ Comment (pack scauthor) (pack sctext) (parseSeconds scdate) + +instance JSON Entry where + showJSON = undefined + readJSON val = do + obj <- jsonObject val + sauthor <- jsonField "author" obj + stitle <- jsonField "title" obj + day <- jsonField "day" obj + month <- jsonField "month" obj + year <- jsonField "year" obj + stext <- jsonField "text" obj + comments <- jsonField "comments" obj + oldid <- jsonField "id_" obj + let leTime = parseShittyTime year month day oldid + return $ Entry (EntryId $ getUnixTime leTime) DE (pack sauthor) (pack stitle) (pack stext) (Text.empty) + leTime [] comments + + +getUnixTime :: UTCTime -> Integer +getUnixTime t = read $ formatTime defaultTimeLocale "%s" t + +parseSeconds :: Integer -> UTCTime +parseSeconds t = readTime defaultTimeLocale "%s" $ show t + +parseShittyTime :: Int -> Int -> Int -> String -> UTCTime +parseShittyTime y m d i = readTime defaultTimeLocale "%Y %m %e %k:%M:%S" newPartTime + where + firstPart = take 2 i + secondPart = take 2 $ drop 2 i + thirdPart = drop 4 i + newPartTime = concat $ intersperse " " [show y, showMonth m, show d, " "] ++ + intersperse ":" [firstPart, secondPart, thirdPart] + showMonth mn + | mn < 10 = "0" ++ show mn + | otherwise = show mn + +getOldEntries = runCouchDB' $ queryView (db "tazblog") (doc "entries") (doc "latestDE") [] + +parseOldEntries :: IO [Entry] +parseOldEntries = do + queryResult <- getOldEntries + let entries = map (stripResult . readJSON . snd) queryResult + return entries + +stripResult :: Result a -> a +stripResult (Ok z) = z +stripResult (Error s) = error $ "JSON error: " ++ s + +pasteToDB :: AcidState Blog -> Entry -> IO (EventResult InsertEntry) +pasteToDB acid e = update' acid (InsertEntry e) + +main :: IO() +main = do + bracket (openLocalState initialBlogState) + (createCheckpointAndClose) + (\acid -> convertEntries acid) + +convertEntries acid = do + entries <- parseOldEntries + let x = map (pasteToDB acid) entries + putStrLn "Conversion successful" -- cgit 1.4.1 From c6124d9aa71f1a6241e4d3d816e80dee49b4cc6b Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Mon, 12 Mar 2012 09:47:30 +0100 Subject: * changes to migration script. As expected it doesn't work yet --- tools/acid-migrate/Acid.hs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tools/acid-migrate/Acid.hs b/tools/acid-migrate/Acid.hs index ab81cdceeb..e4b00a6cae 100644 --- a/tools/acid-migrate/Acid.hs +++ b/tools/acid-migrate/Acid.hs @@ -30,7 +30,7 @@ import qualified Data.Text as Text {-CouchDB imports-} -import Database.CouchDB +import Database.CouchDB hiding (runCouchDB') import Database.CouchDB.JSON import Text.JSON import Data.List (intersperse) @@ -187,6 +187,10 @@ $(makeAcidic ''Blog ]) -- CouchDB database functions + +runCouchDB' :: CouchMonad a -> IO a +runCouchDB' = runCouchDB "hackbox.local" 5984 + instance JSON Comment where showJSON = undefined readJSON val = do @@ -207,7 +211,7 @@ instance JSON Entry where year <- jsonField "year" obj stext <- jsonField "text" obj comments <- jsonField "comments" obj - oldid <- jsonField "id_" obj + oldid <- jsonField "_id" obj let leTime = parseShittyTime year month day oldid return $ Entry (EntryId $ getUnixTime leTime) DE (pack sauthor) (pack stitle) (pack stext) (Text.empty) leTime [] comments @@ -255,4 +259,5 @@ main = do convertEntries acid = do entries <- parseOldEntries let x = map (pasteToDB acid) entries - putStrLn "Conversion successful" + let titles = map (title) entries + putStrLn $ show titles -- cgit 1.4.1 From e6b91ce813e77a0fd87e692c7bab1d066ddf1e7b Mon Sep 17 00:00:00 2001 From: "\"Vincent Ambo ext:(%22)" Date: Mon, 12 Mar 2012 12:52:48 +0100 Subject: acid-migrate: * show instance for EntryId * Comment/Entry deriving Show * trying to force explicit evaluation --- tools/acid-migrate/Acid.hs | 237 +++++++++++++++++++++++---------------------- 1 file changed, 122 insertions(+), 115 deletions(-) diff --git a/tools/acid-migrate/Acid.hs b/tools/acid-migrate/Acid.hs index e4b00a6cae..fa7ee06d15 100644 --- a/tools/acid-migrate/Acid.hs +++ b/tools/acid-migrate/Acid.hs @@ -1,5 +1,5 @@ {-# LANGUAGE DeriveDataTypeable, GeneralizedNewtypeDeriving, RecordWildCards, -TemplateHaskell, TypeFamilies, OverloadedStrings, ScopedTypeVariables #-} +TemplateHaskell, TypeFamilies, OverloadedStrings, ScopedTypeVariables, BangPatterns #-} module Main where import Control.Applicative ((<$>), optional) @@ -33,7 +33,7 @@ import qualified Data.Text as Text import Database.CouchDB hiding (runCouchDB') import Database.CouchDB.JSON import Text.JSON -import Data.List (intersperse) +import Data.List (intersperse, (\\)) import System.Locale (defaultTimeLocale) -- data types and acid-state setup @@ -41,34 +41,37 @@ import System.Locale (defaultTimeLocale) newtype EntryId = EntryId { unEntryId :: Integer } deriving (Eq, Ord, Data, Enum, Typeable, SafeCopy) +instance Show EntryId where + show = show . unEntryId + data BlogLang = EN | DE - deriving (Eq, Ord, Data, Typeable) + deriving (Eq, Ord, Data, Typeable) instance Show BlogLang where - show DE = "de" - show EN = "en" + show DE = "de" + show EN = "en" $(deriveSafeCopy 0 'base ''BlogLang) data Comment = Comment {  - cauthor :: Text, - ctext :: Text, - cdate :: UTCTime -} deriving (Eq, Ord, Data, Typeable) + cauthor :: Text, + ctext :: Text, + cdate :: UTCTime +} deriving (Eq, Ord, Show, Data, Typeable) $(deriveSafeCopy 0 'base ''Comment) data Entry = Entry { - entryId :: EntryId, - lang :: BlogLang, - author :: Text, - title :: Text, - btext :: Text, - mtext :: Text, - edate :: UTCTime, - tags :: [Text], - comments :: [Comment] -} deriving (Eq, Ord, Data, Typeable) + entryId :: EntryId, + lang :: BlogLang, + author :: Text, + title :: Text, + btext :: Text, + mtext :: Text, + edate :: UTCTime, + tags :: [Text], + comments :: [Comment] +} deriving (Eq, Ord, Show, Data, Typeable) $(deriveSafeCopy 0 'base ''Entry) @@ -77,144 +80,144 @@ newtype Author = Author Text deriving (Eq, Ord, Data, Typeable, SafeCopy) newtype Title = Title Text deriving (Eq, Ord, Data, Typeable, SafeCopy) newtype BText = BText Text deriving (Eq, Ord, Data, Typeable, SafeCopy) -- standard text newtype MText = MText Text deriving (Eq, Ord, Data, Typeable, SafeCopy) -- "read more" text -newtype Tag = Tag Text deriving (Eq, Ord, Data, Typeable, SafeCopy) +newtype Tag = Tag Text deriving (Eq, Ord, Data, Typeable, SafeCopy) newtype EDate = EDate UTCTime deriving (Eq, Ord, Data, Typeable, SafeCopy) newtype SDate = SDate UTCTime deriving (Eq, Ord, Data, Typeable, SafeCopy) newtype Username = Username Text deriving (Eq, Ord, Data, Typeable, SafeCopy) newtype SessionID = SessionID Text deriving (Eq, Ord, Data, Typeable, SafeCopy) instance Indexable Entry where - empty = ixSet [ ixFun $ \e -> [ entryId e] - , ixFun $ (:[]) . lang - , ixFun $ \e -> [ Author $ author e ] - , ixFun $ \e -> [ Title $ title e] - , ixFun $ \e -> [ BText $ btext e] - , ixFun $ \e -> [ MText $ mtext e] - , ixFun $ \e -> [ EDate $ edate e] - , ixFun $ \e -> map Tag (tags e) - , ixFun $ comments - ] + empty = ixSet [ ixFun $ \e -> [ entryId e] + , ixFun $ (:[]) . lang + , ixFun $ \e -> [ Author $ author e ] + , ixFun $ \e -> [ Title $ title e] + , ixFun $ \e -> [ BText $ btext e] + , ixFun $ \e -> [ MText $ mtext e] + , ixFun $ \e -> [ EDate $ edate e] + , ixFun $ \e -> map Tag (tags e) + , ixFun $ comments + ] data User = User { - username :: Text, - password :: ByteString + username :: Text, + password :: ByteString } deriving (Eq, Ord, Data, Typeable) $(deriveSafeCopy 0 'base ''User) data Session = Session { - sessionID :: Text, - user :: User, - sdate :: UTCTime + sessionID :: Text, + user :: User, + sdate :: UTCTime } deriving (Eq, Ord, Data, Typeable) $(deriveSafeCopy 0 'base ''Session) instance Indexable User where - empty = ixSet [ ixFun $ \u -> [Username $ username u] - , ixFun $ (:[]) . password - ] + empty = ixSet [ ixFun $ \u -> [Username $ username u] + , ixFun $ (:[]) . password + ] instance Indexable Session where - empty = ixSet [ ixFun $ \s -> [SessionID $ sessionID s] - , ixFun $ (:[]) . user - , ixFun $ \s -> [SDate $ sdate s] - ] + empty = ixSet [ ixFun $ \s -> [SessionID $ sessionID s] + , ixFun $ (:[]) . user + , ixFun $ \s -> [SDate $ sdate s] + ] data Blog = Blog { - blogSessions :: IxSet Session, - blogUsers :: IxSet User, - blogEntries :: IxSet Entry + blogSessions :: IxSet Session, + blogUsers :: IxSet User, + blogEntries :: IxSet Entry } deriving (Data, Typeable) $(deriveSafeCopy 0 'base ''Blog) initialBlogState :: Blog initialBlogState = - Blog { blogSessions = empty - , blogUsers = empty - , blogEntries = empty } + Blog { blogSessions = empty + , blogUsers = empty + , blogEntries = empty } -- acid-state database functions (purity is necessary!) insertEntry :: Entry -> Update Blog Entry insertEntry e = - do b@Blog{..} <- get - put $ b { blogEntries = IxSet.insert e blogEntries } - return e + do b@Blog{..} <- get + put $ b { blogEntries = IxSet.insert e blogEntries } + return e updateEntry :: Entry -> Update Blog Entry updateEntry e = - do b@Blog{..} <- get - put $ b { blogEntries = IxSet.updateIx (entryId e) e blogEntries} - return e + do b@Blog{..} <- get + put $ b { blogEntries = IxSet.updateIx (entryId e) e blogEntries} + return e getPost :: EntryId -> Query Blog (Maybe Entry) getPost eid = - do b@Blog{..} <- ask - return $ getOne $ blogEntries @= eid + do b@Blog{..} <- ask + return $ getOne $ blogEntries @= eid latestPosts :: Query Blog [Entry] latestPosts = - do b@Blog{..} <- ask - return $ IxSet.toDescList (Proxy :: Proxy UTCTime) $ blogEntries + do b@Blog{..} <- ask + return $ IxSet.toDescList (Proxy :: Proxy UTCTime) $ blogEntries addSession :: Text -> User -> UTCTime -> Update Blog Session addSession sId u t = - do b@Blog{..} <- get - let s = Session sId u t - put $ b { blogSessions = IxSet.insert s blogSessions} - return s + do b@Blog{..} <- get + let s = Session sId u t + put $ b { blogSessions = IxSet.insert s blogSessions} + return s addUser :: Text -> String -> Update Blog User addUser un pw = - do b@Blog{..} <- get - let u = User un $ hashString pw - put $ b { blogUsers = IxSet.insert u blogUsers} - return u + do b@Blog{..} <- get + let u = User un $ hashString pw + put $ b { blogUsers = IxSet.insert u blogUsers} + return u -- various functions hashString :: String -> ByteString hashString = B64.encode . SHA.hash . B.pack $(makeAcidic ''Blog - [ 'insertEntry - , 'updateEntry - , 'getPost - , 'latestPosts - , 'addSession - , 'addUser - ]) + [ 'insertEntry + , 'updateEntry + , 'getPost + , 'latestPosts + , 'addSession + , 'addUser + ]) -- CouchDB database functions runCouchDB' :: CouchMonad a -> IO a -runCouchDB' = runCouchDB "hackbox.local" 5984 +runCouchDB' = runCouchDB "127.0.0.1" 5984 instance JSON Comment where - showJSON = undefined - readJSON val = do - obj <- jsonObject val - scauthor <- jsonField "cauthor" obj - scdate <- jsonField "cdate" obj - sctext <- jsonField "cdate" obj - return $ Comment (pack scauthor) (pack sctext) (parseSeconds scdate) + showJSON = undefined + readJSON val = do + obj <- jsonObject val + scauthor <- jsonField "cauthor" obj + scdate <- jsonField "cdate" obj + sctext <- jsonField "cdate" obj + return $ Comment (pack scauthor) (pack sctext) (parseSeconds scdate) instance JSON Entry where - showJSON = undefined - readJSON val = do - obj <- jsonObject val - sauthor <- jsonField "author" obj - stitle <- jsonField "title" obj - day <- jsonField "day" obj - month <- jsonField "month" obj - year <- jsonField "year" obj - stext <- jsonField "text" obj - comments <- jsonField "comments" obj - oldid <- jsonField "_id" obj - let leTime = parseShittyTime year month day oldid - return $ Entry (EntryId $ getUnixTime leTime) DE (pack sauthor) (pack stitle) (pack stext) (Text.empty) - leTime [] comments + showJSON = undefined + readJSON val = do + obj <- jsonObject val + sauthor <- jsonField "author" obj + stitle <- jsonField "title" obj + day <- jsonField "day" obj + month <- jsonField "month" obj + year <- jsonField "year" obj + stext <- jsonField "text" obj + --comments <- jsonField "comments" obj + oldid <- jsonField "_id" obj + let leTime = parseShittyTime year month day oldid + return $ Entry (EntryId $ getUnixTime leTime) DE (pack sauthor) (pack $ stitle \\ "\n") (pack stext) (Text.empty) + leTime [] [] getUnixTime :: UTCTime -> Integer @@ -225,39 +228,43 @@ parseSeconds t = readTime defaultTimeLocale "%s" $ show t parseShittyTime :: Int -> Int -> Int -> String -> UTCTime parseShittyTime y m d i = readTime defaultTimeLocale "%Y %m %e %k:%M:%S" newPartTime - where - firstPart = take 2 i - secondPart = take 2 $ drop 2 i - thirdPart = drop 4 i - newPartTime = concat $ intersperse " " [show y, showMonth m, show d, " "] ++ - intersperse ":" [firstPart, secondPart, thirdPart] - showMonth mn - | mn < 10 = "0" ++ show mn - | otherwise = show mn + where + firstPart = take 2 i + secondPart = take 2 $ drop 2 i + thirdPart = drop 4 i + newPartTime = concat $ intersperse " " [show y, showMonth m, show d, " "] ++ + intersperse ":" [firstPart, secondPart, thirdPart] + showMonth mn + | mn < 10 = "0" ++ show mn + | otherwise = show mn getOldEntries = runCouchDB' $ queryView (db "tazblog") (doc "entries") (doc "latestDE") [] parseOldEntries :: IO [Entry] parseOldEntries = do - queryResult <- getOldEntries - let entries = map (stripResult . readJSON . snd) queryResult - return entries + queryResult <- getOldEntries + let entries = map (stripResult . readJSON . snd) queryResult + return entries stripResult :: Result a -> a stripResult (Ok z) = z stripResult (Error s) = error $ "JSON error: " ++ s pasteToDB :: AcidState Blog -> Entry -> IO (EventResult InsertEntry) -pasteToDB acid e = update' acid (InsertEntry e) +pasteToDB acid !e = update' acid (InsertEntry e) main :: IO() main = do - bracket (openLocalState initialBlogState) - (createCheckpointAndClose) - (\acid -> convertEntries acid) + bracket (openLocalState initialBlogState) + (createCheckpointAndClose) + (\acid -> convertEntries acid) convertEntries acid = do - entries <- parseOldEntries - let x = map (pasteToDB acid) entries - let titles = map (title) entries - putStrLn $ show titles + entries <- parseOldEntries + let x = map (pasteToDB acid) entries + let y = map forceHack x + putStrLn $ show entries + where + forceHack !x = do + xy <- x + return x -- cgit 1.4.1 From 93268c683cf47db643a13b4a4f3a2291bc4df481 Mon Sep 17 00:00:00 2001 From: "\"Vincent Ambo ext:(%22)" Date: Mon, 12 Mar 2012 14:23:45 +0100 Subject: acid-migrate: * successfully forced evaluation --- tools/acid-migrate/Acid.hs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tools/acid-migrate/Acid.hs b/tools/acid-migrate/Acid.hs index fa7ee06d15..756f215e55 100644 --- a/tools/acid-migrate/Acid.hs +++ b/tools/acid-migrate/Acid.hs @@ -261,10 +261,10 @@ main = do convertEntries acid = do entries <- parseOldEntries - let x = map (pasteToDB acid) entries - let y = map forceHack x - putStrLn $ show entries + let r = map forceHack entries + rs <- sequence r + putStrLn $ show rs where forceHack !x = do - xy <- x - return x + xy <- pasteToDB acid x + return $ show xy -- cgit 1.4.1 From 1c4db3b576febde673a1b0bb615b6aee174f9cee Mon Sep 17 00:00:00 2001 From: "\"Vincent Ambo ext:(%22)" Date: Mon, 12 Mar 2012 15:12:39 +0100 Subject: acid-migrate: * successful conversion and test --- tools/acid-migrate/Acid.hs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/tools/acid-migrate/Acid.hs b/tools/acid-migrate/Acid.hs index 756f215e55..bc360694a6 100644 --- a/tools/acid-migrate/Acid.hs +++ b/tools/acid-migrate/Acid.hs @@ -160,7 +160,7 @@ getPost eid = latestPosts :: Query Blog [Entry] latestPosts = do b@Blog{..} <- ask - return $ IxSet.toDescList (Proxy :: Proxy UTCTime) $ blogEntries + return $ IxSet.toDescList (Proxy :: Proxy EDate) $ blogEntries addSession :: Text -> User -> UTCTime -> Update Blog Session addSession sId u t = @@ -199,9 +199,10 @@ instance JSON Comment where readJSON val = do obj <- jsonObject val scauthor <- jsonField "cauthor" obj - scdate <- jsonField "cdate" obj - sctext <- jsonField "cdate" obj - return $ Comment (pack scauthor) (pack sctext) (parseSeconds scdate) + jsscdate <- jsonField "cdate" obj :: Result JSValue + let rcdate = stripResult $ jsonInt jsscdate + sctext <- jsonField "ctext" obj + return $ Comment (pack scauthor) (pack sctext) (parseSeconds rcdate) instance JSON Entry where showJSON = undefined @@ -213,11 +214,11 @@ instance JSON Entry where month <- jsonField "month" obj year <- jsonField "year" obj stext <- jsonField "text" obj - --comments <- jsonField "comments" obj + comments <- jsonField "comments" obj oldid <- jsonField "_id" obj let leTime = parseShittyTime year month day oldid return $ Entry (EntryId $ getUnixTime leTime) DE (pack sauthor) (pack $ stitle \\ "\n") (pack stext) (Text.empty) - leTime [] [] + leTime [] comments getUnixTime :: UTCTime -> Integer @@ -268,3 +269,9 @@ convertEntries acid = do forceHack !x = do xy <- pasteToDB acid x return $ show xy + +testThis :: IO () +testThis = do + acid <- openLocalState initialBlogState + allE <- query' acid LatestPosts + putStrLn $ show allE \ No newline at end of file -- cgit 1.4.1 From 6092eb6f5e095c7a20f64e4149399391506dd9a0 Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Tue, 13 Mar 2012 05:31:13 +0100 Subject: * blog is now running off acid-state (this thing is *fast*) --- src/Blog.hs | 70 ++++--------- src/BlogDB.hs | 208 +++++++++++++++++++++++++++++++++++++ src/Locales.hs | 13 ++- src/Main.hs | 250 +++++++++------------------------------------ tools/acid-migrate/Acid.hs | 6 +- 5 files changed, 286 insertions(+), 261 deletions(-) create mode 100644 src/BlogDB.hs diff --git a/src/Blog.hs b/src/Blog.hs index aa1882073e..5f95d70058 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -1,4 +1,4 @@ -{-# LANGUAGE OverloadedStrings, ScopedTypeVariables, DeriveDataTypeable #-} +{-# LANGUAGE OverloadedStrings, ScopedTypeVariables, DeriveDataTypeable, RecordWildCards #-} module Blog where @@ -16,34 +16,7 @@ import qualified Text.Blaze.Html5 as H import qualified Text.Blaze.Html5.Attributes as A import Locales - -data Comment = Comment{ - cauthor :: String, - ctext :: String, - cdate :: Integer -} deriving (Show, Data, Typeable) - -data Author = Author { - username :: String, - password :: String -} deriving (Show, Data, Typeable) - -data Entry = Entry{ - _id :: String, - year :: Int, - month :: Int, - day :: Int, - lang :: BlogLang, - title :: String, - author :: String, - text :: String, - mtext :: String, - comments :: [Comment] -} deriving (Show, Data, Typeable) - -blogText :: (a -> String) -> a -> Text -blogText f = T.pack . f - +import BlogDB -- custom list functions intersperse' :: a -> [a] -> [a] @@ -99,29 +72,29 @@ renderEntries showAll entries topText footerLinks = showEntry :: Entry -> Html showEntry e = H.li $ do entryLink e - preEscapedText $ T.concat [" ", blogText text e, "
 
"] + preEscapedText $ T.concat [" ", btext e, "
 
"] entryLink e = H.a ! A.href (toValue $ concat $ intersperse' "/" $ linkElems e) $ toHtml ("[" ++ show(length $ comments e) ++ "]") - linkElems e = [show(lang e), show(year e), show(month e), show(day e), _id e] + linkElems e = [show(lang e), show $ entryId e] getFooterLinks (Just h) = h getFooterLinks Nothing = mempty renderEntry :: Entry -> Html -renderEntry entry = H.div ! A.class_ "innerBox" $ do - H.div ! A.class_ "innerBoxTop" $ toHtml $ title entry +renderEntry (Entry{..}) = H.div ! A.class_ "innerBox" $ do + H.div ! A.class_ "innerBoxTop" $ toHtml $ title H.div ! A.class_ "innerBoxMiddle" $ do H.article $ H.ul $ H.li $ do - preEscapedText $ blogText text entry - preEscapedText $ blogText mtext entry + preEscapedText $ btext + preEscapedText $ mtext H.div ! A.class_ "innerBoxComments" $ do - H.div ! A.class_ "cHead" $ toHtml $ cHead (lang entry) -- ! A.style "font-size:large;font-weight:bold;" - H.ul $ renderComments (comments entry) (lang entry) - renderCommentBox (lang entry) (_id entry) + H.div ! A.class_ "cHead" $ toHtml $ cHead lang -- ! A.style "font-size:large;font-weight:bold;" + H.ul $ renderComments comments lang + renderCommentBox lang entryId -renderCommentBox :: BlogLang -> String -> Html +renderCommentBox :: BlogLang -> EntryId -> Html renderCommentBox cLang cId = do H.div ! A.class_ "cHead" $ toHtml $ cwHead cLang - H.form ! A.method "POST" ! A.action (toValue $ "/" ++ (show cLang) ++ "/postcomment/" ++ cId) $ do + H.form ! A.method "POST" ! A.action (toValue $ "/" ++ (show cLang) ++ "/postcomment/" ++ show cId) $ do H.p $ H.label $ do H.span $ "Name:" --toHtml ("Name:" :: String) H.input ! A.name "cname" @@ -135,16 +108,11 @@ renderComments [] lang = H.li $ toHtml $ noComments lang renderComments comments lang = sequence_ $ map showComment comments where showComment :: Comment -> Html - showComment c = H.li $ do - H.a ! A.name (toValue $ cdate c) ! A.href (toValue $ "#" ++ (show $ cdate c)) ! A.class_ "cl" $ - H.i $ toHtml $ (cauthor c ++ ": ") - preEscapedText $ blogText ctext c - H.p ! A.class_ "tt" $ toHtml (timeString $ cdate c) - getTime :: Integer -> Maybe UTCTime - getTime t = parseTime defaultTimeLocale "%s" (show t) - showTime lang (Just t) = formatTime defaultTimeLocale (cTimeFormat lang) t - showTime _ Nothing = "[???]" -- this can not happen?? - timeString = (showTime lang) . getTime + showComment (Comment{..}) = H.li $ do + H.i $ toHtml $ T.append cauthor ": " + preEscapedText $ ctext + H.p ! A.class_ "tt" $ toHtml $ timeString cdate + timeString t = formatTime defaultTimeLocale (cTimeFormat lang) t showLinks :: Maybe Int -> BlogLang -> Html showLinks (Just i) lang @@ -161,7 +129,7 @@ showFooter l v = H.div ! A.class_ "rightbox" ! A.style "text-align:right;" $ do toHtml ("Proudly made with " :: Text) H.a ! A.href "http://haskell.org" $ "Haskell" toHtml (", " :: Text) - H.a ! A.href "http://couchdb.apache.org/" $ "CouchDB" + H.a ! A.href "http://hackage.haskell.org/package/acid-state-0.6.3" $ "Acid-State" toHtml (" and without PHP, Java, Perl, MySQL and Python." :: Text) H.br H.a ! A.href (toValue repoURL) $ toHtml $ T.append "Version " v diff --git a/src/BlogDB.hs b/src/BlogDB.hs new file mode 100644 index 0000000000..cade9327e7 --- /dev/null +++ b/src/BlogDB.hs @@ -0,0 +1,208 @@ +{-# LANGUAGE DeriveDataTypeable, GeneralizedNewtypeDeriving, RecordWildCards, +TemplateHaskell, TypeFamilies, OverloadedStrings, ScopedTypeVariables, BangPatterns #-} + +module BlogDB where + +import Control.Monad.Reader (ask) +import Control.Monad.State (get, put) +import Data.Acid +import Data.Acid.Advanced +import Data.Acid.Local +import Data.ByteString (ByteString) +import Data.Data (Data, Typeable) +import Data.IxSet (Indexable(..), IxSet(..), (@=), Proxy(..), getOne, ixFun, ixSet) +import Data.List (insert) +import Data.SafeCopy (SafeCopy, base, deriveSafeCopy) +import Data.Text (Text, pack) +import Data.Text.Lazy (toStrict) +import Data.Time +import Happstack.Server (ServerPart) + +import qualified Crypto.Hash.SHA512 as SHA (hash) +import qualified Data.ByteString.Char8 as B +import qualified Data.ByteString.Base64 as B64 (encode) +import qualified Data.IxSet as IxSet +import qualified Data.Text as Text + + +newtype EntryId = EntryId { unEntryId :: Integer } + deriving (Eq, Ord, Data, Enum, Typeable, SafeCopy) + +instance Show EntryId where + show = show . unEntryId + +data BlogLang = EN | DE + deriving (Eq, Ord, Data, Typeable) + +instance Show BlogLang where + show DE = "de" + show EN = "en" + +$(deriveSafeCopy 0 'base ''BlogLang) + +data Comment = Comment {  + cauthor :: Text, + ctext :: Text, + cdate :: UTCTime +} deriving (Eq, Ord, Show, Data, Typeable) + +$(deriveSafeCopy 0 'base ''Comment) + +data Entry = Entry { + entryId :: EntryId, + lang :: BlogLang, + author :: Text, + title :: Text, + btext :: Text, + mtext :: Text, + edate :: UTCTime, + tags :: [Text], + comments :: [Comment] +} deriving (Eq, Ord, Show, Data, Typeable) + +$(deriveSafeCopy 0 'base ''Entry) + +-- ixSet requires different datatypes for field indexes, so let's define some +newtype Author = Author Text deriving (Eq, Ord, Data, Typeable, SafeCopy) +newtype Title = Title Text deriving (Eq, Ord, Data, Typeable, SafeCopy) +newtype BText = BText Text deriving (Eq, Ord, Data, Typeable, SafeCopy) -- standard text +newtype MText = MText Text deriving (Eq, Ord, Data, Typeable, SafeCopy) -- "read more" text +newtype Tag = Tag Text deriving (Eq, Ord, Data, Typeable, SafeCopy) +newtype EDate = EDate UTCTime deriving (Eq, Ord, Data, Typeable, SafeCopy) +newtype SDate = SDate UTCTime deriving (Eq, Ord, Data, Typeable, SafeCopy) +newtype Username = Username Text deriving (Eq, Ord, Data, Typeable, SafeCopy) +newtype SessionID = SessionID Text deriving (Eq, Ord, Data, Typeable, SafeCopy) + +instance Indexable Entry where + empty = ixSet [ ixFun $ \e -> [ entryId e] + , ixFun $ (:[]) . lang + , ixFun $ \e -> [ Author $ author e ] + , ixFun $ \e -> [ Title $ title e] + , ixFun $ \e -> [ BText $ btext e] + , ixFun $ \e -> [ MText $ mtext e] + , ixFun $ \e -> [ EDate $ edate e] + , ixFun $ \e -> map Tag (tags e) + , ixFun $ comments + ] + +data User = User { + username :: Text, + password :: ByteString +} deriving (Eq, Ord, Data, Typeable) + +$(deriveSafeCopy 0 'base ''User) + +data Session = Session { + sessionID :: Text, + user :: User, + sdate :: UTCTime +} deriving (Eq, Ord, Data, Typeable) + +$(deriveSafeCopy 0 'base ''Session) + +instance Indexable User where + empty = ixSet [ ixFun $ \u -> [Username $ username u] + , ixFun $ (:[]) . password + ] + +instance Indexable Session where + empty = ixSet [ ixFun $ \s -> [SessionID $ sessionID s] + , ixFun $ (:[]) . user + , ixFun $ \s -> [SDate $ sdate s] + ] + +data Blog = Blog { + blogSessions :: IxSet Session, + blogUsers :: IxSet User, + blogEntries :: IxSet Entry +} deriving (Data, Typeable) + +$(deriveSafeCopy 0 'base ''Blog) + +initialBlogState :: Blog +initialBlogState = + Blog { blogSessions = empty + , blogUsers = empty + , blogEntries = empty } + +-- acid-state database functions (purity is necessary!) + +insertEntry :: Entry -> Update Blog Entry +insertEntry e = + do b@Blog{..} <- get + put $ b { blogEntries = IxSet.insert e blogEntries } + return e + +addComment :: EntryId -> Comment -> Update Blog Entry +addComment eId c = + do b@Blog{..} <- get + let (Just e) = getOne $ blogEntries @= eId + let newEntry = e { comments = insert c $ comments e } + put $ b { blogEntries = IxSet.updateIx eId newEntry blogEntries } + return newEntry + +updateEntry :: Entry -> Update Blog Entry +updateEntry e = + do b@Blog{..} <- get + put $ b { blogEntries = IxSet.updateIx (entryId e) e blogEntries} + return e + +getEntry :: EntryId -> Query Blog (Maybe Entry) +getEntry eId = + do b@Blog{..} <- ask + return $ getOne $ blogEntries @= eId + +latestEntries :: BlogLang -> Query Blog [Entry] +latestEntries lang = + do b@Blog{..} <- ask + return $ IxSet.toDescList (Proxy :: Proxy EDate) $ blogEntries @= lang + +addSession :: Text -> User -> UTCTime -> Update Blog Session +addSession sId u t = + do b@Blog{..} <- get + let s = Session sId u t + put $ b { blogSessions = IxSet.insert s blogSessions} + return s + +getSession :: SessionID -> Query Blog (Maybe Session) +getSession sId = + do b@Blog{..} <- ask + return $ getOne $ blogSessions @= sId + +addUser :: Text -> String -> Update Blog User +addUser un pw = + do b@Blog{..} <- get + let u = User un $ hashString pw + put $ b { blogUsers = IxSet.insert u blogUsers} + return u + +getUser :: Username -> Query Blog (Maybe User) +getUser uN = + do b@Blog{..} <- ask + return $ getOne $ blogUsers @= uN + +checkUser :: Username -> String -> Query Blog (Bool) +checkUser uN pw = + do b@Blog{..} <- ask + let user = getOne $ blogUsers @= uN + case user of + Nothing -> return False + (Just u) -> return $ (password u) == hashString pw + +-- various functions +hashString :: String -> ByteString +hashString = B64.encode . SHA.hash . B.pack + +$(makeAcidic ''Blog + [ 'insertEntry + , 'addComment + , 'updateEntry + , 'getEntry + , 'latestEntries + , 'addSession + , 'getSession + , 'addUser + , 'getUser + , 'checkUser + ]) + diff --git a/src/Locales.hs b/src/Locales.hs index 56bc42d10b..393a69f8fc 100644 --- a/src/Locales.hs +++ b/src/Locales.hs @@ -6,18 +6,13 @@ import Data.Data (Data, Typeable) import Data.Text (Text) import qualified Data.Text as T +import BlogDB (BlogLang (..)) + {- to add a language simply define its abbreviation and Show instance then - translate the appropriate strings and add CouchDB views in Server.hs -} -data BlogLang = EN | DE deriving (Data, Typeable) - -instance Show BlogLang where - show EN = "en" - show DE = "de" - data BlogError = NotFound | DBError - version = "2.2b" allLang = [EN, DE] @@ -77,6 +72,10 @@ nextText :: BlogLang -> Text nextText DE = "Später" nextText EN = "Later" +readMore :: BlogLang -> Text +readMore DE = "[Weiterlesen]" +readMore EN = "[Read more]" + -- contact information contactText :: BlogLang -> Text contactText DE = "Wer mich kontaktieren will: " diff --git a/src/Main.hs b/src/Main.hs index 7990b8811a..58de322183 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -1,6 +1,6 @@ {-# LANGUAGE OverloadedStrings, ScopedTypeVariables, GeneralizedNewtypeDeriving, DeriveDataTypeable, FlexibleContexts, MultiParamTypeClasses, TemplateHaskell, - TypeFamilies, RecordWildCards #-} + TypeFamilies, RecordWildCards, BangPatterns #-} module Main where @@ -21,51 +21,15 @@ import Data.Text (Text) import qualified Data.Text as T import Data.Time import Data.SafeCopy (base, deriveSafeCopy) -import Database.CouchDB -import Happstack.Server +import Happstack.Server hiding (Session) import Network.CGI (liftIO) -import Text.JSON.Generic import System.Environment(getEnv) import System.Locale (defaultTimeLocale) import Blog +import BlogDB hiding (addComment) import Locales -data SessionState = SessionState { sessions :: [(String, Integer)] } -- id/date - deriving (Eq, Ord, Read, Show, Data, Typeable) - -initialSession :: SessionState -initialSession = SessionState [] - -$(deriveSafeCopy 0 'base ''SessionState) - - -data AccountState = AccountState { accounts :: [Account] } - deriving (Read, Show, Data, Typeable) - -data Account = Account { account :: String - , password :: ByteString - } deriving (Read, Show, Data, Typeable) - -{-session handling functions-} - -addSession :: (String, Integer) -> Update SessionState [(String, Integer)] -addSession newS = do - s@SessionState{..} <- get - let newSessions = newS : sessions - put $ s{ sessions = newSessions } - return newSessions - -querySessions :: Query SessionState [(String, Integer)] -querySessions = sessions <$> ask - -$(makeAcidic ''SessionState ['addSession, 'querySessions]) -$(makeAcidic ''AccountState []) -{- various functions -} - -hashString :: String -> ByteString -hashString = B64.encode . SHA.hash . pack - {- Server -} tmpPolicy :: BodyPolicy @@ -75,48 +39,18 @@ main :: IO() main = do putStrLn ("TazBlog " ++ version ++ " in Haskell starting") tbDir <- getEnv "TAZBLOG" - bracket (openLocalStateFrom (tbDir ++ "/State/SessionState") initialAccounts) + bracket (openLocalStateFrom (tbDir ++ "/BlogState") initialBlogState) (createCheckpointAndClose) - (\sessionAcid -> bracket (openLocalStateFrom (tbDir ++ "/State/AccountState") ) - (createCheckpointAndClose) - (\accountAcid -> simpleHTTP nullConf $ - tazBlog sessionAcid accountAcid)) - - + (\acid -> simpleHTTP nullConf $ tazBlog acid) - - -initialAccounts :: AccountState -initialAccounts = [] - -askAccount :: IO Account -askAccount = do - putStrLn "Enter name for the account:" - n <- getLine - putStrLn "Enter password for the account:" - p <- getLine - return $ Account n $ hashString p - -guardSession :: AcidState SessionState -> ServerPartT IO () -guardSession acid = do - sID <- lookCookieValue "session" - sDate <- readCookieValue "sdate" - cSessions <- query' acid QuerySessions - cDate <- liftIO $ currentSeconds - when (not $ elem (sID, sDate) cSessions) - mzero - when (32400 > (cDate - sDate)) - mzero - -tazBlog :: AcidState SessionState -> ServerPart Response +tazBlog :: AcidState Blog -> ServerPart Response tazBlog acid = do - msum [ dir (show DE) $ blogHandler DE - , dir (show EN) $ blogHandler EN + msum [ dir (show DE) $ blogHandler acid DE + , dir (show EN) $ blogHandler acid EN , do nullDir - showIndex DE + showIndex acid DE , do dir " " $ nullDir seeOther ("https://plus.google.com/115916629925754851590" :: String) (toResponse ()) - , path $ \(id_ :: Int) -> getEntryLink id_ , path $ \(year :: Int) -> path $ \(month :: Int) -> path $ \(id_ :: String) -> formatOldLink year month id_ , dir "res" $ serveDirectory DisableBrowsing [] "../res" , dir "notice" $ ok $ toResponse showSiteNotice @@ -127,18 +61,29 @@ tazBlog acid = do , serveDirectory DisableBrowsing [] "../res" ] -blogHandler :: BlogLang -> ServerPart Response -blogHandler lang = - msum [ path $ \(year :: Int) -> path $ \(month :: Int) -> path $ --single entry - \(day :: Int) -> path $ \(id_ :: String) -> showEntry lang id_ - , path $ \(year :: Int ) -> path $ \(month :: Int) -> showMonth year month lang +blogHandler :: AcidState Blog -> BlogLang -> ServerPart Response +blogHandler acid lang = + msum [ path $ \(eId :: Integer) -> showEntry acid lang $ EntryId eId , do decodeBody tmpPolicy - dir "postcomment" $ path $ \(id_ :: String) -> addComment id_ + dir "postcomment" $ path $ + \(eId :: Integer) -> addComment acid $ EntryId eId , do nullDir - showIndex lang + showIndex acid lang ] +guardSession :: AcidState Blog -> ServerPartT IO () +guardSession acid = do + (sId :: Text) <- readCookieValue "session" + (Just Session{..}) <- query' acid (GetSession $ SessionID sId) + (uName :: Text) <- readCookieValue "sUser" + now <- liftIO $ getCurrentTime + unless (and [uName == username user, sessionTimeDiff now sdate]) + mzero + where + sessionTimeDiff :: UTCTime -> UTCTime -> Bool + sessionTimeDiff now sdate = (diffUTCTime now sdate) > 43200 + adminHandler :: ServerPart Response adminHandler = undefined @@ -147,32 +92,21 @@ formatOldLink y m id_ = flip seeOther (toResponse ()) $ concat $ intersperse' "/" ["de", show y, show m, replace '.' '/' id_] -showEntry :: BlogLang -> String -> ServerPart Response -showEntry lang id_ = do - entryJS <- liftIO $ runCouchDB' $ getDoc (db "tazblog") (doc id_) - let entry = maybeDoc entryJS +showEntry :: AcidState Blog -> BlogLang -> EntryId -> ServerPart Response +showEntry acid lang eId = do + entry <- query' acid (GetEntry eId) ok $ tryEntry entry lang tryEntry :: Maybe Entry -> BlogLang -> Response tryEntry Nothing lang = toResponse $ showError NotFound lang tryEntry (Just entry) _ = toResponse $ blogTemplate eLang eTitle $ renderEntry entry where - eTitle = T.pack $ ": " ++ title entry + eTitle = T.append ": " (title entry) eLang = lang entry -getEntryLink :: Int -> ServerPart Response -getEntryLink id_ = do - entryJS <- liftIO $ runCouchDB' $ getDoc (db "tazblog") (doc $ show id_) - let entry = maybeDoc entryJS - seeOther (makeLink entry) (toResponse()) - where - makeLink :: Maybe Entry -> String - makeLink (Just e) = concat $ intersperse' "/" [show $ lang e, show $ year e, show $ month e, show $ day e, show id_] - makeLink Nothing = "/" - -showIndex :: BlogLang -> ServerPart Response -showIndex lang = do - entries <- getLatest lang [("descending", showJSON True)] +showIndex :: AcidState Blog -> BlogLang -> ServerPart Response +showIndex acid lang = do + entries <- query' acid (LatestEntries lang) (page :: Maybe Int) <- optional $ lookRead "page" ok $ toResponse $ blogTemplate lang "" $ renderEntries False (eDrop page entries) (topText lang) (Just $ showLinks page lang) @@ -180,110 +114,24 @@ showIndex lang = do eDrop :: Maybe Int -> [a] -> [a] eDrop (Just i) = drop ((i-1) * 6) eDrop Nothing = drop 0 - -showMonth :: Int -> Int -> BlogLang -> ServerPart Response -showMonth y m lang = do - entries <- getLatest lang $ ("descending", showJSON True) : makeQuery startkey endkey - ok $ toResponse $ blogTemplate lang month - $ renderEntries True entries month Nothing - where - month = getMonth lang y m - startkey = JSArray [toJSON y, toJSON m] - endkey = JSArray [toJSON y, toJSON m, JSObject (toJSObject [] )] -addComment :: String -> ServerPart Response -addComment id_ = do - rda <- liftIO $ currentSeconds >>= return - nComment <- Comment <$> look "cname" - <*> look "ctext" - <*> pure rda - rev <- updateDBDoc (doc id_) $ insertComment nComment - liftIO $ putStrLn $ show rev - seeOther ("/" ++ id_) (toResponse()) +addComment :: AcidState Blog -> EntryId -> ServerPart Response +addComment acid eId = do + now <- liftIO $ getCurrentTime >>= return + nComment <- Comment <$> lookText' "cname" + <*> lookText' "ctext" + <*> pure now + update' acid (AddComment eId nComment) + seeOther ("/" ++ show eId) (toResponse()) -processLogin :: AcidState SessionState -> ServerPart Response +processLogin :: AcidState Blog -> ServerPart Response processLogin acid = do decodeBody tmpPolicy - account <- look "account" + account <- lookText' "account" password <- look "password" - ok $ toResponse ("Shut up" :: String) - - --- http://tazj.in/2012/02/10.155234 - -currentSeconds :: IO Integer -currentSeconds = do - now <- getCurrentTime - let s = read (formatTime defaultTimeLocale "%s" now) :: Integer - return s - -{- CouchDB functions -} - -getLatest :: BlogLang -> [(String, JSValue)] -> ServerPart [Entry] -getLatest lang arg = do - queryResult <- queryDB view arg - let entries = map (stripResult . fromJSON . snd) queryResult - return entries - where - view = case lang of - EN -> "latestEN" - DE -> "latestDE" - -insertComment :: Comment -> JSValue -> IO JSValue -insertComment c jEntry = return $ toJSON $ e { comments = c : (comments e)} - where - e = convertJSON jEntry :: Entry - -makeQuery :: JSON a => a -> a -> [(String, JSValue)] -makeQuery qsk qek = [("startkey", (showJSON qsk)) - ,("endkey", (showJSON qek))] - -queryDB :: JSON a => String -> [(String, JSValue)] -> ServerPart [(Doc, a)] -queryDB view arg = liftIO . runCouchDB' $ queryView (db "tazblog") (doc "entries") (doc view) arg - -maybeDoc :: Data a => Maybe (Doc, Rev, JSValue) -> Maybe a -maybeDoc (Just(_,_,v)) = Just( stripResult $ fromJSON v) -maybeDoc Nothing = Nothing - -updateDBDoc :: JSON a => Doc -> (a -> IO a) -> ServerPart (Maybe Rev) -updateDBDoc docn f = liftIO $ runCouchDB' $ getAndUpdateDoc (db "tazblog") docn f - -stripResult :: Result a -> a -stripResult (Ok z) = z -stripResult (Error s) = error $ "JSON error: " ++ s - -convertJSON :: Data a => JSValue -> a -convertJSON = stripResult . fromJSON - -getMonthCount :: BlogLang -> Int -> Int -> ServerPart Int -getMonthCount lang y m = do - count <- queryDB (view lang) $ makeQuery startkey endkey - return . stripCount $ map (stripResult . fromJSON . snd) count + login <- query' acid (CheckUser (Username account) password) + if' login + (addSessionCookie account) + (ok $ toResponse $ ("Fail?" :: Text)) where - startkey = JSArray [toJSON ("count" :: String), toJSON y, toJSON m] - endkey = JSArray [toJSON ("count" :: String), toJSON y, toJSON m, JSObject (toJSObject [] )] - stripCount :: [Int] -> Int - stripCount [x] = x - stripCount [] = 0 - view DE = "countDE" - view EN = "countEN" - - -{- CouchDB View Setup -} -latestDEView = "function(doc){ if(doc.lang == 'DE'){ emit([doc.year, doc.month, doc.day, doc._id], doc); } }" -latestENView = "function(doc){ if(doc.lang == 'EN'){ emit([doc.year, doc.month, doc.day, doc._id], doc); } }" -countDEView = "function(doc){ if(doc.lang == 'DE'){ emit(['count', doc.year, doc.month, doc.day, doc._id], 1); } }" -countENView = "function(doc){ if(doc.lang == 'EN'){ emit(['count', doc.year, doc.month, doc.day, doc._id], 1); } }" -countReduce = "function(keys, values, rereduce) { return sum(values); }" - -latestDE = ViewMap "latestDE" latestDEView -latestEN = ViewMap "latestEN" latestENView -countDE = ViewMapReduce "countDE" countDEView countReduce -countEN = ViewMapReduce "countEN" countENView countReduce - -setupBlogViews :: IO () -setupBlogViews = runCouchDB' $ - newView "tazblog" "entries" [latestDE, latestEN, countDE, countEN] - - - + addSessionCookie = undefined \ No newline at end of file diff --git a/tools/acid-migrate/Acid.hs b/tools/acid-migrate/Acid.hs index bc360694a6..276102eb03 100644 --- a/tools/acid-migrate/Acid.hs +++ b/tools/acid-migrate/Acid.hs @@ -19,7 +19,8 @@ import Data.SafeCopy (SafeCopy, base, deriveSafeCopy) import Data.Text (Text, pack) import Data.Text.Lazy (toStrict) import Data.Time -import Happstack.Server hiding (Session) +import System.Environment(getEnv) + import qualified Crypto.Hash.SHA512 as SHA (hash) import qualified Data.ByteString.Char8 as B @@ -256,7 +257,8 @@ pasteToDB acid !e = update' acid (InsertEntry e) main :: IO() main = do - bracket (openLocalState initialBlogState) + tbDir <- getEnv "TAZBLOG" + bracket (openLocalStateFrom (tbDir ++ "/BlogState") initialBlogState) (createCheckpointAndClose) (\acid -> convertEntries acid) -- cgit 1.4.1 From 2cb2900b0747dfb83ebc78e7f129bd26fba920fe Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Tue, 13 Mar 2012 06:35:56 +0100 Subject: * updated some stuff, work on sessions --- TODO | 1 + src/Blog.hs | 12 ++++++++---- src/BlogDB.hs | 21 +++++++++++++++------ src/Main.hs | 19 ++++++++++++++----- 4 files changed, 38 insertions(+), 15 deletions(-) diff --git a/TODO b/TODO index 7c2dcaa91e..064ad8d364 100644 --- a/TODO +++ b/TODO @@ -1 +1,2 @@ * handle BlogErrors +* fix sessions \ No newline at end of file diff --git a/src/Blog.hs b/src/Blog.hs index 5f95d70058..da8dd24dc6 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -117,12 +117,15 @@ renderComments comments lang = sequence_ $ map showComment comments showLinks :: Maybe Int -> BlogLang -> Html showLinks (Just i) lang | ( i > 1) = H.div ! A.class_ "centerbox" $ do - H.a ! A.href (toValue $ "/?page=" ++ show (i+1)) $ toHtml $ backText lang + H.a ! A.href (toValue $ "/" ++ show lang ++ "/?page=" ++ show (i+1)) $ + toHtml $ backText lang toHtml (" -- " :: Text) - H.a ! A.href (toValue $ "/?page=" ++ show (i-1)) $ toHtml $ nextText lang + H.a ! A.href (toValue $ "/" ++ show lang ++ "/?page=" ++ show (i-1)) $ + toHtml $ nextText lang | ( i <= 1 ) = showLinks Nothing lang showLinks Nothing lang = H.div ! A.class_ "centerbox" $ - H.a ! A.href "/?page=2" $ toHtml $ backText lang + H.a ! A.href (toValue $ "/" ++ show lang ++ "/?page=2") $ + toHtml $ backText lang showFooter :: BlogLang -> Text -> Html showFooter l v = H.div ! A.class_ "rightbox" ! A.style "text-align:right;" $ do @@ -164,12 +167,13 @@ adminTemplate body title = H.docTypeHtml $ do adminLogin :: Html adminLogin = H.div ! A.class_ "loginBox" $ do H.div ! A.class_ "loginBoxTop" $ "TazBlog Admin: Login" - H.div ! A.class_ "loginBoxMiddle" $ H.form ! A.action "/login" ! A.method "post" $ do + H.div ! A.class_ "loginBoxMiddle" $ H.form ! A.action "/dologin" ! A.method "post" $ do H.p $ "Account ID" H.p $ H.input ! A.type_ "text" ! A.style "font-size: 2;" ! A.name "account" ! A.value "tazjin" ! A.readonly "1" H.p $ "Passwort" H.p $ H.input ! A.type_ "password" ! A.style "font-size: 2;" ! A.name "password" + H.p $ H.input ! A.alt "Anmelden" ! A.type_ "image" ! A.src "/res/signin.gif" -- Error pages showError :: BlogError -> BlogLang -> Html diff --git a/src/BlogDB.hs b/src/BlogDB.hs index cade9327e7..9bffd79c3b 100644 --- a/src/BlogDB.hs +++ b/src/BlogDB.hs @@ -16,7 +16,7 @@ import Data.SafeCopy (SafeCopy, base, deriveSafeCopy) import Data.Text (Text, pack) import Data.Text.Lazy (toStrict) import Data.Time -import Happstack.Server (ServerPart) +import System.Environment(getEnv) import qualified Crypto.Hash.SHA512 as SHA (hash) import qualified Data.ByteString.Char8 as B @@ -157,12 +157,11 @@ latestEntries lang = do b@Blog{..} <- ask return $ IxSet.toDescList (Proxy :: Proxy EDate) $ blogEntries @= lang -addSession :: Text -> User -> UTCTime -> Update Blog Session -addSession sId u t = +addSession :: Session -> Update Blog Session +addSession nSession = do b@Blog{..} <- get - let s = Session sId u t - put $ b { blogSessions = IxSet.insert s blogSessions} - return s + put $ b { blogSessions = IxSet.insert nSession blogSessions} + return nSession getSession :: SessionID -> Query Blog (Maybe Session) getSession sId = @@ -206,3 +205,13 @@ $(makeAcidic ''Blog , 'checkUser ]) +interactiveUserAdd :: IO () +interactiveUserAdd = do + tbDir <- getEnv "TAZBLOG" + acid <- openLocalStateFrom (tbDir ++ "/BlogState") initialBlogState + putStrLn "Username:" + un <- getLine + putStrLn "Password:" + pw <- getLine + update' acid (AddUser (pack un) pw) + createCheckpointAndClose acid diff --git a/src/Main.hs b/src/Main.hs index 58de322183..3c585658a8 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -14,7 +14,7 @@ import Data.Acid import Data.Acid.Advanced import Data.Acid.Local import qualified Data.ByteString.Base64 as B64 (encode) -import Data.ByteString.Char8 (ByteString, pack) +import Data.ByteString.Char8 (ByteString, pack, unpack) import Data.Data (Data, Typeable) import Data.Monoid (mempty) import Data.Text (Text) @@ -50,7 +50,7 @@ tazBlog acid = do , do nullDir showIndex acid DE , do dir " " $ nullDir - seeOther ("https://plus.google.com/115916629925754851590" :: String) (toResponse ()) + seeOther ("https://plus.google.com/115916629925754851590" :: Text) (toResponse ()) , path $ \(year :: Int) -> path $ \(month :: Int) -> path $ \(id_ :: String) -> formatOldLink year month id_ , dir "res" $ serveDirectory DisableBrowsing [] "../res" , dir "notice" $ ok $ toResponse showSiteNotice @@ -131,7 +131,16 @@ processLogin acid = do password <- look "password" login <- query' acid (CheckUser (Username account) password) if' login - (addSessionCookie account) - (ok $ toResponse $ ("Fail?" :: Text)) + (createSession account) + (ok $ toResponse $ adminTemplate adminLogin "Login failed") where - addSessionCookie = undefined \ No newline at end of file + createSession account = do + now <- liftIO getCurrentTime + let sId = hashString $ show now + addCookie (MaxAge 43200) (mkCookie "session" $ unpack sId) + addCookie (MaxAge 43200) (mkCookie "sUser" $ T.unpack account) + (Just user) <- query' acid (GetUser $ Username account) + let nSession = Session (T.pack $ unpack sId) user now + update' acid (AddSession nSession) + seeOther ("/admin?do=login" :: Text) (toResponse()) + -- cgit 1.4.1 From 4491a9087c6ee1edbd8d4cd04b58305188929894 Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Tue, 13 Mar 2012 06:40:00 +0100 Subject: * updated TODO --- TODO | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/TODO b/TODO index 064ad8d364..b644874389 100644 --- a/TODO +++ b/TODO @@ -1,2 +1,4 @@ * handle BlogErrors -* fix sessions \ No newline at end of file +* fix sessions +* add readMore link +* add flushSessions :: IO() \ No newline at end of file -- cgit 1.4.1 From eaa9ed5b981375167d3c0f31d6eeff84a397e547 Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Tue, 13 Mar 2012 19:50:13 +0100 Subject: * removed dependency on Network.CGI * Cabal & License file --- LICENSE | 1 + TazBlog.cabal | 31 +++++++++++++++++++++++++++++++ src/Main.hs | 2 +- 3 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 LICENSE create mode 100644 TazBlog.cabal diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000..c3b19de170 --- /dev/null +++ b/LICENSE @@ -0,0 +1 @@ +I don't feel like writing a license for this. Do whatever you want with this, but credit me. \ No newline at end of file diff --git a/TazBlog.cabal b/TazBlog.cabal new file mode 100644 index 0000000000..2559a69613 --- /dev/null +++ b/TazBlog.cabal @@ -0,0 +1,31 @@ +Name: TazBlog +Version: 3.0 +Synopsis: Tazjin's Blog +License-file: LICENSE +Author: Vincent Ambo +Maintainer: tazjin@gmail.com +Category: Web blog +Build-type: Simple +cabal-version: >= 1.2 + + +Executable tazblog + hs-source-dirs: src + main-is: Main.hs + + Build-depends: + base, + bytestring, + happstack-server, + text, + blaze-html, + crypto-api, + cryptohash, + old-locale, + time, + base64-bytestring, + acid-state, + ixset, + safecopy, + mtl, + transformers \ No newline at end of file diff --git a/src/Main.hs b/src/Main.hs index 3c585658a8..b979c3bb8a 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -7,6 +7,7 @@ module Main where import Control.Applicative ((<$>), (<*>), optional, pure) import Control.Exception (bracket) import Control.Monad (msum, mzero, when, unless) +import Control.Monad.IO.Class (liftIO) import Control.Monad.State (get, put) import Control.Monad.Reader (ask) import qualified Crypto.Hash.SHA512 as SHA @@ -22,7 +23,6 @@ import qualified Data.Text as T import Data.Time import Data.SafeCopy (base, deriveSafeCopy) import Happstack.Server hiding (Session) -import Network.CGI (liftIO) import System.Environment(getEnv) import System.Locale (defaultTimeLocale) -- cgit 1.4.1 From f6446aec725234ea38b5431defa8e4c987e07f20 Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Tue, 13 Mar 2012 21:29:06 +0100 Subject: * added flushSessions :: IO() * updated TODO --- TODO | 3 +-- src/BlogDB.hs | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/TODO b/TODO index b644874389..7b2c54f446 100644 --- a/TODO +++ b/TODO @@ -1,4 +1,3 @@ * handle BlogErrors * fix sessions -* add readMore link -* add flushSessions :: IO() \ No newline at end of file +* add readMore link \ No newline at end of file diff --git a/src/BlogDB.hs b/src/BlogDB.hs index 9bffd79c3b..d5a964da8a 100644 --- a/src/BlogDB.hs +++ b/src/BlogDB.hs @@ -168,6 +168,12 @@ getSession sId = do b@Blog{..} <- ask return $ getOne $ blogSessions @= sId +clearSessions :: Update Blog [Session] +clearSessions = + do b@Blog{..} <- get + put $ b { blogSessions = empty } + return [] + addUser :: Text -> String -> Update Blog User addUser un pw = do b@Blog{..} <- get @@ -203,6 +209,7 @@ $(makeAcidic ''Blog , 'addUser , 'getUser , 'checkUser + , 'clearSessions ]) interactiveUserAdd :: IO () @@ -215,3 +222,10 @@ interactiveUserAdd = do pw <- getLine update' acid (AddUser (pack un) pw) createCheckpointAndClose acid + +flushSessions :: IO () +flushSessions = do + tbDir <- getEnv "TAZBLOG" + acid <- openLocalStateFrom (tbDir ++ "/BlogState") initialBlogState + update' acid (ClearSessions) + createCheckpointAndClose acid -- cgit 1.4.1 From e6746984f585168229d902096e22177a6e55a6c2 Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Wed, 14 Mar 2012 00:37:00 +0100 Subject: * changed comment structure to sort by UTCTime * postEntry function done; adminHandler doesn't work? --- TODO | 4 +- src/Blog.hs | 27 +++++++++-- src/BlogDB.hs | 17 ++++--- src/Main.hs | 109 ++++++++++++++++++++++++++++++++------------- tools/acid-migrate/Acid.hs | 8 ++-- 5 files changed, 121 insertions(+), 44 deletions(-) diff --git a/TODO b/TODO index 7b2c54f446..3de1a19190 100644 --- a/TODO +++ b/TODO @@ -1,3 +1,3 @@ * handle BlogErrors -* fix sessions -* add readMore link \ No newline at end of file +* add readMore link +* Twitter: http://twitter.github.com/bootstrap/index.html \ No newline at end of file diff --git a/src/Blog.hs b/src/Blog.hs index da8dd24dc6..8e4c76b621 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -155,8 +155,8 @@ showSiteNotice = H.docTypeHtml $ do {- Administration pages -} -adminTemplate :: Html -> Text -> Html -adminTemplate body title = H.docTypeHtml $ do +adminTemplate :: Text -> Html -> Html +adminTemplate title body = H.docTypeHtml $ do H.head $ do H.link ! A.rel "stylesheet" ! A.type_ "text/css" ! A.href "/res/admin.css" ! A.media "all" H.meta ! A.httpEquiv "content-type" ! A.content "text/html;charset=UTF-8" @@ -165,7 +165,8 @@ adminTemplate body title = H.docTypeHtml $ do body adminLogin :: Html -adminLogin = H.div ! A.class_ "loginBox" $ do +adminLogin = adminTemplate "Login" $ + H.div ! A.class_ "loginBox" $ do H.div ! A.class_ "loginBoxTop" $ "TazBlog Admin: Login" H.div ! A.class_ "loginBoxMiddle" $ H.form ! A.action "/dologin" ! A.method "post" $ do H.p $ "Account ID" @@ -175,6 +176,26 @@ adminLogin = H.div ! A.class_ "loginBox" $ do H.p $ H.input ! A.type_ "password" ! A.style "font-size: 2;" ! A.name "password" H.p $ H.input ! A.alt "Anmelden" ! A.type_ "image" ! A.src "/res/signin.gif" +adminIndex :: Text -> Html +adminIndex sUser = adminTemplate "Index" $ + H.div ! A.style "float: center;" $ + H.form ! A.action "/admin/postentry" ! A.method "POST" $ do + H.table $ do + H.tr $ do H.td $ "Titel:" + H.td $ H.input ! A.type_ "text" ! A.name "title" + H.tr $ do H.td $ "Sprache:" + H.td $ H.select ! A.name "lang" $ do + H.option ! A.value "de" $ "Deutsch" + H.option ! A.value "en" $ "Englisch" + H.tr $ do H.td ! A.style "vertical-align: top;" $ "Text:" + H.td $ H.textarea ! A.name "btext" ! A.cols "100" ! A.rows "15" $ mempty + H.tr $ do H.td ! A.style "vertical-align: top;" $ "Mehr Text:" + H.td $ H.textarea ! A.name "mtext" ! A.cols "100" ! A.rows "15" $ mempty + H.input ! A.type_ "hidden" ! A.name "author" ! A.value (toValue sUser) + H.input ! A.style "margin-left: 20px" ! A.type_ "submit" ! A.value "Absenden" + H.p $ do preEscapedText "Startseite -- Entrylist: DE" + preEscapedText " & EN -- Backup (NYI)" + -- Error pages showError :: BlogError -> BlogLang -> Html showError NotFound l = undefined diff --git a/src/BlogDB.hs b/src/BlogDB.hs index d5a964da8a..7a4f869eb7 100644 --- a/src/BlogDB.hs +++ b/src/BlogDB.hs @@ -40,10 +40,10 @@ instance Show BlogLang where $(deriveSafeCopy 0 'base ''BlogLang) -data Comment = Comment {  +data Comment = Comment { + cdate :: UTCTime, cauthor :: Text, - ctext :: Text, - cdate :: UTCTime + ctext :: Text } deriving (Eq, Ord, Show, Data, Typeable) $(deriveSafeCopy 0 'base ''Comment) @@ -221,11 +221,18 @@ interactiveUserAdd = do putStrLn "Password:" pw <- getLine update' acid (AddUser (pack un) pw) - createCheckpointAndClose acid + closeAcidState acid flushSessions :: IO () flushSessions = do tbDir <- getEnv "TAZBLOG" acid <- openLocalStateFrom (tbDir ++ "/BlogState") initialBlogState update' acid (ClearSessions) - createCheckpointAndClose acid + closeAcidState acid + +archiveState :: IO () +archiveState = do + tbDir <- getEnv "TAZBLOG" + acid <- openLocalStateFrom (tbDir ++ "/BlogState") initialBlogState + createArchive acid + closeAcidState acid diff --git a/src/Main.hs b/src/Main.hs index b979c3bb8a..43faeac93f 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -27,13 +27,13 @@ import System.Environment(getEnv) import System.Locale (defaultTimeLocale) import Blog -import BlogDB hiding (addComment) +import BlogDB hiding (addComment, updateEntry) import Locales {- Server -} tmpPolicy :: BodyPolicy -tmpPolicy = (defaultBodyPolicy "./tmp/" 0 1000 1000) +tmpPolicy = (defaultBodyPolicy "./tmp/" 0 200000 1000) main :: IO() main = do @@ -44,7 +44,7 @@ main = do (\acid -> simpleHTTP nullConf $ tazBlog acid) tazBlog :: AcidState Blog -> ServerPart Response -tazBlog acid = do +tazBlog acid = msum [ dir (show DE) $ blogHandler acid DE , dir (show EN) $ blogHandler acid EN , do nullDir @@ -55,8 +55,8 @@ tazBlog acid = do , dir "res" $ serveDirectory DisableBrowsing [] "../res" , dir "notice" $ ok $ toResponse showSiteNotice , do dir "admin" $ guardSession acid - adminHandler - , dir "admin" $ ok $ toResponse $ adminTemplate adminLogin "Login" + adminHandler acid + , dir "admin" $ ok $ toResponse $ adminLogin , dir "dologin" $ processLogin acid , serveDirectory DisableBrowsing [] "../res" ] @@ -64,29 +64,13 @@ tazBlog acid = do blogHandler :: AcidState Blog -> BlogLang -> ServerPart Response blogHandler acid lang = msum [ path $ \(eId :: Integer) -> showEntry acid lang $ EntryId eId - , do - decodeBody tmpPolicy - dir "postcomment" $ path $ - \(eId :: Integer) -> addComment acid $ EntryId eId + , do decodeBody tmpPolicy + dir "postcomment" $ path $ + \(eId :: Integer) -> addComment acid lang $ EntryId eId , do nullDir showIndex acid lang ] -guardSession :: AcidState Blog -> ServerPartT IO () -guardSession acid = do - (sId :: Text) <- readCookieValue "session" - (Just Session{..}) <- query' acid (GetSession $ SessionID sId) - (uName :: Text) <- readCookieValue "sUser" - now <- liftIO $ getCurrentTime - unless (and [uName == username user, sessionTimeDiff now sdate]) - mzero - where - sessionTimeDiff :: UTCTime -> UTCTime -> Bool - sessionTimeDiff now sdate = (diffUTCTime now sdate) > 43200 - -adminHandler :: ServerPart Response -adminHandler = undefined - formatOldLink :: Int -> Int -> String -> ServerPart Response formatOldLink y m id_ = flip seeOther (toResponse ()) $ @@ -115,14 +99,79 @@ showIndex acid lang = do eDrop (Just i) = drop ((i-1) * 6) eDrop Nothing = drop 0 -addComment :: AcidState Blog -> EntryId -> ServerPart Response -addComment acid eId = do +addComment :: AcidState Blog -> BlogLang -> EntryId -> ServerPart Response +addComment acid lang eId = do now <- liftIO $ getCurrentTime >>= return - nComment <- Comment <$> lookText' "cname" + nComment <- Comment <$> pure now + <*> lookText' "cname" <*> lookText' "ctext" - <*> pure now update' acid (AddComment eId nComment) - seeOther ("/" ++ show eId) (toResponse()) + seeOther ("/" ++ show lang ++ "/" ++ show eId) (toResponse()) + +{- ADMIN stuff -} + +adminHandler :: AcidState Blog -> ServerPart Response +adminHandler acid = + msum [ dir "postentry" $ postEntry acid + , dir "entrylist" $ dir (show DE) $ entryList DE + , dir "entrylist" $ dir (show EN) $ entryList EN + , dir "edit" $ path $ \(eId :: Integer) -> editEntry eId + , dir "doedit" $ updateEntry + , ok $ toResponse $ adminIndex ("tazjin" :: Text) --User NYI + ] + +updateEntry :: ServerPart Response +updateEntry = undefined + +postEntry :: AcidState Blog -> ServerPart Response +postEntry acid = do + liftIO $ putStrLn "postEntry called" + --decodeBody tmpPolicy + now <- liftIO $ getCurrentTime + let eId = timeToId now + lang <- lookText' "lang" + nEntry <- Entry <$> pure eId + <*> getLang lang + <*> lookText' "author" + <*> lookText' "title" + <*> lookText' "btext" + <*> lookText' "mtext" + <*> pure now + <*> pure [] -- NYI + <*> pure [] + update' acid (InsertEntry nEntry) + seeOther ("/" ++ (T.unpack lang) ++ "/" ++ show eId) (toResponse()) + where + timeToId :: UTCTime -> EntryId + timeToId t = EntryId . read $ formatTime defaultTimeLocale "%s" t + getLang :: Text -> ServerPart BlogLang + getLang "de" = return DE + getLang "en" = return EN + + +entryList :: BlogLang -> ServerPart Response +entryList lang = undefined + +editEntry :: Integer -> ServerPart Response +editEntry i = undefined + where + eId = EntryId i + +guardSession :: AcidState Blog -> ServerPartT IO () +guardSession acid = do + (sId :: Text) <- readCookieValue "session" + (uName :: Text) <- readCookieValue "sUser" + now <- liftIO $ getCurrentTime + mS <- query' acid (GetSession $ SessionID sId) + case mS of + Nothing -> mzero + (Just Session{..}) -> unless (and [ uName == username user + , sessionTimeDiff now sdate]) + mzero + where + sessionTimeDiff :: UTCTime -> UTCTime -> Bool + sessionTimeDiff now sdate = (diffUTCTime now sdate) < 43200 + processLogin :: AcidState Blog -> ServerPart Response processLogin acid = do @@ -132,7 +181,7 @@ processLogin acid = do login <- query' acid (CheckUser (Username account) password) if' login (createSession account) - (ok $ toResponse $ adminTemplate adminLogin "Login failed") + (ok $ toResponse $ adminLogin) where createSession account = do now <- liftIO getCurrentTime diff --git a/tools/acid-migrate/Acid.hs b/tools/acid-migrate/Acid.hs index 276102eb03..10ab3e23d0 100644 --- a/tools/acid-migrate/Acid.hs +++ b/tools/acid-migrate/Acid.hs @@ -54,10 +54,10 @@ instance Show BlogLang where $(deriveSafeCopy 0 'base ''BlogLang) -data Comment = Comment {  +data Comment = Comment { + cdate :: UTCTime, cauthor :: Text, - ctext :: Text, - cdate :: UTCTime + ctext :: Text } deriving (Eq, Ord, Show, Data, Typeable) $(deriveSafeCopy 0 'base ''Comment) @@ -203,7 +203,7 @@ instance JSON Comment where jsscdate <- jsonField "cdate" obj :: Result JSValue let rcdate = stripResult $ jsonInt jsscdate sctext <- jsonField "ctext" obj - return $ Comment (pack scauthor) (pack sctext) (parseSeconds rcdate) + return $ Comment (parseSeconds rcdate) (pack scauthor) (pack sctext) instance JSON Entry where showJSON = undefined -- cgit 1.4.1 From 36c4d7e84e1b2e18389eeda66c33d25fc7819f15 Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Wed, 14 Mar 2012 00:38:58 +0100 Subject: * escaping comments --- TODO | 2 +- src/Blog.hs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/TODO b/TODO index 3de1a19190..b2b76bbbb8 100644 --- a/TODO +++ b/TODO @@ -1,3 +1,3 @@ * handle BlogErrors * add readMore link -* Twitter: http://twitter.github.com/bootstrap/index.html \ No newline at end of file +* Bootstrap: http://twitter.github.com/bootstrap/index.html diff --git a/src/Blog.hs b/src/Blog.hs index 8e4c76b621..48b67a93b0 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -110,7 +110,7 @@ renderComments comments lang = sequence_ $ map showComment comments showComment :: Comment -> Html showComment (Comment{..}) = H.li $ do H.i $ toHtml $ T.append cauthor ": " - preEscapedText $ ctext + toHtml ctext H.p ! A.class_ "tt" $ toHtml $ timeString cdate timeString t = formatTime defaultTimeLocale (cTimeFormat lang) t -- cgit 1.4.1 From 47e1be1f7852c141f604c36199dd767d3a5c3d86 Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Wed, 14 Mar 2012 01:36:57 +0100 Subject: * fixed entry-adding * temporarily removed adminHandler and merged it into tazBlog :: ServerPart Response --- src/Blog.hs | 2 +- src/Main.hs | 32 +++++++++++++++++++------------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/Blog.hs b/src/Blog.hs index 48b67a93b0..8e53046edd 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -85,7 +85,7 @@ renderEntry (Entry{..}) = H.div ! A.class_ "innerBox" $ do H.div ! A.class_ "innerBoxMiddle" $ do H.article $ H.ul $ H.li $ do preEscapedText $ btext - preEscapedText $ mtext + H.p $ preEscapedText $ mtext H.div ! A.class_ "innerBoxComments" $ do H.div ! A.class_ "cHead" $ toHtml $ cHead lang -- ! A.style "font-size:large;font-weight:bold;" H.ul $ renderComments comments lang diff --git a/src/Main.hs b/src/Main.hs index 43faeac93f..b05599de58 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -54,13 +54,29 @@ tazBlog acid = , path $ \(year :: Int) -> path $ \(month :: Int) -> path $ \(id_ :: String) -> formatOldLink year month id_ , dir "res" $ serveDirectory DisableBrowsing [] "../res" , dir "notice" $ ok $ toResponse showSiteNotice - , do dir "admin" $ guardSession acid - adminHandler acid + , do dirs "admin/postentry" $ nullDir + guardSession acid + postEntry acid + , do dir "admin" $ nullDir + guardSession acid + ok $ toResponse $ adminIndex ("tazjin" :: Text) , dir "admin" $ ok $ toResponse $ adminLogin , dir "dologin" $ processLogin acid , serveDirectory DisableBrowsing [] "../res" ] +{- +adminHandler :: AcidState Blog -> ServerPart Response +adminHandler acid = + msum [ dir "postentry" $ postEntry acid + , dir "entrylist" $ dir (show DE) $ entryList DE + , dir "entrylist" $ dir (show EN) $ entryList EN + , dir "edit" $ path $ \(eId :: Integer) -> editEntry eId + , dir "doedit" $ updateEntry + , ok $ toResponse $ adminIndex ("tazjin" :: Text) --User NYI + ] +-} + blogHandler :: AcidState Blog -> BlogLang -> ServerPart Response blogHandler acid lang = msum [ path $ \(eId :: Integer) -> showEntry acid lang $ EntryId eId @@ -110,23 +126,13 @@ addComment acid lang eId = do {- ADMIN stuff -} -adminHandler :: AcidState Blog -> ServerPart Response -adminHandler acid = - msum [ dir "postentry" $ postEntry acid - , dir "entrylist" $ dir (show DE) $ entryList DE - , dir "entrylist" $ dir (show EN) $ entryList EN - , dir "edit" $ path $ \(eId :: Integer) -> editEntry eId - , dir "doedit" $ updateEntry - , ok $ toResponse $ adminIndex ("tazjin" :: Text) --User NYI - ] updateEntry :: ServerPart Response updateEntry = undefined postEntry :: AcidState Blog -> ServerPart Response postEntry acid = do - liftIO $ putStrLn "postEntry called" - --decodeBody tmpPolicy + decodeBody tmpPolicy now <- liftIO $ getCurrentTime let eId = timeToId now lang <- lookText' "lang" -- cgit 1.4.1 From df9a17b695c82d46eeaddb1cb1feb9fed4c81d3a Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Thu, 15 Mar 2012 18:32:01 +0100 Subject: * updating entries and entrylist * entryEscape ("\n" -> "
") --- src/Blog.hs | 33 ++++++++++++++++++++++++++++ src/Main.hs | 71 +++++++++++++++++++++++++++++++++++++++---------------------- 2 files changed, 78 insertions(+), 26 deletions(-) diff --git a/src/Blog.hs b/src/Blog.hs index 8e53046edd..f7e5f5f3b7 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -193,6 +193,39 @@ adminIndex sUser = adminTemplate "Index" $ H.td $ H.textarea ! A.name "mtext" ! A.cols "100" ! A.rows "15" $ mempty H.input ! A.type_ "hidden" ! A.name "author" ! A.value (toValue sUser) H.input ! A.style "margin-left: 20px" ! A.type_ "submit" ! A.value "Absenden" + adminFooter + +adminFooter :: Html +adminFooter = H.p $ do + preEscapedText "Startseite -- Entrylist: DE" + preEscapedText " & EN -- Backup (NYI)" + +adminEntryList :: [Entry] -> Html +adminEntryList entries = adminTemplate "Entrylist" $ + H.div ! A.style "float: center;" $ do + H.table $ do + sequence_ $ map showEntryItem entries + adminFooter + where + showEntryItem :: Entry -> Html + showEntryItem (Entry{..}) = H.tr $ do + H.td $ H.a ! A.href (toValue $ "/admin/edit/" ++ show entryId) $ toHtml title + H.td $ toHtml $ formatTime defaultTimeLocale "[On %D at %H:%M]" edate + + +editPage :: Entry -> Html +editPage (Entry{..}) = adminTemplate "Index" $ + H.div ! A.style "float: center;" $ + H.form ! A.action "/admin/updateentry" ! A.method "POST" $ do + H.table $ do + H.tr $ do H.td $ "Titel:" + H.td $ H.input ! A.type_ "text" ! A.name "title" ! A.value (toValue title) + H.tr $ do H.td ! A.style "vertical-align: top;" $ "Text:" + H.td $ H.textarea ! A.name "btext" ! A.cols "100" ! A.rows "15" $ toHtml btext + H.tr $ do H.td ! A.style "vertical-align: top;" $ "Mehr Text:" + H.td $ H.textarea ! A.name "mtext" ! A.cols "100" ! A.rows "15" $ toHtml mtext + H.input ! A.type_ "hidden" ! A.name "eid" ! A.value (toValue $ unEntryId entryId) + H.input ! A.style "margin-left: 20px" ! A.type_ "submit" ! A.value "Absenden" H.p $ do preEscapedText "Startseite -- Entrylist: DE" preEscapedText " & EN -- Backup (NYI)" diff --git a/src/Main.hs b/src/Main.hs index b05599de58..fdf2134c92 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -54,9 +54,20 @@ tazBlog acid = , path $ \(year :: Int) -> path $ \(month :: Int) -> path $ \(id_ :: String) -> formatOldLink year month id_ , dir "res" $ serveDirectory DisableBrowsing [] "../res" , dir "notice" $ ok $ toResponse showSiteNotice + {- :Admin handlers -} , do dirs "admin/postentry" $ nullDir guardSession acid postEntry acid + , do dirs "admin/entrylist" $ dir (show DE) $ nullDir + guardSession acid + entryList acid DE + , do dirs "admin/entrylist" $ dir (show EN) $ nullDir + guardSession acid + entryList acid EN + , do guardSession acid + dirs "admin/edit" $ path $ \(eId :: Integer) -> editEntry acid eId + , do dirs "admin/updateentry" $ nullDir + updateEntry acid , do dir "admin" $ nullDir guardSession acid ok $ toResponse $ adminIndex ("tazjin" :: Text) @@ -65,18 +76,6 @@ tazBlog acid = , serveDirectory DisableBrowsing [] "../res" ] -{- -adminHandler :: AcidState Blog -> ServerPart Response -adminHandler acid = - msum [ dir "postentry" $ postEntry acid - , dir "entrylist" $ dir (show DE) $ entryList DE - , dir "entrylist" $ dir (show EN) $ entryList EN - , dir "edit" $ path $ \(eId :: Integer) -> editEntry eId - , dir "doedit" $ updateEntry - , ok $ toResponse $ adminIndex ("tazjin" :: Text) --User NYI - ] --} - blogHandler :: AcidState Blog -> BlogLang -> ServerPart Response blogHandler acid lang = msum [ path $ \(eId :: Integer) -> showEntry acid lang $ EntryId eId @@ -124,45 +123,65 @@ addComment acid lang eId = do update' acid (AddComment eId nComment) seeOther ("/" ++ show lang ++ "/" ++ show eId) (toResponse()) -{- ADMIN stuff -} - - -updateEntry :: ServerPart Response -updateEntry = undefined +{- ADMIN stuff -} postEntry :: AcidState Blog -> ServerPart Response postEntry acid = do decodeBody tmpPolicy now <- liftIO $ getCurrentTime let eId = timeToId now - lang <- lookText' "lang" + lang <- look "lang" + nBtext <- lookText' "btext" + nMtext <- lookText' "mtext" nEntry <- Entry <$> pure eId <*> getLang lang <*> lookText' "author" <*> lookText' "title" - <*> lookText' "btext" - <*> lookText' "mtext" + <*> pure (entryEscape nBtext) + <*> pure (entryEscape nMtext) <*> pure now <*> pure [] -- NYI <*> pure [] update' acid (InsertEntry nEntry) - seeOther ("/" ++ (T.unpack lang) ++ "/" ++ show eId) (toResponse()) + seeOther ("/" ++ lang ++ "/" ++ show eId) (toResponse()) where timeToId :: UTCTime -> EntryId timeToId t = EntryId . read $ formatTime defaultTimeLocale "%s" t - getLang :: Text -> ServerPart BlogLang + getLang :: String -> ServerPart BlogLang getLang "de" = return DE getLang "en" = return EN +entryEscape :: Text -> Text +entryEscape = T.replace "\n" "
" -entryList :: BlogLang -> ServerPart Response -entryList lang = undefined +entryList :: AcidState Blog -> BlogLang -> ServerPart Response +entryList acid lang = do + entries <- query' acid (LatestEntries lang) + ok $ toResponse $ adminEntryList entries -editEntry :: Integer -> ServerPart Response -editEntry i = undefined +editEntry :: AcidState Blog -> Integer -> ServerPart Response +editEntry acid i = do + (Just entry) <- query' acid (GetEntry eId) + ok $ toResponse $ editPage entry where eId = EntryId i +updateEntry :: AcidState Blog -> ServerPart Response +updateEntry acid = do + decodeBody tmpPolicy + (eId :: Integer) <- lookRead "eid" + (Just entry) <- query' acid (GetEntry $ EntryId eId) + nTitle <- lookText' "title" + nBtext <- lookText' "btext" + nMtext <- lookText' "mtext" + let nEntry = entry { title = nTitle + , btext = entryEscape nBtext + , mtext = entryEscape nMtext} + update' acid (UpdateEntry nEntry) + seeOther (concat $ intersperse' "/" [show $ lang entry, show eId]) + (toResponse ()) + + guardSession :: AcidState Blog -> ServerPartT IO () guardSession acid = do (sId :: Text) <- readCookieValue "session" -- cgit 1.4.1 From 579c11cd2a62ea81047b0649dc7aa473a39456b7 Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Thu, 15 Mar 2012 20:16:39 +0100 Subject: * version 3.0 * fixed read-more links --- src/Blog.hs | 14 ++++++++++---- src/Locales.hs | 6 +++--- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/Blog.hs b/src/Blog.hs index f7e5f5f3b7..534803baa4 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -2,6 +2,7 @@ module Blog where +import Control.Monad (when, unless) import Data.Data (Data, Typeable) import Data.List (intersperse) import Data.Monoid (mempty) @@ -71,10 +72,15 @@ renderEntries showAll entries topText footerLinks = where showEntry :: Entry -> Html showEntry e = H.li $ do - entryLink e - preEscapedText $ T.concat [" ", btext e, "
 
"] - entryLink e = H.a ! A.href (toValue $ concat $ intersperse' "/" $ linkElems e) $ - toHtml ("[" ++ show(length $ comments e) ++ "]") + entryLink e $ T.pack $ show(length $ comments e) + preEscapedText $ T.append " " $ btext e + when ( mtext e /= T.empty ) $ + H.p $ entryLink e $ readMore $ lang e + unless ( mtext e /= T.empty ) $ + preEscapedText "
 
" + entryLink :: Entry -> Text -> Html + entryLink e s = H.a ! A.href (toValue $ concat $ intersperse' "/" $ linkElems e) $ + toHtml (T.concat ["[", s, "]"]) linkElems e = [show(lang e), show $ entryId e] getFooterLinks (Just h) = h getFooterLinks Nothing = mempty diff --git a/src/Locales.hs b/src/Locales.hs index 393a69f8fc..9330fd81fb 100644 --- a/src/Locales.hs +++ b/src/Locales.hs @@ -13,7 +13,7 @@ import BlogDB (BlogLang (..)) data BlogError = NotFound | DBError -version = "2.2b" +version = "3.0" allLang = [EN, DE] @@ -73,8 +73,8 @@ nextText DE = "Später" nextText EN = "Later" readMore :: BlogLang -> Text -readMore DE = "[Weiterlesen]" -readMore EN = "[Read more]" +readMore DE = "Weiterlesen" +readMore EN = "Read more" -- contact information contactText :: BlogLang -> Text -- cgit 1.4.1 From cc1bf634682ed63c0f916acff8b910ddd6f5d135 Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Thu, 15 Mar 2012 20:19:25 +0100 Subject: * port 80 --- src/Main.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Main.hs b/src/Main.hs index fdf2134c92..86d51bdc9a 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -41,7 +41,7 @@ main = do tbDir <- getEnv "TAZBLOG" bracket (openLocalStateFrom (tbDir ++ "/BlogState") initialBlogState) (createCheckpointAndClose) - (\acid -> simpleHTTP nullConf $ tazBlog acid) + (\acid -> simpleHTTP nullConf {port = 80} $ tazBlog acid) tazBlog :: AcidState Blog -> ServerPart Response tazBlog acid = -- cgit 1.4.1 From f591f6b4f72a6a6989a7631b17fbd9ee5d7f6c6d Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Thu, 15 Mar 2012 20:51:53 +0100 Subject: * newlines in comments --- src/Main.hs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Main.hs b/src/Main.hs index 86d51bdc9a..fe111f7666 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -117,9 +117,10 @@ showIndex acid lang = do addComment :: AcidState Blog -> BlogLang -> EntryId -> ServerPart Response addComment acid lang eId = do now <- liftIO $ getCurrentTime >>= return + nCtext <- lookText' "ctext" nComment <- Comment <$> pure now <*> lookText' "cname" - <*> lookText' "ctext" + <*> pure (entryEscape nCtext) update' acid (AddComment eId nComment) seeOther ("/" ++ show lang ++ "/" ++ show eId) (toResponse()) -- cgit 1.4.1 From b8a045d163a50c138bfe6300fc39b5cddc40f5d1 Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Thu, 15 Mar 2012 21:26:45 +0100 Subject: * proper comment escaping --- src/Blog.hs | 2 +- src/Main.hs | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Blog.hs b/src/Blog.hs index 534803baa4..5914052f93 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -116,7 +116,7 @@ renderComments comments lang = sequence_ $ map showComment comments showComment :: Comment -> Html showComment (Comment{..}) = H.li $ do H.i $ toHtml $ T.append cauthor ": " - toHtml ctext + preEscapedText ctext H.p ! A.class_ "tt" $ toHtml $ timeString cdate timeString t = formatTime defaultTimeLocale (cTimeFormat lang) t diff --git a/src/Main.hs b/src/Main.hs index fe111f7666..8adef253a5 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -120,10 +120,18 @@ addComment acid lang eId = do nCtext <- lookText' "ctext" nComment <- Comment <$> pure now <*> lookText' "cname" - <*> pure (entryEscape nCtext) + <*> pure (commentEscape nCtext) update' acid (AddComment eId nComment) seeOther ("/" ++ show lang ++ "/" ++ show eId) (toResponse()) +commentEscape :: Text -> Text +commentEscape = newlineEscape . ltEscape . gtEscape . ampEscape + where + newlineEscape = T.replace "\n" "
" + ampEscape = T.replace "&" "&" + ltEscape = T.replace "<" "<" + gtEscape = T.replace ">" ">" + {- ADMIN stuff -} postEntry :: AcidState Blog -> ServerPart Response -- cgit 1.4.1 From b7e34eba74918c04ab247ba7d761ddc3e6c46321 Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Sun, 18 Mar 2012 23:23:02 +0100 Subject: * changed entryEscape --- src/Main.hs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Main.hs b/src/Main.hs index 8adef253a5..84fa0d9dd3 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -161,7 +161,10 @@ postEntry acid = do getLang "en" = return EN entryEscape :: Text -> Text -entryEscape = T.replace "\n" "
" +entryEscape = newlineEscape . newlineRNEscape + where + newlineEscape = T.replace "\n" "
" + newlineRNEscape = T.replace "\r\n" "
" entryList :: AcidState Blog -> BlogLang -> ServerPart Response entryList acid lang = do -- cgit 1.4.1 From 515660fa7deeeb6753768378e0cfa38a4616e03a Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Sun, 18 Mar 2012 23:47:13 +0100 Subject: * fixed 404 page --- TODO | 2 -- res/blogstyle.css | 10 ++++++++++ src/Blog.hs | 7 ++++++- src/Locales.hs | 9 +++++++++ src/Main.hs | 2 ++ 5 files changed, 27 insertions(+), 3 deletions(-) diff --git a/TODO b/TODO index b2b76bbbb8..fdb963dd79 100644 --- a/TODO +++ b/TODO @@ -1,3 +1 @@ -* handle BlogErrors -* add readMore link * Bootstrap: http://twitter.github.com/bootstrap/index.html diff --git a/res/blogstyle.css b/res/blogstyle.css index d8e878b012..37cf90c10a 100644 --- a/res/blogstyle.css +++ b/res/blogstyle.css @@ -118,6 +118,16 @@ body { padding-left: 20px } +.notFoundFace { + text-align: center; + font-size: 100px; +} + +.notFoundText { + text-align: center; + font-size: 24px; + font-weight: bold; +} label span { width: 6%; diff --git a/src/Blog.hs b/src/Blog.hs index 5914052f93..d6b806985e 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -237,4 +237,9 @@ editPage (Entry{..}) = adminTemplate "Index" $ -- Error pages showError :: BlogError -> BlogLang -> Html -showError NotFound l = undefined +showError NotFound l = blogTemplate l (T.append ": " $ notFound l) $ + H.div ! A.class_ "innerBox" $ do + H.div ! A.class_ "innerBoxTop" $ toHtml $ notFound l + H.div ! A.class_ "innerBoxMiddle" $ do + H.p ! A.class_ "notFoundFace" $ toHtml (":'(" :: Text) + H.p ! A.class_ "notFoundText" $ toHtml $ notFoundText l diff --git a/src/Locales.hs b/src/Locales.hs index 9330fd81fb..c1dc02453c 100644 --- a/src/Locales.hs +++ b/src/Locales.hs @@ -115,6 +115,15 @@ cSend :: BlogLang -> Text cSend DE = "Absenden" cSend EN = "Submit" +-- errors +notFound :: BlogLang -> Text +notFound DE = "Nicht gefunden" +notFound EN = "Not found" + +notFoundText :: BlogLang -> Text +notFoundText DE = "Das gewünschte Objekt wurde leider nicht gefunden." +notFoundText EN = "The requested object could unfortunately not be found." + -- right side text (this is inserted AS IS. Escape HTML!) rightText :: BlogLang -> Text rightText DE = "English version available here." diff --git a/src/Main.hs b/src/Main.hs index 84fa0d9dd3..f12c743494 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -74,6 +74,7 @@ tazBlog acid = , dir "admin" $ ok $ toResponse $ adminLogin , dir "dologin" $ processLogin acid , serveDirectory DisableBrowsing [] "../res" + , ok $ toResponse $ showError NotFound DE ] blogHandler :: AcidState Blog -> BlogLang -> ServerPart Response @@ -84,6 +85,7 @@ blogHandler acid lang = \(eId :: Integer) -> addComment acid lang $ EntryId eId , do nullDir showIndex acid lang + , ok $ toResponse $ showError NotFound lang ] formatOldLink :: Int -> Int -> String -> ServerPart Response -- cgit 1.4.1 From da388782122779e865fc5454e5182d95c7f8fa26 Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Sun, 18 Mar 2012 23:49:50 +0100 Subject: * correctly serving 404s with status code 404 :| --- src/Blog.hs | 4 ++-- src/Locales.hs | 6 +++--- src/Main.hs | 12 ++++++------ 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Blog.hs b/src/Blog.hs index d6b806985e..f481e578fd 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -237,9 +237,9 @@ editPage (Entry{..}) = adminTemplate "Index" $ -- Error pages showError :: BlogError -> BlogLang -> Html -showError NotFound l = blogTemplate l (T.append ": " $ notFound l) $ +showError NotFound l = blogTemplate l (T.append ": " $ notFoundTitle l) $ H.div ! A.class_ "innerBox" $ do - H.div ! A.class_ "innerBoxTop" $ toHtml $ notFound l + H.div ! A.class_ "innerBoxTop" $ toHtml $ notFoundTitle l H.div ! A.class_ "innerBoxMiddle" $ do H.p ! A.class_ "notFoundFace" $ toHtml (":'(" :: Text) H.p ! A.class_ "notFoundText" $ toHtml $ notFoundText l diff --git a/src/Locales.hs b/src/Locales.hs index c1dc02453c..7d36b2d2b1 100644 --- a/src/Locales.hs +++ b/src/Locales.hs @@ -116,9 +116,9 @@ cSend DE = "Absenden" cSend EN = "Submit" -- errors -notFound :: BlogLang -> Text -notFound DE = "Nicht gefunden" -notFound EN = "Not found" +notFoundTitle :: BlogLang -> Text +notFoundTitle DE = "Nicht gefunden" +notFoundTitle EN = "Not found" notFoundText :: BlogLang -> Text notFoundText DE = "Das gewünschte Objekt wurde leider nicht gefunden." diff --git a/src/Main.hs b/src/Main.hs index f12c743494..ce423f932d 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -74,7 +74,7 @@ tazBlog acid = , dir "admin" $ ok $ toResponse $ adminLogin , dir "dologin" $ processLogin acid , serveDirectory DisableBrowsing [] "../res" - , ok $ toResponse $ showError NotFound DE + , notFound $ toResponse $ showError NotFound DE ] blogHandler :: AcidState Blog -> BlogLang -> ServerPart Response @@ -85,7 +85,7 @@ blogHandler acid lang = \(eId :: Integer) -> addComment acid lang $ EntryId eId , do nullDir showIndex acid lang - , ok $ toResponse $ showError NotFound lang + , notFound $ toResponse $ showError NotFound lang ] formatOldLink :: Int -> Int -> String -> ServerPart Response @@ -96,11 +96,11 @@ formatOldLink y m id_ = showEntry :: AcidState Blog -> BlogLang -> EntryId -> ServerPart Response showEntry acid lang eId = do entry <- query' acid (GetEntry eId) - ok $ tryEntry entry lang + tryEntry entry lang -tryEntry :: Maybe Entry -> BlogLang -> Response -tryEntry Nothing lang = toResponse $ showError NotFound lang -tryEntry (Just entry) _ = toResponse $ blogTemplate eLang eTitle $ renderEntry entry +tryEntry :: Maybe Entry -> BlogLang -> ServerPart Response +tryEntry Nothing lang = notFound $ toResponse $ showError NotFound lang +tryEntry (Just entry) _ = ok $ toResponse $ blogTemplate eLang eTitle $ renderEntry entry where eTitle = T.append ": " (title entry) eLang = lang entry -- cgit 1.4.1 From a762db84ae1c20e2f5faa64aefe5e70ac2a33b4b Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Mon, 19 Mar 2012 00:08:27 +0100 Subject: * min-height for --- res/blogstyle.css | 1 + 1 file changed, 1 insertion(+) diff --git a/res/blogstyle.css b/res/blogstyle.css index 37cf90c10a..3a5070ab34 100644 --- a/res/blogstyle.css +++ b/res/blogstyle.css @@ -11,6 +11,7 @@ body { padding-top: 20px; font-family: 'PT Sans', sans-serif; + min-height: 850px; background-image: linear-gradient(bottom, rgb(245,245,245) 66%, rgb(239,239,239) 83%); background-image: -o-linear-gradient(bottom, rgb(245,245,245) 66%, rgb(239,239,239) 83%); background-image: -moz-linear-gradient(bottom, rgb(245,245,245) 66%, rgb(239,239,239) 83%); -- cgit 1.4.1 From 39a30af9c21e173aadacc1d734747f20e9f07e24 Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Mon, 19 Mar 2012 04:27:00 +0100 Subject: * Google analytics --- src/Blog.hs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/Blog.hs b/src/Blog.hs index f481e578fd..fd69d4df5f 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -26,6 +26,18 @@ intersperse' sep l = sep : intersperse sep l replace :: Eq a => a -> a -> [a] -> [a] replace x y = map (\z -> if z == x then y else z) +analytics :: Text +analytics = T.pack $ unlines [""] + blogTemplate :: BlogLang -> Text -> Html -> Html blogTemplate lang t_append body = H.docTypeHtml $ do --add body H.head $ do @@ -34,6 +46,7 @@ blogTemplate lang t_append body = H.docTypeHtml $ do --add body H.link ! A.rel "stylesheet" ! A.type_ "text/css" ! A.href "/res/blogstyle.css" ! A.media "all" H.meta ! A.httpEquiv "content-type" ! A.content "text/html;charset=UTF-8" --H.style ! A.type_ "text/css" ! A.title "iOS iMessage" ! A.media "screen and (max-device-width: 1024px)" $ "#cosx{display:none;}" + preEscapedText analytics H.body $ do H.div ! A.class_ "mainshell" $ H.div ! A.class_ "gradBox" $ do H.div ! A.class_ "header" $ do -- cgit 1.4.1 From 877a7f84b04e424ffe04e4d0eee889890454978c Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Tue, 20 Mar 2012 00:26:50 +0100 Subject: version 3.1: * entirely new design (looks a lot better. Thanks to @not_eden and @agoptron for their advice) * multi-author support (I won't use it yet) * blogstyle.css serves as the "source" for blog.css * displaying article eDate and author on entry page --- TODO | 1 + res/blog.css | 1 + res/blogstyle.css | 182 ++++++++++++++++++++++++++++-------------------------- src/Blog.hs | 91 +++++++++++++-------------- src/Locales.hs | 14 ++++- src/Main.hs | 2 +- 6 files changed, 153 insertions(+), 138 deletions(-) create mode 100644 res/blog.css diff --git a/TODO b/TODO index fdb963dd79..30448cf667 100644 --- a/TODO +++ b/TODO @@ -1 +1,2 @@ * Bootstrap: http://twitter.github.com/bootstrap/index.html +* use forM_ ( http://jaspervdj.be/blaze/tutorial.html ) \ No newline at end of file diff --git a/res/blog.css b/res/blog.css new file mode 100644 index 0000000000..8ab3522d74 --- /dev/null +++ b/res/blog.css @@ -0,0 +1 @@ +@charset UTF-8;@font-face{font-family:'PT Sans';font-style:normal;font-weight:400;src:local('PT Sans'), local(PTSans-Regular), url(http://themes.googleusercontent.com/static/fonts/ptsans/v2/7YGmE4Ls5b94ct65u07hVQLUuEpTyoUstqEm5AMlJo4.woff) format(woff);}html,body{margin:0;padding:0;}body{padding-top:10px;font-family:'PT Sans', sans-serif;min-height:850px;background-color:#4A525A;color:#EEE;}a{color:#000;}input,textarea,select{border:1px solid #555;font-size:15px;line-height:1.2em;width:550px;background:#F9F9F9;-webkit-border-radius:.5em;padding:.5em;}.header{z-index:4;padding-left:20px;padding-bottom:70px;padding-top:30px;position:relative;box-shadow:0 6px 5px 1px #848F9A;}.link{color:#EEE;}.middle{position:relative;z-index:2;display:block;width:100%;padding-top:40px;background-color:#EEE;color:#000;}.footer{z-index:4;position:relative;background-color:#4A525A;margin-top:30px;padding-top:20px;box-shadow:0 -6px 5px 1px #848F9A;color:#EEE;}.btitle{text-decoration:none;color:#EEE;font-size:x-large;font-weight:700;margin-top:15px;margin-bottom:10px;}.contacts{float:left;}.righttext{float:right;padding-right:20px;}.rightbox{text-align:right;padding-right:14px;}.innerTitle{margin-left:10px;font-weight:700;}.innerBoxComments{margin-left:10px;}.innerContainer{padding-right:20px;}.centerbox{text-align:center;min-height:45px;}.cInput{margin-left:15px;}.tt{font-family:"courier new",courier,monospace;font-size:13px;}.cl{text-decoration:none;color:#000;}.cHead{font-size:large;font-weight:700;}.notFoundFace{text-align:center;font-size:100px;}.notFoundText{text-align:center;font-size:24px;font-weight:700;} diff --git a/res/blogstyle.css b/res/blogstyle.css index 3a5070ab34..d58c405cda 100644 --- a/res/blogstyle.css +++ b/res/blogstyle.css @@ -8,51 +8,118 @@ src: local('PT Sans'), local('PTSans-Regular'), url('http://themes.googleusercontent.com/static/fonts/ptsans/v2/7YGmE4Ls5b94ct65u07hVQLUuEpTyoUstqEm5AMlJo4.woff') format('woff'); } +html, body{ + margin: 0; + padding: 0; +} + body { - padding-top: 20px; + padding-top: 10px; font-family: 'PT Sans', sans-serif; min-height: 850px; - background-image: linear-gradient(bottom, rgb(245,245,245) 66%, rgb(239,239,239) 83%); - background-image: -o-linear-gradient(bottom, rgb(245,245,245) 66%, rgb(239,239,239) 83%); - background-image: -moz-linear-gradient(bottom, rgb(245,245,245) 66%, rgb(239,239,239) 83%); - background-image: -webkit-linear-gradient(bottom, rgb(245,245,245) 66%, rgb(239,239,239) 83%); - background-image: -ms-linear-gradient(bottom, rgb(245,245,245) 66%, rgb(239,239,239) 83%); - background-image: -webkit-gradient( - linear, - left bottom, - left top, - color-stop(0.66, rgb(245,245,245)), - color-stop(0.83, rgb(239,239,239)) - ); + background-color: #4A525A; + color: #EEE; } -.mainshell { - width: 98%; - margin: auto; +a { + color: black; } -.gradBox { - width: 98%; - margin: auto; +input, textarea, select { + border: 1px solid #555; + padding: 0.5em; + font-size: 15px; + line-height: 1.2em; + width: 550px; + background: #F9F9F9; + -webkit-border-radius: 0.5em; + } + +/* site sections */ +.header { + z-index: 4; + padding-left: 20px; + padding-bottom: 70px; + padding-top: 30px; + position: relative; + box-shadow: 0 6px 5px 1px #848F9A; } -.myclear { - clear: both; - height: 20px; +.link { + color: #EEE; } -.centerbox { - text-align:center; - min-height: 45px; +.middle { + position: relative; + z-index: 2; + display: block; + width: 100%; + padding-top: 40px; + background-color: #EEE; + color: black; +} + +.footer { + z-index: 4; + position: relative; + background-color: #4A525A; + margin-top: 30px; + padding-top: 20px; + box-shadow: 0 -6px 5px 1px #848F9A; + color: #EEE; +} + +/* header elements */ + +.btitle { + text-decoration:none; + color: #EEE; + font-size:x-large; + font-weight:bold; + margin-top: 15px; + margin-bottom: 10px; +} + +.contacts { + float: left; +} + +.righttext { + float:right; + padding-right: 20px; } .rightbox { text-align:right; + padding-right: 14px; } -.innerBox { - width: 100%; - margin-top: 20px; +/* middle elements */ +.innerTitle { + margin-left: 10px; + font-weight: bold; +} + +.innerBoxComments{ + margin-left: 10px; +} + +.innerContainer { + padding-right: 20px; + +} + +/* common elements */ +.centerbox { + text-align:center; + min-height: 45px; +} + + +/* style elements */ + +.cInput { + margin-left: 15px; } .tt { @@ -69,56 +136,6 @@ body { font-weight:bold; } -.innerBoxTop { - height: 28px; - color: #000000; - font-weight: bold; - font-size: 16px; - padding-left: 20px; - padding-top: 11px; - border: 1px solid #b6b6b6; - border-top-left-radius: 6px; - border-top-right-radius: 6px; - -moz-border-radius-topleft: 6px; - -moz-border-radius-topright: 6px; - -webkit-border-top-left-radius: 6px; - -webkit-border-top-right-radius: 6px; - border-bottom: 1px solid #dcdcdc; - background-image: linear-gradient(bottom, rgb(246,246,246) 31%, rgb(234,234,234) 83%); - background-image: -o-linear-gradient(bottom, rgb(246,246,246) 31%, rgb(234,234,234) 83%); - background-image: -moz-linear-gradient(bottom, rgb(246,246,246) 31%, rgb(234,234,234) 83%); - background-image: -webkit-linear-gradient(bottom, rgb(246,246,246) 31%, rgb(234,234,234) 83%); - background-image: -ms-linear-gradient(bottom, rgb(246,246,246) 31%, rgb(234,234,234) 83%); - - background-image: -webkit-gradient( - linear, - left bottom, - left top, - color-stop(0.31, rgb(246,246,246)), - color-stop(0.83, rgb(234,234,234)) - ); -} - -.innerBoxMiddle { - border: 1px solid #b6b6b6; - border-bottom-left-radius: 6px; - border-bottom-right-radius: 6px; - -moz-border-radius-bottomleft: 6px; - -moz-border-radius-bottomright: 6px; - -webkit-border-bottom-left-radius: 6px; - -webkit-border-bottom-right-radius: 6px; - border-top: 0px hidden; - background-color: #FFFFFF; - min-height: 500px; - height: auto; - padding-top: 21px; - padding-right: 2px; -} - -.innerBoxComments { - padding-left: 20px -} - .notFoundFace { text-align: center; font-size: 100px; @@ -129,12 +146,3 @@ body { font-size: 24px; font-weight: bold; } - -label span { - width: 6%; - float: left; -} - -label input { - display: block; -} diff --git a/src/Blog.hs b/src/Blog.hs index fd69d4df5f..33d7c37793 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -43,45 +43,43 @@ blogTemplate lang t_append body = H.docTypeHtml $ do --add body H.head $ do H.title $ (toHtml $ blogTitle lang t_append) H.link ! A.rel "alternate" ! A.type_ "application/rss+xml" ! A.title "RSS-Feed" ! A.href "/rss" - H.link ! A.rel "stylesheet" ! A.type_ "text/css" ! A.href "/res/blogstyle.css" ! A.media "all" + H.link ! A.rel "stylesheet" ! A.type_ "text/css" ! A.href "/res/blog.css" ! A.media "all" H.meta ! A.httpEquiv "content-type" ! A.content "text/html;charset=UTF-8" --H.style ! A.type_ "text/css" ! A.title "iOS iMessage" ! A.media "screen and (max-device-width: 1024px)" $ "#cosx{display:none;}" preEscapedText analytics H.body $ do - H.div ! A.class_ "mainshell" $ H.div ! A.class_ "gradBox" $ do - H.div ! A.class_ "header" $ do - H.a ! A.href (toValue $ "/" ++ show lang) ! - A.style "text-decoration:none;color:black;font-size:x-large;font-weight:bold;" $ - toHtml $ blogTitle lang "" - H.p ! A.style "clear: both;" $ do - H.span ! A.style "float: left;" ! A.id "cosx" $ H.b $ contactInfo iMessage - -- H.span ! A.id "cios" ! A.style "display:none;" $ H.b $ contactInfo "sms:tazjin@me.com" - H.span ! A.style "float:right;" $ preEscapedText $ rightText lang - H.div ! A.class_ "myclear" $ mempty + H.div ! A.class_ "header" $ do + H.a ! A.class_ "btitle" ! A.href (toValue $ "/" ++ show lang) $ + toHtml $ blogTitle lang "" + H.p ! A.style "clear: both;" $ do + H.span ! A.class_ "contacts" ! A.id "cosx" $ H.b $ contactInfo iMessage + -- H.span ! A.id "cios" ! A.style "display:none;" $ H.b $ contactInfo "sms:tazjin@me.com" + H.span ! A.class_ "righttext" $ preEscapedText $ rightText lang + H.div ! A.class_ "middle" $ do body - H.div ! A.class_ "myclear" $ mempty - showFooter lang $ T.pack version - H.div ! A.class_ "centerbox" $ - H.img ! A.src "http://getpunchd.com/img/june/idiots.png" ! A.alt "" + H.div ! A.class_ "footer" $ do + showFooter lang $ T.pack version + H.div ! A.class_ "centerbox" $ + H.span ! A.style "font-size: 17px; font-family: Helvetica;" $ "ಠ_ಠ" + --H.img ! A.src "http://cl.ly/F9m4/idiots.png" ! A.alt "" where contactInfo (imu :: Text) = do toHtml $ contactText lang - H.a ! A.href (toValue mailTo) $ "Mail" + H.a ! A.class_ "link" ! A.href (toValue mailTo) $ "Mail" ", " - H.a ! A.href (toValue twitter) ! A.target "_blank" $ "Twitter" + H.a ! A.class_ "link" ! A.href (toValue twitter) ! A.target "_blank" $ "Twitter" toHtml $ orText lang - H.a ! A.href (toValue imu) ! A.target "_blank" $ "iMessage" + H.a ! A.class_ "link" ! A.href (toValue imu) ! A.target "_blank" $ "iMessage" "." renderEntries :: Bool -> [Entry] -> Text -> Maybe Html -> Html -renderEntries showAll entries topText footerLinks = - H.div ! A.class_ "innerBox" $ do - H.div ! A.class_ "innerBoxTop" $ toHtml topText - H.div ! A.class_ "innerBoxMiddle" $ do - H.ul $ if' showAll - (sequence_ $ map showEntry entries) - (sequence_ . take 6 $ map showEntry entries) - getFooterLinks footerLinks +renderEntries showAll entries topText footerLinks = do + H.span ! A.class_ "innerTitle" $ toHtml topText + H.div ! A.class_ "innerContainer" $ do + H.ul $ if' showAll + (sequence_ $ map showEntry entries) + (sequence_ . take 6 $ map showEntry entries) + getFooterLinks footerLinks where showEntry :: Entry -> Html showEntry e = H.li $ do @@ -99,9 +97,10 @@ renderEntries showAll entries topText footerLinks = getFooterLinks Nothing = mempty renderEntry :: Entry -> Html -renderEntry (Entry{..}) = H.div ! A.class_ "innerBox" $ do - H.div ! A.class_ "innerBoxTop" $ toHtml $ title - H.div ! A.class_ "innerBoxMiddle" $ do +renderEntry (Entry{..}) = do + H.span ! A.class_ "innerTitle" $ toHtml $ title + H.span ! A.class_ "righttext" $ H.i $ toHtml $ woText + H.div ! A.class_ "innerContainer" $ do H.article $ H.ul $ H.li $ do preEscapedText $ btext H.p $ preEscapedText $ mtext @@ -109,18 +108,17 @@ renderEntry (Entry{..}) = H.div ! A.class_ "innerBox" $ do H.div ! A.class_ "cHead" $ toHtml $ cHead lang -- ! A.style "font-size:large;font-weight:bold;" H.ul $ renderComments comments lang renderCommentBox lang entryId + where + woText = flip T.append author $ T.pack $ (formatTime defaultTimeLocale (eTimeFormat lang) edate) renderCommentBox :: BlogLang -> EntryId -> Html renderCommentBox cLang cId = do H.div ! A.class_ "cHead" $ toHtml $ cwHead cLang H.form ! A.method "POST" ! A.action (toValue $ "/" ++ (show cLang) ++ "/postcomment/" ++ show cId) $ do - H.p $ H.label $ do - H.span $ "Name:" --toHtml ("Name:" :: String) - H.input ! A.name "cname" - H.p $ H.label $ do - H.span $ toHtml $ cSingle cLang -- toHtml (cSingle lang) - H.textarea ! A.name "ctext" ! A.cols "50" ! A.rows "13" $ mempty - H.p $ H.input ! A.type_ "submit" ! A.value (toValue $ cSend cLang) + H.p $ H.input ! A.name "cname" ! A.placeholder "Name" ! A.class_ "cInput" + H.p $ H.label $ H.textarea ! A.name "ctext" ! A.cols "50" ! A.rows "13" ! A.class_ "cInput" ! + A.placeholder (toValue $ cTextPlaceholder cLang) $ mempty + H.p $ H.input ! A.class_ "cInput" ! A.style "width: 120px;" ! A.type_ "submit" ! A.value (toValue $ cSend cLang) renderComments :: [Comment] -> BlogLang -> Html renderComments [] lang = H.li $ toHtml $ noComments lang @@ -149,14 +147,14 @@ showLinks Nothing lang = H.div ! A.class_ "centerbox" $ showFooter :: BlogLang -> Text -> Html showFooter l v = H.div ! A.class_ "rightbox" ! A.style "text-align:right;" $ do toHtml ("Proudly made with " :: Text) - H.a ! A.href "http://haskell.org" $ "Haskell" + H.a ! A.class_ "link" ! A.href "http://haskell.org" $ "Haskell" toHtml (", " :: Text) - H.a ! A.href "http://hackage.haskell.org/package/acid-state-0.6.3" $ "Acid-State" + H.a ! A.class_ "link" ! A.href "http://hackage.haskell.org/package/acid-state-0.6.3" $ "Acid-State" toHtml (" and without PHP, Java, Perl, MySQL and Python." :: Text) H.br - H.a ! A.href (toValue repoURL) $ toHtml $ T.append "Version " v + H.a ! A.class_ "link" ! A.href (toValue repoURL) $ toHtml $ T.append "Version " v preEscapedText " " - H.a ! A.href "/notice" $ toHtml $ noticeText l + H.a ! A.class_ "link" ! A.href "/notice" $ toHtml $ noticeText l showSiteNotice :: Html showSiteNotice = H.docTypeHtml $ do @@ -190,7 +188,7 @@ adminLogin = adminTemplate "Login" $ H.div ! A.class_ "loginBoxMiddle" $ H.form ! A.action "/dologin" ! A.method "post" $ do H.p $ "Account ID" H.p $ H.input ! A.type_ "text" ! A.style "font-size: 2;" - ! A.name "account" ! A.value "tazjin" ! A.readonly "1" + ! A.name "account" -- ! A.value "tazjin" ! A.readonly "1" H.p $ "Passwort" H.p $ H.input ! A.type_ "password" ! A.style "font-size: 2;" ! A.name "password" H.p $ H.input ! A.alt "Anmelden" ! A.type_ "image" ! A.src "/res/signin.gif" @@ -250,9 +248,8 @@ editPage (Entry{..}) = adminTemplate "Index" $ -- Error pages showError :: BlogError -> BlogLang -> Html -showError NotFound l = blogTemplate l (T.append ": " $ notFoundTitle l) $ - H.div ! A.class_ "innerBox" $ do - H.div ! A.class_ "innerBoxTop" $ toHtml $ notFoundTitle l - H.div ! A.class_ "innerBoxMiddle" $ do - H.p ! A.class_ "notFoundFace" $ toHtml (":'(" :: Text) - H.p ! A.class_ "notFoundText" $ toHtml $ notFoundText l +showError NotFound l = blogTemplate l (T.append ": " $ notFoundTitle l) $ do + H.span ! A.class_ "innerTitle" $ toHtml $ notFoundTitle l + H.div ! A.class_ "innerContainer" $ do + H.p ! A.class_ "notFoundFace" $ toHtml (":'(" :: Text) + H.p ! A.class_ "notFoundText" $ toHtml $ notFoundText l diff --git a/src/Locales.hs b/src/Locales.hs index 7d36b2d2b1..b73321f4f6 100644 --- a/src/Locales.hs +++ b/src/Locales.hs @@ -13,7 +13,7 @@ import BlogDB (BlogLang (..)) data BlogError = NotFound | DBError -version = "3.0" +version = "3.1" allLang = [EN, DE] @@ -76,6 +76,10 @@ readMore :: BlogLang -> Text readMore DE = "Weiterlesen" readMore EN = "Read more" +eTimeFormat :: BlogLang -> String +eTimeFormat DE = "Geschrieben am %d.%m.%y von " +eTimeFormat EN = "Written on %D by " + -- contact information contactText :: BlogLang -> Text contactText DE = "Wer mich kontaktieren will: " @@ -115,6 +119,10 @@ cSend :: BlogLang -> Text cSend DE = "Absenden" cSend EN = "Submit" +cTextPlaceholder :: BlogLang -> Text +cTextPlaceholder DE = "Kommentartext hier eingeben :]" +cTextPlaceholder EN = "Enter your comment here :]" + -- errors notFoundTitle :: BlogLang -> Text notFoundTitle DE = "Nicht gefunden" @@ -126,8 +134,8 @@ notFoundText EN = "The requested object could unfortunately not be found." -- right side text (this is inserted AS IS. Escape HTML!) rightText :: BlogLang -> Text -rightText DE = "English version available here." -rightText EN = "Deutsche Version hier verfügbar." +rightText DE = "English version available here." +rightText EN = "Deutsche Version hier verfügbar." -- static information repoURL :: Text = "https://bitbucket.org/tazjin/tazblog-haskell" diff --git a/src/Main.hs b/src/Main.hs index ce423f932d..d376f9ef09 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -146,7 +146,7 @@ postEntry acid = do nMtext <- lookText' "mtext" nEntry <- Entry <$> pure eId <*> getLang lang - <*> lookText' "author" + <*> readCookieValue "sUser" <*> lookText' "title" <*> pure (entryEscape nBtext) <*> pure (entryEscape nMtext) -- cgit 1.4.1 From ed05e92669a62b8d7b84f1ef334cbd2f44d6a776 Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Tue, 20 Mar 2012 18:01:36 +0100 Subject: * gzip compression for dynamic files --- src/Main.hs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Main.hs b/src/Main.hs index d376f9ef09..f0c28b9b5b 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -23,6 +23,7 @@ import qualified Data.Text as T import Data.Time import Data.SafeCopy (base, deriveSafeCopy) import Happstack.Server hiding (Session) +import Happstack.Server.Compression import System.Environment(getEnv) import System.Locale (defaultTimeLocale) @@ -44,7 +45,8 @@ main = do (\acid -> simpleHTTP nullConf {port = 80} $ tazBlog acid) tazBlog :: AcidState Blog -> ServerPart Response -tazBlog acid = +tazBlog acid = do + compr <- compressedResponseFilter msum [ dir (show DE) $ blogHandler acid DE , dir (show EN) $ blogHandler acid EN , do nullDir -- cgit 1.4.1 From f6f066a93e7f62846b5bac5d469f4f4c6170c351 Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Tue, 20 Mar 2012 20:16:27 +0100 Subject: * font-weight for .contacts --- res/blog.css | 2 +- res/blogstyle.css | 1 + src/Blog.hs | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/res/blog.css b/res/blog.css index 8ab3522d74..7bad2c6049 100644 --- a/res/blog.css +++ b/res/blog.css @@ -1 +1 @@ -@charset UTF-8;@font-face{font-family:'PT Sans';font-style:normal;font-weight:400;src:local('PT Sans'), local(PTSans-Regular), url(http://themes.googleusercontent.com/static/fonts/ptsans/v2/7YGmE4Ls5b94ct65u07hVQLUuEpTyoUstqEm5AMlJo4.woff) format(woff);}html,body{margin:0;padding:0;}body{padding-top:10px;font-family:'PT Sans', sans-serif;min-height:850px;background-color:#4A525A;color:#EEE;}a{color:#000;}input,textarea,select{border:1px solid #555;font-size:15px;line-height:1.2em;width:550px;background:#F9F9F9;-webkit-border-radius:.5em;padding:.5em;}.header{z-index:4;padding-left:20px;padding-bottom:70px;padding-top:30px;position:relative;box-shadow:0 6px 5px 1px #848F9A;}.link{color:#EEE;}.middle{position:relative;z-index:2;display:block;width:100%;padding-top:40px;background-color:#EEE;color:#000;}.footer{z-index:4;position:relative;background-color:#4A525A;margin-top:30px;padding-top:20px;box-shadow:0 -6px 5px 1px #848F9A;color:#EEE;}.btitle{text-decoration:none;color:#EEE;font-size:x-large;font-weight:700;margin-top:15px;margin-bottom:10px;}.contacts{float:left;}.righttext{float:right;padding-right:20px;}.rightbox{text-align:right;padding-right:14px;}.innerTitle{margin-left:10px;font-weight:700;}.innerBoxComments{margin-left:10px;}.innerContainer{padding-right:20px;}.centerbox{text-align:center;min-height:45px;}.cInput{margin-left:15px;}.tt{font-family:"courier new",courier,monospace;font-size:13px;}.cl{text-decoration:none;color:#000;}.cHead{font-size:large;font-weight:700;}.notFoundFace{text-align:center;font-size:100px;}.notFoundText{text-align:center;font-size:24px;font-weight:700;} +@charset UTF-8;@font-face{font-family:'PT Sans';font-style:normal;font-weight:400;src:local('PT Sans'), local(PTSans-Regular), url(http://themes.googleusercontent.com/static/fonts/ptsans/v2/7YGmE4Ls5b94ct65u07hVQLUuEpTyoUstqEm5AMlJo4.woff) format(woff);}html,body{margin:0;padding:0;}body{padding-top:10px;font-family:'PT Sans', sans-serif;min-height:850px;background-color:#4A525A;color:#EEE;}a{color:#000;}input,textarea,select{border:1px solid #555;font-size:15px;line-height:1.2em;width:550px;background:#F9F9F9;-webkit-border-radius:.5em;padding:.5em;}.header{z-index:4;padding-left:20px;padding-bottom:70px;padding-top:30px;position:relative;box-shadow:0 6px 5px 1px #848F9A;}.link{color:#EEE;}.middle{position:relative;z-index:2;display:block;width:100%;padding-top:40px;background-color:#EEE;color:#000;}.footer{z-index:4;position:relative;background-color:#4A525A;margin-top:30px;padding-top:20px;box-shadow:0 -6px 5px 1px #848F9A;color:#EEE;}.btitle{text-decoration:none;color:#EEE;font-size:x-large;font-weight:700;margin-top:15px;margin-bottom:10px;}.contacts{float:left;font-weight:bolder;}.righttext{float:right;padding-right:20px;}.rightbox{text-align:right;padding-right:14px;}.innerTitle{margin-left:10px;font-weight:700;}.innerBoxComments{margin-left:10px;}.innerContainer{padding-right:20px;}.centerbox{text-align:center;min-height:45px;}.cInput{margin-left:15px;}.tt{font-family:"courier new",courier,monospace;font-size:13px;}.cl{text-decoration:none;color:#000;}.cHead{font-size:large;font-weight:700;}.notFoundFace{text-align:center;font-size:100px;}.notFoundText{text-align:center;font-size:24px;font-weight:700;} diff --git a/res/blogstyle.css b/res/blogstyle.css index d58c405cda..a74c9f1d07 100644 --- a/res/blogstyle.css +++ b/res/blogstyle.css @@ -82,6 +82,7 @@ input, textarea, select { .contacts { float: left; + font-weight: bolder; } .righttext { diff --git a/src/Blog.hs b/src/Blog.hs index 33d7c37793..0e6ed51954 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -52,7 +52,7 @@ blogTemplate lang t_append body = H.docTypeHtml $ do --add body H.a ! A.class_ "btitle" ! A.href (toValue $ "/" ++ show lang) $ toHtml $ blogTitle lang "" H.p ! A.style "clear: both;" $ do - H.span ! A.class_ "contacts" ! A.id "cosx" $ H.b $ contactInfo iMessage + H.span ! A.class_ "contacts" ! A.id "cosx" $ contactInfo iMessage -- H.span ! A.id "cios" ! A.style "display:none;" $ H.b $ contactInfo "sms:tazjin@me.com" H.span ! A.class_ "righttext" $ preEscapedText $ rightText lang H.div ! A.class_ "middle" $ do -- cgit 1.4.1 From 6a8ffaf25a87905a9a6b15b26ffd1db03312d793 Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Thu, 22 Mar 2012 14:34:04 +0100 Subject: * cleaned up res/ folder * serving static/ content from res/ with 20 years expiration date * linen texture --- res/blog.css | 1 - res/blogstyle.css | 8 ++++---- res/blogv31.css | 1 + res/drama.wav | Bin 208300 -> 0 bytes res/ios-linen.jpg | Bin 0 -> 6039 bytes res/loginBoxTop.png | Bin 606 -> 0 bytes res/twtbtn.png | Bin 8058 -> 0 bytes src/Blog.hs | 6 +++--- src/Main.hs | 3 +++ 9 files changed, 11 insertions(+), 8 deletions(-) delete mode 100644 res/blog.css create mode 100644 res/blogv31.css delete mode 100644 res/drama.wav create mode 100644 res/ios-linen.jpg delete mode 100644 res/loginBoxTop.png delete mode 100644 res/twtbtn.png diff --git a/res/blog.css b/res/blog.css deleted file mode 100644 index 7bad2c6049..0000000000 --- a/res/blog.css +++ /dev/null @@ -1 +0,0 @@ -@charset UTF-8;@font-face{font-family:'PT Sans';font-style:normal;font-weight:400;src:local('PT Sans'), local(PTSans-Regular), url(http://themes.googleusercontent.com/static/fonts/ptsans/v2/7YGmE4Ls5b94ct65u07hVQLUuEpTyoUstqEm5AMlJo4.woff) format(woff);}html,body{margin:0;padding:0;}body{padding-top:10px;font-family:'PT Sans', sans-serif;min-height:850px;background-color:#4A525A;color:#EEE;}a{color:#000;}input,textarea,select{border:1px solid #555;font-size:15px;line-height:1.2em;width:550px;background:#F9F9F9;-webkit-border-radius:.5em;padding:.5em;}.header{z-index:4;padding-left:20px;padding-bottom:70px;padding-top:30px;position:relative;box-shadow:0 6px 5px 1px #848F9A;}.link{color:#EEE;}.middle{position:relative;z-index:2;display:block;width:100%;padding-top:40px;background-color:#EEE;color:#000;}.footer{z-index:4;position:relative;background-color:#4A525A;margin-top:30px;padding-top:20px;box-shadow:0 -6px 5px 1px #848F9A;color:#EEE;}.btitle{text-decoration:none;color:#EEE;font-size:x-large;font-weight:700;margin-top:15px;margin-bottom:10px;}.contacts{float:left;font-weight:bolder;}.righttext{float:right;padding-right:20px;}.rightbox{text-align:right;padding-right:14px;}.innerTitle{margin-left:10px;font-weight:700;}.innerBoxComments{margin-left:10px;}.innerContainer{padding-right:20px;}.centerbox{text-align:center;min-height:45px;}.cInput{margin-left:15px;}.tt{font-family:"courier new",courier,monospace;font-size:13px;}.cl{text-decoration:none;color:#000;}.cHead{font-size:large;font-weight:700;}.notFoundFace{text-align:center;font-size:100px;}.notFoundText{text-align:center;font-size:24px;font-weight:700;} diff --git a/res/blogstyle.css b/res/blogstyle.css index a74c9f1d07..9f5d69521e 100644 --- a/res/blogstyle.css +++ b/res/blogstyle.css @@ -14,10 +14,8 @@ html, body{ } body { - padding-top: 10px; font-family: 'PT Sans', sans-serif; min-height: 850px; - background-color: #4A525A; color: #EEE; } @@ -37,12 +35,13 @@ input, textarea, select { /* site sections */ .header { + background: url(/static/ios-linen.jpg); z-index: 4; padding-left: 20px; padding-bottom: 70px; padding-top: 30px; position: relative; - box-shadow: 0 6px 5px 1px #848F9A; + box-shadow: 0 6px 5px 1px #343537; } .link { @@ -60,12 +59,13 @@ input, textarea, select { } .footer { + background: url(/static/ios-linen.jpg); z-index: 4; position: relative; background-color: #4A525A; margin-top: 30px; padding-top: 20px; - box-shadow: 0 -6px 5px 1px #848F9A; + box-shadow: 0 -6px 5px 1px #343537; color: #EEE; } diff --git a/res/blogv31.css b/res/blogv31.css new file mode 100644 index 0000000000..15656b0881 --- /dev/null +++ b/res/blogv31.css @@ -0,0 +1 @@ +@charset UTF-8;@font-face{font-family:'PT Sans';font-style:normal;font-weight:400;src:local('PT Sans'), local(PTSans-Regular), url(http://themes.googleusercontent.com/static/fonts/ptsans/v2/7YGmE4Ls5b94ct65u07hVQLUuEpTyoUstqEm5AMlJo4.woff) format(woff);}html,body{margin:0;padding:0;}body{font-family:'PT Sans', sans-serif;min-height:850px;color:#EEE;}a{color:#000;}input,textarea,select{border:1px solid #555;font-size:15px;line-height:1.2em;width:550px;background:#F9F9F9;-webkit-border-radius:.5em;padding:.5em;}.header{background:url(/static/ios-linen.jpg);z-index:4;padding-left:20px;padding-bottom:70px;padding-top:30px;position:relative;box-shadow:0 6px 5px 1px #343537;}.link{color:#EEE;}.middle{position:relative;z-index:2;display:block;width:100%;padding-top:40px;background-color:#EEE;color:#000;}.footer{background:url(/static/ios-linen.jpg);z-index:4;position:relative;background-color:#4A525A;margin-top:30px;padding-top:20px;box-shadow:0 -6px 5px 1px #343537;color:#EEE;}.btitle{text-decoration:none;color:#EEE;font-size:x-large;font-weight:700;margin-top:15px;margin-bottom:10px;}.contacts{float:left;font-weight:bolder;}.righttext{float:right;padding-right:20px;}.rightbox{text-align:right;padding-right:14px;}.innerTitle{margin-left:10px;font-weight:700;}.innerBoxComments{margin-left:10px;}.innerContainer{padding-right:20px;}.centerbox{text-align:center;min-height:45px;}.cInput{margin-left:15px;}.tt{font-family:"courier new",courier,monospace;font-size:13px;}.cl{text-decoration:none;color:#000;}.cHead{font-size:large;font-weight:700;}.notFoundFace{text-align:center;font-size:100px;}.notFoundText{text-align:center;font-size:24px;font-weight:700;} diff --git a/res/drama.wav b/res/drama.wav deleted file mode 100644 index 20d326c5f4..0000000000 Binary files a/res/drama.wav and /dev/null differ diff --git a/res/ios-linen.jpg b/res/ios-linen.jpg new file mode 100644 index 0000000000..c03636fd1d Binary files /dev/null and b/res/ios-linen.jpg differ diff --git a/res/loginBoxTop.png b/res/loginBoxTop.png deleted file mode 100644 index 8a0ee3ba8d..0000000000 Binary files a/res/loginBoxTop.png and /dev/null differ diff --git a/res/twtbtn.png b/res/twtbtn.png deleted file mode 100644 index 3a54c73c4c..0000000000 Binary files a/res/twtbtn.png and /dev/null differ diff --git a/src/Blog.hs b/src/Blog.hs index 0e6ed51954..7e719466a0 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -43,7 +43,7 @@ blogTemplate lang t_append body = H.docTypeHtml $ do --add body H.head $ do H.title $ (toHtml $ blogTitle lang t_append) H.link ! A.rel "alternate" ! A.type_ "application/rss+xml" ! A.title "RSS-Feed" ! A.href "/rss" - H.link ! A.rel "stylesheet" ! A.type_ "text/css" ! A.href "/res/blog.css" ! A.media "all" + H.link ! A.rel "stylesheet" ! A.type_ "text/css" ! A.href "/static/blogv31.css" ! A.media "all" H.meta ! A.httpEquiv "content-type" ! A.content "text/html;charset=UTF-8" --H.style ! A.type_ "text/css" ! A.title "iOS iMessage" ! A.media "screen and (max-device-width: 1024px)" $ "#cosx{display:none;}" preEscapedText analytics @@ -175,7 +175,7 @@ showSiteNotice = H.docTypeHtml $ do adminTemplate :: Text -> Html -> Html adminTemplate title body = H.docTypeHtml $ do H.head $ do - H.link ! A.rel "stylesheet" ! A.type_ "text/css" ! A.href "/res/admin.css" ! A.media "all" + H.link ! A.rel "stylesheet" ! A.type_ "text/css" ! A.href "/static/admin.css" ! A.media "all" H.meta ! A.httpEquiv "content-type" ! A.content "text/html;charset=UTF-8" H.title $ toHtml $ T.append "TazBlog Admin: " title H.body @@ -191,7 +191,7 @@ adminLogin = adminTemplate "Login" $ ! A.name "account" -- ! A.value "tazjin" ! A.readonly "1" H.p $ "Passwort" H.p $ H.input ! A.type_ "password" ! A.style "font-size: 2;" ! A.name "password" - H.p $ H.input ! A.alt "Anmelden" ! A.type_ "image" ! A.src "/res/signin.gif" + H.p $ H.input ! A.alt "Anmelden" ! A.type_ "image" ! A.src "/static/signin.gif" adminIndex :: Text -> Html adminIndex sUser = adminTemplate "Index" $ diff --git a/src/Main.hs b/src/Main.hs index f0c28b9b5b..f1bc9114a6 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -75,6 +75,9 @@ tazBlog acid = do ok $ toResponse $ adminIndex ("tazjin" :: Text) , dir "admin" $ ok $ toResponse $ adminLogin , dir "dologin" $ processLogin acid + , do setHeaderM "cache-control" "max-age=630720000" + setHeaderM "expires" "Tue, 20 Jan 2037 04:20:42 GMT" + dir "static" $ serveDirectory DisableBrowsing [] "../res" , serveDirectory DisableBrowsing [] "../res" , notFound $ toResponse $ showError NotFound DE ] -- cgit 1.4.1 From c620521f4752531603825c02e9e125142f0596df Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Thu, 22 Mar 2012 14:43:59 +0100 Subject: * restored missing file --- res/loginBoxTop.png | Bin 0 -> 606 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 res/loginBoxTop.png diff --git a/res/loginBoxTop.png b/res/loginBoxTop.png new file mode 100644 index 0000000000..8a0ee3ba8d Binary files /dev/null and b/res/loginBoxTop.png differ -- cgit 1.4.1 From ac1a4ddcb0ab107fbe8e1901ef0d72a11ca69a9d Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Thu, 22 Mar 2012 14:46:35 +0100 Subject: * backup script --- backup.sh | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 backup.sh diff --git a/backup.sh b/backup.sh new file mode 100644 index 0000000000..bbc3167324 --- /dev/null +++ b/backup.sh @@ -0,0 +1,2 @@ +#!/bin/bash +tar cf backup.tar BlogState/ -- cgit 1.4.1 From 87924c405dd6b321f3ba79e02ef89e4b37dd3387 Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Thu, 22 Mar 2012 20:58:13 +0100 Subject: version 3.1.1: * added background texture for .middle * removed accidental
which caused W3 validation to fail New design is now complete. --HG-- rename : res/ios-linen.jpg => res/hbg.jpg --- res/bg.gif | Bin 0 -> 15015 bytes res/blogstyle.css | 6 +++--- res/blogv311.css | 1 + res/hbg.jpg | Bin 0 -> 6039 bytes res/ios-linen.jpg | Bin 6039 -> 0 bytes src/Blog.hs | 4 ++-- src/Locales.hs | 2 +- 7 files changed, 7 insertions(+), 6 deletions(-) create mode 100644 res/bg.gif create mode 100644 res/blogv311.css create mode 100644 res/hbg.jpg delete mode 100644 res/ios-linen.jpg diff --git a/res/bg.gif b/res/bg.gif new file mode 100644 index 0000000000..62c5cba41c Binary files /dev/null and b/res/bg.gif differ diff --git a/res/blogstyle.css b/res/blogstyle.css index 9f5d69521e..50caad8ea6 100644 --- a/res/blogstyle.css +++ b/res/blogstyle.css @@ -35,7 +35,7 @@ input, textarea, select { /* site sections */ .header { - background: url(/static/ios-linen.jpg); + background: url(/static/hbg.jpg); z-index: 4; padding-left: 20px; padding-bottom: 70px; @@ -54,12 +54,12 @@ input, textarea, select { display: block; width: 100%; padding-top: 40px; - background-color: #EEE; + background: url(/static/bg.gif); color: black; } .footer { - background: url(/static/ios-linen.jpg); + background: url(/static/hbg.jpg); z-index: 4; position: relative; background-color: #4A525A; diff --git a/res/blogv311.css b/res/blogv311.css new file mode 100644 index 0000000000..0a3fa957a0 --- /dev/null +++ b/res/blogv311.css @@ -0,0 +1 @@ +@charset UTF-8;@font-face{font-family:'PT Sans';font-style:normal;font-weight:400;src:local('PT Sans'), local(PTSans-Regular), url(http://themes.googleusercontent.com/static/fonts/ptsans/v2/7YGmE4Ls5b94ct65u07hVQLUuEpTyoUstqEm5AMlJo4.woff) format(woff);}html,body{margin:0;padding:0;}body{font-family:'PT Sans', sans-serif;min-height:850px;color:#EEE;}a{color:#000;}input,textarea,select{border:1px solid #555;font-size:15px;line-height:1.2em;width:550px;background:#F9F9F9;-webkit-border-radius:.5em;padding:.5em;}.header{background:url(/static/hbg.jpg);z-index:4;padding-left:20px;padding-bottom:70px;padding-top:30px;position:relative;box-shadow:0 6px 5px 1px #343537;}.link{color:#EEE;}.middle{position:relative;z-index:2;display:block;width:100%;padding-top:40px;background:url(/static/bg.gif);color:#000;}.footer{background:url(/static/hbg.jpg);z-index:4;position:relative;background-color:#4A525A;margin-top:30px;padding-top:20px;box-shadow:0 -6px 5px 1px #343537;color:#EEE;}.btitle{text-decoration:none;color:#EEE;font-size:x-large;font-weight:700;margin-top:15px;margin-bottom:10px;}.contacts{float:left;font-weight:bolder;}.righttext{float:right;padding-right:20px;}.rightbox{text-align:right;padding-right:14px;}.innerTitle{margin-left:10px;font-weight:700;}.innerBoxComments{margin-left:10px;}.innerContainer{padding-right:20px;}.centerbox{text-align:center;min-height:45px;}.cInput{margin-left:15px;}.tt{font-family:"courier new",courier,monospace;font-size:13px;}.cl{text-decoration:none;color:#000;}.cHead{font-size:large;font-weight:700;}.notFoundFace{text-align:center;font-size:100px;}.notFoundText{text-align:center;font-size:24px;font-weight:700;} diff --git a/res/hbg.jpg b/res/hbg.jpg new file mode 100644 index 0000000000..c03636fd1d Binary files /dev/null and b/res/hbg.jpg differ diff --git a/res/ios-linen.jpg b/res/ios-linen.jpg deleted file mode 100644 index c03636fd1d..0000000000 Binary files a/res/ios-linen.jpg and /dev/null differ diff --git a/src/Blog.hs b/src/Blog.hs index 7e719466a0..a9707fa07f 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -43,7 +43,7 @@ blogTemplate lang t_append body = H.docTypeHtml $ do --add body H.head $ do H.title $ (toHtml $ blogTitle lang t_append) H.link ! A.rel "alternate" ! A.type_ "application/rss+xml" ! A.title "RSS-Feed" ! A.href "/rss" - H.link ! A.rel "stylesheet" ! A.type_ "text/css" ! A.href "/static/blogv31.css" ! A.media "all" + H.link ! A.rel "stylesheet" ! A.type_ "text/css" ! A.href "/static/blogv311.css" ! A.media "all" H.meta ! A.httpEquiv "content-type" ! A.content "text/html;charset=UTF-8" --H.style ! A.type_ "text/css" ! A.title "iOS iMessage" ! A.media "screen and (max-device-width: 1024px)" $ "#cosx{display:none;}" preEscapedText analytics @@ -88,7 +88,7 @@ renderEntries showAll entries topText footerLinks = do when ( mtext e /= T.empty ) $ H.p $ entryLink e $ readMore $ lang e unless ( mtext e /= T.empty ) $ - preEscapedText "
 
" + preEscapedText "
 " entryLink :: Entry -> Text -> Html entryLink e s = H.a ! A.href (toValue $ concat $ intersperse' "/" $ linkElems e) $ toHtml (T.concat ["[", s, "]"]) diff --git a/src/Locales.hs b/src/Locales.hs index b73321f4f6..a3adaa5179 100644 --- a/src/Locales.hs +++ b/src/Locales.hs @@ -13,7 +13,7 @@ import BlogDB (BlogLang (..)) data BlogError = NotFound | DBError -version = "3.1" +version = "3.1.1" allLang = [EN, DE] -- cgit 1.4.1 From 2738271e6733a9f24b4c09110ca98a0b56689f0f Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Fri, 23 Mar 2012 03:20:36 +0100 Subject: version 3.1.2: * CSS changes for HsColour --- res/blogstyle.css | 79 +++++++++++++++++++++++++++++++++++++++++++++++++----- res/blogv31.css | 1 - res/blogv311.css | 1 - res/blogv312.css | 1 + res/cbg.jpg | Bin 0 -> 12124 bytes src/Blog.hs | 3 ++- src/Locales.hs | 2 +- 7 files changed, 76 insertions(+), 11 deletions(-) delete mode 100644 res/blogv31.css delete mode 100644 res/blogv311.css create mode 100644 res/blogv312.css create mode 100644 res/cbg.jpg diff --git a/res/blogstyle.css b/res/blogstyle.css index 50caad8ea6..5f8475613a 100644 --- a/res/blogstyle.css +++ b/res/blogstyle.css @@ -1,12 +1,7 @@ @charset "UTF-8"; /* CSS Document */ - -@font-face { - font-family: 'PT Sans'; - font-style: normal; - font-weight: normal; - src: local('PT Sans'), local('PTSans-Regular'), url('http://themes.googleusercontent.com/static/fonts/ptsans/v2/7YGmE4Ls5b94ct65u07hVQLUuEpTyoUstqEm5AMlJo4.woff') format('woff'); -} +@import url(http://fonts.googleapis.com/css?family=Droid+Sans+Mono); +@import url(http://fonts.googleapis.com/css?family=PT+Sans); html, body{ margin: 0; @@ -147,3 +142,73 @@ input, textarea, select { font-size: 24px; font-weight: bold; } + +/* HsColour style */ + +.code +{ + box-shadow: 3px 3px 5px 1px #888; + border-radius: 10px; + padding: 0.75em; + + font-size: 11pt; + width: 60em; + color: white; + line-height: 1.2em; + font-family: 'Droid Sans Mono', sans-serif; + background: black; + background-image:url('/static/cbg.jpg'); + background-repeat: no-repeat; + } + +.code pre +{ + font-family: 'Droid Sans Mono', sans-serif; +} + +kbd +{ + font-family: 'Droid Sans Mono', sans-serif; + color: #333; + font-size: 0.8em; +} + +.wide +{ + width: 90em; +} + + +code +{ + line-height: 1.5em; + border: 1px; + } + +.source-code +{ + font-size: 0.75em; + color: #666; + } + +.warning +{ + color: red; + } + + +.hs-keyglyph { color: DarkGoldenrod; } +.hs-layout { color: white;} +.hs-keyword { color: skyblue; } +.hs-comment, .hs-comment a { color: cadetblue;} +.hs-str { color: Darkorange; } +.hs-chr { color: RosyBrown;} +.hs-conid { color: GreenYellow; } +.hs-varid { color: white; } +.hs-num { color: white; } +.hs-varop { color: DarkGoldenrod; } +.hs-conop { color: DarkGoldenrod; } +.hs-sel { color: FireBrick; } +.hs-cpp { color: yellow; } +.hs-definition { color: gold; } + diff --git a/res/blogv31.css b/res/blogv31.css deleted file mode 100644 index 15656b0881..0000000000 --- a/res/blogv31.css +++ /dev/null @@ -1 +0,0 @@ -@charset UTF-8;@font-face{font-family:'PT Sans';font-style:normal;font-weight:400;src:local('PT Sans'), local(PTSans-Regular), url(http://themes.googleusercontent.com/static/fonts/ptsans/v2/7YGmE4Ls5b94ct65u07hVQLUuEpTyoUstqEm5AMlJo4.woff) format(woff);}html,body{margin:0;padding:0;}body{font-family:'PT Sans', sans-serif;min-height:850px;color:#EEE;}a{color:#000;}input,textarea,select{border:1px solid #555;font-size:15px;line-height:1.2em;width:550px;background:#F9F9F9;-webkit-border-radius:.5em;padding:.5em;}.header{background:url(/static/ios-linen.jpg);z-index:4;padding-left:20px;padding-bottom:70px;padding-top:30px;position:relative;box-shadow:0 6px 5px 1px #343537;}.link{color:#EEE;}.middle{position:relative;z-index:2;display:block;width:100%;padding-top:40px;background-color:#EEE;color:#000;}.footer{background:url(/static/ios-linen.jpg);z-index:4;position:relative;background-color:#4A525A;margin-top:30px;padding-top:20px;box-shadow:0 -6px 5px 1px #343537;color:#EEE;}.btitle{text-decoration:none;color:#EEE;font-size:x-large;font-weight:700;margin-top:15px;margin-bottom:10px;}.contacts{float:left;font-weight:bolder;}.righttext{float:right;padding-right:20px;}.rightbox{text-align:right;padding-right:14px;}.innerTitle{margin-left:10px;font-weight:700;}.innerBoxComments{margin-left:10px;}.innerContainer{padding-right:20px;}.centerbox{text-align:center;min-height:45px;}.cInput{margin-left:15px;}.tt{font-family:"courier new",courier,monospace;font-size:13px;}.cl{text-decoration:none;color:#000;}.cHead{font-size:large;font-weight:700;}.notFoundFace{text-align:center;font-size:100px;}.notFoundText{text-align:center;font-size:24px;font-weight:700;} diff --git a/res/blogv311.css b/res/blogv311.css deleted file mode 100644 index 0a3fa957a0..0000000000 --- a/res/blogv311.css +++ /dev/null @@ -1 +0,0 @@ -@charset UTF-8;@font-face{font-family:'PT Sans';font-style:normal;font-weight:400;src:local('PT Sans'), local(PTSans-Regular), url(http://themes.googleusercontent.com/static/fonts/ptsans/v2/7YGmE4Ls5b94ct65u07hVQLUuEpTyoUstqEm5AMlJo4.woff) format(woff);}html,body{margin:0;padding:0;}body{font-family:'PT Sans', sans-serif;min-height:850px;color:#EEE;}a{color:#000;}input,textarea,select{border:1px solid #555;font-size:15px;line-height:1.2em;width:550px;background:#F9F9F9;-webkit-border-radius:.5em;padding:.5em;}.header{background:url(/static/hbg.jpg);z-index:4;padding-left:20px;padding-bottom:70px;padding-top:30px;position:relative;box-shadow:0 6px 5px 1px #343537;}.link{color:#EEE;}.middle{position:relative;z-index:2;display:block;width:100%;padding-top:40px;background:url(/static/bg.gif);color:#000;}.footer{background:url(/static/hbg.jpg);z-index:4;position:relative;background-color:#4A525A;margin-top:30px;padding-top:20px;box-shadow:0 -6px 5px 1px #343537;color:#EEE;}.btitle{text-decoration:none;color:#EEE;font-size:x-large;font-weight:700;margin-top:15px;margin-bottom:10px;}.contacts{float:left;font-weight:bolder;}.righttext{float:right;padding-right:20px;}.rightbox{text-align:right;padding-right:14px;}.innerTitle{margin-left:10px;font-weight:700;}.innerBoxComments{margin-left:10px;}.innerContainer{padding-right:20px;}.centerbox{text-align:center;min-height:45px;}.cInput{margin-left:15px;}.tt{font-family:"courier new",courier,monospace;font-size:13px;}.cl{text-decoration:none;color:#000;}.cHead{font-size:large;font-weight:700;}.notFoundFace{text-align:center;font-size:100px;}.notFoundText{text-align:center;font-size:24px;font-weight:700;} diff --git a/res/blogv312.css b/res/blogv312.css new file mode 100644 index 0000000000..69c3775b2e --- /dev/null +++ b/res/blogv312.css @@ -0,0 +1 @@ +@charset UTF-8;@import url(http://fonts.googleapis.com/css?family=Droid+Sans+Mono);@import url(http://fonts.googleapis.com/css?family=PT+Sans);html,body{margin:0;padding:0;}body{font-family:'PT Sans', sans-serif;min-height:850px;color:#EEE;}a{color:#000;}input,textarea,select{border:1px solid #555;font-size:15px;line-height:1.2em;width:550px;background:#F9F9F9;-webkit-border-radius:.5em;padding:.5em;}.header{background:url(/static/hbg.jpg);z-index:4;padding-left:20px;padding-bottom:70px;padding-top:30px;position:relative;box-shadow:0 6px 5px 1px #343537;}.link{color:#EEE;}.middle{position:relative;z-index:2;display:block;width:100%;padding-top:40px;background:url(/static/bg.gif);color:#000;}.footer{background:url(/static/hbg.jpg);z-index:4;position:relative;background-color:#4A525A;margin-top:30px;padding-top:20px;box-shadow:0 -6px 5px 1px #343537;color:#EEE;}.btitle{text-decoration:none;color:#EEE;font-size:x-large;font-weight:700;margin-top:15px;margin-bottom:10px;}.contacts{float:left;font-weight:bolder;}.righttext{float:right;padding-right:20px;}.rightbox{text-align:right;padding-right:14px;}.innerTitle{margin-left:10px;font-weight:700;}.innerBoxComments{margin-left:10px;}.innerContainer{padding-right:20px;}.centerbox{text-align:center;min-height:45px;}.cInput{margin-left:15px;}.tt{font-family:"courier new",courier,monospace;font-size:13px;}.cl{text-decoration:none;color:#000;}.cHead{font-size:large;font-weight:700;}.notFoundFace{text-align:center;font-size:100px;}.notFoundText{text-align:center;font-size:24px;font-weight:700;}.code{box-shadow:3px 3px 5px 1px #888;border-radius:10px;font-size:11pt;width:60em;color:#FFF;line-height:1.2em;font-family:'Droid Sans Mono', sans-serif;background:#000;background-image:url(/static/cbg.jpg);background-repeat:no-repeat;padding:.75em;}.code pre{font-family:'Droid Sans Mono', sans-serif;}kbd{font-family:'Droid Sans Mono', sans-serif;color:#333;font-size:.8em;}.wide{width:90em;}code{line-height:1.5em;border:1px;}.source-code{font-size:.75em;color:#666;}.warning{color:red;}.hs-keyword{color:#87CEEB;}.hs-comment,.hs-comment a{color:#5F9EA0;}.hs-str{color:#FF8C00;}.hs-chr{color:#BC8F8F;}.hs-conid{color:#ADFF2F;}.hs-sel{color:#B22222;}.hs-cpp{color:#FF0;}.hs-definition{color:#FFD700;}.hs-keyglyph,.hs-varop,.hs-conop{color:#B8860B;}.hs-layout,.hs-varid,.hs-num{color:#FFF;} \ No newline at end of file diff --git a/res/cbg.jpg b/res/cbg.jpg new file mode 100644 index 0000000000..c83bb4c295 Binary files /dev/null and b/res/cbg.jpg differ diff --git a/src/Blog.hs b/src/Blog.hs index a9707fa07f..3f1fe063cc 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -43,7 +43,8 @@ blogTemplate lang t_append body = H.docTypeHtml $ do --add body H.head $ do H.title $ (toHtml $ blogTitle lang t_append) H.link ! A.rel "alternate" ! A.type_ "application/rss+xml" ! A.title "RSS-Feed" ! A.href "/rss" - H.link ! A.rel "stylesheet" ! A.type_ "text/css" ! A.href "/static/blogv311.css" ! A.media "all" + H.link ! A.rel "stylesheet" ! A.type_ "text/css" ! A.href "/static/blogv312.css" ! A.media "all" + --H.link ! A.rel "stylesheet" ! A.type_ "text/css" ! A.href "/res/blogstyle.css" ! A.media "all" H.meta ! A.httpEquiv "content-type" ! A.content "text/html;charset=UTF-8" --H.style ! A.type_ "text/css" ! A.title "iOS iMessage" ! A.media "screen and (max-device-width: 1024px)" $ "#cosx{display:none;}" preEscapedText analytics diff --git a/src/Locales.hs b/src/Locales.hs index a3adaa5179..fc072ed61c 100644 --- a/src/Locales.hs +++ b/src/Locales.hs @@ -13,7 +13,7 @@ import BlogDB (BlogLang (..)) data BlogError = NotFound | DBError -version = "3.1.1" +version = "3.1.2" allLang = [EN, DE] -- cgit 1.4.1 From a57faddd25e429714274b8c3182c8d2c6c46b251 Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Fri, 23 Mar 2012 22:52:54 +0100 Subject: * sequence_ $ map to mapM_ --- src/Blog.hs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Blog.hs b/src/Blog.hs index 3f1fe063cc..f0760f0a02 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -78,8 +78,8 @@ renderEntries showAll entries topText footerLinks = do H.span ! A.class_ "innerTitle" $ toHtml topText H.div ! A.class_ "innerContainer" $ do H.ul $ if' showAll - (sequence_ $ map showEntry entries) - (sequence_ . take 6 $ map showEntry entries) + (mapM_ showEntry entries) + (mapM_ showEntry $ take 6 entries) getFooterLinks footerLinks where showEntry :: Entry -> Html @@ -123,7 +123,7 @@ renderCommentBox cLang cId = do renderComments :: [Comment] -> BlogLang -> Html renderComments [] lang = H.li $ toHtml $ noComments lang -renderComments comments lang = sequence_ $ map showComment comments +renderComments comments lang = mapM_ showComment comments where showComment :: Comment -> Html showComment (Comment{..}) = H.li $ do @@ -222,7 +222,7 @@ adminEntryList :: [Entry] -> Html adminEntryList entries = adminTemplate "Entrylist" $ H.div ! A.style "float: center;" $ do H.table $ do - sequence_ $ map showEntryItem entries + mapM_ showEntryItem entries adminFooter where showEntryItem :: Entry -> Html -- cgit 1.4.1 From a405e185bac6673645d96defb3800b7a18ca351d Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Fri, 23 Mar 2012 22:55:59 +0100 Subject: Updated .hgignore to include the entire BlogState/ directory and *.orig files --- .hgignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.hgignore b/.hgignore index 558cd176c6..f0e6fe0d5e 100644 --- a/.hgignore +++ b/.hgignore @@ -9,3 +9,5 @@ music *.sublime* *.8 *.geany +*.orig +BlogState/ \ No newline at end of file -- cgit 1.4.1 From efbec9ff76ada0e59f8fc5c37a4c2734ccbf7ce2 Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Sat, 24 Mar 2012 00:32:38 +0100 Subject: * added RSS.hs: functions to create an RSS feed * added RSS feed handler * FromReqURI instance for BlogLang * fixed RSS-feed link --- src/Blog.hs | 3 ++- src/BlogDB.hs | 13 +++++++++++-- src/Locales.hs | 15 +++++++++++++++ src/Main.hs | 25 +++++++++++++++---------- src/RSS.hs | 42 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 85 insertions(+), 13 deletions(-) create mode 100644 src/RSS.hs diff --git a/src/Blog.hs b/src/Blog.hs index f0760f0a02..208552c10e 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -42,7 +42,7 @@ blogTemplate :: BlogLang -> Text -> Html -> Html blogTemplate lang t_append body = H.docTypeHtml $ do --add body H.head $ do H.title $ (toHtml $ blogTitle lang t_append) - H.link ! A.rel "alternate" ! A.type_ "application/rss+xml" ! A.title "RSS-Feed" ! A.href "/rss" + H.link ! A.rel "alternate" ! A.type_ "application/rss+xml" ! A.title "RSS-Feed" ! A.href (toValue feedURL) H.link ! A.rel "stylesheet" ! A.type_ "text/css" ! A.href "/static/blogv312.css" ! A.media "all" --H.link ! A.rel "stylesheet" ! A.type_ "text/css" ! A.href "/res/blogstyle.css" ! A.media "all" H.meta ! A.httpEquiv "content-type" ! A.content "text/html;charset=UTF-8" @@ -72,6 +72,7 @@ blogTemplate lang t_append body = H.docTypeHtml $ do --add body toHtml $ orText lang H.a ! A.class_ "link" ! A.href (toValue imu) ! A.target "_blank" $ "iMessage" "." + feedURL = "/" ++ show lang ++ "/rss" renderEntries :: Bool -> [Entry] -> Text -> Maybe Html -> Html renderEntries showAll entries topText footerLinks = do diff --git a/src/BlogDB.hs b/src/BlogDB.hs index 7a4f869eb7..ea574c7084 100644 --- a/src/BlogDB.hs +++ b/src/BlogDB.hs @@ -9,14 +9,16 @@ import Data.Acid import Data.Acid.Advanced import Data.Acid.Local import Data.ByteString (ByteString) +import Data.Char (toLower) import Data.Data (Data, Typeable) import Data.IxSet (Indexable(..), IxSet(..), (@=), Proxy(..), getOne, ixFun, ixSet) -import Data.List (insert) +import Data.List (insert) import Data.SafeCopy (SafeCopy, base, deriveSafeCopy) import Data.Text (Text, pack) import Data.Text.Lazy (toStrict) import Data.Time -import System.Environment(getEnv) +import Happstack.Server (FromReqURI(..)) +import System.Environment (getEnv) import qualified Crypto.Hash.SHA512 as SHA (hash) import qualified Data.ByteString.Char8 as B @@ -38,6 +40,13 @@ instance Show BlogLang where show DE = "de" show EN = "en" +instance FromReqURI BlogLang where + fromReqURI sub = + case map toLower sub of + "de" -> Just DE + "en" -> Just EN + _ -> Nothing + $(deriveSafeCopy 0 'base ''BlogLang) data Comment = Comment { diff --git a/src/Locales.hs b/src/Locales.hs index fc072ed61c..589235cea5 100644 --- a/src/Locales.hs +++ b/src/Locales.hs @@ -3,8 +3,11 @@ module Locales where import Data.Data (Data, Typeable) +import Data.Maybe (fromMaybe) import Data.Text (Text) import qualified Data.Text as T +import Network.URI + import BlogDB (BlogLang (..)) @@ -123,6 +126,18 @@ cTextPlaceholder :: BlogLang -> Text cTextPlaceholder DE = "Kommentartext hier eingeben :]" cTextPlaceholder EN = "Enter your comment here :]" +-- RSS Strings +rssTitle :: BlogLang -> String +rssTitle DE = "Tazjins Blog" +rssTitle EN = "Tazjin's Blog" + +rssDesc :: BlogLang -> String +rssDesc DE = "Feed zu Tazjins Blog" +rssDesc EN = "Feed for Tazjin's Blog" + +rssLink :: BlogLang -> URI +rssLink l = fromMaybe nullURI $ parseURI ("http://tazj.in/" ++ show l) + -- errors notFoundTitle :: BlogLang -> Text notFoundTitle DE = "Nicht gefunden" diff --git a/src/Main.hs b/src/Main.hs index f1bc9114a6..bf8b52b49b 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -30,6 +30,7 @@ import System.Locale (defaultTimeLocale) import Blog import BlogDB hiding (addComment, updateEntry) import Locales +import RSS {- Server -} @@ -47,12 +48,10 @@ main = do tazBlog :: AcidState Blog -> ServerPart Response tazBlog acid = do compr <- compressedResponseFilter - msum [ dir (show DE) $ blogHandler acid DE - , dir (show EN) $ blogHandler acid EN - , do nullDir - showIndex acid DE - , do dir " " $ nullDir - seeOther ("https://plus.google.com/115916629925754851590" :: Text) (toResponse ()) + msum [ path $ \(lang :: BlogLang) -> blogHandler acid lang + , nullDir >> showIndex acid DE + , dir " " $ nullDir >> + seeOther ("https://plus.google.com/115916629925754851590" :: Text) (toResponse ()) , path $ \(year :: Int) -> path $ \(month :: Int) -> path $ \(id_ :: String) -> formatOldLink year month id_ , dir "res" $ serveDirectory DisableBrowsing [] "../res" , dir "notice" $ ok $ toResponse showSiteNotice @@ -68,8 +67,7 @@ tazBlog acid = do entryList acid EN , do guardSession acid dirs "admin/edit" $ path $ \(eId :: Integer) -> editEntry acid eId - , do dirs "admin/updateentry" $ nullDir - updateEntry acid + , dirs "admin/updateentry" $ nullDir >> updateEntry acid , do dir "admin" $ nullDir guardSession acid ok $ toResponse $ adminIndex ("tazjin" :: Text) @@ -88,8 +86,8 @@ blogHandler acid lang = , do decodeBody tmpPolicy dir "postcomment" $ path $ \(eId :: Integer) -> addComment acid lang $ EntryId eId - , do nullDir - showIndex acid lang + , nullDir >> showIndex acid lang + , dir "rss" $ nullDir >> showRSS acid lang , notFound $ toResponse $ showError NotFound lang ] @@ -121,6 +119,13 @@ showIndex acid lang = do eDrop (Just i) = drop ((i-1) * 6) eDrop Nothing = drop 0 +showRSS :: AcidState Blog -> BlogLang -> ServerPart Response +showRSS acid lang = do + entries <- query' acid (LatestEntries lang) + feed <- liftIO $ renderFeed lang $ take 6 entries + setHeaderM "content-type" "text/xml" + ok $ toResponse feed + addComment :: AcidState Blog -> BlogLang -> EntryId -> ServerPart Response addComment acid lang eId = do now <- liftIO $ getCurrentTime >>= return diff --git a/src/RSS.hs b/src/RSS.hs new file mode 100644 index 0000000000..05ae40ece5 --- /dev/null +++ b/src/RSS.hs @@ -0,0 +1,42 @@ +{-# LANGUAGE RecordWildCards #-} + +module RSS (renderFeed) where + +import qualified Data.Text as T + +import Data.Maybe (fromMaybe) +import Data.Time (getCurrentTime, UTCTime) +import Network.URI +import Text.RSS + +import Locales +import BlogDB hiding (Title) + +createChannel :: BlogLang -> UTCTime -> [ChannelElem] +createChannel l now = [ Language $ show l + , Copyright "Vincent Ambo" + , WebMaster "tazjin@googlemail.com" + , ChannelPubDate now + ] + +createRSS :: BlogLang -> UTCTime -> [Item] -> RSS +createRSS l t i = RSS (rssTitle l) (rssLink l) (rssDesc l) (createChannel l t) i + +createItem :: Entry -> Item +createItem Entry{..} = [ Title $ T.unpack title + , Link $ makeLink lang entryId + , Description $ T.unpack btext + , PubDate edate] + +makeLink :: BlogLang -> EntryId -> URI +makeLink l i = let url = "http://tazj.in/" ++ show l ++ "/" ++ show i + in fromMaybe nullURI $ parseURI url + +createItems :: [Entry] -> [Item] +createItems = map createItem + +createFeed :: BlogLang -> [Entry] -> IO RSS +createFeed l e = getCurrentTime >>= (\t -> return $ createRSS l t $ createItems e ) + +renderFeed :: BlogLang -> [Entry] -> IO String +renderFeed l e = createFeed l e >>= (\feed -> return $ showXML $ rssToXML feed) \ No newline at end of file -- cgit 1.4.1 From 7f4761bf7c25d9c99e8ef329678ce20f4026ba75 Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Sat, 24 Mar 2012 00:34:41 +0100 Subject: * changed version tag to 3.2 --- src/Locales.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Locales.hs b/src/Locales.hs index 589235cea5..8041c4178b 100644 --- a/src/Locales.hs +++ b/src/Locales.hs @@ -16,7 +16,7 @@ import BlogDB (BlogLang (..)) data BlogError = NotFound | DBError -version = "3.1.2" +version = "3.2" allLang = [EN, DE] -- cgit 1.4.1 From 74ce7d9bf0c837b89c48579b6c97dac3ce042093 Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Sun, 25 Mar 2012 19:01:56 +0200 Subject: * small colouriser tool to run .hs files through HsColour --- tools/colouriser/LICENSE | 3 ++ tools/colouriser/colour.cabal | 64 +++++++++++++++++++++++++++++++++++++++++++ tools/colouriser/colour.hs | 21 ++++++++++++++ 3 files changed, 88 insertions(+) create mode 100644 tools/colouriser/LICENSE create mode 100644 tools/colouriser/colour.cabal create mode 100644 tools/colouriser/colour.hs diff --git a/tools/colouriser/LICENSE b/tools/colouriser/LICENSE new file mode 100644 index 0000000000..44ade87351 --- /dev/null +++ b/tools/colouriser/LICENSE @@ -0,0 +1,3 @@ +This program comes with absolutely no warranty and I can't guarantee that it's not going to explode in your face. + +In addition to this, I don't care what you do with this. \ No newline at end of file diff --git a/tools/colouriser/colour.cabal b/tools/colouriser/colour.cabal new file mode 100644 index 0000000000..c74e6f2da8 --- /dev/null +++ b/tools/colouriser/colour.cabal @@ -0,0 +1,64 @@ +-- colour.cabal auto-generated by cabal init. For additional options, +-- see +-- http://www.haskell.org/cabal/release/cabal-latest/doc/users-guide/authors.html#pkg-descr. +-- The name of the package. +Name: colour + +-- The package version. See the Haskell package versioning policy +-- (http://www.haskell.org/haskellwiki/Package_versioning_policy) for +-- standards guiding when and how versions should be incremented. +Version: 0.1 + +-- A short (one-line) description of the package. +Synopsis: Shortcut program to use HsColour + +-- A longer description of the package. +-- Description: + +-- URL for the project homepage or repository. +Homepage: http://tazj.in/ + +-- The license under which the package is released. +License: OtherLicense + +-- The file containing the license text. +License-file: LICENSE + +-- The package author(s). +Author: tazjin + +-- An email address to which users can send suggestions, bug reports, +-- and patches. +-- Maintainer: + +-- A copyright notice. +-- Copyright: + +Category: Web + +Build-type: Simple + +-- Extra files to be distributed with the package, such as examples or +-- a README. +-- Extra-source-files: + +-- Constraint on the version of Cabal needed to build this package. +Cabal-version: >=1.2 + + +Executable colour + -- .hs or .lhs file containing the Main module. + Main-is: colour.hs + + -- Packages needed in order to build this package. + Build-depends: + base, + options, + hscolour + + -- Modules not exported by this package. + -- Other-modules: + + -- Extra tools (e.g. alex, hsc2hs, ...) needed to build the source. + -- Build-tools: + \ No newline at end of file diff --git a/tools/colouriser/colour.hs b/tools/colouriser/colour.hs new file mode 100644 index 0000000000..03ae8d51f4 --- /dev/null +++ b/tools/colouriser/colour.hs @@ -0,0 +1,21 @@ +{-# LANGUAGE TemplateHaskell #-} + +import Control.Monad (unless) +import Language.Haskell.HsColour.Colourise (defaultColourPrefs) +import Language.Haskell.HsColour.CSS +import Options + +defineOptions "MainOptions" $ do + stringOption "optFile" "file" "" + "Name of the .hs file. Will be used for the HTML file as well" + +colorCode :: String -> String -> IO () +colorCode input output = do + code <- readFile input + writeFile output $ hscolour False code + +main :: IO () +main = runCommand $ \opts args -> do + let file = optFile opts + unless (file == "") $ + colorCode (file ++ ".hs") (file ++ ".html") \ No newline at end of file -- cgit 1.4.1 From 8b64d14a72b5f17cc27c22099ec912cb953988a4 Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Sun, 25 Mar 2012 19:29:38 +0200 Subject: * removed System.Environment from Main.hs * using the brand-new Options package to parse command line options (--port and --statedir) --- src/Main.hs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/Main.hs b/src/Main.hs index bf8b52b49b..06bf4d3f63 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -24,7 +24,7 @@ import Data.Time import Data.SafeCopy (base, deriveSafeCopy) import Happstack.Server hiding (Session) import Happstack.Server.Compression -import System.Environment(getEnv) +import Options import System.Locale (defaultTimeLocale) import Blog @@ -34,16 +34,23 @@ import RSS {- Server -} +defineOptions "MainOptions" $ do + stringOption "optState" "statedir" "../" + "Directory in which the /BlogState dir is located.\ + \ The default is ../ (if run from src/)" + intOption "optPort" "port" 8000 + "The port to run the web server on. Default is 8000" + tmpPolicy :: BodyPolicy tmpPolicy = (defaultBodyPolicy "./tmp/" 0 200000 1000) main :: IO() main = do putStrLn ("TazBlog " ++ version ++ " in Haskell starting") - tbDir <- getEnv "TAZBLOG" - bracket (openLocalStateFrom (tbDir ++ "/BlogState") initialBlogState) - (createCheckpointAndClose) - (\acid -> simpleHTTP nullConf {port = 80} $ tazBlog acid) + runCommand $ \opts args -> + bracket (openLocalStateFrom (optState opts ++ "BlogState") initialBlogState) + (createCheckpointAndClose) + (\acid -> simpleHTTP nullConf {port = optPort opts} $ tazBlog acid) tazBlog :: AcidState Blog -> ServerPart Response tazBlog acid = do -- cgit 1.4.1 From 658ba0c99932254883b39250fc2c8b7920d1be74 Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Sun, 25 Mar 2012 19:35:14 +0200 Subject: * updated Cabal file --- TazBlog.cabal | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/TazBlog.cabal b/TazBlog.cabal index 2559a69613..2c24fbdc2f 100644 --- a/TazBlog.cabal +++ b/TazBlog.cabal @@ -28,4 +28,7 @@ Executable tazblog ixset, safecopy, mtl, - transformers \ No newline at end of file + transformers, + network, + options, + rss \ No newline at end of file -- cgit 1.4.1 From dfc81e1282f059f30037edd50aa1444ac4962211 Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Sun, 25 Mar 2012 19:35:30 +0200 Subject: * updated version in Cabal file --- TazBlog.cabal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TazBlog.cabal b/TazBlog.cabal index 2c24fbdc2f..0a95c031d9 100644 --- a/TazBlog.cabal +++ b/TazBlog.cabal @@ -1,5 +1,5 @@ Name: TazBlog -Version: 3.0 +Version: 3.2 Synopsis: Tazjin's Blog License-file: LICENSE Author: Vincent Ambo -- cgit 1.4.1 From 4b66c192420c9872f84ce7f586569b5d786faa39 Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Sun, 25 Mar 2012 19:54:26 +0200 Subject: * updated TODO --- TODO | 1 - 1 file changed, 1 deletion(-) diff --git a/TODO b/TODO index 30448cf667..fdb963dd79 100644 --- a/TODO +++ b/TODO @@ -1,2 +1 @@ * Bootstrap: http://twitter.github.com/bootstrap/index.html -* use forM_ ( http://jaspervdj.be/blaze/tutorial.html ) \ No newline at end of file -- cgit 1.4.1 From 1e45dcf7c8ddd00e08b51dfc4f325426ab96a265 Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Sun, 25 Mar 2012 19:56:38 +0200 Subject: * update and run scripts (using privbind) --- run.sh | 2 ++ update.sh | 5 +++++ 2 files changed, 7 insertions(+) create mode 100644 run.sh create mode 100644 update.sh diff --git a/run.sh b/run.sh new file mode 100644 index 0000000000..7ac433b60b --- /dev/null +++ b/run.sh @@ -0,0 +1,2 @@ +#!/bin/bash +sudo privbind -u tazjin tazblog --port 80 --statedir $TAZBLOG \ No newline at end of file diff --git a/update.sh b/update.sh new file mode 100644 index 0000000000..a4229f941b --- /dev/null +++ b/update.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +hg pull +hg update +cabal install --reinstall \ No newline at end of file -- cgit 1.4.1 From 5b80f528c7c518c7d82e70635f265be3c0120327 Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Sun, 25 Mar 2012 20:56:19 +0200 Subject: * default rss feed link now points to an XML file --- src/Blog.hs | 2 +- src/Main.hs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Blog.hs b/src/Blog.hs index 208552c10e..631bfa013d 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -72,7 +72,7 @@ blogTemplate lang t_append body = H.docTypeHtml $ do --add body toHtml $ orText lang H.a ! A.class_ "link" ! A.href (toValue imu) ! A.target "_blank" $ "iMessage" "." - feedURL = "/" ++ show lang ++ "/rss" + feedURL = "/" ++ show lang ++ "/rss.xml" renderEntries :: Bool -> [Entry] -> Text -> Maybe Html -> Html renderEntries showAll entries topText footerLinks = do diff --git a/src/Main.hs b/src/Main.hs index 06bf4d3f63..0ad5d979c2 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -95,6 +95,7 @@ blogHandler acid lang = \(eId :: Integer) -> addComment acid lang $ EntryId eId , nullDir >> showIndex acid lang , dir "rss" $ nullDir >> showRSS acid lang + , dir "rss.xml" $ nullDir >> showRSS acid lang , notFound $ toResponse $ showError NotFound lang ] -- cgit 1.4.1 From 3e16a443e67932ffa99d33e45591f14c0e44ef5a Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Wed, 4 Apr 2012 02:20:56 +0200 Subject: version 3.3: * added reCaptcha again (got too much spam) --- res/blogstyle.css | 9 +++++++++ res/blogv312.css | 1 - res/blogv33.css | 1 + src/Blog.hs | 25 +++++++++++++++++++++++-- src/Locales.hs | 6 +++++- src/Main.hs | 32 +++++++++++++++++++++----------- 6 files changed, 59 insertions(+), 15 deletions(-) delete mode 100644 res/blogv312.css create mode 100644 res/blogv33.css diff --git a/res/blogstyle.css b/res/blogstyle.css index 5f8475613a..c2cacfd011 100644 --- a/res/blogstyle.css +++ b/res/blogstyle.css @@ -118,6 +118,15 @@ input, textarea, select { margin-left: 15px; } +.cCaptcha { + padding: 5px; + border: 1px solid #555; + -webkit-border-radius: 0.5em; + margin-left: 15px; + width: 555px; + background: #F9F9F9; +} + .tt { font-family: "courier new",courier,monospace; font-size: 13px; diff --git a/res/blogv312.css b/res/blogv312.css deleted file mode 100644 index 69c3775b2e..0000000000 --- a/res/blogv312.css +++ /dev/null @@ -1 +0,0 @@ -@charset UTF-8;@import url(http://fonts.googleapis.com/css?family=Droid+Sans+Mono);@import url(http://fonts.googleapis.com/css?family=PT+Sans);html,body{margin:0;padding:0;}body{font-family:'PT Sans', sans-serif;min-height:850px;color:#EEE;}a{color:#000;}input,textarea,select{border:1px solid #555;font-size:15px;line-height:1.2em;width:550px;background:#F9F9F9;-webkit-border-radius:.5em;padding:.5em;}.header{background:url(/static/hbg.jpg);z-index:4;padding-left:20px;padding-bottom:70px;padding-top:30px;position:relative;box-shadow:0 6px 5px 1px #343537;}.link{color:#EEE;}.middle{position:relative;z-index:2;display:block;width:100%;padding-top:40px;background:url(/static/bg.gif);color:#000;}.footer{background:url(/static/hbg.jpg);z-index:4;position:relative;background-color:#4A525A;margin-top:30px;padding-top:20px;box-shadow:0 -6px 5px 1px #343537;color:#EEE;}.btitle{text-decoration:none;color:#EEE;font-size:x-large;font-weight:700;margin-top:15px;margin-bottom:10px;}.contacts{float:left;font-weight:bolder;}.righttext{float:right;padding-right:20px;}.rightbox{text-align:right;padding-right:14px;}.innerTitle{margin-left:10px;font-weight:700;}.innerBoxComments{margin-left:10px;}.innerContainer{padding-right:20px;}.centerbox{text-align:center;min-height:45px;}.cInput{margin-left:15px;}.tt{font-family:"courier new",courier,monospace;font-size:13px;}.cl{text-decoration:none;color:#000;}.cHead{font-size:large;font-weight:700;}.notFoundFace{text-align:center;font-size:100px;}.notFoundText{text-align:center;font-size:24px;font-weight:700;}.code{box-shadow:3px 3px 5px 1px #888;border-radius:10px;font-size:11pt;width:60em;color:#FFF;line-height:1.2em;font-family:'Droid Sans Mono', sans-serif;background:#000;background-image:url(/static/cbg.jpg);background-repeat:no-repeat;padding:.75em;}.code pre{font-family:'Droid Sans Mono', sans-serif;}kbd{font-family:'Droid Sans Mono', sans-serif;color:#333;font-size:.8em;}.wide{width:90em;}code{line-height:1.5em;border:1px;}.source-code{font-size:.75em;color:#666;}.warning{color:red;}.hs-keyword{color:#87CEEB;}.hs-comment,.hs-comment a{color:#5F9EA0;}.hs-str{color:#FF8C00;}.hs-chr{color:#BC8F8F;}.hs-conid{color:#ADFF2F;}.hs-sel{color:#B22222;}.hs-cpp{color:#FF0;}.hs-definition{color:#FFD700;}.hs-keyglyph,.hs-varop,.hs-conop{color:#B8860B;}.hs-layout,.hs-varid,.hs-num{color:#FFF;} \ No newline at end of file diff --git a/res/blogv33.css b/res/blogv33.css new file mode 100644 index 0000000000..3be4840a9b --- /dev/null +++ b/res/blogv33.css @@ -0,0 +1 @@ +@charset UTF-8;@import url(http://fonts.googleapis.com/css?family=Droid+Sans+Mono);@import url(http://fonts.googleapis.com/css?family=PT+Sans);html,body{margin:0;padding:0;}body{font-family:'PT Sans', sans-serif;min-height:850px;color:#EEE;}a{color:#000;}input,textarea,select{border:1px solid #555;font-size:15px;line-height:1.2em;width:550px;background:#F9F9F9;-webkit-border-radius:.5em;padding:.5em;}.header{background:url(/static/hbg.jpg);z-index:4;padding-left:20px;padding-bottom:70px;padding-top:30px;position:relative;box-shadow:0 6px 5px 1px #343537;}.link{color:#EEE;}.middle{position:relative;z-index:2;display:block;width:100%;padding-top:40px;background:url(/static/bg.gif);color:#000;}.footer{background:url(/static/hbg.jpg);z-index:4;position:relative;background-color:#4A525A;margin-top:30px;padding-top:20px;box-shadow:0 -6px 5px 1px #343537;color:#EEE;}.btitle{text-decoration:none;color:#EEE;font-size:x-large;font-weight:700;margin-top:15px;margin-bottom:10px;}.contacts{float:left;font-weight:bolder;}.righttext{float:right;padding-right:20px;}.rightbox{text-align:right;padding-right:14px;}.innerTitle{margin-left:10px;font-weight:700;}.innerBoxComments{margin-left:10px;}.innerContainer{padding-right:20px;}.centerbox{text-align:center;min-height:45px;}.cInput{margin-left:15px;}.cCaptcha{border:1px solid #555;-webkit-border-radius:.5em;margin-left:15px;width:555px;background:#F9F9F9;padding:5px;}.tt{font-family:"courier new",courier,monospace;font-size:13px;}.cl{text-decoration:none;color:#000;}.cHead{font-size:large;font-weight:700;}.notFoundFace{text-align:center;font-size:100px;}.notFoundText{text-align:center;font-size:24px;font-weight:700;}.code{box-shadow:3px 3px 5px 1px #888;border-radius:10px;font-size:11pt;width:60em;color:#FFF;line-height:1.2em;font-family:'Droid Sans Mono', sans-serif;background:#000;background-image:url(/static/cbg.jpg);background-repeat:no-repeat;padding:.75em;}.code pre{font-family:'Droid Sans Mono', sans-serif;}kbd{font-family:'Droid Sans Mono', sans-serif;color:#333;font-size:.8em;}.wide{width:90em;}code{line-height:1.5em;border:1px;}.source-code{font-size:.75em;color:#666;}.warning{color:red;}.hs-keyword{color:#87CEEB;}.hs-comment,.hs-comment a{color:#5F9EA0;}.hs-str{color:#FF8C00;}.hs-chr{color:#BC8F8F;}.hs-conid{color:#ADFF2F;}.hs-sel{color:#B22222;}.hs-cpp{color:#FF0;}.hs-definition{color:#FFD700;}.hs-keyglyph,.hs-varop,.hs-conop{color:#B8860B;}.hs-layout,.hs-varid,.hs-num{color:#FFF;} diff --git a/src/Blog.hs b/src/Blog.hs index 631bfa013d..2d3fe305bf 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -9,12 +9,14 @@ import Data.Monoid (mempty) import Data.Text (Text) import qualified Data.Text as T import Data.Time +import Network.Captcha.ReCaptcha import System.Locale (defaultTimeLocale) -import Text.Blaze (toValue, preEscapedText) +import Text.Blaze (toValue, preEscapedText, preEscapedString) import Text.Blaze.Html5 (Html, (!), a, form, input, p, toHtml, label) import Text.Blaze.Html5.Attributes (action, enctype, href, name, size, type_, value) import qualified Text.Blaze.Html5 as H import qualified Text.Blaze.Html5.Attributes as A +import Text.XHtml.Strict (showHtmlFragment) import Locales import BlogDB @@ -26,6 +28,21 @@ intersperse' sep l = sep : intersperse sep l replace :: Eq a => a -> a -> [a] -> [a] replace x y = map (\z -> if z == x then y else z) +-- javascript and others + +captcha :: Html +captcha = H.div ! A.class_ "cCaptcha" $ + do H.script ! A.src "http://api.recaptcha.net/challenge?k=6LfQXccSAAAAAIjKm26XlFnBMAgvaKlOAjVWEEnM" ! A.type_ "text/javascript" $ "" + H.noscript $ H.iframe ! A.src "http://api.recaptcha.net/noscript?k=6LfQXccSAAAAAIjKm26XlFnBMAgvaKlOAjVWEEnM" ! A.height "300" ! + A.width "500" ! A.seamless "" $ do + H.br + H.textarea ! A.name "recaptcha_challenge_field" ! A.rows "3" ! A.cols "40" $ "" + H.input ! A.type_ "hidden" ! A.name "recaptcha_response_field" ! A.value "manual_challenge" + +captchaOptions :: BlogLang -> Html +captchaOptions lang = H.script ! A.type_ "text/javascript" $ toHtml $ + T.concat ["var RecaptchaOptions = { theme: 'clean', lang: '", showLangText lang, "'};"] + analytics :: Text analytics = T.pack $ unlines [""] +-- blog HTML + blogTemplate :: BlogLang -> Text -> Html -> Html blogTemplate lang t_append body = H.docTypeHtml $ do --add body H.head $ do H.title $ (toHtml $ blogTitle lang t_append) H.link ! A.rel "alternate" ! A.type_ "application/rss+xml" ! A.title "RSS-Feed" ! A.href (toValue feedURL) - H.link ! A.rel "stylesheet" ! A.type_ "text/css" ! A.href "/static/blogv312.css" ! A.media "all" + H.link ! A.rel "stylesheet" ! A.type_ "text/css" ! A.href "/static/blogv33.css" ! A.media "all" --H.link ! A.rel "stylesheet" ! A.type_ "text/css" ! A.href "/res/blogstyle.css" ! A.media "all" H.meta ! A.httpEquiv "content-type" ! A.content "text/html;charset=UTF-8" --H.style ! A.type_ "text/css" ! A.title "iOS iMessage" ! A.media "screen and (max-device-width: 1024px)" $ "#cosx{display:none;}" @@ -116,10 +135,12 @@ renderEntry (Entry{..}) = do renderCommentBox :: BlogLang -> EntryId -> Html renderCommentBox cLang cId = do H.div ! A.class_ "cHead" $ toHtml $ cwHead cLang + captchaOptions cLang H.form ! A.method "POST" ! A.action (toValue $ "/" ++ (show cLang) ++ "/postcomment/" ++ show cId) $ do H.p $ H.input ! A.name "cname" ! A.placeholder "Name" ! A.class_ "cInput" H.p $ H.label $ H.textarea ! A.name "ctext" ! A.cols "50" ! A.rows "13" ! A.class_ "cInput" ! A.placeholder (toValue $ cTextPlaceholder cLang) $ mempty + H.p $ H.label $ captcha H.p $ H.input ! A.class_ "cInput" ! A.style "width: 120px;" ! A.type_ "submit" ! A.value (toValue $ cSend cLang) renderComments :: [Comment] -> BlogLang -> Html diff --git a/src/Locales.hs b/src/Locales.hs index 8041c4178b..b494343036 100644 --- a/src/Locales.hs +++ b/src/Locales.hs @@ -16,7 +16,7 @@ import BlogDB (BlogLang (..)) data BlogError = NotFound | DBError -version = "3.2" +version = "3.3" allLang = [EN, DE] @@ -28,6 +28,10 @@ blogTitle :: BlogLang -> Text -> Text blogTitle DE s = T.concat ["Tazjins Blog", s] blogTitle EN s = T.concat ["Tazjin's Blog", s] +showLangText :: BlogLang -> Text +showLangText EN = "en" +showLangText DE = "de" + -- index site headline topText DE = "Aktuelle Einträge" topText EN = "Latest entries" diff --git a/src/Main.hs b/src/Main.hs index 0ad5d979c2..203d0af0af 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -24,6 +24,7 @@ import Data.Time import Data.SafeCopy (base, deriveSafeCopy) import Happstack.Server hiding (Session) import Happstack.Server.Compression +import Network.Captcha.ReCaptcha import Options import System.Locale (defaultTimeLocale) @@ -38,6 +39,8 @@ defineOptions "MainOptions" $ do stringOption "optState" "statedir" "../" "Directory in which the /BlogState dir is located.\ \ The default is ../ (if run from src/)" + stringOption "optCaptcha" "captchakey" "" + "The reCaptcha private key" intOption "optPort" "port" 8000 "The port to run the web server on. Default is 8000" @@ -50,12 +53,12 @@ main = do runCommand $ \opts args -> bracket (openLocalStateFrom (optState opts ++ "BlogState") initialBlogState) (createCheckpointAndClose) - (\acid -> simpleHTTP nullConf {port = optPort opts} $ tazBlog acid) + (\acid -> simpleHTTP nullConf {port = optPort opts} $ tazBlog acid $ optCaptcha opts) -tazBlog :: AcidState Blog -> ServerPart Response -tazBlog acid = do +tazBlog :: AcidState Blog -> String -> ServerPart Response +tazBlog acid captchakey = do compr <- compressedResponseFilter - msum [ path $ \(lang :: BlogLang) -> blogHandler acid lang + msum [ path $ \(lang :: BlogLang) -> blogHandler acid lang captchakey , nullDir >> showIndex acid DE , dir " " $ nullDir >> seeOther ("https://plus.google.com/115916629925754851590" :: Text) (toResponse ()) @@ -87,12 +90,12 @@ tazBlog acid = do , notFound $ toResponse $ showError NotFound DE ] -blogHandler :: AcidState Blog -> BlogLang -> ServerPart Response -blogHandler acid lang = +blogHandler :: AcidState Blog -> BlogLang -> String -> ServerPart Response +blogHandler acid lang captchakey = msum [ path $ \(eId :: Integer) -> showEntry acid lang $ EntryId eId , do decodeBody tmpPolicy dir "postcomment" $ path $ - \(eId :: Integer) -> addComment acid lang $ EntryId eId + \(eId :: Integer) -> addComment acid lang captchakey $ EntryId eId , nullDir >> showIndex acid lang , dir "rss" $ nullDir >> showRSS acid lang , dir "rss.xml" $ nullDir >> showRSS acid lang @@ -134,15 +137,22 @@ showRSS acid lang = do setHeaderM "content-type" "text/xml" ok $ toResponse feed -addComment :: AcidState Blog -> BlogLang -> EntryId -> ServerPart Response -addComment acid lang eId = do +addComment :: AcidState Blog -> BlogLang -> String -> EntryId -> ServerPart Response +addComment acid lang captchakey eId = do now <- liftIO $ getCurrentTime >>= return nCtext <- lookText' "ctext" nComment <- Comment <$> pure now <*> lookText' "cname" <*> pure (commentEscape nCtext) - update' acid (AddComment eId nComment) - seeOther ("/" ++ show lang ++ "/" ++ show eId) (toResponse()) + -- captcha verification + challenge <- look "recaptcha_challenge_field" + response <- look "recaptcha_response_field" + (userIp, _) <- askRq >>= return . rqPeer + validation <- liftIO $ validateCaptcha captchakey userIp challenge response + case validation of + Right _ -> update' acid (AddComment eId nComment) + >> seeOther ("/" ++ show lang ++ "/" ++ show eId) (toResponse()) + Left _ -> (liftIO $ putStrLn "Captcha failed") >> seeOther ("/" ++ show lang ++ "/" ++ show eId) (toResponse()) commentEscape :: Text -> Text commentEscape = newlineEscape . ltEscape . gtEscape . ampEscape -- cgit 1.4.1 From ad1ee4d89ef70b2b594c906984e4faf683762743 Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Wed, 4 Apr 2012 02:21:42 +0200 Subject: * updated cabal file (dependency on recaptcha package) --- TazBlog.cabal | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/TazBlog.cabal b/TazBlog.cabal index 0a95c031d9..261fb34307 100644 --- a/TazBlog.cabal +++ b/TazBlog.cabal @@ -31,4 +31,5 @@ Executable tazblog transformers, network, options, - rss \ No newline at end of file + rss, + recaptcha \ No newline at end of file -- cgit 1.4.1 From 533463511fea14385f7e35498f52dd7199e1ce54 Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Wed, 4 Apr 2012 02:50:40 +0200 Subject: * changes to cabal file --- TazBlog.cabal | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/TazBlog.cabal b/TazBlog.cabal index 261fb34307..c76c8d5a6e 100644 --- a/TazBlog.cabal +++ b/TazBlog.cabal @@ -1,5 +1,5 @@ Name: TazBlog -Version: 3.2 +Version: 3.3 Synopsis: Tazjin's Blog License-file: LICENSE Author: Vincent Ambo @@ -32,4 +32,5 @@ Executable tazblog network, options, rss, - recaptcha \ No newline at end of file + recaptcha. + xhtml \ No newline at end of file -- cgit 1.4.1 From d15a01007ee50d5b7c75a186b6c4d72fc47b45b7 Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Wed, 4 Apr 2012 04:10:26 +0200 Subject: * comment deletion (this doesn't look nice, but nobody except for me sees the admin page so I DON'T CARE :D) --- src/Blog.hs | 17 +++++++++++++++++ src/BlogDB.hs | 9 +++++++++ src/Main.hs | 15 +++++++++++++-- 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/src/Blog.hs b/src/Blog.hs index 2d3fe305bf..a63d0039eb 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -266,9 +266,26 @@ editPage (Entry{..}) = adminTemplate "Index" $ H.td $ H.textarea ! A.name "mtext" ! A.cols "100" ! A.rows "15" $ toHtml mtext H.input ! A.type_ "hidden" ! A.name "eid" ! A.value (toValue $ unEntryId entryId) H.input ! A.style "margin-left: 20px" ! A.type_ "submit" ! A.value "Absenden" + H.div ! A.class_ "editComments" $ editComments comments entryId H.p $ do preEscapedText "Startseite -- Entrylist: DE" preEscapedText " & EN -- Backup (NYI)" +editComments :: [Comment] -> EntryId -> Html +editComments clist eId = H.table $ mapM_ editComment clist + where + editComment (Comment{..}) = H.tr $ do H.td $ toHtml cauthor + H.td $ toHtml $ formatTime defaultTimeLocale "%c" cdate + H.td $ cDeleteLink cdate + cDeleteLink cdate = H.a ! A.href (toValue $ "/admin/cdelete/" ++ show eId + ++ formatTime defaultTimeLocale "/%s%Q" cdate) $ "Löschen" + +commentDeleted :: EntryId -> Html +commentDeleted eId = adminTemplate "Kommentar gelöscht" $ do + H.div $ "Der Kommentar wurde gelöscht." + H.br + H.a ! A.href (toValue $ "/de/" ++ show eId) $ "Eintrag ansehen | " + H.a ! A.href (toValue $ "/admin/edit/" ++ show eId) $ "Eintrag bearbeiten" + -- Error pages showError :: BlogError -> BlogLang -> Html showError NotFound l = blogTemplate l (T.append ": " $ notFoundTitle l) $ do diff --git a/src/BlogDB.hs b/src/BlogDB.hs index ea574c7084..611a08914a 100644 --- a/src/BlogDB.hs +++ b/src/BlogDB.hs @@ -150,6 +150,14 @@ addComment eId c = put $ b { blogEntries = IxSet.updateIx eId newEntry blogEntries } return newEntry +deleteComment :: EntryId -> UTCTime -> Update Blog Entry +deleteComment eId cDate = + do b@Blog{..} <- get + let (Just e) = getOne $ blogEntries @= eId + let newEntry = e {comments = filter (\c -> cdate c /= cDate) (comments e)} + put $ b { blogEntries = IxSet.updateIx eId newEntry blogEntries } + return newEntry + updateEntry :: Entry -> Update Blog Entry updateEntry e = do b@Blog{..} <- get @@ -210,6 +218,7 @@ hashString = B64.encode . SHA.hash . B.pack $(makeAcidic ''Blog [ 'insertEntry , 'addComment + , 'deleteComment , 'updateEntry , 'getEntry , 'latestEntries diff --git a/src/Main.hs b/src/Main.hs index 203d0af0af..656c9cfca7 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -17,6 +17,7 @@ import Data.Acid.Local import qualified Data.ByteString.Base64 as B64 (encode) import Data.ByteString.Char8 (ByteString, pack, unpack) import Data.Data (Data, Typeable) +import Data.Maybe (fromJust) import Data.Monoid (mempty) import Data.Text (Text) import qualified Data.Text as T @@ -29,7 +30,7 @@ import Options import System.Locale (defaultTimeLocale) import Blog -import BlogDB hiding (addComment, updateEntry) +import BlogDB hiding (addComment, updateEntry, deleteComment) import Locales import RSS @@ -77,7 +78,11 @@ tazBlog acid captchakey = do entryList acid EN , do guardSession acid dirs "admin/edit" $ path $ \(eId :: Integer) -> editEntry acid eId - , dirs "admin/updateentry" $ nullDir >> updateEntry acid + , do guardSession acid + dirs "admin/updateentry" $ nullDir >> updateEntry acid + , do guardSession acid + dirs "admin/cdelete" $ path $ \(eId :: Integer) -> path $ \(cId :: String) -> + deleteComment acid (EntryId eId) cId , do dir "admin" $ nullDir guardSession acid ok $ toResponse $ adminIndex ("tazjin" :: Text) @@ -223,6 +228,12 @@ updateEntry acid = do seeOther (concat $ intersperse' "/" [show $ lang entry, show eId]) (toResponse ()) +deleteComment :: AcidState Blog -> EntryId -> String -> ServerPart Response +deleteComment acid eId cId = do + nEntry <- update' acid (DeleteComment eId cDate) + ok $ toResponse $ commentDeleted eId + where + (cDate :: UTCTime) = fromJust $ parseTime defaultTimeLocale "%s%Q" cId guardSession :: AcidState Blog -> ServerPartT IO () guardSession acid = do -- cgit 1.4.1 From d1297a50b53317c381f7ff9a1146fe8a314ac0bc Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Wed, 4 Apr 2012 04:13:18 +0200 Subject: * typo in cabal file --- TazBlog.cabal | 4 ++-- src/Locales.hs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/TazBlog.cabal b/TazBlog.cabal index c76c8d5a6e..047517c1fb 100644 --- a/TazBlog.cabal +++ b/TazBlog.cabal @@ -1,5 +1,5 @@ Name: TazBlog -Version: 3.3 +Version: 3.3.1 Synopsis: Tazjin's Blog License-file: LICENSE Author: Vincent Ambo @@ -32,5 +32,5 @@ Executable tazblog network, options, rss, - recaptcha. + recaptcha, xhtml \ No newline at end of file diff --git a/src/Locales.hs b/src/Locales.hs index b494343036..e7f87afa46 100644 --- a/src/Locales.hs +++ b/src/Locales.hs @@ -16,7 +16,7 @@ import BlogDB (BlogLang (..)) data BlogError = NotFound | DBError -version = "3.3" +version = "3.3.1" allLang = [EN, DE] -- cgit 1.4.1 From bb981085a6b7c11c479d7bc9ace6219ce8f8f311 Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Fri, 6 Apr 2012 20:32:25 +0200 Subject: * removed entryEscape -> Posting pure HTML from now on (pre tag where necessary) --- src/Main.hs | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/Main.hs b/src/Main.hs index 656c9cfca7..c347250816 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -181,8 +181,8 @@ postEntry acid = do <*> getLang lang <*> readCookieValue "sUser" <*> lookText' "title" - <*> pure (entryEscape nBtext) - <*> pure (entryEscape nMtext) + <*> pure nBtext + <*> pure nMtext <*> pure now <*> pure [] -- NYI <*> pure [] @@ -195,12 +195,6 @@ postEntry acid = do getLang "de" = return DE getLang "en" = return EN -entryEscape :: Text -> Text -entryEscape = newlineEscape . newlineRNEscape - where - newlineEscape = T.replace "\n" "
" - newlineRNEscape = T.replace "\r\n" "
" - entryList :: AcidState Blog -> BlogLang -> ServerPart Response entryList acid lang = do entries <- query' acid (LatestEntries lang) @@ -213,7 +207,7 @@ editEntry acid i = do where eId = EntryId i -updateEntry :: AcidState Blog -> ServerPart Response +updateEntry :: AcidState Blog -> ServerPart Response -- TODO: Clean this up updateEntry acid = do decodeBody tmpPolicy (eId :: Integer) <- lookRead "eid" @@ -222,8 +216,8 @@ updateEntry acid = do nBtext <- lookText' "btext" nMtext <- lookText' "mtext" let nEntry = entry { title = nTitle - , btext = entryEscape nBtext - , mtext = entryEscape nMtext} + , btext = nBtext + , mtext = nMtext} update' acid (UpdateEntry nEntry) seeOther (concat $ intersperse' "/" [show $ lang entry, show eId]) (toResponse ()) -- cgit 1.4.1 From 84b6f5b417920de46eed99df1942a76e66a0e72d Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Thu, 12 Apr 2012 02:56:33 +0200 Subject: * colouring tool changes --- tools/colouriser/colour.cabal | 4 ++-- tools/colouriser/colour.hs | 11 +++++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/tools/colouriser/colour.cabal b/tools/colouriser/colour.cabal index c74e6f2da8..99a3c9cc03 100644 --- a/tools/colouriser/colour.cabal +++ b/tools/colouriser/colour.cabal @@ -7,7 +7,7 @@ Name: colour -- The package version. See the Haskell package versioning policy -- (http://www.haskell.org/haskellwiki/Package_versioning_policy) for -- standards guiding when and how versions should be incremented. -Version: 0.1 +Version: 0.2 -- A short (one-line) description of the package. Synopsis: Shortcut program to use HsColour @@ -61,4 +61,4 @@ Executable colour -- Extra tools (e.g. alex, hsc2hs, ...) needed to build the source. -- Build-tools: - \ No newline at end of file + diff --git a/tools/colouriser/colour.hs b/tools/colouriser/colour.hs index 03ae8d51f4..3e6e39ba45 100644 --- a/tools/colouriser/colour.hs +++ b/tools/colouriser/colour.hs @@ -9,13 +9,16 @@ defineOptions "MainOptions" $ do stringOption "optFile" "file" "" "Name of the .hs file. Will be used for the HTML file as well" -colorCode :: String -> String -> IO () -colorCode input output = do +colorCode :: String -> IO () +colorCode input = do code <- readFile input - writeFile output $ hscolour False code + putStr $ concat [ "

" + , hscolour False code + , "
" + ] main :: IO () main = runCommand $ \opts args -> do let file = optFile opts unless (file == "") $ - colorCode (file ++ ".hs") (file ++ ".html") \ No newline at end of file + colorCode file \ No newline at end of file -- cgit 1.4.1 From b0522ec2f5085bd74533a89f43dff29e1cd59788 Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Sun, 22 Apr 2012 21:31:41 +0200 Subject: * max-width for
    element --- src/Blog.hs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Blog.hs b/src/Blog.hs index a63d0039eb..441f6ac178 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -97,7 +97,7 @@ renderEntries :: Bool -> [Entry] -> Text -> Maybe Html -> Html renderEntries showAll entries topText footerLinks = do H.span ! A.class_ "innerTitle" $ toHtml topText H.div ! A.class_ "innerContainer" $ do - H.ul $ if' showAll + H.ul ! A.style "max-width: 1000px;" $ if' showAll (mapM_ showEntry entries) (mapM_ showEntry $ take 6 entries) getFooterLinks footerLinks @@ -122,12 +122,12 @@ renderEntry (Entry{..}) = do H.span ! A.class_ "innerTitle" $ toHtml $ title H.span ! A.class_ "righttext" $ H.i $ toHtml $ woText H.div ! A.class_ "innerContainer" $ do - H.article $ H.ul $ H.li $ do + H.article $ H.ul ! A.style "max-width: 1000px;" $ H.li $ do preEscapedText $ btext H.p $ preEscapedText $ mtext H.div ! A.class_ "innerBoxComments" $ do H.div ! A.class_ "cHead" $ toHtml $ cHead lang -- ! A.style "font-size:large;font-weight:bold;" - H.ul $ renderComments comments lang + H.ul ! A.style "max-width: 1000px;" $ renderComments comments lang renderCommentBox lang entryId where woText = flip T.append author $ T.pack $ (formatTime defaultTimeLocale (eTimeFormat lang) edate) -- cgit 1.4.1 From 04d549b817f9de60052b802b6a40520b38511b2e Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Sun, 22 Apr 2012 21:46:35 +0200 Subject: * changed
      width to 57em --- src/Blog.hs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Blog.hs b/src/Blog.hs index 441f6ac178..7f6891cf6e 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -97,7 +97,7 @@ renderEntries :: Bool -> [Entry] -> Text -> Maybe Html -> Html renderEntries showAll entries topText footerLinks = do H.span ! A.class_ "innerTitle" $ toHtml topText H.div ! A.class_ "innerContainer" $ do - H.ul ! A.style "max-width: 1000px;" $ if' showAll + H.ul ! A.style "max-width: 57em;" $ if' showAll (mapM_ showEntry entries) (mapM_ showEntry $ take 6 entries) getFooterLinks footerLinks @@ -122,12 +122,12 @@ renderEntry (Entry{..}) = do H.span ! A.class_ "innerTitle" $ toHtml $ title H.span ! A.class_ "righttext" $ H.i $ toHtml $ woText H.div ! A.class_ "innerContainer" $ do - H.article $ H.ul ! A.style "max-width: 1000px;" $ H.li $ do + H.article $ H.ul ! A.style "max-width: 57em;" $ H.li $ do preEscapedText $ btext H.p $ preEscapedText $ mtext H.div ! A.class_ "innerBoxComments" $ do H.div ! A.class_ "cHead" $ toHtml $ cHead lang -- ! A.style "font-size:large;font-weight:bold;" - H.ul ! A.style "max-width: 1000px;" $ renderComments comments lang + H.ul ! A.style "max-width: 57em;" $ renderComments comments lang renderCommentBox lang entryId where woText = flip T.append author $ T.pack $ (formatTime defaultTimeLocale (eTimeFormat lang) edate) -- cgit 1.4.1 From 04eb6244978770138b0b624de32a8304087d911e Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Sun, 22 Apr 2012 21:57:35 +0200 Subject: * removed captchas (well, commented them out) --- src/Blog.hs | 2 +- src/Main.hs | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Blog.hs b/src/Blog.hs index 7f6891cf6e..d7a53e1f42 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -140,7 +140,7 @@ renderCommentBox cLang cId = do H.p $ H.input ! A.name "cname" ! A.placeholder "Name" ! A.class_ "cInput" H.p $ H.label $ H.textarea ! A.name "ctext" ! A.cols "50" ! A.rows "13" ! A.class_ "cInput" ! A.placeholder (toValue $ cTextPlaceholder cLang) $ mempty - H.p $ H.label $ captcha + -- H.p $ H.label $ captcha H.p $ H.input ! A.class_ "cInput" ! A.style "width: 120px;" ! A.type_ "submit" ! A.value (toValue $ cSend cLang) renderComments :: [Comment] -> BlogLang -> Html diff --git a/src/Main.hs b/src/Main.hs index c347250816..4d1ec4af57 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -149,7 +149,9 @@ addComment acid lang captchakey eId = do nComment <- Comment <$> pure now <*> lookText' "cname" <*> pure (commentEscape nCtext) - -- captcha verification + update' acid (AddComment eId nComment) + >> seeOther ("/" ++ show lang ++ "/" ++ show eId) (toResponse()) + {- -- captcha verification challenge <- look "recaptcha_challenge_field" response <- look "recaptcha_response_field" (userIp, _) <- askRq >>= return . rqPeer @@ -157,7 +159,7 @@ addComment acid lang captchakey eId = do case validation of Right _ -> update' acid (AddComment eId nComment) >> seeOther ("/" ++ show lang ++ "/" ++ show eId) (toResponse()) - Left _ -> (liftIO $ putStrLn "Captcha failed") >> seeOther ("/" ++ show lang ++ "/" ++ show eId) (toResponse()) + Left _ -> (liftIO $ putStrLn "Captcha failed") >> seeOther ("/" ++ show lang ++ "/" ++ show eId) (toResponse()) -} commentEscape :: Text -> Text commentEscape = newlineEscape . ltEscape . gtEscape . ampEscape -- cgit 1.4.1 From e28c43f0186834da90c96f1218b72176c602bfa0 Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Tue, 24 Apr 2012 04:38:26 +0200 Subject: * set default optimization level to O2 --- TazBlog.cabal | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/TazBlog.cabal b/TazBlog.cabal index 047517c1fb..7397701ac8 100644 --- a/TazBlog.cabal +++ b/TazBlog.cabal @@ -12,6 +12,7 @@ cabal-version: >= 1.2 Executable tazblog hs-source-dirs: src main-is: Main.hs + ghc-options: -O2 Build-depends: base, @@ -33,4 +34,4 @@ Executable tazblog options, rss, recaptcha, - xhtml \ No newline at end of file + xhtml -- cgit 1.4.1 From 8a750bd133d3f7d57ce87ac66e691f4d1ea6714f Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Wed, 25 Apr 2012 20:22:45 +0200 Subject: * almost done moving to Hamlet from Blaze --- src/Blog.hs | 501 +++++++++++++++++++++++++++++++----------------------------- 1 file changed, 263 insertions(+), 238 deletions(-) diff --git a/src/Blog.hs b/src/Blog.hs index d7a53e1f42..538dd92124 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -1,25 +1,20 @@ -{-# LANGUAGE OverloadedStrings, ScopedTypeVariables, DeriveDataTypeable, RecordWildCards #-} +{-# LANGUAGE OverloadedStrings, ScopedTypeVariables, DeriveDataTypeable, QuasiQuotes, RecordWildCards #-} module Blog where -import Control.Monad (when, unless) -import Data.Data (Data, Typeable) -import Data.List (intersperse) -import Data.Monoid (mempty) -import Data.Text (Text) -import qualified Data.Text as T -import Data.Time -import Network.Captcha.ReCaptcha -import System.Locale (defaultTimeLocale) -import Text.Blaze (toValue, preEscapedText, preEscapedString) -import Text.Blaze.Html5 (Html, (!), a, form, input, p, toHtml, label) -import Text.Blaze.Html5.Attributes (action, enctype, href, name, size, type_, value) -import qualified Text.Blaze.Html5 as H -import qualified Text.Blaze.Html5.Attributes as A -import Text.XHtml.Strict (showHtmlFragment) +import Control.Monad (when, unless) +import Data.Data (Data, Typeable) +import Data.List (intersperse) +import Data.Monoid (mempty) +import Data.Text (Text, append, pack, empty) +import Data.Time +import Network.Captcha.ReCaptcha +import System.Locale (defaultTimeLocale) +import Text.Hamlet +import Locales +import BlogDB -import Locales -import BlogDB +import qualified Data.Text as T -- custom list functions intersperse' :: a -> [a] -> [a] @@ -28,21 +23,12 @@ intersperse' sep l = sep : intersperse sep l replace :: Eq a => a -> a -> [a] -> [a] replace x y = map (\z -> if z == x then y else z) --- javascript and others - -captcha :: Html -captcha = H.div ! A.class_ "cCaptcha" $ - do H.script ! A.src "http://api.recaptcha.net/challenge?k=6LfQXccSAAAAAIjKm26XlFnBMAgvaKlOAjVWEEnM" ! A.type_ "text/javascript" $ "" - H.noscript $ H.iframe ! A.src "http://api.recaptcha.net/noscript?k=6LfQXccSAAAAAIjKm26XlFnBMAgvaKlOAjVWEEnM" ! A.height "300" ! - A.width "500" ! A.seamless "" $ do - H.br - H.textarea ! A.name "recaptcha_challenge_field" ! A.rows "3" ! A.cols "40" $ "" - H.input ! A.type_ "hidden" ! A.name "recaptcha_response_field" ! A.value "manual_challenge" +show' :: Show a => a -> Text +show' = pack . show -captchaOptions :: BlogLang -> Html -captchaOptions lang = H.script ! A.type_ "text/javascript" $ toHtml $ - T.concat ["var RecaptchaOptions = { theme: 'clean', lang: '", showLangText lang, "'};"] +data BlogURL = BlogURL +-- javascript and others analytics :: Text analytics = T.pack $ unlines [""] -- blog HTML - blogTemplate :: BlogLang -> Text -> Html -> Html -blogTemplate lang t_append body = H.docTypeHtml $ do --add body - H.head $ do - H.title $ (toHtml $ blogTitle lang t_append) - H.link ! A.rel "alternate" ! A.type_ "application/rss+xml" ! A.title "RSS-Feed" ! A.href (toValue feedURL) - H.link ! A.rel "stylesheet" ! A.type_ "text/css" ! A.href "/static/blogv33.css" ! A.media "all" - --H.link ! A.rel "stylesheet" ! A.type_ "text/css" ! A.href "/res/blogstyle.css" ! A.media "all" - H.meta ! A.httpEquiv "content-type" ! A.content "text/html;charset=UTF-8" - --H.style ! A.type_ "text/css" ! A.title "iOS iMessage" ! A.media "screen and (max-device-width: 1024px)" $ "#cosx{display:none;}" - preEscapedText analytics - H.body $ do - H.div ! A.class_ "header" $ do - H.a ! A.class_ "btitle" ! A.href (toValue $ "/" ++ show lang) $ - toHtml $ blogTitle lang "" - H.p ! A.style "clear: both;" $ do - H.span ! A.class_ "contacts" ! A.id "cosx" $ contactInfo iMessage - -- H.span ! A.id "cios" ! A.style "display:none;" $ H.b $ contactInfo "sms:tazjin@me.com" - H.span ! A.class_ "righttext" $ preEscapedText $ rightText lang - H.div ! A.class_ "middle" $ do - body - H.div ! A.class_ "footer" $ do - showFooter lang $ T.pack version - H.div ! A.class_ "centerbox" $ - H.span ! A.style "font-size: 17px; font-family: Helvetica;" $ "ಠ_ಠ" - --H.img ! A.src "http://cl.ly/F9m4/idiots.png" ! A.alt "" - where - contactInfo (imu :: Text) = do - toHtml $ contactText lang - H.a ! A.class_ "link" ! A.href (toValue mailTo) $ "Mail" - ", " - H.a ! A.class_ "link" ! A.href (toValue twitter) ! A.target "_blank" $ "Twitter" - toHtml $ orText lang - H.a ! A.class_ "link" ! A.href (toValue imu) ! A.target "_blank" $ "iMessage" - "." - feedURL = "/" ++ show lang ++ "/rss.xml" +blogTemplate lang t_append body = [shamlet| +$doctype 5 + + #{blogTitle lang t_append} + <link rel="stylesheet" type="text/css" href="/static/blogv33.css" media="all"> + <link rel="alternate" type="application/rss+xml" title="RSS-Feed" href=#{rssUrl}> + <meta http-equiv="content-type" content="text/html;charset=UTF-8"> + #{analytics} + <body> + <div class="header"> + <a class="btitle" href=#{append "/" (show' lang)}>#{blogTitle lang empty} + <p style="clear: both;"> + <span class="contacts" id="cosx">#{contactInfo iMessage} + <span class="righttext">#{rightText lang} + <div class="middle"> + #{body} + <div class="footer"> + #{showFooter lang $ pack version} + <div class="centerbox"> + <span style="font-size:17px;font-family:Helvetica;">ಠ_ಠ +|] + where + rssUrl = T.concat ["/", show' lang, "/rss.xml"] + contactInfo imu = [shamlet| +#{contactText lang} +<a class="link" href=#{mailTo}>Mail +, # +<a class="link" href=#{twitter} target="_blank">Twitter +#{orText lang} +<a class="link" href=#{imu} target="_blank">iMessage +. +|] -renderEntries :: Bool -> [Entry] -> Text -> Maybe Html -> Html -renderEntries showAll entries topText footerLinks = do - H.span ! A.class_ "innerTitle" $ toHtml topText - H.div ! A.class_ "innerContainer" $ do - H.ul ! A.style "max-width: 57em;" $ if' showAll - (mapM_ showEntry entries) - (mapM_ showEntry $ take 6 entries) - getFooterLinks footerLinks - where - showEntry :: Entry -> Html - showEntry e = H.li $ do - entryLink e $ T.pack $ show(length $ comments e) - preEscapedText $ T.append " " $ btext e - when ( mtext e /= T.empty ) $ - H.p $ entryLink e $ readMore $ lang e - unless ( mtext e /= T.empty ) $ - preEscapedText "<br> " - entryLink :: Entry -> Text -> Html - entryLink e s = H.a ! A.href (toValue $ concat $ intersperse' "/" $ linkElems e) $ - toHtml (T.concat ["[", s, "]"]) - linkElems e = [show(lang e), show $ entryId e] - getFooterLinks (Just h) = h - getFooterLinks Nothing = mempty +showFooter :: BlogLang -> Text -> Html +showFooter l v = [shamlet| +<div class="rightbox" style="text-align:right;"> + Proudly made with # + <a class="link" href="http://haskell.org">Haskell + , # + <a class="link" href="http://hackage.haskell.org/package/acid-state-0.6.3">Acid-State + / and without PHP, Java, Perl, MySQL and Python. + <p> + <a class="link" href=#{repoURL}>#{append "Version " v} +   + <a class="link" href="/notice">#{noticeText l} +|] -renderEntry :: Entry -> Html -renderEntry (Entry{..}) = do - H.span ! A.class_ "innerTitle" $ toHtml $ title - H.span ! A.class_ "righttext" $ H.i $ toHtml $ woText - H.div ! A.class_ "innerContainer" $ do - H.article $ H.ul ! A.style "max-width: 57em;" $ H.li $ do - preEscapedText $ btext - H.p $ preEscapedText $ mtext - H.div ! A.class_ "innerBoxComments" $ do - H.div ! A.class_ "cHead" $ toHtml $ cHead lang -- ! A.style "font-size:large;font-weight:bold;" - H.ul ! A.style "max-width: 57em;" $ renderComments comments lang - renderCommentBox lang entryId +renderEntries :: Bool -> [Entry] -> Text -> Maybe Html -> Html +renderEntries showAll entries topText footerLinks = [shamlet| +<span class="innerTitle">#{topText} +<div class="innerContainer"> + <ul style="max-width:57em;"> + $forall entry <- elist + <li> + <a href=#{linkElems entry}>#{linkText $ length $ comments entry} + #{append " " $ btext entry} + $if ((/=) (mtext entry) empty) + <p><a href=#{linkElems entry}>#{readMore $ lang entry} + $else + <br>  + $maybe links <- footerLinks + #{links} +|] where - woText = flip T.append author $ T.pack $ (formatTime defaultTimeLocale (eTimeFormat lang) edate) + elist = if' showAll entries (take 6 entries) + linkElems Entry{..} = concat $ intersperse' "/" [show lang, show entryId] + linkText n = T.concat ["[", show' n, "]"] -renderCommentBox :: BlogLang -> EntryId -> Html -renderCommentBox cLang cId = do - H.div ! A.class_ "cHead" $ toHtml $ cwHead cLang - captchaOptions cLang - H.form ! A.method "POST" ! A.action (toValue $ "/" ++ (show cLang) ++ "/postcomment/" ++ show cId) $ do - H.p $ H.input ! A.name "cname" ! A.placeholder "Name" ! A.class_ "cInput" - H.p $ H.label $ H.textarea ! A.name "ctext" ! A.cols "50" ! A.rows "13" ! A.class_ "cInput" ! - A.placeholder (toValue $ cTextPlaceholder cLang) $ mempty - -- H.p $ H.label $ captcha - H.p $ H.input ! A.class_ "cInput" ! A.style "width: 120px;" ! A.type_ "submit" ! A.value (toValue $ cSend cLang) +showLinks :: Maybe Int -> BlogLang -> Html +showLinks (Just i) lang = [shamlet| + $if ((>) i 1) + <div class="centerbox"> + <a href=#{nLink $ succ i}>#{backText lang} + / -- # + <a href=#{nLink $ pred i}>#{nextText lang} + $elseif ((<=) i 1) + #{showLinks Nothing lang} +|] + where + nLink page = T.concat ["/", show' lang, "/?page=", show' page] +showLinks Nothing lang = [shamlet| +<div class="centerbox"> + <a href=#{nLink}>#{backText lang} +|] + where + nLink = T.concat ["/", show' lang, "/?page=2"] -renderComments :: [Comment] -> BlogLang -> Html -renderComments [] lang = H.li $ toHtml $ noComments lang -renderComments comments lang = mapM_ showComment comments - where - showComment :: Comment -> Html - showComment (Comment{..}) = H.li $ do - H.i $ toHtml $ T.append cauthor ": " - preEscapedText ctext - H.p ! A.class_ "tt" $ toHtml $ timeString cdate - timeString t = formatTime defaultTimeLocale (cTimeFormat lang) t +renderEntry :: Entry -> Html +renderEntry Entry{..} = [shamlet| +<span class="innerTitle">#{title} +<span class="righttext"><i>#{woText} +<div class="innerContainer"> + <article> + <ul style="max-width:57em;"> + <li> + #{btext} + <p>#{mtext} + <div class="innerBoxComments"> + <div class="cHead">#{cHead lang} + <ul style="max-width:57em;">#{renderComments comments lang} + #{renderCommentBox lang entryId} +|] + where + woText = flip T.append author $ T.pack $ (formatTime defaultTimeLocale (eTimeFormat lang) edate) -showLinks :: Maybe Int -> BlogLang -> Html -showLinks (Just i) lang - | ( i > 1) = H.div ! A.class_ "centerbox" $ do - H.a ! A.href (toValue $ "/" ++ show lang ++ "/?page=" ++ show (i+1)) $ - toHtml $ backText lang - toHtml (" -- " :: Text) - H.a ! A.href (toValue $ "/" ++ show lang ++ "/?page=" ++ show (i-1)) $ - toHtml $ nextText lang - | ( i <= 1 ) = showLinks Nothing lang -showLinks Nothing lang = H.div ! A.class_ "centerbox" $ - H.a ! A.href (toValue $ "/" ++ show lang ++ "/?page=2") $ - toHtml $ backText lang +renderComments :: [Comment] -> BlogLang -> Html +renderComments [] lang = [shamlet|<li>#{noComments lang}|] +renderComments comments lang = [shamlet| +$forall comment <- comments + <li> + <i>#{append (cauthor comment) ": "} + #{ctext comment} + <p class="tt">#{timeString $ cdate comment} +|] + where + timeString = formatTime defaultTimeLocale (cTimeFormat lang) -showFooter :: BlogLang -> Text -> Html -showFooter l v = H.div ! A.class_ "rightbox" ! A.style "text-align:right;" $ do - toHtml ("Proudly made with " :: Text) - H.a ! A.class_ "link" ! A.href "http://haskell.org" $ "Haskell" - toHtml (", " :: Text) - H.a ! A.class_ "link" ! A.href "http://hackage.haskell.org/package/acid-state-0.6.3" $ "Acid-State" - toHtml (" and without PHP, Java, Perl, MySQL and Python." :: Text) - H.br - H.a ! A.class_ "link" ! A.href (toValue repoURL) $ toHtml $ T.append "Version " v - preEscapedText " " - H.a ! A.class_ "link" ! A.href "/notice" $ toHtml $ noticeText l + +renderCommentBox :: BlogLang -> EntryId -> Html +renderCommentBox cLang cId = [shamlet| +<div class="cHead">#{cwHead cLang} +<form method="POST" action=#{aLink}> + <p><input name="cname" placeholder="Name" class="cInput"> + <p> + <label> + <textarea name="ctext" cols="50" rows="13" class="cInput" placeholder=#{cTextPlaceholder cLang}> + <p><input class="cInput" style="width:120px;" type="submit" value=#{cSend cLang}> +|] + where + aLink = T.concat ["/", show' cLang, "/postcomment", show' cId] showSiteNotice :: Html -showSiteNotice = H.docTypeHtml $ do - H.title $ "Impressum" - H.h2 $ preEscapedText "Impressum und <a alt=\"Verantwortlich im Sinne des Presserechtes\">ViSdP</a>" - H.i $ "[German law demands this]" - H.br - H.p $ do - toHtml ("Vincent Ambo" :: Text) - H.br - toHtml ("Benfleetstr. 8" :: Text) - H.br - toHtml ("50858 Köln" :: Text) - H.p $ H.a ! A.href "/" ! A.style "color:black" $ "Back" +showSiteNotice = [shamlet| +$doctype 5 +<head> + <title>Impressum +<body> + <h2> + Impressum und # + <a alt="Verantwortlich im Sinne des Presserechtes">ViSdP + <i>[German law demands this] + <br> + <p> + Vincent Ambo + <br> + Benfleetstr. 8 + <br> + 50858 Köln + <p><a href="/" style="color:black;">Back +|] {- Administration pages -} adminTemplate :: Text -> Html -> Html -adminTemplate title body = H.docTypeHtml $ do - H.head $ do - H.link ! A.rel "stylesheet" ! A.type_ "text/css" ! A.href "/static/admin.css" ! A.media "all" - H.meta ! A.httpEquiv "content-type" ! A.content "text/html;charset=UTF-8" - H.title $ toHtml $ T.append "TazBlog Admin: " title - H.body - body +adminTemplate title body = [shamlet| +$doctype 5 +<head> + <link rel="stylesheet" type="text/css" href="/static/admin.css" media="all"> + <meta http-equiv="content-type" content="text/html;charset=UTF-8"> + <title>#{append "TazBlog Admin: " title} +<body> + #{body} +|] adminLogin :: Html -adminLogin = adminTemplate "Login" $ - H.div ! A.class_ "loginBox" $ do - H.div ! A.class_ "loginBoxTop" $ "TazBlog Admin: Login" - H.div ! A.class_ "loginBoxMiddle" $ H.form ! A.action "/dologin" ! A.method "post" $ do - H.p $ "Account ID" - H.p $ H.input ! A.type_ "text" ! A.style "font-size: 2;" - ! A.name "account" -- ! A.value "tazjin" ! A.readonly "1" - H.p $ "Passwort" - H.p $ H.input ! A.type_ "password" ! A.style "font-size: 2;" ! A.name "password" - H.p $ H.input ! A.alt "Anmelden" ! A.type_ "image" ! A.src "/static/signin.gif" +adminLogin = adminTemplate "Login" $ [shamlet| +<div class="loginBox"> + <div class="loginBoxTop">TazBlog Admin: Login + <div class="loginBoxMiddle"> + <form action="/dologin" method="POST"> + <p>Account ID + <p><input type="text" style="font-size:2;" name="account" value="tazjin" readonly="1"> + <p>Passwort + <p><input type="password" style="font-size:2;" name="password"> + <p><input alt="Anmelden" type="image" src="/static/signin.gif"> +|] adminIndex :: Text -> Html -adminIndex sUser = adminTemplate "Index" $ - H.div ! A.style "float: center;" $ - H.form ! A.action "/admin/postentry" ! A.method "POST" $ do - H.table $ do - H.tr $ do H.td $ "Titel:" - H.td $ H.input ! A.type_ "text" ! A.name "title" - H.tr $ do H.td $ "Sprache:" - H.td $ H.select ! A.name "lang" $ do - H.option ! A.value "de" $ "Deutsch" - H.option ! A.value "en" $ "Englisch" - H.tr $ do H.td ! A.style "vertical-align: top;" $ "Text:" - H.td $ H.textarea ! A.name "btext" ! A.cols "100" ! A.rows "15" $ mempty - H.tr $ do H.td ! A.style "vertical-align: top;" $ "Mehr Text:" - H.td $ H.textarea ! A.name "mtext" ! A.cols "100" ! A.rows "15" $ mempty - H.input ! A.type_ "hidden" ! A.name "author" ! A.value (toValue sUser) - H.input ! A.style "margin-left: 20px" ! A.type_ "submit" ! A.value "Absenden" - adminFooter +adminIndex sUser = adminTemplate "Index" $ [shamlet| +<div style="float:center;"> + <form action="/admin/postentry" method="POST"> + <table> + <tr> + <thead><td>Titel: + <td><input type="text" name="title"> + <tr> + <thead><td>Sprache: + <td><select name="lang"> + <option value="de">Deutsch + <option value="en">Englisch + <tr> + <thead><td>Text: + <td><textarea name="btext" cols="100" rows="15"> + <tr> + <thead><td style="vertical-align:top;">Mehr Text: + <td><textarea name="mtext" cols="100" rows="15"> + <input type="hidden" name="author" value=#{sUser}> + <input style="margin-left:20px;" type="submit" value="Absenden"> + #{adminFooter} +|] adminFooter :: Html -adminFooter = H.p $ do - preEscapedText "<a href=/>Startseite</a> -- Entrylist: <a href=/admin/entrylist/de>DE</a>" - preEscapedText " & <a href=/admin/entrylist/en>EN</a> -- <a href=#>Backup</a> (NYI)" +adminFooter = [shamlet| +<a href="/">Startseite +/ -- Entrylist: # +<a href="/admin/entrylist/de">DE +/ & # +<a href="/admin/entrylist/en">EN +/ -- # +<a href="#">Backup +/ (NYI) +|] adminEntryList :: [Entry] -> Html -adminEntryList entries = adminTemplate "Entrylist" $ - H.div ! A.style "float: center;" $ do - H.table $ do - mapM_ showEntryItem entries - adminFooter - where - showEntryItem :: Entry -> Html - showEntryItem (Entry{..}) = H.tr $ do - H.td $ H.a ! A.href (toValue $ "/admin/edit/" ++ show entryId) $ toHtml title - H.td $ toHtml $ formatTime defaultTimeLocale "[On %D at %H:%M]" edate - +adminEntryList entries = adminTemplate "EntryList" $ [shamlet| +<div style="float: center;"> + <table> + $forall entry <- entries + <tr> + <td><a href=#{append "/admin/edit" (show' $ entryId entry)}>#{title entry} + <td>#{formatPostDate $ edate entry} +|] + where + formatPostDate = formatTime defaultTimeLocale "[On %D at %H:%M]" editPage :: Entry -> Html -editPage (Entry{..}) = adminTemplate "Index" $ - H.div ! A.style "float: center;" $ - H.form ! A.action "/admin/updateentry" ! A.method "POST" $ do - H.table $ do - H.tr $ do H.td $ "Titel:" - H.td $ H.input ! A.type_ "text" ! A.name "title" ! A.value (toValue title) - H.tr $ do H.td ! A.style "vertical-align: top;" $ "Text:" - H.td $ H.textarea ! A.name "btext" ! A.cols "100" ! A.rows "15" $ toHtml btext - H.tr $ do H.td ! A.style "vertical-align: top;" $ "Mehr Text:" - H.td $ H.textarea ! A.name "mtext" ! A.cols "100" ! A.rows "15" $ toHtml mtext - H.input ! A.type_ "hidden" ! A.name "eid" ! A.value (toValue $ unEntryId entryId) - H.input ! A.style "margin-left: 20px" ! A.type_ "submit" ! A.value "Absenden" - H.div ! A.class_ "editComments" $ editComments comments entryId - H.p $ do preEscapedText "<a href=/>Startseite</a> -- Entrylist: <a href=/admin/entrylist/de>DE</a>" - preEscapedText " & <a href=/admin/entrylist/en>EN</a> -- <a href=#>Backup</a> (NYI)" +editPage (Entry{..}) = adminTemplate "Index" $ [shamlet| +<div style="float:center;"> + <form action="/admin/updateentry" method="POST"> + <table> + <tr> + <td>Titel: + <td><input type="text" name="title" value=#{title}> + <tr> + <td style="vertical-align:top;">Text: + <td><textarea name="btext" cols="100" rows="15">#{btext} + <tr> + <td style="vertical-align:top;">Mehr Text: + <td><textarea name="mtext" cols="100" rows="15">#{mtext} + <input type="hidden" name="eid" value=#{unEntryId entryId}> + <input type="submit" style="margin-left:20px;" value="Absenden"> + <div class="editComments">#{editComments comments entryId} + <p>#{adminFooter} +|] editComments :: [Comment] -> EntryId -> Html -editComments clist eId = H.table $ mapM_ editComment clist - where - editComment (Comment{..}) = H.tr $ do H.td $ toHtml cauthor - H.td $ toHtml $ formatTime defaultTimeLocale "%c" cdate - H.td $ cDeleteLink cdate - cDeleteLink cdate = H.a ! A.href (toValue $ "/admin/cdelete/" ++ show eId - ++ formatTime defaultTimeLocale "/%s%Q" cdate) $ "Löschen" +editComments comments eId = [shamlet| +<table> + $forall c <- comments + <tr> + <td>#{cauthor c} + <td>#{cPostTime $ cdate c} + <tr> + <td><a href=#{cDeleteLink $ cdate c}>Löschen +|] + where + cPostTime = formatTime defaultTimeLocale "%c" + cDeleteLink cd = concat ["/admin/cdelete", show eId, formatTime defaultTimeLocale "/%s%Q" cd] commentDeleted :: EntryId -> Html -commentDeleted eId = adminTemplate "Kommentar gelöscht" $ do - H.div $ "Der Kommentar wurde gelöscht." - H.br - H.a ! A.href (toValue $ "/de/" ++ show eId) $ "Eintrag ansehen | " - H.a ! A.href (toValue $ "/admin/edit/" ++ show eId) $ "Eintrag bearbeiten" +commentDeleted eId = adminTemplate "Kommentar gelöscht" $ [shamlet| +<div>Der Kommentar wurde gelöscht. +<br> +<a href=#{append "/de/" $ show' eId}>Eintrag ansehen | # +<a href=#{append "/admin/edit/" $ show' eId}>Eintrag bearbeiten +|] --- Error pages showError :: BlogError -> BlogLang -> Html -showError NotFound l = blogTemplate l (T.append ": " $ notFoundTitle l) $ do - H.span ! A.class_ "innerTitle" $ toHtml $ notFoundTitle l - H.div ! A.class_ "innerContainer" $ do - H.p ! A.class_ "notFoundFace" $ toHtml (":'(" :: Text) - H.p ! A.class_ "notFoundText" $ toHtml $ notFoundText l +showError NotFound l = blogTemplate l (T.append ": " $ notFoundTitle l) $ [shamlet| +<span class="innerTitle">#{notFoundTitle l} +<div class="innerTitle"> + <p class="notFoundFace">:( + <p class="notFoundText">#{notFoundText l} +|] + -- cgit 1.4.1 From 520df7957c9af74a874a39f02a9dd8bd3255eeaf Mon Sep 17 00:00:00 2001 From: Vincent Ambo <v.ambo@me.com> Date: Wed, 25 Apr 2012 23:45:47 +0200 Subject: * completed switch to Hamlet and Lucius --- src/Blog.hs | 58 +++++++++++++++++++++++++--------------------------------- src/Main.hs | 4 ++++ 2 files changed, 29 insertions(+), 33 deletions(-) diff --git a/src/Blog.hs b/src/Blog.hs index 538dd92124..51c8b086d9 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -1,4 +1,4 @@ -{-# LANGUAGE OverloadedStrings, ScopedTypeVariables, DeriveDataTypeable, QuasiQuotes, RecordWildCards #-} +{-# LANGUAGE OverloadedStrings, ScopedTypeVariables, DeriveDataTypeable, TemplateHaskell, QuasiQuotes, RecordWildCards #-} module Blog where @@ -10,7 +10,9 @@ import Data.Text (Text, append, pack, empty) import Data.Time import Network.Captcha.ReCaptcha import System.Locale (defaultTimeLocale) +import Text.Blaze (preEscapedText) import Text.Hamlet +import Text.Lucius import Locales import BlogDB @@ -28,39 +30,28 @@ show' = pack . show data BlogURL = BlogURL --- javascript and others -analytics :: Text -analytics = T.pack $ unlines ["<script type=\"text/javascript\">" - ," var _gaq = _gaq || [];" - ," _gaq.push(['_setAccount', 'UA-26042394-1']);" - ," _gaq.push(['_trackPageview']);" - ," (function() {" - ," var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;" - ," ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';" - ," var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);" - ," })();" - ,"</script>"] - +-- blog CSS (admin is still static) +stylesheetSource = $(luciusFile "../res/blogstyle.lucius") +blogStyle = renderCssUrl undefined stylesheetSource -- blog HTML blogTemplate :: BlogLang -> Text -> Html -> Html blogTemplate lang t_append body = [shamlet| $doctype 5 <head> <title>#{blogTitle lang t_append} - <link rel="stylesheet" type="text/css" href="/static/blogv33.css" media="all"> + <link rel="stylesheet" type="text/css" href="/static/blogv34.css" media="all"> <link rel="alternate" type="application/rss+xml" title="RSS-Feed" href=#{rssUrl}> <meta http-equiv="content-type" content="text/html;charset=UTF-8"> - #{analytics} <body> <div class="header"> - <a class="btitle" href=#{append "/" (show' lang)}>#{blogTitle lang empty} - <p style="clear: both;"> - <span class="contacts" id="cosx">#{contactInfo iMessage} - <span class="righttext">#{rightText lang} + <a class="btitle" href=#{append "/" (show' lang)}>#{blogTitle lang empty} + <p style="clear: both;"> + <span class="contacts" id="cosx">^{contactInfo iMessage} + <span class="righttext">^{preEscapedText $ rightText lang} <div class="middle"> - #{body} + ^{body} <div class="footer"> - #{showFooter lang $ pack version} + ^{showFooter lang $ pack version} <div class="centerbox"> <span style="font-size:17px;font-family:Helvetica;">ಠ_ಠ |] @@ -98,13 +89,13 @@ renderEntries showAll entries topText footerLinks = [shamlet| $forall entry <- elist <li> <a href=#{linkElems entry}>#{linkText $ length $ comments entry} - #{append " " $ btext entry} + ^{preEscapedText $ append " " $ btext entry} $if ((/=) (mtext entry) empty) <p><a href=#{linkElems entry}>#{readMore $ lang entry} $else <br>  $maybe links <- footerLinks - #{links} + ^{links} |] where elist = if' showAll entries (take 6 entries) @@ -119,7 +110,7 @@ showLinks (Just i) lang = [shamlet| / -- # <a href=#{nLink $ pred i}>#{nextText lang} $elseif ((<=) i 1) - #{showLinks Nothing lang} + ^{showLinks Nothing lang} |] where nLink page = T.concat ["/", show' lang, "/?page=", show' page] @@ -133,17 +124,18 @@ showLinks Nothing lang = [shamlet| renderEntry :: Entry -> Html renderEntry Entry{..} = [shamlet| <span class="innerTitle">#{title} -<span class="righttext"><i>#{woText} +<span class="righttext"> + <i>#{woText} <div class="innerContainer"> <article> <ul style="max-width:57em;"> <li> - #{btext} - <p>#{mtext} + ^{preEscapedText $ btext} + <p>^{preEscapedText $ mtext} <div class="innerBoxComments"> <div class="cHead">#{cHead lang} <ul style="max-width:57em;">#{renderComments comments lang} - #{renderCommentBox lang entryId} + ^{renderCommentBox lang entryId} |] where woText = flip T.append author $ T.pack $ (formatTime defaultTimeLocale (eTimeFormat lang) edate) @@ -154,7 +146,7 @@ renderComments comments lang = [shamlet| $forall comment <- comments <li> <i>#{append (cauthor comment) ": "} - #{ctext comment} + ^{preEscapedText $ ctext comment} <p class="tt">#{timeString $ cdate comment} |] where @@ -204,7 +196,7 @@ $doctype 5 <meta http-equiv="content-type" content="text/html;charset=UTF-8"> <title>#{append "TazBlog Admin: " title} <body> - #{body} + ^{body} |] adminLogin :: Html @@ -241,7 +233,7 @@ adminIndex sUser = adminTemplate "Index" $ [shamlet| <td><textarea name="mtext" cols="100" rows="15"> <input type="hidden" name="author" value=#{sUser}> <input style="margin-left:20px;" type="submit" value="Absenden"> - #{adminFooter} + ^{adminFooter} |] adminFooter :: Html @@ -285,7 +277,7 @@ editPage (Entry{..}) = adminTemplate "Index" $ [shamlet| <input type="hidden" name="eid" value=#{unEntryId entryId}> <input type="submit" style="margin-left:20px;" value="Absenden"> <div class="editComments">#{editComments comments entryId} - <p>#{adminFooter} + <p>^{adminFooter} |] editComments :: [Comment] -> EntryId -> Html diff --git a/src/Main.hs b/src/Main.hs index 4d1ec4af57..3efcf5be34 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -88,6 +88,10 @@ tazBlog acid captchakey = do ok $ toResponse $ adminIndex ("tazjin" :: Text) , dir "admin" $ ok $ toResponse $ adminLogin , dir "dologin" $ processLogin acid + , do dirs "static/blogv34.css" $ nullDir + setHeaderM "content-type" "text/css" + neverExpires + ok $ toResponse $ blogStyle , do setHeaderM "cache-control" "max-age=630720000" setHeaderM "expires" "Tue, 20 Jan 2037 04:20:42 GMT" dir "static" $ serveDirectory DisableBrowsing [] "../res" -- cgit 1.4.1 From 791d820bd4c611304d415d593342a3171c7e8aec Mon Sep 17 00:00:00 2001 From: Vincent Ambo <v.ambo@me.com> Date: Wed, 25 Apr 2012 23:57:15 +0200 Subject: * upped version to 3.4 --- TazBlog.cabal | 2 +- src/Locales.hs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/TazBlog.cabal b/TazBlog.cabal index 7397701ac8..06378b6f14 100644 --- a/TazBlog.cabal +++ b/TazBlog.cabal @@ -1,5 +1,5 @@ Name: TazBlog -Version: 3.3.1 +Version: 3.4 Synopsis: Tazjin's Blog License-file: LICENSE Author: Vincent Ambo diff --git a/src/Locales.hs b/src/Locales.hs index e7f87afa46..8e2149fa74 100644 --- a/src/Locales.hs +++ b/src/Locales.hs @@ -16,7 +16,7 @@ import BlogDB (BlogLang (..)) data BlogError = NotFound | DBError -version = "3.3.1" +version = "3.4" allLang = [EN, DE] -- cgit 1.4.1 From 03fc5b4e6e6215a954311d3986feb4674ba052e4 Mon Sep 17 00:00:00 2001 From: Vincent Ambo <v.ambo@me.com> Date: Thu, 26 Apr 2012 00:23:35 +0200 Subject: * added lucius file --HG-- rename : res/blogstyle.css => res/blogstyle.lucius --- res/blogstyle.css | 223 --------------------------------------------------- res/blogstyle.lucius | 223 +++++++++++++++++++++++++++++++++++++++++++++++++++ res/blogv33.css | 1 - 3 files changed, 223 insertions(+), 224 deletions(-) delete mode 100644 res/blogstyle.css create mode 100644 res/blogstyle.lucius delete mode 100644 res/blogv33.css diff --git a/res/blogstyle.css b/res/blogstyle.css deleted file mode 100644 index c2cacfd011..0000000000 --- a/res/blogstyle.css +++ /dev/null @@ -1,223 +0,0 @@ -@charset "UTF-8"; -/* CSS Document */ -@import url(http://fonts.googleapis.com/css?family=Droid+Sans+Mono); -@import url(http://fonts.googleapis.com/css?family=PT+Sans); - -html, body{ - margin: 0; - padding: 0; -} - -body { - font-family: 'PT Sans', sans-serif; - min-height: 850px; - color: #EEE; -} - -a { - color: black; -} - -input, textarea, select { - border: 1px solid #555; - padding: 0.5em; - font-size: 15px; - line-height: 1.2em; - width: 550px; - background: #F9F9F9; - -webkit-border-radius: 0.5em; - } - -/* site sections */ -.header { - background: url(/static/hbg.jpg); - z-index: 4; - padding-left: 20px; - padding-bottom: 70px; - padding-top: 30px; - position: relative; - box-shadow: 0 6px 5px 1px #343537; -} - -.link { - color: #EEE; -} - -.middle { - position: relative; - z-index: 2; - display: block; - width: 100%; - padding-top: 40px; - background: url(/static/bg.gif); - color: black; -} - -.footer { - background: url(/static/hbg.jpg); - z-index: 4; - position: relative; - background-color: #4A525A; - margin-top: 30px; - padding-top: 20px; - box-shadow: 0 -6px 5px 1px #343537; - color: #EEE; -} - -/* header elements */ - -.btitle { - text-decoration:none; - color: #EEE; - font-size:x-large; - font-weight:bold; - margin-top: 15px; - margin-bottom: 10px; -} - -.contacts { - float: left; - font-weight: bolder; -} - -.righttext { - float:right; - padding-right: 20px; -} - -.rightbox { - text-align:right; - padding-right: 14px; -} - -/* middle elements */ -.innerTitle { - margin-left: 10px; - font-weight: bold; -} - -.innerBoxComments{ - margin-left: 10px; -} - -.innerContainer { - padding-right: 20px; - -} - -/* common elements */ -.centerbox { - text-align:center; - min-height: 45px; -} - - -/* style elements */ - -.cInput { - margin-left: 15px; -} - -.cCaptcha { - padding: 5px; - border: 1px solid #555; - -webkit-border-radius: 0.5em; - margin-left: 15px; - width: 555px; - background: #F9F9F9; -} - -.tt { - font-family: "courier new",courier,monospace; - font-size: 13px; -} - -.cl { - text-decoration:none;color:black; -} - -.cHead { - font-size:large; - font-weight:bold; -} - -.notFoundFace { - text-align: center; - font-size: 100px; -} - -.notFoundText { - text-align: center; - font-size: 24px; - font-weight: bold; -} - -/* HsColour style */ - -.code -{ - box-shadow: 3px 3px 5px 1px #888; - border-radius: 10px; - padding: 0.75em; - - font-size: 11pt; - width: 60em; - color: white; - line-height: 1.2em; - font-family: 'Droid Sans Mono', sans-serif; - background: black; - background-image:url('/static/cbg.jpg'); - background-repeat: no-repeat; - } - -.code pre -{ - font-family: 'Droid Sans Mono', sans-serif; -} - -kbd -{ - font-family: 'Droid Sans Mono', sans-serif; - color: #333; - font-size: 0.8em; -} - -.wide -{ - width: 90em; -} - - -code -{ - line-height: 1.5em; - border: 1px; - } - -.source-code -{ - font-size: 0.75em; - color: #666; - } - -.warning -{ - color: red; - } - - -.hs-keyglyph { color: DarkGoldenrod; } -.hs-layout { color: white;} -.hs-keyword { color: skyblue; } -.hs-comment, .hs-comment a { color: cadetblue;} -.hs-str { color: Darkorange; } -.hs-chr { color: RosyBrown;} -.hs-conid { color: GreenYellow; } -.hs-varid { color: white; } -.hs-num { color: white; } -.hs-varop { color: DarkGoldenrod; } -.hs-conop { color: DarkGoldenrod; } -.hs-sel { color: FireBrick; } -.hs-cpp { color: yellow; } -.hs-definition { color: gold; } - diff --git a/res/blogstyle.lucius b/res/blogstyle.lucius new file mode 100644 index 0000000000..c2cacfd011 --- /dev/null +++ b/res/blogstyle.lucius @@ -0,0 +1,223 @@ +@charset "UTF-8"; +/* CSS Document */ +@import url(http://fonts.googleapis.com/css?family=Droid+Sans+Mono); +@import url(http://fonts.googleapis.com/css?family=PT+Sans); + +html, body{ + margin: 0; + padding: 0; +} + +body { + font-family: 'PT Sans', sans-serif; + min-height: 850px; + color: #EEE; +} + +a { + color: black; +} + +input, textarea, select { + border: 1px solid #555; + padding: 0.5em; + font-size: 15px; + line-height: 1.2em; + width: 550px; + background: #F9F9F9; + -webkit-border-radius: 0.5em; + } + +/* site sections */ +.header { + background: url(/static/hbg.jpg); + z-index: 4; + padding-left: 20px; + padding-bottom: 70px; + padding-top: 30px; + position: relative; + box-shadow: 0 6px 5px 1px #343537; +} + +.link { + color: #EEE; +} + +.middle { + position: relative; + z-index: 2; + display: block; + width: 100%; + padding-top: 40px; + background: url(/static/bg.gif); + color: black; +} + +.footer { + background: url(/static/hbg.jpg); + z-index: 4; + position: relative; + background-color: #4A525A; + margin-top: 30px; + padding-top: 20px; + box-shadow: 0 -6px 5px 1px #343537; + color: #EEE; +} + +/* header elements */ + +.btitle { + text-decoration:none; + color: #EEE; + font-size:x-large; + font-weight:bold; + margin-top: 15px; + margin-bottom: 10px; +} + +.contacts { + float: left; + font-weight: bolder; +} + +.righttext { + float:right; + padding-right: 20px; +} + +.rightbox { + text-align:right; + padding-right: 14px; +} + +/* middle elements */ +.innerTitle { + margin-left: 10px; + font-weight: bold; +} + +.innerBoxComments{ + margin-left: 10px; +} + +.innerContainer { + padding-right: 20px; + +} + +/* common elements */ +.centerbox { + text-align:center; + min-height: 45px; +} + + +/* style elements */ + +.cInput { + margin-left: 15px; +} + +.cCaptcha { + padding: 5px; + border: 1px solid #555; + -webkit-border-radius: 0.5em; + margin-left: 15px; + width: 555px; + background: #F9F9F9; +} + +.tt { + font-family: "courier new",courier,monospace; + font-size: 13px; +} + +.cl { + text-decoration:none;color:black; +} + +.cHead { + font-size:large; + font-weight:bold; +} + +.notFoundFace { + text-align: center; + font-size: 100px; +} + +.notFoundText { + text-align: center; + font-size: 24px; + font-weight: bold; +} + +/* HsColour style */ + +.code +{ + box-shadow: 3px 3px 5px 1px #888; + border-radius: 10px; + padding: 0.75em; + + font-size: 11pt; + width: 60em; + color: white; + line-height: 1.2em; + font-family: 'Droid Sans Mono', sans-serif; + background: black; + background-image:url('/static/cbg.jpg'); + background-repeat: no-repeat; + } + +.code pre +{ + font-family: 'Droid Sans Mono', sans-serif; +} + +kbd +{ + font-family: 'Droid Sans Mono', sans-serif; + color: #333; + font-size: 0.8em; +} + +.wide +{ + width: 90em; +} + + +code +{ + line-height: 1.5em; + border: 1px; + } + +.source-code +{ + font-size: 0.75em; + color: #666; + } + +.warning +{ + color: red; + } + + +.hs-keyglyph { color: DarkGoldenrod; } +.hs-layout { color: white;} +.hs-keyword { color: skyblue; } +.hs-comment, .hs-comment a { color: cadetblue;} +.hs-str { color: Darkorange; } +.hs-chr { color: RosyBrown;} +.hs-conid { color: GreenYellow; } +.hs-varid { color: white; } +.hs-num { color: white; } +.hs-varop { color: DarkGoldenrod; } +.hs-conop { color: DarkGoldenrod; } +.hs-sel { color: FireBrick; } +.hs-cpp { color: yellow; } +.hs-definition { color: gold; } + diff --git a/res/blogv33.css b/res/blogv33.css deleted file mode 100644 index 3be4840a9b..0000000000 --- a/res/blogv33.css +++ /dev/null @@ -1 +0,0 @@ -@charset UTF-8;@import url(http://fonts.googleapis.com/css?family=Droid+Sans+Mono);@import url(http://fonts.googleapis.com/css?family=PT+Sans);html,body{margin:0;padding:0;}body{font-family:'PT Sans', sans-serif;min-height:850px;color:#EEE;}a{color:#000;}input,textarea,select{border:1px solid #555;font-size:15px;line-height:1.2em;width:550px;background:#F9F9F9;-webkit-border-radius:.5em;padding:.5em;}.header{background:url(/static/hbg.jpg);z-index:4;padding-left:20px;padding-bottom:70px;padding-top:30px;position:relative;box-shadow:0 6px 5px 1px #343537;}.link{color:#EEE;}.middle{position:relative;z-index:2;display:block;width:100%;padding-top:40px;background:url(/static/bg.gif);color:#000;}.footer{background:url(/static/hbg.jpg);z-index:4;position:relative;background-color:#4A525A;margin-top:30px;padding-top:20px;box-shadow:0 -6px 5px 1px #343537;color:#EEE;}.btitle{text-decoration:none;color:#EEE;font-size:x-large;font-weight:700;margin-top:15px;margin-bottom:10px;}.contacts{float:left;font-weight:bolder;}.righttext{float:right;padding-right:20px;}.rightbox{text-align:right;padding-right:14px;}.innerTitle{margin-left:10px;font-weight:700;}.innerBoxComments{margin-left:10px;}.innerContainer{padding-right:20px;}.centerbox{text-align:center;min-height:45px;}.cInput{margin-left:15px;}.cCaptcha{border:1px solid #555;-webkit-border-radius:.5em;margin-left:15px;width:555px;background:#F9F9F9;padding:5px;}.tt{font-family:"courier new",courier,monospace;font-size:13px;}.cl{text-decoration:none;color:#000;}.cHead{font-size:large;font-weight:700;}.notFoundFace{text-align:center;font-size:100px;}.notFoundText{text-align:center;font-size:24px;font-weight:700;}.code{box-shadow:3px 3px 5px 1px #888;border-radius:10px;font-size:11pt;width:60em;color:#FFF;line-height:1.2em;font-family:'Droid Sans Mono', sans-serif;background:#000;background-image:url(/static/cbg.jpg);background-repeat:no-repeat;padding:.75em;}.code pre{font-family:'Droid Sans Mono', sans-serif;}kbd{font-family:'Droid Sans Mono', sans-serif;color:#333;font-size:.8em;}.wide{width:90em;}code{line-height:1.5em;border:1px;}.source-code{font-size:.75em;color:#666;}.warning{color:red;}.hs-keyword{color:#87CEEB;}.hs-comment,.hs-comment a{color:#5F9EA0;}.hs-str{color:#FF8C00;}.hs-chr{color:#BC8F8F;}.hs-conid{color:#ADFF2F;}.hs-sel{color:#B22222;}.hs-cpp{color:#FF0;}.hs-definition{color:#FFD700;}.hs-keyglyph,.hs-varop,.hs-conop{color:#B8860B;}.hs-layout,.hs-varid,.hs-num{color:#FFF;} -- cgit 1.4.1 From 584fd922bcb3146e156bcd6cb1f7760493fbc604 Mon Sep 17 00:00:00 2001 From: Vincent Ambo <v.ambo@me.com> Date: Thu, 26 Apr 2012 01:13:52 +0200 Subject: * some fixes --- src/Blog.hs | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/src/Blog.hs b/src/Blog.hs index 51c8b086d9..d80b89bb10 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -74,7 +74,7 @@ showFooter l v = [shamlet| <a class="link" href="http://haskell.org">Haskell , # <a class="link" href="http://hackage.haskell.org/package/acid-state-0.6.3">Acid-State - / and without PHP, Java, Perl, MySQL and Python. + \ and without PHP, Java, Perl, MySQL and Python. <p> <a class="link" href=#{repoURL}>#{append "Version " v}   @@ -107,7 +107,7 @@ showLinks (Just i) lang = [shamlet| $if ((>) i 1) <div class="centerbox"> <a href=#{nLink $ succ i}>#{backText lang} - / -- # + \ -- # <a href=#{nLink $ pred i}>#{nextText lang} $elseif ((<=) i 1) ^{showLinks Nothing lang} @@ -227,10 +227,13 @@ adminIndex sUser = adminTemplate "Index" $ [shamlet| <option value="en">Englisch <tr> <thead><td>Text: - <td><textarea name="btext" cols="100" rows="15"> + <td> + <textarea name="btext" cols="100" rows="15"> <tr> - <thead><td style="vertical-align:top;">Mehr Text: - <td><textarea name="mtext" cols="100" rows="15"> + <thead> + <td style="vertical-align:top;">Mehr Text: + <td> + <textarea name="mtext" cols="100" rows="15"> <input type="hidden" name="author" value=#{sUser}> <input style="margin-left:20px;" type="submit" value="Absenden"> ^{adminFooter} @@ -239,13 +242,13 @@ adminIndex sUser = adminTemplate "Index" $ [shamlet| adminFooter :: Html adminFooter = [shamlet| <a href="/">Startseite -/ -- Entrylist: # +\ -- Entrylist: # <a href="/admin/entrylist/de">DE -/ & # +\ & # <a href="/admin/entrylist/en">EN -/ -- # +\ -- # <a href="#">Backup -/ (NYI) +\ (NYI) |] adminEntryList :: [Entry] -> Html @@ -254,7 +257,7 @@ adminEntryList entries = adminTemplate "EntryList" $ [shamlet| <table> $forall entry <- entries <tr> - <td><a href=#{append "/admin/edit" (show' $ entryId entry)}>#{title entry} + <td><a href=#{append "/admin/edit/" (show' $ entryId entry)}>#{title entry} <td>#{formatPostDate $ edate entry} |] where @@ -267,13 +270,16 @@ editPage (Entry{..}) = adminTemplate "Index" $ [shamlet| <table> <tr> <td>Titel: - <td><input type="text" name="title" value=#{title}> + <td> + <input type="text" name="title" value=#{title}> <tr> <td style="vertical-align:top;">Text: - <td><textarea name="btext" cols="100" rows="15">#{btext} + <td> + <textarea name="btext" cols="100" rows="15">#{btext} <tr> <td style="vertical-align:top;">Mehr Text: - <td><textarea name="mtext" cols="100" rows="15">#{mtext} + <td> + <textarea name="mtext" cols="100" rows="15">#{mtext} <input type="hidden" name="eid" value=#{unEntryId entryId}> <input type="submit" style="margin-left:20px;" value="Absenden"> <div class="editComments">#{editComments comments entryId} -- cgit 1.4.1 From 0f1e6c2a6b19e16f5297b16a52e4cb94e121f838 Mon Sep 17 00:00:00 2001 From: Vincent Ambo <v.ambo@me.com> Date: Sat, 28 Apr 2012 21:52:13 +0200 Subject: * fixed iMessage link --- src/Blog.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Blog.hs b/src/Blog.hs index d80b89bb10..3bff446079 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -63,7 +63,7 @@ $doctype 5 , # <a class="link" href=#{twitter} target="_blank">Twitter #{orText lang} -<a class="link" href=#{imu} target="_blank">iMessage +<a class="link" href=#{imu}>iMessage . |] -- cgit 1.4.1 From 3a39dfc19ee59e78de73ff8fdcf8108b751ebcf8 Mon Sep 17 00:00:00 2001 From: "\"Vincent Ambo ext:(%22)" <tazjin@me.com> Date: Wed, 24 Apr 2013 14:35:25 +0200 Subject: * small fixes that I don't remember --- TazBlog.cabal | 3 ++- src/Blog.hs | 4 ++-- src/Main.hs | 3 ++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/TazBlog.cabal b/TazBlog.cabal index 06378b6f14..cd7d30adfc 100644 --- a/TazBlog.cabal +++ b/TazBlog.cabal @@ -34,4 +34,5 @@ Executable tazblog options, rss, recaptcha, - xhtml + hamlet, + shakespeare-css diff --git a/src/Blog.hs b/src/Blog.hs index 3bff446079..fd31530c1d 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -31,7 +31,7 @@ show' = pack . show data BlogURL = BlogURL -- blog CSS (admin is still static) -stylesheetSource = $(luciusFile "../res/blogstyle.lucius") +stylesheetSource = $(luciusFile "res/blogstyle.lucius") blogStyle = renderCssUrl undefined stylesheetSource -- blog HTML blogTemplate :: BlogLang -> Text -> Html -> Html @@ -164,7 +164,7 @@ renderCommentBox cLang cId = [shamlet| <p><input class="cInput" style="width:120px;" type="submit" value=#{cSend cLang}> |] where - aLink = T.concat ["/", show' cLang, "/postcomment", show' cId] + aLink = T.concat ["/", show' cLang, "/postcomment/", show' cId] showSiteNotice :: Html showSiteNotice = [shamlet| diff --git a/src/Main.hs b/src/Main.hs index 3efcf5be34..a83c9958ef 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -90,7 +90,8 @@ tazBlog acid captchakey = do , dir "dologin" $ processLogin acid , do dirs "static/blogv34.css" $ nullDir setHeaderM "content-type" "text/css" - neverExpires + setHeaderM "cache-control" "max-age=630720000" + setHeaderM "expires" "Tue, 20 Jan 2037 04:20:42 GMT" ok $ toResponse $ blogStyle , do setHeaderM "cache-control" "max-age=630720000" setHeaderM "expires" "Tue, 20 Jan 2037 04:20:42 GMT" -- cgit 1.4.1 From de09c2cc68c1bc7914687a228aa3805ac8bcb9b1 Mon Sep 17 00:00:00 2001 From: "\"Vincent Ambo ext:(%22)" <tazjin@gmail.com> Date: Sat, 27 Apr 2013 22:15:46 +0200 Subject: * fixed comment deletion link * updated contact info * fixed compatibility with current blaze --- TazBlog.cabal | 1 + src/Blog.hs | 23 ++++++++++------------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/TazBlog.cabal b/TazBlog.cabal index cd7d30adfc..ed1f886a19 100644 --- a/TazBlog.cabal +++ b/TazBlog.cabal @@ -20,6 +20,7 @@ Executable tazblog happstack-server, text, blaze-html, + blaze-markup, crypto-api, cryptohash, old-locale, diff --git a/src/Blog.hs b/src/Blog.hs index fd31530c1d..4906ad5d40 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -10,7 +10,7 @@ import Data.Text (Text, append, pack, empty) import Data.Time import Network.Captcha.ReCaptcha import System.Locale (defaultTimeLocale) -import Text.Blaze (preEscapedText) +import Text.Blaze.Html (preEscapedToHtml) import Text.Hamlet import Text.Lucius import Locales @@ -47,7 +47,7 @@ $doctype 5 <a class="btitle" href=#{append "/" (show' lang)}>#{blogTitle lang empty} <p style="clear: both;"> <span class="contacts" id="cosx">^{contactInfo iMessage} - <span class="righttext">^{preEscapedText $ rightText lang} + <span class="righttext">^{preEscapedToHtml $ rightText lang} <div class="middle"> ^{body} <div class="footer"> @@ -89,7 +89,7 @@ renderEntries showAll entries topText footerLinks = [shamlet| $forall entry <- elist <li> <a href=#{linkElems entry}>#{linkText $ length $ comments entry} - ^{preEscapedText $ append " " $ btext entry} + ^{preEscapedToHtml $ append " " $ btext entry} $if ((/=) (mtext entry) empty) <p><a href=#{linkElems entry}>#{readMore $ lang entry} $else @@ -130,8 +130,8 @@ renderEntry Entry{..} = [shamlet| <article> <ul style="max-width:57em;"> <li> - ^{preEscapedText $ btext} - <p>^{preEscapedText $ mtext} + ^{preEscapedToHtml $ btext} + <p>^{preEscapedToHtml $ mtext} <div class="innerBoxComments"> <div class="cHead">#{cHead lang} <ul style="max-width:57em;">#{renderComments comments lang} @@ -146,7 +146,7 @@ renderComments comments lang = [shamlet| $forall comment <- comments <li> <i>#{append (cauthor comment) ": "} - ^{preEscapedText $ ctext comment} + ^{preEscapedToHtml $ ctext comment} <p class="tt">#{timeString $ cdate comment} |] where @@ -172,17 +172,14 @@ $doctype 5 <head> <title>Impressum <body> - <h2> - Impressum und # - <a alt="Verantwortlich im Sinne des Presserechtes">ViSdP - <i>[German law demands this] + <h2>Impressum <br> <p> Vincent Ambo <br> - Benfleetstr. 8 + Gyllenborgsgatan 8, LGH 1306 <br> - 50858 Köln + 11243 Stockholm <p><a href="/" style="color:black;">Back |] @@ -298,7 +295,7 @@ editComments comments eId = [shamlet| |] where cPostTime = formatTime defaultTimeLocale "%c" - cDeleteLink cd = concat ["/admin/cdelete", show eId, formatTime defaultTimeLocale "/%s%Q" cd] + cDeleteLink cd = concat ["/admin/cdelete/", show eId, formatTime defaultTimeLocale "/%s%Q" cd] commentDeleted :: EntryId -> Html commentDeleted eId = adminTemplate "Kommentar gelöscht" $ [shamlet| -- cgit 1.4.1 From 1bdbe4af642cbaaacb15fbf882a3ad87ebf1e486 Mon Sep 17 00:00:00 2001 From: "\"Vincent Ambo ext:(%22)" <tazjin@gmail.com> Date: Sat, 27 Apr 2013 22:17:54 +0200 Subject: * switched main language to English --- src/Main.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Main.hs b/src/Main.hs index a83c9958ef..0c4fcdffcd 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -60,7 +60,7 @@ tazBlog :: AcidState Blog -> String -> ServerPart Response tazBlog acid captchakey = do compr <- compressedResponseFilter msum [ path $ \(lang :: BlogLang) -> blogHandler acid lang captchakey - , nullDir >> showIndex acid DE + , nullDir >> showIndex acid EN , dir " " $ nullDir >> seeOther ("https://plus.google.com/115916629925754851590" :: Text) (toResponse ()) , path $ \(year :: Int) -> path $ \(month :: Int) -> path $ \(id_ :: String) -> formatOldLink year month id_ -- cgit 1.4.1 From b3fb7f0f341bf3574a5c42fbbf3e0ddc3ea286a1 Mon Sep 17 00:00:00 2001 From: "\"Vincent Ambo ext:(%22)" <tazjin@gmail.com> Date: Sun, 28 Apr 2013 13:11:38 +0200 Subject: * re-enabled captchas --- src/Blog.hs | 20 ++++++++++++++++++++ src/Main.hs | 6 ++---- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/Blog.hs b/src/Blog.hs index 4906ad5d40..10b1188615 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -152,15 +152,35 @@ $forall comment <- comments where timeString = formatTime defaultTimeLocale (cTimeFormat lang) +captcha :: Html +captcha = [shamlet| +<div class="cCaptcha"> + <script src="http://api.recaptcha.net/challenge?k=6LfQXccSAAAAAIjKm26XlFnBMAgvaKlOAjVWEEnM" type="text/javascript"> + <noscript> + <iframe src="http://api.recaptcha.net/noscript?k=6LfQXccSAAAAAIjKm26XlFnBMAgvaKlOAjVWEEnM" height="300" width="500" seamless> + <br> + <textarea name="recaptcha_challenge_field" rows="3" cols="40"> + <input type="hidden" name="recaptcha_response_field" value="manual_challenge"> +|] + +captchaOptions :: BlogLang -> Html +captchaOptions lang = [shamlet|<script type="text/javascript">^{preEscapedToHtml options}|] + where + options = T.concat ["var RecaptchaOptions = { theme: 'clean', lang: '", showLangText lang, "'};"] + renderCommentBox :: BlogLang -> EntryId -> Html renderCommentBox cLang cId = [shamlet| <div class="cHead">#{cwHead cLang} +^{captchaOptions cLang} <form method="POST" action=#{aLink}> <p><input name="cname" placeholder="Name" class="cInput"> <p> <label> <textarea name="ctext" cols="50" rows="13" class="cInput" placeholder=#{cTextPlaceholder cLang}> + <p> + <label> + ^{captcha} <p><input class="cInput" style="width:120px;" type="submit" value=#{cSend cLang}> |] where diff --git a/src/Main.hs b/src/Main.hs index 0c4fcdffcd..a6f59acc28 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -154,9 +154,7 @@ addComment acid lang captchakey eId = do nComment <- Comment <$> pure now <*> lookText' "cname" <*> pure (commentEscape nCtext) - update' acid (AddComment eId nComment) - >> seeOther ("/" ++ show lang ++ "/" ++ show eId) (toResponse()) - {- -- captcha verification + -- captcha verification challenge <- look "recaptcha_challenge_field" response <- look "recaptcha_response_field" (userIp, _) <- askRq >>= return . rqPeer @@ -164,7 +162,7 @@ addComment acid lang captchakey eId = do case validation of Right _ -> update' acid (AddComment eId nComment) >> seeOther ("/" ++ show lang ++ "/" ++ show eId) (toResponse()) - Left _ -> (liftIO $ putStrLn "Captcha failed") >> seeOther ("/" ++ show lang ++ "/" ++ show eId) (toResponse()) -} + Left _ -> (liftIO $ putStrLn "Captcha failed") >> seeOther ("/" ++ show lang ++ "/" ++ show eId) (toResponse()) commentEscape :: Text -> Text commentEscape = newlineEscape . ltEscape . gtEscape . ampEscape -- cgit 1.4.1 From 247265f35499d82706fb99ac0a68c90cc169ca1a Mon Sep 17 00:00:00 2001 From: "\"Vincent Ambo ext:(%22)" <tazjin@gmail.com> Date: Sun, 28 Apr 2013 13:12:08 +0200 Subject: * saved the function that I cleaned the comments with. The function is horrendous --- tools/fixcomments.hs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 tools/fixcomments.hs diff --git a/tools/fixcomments.hs b/tools/fixcomments.hs new file mode 100644 index 0000000000..dc89dbdd64 --- /dev/null +++ b/tools/fixcomments.hs @@ -0,0 +1,21 @@ + +fixComments :: AcidState Blog -> IO () +fixComments acid = do + entriesDE <- query' acid $ LatestEntries DE + entriesEN <- query' acid $ LatestEntries EN + filterComments entriesDE + filterComments entriesEN + where + (cDate :: UTCTime) = fromJust $ parseTime defaultTimeLocale "%d.%m.%Y %T" "22.04.2012 21:57:35" + foldOp :: [(EntryId, [UTCTime])] -> Entry -> [(EntryId, [UTCTime])] + foldOp l e = let c = map cdate $ filter (\c1 -> cdate c1 > cDate) $ comments e + in if null c then l + else (entryId e, c) : l + pred :: Entry -> Bool + pred e = let f eId [] = False + f eId (c:r) = if (cdate c > cDate) then True + else f eId r + in f (entryId e) (comments e) + filterComments entries = mapM_ removeComments $ foldl foldOp [] $ filter pred entries + removeComments :: (EntryId, [UTCTime]) -> IO () + removeComments (eId, comments) = mapM_ (\c -> update' acid $ DeleteComment eId c) comments \ No newline at end of file -- cgit 1.4.1 From 0f98c3f489b8a00f1a6ce539b8f853a07610c2e1 Mon Sep 17 00:00:00 2001 From: "\"Vincent Ambo ext:(%22)" <tazjin@gmail.com> Date: Sun, 28 Apr 2013 13:45:08 +0200 Subject: * removed iMessage --- src/Blog.hs | 9 +++------ src/Locales.hs | 4 +--- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/Blog.hs b/src/Blog.hs index 10b1188615..d3382357fc 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -46,7 +46,7 @@ $doctype 5 <div class="header"> <a class="btitle" href=#{append "/" (show' lang)}>#{blogTitle lang empty} <p style="clear: both;"> - <span class="contacts" id="cosx">^{contactInfo iMessage} + <span class="contacts" id="cosx">^{contactInfo} <span class="righttext">^{preEscapedToHtml $ rightText lang} <div class="middle"> ^{body} @@ -57,14 +57,11 @@ $doctype 5 |] where rssUrl = T.concat ["/", show' lang, "/rss.xml"] - contactInfo imu = [shamlet| + contactInfo = [shamlet| #{contactText lang} <a class="link" href=#{mailTo}>Mail -, # -<a class="link" href=#{twitter} target="_blank">Twitter #{orText lang} -<a class="link" href=#{imu}>iMessage -. +<a class="link" href=#{twitter} target="_blank">Twitter |] showFooter :: BlogLang -> Text -> Html diff --git a/src/Locales.hs b/src/Locales.hs index 8e2149fa74..aaddb484cb 100644 --- a/src/Locales.hs +++ b/src/Locales.hs @@ -158,7 +158,5 @@ rightText EN = "Deutsche Version <a href=\"/de\" class=\"link\">hier verfüg -- static information repoURL :: Text = "https://bitbucket.org/tazjin/tazblog-haskell" -mailTo :: Text = "mailto:hej@tazj.in" +mailTo :: Text = "mailto:tazjin@gmail.com" twitter :: Text = "http://twitter.com/#!/tazjin" -iMessage :: Text = "imessage:tazjin@me.com" -iMessage' :: Text = "sms:tazjin@me.com" -- cgit 1.4.1 From 8f1b6b5c4e34c59d7ceb19ef2fe552a104d7bf62 Mon Sep 17 00:00:00 2001 From: "\"Vincent Ambo ext:(%22)" <tazjin@gmail.com> Date: Sun, 28 Apr 2013 14:26:45 +0200 Subject: * added Markdown support --- TazBlog.cabal | 3 ++- src/Blog.hs | 35 +++++++++++++++++++++++++++++------ 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/TazBlog.cabal b/TazBlog.cabal index ed1f886a19..63f9ad75e0 100644 --- a/TazBlog.cabal +++ b/TazBlog.cabal @@ -36,4 +36,5 @@ Executable tazblog rss, recaptcha, hamlet, - shakespeare-css + shakespeare-css, + markdown diff --git a/src/Blog.hs b/src/Blog.hs index d3382357fc..9c66a20101 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -5,14 +5,17 @@ module Blog where import Control.Monad (when, unless) import Data.Data (Data, Typeable) import Data.List (intersperse) +import Data.Maybe (fromJust) import Data.Monoid (mempty) import Data.Text (Text, append, pack, empty) +import Data.Text.Lazy (fromStrict) import Data.Time import Network.Captcha.ReCaptcha import System.Locale (defaultTimeLocale) import Text.Blaze.Html (preEscapedToHtml) import Text.Hamlet -import Text.Lucius +import Text.Lucius +import Text.Markdown import Locales import BlogDB @@ -28,11 +31,16 @@ replace x y = map (\z -> if z == x then y else z) show' :: Show a => a -> Text show' = pack . show +-- |After this time all entries are Markdown +markdownCutoff :: UTCTime +markdownCutoff = fromJust $ parseTime defaultTimeLocale "%s" "1367149834" + data BlogURL = BlogURL -- blog CSS (admin is still static) stylesheetSource = $(luciusFile "res/blogstyle.lucius") blogStyle = renderCssUrl undefined stylesheetSource + -- blog HTML blogTemplate :: BlogLang -> Text -> Html -> Html blogTemplate lang t_append body = [shamlet| @@ -78,6 +86,12 @@ showFooter l v = [shamlet| <a class="link" href="/notice">#{noticeText l} |] +isEntryMarkdown :: Entry -> Bool +isEntryMarkdown e = edate e > markdownCutoff + +renderEntryMarkdown :: Text -> Html +renderEntryMarkdown = markdown def {msXssProtect = False} . fromStrict + renderEntries :: Bool -> [Entry] -> Text -> Maybe Html -> Html renderEntries showAll entries topText footerLinks = [shamlet| <span class="innerTitle">#{topText} @@ -85,8 +99,13 @@ renderEntries showAll entries topText footerLinks = [shamlet| <ul style="max-width:57em;"> $forall entry <- elist <li> - <a href=#{linkElems entry}>#{linkText $ length $ comments entry} - ^{preEscapedToHtml $ append " " $ btext entry} + $if (isEntryMarkdown entry) + <a href=#{linkElems entry}>#{linkText $ length $ comments entry} + <b>#{title entry} + ^{renderEntryMarkdown $ append " " $ btext entry} + $else + <a href=#{linkElems entry}>#{linkText $ length $ comments entry} + ^{preEscapedToHtml $ append " " $ btext entry} $if ((/=) (mtext entry) empty) <p><a href=#{linkElems entry}>#{readMore $ lang entry} $else @@ -119,7 +138,7 @@ showLinks Nothing lang = [shamlet| nLink = T.concat ["/", show' lang, "/?page=2"] renderEntry :: Entry -> Html -renderEntry Entry{..} = [shamlet| +renderEntry e@Entry{..} = [shamlet| <span class="innerTitle">#{title} <span class="righttext"> <i>#{woText} @@ -127,8 +146,12 @@ renderEntry Entry{..} = [shamlet| <article> <ul style="max-width:57em;"> <li> - ^{preEscapedToHtml $ btext} - <p>^{preEscapedToHtml $ mtext} + $if (isEntryMarkdown e) + ^{renderEntryMarkdown btext} + <p>^{renderEntryMarkdown $ mtext} + $else + ^{preEscapedToHtml $ btext} + <p>^{preEscapedToHtml $ mtext} <div class="innerBoxComments"> <div class="cHead">#{cHead lang} <ul style="max-width:57em;">#{renderComments comments lang} -- cgit 1.4.1 From 9719b5a62de0795a5cb62778de06c3aa7f206e95 Mon Sep 17 00:00:00 2001 From: "\"Vincent Ambo ext:(%22)" <tazjin@gmail.com> Date: Sun, 28 Apr 2013 14:30:00 +0200 Subject: Used stylish-haskell on all source files --- src/Blog.hs | 45 +++++++++++++++++-------------- src/BlogDB.hs | 84 +++++++++++++++++++++++++++++++--------------------------- src/Locales.hs | 18 +++++++------ src/Main.hs | 69 +++++++++++++++++++++++++---------------------- src/RSS.hs | 18 ++++++------- 5 files changed, 127 insertions(+), 107 deletions(-) diff --git a/src/Blog.hs b/src/Blog.hs index 9c66a20101..2c1a546a2a 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -1,25 +1,30 @@ -{-# LANGUAGE OverloadedStrings, ScopedTypeVariables, DeriveDataTypeable, TemplateHaskell, QuasiQuotes, RecordWildCards #-} +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TemplateHaskell #-} module Blog where -import Control.Monad (when, unless) -import Data.Data (Data, Typeable) -import Data.List (intersperse) -import Data.Maybe (fromJust) -import Data.Monoid (mempty) -import Data.Text (Text, append, pack, empty) -import Data.Text.Lazy (fromStrict) -import Data.Time -import Network.Captcha.ReCaptcha -import System.Locale (defaultTimeLocale) -import Text.Blaze.Html (preEscapedToHtml) -import Text.Hamlet -import Text.Lucius -import Text.Markdown -import Locales -import BlogDB - -import qualified Data.Text as T +import BlogDB +import Control.Monad (unless, when) +import Data.Data (Data, Typeable) +import Data.List (intersperse) +import Data.Maybe (fromJust) +import Data.Monoid (mempty) +import Data.Text (Text, append, empty, pack) +import Data.Text.Lazy (fromStrict) +import Data.Time +import Locales +import Network.Captcha.ReCaptcha +import System.Locale (defaultTimeLocale) +import Text.Blaze.Html (preEscapedToHtml) +import Text.Hamlet +import Text.Lucius +import Text.Markdown + +import qualified Data.Text as T -- custom list functions intersperse' :: a -> [a] -> [a] @@ -39,7 +44,7 @@ data BlogURL = BlogURL -- blog CSS (admin is still static) stylesheetSource = $(luciusFile "res/blogstyle.lucius") -blogStyle = renderCssUrl undefined stylesheetSource +blogStyle = renderCssUrl undefined stylesheetSource -- blog HTML blogTemplate :: BlogLang -> Text -> Html -> Html diff --git a/src/BlogDB.hs b/src/BlogDB.hs index 611a08914a..b2551ecc3d 100644 --- a/src/BlogDB.hs +++ b/src/BlogDB.hs @@ -1,30 +1,36 @@ -{-# LANGUAGE DeriveDataTypeable, GeneralizedNewtypeDeriving, RecordWildCards, -TemplateHaskell, TypeFamilies, OverloadedStrings, ScopedTypeVariables, BangPatterns #-} +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeFamilies #-} module BlogDB where -import Control.Monad.Reader (ask) -import Control.Monad.State (get, put) -import Data.Acid -import Data.Acid.Advanced -import Data.Acid.Local -import Data.ByteString (ByteString) -import Data.Char (toLower) -import Data.Data (Data, Typeable) -import Data.IxSet (Indexable(..), IxSet(..), (@=), Proxy(..), getOne, ixFun, ixSet) -import Data.List (insert) -import Data.SafeCopy (SafeCopy, base, deriveSafeCopy) -import Data.Text (Text, pack) -import Data.Text.Lazy (toStrict) -import Data.Time -import Happstack.Server (FromReqURI(..)) -import System.Environment (getEnv) - -import qualified Crypto.Hash.SHA512 as SHA (hash) -import qualified Data.ByteString.Char8 as B +import Control.Monad.Reader (ask) +import Control.Monad.State (get, put) +import Data.Acid +import Data.Acid.Advanced +import Data.Acid.Local +import Data.ByteString (ByteString) +import Data.Char (toLower) +import Data.Data (Data, Typeable) +import Data.IxSet (Indexable (..), IxSet (..), Proxy (..), + getOne, ixFun, ixSet, (@=)) +import Data.List (insert) +import Data.SafeCopy (SafeCopy, base, deriveSafeCopy) +import Data.Text (Text, pack) +import Data.Text.Lazy (toStrict) +import Data.Time +import Happstack.Server (FromReqURI (..)) +import System.Environment (getEnv) + +import qualified Crypto.Hash.SHA512 as SHA (hash) import qualified Data.ByteString.Base64 as B64 (encode) -import qualified Data.IxSet as IxSet -import qualified Data.Text as Text +import qualified Data.ByteString.Char8 as B +import qualified Data.IxSet as IxSet +import qualified Data.Text as Text newtype EntryId = EntryId { unEntryId :: Integer } @@ -33,7 +39,7 @@ newtype EntryId = EntryId { unEntryId :: Integer } instance Show EntryId where show = show . unEntryId -data BlogLang = EN | DE +data BlogLang = EN | DE deriving (Eq, Ord, Data, Typeable) instance Show BlogLang where @@ -41,7 +47,7 @@ instance Show BlogLang where show EN = "en" instance FromReqURI BlogLang where - fromReqURI sub = + fromReqURI sub = case map toLower sub of "de" -> Just DE "en" -> Just EN @@ -58,14 +64,14 @@ data Comment = Comment { $(deriveSafeCopy 0 'base ''Comment) data Entry = Entry { - entryId :: EntryId, - lang :: BlogLang, - author :: Text, - title :: Text, - btext :: Text, - mtext :: Text, - edate :: UTCTime, - tags :: [Text], + entryId :: EntryId, + lang :: BlogLang, + author :: Text, + title :: Text, + btext :: Text, + mtext :: Text, + edate :: UTCTime, + tags :: [Text], comments :: [Comment] } deriving (Eq, Ord, Show, Data, Typeable) @@ -82,7 +88,7 @@ newtype SDate = SDate UTCTime deriving (Eq, Ord, Data, Typeable, SafeCopy) newtype Username = Username Text deriving (Eq, Ord, Data, Typeable, SafeCopy) newtype SessionID = SessionID Text deriving (Eq, Ord, Data, Typeable, SafeCopy) -instance Indexable Entry where +instance Indexable Entry where empty = ixSet [ ixFun $ \e -> [ entryId e] , ixFun $ (:[]) . lang , ixFun $ \e -> [ Author $ author e ] @@ -111,7 +117,7 @@ $(deriveSafeCopy 0 'base ''Session) instance Indexable User where empty = ixSet [ ixFun $ \u -> [Username $ username u] - , ixFun $ (:[]) . password + , ixFun $ (:[]) . password ] instance Indexable Session where @@ -128,8 +134,8 @@ data Blog = Blog { $(deriveSafeCopy 0 'base ''Blog) -initialBlogState :: Blog -initialBlogState = +initialBlogState :: Blog +initialBlogState = Blog { blogSessions = empty , blogUsers = empty , blogEntries = empty } @@ -137,7 +143,7 @@ initialBlogState = -- acid-state database functions (purity is necessary!) insertEntry :: Entry -> Update Blog Entry -insertEntry e = +insertEntry e = do b@Blog{..} <- get put $ b { blogEntries = IxSet.insert e blogEntries } return e @@ -159,7 +165,7 @@ deleteComment eId cDate = return newEntry updateEntry :: Entry -> Update Blog Entry -updateEntry e = +updateEntry e = do b@Blog{..} <- get put $ b { blogEntries = IxSet.updateIx (entryId e) e blogEntries} return e diff --git a/src/Locales.hs b/src/Locales.hs index aaddb484cb..3514859437 100644 --- a/src/Locales.hs +++ b/src/Locales.hs @@ -1,15 +1,17 @@ -{-# LANGUAGE ScopedTypeVariables, DeriveDataTypeable, OverloadedStrings #-} +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} module Locales where -import Data.Data (Data, Typeable) -import Data.Maybe (fromMaybe) -import Data.Text (Text) -import qualified Data.Text as T +import Data.Data (Data, Typeable) +import Data.Maybe (fromMaybe) +import Data.Text (Text) +import qualified Data.Text as T import Network.URI -import BlogDB (BlogLang (..)) +import BlogDB (BlogLang (..)) {- to add a language simply define its abbreviation and Show instance then - translate the appropriate strings and add CouchDB views in Server.hs -} @@ -40,7 +42,7 @@ getMonth :: BlogLang -> Int -> Int -> Text getMonth l y m = T.append (monthName l m) $ T.pack $ show y where monthName :: BlogLang -> Int -> Text - monthName DE m = case m of + monthName DE m = case m of 1 -> "Januar " 2 -> "Februar " 3 -> "März " @@ -116,7 +118,7 @@ cwHead EN = "Comment:" cSingle :: BlogLang -> Text cSingle DE = "Kommentar:" --input label -cSingle EN = "Comment:" +cSingle EN = "Comment:" cTimeFormat :: BlogLang -> String --formatTime expects a String cTimeFormat DE = "[Am %d.%m.%y um %H:%M Uhr]" diff --git a/src/Main.hs b/src/Main.hs index a6f59acc28..09215ba9d5 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -1,36 +1,43 @@ -{-# LANGUAGE OverloadedStrings, ScopedTypeVariables, GeneralizedNewtypeDeriving, - DeriveDataTypeable, FlexibleContexts, MultiParamTypeClasses, TemplateHaskell, - TypeFamilies, RecordWildCards, BangPatterns #-} +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeFamilies #-} module Main where -import Control.Applicative ((<$>), (<*>), optional, pure) -import Control.Exception (bracket) -import Control.Monad (msum, mzero, when, unless) -import Control.Monad.IO.Class (liftIO) -import Control.Monad.State (get, put) -import Control.Monad.Reader (ask) -import qualified Crypto.Hash.SHA512 as SHA +import Control.Applicative (optional, pure, (<$>), (<*>)) +import Control.Exception (bracket) +import Control.Monad (msum, mzero, unless, when) +import Control.Monad.IO.Class (liftIO) +import Control.Monad.Reader (ask) +import Control.Monad.State (get, put) +import qualified Crypto.Hash.SHA512 as SHA import Data.Acid import Data.Acid.Advanced import Data.Acid.Local -import qualified Data.ByteString.Base64 as B64 (encode) -import Data.ByteString.Char8 (ByteString, pack, unpack) -import Data.Data (Data, Typeable) -import Data.Maybe (fromJust) -import Data.Monoid (mempty) -import Data.Text (Text) -import qualified Data.Text as T +import qualified Data.ByteString.Base64 as B64 (encode) +import Data.ByteString.Char8 (ByteString, pack, unpack) +import Data.Data (Data, Typeable) +import Data.Maybe (fromJust) +import Data.Monoid (mempty) +import Data.SafeCopy (base, deriveSafeCopy) +import Data.Text (Text) +import qualified Data.Text as T import Data.Time -import Data.SafeCopy (base, deriveSafeCopy) -import Happstack.Server hiding (Session) +import Happstack.Server hiding (Session) import Happstack.Server.Compression import Network.Captcha.ReCaptcha import Options -import System.Locale (defaultTimeLocale) +import System.Locale (defaultTimeLocale) import Blog -import BlogDB hiding (addComment, updateEntry, deleteComment) +import BlogDB hiding (addComment, deleteComment, + updateEntry) import Locales import RSS @@ -86,7 +93,7 @@ tazBlog acid captchakey = do , do dir "admin" $ nullDir guardSession acid ok $ toResponse $ adminIndex ("tazjin" :: Text) - , dir "admin" $ ok $ toResponse $ adminLogin + , dir "admin" $ ok $ toResponse $ adminLogin , dir "dologin" $ processLogin acid , do dirs "static/blogv34.css" $ nullDir setHeaderM "content-type" "text/css" @@ -101,10 +108,10 @@ tazBlog acid captchakey = do ] blogHandler :: AcidState Blog -> BlogLang -> String -> ServerPart Response -blogHandler acid lang captchakey = +blogHandler acid lang captchakey = msum [ path $ \(eId :: Integer) -> showEntry acid lang $ EntryId eId , do decodeBody tmpPolicy - dir "postcomment" $ path $ + dir "postcomment" $ path $ \(eId :: Integer) -> addComment acid lang captchakey $ EntryId eId , nullDir >> showIndex acid lang , dir "rss" $ nullDir >> showRSS acid lang @@ -113,8 +120,8 @@ blogHandler acid lang captchakey = ] formatOldLink :: Int -> Int -> String -> ServerPart Response -formatOldLink y m id_ = - flip seeOther (toResponse ()) $ +formatOldLink y m id_ = + flip seeOther (toResponse ()) $ concat $ intersperse' "/" ["de", show y, show m, replace '.' '/' id_] showEntry :: AcidState Blog -> BlogLang -> EntryId -> ServerPart Response @@ -133,12 +140,12 @@ showIndex :: AcidState Blog -> BlogLang -> ServerPart Response showIndex acid lang = do entries <- query' acid (LatestEntries lang) (page :: Maybe Int) <- optional $ lookRead "page" - ok $ toResponse $ blogTemplate lang "" $ + ok $ toResponse $ blogTemplate lang "" $ renderEntries False (eDrop page entries) (topText lang) (Just $ showLinks page lang) where eDrop :: Maybe Int -> [a] -> [a] eDrop (Just i) = drop ((i-1) * 6) - eDrop Nothing = drop 0 + eDrop Nothing = drop 0 showRSS :: AcidState Blog -> BlogLang -> ServerPart Response showRSS acid lang = do @@ -159,8 +166,8 @@ addComment acid lang captchakey eId = do response <- look "recaptcha_response_field" (userIp, _) <- askRq >>= return . rqPeer validation <- liftIO $ validateCaptcha captchakey userIp challenge response - case validation of - Right _ -> update' acid (AddComment eId nComment) + case validation of + Right _ -> update' acid (AddComment eId nComment) >> seeOther ("/" ++ show lang ++ "/" ++ show eId) (toResponse()) Left _ -> (liftIO $ putStrLn "Captcha failed") >> seeOther ("/" ++ show lang ++ "/" ++ show eId) (toResponse()) @@ -172,7 +179,7 @@ commentEscape = newlineEscape . ltEscape . gtEscape . ampEscape ltEscape = T.replace "<" "<" gtEscape = T.replace ">" ">" -{- ADMIN stuff -} +{- ADMIN stuff -} postEntry :: AcidState Blog -> ServerPart Response postEntry acid = do diff --git a/src/RSS.hs b/src/RSS.hs index 05ae40ece5..50531c3f80 100644 --- a/src/RSS.hs +++ b/src/RSS.hs @@ -2,15 +2,15 @@ module RSS (renderFeed) where -import qualified Data.Text as T +import qualified Data.Text as T -import Data.Maybe (fromMaybe) -import Data.Time (getCurrentTime, UTCTime) -import Network.URI -import Text.RSS +import Data.Maybe (fromMaybe) +import Data.Time (UTCTime, getCurrentTime) +import Network.URI +import Text.RSS -import Locales -import BlogDB hiding (Title) +import BlogDB hiding (Title) +import Locales createChannel :: BlogLang -> UTCTime -> [ChannelElem] createChannel l now = [ Language $ show l @@ -23,7 +23,7 @@ createRSS :: BlogLang -> UTCTime -> [Item] -> RSS createRSS l t i = RSS (rssTitle l) (rssLink l) (rssDesc l) (createChannel l t) i createItem :: Entry -> Item -createItem Entry{..} = [ Title $ T.unpack title +createItem Entry{..} = [ Title $ T.unpack title , Link $ makeLink lang entryId , Description $ T.unpack btext , PubDate edate] @@ -39,4 +39,4 @@ createFeed :: BlogLang -> [Entry] -> IO RSS createFeed l e = getCurrentTime >>= (\t -> return $ createRSS l t $ createItems e ) renderFeed :: BlogLang -> [Entry] -> IO String -renderFeed l e = createFeed l e >>= (\feed -> return $ showXML $ rssToXML feed) \ No newline at end of file +renderFeed l e = createFeed l e >>= (\feed -> return $ showXML $ rssToXML feed) -- cgit 1.4.1 From c5206173e6445507f8468457ac5534065e8b9909 Mon Sep 17 00:00:00 2001 From: "\"Vincent Ambo ext:(%22)" <tazjin@gmail.com> Date: Sun, 28 Apr 2013 14:44:14 +0200 Subject: ran hlint --- src/Blog.hs | 2 +- src/BlogDB.hs | 8 ++++---- src/Main.hs | 43 +++++++++++++++++++++---------------------- src/RSS.hs | 13 +++++++------ 4 files changed, 33 insertions(+), 33 deletions(-) diff --git a/src/Blog.hs b/src/Blog.hs index 2c1a546a2a..e999d18fad 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -163,7 +163,7 @@ renderEntry e@Entry{..} = [shamlet| ^{renderCommentBox lang entryId} |] where - woText = flip T.append author $ T.pack $ (formatTime defaultTimeLocale (eTimeFormat lang) edate) + woText = flip T.append author $ T.pack $ formatTime defaultTimeLocale (eTimeFormat lang) edate renderComments :: [Comment] -> BlogLang -> Html renderComments [] lang = [shamlet|<li>#{noComments lang}|] diff --git a/src/BlogDB.hs b/src/BlogDB.hs index b2551ecc3d..c054d7f17d 100644 --- a/src/BlogDB.hs +++ b/src/BlogDB.hs @@ -97,7 +97,7 @@ instance Indexable Entry where , ixFun $ \e -> [ MText $ mtext e] , ixFun $ \e -> [ EDate $ edate e] , ixFun $ \e -> map Tag (tags e) - , ixFun $ comments + , ixFun comments ] data User = User { @@ -209,13 +209,13 @@ getUser uN = do b@Blog{..} <- ask return $ getOne $ blogUsers @= uN -checkUser :: Username -> String -> Query Blog (Bool) +checkUser :: Username -> String -> Query Blog Bool checkUser uN pw = do b@Blog{..} <- ask let user = getOne $ blogUsers @= uN case user of Nothing -> return False - (Just u) -> return $ (password u) == hashString pw + (Just u) -> return $ password u == hashString pw -- various functions hashString :: String -> ByteString @@ -251,7 +251,7 @@ flushSessions :: IO () flushSessions = do tbDir <- getEnv "TAZBLOG" acid <- openLocalStateFrom (tbDir ++ "/BlogState") initialBlogState - update' acid (ClearSessions) + update' acid ClearSessions closeAcidState acid archiveState :: IO () diff --git a/src/Main.hs b/src/Main.hs index 09215ba9d5..3f806328e5 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -12,7 +12,7 @@ module Main where import Control.Applicative (optional, pure, (<$>), (<*>)) import Control.Exception (bracket) -import Control.Monad (msum, mzero, unless, when) +import Control.Monad (liftM, msum, mzero, unless, when) import Control.Monad.IO.Class (liftIO) import Control.Monad.Reader (ask) import Control.Monad.State (get, put) @@ -53,14 +53,14 @@ defineOptions "MainOptions" $ do "The port to run the web server on. Default is 8000" tmpPolicy :: BodyPolicy -tmpPolicy = (defaultBodyPolicy "./tmp/" 0 200000 1000) +tmpPolicy = defaultBodyPolicy "./tmp/" 0 200000 1000 main :: IO() main = do putStrLn ("TazBlog " ++ version ++ " in Haskell starting") runCommand $ \opts args -> bracket (openLocalStateFrom (optState opts ++ "BlogState") initialBlogState) - (createCheckpointAndClose) + createCheckpointAndClose (\acid -> simpleHTTP nullConf {port = optPort opts} $ tazBlog acid $ optCaptcha opts) tazBlog :: AcidState Blog -> String -> ServerPart Response @@ -74,13 +74,13 @@ tazBlog acid captchakey = do , dir "res" $ serveDirectory DisableBrowsing [] "../res" , dir "notice" $ ok $ toResponse showSiteNotice {- :Admin handlers -} - , do dirs "admin/postentry" $ nullDir + , do dirs "admin/postentry" nullDir guardSession acid postEntry acid - , do dirs "admin/entrylist" $ dir (show DE) $ nullDir + , do dirs "admin/entrylist" $ dir (show DE) nullDir guardSession acid entryList acid DE - , do dirs "admin/entrylist" $ dir (show EN) $ nullDir + , do dirs "admin/entrylist" $ dir (show EN) nullDir guardSession acid entryList acid EN , do guardSession acid @@ -90,16 +90,16 @@ tazBlog acid captchakey = do , do guardSession acid dirs "admin/cdelete" $ path $ \(eId :: Integer) -> path $ \(cId :: String) -> deleteComment acid (EntryId eId) cId - , do dir "admin" $ nullDir + , do dir "admin" nullDir guardSession acid ok $ toResponse $ adminIndex ("tazjin" :: Text) - , dir "admin" $ ok $ toResponse $ adminLogin + , dir "admin" $ ok $ toResponse adminLogin , dir "dologin" $ processLogin acid - , do dirs "static/blogv34.css" $ nullDir + , do dirs "static/blogv34.css" nullDir setHeaderM "content-type" "text/css" setHeaderM "cache-control" "max-age=630720000" setHeaderM "expires" "Tue, 20 Jan 2037 04:20:42 GMT" - ok $ toResponse $ blogStyle + ok $ toResponse blogStyle , do setHeaderM "cache-control" "max-age=630720000" setHeaderM "expires" "Tue, 20 Jan 2037 04:20:42 GMT" dir "static" $ serveDirectory DisableBrowsing [] "../res" @@ -156,7 +156,7 @@ showRSS acid lang = do addComment :: AcidState Blog -> BlogLang -> String -> EntryId -> ServerPart Response addComment acid lang captchakey eId = do - now <- liftIO $ getCurrentTime >>= return + now <- liftIO getCurrentTime nCtext <- lookText' "ctext" nComment <- Comment <$> pure now <*> lookText' "cname" @@ -164,12 +164,12 @@ addComment acid lang captchakey eId = do -- captcha verification challenge <- look "recaptcha_challenge_field" response <- look "recaptcha_response_field" - (userIp, _) <- askRq >>= return . rqPeer + (userIp, _) <- liftM rqPeer askRq -- FIXME askRq >>= return . rqPeer validation <- liftIO $ validateCaptcha captchakey userIp challenge response case validation of Right _ -> update' acid (AddComment eId nComment) >> seeOther ("/" ++ show lang ++ "/" ++ show eId) (toResponse()) - Left _ -> (liftIO $ putStrLn "Captcha failed") >> seeOther ("/" ++ show lang ++ "/" ++ show eId) (toResponse()) + Left _ -> liftIO (putStrLn "Captcha failed") >> seeOther ("/" ++ show lang ++ "/" ++ show eId) (toResponse()) commentEscape :: Text -> Text commentEscape = newlineEscape . ltEscape . gtEscape . ampEscape @@ -184,7 +184,7 @@ commentEscape = newlineEscape . ltEscape . gtEscape . ampEscape postEntry :: AcidState Blog -> ServerPart Response postEntry acid = do decodeBody tmpPolicy - now <- liftIO $ getCurrentTime + now <- liftIO getCurrentTime let eId = timeToId now lang <- look "lang" nBtext <- lookText' "btext" @@ -245,16 +245,15 @@ guardSession :: AcidState Blog -> ServerPartT IO () guardSession acid = do (sId :: Text) <- readCookieValue "session" (uName :: Text) <- readCookieValue "sUser" - now <- liftIO $ getCurrentTime + now <- liftIO getCurrentTime mS <- query' acid (GetSession $ SessionID sId) case mS of Nothing -> mzero - (Just Session{..}) -> unless (and [ uName == username user - , sessionTimeDiff now sdate]) - mzero + (Just Session{..}) -> unless ((uName == username user) && sessionTimeDiff now sdate) + mzero where sessionTimeDiff :: UTCTime -> UTCTime -> Bool - sessionTimeDiff now sdate = (diffUTCTime now sdate) < 43200 + sessionTimeDiff now sdate = diffUTCTime now sdate < 43200 processLogin :: AcidState Blog -> ServerPart Response @@ -263,9 +262,9 @@ processLogin acid = do account <- lookText' "account" password <- look "password" login <- query' acid (CheckUser (Username account) password) - if' login - (createSession account) - (ok $ toResponse $ adminLogin) + if login + then createSession account + else ok $ toResponse adminLogin where createSession account = do now <- liftIO getCurrentTime diff --git a/src/RSS.hs b/src/RSS.hs index 50531c3f80..045702ece4 100644 --- a/src/RSS.hs +++ b/src/RSS.hs @@ -2,14 +2,15 @@ module RSS (renderFeed) where -import qualified Data.Text as T +import qualified Data.Text as T -import Data.Maybe (fromMaybe) -import Data.Time (UTCTime, getCurrentTime) +import Control.Monad (liftM) +import Data.Maybe (fromMaybe) +import Data.Time (UTCTime, getCurrentTime) import Network.URI import Text.RSS -import BlogDB hiding (Title) +import BlogDB hiding (Title) import Locales createChannel :: BlogLang -> UTCTime -> [ChannelElem] @@ -20,7 +21,7 @@ createChannel l now = [ Language $ show l ] createRSS :: BlogLang -> UTCTime -> [Item] -> RSS -createRSS l t i = RSS (rssTitle l) (rssLink l) (rssDesc l) (createChannel l t) i +createRSS l t = RSS (rssTitle l) (rssLink l) (rssDesc l) (createChannel l t) createItem :: Entry -> Item createItem Entry{..} = [ Title $ T.unpack title @@ -39,4 +40,4 @@ createFeed :: BlogLang -> [Entry] -> IO RSS createFeed l e = getCurrentTime >>= (\t -> return $ createRSS l t $ createItems e ) renderFeed :: BlogLang -> [Entry] -> IO String -renderFeed l e = createFeed l e >>= (\feed -> return $ showXML $ rssToXML feed) +renderFeed l e = liftM (showXML . rssToXML) (createFeed l e) -- cgit 1.4.1 From 92f4aecbeee38f44b8f842df936d5633613f35a9 Mon Sep 17 00:00:00 2001 From: "\"Vincent Ambo ext:(%22)" <tazjin@gmail.com> Date: Sun, 28 Apr 2013 14:49:38 +0200 Subject: * version bump to 3.5 --- TazBlog.cabal | 2 +- src/Blog.hs | 1 - src/Locales.hs | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/TazBlog.cabal b/TazBlog.cabal index 63f9ad75e0..1295363f60 100644 --- a/TazBlog.cabal +++ b/TazBlog.cabal @@ -1,5 +1,5 @@ Name: TazBlog -Version: 3.4 +Version: 3.5 Synopsis: Tazjin's Blog License-file: LICENSE Author: Vincent Ambo diff --git a/src/Blog.hs b/src/Blog.hs index e999d18fad..83a70bdfb7 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -40,7 +40,6 @@ show' = pack . show markdownCutoff :: UTCTime markdownCutoff = fromJust $ parseTime defaultTimeLocale "%s" "1367149834" -data BlogURL = BlogURL -- blog CSS (admin is still static) stylesheetSource = $(luciusFile "res/blogstyle.lucius") diff --git a/src/Locales.hs b/src/Locales.hs index 3514859437..035ecd0030 100644 --- a/src/Locales.hs +++ b/src/Locales.hs @@ -18,7 +18,7 @@ import BlogDB (BlogLang (..)) data BlogError = NotFound | DBError -version = "3.4" +version = "3.5" allLang = [EN, DE] -- cgit 1.4.1 From 9af249c0b25a12c00d04bf9601fbd48c30cddba4 Mon Sep 17 00:00:00 2001 From: "\"Vincent Ambo ext:(%22)" <tazjin@gmail.com> Date: Sun, 28 Apr 2013 23:07:56 +0200 Subject: * started moving blog to bootstrap, but keeping the design. Currently far from done --- src/Blog.hs | 91 +++++++++++++++++++++++++++++++++++++++------------------- src/Locales.hs | 4 +-- src/Main.hs | 2 +- 3 files changed, 64 insertions(+), 33 deletions(-) diff --git a/src/Blog.hs b/src/Blog.hs index 83a70bdfb7..2011a0fb27 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -42,21 +42,13 @@ markdownCutoff = fromJust $ parseTime defaultTimeLocale "%s" "1367149834" -- blog CSS (admin is still static) -stylesheetSource = $(luciusFile "res/blogstyle.lucius") +stylesheetSource = $(luciusFile "../res/blogbs.lucius") blogStyle = renderCssUrl undefined stylesheetSource --- blog HTML -blogTemplate :: BlogLang -> Text -> Html -> Html -blogTemplate lang t_append body = [shamlet| -$doctype 5 - <head> - <title>#{blogTitle lang t_append} - <link rel="stylesheet" type="text/css" href="/static/blogv34.css" media="all"> - <link rel="alternate" type="application/rss+xml" title="RSS-Feed" href=#{rssUrl}> - <meta http-equiv="content-type" content="text/html;charset=UTF-8"> - <body> +-- <link rel="stylesheet" type="text/css" href="/static/blogv34.css" media="all"> + +{- <div class="header"> - <a class="btitle" href=#{append "/" (show' lang)}>#{blogTitle lang empty} <p style="clear: both;"> <span class="contacts" id="cosx">^{contactInfo} <span class="righttext">^{preEscapedToHtml $ rightText lang} @@ -66,6 +58,36 @@ $doctype 5 ^{showFooter lang $ pack version} <div class="centerbox"> <span style="font-size:17px;font-family:Helvetica;">ಠ_ಠ +-} + +-- blog HTML +blogTemplate :: BlogLang -> Text -> Html -> Html +blogTemplate lang t_append body = [shamlet| +$doctype 5 + <head> + <title>#{blogTitle lang t_append} + <link rel="stylesheet" type="text/css" href="/static/bootstrap.css" media="all"> + <link rel="stylesheet" type="text/css" href="/static/blogv300.css" media="all"> + <link rel="alternate" type="application/rss+xml" title="RSS-Feed" href=#{rssUrl}> + <meta http-equiv="content-type" content="text/html;charset=UTF-8"> + <body> + <div .container .header> + <div .row> + <div .span12 .blogtitle> + <a class="btitle" href=#{append "/" (show' lang)}>#{blogTitle lang empty} + <div .row> + <br> + <div .span6> + <span .contacts #cosx>^{contactInfo} + <div .span6> + <span .righttext>^{preEscapedToHtml $ rightText lang} + <div .container> + ^{body} + <div .container> + <footer> + ^{showFooter lang $ pack version} + <div class="centerbox"> + <span style="font-size:17px;font-family:Helvetica;">ಠ_ಠ |] where rssUrl = T.concat ["/", show' lang, "/rss.xml"] @@ -98,23 +120,32 @@ renderEntryMarkdown = markdown def {msXssProtect = False} . fromStrict renderEntries :: Bool -> [Entry] -> Text -> Maybe Html -> Html renderEntries showAll entries topText footerLinks = [shamlet| -<span class="innerTitle">#{topText} -<div class="innerContainer"> - <ul style="max-width:57em;"> - $forall entry <- elist - <li> - $if (isEntryMarkdown entry) - <a href=#{linkElems entry}>#{linkText $ length $ comments entry} - <b>#{title entry} - ^{renderEntryMarkdown $ append " " $ btext entry} - $else - <a href=#{linkElems entry}>#{linkText $ length $ comments entry} - ^{preEscapedToHtml $ append " " $ btext entry} - $if ((/=) (mtext entry) empty) - <p><a href=#{linkElems entry}>#{readMore $ lang entry} - $else - <br>  - $maybe links <- footerLinks +<div .row> + <div .span12> + <p> + <span class="innerTitle">#{topText} +$forall entry <- elist + <div .row > + <div .span2> + <a #bar href=#{linkElems entry}> + <b>#{title entry} + <br> + <i>#{pack $ formatTime defaultTimeLocale "%Y-%M-%d" $ edate entry} + <br> + #{linkText $ length $ comments entry} + #{cHead $ lang entry} + <div .span10> + $if (isEntryMarkdown entry) + ^{renderEntryMarkdown $ append " " $ btext entry} + $else + ^{preEscapedToHtml $ append " " $ btext entry} + $if ((/=) (mtext entry) empty) + <p> + <a #foo href=#{linkElems entry}>#{readMore $ lang entry} + $else + <br>  + <hr> +$maybe links <- footerLinks ^{links} |] where @@ -157,7 +188,7 @@ renderEntry e@Entry{..} = [shamlet| ^{preEscapedToHtml $ btext} <p>^{preEscapedToHtml $ mtext} <div class="innerBoxComments"> - <div class="cHead">#{cHead lang} + <div class="cHead">#{cHead lang}: <ul style="max-width:57em;">#{renderComments comments lang} ^{renderCommentBox lang entryId} |] diff --git a/src/Locales.hs b/src/Locales.hs index 035ecd0030..09911c302e 100644 --- a/src/Locales.hs +++ b/src/Locales.hs @@ -109,8 +109,8 @@ noComments DE = " Keine Kommentare" noComments EN = " No comments yet" cHead :: BlogLang -> Text -cHead DE = "Kommentare:" -cHead EN = "Comments:" +cHead DE = "Kommentare" +cHead EN = "Comments" cwHead :: BlogLang -> Text cwHead DE = "Kommentieren:" diff --git a/src/Main.hs b/src/Main.hs index 3f806328e5..82e6c5663a 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -95,7 +95,7 @@ tazBlog acid captchakey = do ok $ toResponse $ adminIndex ("tazjin" :: Text) , dir "admin" $ ok $ toResponse adminLogin , dir "dologin" $ processLogin acid - , do dirs "static/blogv34.css" nullDir + , do dirs "static/blogv300.css" nullDir setHeaderM "content-type" "text/css" setHeaderM "cache-control" "max-age=630720000" setHeaderM "expires" "Tue, 20 Jan 2037 04:20:42 GMT" -- cgit 1.4.1 From 9904c6117ffc5cd32d3441191a33c7bc49e5f1f4 Mon Sep 17 00:00:00 2001 From: "\"Vincent Ambo ext:(%22)" <tazjin@gmail.com> Date: Sun, 28 Apr 2013 23:08:16 +0200 Subject: * adding bootstrap.css --- res/bootstrap.css | 6155 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 6155 insertions(+) create mode 100644 res/bootstrap.css diff --git a/res/bootstrap.css b/res/bootstrap.css new file mode 100644 index 0000000000..5233f1312d --- /dev/null +++ b/res/bootstrap.css @@ -0,0 +1,6155 @@ +/*! + * Bootstrap v2.3.1 + * + * Copyright 2012 Twitter, Inc + * Licensed under the Apache License v2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Designed and built with all the love in the world @twitter by @mdo and @fat. + */ + +.clearfix { + *zoom: 1; +} + +.clearfix:before, +.clearfix:after { + display: table; + line-height: 0; + content: ""; +} + +.clearfix:after { + clear: both; +} + +.hide-text { + font: 0/0 a; + color: transparent; + text-shadow: none; + background-color: transparent; + border: 0; +} + +.input-block-level { + display: block; + width: 100%; + min-height: 30px; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +nav, +section { + display: block; +} + +audio, +canvas, +video { + display: inline-block; + *display: inline; + *zoom: 1; +} + +audio:not([controls]) { + display: none; +} + +html { + font-size: 100%; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; +} + +a:focus { + outline: thin dotted #333; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +a:hover, +a:active { + outline: 0; +} + +sub, +sup { + position: relative; + font-size: 75%; + line-height: 0; + vertical-align: baseline; +} + +sup { + top: -0.5em; +} + +sub { + bottom: -0.25em; +} + +img { + width: auto\9; + height: auto; + max-width: 100%; + vertical-align: middle; + border: 0; + -ms-interpolation-mode: bicubic; +} + +#map_canvas img, +.google-maps img { + max-width: none; +} + +button, +input, +select, +textarea { + margin: 0; + font-size: 100%; + vertical-align: middle; +} + +button, +input { + *overflow: visible; + line-height: normal; +} + +button::-moz-focus-inner, +input::-moz-focus-inner { + padding: 0; + border: 0; +} + +button, +html input[type="button"], +input[type="reset"], +input[type="submit"] { + cursor: pointer; + -webkit-appearance: button; +} + +label, +select, +button, +input[type="button"], +input[type="reset"], +input[type="submit"], +input[type="radio"], +input[type="checkbox"] { + cursor: pointer; +} + +input[type="search"] { + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; + -webkit-appearance: textfield; +} + +input[type="search"]::-webkit-search-decoration, +input[type="search"]::-webkit-search-cancel-button { + -webkit-appearance: none; +} + +textarea { + overflow: auto; + vertical-align: top; +} + +@media print { + * { + color: #000 !important; + text-shadow: none !important; + background: transparent !important; + box-shadow: none !important; + } + a, + a:visited { + text-decoration: underline; + } + a[href]:after { + content: " (" attr(href) ")"; + } + abbr[title]:after { + content: " (" attr(title) ")"; + } + .ir a:after, + a[href^="javascript:"]:after, + a[href^="#"]:after { + content: ""; + } + pre, + blockquote { + border: 1px solid #999; + page-break-inside: avoid; + } + thead { + display: table-header-group; + } + tr, + img { + page-break-inside: avoid; + } + img { + max-width: 100% !important; + } + @page { + margin: 0.5cm; + } + p, + h2, + h3 { + orphans: 3; + widows: 3; + } + h2, + h3 { + page-break-after: avoid; + } +} + +body { + margin: 0; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 14px; + line-height: 20px; + color: #333333; + background-color: #ffffff; +} + +a { + color: #0088cc; + text-decoration: none; +} + +a:hover, +a:focus { + color: #005580; + text-decoration: underline; +} + +.img-rounded { + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; +} + +.img-polaroid { + padding: 4px; + background-color: #fff; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, 0.2); + -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.img-circle { + -webkit-border-radius: 500px; + -moz-border-radius: 500px; + border-radius: 500px; +} + +.row { + margin-left: -20px; + *zoom: 1; +} + +.row:before, +.row:after { + display: table; + line-height: 0; + content: ""; +} + +.row:after { + clear: both; +} + +[class*="span"] { + float: left; + min-height: 1px; + margin-left: 20px; +} + +.container, +.navbar-static-top .container, +.navbar-fixed-top .container, +.navbar-fixed-bottom .container { + width: 940px; +} + +.span12 { + width: 940px; +} + +.span11 { + width: 860px; +} + +.span10 { + width: 780px; +} + +.span9 { + width: 700px; +} + +.span8 { + width: 620px; +} + +.span7 { + width: 540px; +} + +.span6 { + width: 460px; +} + +.span5 { + width: 380px; +} + +.span4 { + width: 300px; +} + +.span3 { + width: 220px; +} + +.span2 { + width: 140px; +} + +.span1 { + width: 60px; +} + +.offset12 { + margin-left: 980px; +} + +.offset11 { + margin-left: 900px; +} + +.offset10 { + margin-left: 820px; +} + +.offset9 { + margin-left: 740px; +} + +.offset8 { + margin-left: 660px; +} + +.offset7 { + margin-left: 580px; +} + +.offset6 { + margin-left: 500px; +} + +.offset5 { + margin-left: 420px; +} + +.offset4 { + margin-left: 340px; +} + +.offset3 { + margin-left: 260px; +} + +.offset2 { + margin-left: 180px; +} + +.offset1 { + margin-left: 100px; +} + +.row-fluid { + width: 100%; + *zoom: 1; +} + +.row-fluid:before, +.row-fluid:after { + display: table; + line-height: 0; + content: ""; +} + +.row-fluid:after { + clear: both; +} + +.row-fluid [class*="span"] { + display: block; + float: left; + width: 100%; + min-height: 30px; + margin-left: 2.127659574468085%; + *margin-left: 2.074468085106383%; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +.row-fluid [class*="span"]:first-child { + margin-left: 0; +} + +.row-fluid .controls-row [class*="span"] + [class*="span"] { + margin-left: 2.127659574468085%; +} + +.row-fluid .span12 { + width: 100%; + *width: 99.94680851063829%; +} + +.row-fluid .span11 { + width: 91.48936170212765%; + *width: 91.43617021276594%; +} + +.row-fluid .span10 { + width: 82.97872340425532%; + *width: 82.92553191489361%; +} + +.row-fluid .span9 { + width: 74.46808510638297%; + *width: 74.41489361702126%; +} + +.row-fluid .span8 { + width: 65.95744680851064%; + *width: 65.90425531914893%; +} + +.row-fluid .span7 { + width: 57.44680851063829%; + *width: 57.39361702127659%; +} + +.row-fluid .span6 { + width: 48.93617021276595%; + *width: 48.88297872340425%; +} + +.row-fluid .span5 { + width: 40.42553191489362%; + *width: 40.37234042553192%; +} + +.row-fluid .span4 { + width: 31.914893617021278%; + *width: 31.861702127659576%; +} + +.row-fluid .span3 { + width: 23.404255319148934%; + *width: 23.351063829787233%; +} + +.row-fluid .span2 { + width: 14.893617021276595%; + *width: 14.840425531914894%; +} + +.row-fluid .span1 { + width: 6.382978723404255%; + *width: 6.329787234042553%; +} + +.row-fluid .offset12 { + margin-left: 104.25531914893617%; + *margin-left: 104.14893617021275%; +} + +.row-fluid .offset12:first-child { + margin-left: 102.12765957446808%; + *margin-left: 102.02127659574467%; +} + +.row-fluid .offset11 { + margin-left: 95.74468085106382%; + *margin-left: 95.6382978723404%; +} + +.row-fluid .offset11:first-child { + margin-left: 93.61702127659574%; + *margin-left: 93.51063829787232%; +} + +.row-fluid .offset10 { + margin-left: 87.23404255319149%; + *margin-left: 87.12765957446807%; +} + +.row-fluid .offset10:first-child { + margin-left: 85.1063829787234%; + *margin-left: 84.99999999999999%; +} + +.row-fluid .offset9 { + margin-left: 78.72340425531914%; + *margin-left: 78.61702127659572%; +} + +.row-fluid .offset9:first-child { + margin-left: 76.59574468085106%; + *margin-left: 76.48936170212764%; +} + +.row-fluid .offset8 { + margin-left: 70.2127659574468%; + *margin-left: 70.10638297872339%; +} + +.row-fluid .offset8:first-child { + margin-left: 68.08510638297872%; + *margin-left: 67.9787234042553%; +} + +.row-fluid .offset7 { + margin-left: 61.70212765957446%; + *margin-left: 61.59574468085106%; +} + +.row-fluid .offset7:first-child { + margin-left: 59.574468085106375%; + *margin-left: 59.46808510638297%; +} + +.row-fluid .offset6 { + margin-left: 53.191489361702125%; + *margin-left: 53.085106382978715%; +} + +.row-fluid .offset6:first-child { + margin-left: 51.063829787234035%; + *margin-left: 50.95744680851063%; +} + +.row-fluid .offset5 { + margin-left: 44.68085106382979%; + *margin-left: 44.57446808510638%; +} + +.row-fluid .offset5:first-child { + margin-left: 42.5531914893617%; + *margin-left: 42.4468085106383%; +} + +.row-fluid .offset4 { + margin-left: 36.170212765957444%; + *margin-left: 36.06382978723405%; +} + +.row-fluid .offset4:first-child { + margin-left: 34.04255319148936%; + *margin-left: 33.93617021276596%; +} + +.row-fluid .offset3 { + margin-left: 27.659574468085104%; + *margin-left: 27.5531914893617%; +} + +.row-fluid .offset3:first-child { + margin-left: 25.53191489361702%; + *margin-left: 25.425531914893618%; +} + +.row-fluid .offset2 { + margin-left: 19.148936170212764%; + *margin-left: 19.04255319148936%; +} + +.row-fluid .offset2:first-child { + margin-left: 17.02127659574468%; + *margin-left: 16.914893617021278%; +} + +.row-fluid .offset1 { + margin-left: 10.638297872340425%; + *margin-left: 10.53191489361702%; +} + +.row-fluid .offset1:first-child { + margin-left: 8.51063829787234%; + *margin-left: 8.404255319148938%; +} + +[class*="span"].hide, +.row-fluid [class*="span"].hide { + display: none; +} + +[class*="span"].pull-right, +.row-fluid [class*="span"].pull-right { + float: right; +} + +.container { + margin-right: auto; + margin-left: auto; + *zoom: 1; +} + +.container:before, +.container:after { + display: table; + line-height: 0; + content: ""; +} + +.container:after { + clear: both; +} + +.container-fluid { + padding-right: 20px; + padding-left: 20px; + *zoom: 1; +} + +.container-fluid:before, +.container-fluid:after { + display: table; + line-height: 0; + content: ""; +} + +.container-fluid:after { + clear: both; +} + +p { + margin: 0 0 10px; +} + +.lead { + margin-bottom: 20px; + font-size: 21px; + font-weight: 200; + line-height: 30px; +} + +small { + font-size: 85%; +} + +strong { + font-weight: bold; +} + +em { + font-style: italic; +} + +cite { + font-style: normal; +} + +.muted { + color: #999999; +} + +a.muted:hover, +a.muted:focus { + color: #808080; +} + +.text-warning { + color: #c09853; +} + +a.text-warning:hover, +a.text-warning:focus { + color: #a47e3c; +} + +.text-error { + color: #b94a48; +} + +a.text-error:hover, +a.text-error:focus { + color: #953b39; +} + +.text-info { + color: #3a87ad; +} + +a.text-info:hover, +a.text-info:focus { + color: #2d6987; +} + +.text-success { + color: #468847; +} + +a.text-success:hover, +a.text-success:focus { + color: #356635; +} + +.text-left { + text-align: left; +} + +.text-right { + text-align: right; +} + +.text-center { + text-align: center; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + margin: 10px 0; + font-family: inherit; + font-weight: bold; + line-height: 20px; + color: inherit; + text-rendering: optimizelegibility; +} + +h1 small, +h2 small, +h3 small, +h4 small, +h5 small, +h6 small { + font-weight: normal; + line-height: 1; + color: #999999; +} + +h1, +h2, +h3 { + line-height: 40px; +} + +h1 { + font-size: 38.5px; +} + +h2 { + font-size: 31.5px; +} + +h3 { + font-size: 24.5px; +} + +h4 { + font-size: 17.5px; +} + +h5 { + font-size: 14px; +} + +h6 { + font-size: 11.9px; +} + +h1 small { + font-size: 24.5px; +} + +h2 small { + font-size: 17.5px; +} + +h3 small { + font-size: 14px; +} + +h4 small { + font-size: 14px; +} + +.page-header { + padding-bottom: 9px; + margin: 20px 0 30px; + border-bottom: 1px solid #eeeeee; +} + +ul, +ol { + padding: 0; + margin: 0 0 10px 25px; +} + +ul ul, +ul ol, +ol ol, +ol ul { + margin-bottom: 0; +} + +li { + line-height: 20px; +} + +ul.unstyled, +ol.unstyled { + margin-left: 0; + list-style: none; +} + +ul.inline, +ol.inline { + margin-left: 0; + list-style: none; +} + +ul.inline > li, +ol.inline > li { + display: inline-block; + *display: inline; + padding-right: 5px; + padding-left: 5px; + *zoom: 1; +} + +dl { + margin-bottom: 20px; +} + +dt, +dd { + line-height: 20px; +} + +dt { + font-weight: bold; +} + +dd { + margin-left: 10px; +} + +.dl-horizontal { + *zoom: 1; +} + +.dl-horizontal:before, +.dl-horizontal:after { + display: table; + line-height: 0; + content: ""; +} + +.dl-horizontal:after { + clear: both; +} + +.dl-horizontal dt { + float: left; + width: 160px; + overflow: hidden; + clear: left; + text-align: right; + text-overflow: ellipsis; + white-space: nowrap; +} + +.dl-horizontal dd { + margin-left: 180px; +} + +hr { + margin: 20px 0; + border: 0; + border-top: 1px solid #eeeeee; + border-bottom: 1px solid #ffffff; +} + +abbr[title], +abbr[data-original-title] { + cursor: help; + border-bottom: 1px dotted #999999; +} + +abbr.initialism { + font-size: 90%; + text-transform: uppercase; +} + +blockquote { + padding: 0 0 0 15px; + margin: 0 0 20px; + border-left: 5px solid #eeeeee; +} + +blockquote p { + margin-bottom: 0; + font-size: 17.5px; + font-weight: 300; + line-height: 1.25; +} + +blockquote small { + display: block; + line-height: 20px; + color: #999999; +} + +blockquote small:before { + content: '\2014 \00A0'; +} + +blockquote.pull-right { + float: right; + padding-right: 15px; + padding-left: 0; + border-right: 5px solid #eeeeee; + border-left: 0; +} + +blockquote.pull-right p, +blockquote.pull-right small { + text-align: right; +} + +blockquote.pull-right small:before { + content: ''; +} + +blockquote.pull-right small:after { + content: '\00A0 \2014'; +} + +q:before, +q:after, +blockquote:before, +blockquote:after { + content: ""; +} + +address { + display: block; + margin-bottom: 20px; + font-style: normal; + line-height: 20px; +} + +code, +pre { + padding: 0 3px 2px; + font-family: Monaco, Menlo, Consolas, "Courier New", monospace; + font-size: 12px; + color: #333333; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} + +code { + padding: 2px 4px; + color: #d14; + white-space: nowrap; + background-color: #f7f7f9; + border: 1px solid #e1e1e8; +} + +pre { + display: block; + padding: 9.5px; + margin: 0 0 10px; + font-size: 13px; + line-height: 20px; + word-break: break-all; + word-wrap: break-word; + white-space: pre; + white-space: pre-wrap; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +pre.prettyprint { + margin-bottom: 20px; +} + +pre code { + padding: 0; + color: inherit; + white-space: pre; + white-space: pre-wrap; + background-color: transparent; + border: 0; +} + +.pre-scrollable { + max-height: 340px; + overflow-y: scroll; +} + +form { + margin: 0 0 20px; +} + +fieldset { + padding: 0; + margin: 0; + border: 0; +} + +legend { + display: block; + width: 100%; + padding: 0; + margin-bottom: 20px; + font-size: 21px; + line-height: 40px; + color: #333333; + border: 0; + border-bottom: 1px solid #e5e5e5; +} + +legend small { + font-size: 15px; + color: #999999; +} + +label, +input, +button, +select, +textarea { + font-size: 14px; + font-weight: normal; + line-height: 20px; +} + +input, +button, +select, +textarea { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; +} + +label { + display: block; + margin-bottom: 5px; +} + +select, +textarea, +input[type="text"], +input[type="password"], +input[type="datetime"], +input[type="datetime-local"], +input[type="date"], +input[type="month"], +input[type="time"], +input[type="week"], +input[type="number"], +input[type="email"], +input[type="url"], +input[type="search"], +input[type="tel"], +input[type="color"], +.uneditable-input { + display: inline-block; + height: 20px; + padding: 4px 6px; + margin-bottom: 10px; + font-size: 14px; + line-height: 20px; + color: #555555; + vertical-align: middle; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +input, +textarea, +.uneditable-input { + width: 206px; +} + +textarea { + height: auto; +} + +textarea, +input[type="text"], +input[type="password"], +input[type="datetime"], +input[type="datetime-local"], +input[type="date"], +input[type="month"], +input[type="time"], +input[type="week"], +input[type="number"], +input[type="email"], +input[type="url"], +input[type="search"], +input[type="tel"], +input[type="color"], +.uneditable-input { + background-color: #ffffff; + border: 1px solid #cccccc; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -webkit-transition: border linear 0.2s, box-shadow linear 0.2s; + -moz-transition: border linear 0.2s, box-shadow linear 0.2s; + -o-transition: border linear 0.2s, box-shadow linear 0.2s; + transition: border linear 0.2s, box-shadow linear 0.2s; +} + +textarea:focus, +input[type="text"]:focus, +input[type="password"]:focus, +input[type="datetime"]:focus, +input[type="datetime-local"]:focus, +input[type="date"]:focus, +input[type="month"]:focus, +input[type="time"]:focus, +input[type="week"]:focus, +input[type="number"]:focus, +input[type="email"]:focus, +input[type="url"]:focus, +input[type="search"]:focus, +input[type="tel"]:focus, +input[type="color"]:focus, +.uneditable-input:focus { + border-color: rgba(82, 168, 236, 0.8); + outline: 0; + outline: thin dotted \9; + /* IE6-9 */ + + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); +} + +input[type="radio"], +input[type="checkbox"] { + margin: 4px 0 0; + margin-top: 1px \9; + *margin-top: 0; + line-height: normal; +} + +input[type="file"], +input[type="image"], +input[type="submit"], +input[type="reset"], +input[type="button"], +input[type="radio"], +input[type="checkbox"] { + width: auto; +} + +select, +input[type="file"] { + height: 30px; + /* In IE7, the height of the select element cannot be changed by height, only font-size */ + + *margin-top: 4px; + /* For IE7, add top margin to align select with labels */ + + line-height: 30px; +} + +select { + width: 220px; + background-color: #ffffff; + border: 1px solid #cccccc; +} + +select[multiple], +select[size] { + height: auto; +} + +select:focus, +input[type="file"]:focus, +input[type="radio"]:focus, +input[type="checkbox"]:focus { + outline: thin dotted #333; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +.uneditable-input, +.uneditable-textarea { + color: #999999; + cursor: not-allowed; + background-color: #fcfcfc; + border-color: #cccccc; + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); + -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); +} + +.uneditable-input { + overflow: hidden; + white-space: nowrap; +} + +.uneditable-textarea { + width: auto; + height: auto; +} + +input:-moz-placeholder, +textarea:-moz-placeholder { + color: #999999; +} + +input:-ms-input-placeholder, +textarea:-ms-input-placeholder { + color: #999999; +} + +input::-webkit-input-placeholder, +textarea::-webkit-input-placeholder { + color: #999999; +} + +.radio, +.checkbox { + min-height: 20px; + padding-left: 20px; +} + +.radio input[type="radio"], +.checkbox input[type="checkbox"] { + float: left; + margin-left: -20px; +} + +.controls > .radio:first-child, +.controls > .checkbox:first-child { + padding-top: 5px; +} + +.radio.inline, +.checkbox.inline { + display: inline-block; + padding-top: 5px; + margin-bottom: 0; + vertical-align: middle; +} + +.radio.inline + .radio.inline, +.checkbox.inline + .checkbox.inline { + margin-left: 10px; +} + +.input-mini { + width: 60px; +} + +.input-small { + width: 90px; +} + +.input-medium { + width: 150px; +} + +.input-large { + width: 210px; +} + +.input-xlarge { + width: 270px; +} + +.input-xxlarge { + width: 530px; +} + +input[class*="span"], +select[class*="span"], +textarea[class*="span"], +.uneditable-input[class*="span"], +.row-fluid input[class*="span"], +.row-fluid select[class*="span"], +.row-fluid textarea[class*="span"], +.row-fluid .uneditable-input[class*="span"] { + float: none; + margin-left: 0; +} + +.input-append input[class*="span"], +.input-append .uneditable-input[class*="span"], +.input-prepend input[class*="span"], +.input-prepend .uneditable-input[class*="span"], +.row-fluid input[class*="span"], +.row-fluid select[class*="span"], +.row-fluid textarea[class*="span"], +.row-fluid .uneditable-input[class*="span"], +.row-fluid .input-prepend [class*="span"], +.row-fluid .input-append [class*="span"] { + display: inline-block; +} + +input, +textarea, +.uneditable-input { + margin-left: 0; +} + +.controls-row [class*="span"] + [class*="span"] { + margin-left: 20px; +} + +input.span12, +textarea.span12, +.uneditable-input.span12 { + width: 926px; +} + +input.span11, +textarea.span11, +.uneditable-input.span11 { + width: 846px; +} + +input.span10, +textarea.span10, +.uneditable-input.span10 { + width: 766px; +} + +input.span9, +textarea.span9, +.uneditable-input.span9 { + width: 686px; +} + +input.span8, +textarea.span8, +.uneditable-input.span8 { + width: 606px; +} + +input.span7, +textarea.span7, +.uneditable-input.span7 { + width: 526px; +} + +input.span6, +textarea.span6, +.uneditable-input.span6 { + width: 446px; +} + +input.span5, +textarea.span5, +.uneditable-input.span5 { + width: 366px; +} + +input.span4, +textarea.span4, +.uneditable-input.span4 { + width: 286px; +} + +input.span3, +textarea.span3, +.uneditable-input.span3 { + width: 206px; +} + +input.span2, +textarea.span2, +.uneditable-input.span2 { + width: 126px; +} + +input.span1, +textarea.span1, +.uneditable-input.span1 { + width: 46px; +} + +.controls-row { + *zoom: 1; +} + +.controls-row:before, +.controls-row:after { + display: table; + line-height: 0; + content: ""; +} + +.controls-row:after { + clear: both; +} + +.controls-row [class*="span"], +.row-fluid .controls-row [class*="span"] { + float: left; +} + +.controls-row .checkbox[class*="span"], +.controls-row .radio[class*="span"] { + padding-top: 5px; +} + +input[disabled], +select[disabled], +textarea[disabled], +input[readonly], +select[readonly], +textarea[readonly] { + cursor: not-allowed; + background-color: #eeeeee; +} + +input[type="radio"][disabled], +input[type="checkbox"][disabled], +input[type="radio"][readonly], +input[type="checkbox"][readonly] { + background-color: transparent; +} + +.control-group.warning .control-label, +.control-group.warning .help-block, +.control-group.warning .help-inline { + color: #c09853; +} + +.control-group.warning .checkbox, +.control-group.warning .radio, +.control-group.warning input, +.control-group.warning select, +.control-group.warning textarea { + color: #c09853; +} + +.control-group.warning input, +.control-group.warning select, +.control-group.warning textarea { + border-color: #c09853; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} + +.control-group.warning input:focus, +.control-group.warning select:focus, +.control-group.warning textarea:focus { + border-color: #a47e3c; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e; + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e; +} + +.control-group.warning .input-prepend .add-on, +.control-group.warning .input-append .add-on { + color: #c09853; + background-color: #fcf8e3; + border-color: #c09853; +} + +.control-group.error .control-label, +.control-group.error .help-block, +.control-group.error .help-inline { + color: #b94a48; +} + +.control-group.error .checkbox, +.control-group.error .radio, +.control-group.error input, +.control-group.error select, +.control-group.error textarea { + color: #b94a48; +} + +.control-group.error input, +.control-group.error select, +.control-group.error textarea { + border-color: #b94a48; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} + +.control-group.error input:focus, +.control-group.error select:focus, +.control-group.error textarea:focus { + border-color: #953b39; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392; + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392; +} + +.control-group.error .input-prepend .add-on, +.control-group.error .input-append .add-on { + color: #b94a48; + background-color: #f2dede; + border-color: #b94a48; +} + +.control-group.success .control-label, +.control-group.success .help-block, +.control-group.success .help-inline { + color: #468847; +} + +.control-group.success .checkbox, +.control-group.success .radio, +.control-group.success input, +.control-group.success select, +.control-group.success textarea { + color: #468847; +} + +.control-group.success input, +.control-group.success select, +.control-group.success textarea { + border-color: #468847; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} + +.control-group.success input:focus, +.control-group.success select:focus, +.control-group.success textarea:focus { + border-color: #356635; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b; + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b; +} + +.control-group.success .input-prepend .add-on, +.control-group.success .input-append .add-on { + color: #468847; + background-color: #dff0d8; + border-color: #468847; +} + +.control-group.info .control-label, +.control-group.info .help-block, +.control-group.info .help-inline { + color: #3a87ad; +} + +.control-group.info .checkbox, +.control-group.info .radio, +.control-group.info input, +.control-group.info select, +.control-group.info textarea { + color: #3a87ad; +} + +.control-group.info input, +.control-group.info select, +.control-group.info textarea { + border-color: #3a87ad; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} + +.control-group.info input:focus, +.control-group.info select:focus, +.control-group.info textarea:focus { + border-color: #2d6987; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7ab5d3; + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7ab5d3; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7ab5d3; +} + +.control-group.info .input-prepend .add-on, +.control-group.info .input-append .add-on { + color: #3a87ad; + background-color: #d9edf7; + border-color: #3a87ad; +} + +input:focus:invalid, +textarea:focus:invalid, +select:focus:invalid { + color: #b94a48; + border-color: #ee5f5b; +} + +input:focus:invalid:focus, +textarea:focus:invalid:focus, +select:focus:invalid:focus { + border-color: #e9322d; + -webkit-box-shadow: 0 0 6px #f8b9b7; + -moz-box-shadow: 0 0 6px #f8b9b7; + box-shadow: 0 0 6px #f8b9b7; +} + +.form-actions { + padding: 19px 20px 20px; + margin-top: 20px; + margin-bottom: 20px; + background-color: #f5f5f5; + border-top: 1px solid #e5e5e5; + *zoom: 1; +} + +.form-actions:before, +.form-actions:after { + display: table; + line-height: 0; + content: ""; +} + +.form-actions:after { + clear: both; +} + +.help-block, +.help-inline { + color: #595959; +} + +.help-block { + display: block; + margin-bottom: 10px; +} + +.help-inline { + display: inline-block; + *display: inline; + padding-left: 5px; + vertical-align: middle; + *zoom: 1; +} + +.input-append, +.input-prepend { + display: inline-block; + margin-bottom: 10px; + font-size: 0; + white-space: nowrap; + vertical-align: middle; +} + +.input-append input, +.input-prepend input, +.input-append select, +.input-prepend select, +.input-append .uneditable-input, +.input-prepend .uneditable-input, +.input-append .dropdown-menu, +.input-prepend .dropdown-menu, +.input-append .popover, +.input-prepend .popover { + font-size: 14px; +} + +.input-append input, +.input-prepend input, +.input-append select, +.input-prepend select, +.input-append .uneditable-input, +.input-prepend .uneditable-input { + position: relative; + margin-bottom: 0; + *margin-left: 0; + vertical-align: top; + -webkit-border-radius: 0 4px 4px 0; + -moz-border-radius: 0 4px 4px 0; + border-radius: 0 4px 4px 0; +} + +.input-append input:focus, +.input-prepend input:focus, +.input-append select:focus, +.input-prepend select:focus, +.input-append .uneditable-input:focus, +.input-prepend .uneditable-input:focus { + z-index: 2; +} + +.input-append .add-on, +.input-prepend .add-on { + display: inline-block; + width: auto; + height: 20px; + min-width: 16px; + padding: 4px 5px; + font-size: 14px; + font-weight: normal; + line-height: 20px; + text-align: center; + text-shadow: 0 1px 0 #ffffff; + background-color: #eeeeee; + border: 1px solid #ccc; +} + +.input-append .add-on, +.input-prepend .add-on, +.input-append .btn, +.input-prepend .btn, +.input-append .btn-group > .dropdown-toggle, +.input-prepend .btn-group > .dropdown-toggle { + vertical-align: top; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.input-append .active, +.input-prepend .active { + background-color: #a9dba9; + border-color: #46a546; +} + +.input-prepend .add-on, +.input-prepend .btn { + margin-right: -1px; +} + +.input-prepend .add-on:first-child, +.input-prepend .btn:first-child { + -webkit-border-radius: 4px 0 0 4px; + -moz-border-radius: 4px 0 0 4px; + border-radius: 4px 0 0 4px; +} + +.input-append input, +.input-append select, +.input-append .uneditable-input { + -webkit-border-radius: 4px 0 0 4px; + -moz-border-radius: 4px 0 0 4px; + border-radius: 4px 0 0 4px; +} + +.input-append input + .btn-group .btn:last-child, +.input-append select + .btn-group .btn:last-child, +.input-append .uneditable-input + .btn-group .btn:last-child { + -webkit-border-radius: 0 4px 4px 0; + -moz-border-radius: 0 4px 4px 0; + border-radius: 0 4px 4px 0; +} + +.input-append .add-on, +.input-append .btn, +.input-append .btn-group { + margin-left: -1px; +} + +.input-append .add-on:last-child, +.input-append .btn:last-child, +.input-append .btn-group:last-child > .dropdown-toggle { + -webkit-border-radius: 0 4px 4px 0; + -moz-border-radius: 0 4px 4px 0; + border-radius: 0 4px 4px 0; +} + +.input-prepend.input-append input, +.input-prepend.input-append select, +.input-prepend.input-append .uneditable-input { + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.input-prepend.input-append input + .btn-group .btn, +.input-prepend.input-append select + .btn-group .btn, +.input-prepend.input-append .uneditable-input + .btn-group .btn { + -webkit-border-radius: 0 4px 4px 0; + -moz-border-radius: 0 4px 4px 0; + border-radius: 0 4px 4px 0; +} + +.input-prepend.input-append .add-on:first-child, +.input-prepend.input-append .btn:first-child { + margin-right: -1px; + -webkit-border-radius: 4px 0 0 4px; + -moz-border-radius: 4px 0 0 4px; + border-radius: 4px 0 0 4px; +} + +.input-prepend.input-append .add-on:last-child, +.input-prepend.input-append .btn:last-child { + margin-left: -1px; + -webkit-border-radius: 0 4px 4px 0; + -moz-border-radius: 0 4px 4px 0; + border-radius: 0 4px 4px 0; +} + +.input-prepend.input-append .btn-group:first-child { + margin-left: 0; +} + +input.search-query { + padding-right: 14px; + padding-right: 4px \9; + padding-left: 14px; + padding-left: 4px \9; + /* IE7-8 doesn't have border-radius, so don't indent the padding */ + + margin-bottom: 0; + -webkit-border-radius: 15px; + -moz-border-radius: 15px; + border-radius: 15px; +} + +/* Allow for input prepend/append in search forms */ + +.form-search .input-append .search-query, +.form-search .input-prepend .search-query { + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.form-search .input-append .search-query { + -webkit-border-radius: 14px 0 0 14px; + -moz-border-radius: 14px 0 0 14px; + border-radius: 14px 0 0 14px; +} + +.form-search .input-append .btn { + -webkit-border-radius: 0 14px 14px 0; + -moz-border-radius: 0 14px 14px 0; + border-radius: 0 14px 14px 0; +} + +.form-search .input-prepend .search-query { + -webkit-border-radius: 0 14px 14px 0; + -moz-border-radius: 0 14px 14px 0; + border-radius: 0 14px 14px 0; +} + +.form-search .input-prepend .btn { + -webkit-border-radius: 14px 0 0 14px; + -moz-border-radius: 14px 0 0 14px; + border-radius: 14px 0 0 14px; +} + +.form-search input, +.form-inline input, +.form-horizontal input, +.form-search textarea, +.form-inline textarea, +.form-horizontal textarea, +.form-search select, +.form-inline select, +.form-horizontal select, +.form-search .help-inline, +.form-inline .help-inline, +.form-horizontal .help-inline, +.form-search .uneditable-input, +.form-inline .uneditable-input, +.form-horizontal .uneditable-input, +.form-search .input-prepend, +.form-inline .input-prepend, +.form-horizontal .input-prepend, +.form-search .input-append, +.form-inline .input-append, +.form-horizontal .input-append { + display: inline-block; + *display: inline; + margin-bottom: 0; + vertical-align: middle; + *zoom: 1; +} + +.form-search .hide, +.form-inline .hide, +.form-horizontal .hide { + display: none; +} + +.form-search label, +.form-inline label, +.form-search .btn-group, +.form-inline .btn-group { + display: inline-block; +} + +.form-search .input-append, +.form-inline .input-append, +.form-search .input-prepend, +.form-inline .input-prepend { + margin-bottom: 0; +} + +.form-search .radio, +.form-search .checkbox, +.form-inline .radio, +.form-inline .checkbox { + padding-left: 0; + margin-bottom: 0; + vertical-align: middle; +} + +.form-search .radio input[type="radio"], +.form-search .checkbox input[type="checkbox"], +.form-inline .radio input[type="radio"], +.form-inline .checkbox input[type="checkbox"] { + float: left; + margin-right: 3px; + margin-left: 0; +} + +.control-group { + margin-bottom: 10px; +} + +legend + .control-group { + margin-top: 20px; + -webkit-margin-top-collapse: separate; +} + +.form-horizontal .control-group { + margin-bottom: 20px; + *zoom: 1; +} + +.form-horizontal .control-group:before, +.form-horizontal .control-group:after { + display: table; + line-height: 0; + content: ""; +} + +.form-horizontal .control-group:after { + clear: both; +} + +.form-horizontal .control-label { + float: left; + width: 160px; + padding-top: 5px; + text-align: right; +} + +.form-horizontal .controls { + *display: inline-block; + *padding-left: 20px; + margin-left: 180px; + *margin-left: 0; +} + +.form-horizontal .controls:first-child { + *padding-left: 180px; +} + +.form-horizontal .help-block { + margin-bottom: 0; +} + +.form-horizontal input + .help-block, +.form-horizontal select + .help-block, +.form-horizontal textarea + .help-block, +.form-horizontal .uneditable-input + .help-block, +.form-horizontal .input-prepend + .help-block, +.form-horizontal .input-append + .help-block { + margin-top: 10px; +} + +.form-horizontal .form-actions { + padding-left: 180px; +} + +table { + max-width: 100%; + background-color: transparent; + border-collapse: collapse; + border-spacing: 0; +} + +.table { + width: 100%; + margin-bottom: 20px; +} + +.table th, +.table td { + padding: 8px; + line-height: 20px; + text-align: left; + vertical-align: top; + border-top: 1px solid #dddddd; +} + +.table th { + font-weight: bold; +} + +.table thead th { + vertical-align: bottom; +} + +.table caption + thead tr:first-child th, +.table caption + thead tr:first-child td, +.table colgroup + thead tr:first-child th, +.table colgroup + thead tr:first-child td, +.table thead:first-child tr:first-child th, +.table thead:first-child tr:first-child td { + border-top: 0; +} + +.table tbody + tbody { + border-top: 2px solid #dddddd; +} + +.table .table { + background-color: #ffffff; +} + +.table-condensed th, +.table-condensed td { + padding: 4px 5px; +} + +.table-bordered { + border: 1px solid #dddddd; + border-collapse: separate; + *border-collapse: collapse; + border-left: 0; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.table-bordered th, +.table-bordered td { + border-left: 1px solid #dddddd; +} + +.table-bordered caption + thead tr:first-child th, +.table-bordered caption + tbody tr:first-child th, +.table-bordered caption + tbody tr:first-child td, +.table-bordered colgroup + thead tr:first-child th, +.table-bordered colgroup + tbody tr:first-child th, +.table-bordered colgroup + tbody tr:first-child td, +.table-bordered thead:first-child tr:first-child th, +.table-bordered tbody:first-child tr:first-child th, +.table-bordered tbody:first-child tr:first-child td { + border-top: 0; +} + +.table-bordered thead:first-child tr:first-child > th:first-child, +.table-bordered tbody:first-child tr:first-child > td:first-child, +.table-bordered tbody:first-child tr:first-child > th:first-child { + -webkit-border-top-left-radius: 4px; + border-top-left-radius: 4px; + -moz-border-radius-topleft: 4px; +} + +.table-bordered thead:first-child tr:first-child > th:last-child, +.table-bordered tbody:first-child tr:first-child > td:last-child, +.table-bordered tbody:first-child tr:first-child > th:last-child { + -webkit-border-top-right-radius: 4px; + border-top-right-radius: 4px; + -moz-border-radius-topright: 4px; +} + +.table-bordered thead:last-child tr:last-child > th:first-child, +.table-bordered tbody:last-child tr:last-child > td:first-child, +.table-bordered tbody:last-child tr:last-child > th:first-child, +.table-bordered tfoot:last-child tr:last-child > td:first-child, +.table-bordered tfoot:last-child tr:last-child > th:first-child { + -webkit-border-bottom-left-radius: 4px; + border-bottom-left-radius: 4px; + -moz-border-radius-bottomleft: 4px; +} + +.table-bordered thead:last-child tr:last-child > th:last-child, +.table-bordered tbody:last-child tr:last-child > td:last-child, +.table-bordered tbody:last-child tr:last-child > th:last-child, +.table-bordered tfoot:last-child tr:last-child > td:last-child, +.table-bordered tfoot:last-child tr:last-child > th:last-child { + -webkit-border-bottom-right-radius: 4px; + border-bottom-right-radius: 4px; + -moz-border-radius-bottomright: 4px; +} + +.table-bordered tfoot + tbody:last-child tr:last-child td:first-child { + -webkit-border-bottom-left-radius: 0; + border-bottom-left-radius: 0; + -moz-border-radius-bottomleft: 0; +} + +.table-bordered tfoot + tbody:last-child tr:last-child td:last-child { + -webkit-border-bottom-right-radius: 0; + border-bottom-right-radius: 0; + -moz-border-radius-bottomright: 0; +} + +.table-bordered caption + thead tr:first-child th:first-child, +.table-bordered caption + tbody tr:first-child td:first-child, +.table-bordered colgroup + thead tr:first-child th:first-child, +.table-bordered colgroup + tbody tr:first-child td:first-child { + -webkit-border-top-left-radius: 4px; + border-top-left-radius: 4px; + -moz-border-radius-topleft: 4px; +} + +.table-bordered caption + thead tr:first-child th:last-child, +.table-bordered caption + tbody tr:first-child td:last-child, +.table-bordered colgroup + thead tr:first-child th:last-child, +.table-bordered colgroup + tbody tr:first-child td:last-child { + -webkit-border-top-right-radius: 4px; + border-top-right-radius: 4px; + -moz-border-radius-topright: 4px; +} + +.table-striped tbody > tr:nth-child(odd) > td, +.table-striped tbody > tr:nth-child(odd) > th { + background-color: #f9f9f9; +} + +.table-hover tbody tr:hover > td, +.table-hover tbody tr:hover > th { + background-color: #f5f5f5; +} + +table td[class*="span"], +table th[class*="span"], +.row-fluid table td[class*="span"], +.row-fluid table th[class*="span"] { + display: table-cell; + float: none; + margin-left: 0; +} + +.table td.span1, +.table th.span1 { + float: none; + width: 44px; + margin-left: 0; +} + +.table td.span2, +.table th.span2 { + float: none; + width: 124px; + margin-left: 0; +} + +.table td.span3, +.table th.span3 { + float: none; + width: 204px; + margin-left: 0; +} + +.table td.span4, +.table th.span4 { + float: none; + width: 284px; + margin-left: 0; +} + +.table td.span5, +.table th.span5 { + float: none; + width: 364px; + margin-left: 0; +} + +.table td.span6, +.table th.span6 { + float: none; + width: 444px; + margin-left: 0; +} + +.table td.span7, +.table th.span7 { + float: none; + width: 524px; + margin-left: 0; +} + +.table td.span8, +.table th.span8 { + float: none; + width: 604px; + margin-left: 0; +} + +.table td.span9, +.table th.span9 { + float: none; + width: 684px; + margin-left: 0; +} + +.table td.span10, +.table th.span10 { + float: none; + width: 764px; + margin-left: 0; +} + +.table td.span11, +.table th.span11 { + float: none; + width: 844px; + margin-left: 0; +} + +.table td.span12, +.table th.span12 { + float: none; + width: 924px; + margin-left: 0; +} + +.table tbody tr.success > td { + background-color: #dff0d8; +} + +.table tbody tr.error > td { + background-color: #f2dede; +} + +.table tbody tr.warning > td { + background-color: #fcf8e3; +} + +.table tbody tr.info > td { + background-color: #d9edf7; +} + +.table-hover tbody tr.success:hover > td { + background-color: #d0e9c6; +} + +.table-hover tbody tr.error:hover > td { + background-color: #ebcccc; +} + +.table-hover tbody tr.warning:hover > td { + background-color: #faf2cc; +} + +.table-hover tbody tr.info:hover > td { + background-color: #c4e3f3; +} + +[class^="icon-"], +[class*=" icon-"] { + display: inline-block; + width: 14px; + height: 14px; + margin-top: 1px; + *margin-right: .3em; + line-height: 14px; + vertical-align: text-top; + background-image: url("../img/glyphicons-halflings.png"); + background-position: 14px 14px; + background-repeat: no-repeat; +} + +/* White icons with optional class, or on hover/focus/active states of certain elements */ + +.icon-white, +.nav-pills > .active > a > [class^="icon-"], +.nav-pills > .active > a > [class*=" icon-"], +.nav-list > .active > a > [class^="icon-"], +.nav-list > .active > a > [class*=" icon-"], +.navbar-inverse .nav > .active > a > [class^="icon-"], +.navbar-inverse .nav > .active > a > [class*=" icon-"], +.dropdown-menu > li > a:hover > [class^="icon-"], +.dropdown-menu > li > a:focus > [class^="icon-"], +.dropdown-menu > li > a:hover > [class*=" icon-"], +.dropdown-menu > li > a:focus > [class*=" icon-"], +.dropdown-menu > .active > a > [class^="icon-"], +.dropdown-menu > .active > a > [class*=" icon-"], +.dropdown-submenu:hover > a > [class^="icon-"], +.dropdown-submenu:focus > a > [class^="icon-"], +.dropdown-submenu:hover > a > [class*=" icon-"], +.dropdown-submenu:focus > a > [class*=" icon-"] { + background-image: url("../img/glyphicons-halflings-white.png"); +} + +.icon-glass { + background-position: 0 0; +} + +.icon-music { + background-position: -24px 0; +} + +.icon-search { + background-position: -48px 0; +} + +.icon-envelope { + background-position: -72px 0; +} + +.icon-heart { + background-position: -96px 0; +} + +.icon-star { + background-position: -120px 0; +} + +.icon-star-empty { + background-position: -144px 0; +} + +.icon-user { + background-position: -168px 0; +} + +.icon-film { + background-position: -192px 0; +} + +.icon-th-large { + background-position: -216px 0; +} + +.icon-th { + background-position: -240px 0; +} + +.icon-th-list { + background-position: -264px 0; +} + +.icon-ok { + background-position: -288px 0; +} + +.icon-remove { + background-position: -312px 0; +} + +.icon-zoom-in { + background-position: -336px 0; +} + +.icon-zoom-out { + background-position: -360px 0; +} + +.icon-off { + background-position: -384px 0; +} + +.icon-signal { + background-position: -408px 0; +} + +.icon-cog { + background-position: -432px 0; +} + +.icon-trash { + background-position: -456px 0; +} + +.icon-home { + background-position: 0 -24px; +} + +.icon-file { + background-position: -24px -24px; +} + +.icon-time { + background-position: -48px -24px; +} + +.icon-road { + background-position: -72px -24px; +} + +.icon-download-alt { + background-position: -96px -24px; +} + +.icon-download { + background-position: -120px -24px; +} + +.icon-upload { + background-position: -144px -24px; +} + +.icon-inbox { + background-position: -168px -24px; +} + +.icon-play-circle { + background-position: -192px -24px; +} + +.icon-repeat { + background-position: -216px -24px; +} + +.icon-refresh { + background-position: -240px -24px; +} + +.icon-list-alt { + background-position: -264px -24px; +} + +.icon-lock { + background-position: -287px -24px; +} + +.icon-flag { + background-position: -312px -24px; +} + +.icon-headphones { + background-position: -336px -24px; +} + +.icon-volume-off { + background-position: -360px -24px; +} + +.icon-volume-down { + background-position: -384px -24px; +} + +.icon-volume-up { + background-position: -408px -24px; +} + +.icon-qrcode { + background-position: -432px -24px; +} + +.icon-barcode { + background-position: -456px -24px; +} + +.icon-tag { + background-position: 0 -48px; +} + +.icon-tags { + background-position: -25px -48px; +} + +.icon-book { + background-position: -48px -48px; +} + +.icon-bookmark { + background-position: -72px -48px; +} + +.icon-print { + background-position: -96px -48px; +} + +.icon-camera { + background-position: -120px -48px; +} + +.icon-font { + background-position: -144px -48px; +} + +.icon-bold { + background-position: -167px -48px; +} + +.icon-italic { + background-position: -192px -48px; +} + +.icon-text-height { + background-position: -216px -48px; +} + +.icon-text-width { + background-position: -240px -48px; +} + +.icon-align-left { + background-position: -264px -48px; +} + +.icon-align-center { + background-position: -288px -48px; +} + +.icon-align-right { + background-position: -312px -48px; +} + +.icon-align-justify { + background-position: -336px -48px; +} + +.icon-list { + background-position: -360px -48px; +} + +.icon-indent-left { + background-position: -384px -48px; +} + +.icon-indent-right { + background-position: -408px -48px; +} + +.icon-facetime-video { + background-position: -432px -48px; +} + +.icon-picture { + background-position: -456px -48px; +} + +.icon-pencil { + background-position: 0 -72px; +} + +.icon-map-marker { + background-position: -24px -72px; +} + +.icon-adjust { + background-position: -48px -72px; +} + +.icon-tint { + background-position: -72px -72px; +} + +.icon-edit { + background-position: -96px -72px; +} + +.icon-share { + background-position: -120px -72px; +} + +.icon-check { + background-position: -144px -72px; +} + +.icon-move { + background-position: -168px -72px; +} + +.icon-step-backward { + background-position: -192px -72px; +} + +.icon-fast-backward { + background-position: -216px -72px; +} + +.icon-backward { + background-position: -240px -72px; +} + +.icon-play { + background-position: -264px -72px; +} + +.icon-pause { + background-position: -288px -72px; +} + +.icon-stop { + background-position: -312px -72px; +} + +.icon-forward { + background-position: -336px -72px; +} + +.icon-fast-forward { + background-position: -360px -72px; +} + +.icon-step-forward { + background-position: -384px -72px; +} + +.icon-eject { + background-position: -408px -72px; +} + +.icon-chevron-left { + background-position: -432px -72px; +} + +.icon-chevron-right { + background-position: -456px -72px; +} + +.icon-plus-sign { + background-position: 0 -96px; +} + +.icon-minus-sign { + background-position: -24px -96px; +} + +.icon-remove-sign { + background-position: -48px -96px; +} + +.icon-ok-sign { + background-position: -72px -96px; +} + +.icon-question-sign { + background-position: -96px -96px; +} + +.icon-info-sign { + background-position: -120px -96px; +} + +.icon-screenshot { + background-position: -144px -96px; +} + +.icon-remove-circle { + background-position: -168px -96px; +} + +.icon-ok-circle { + background-position: -192px -96px; +} + +.icon-ban-circle { + background-position: -216px -96px; +} + +.icon-arrow-left { + background-position: -240px -96px; +} + +.icon-arrow-right { + background-position: -264px -96px; +} + +.icon-arrow-up { + background-position: -289px -96px; +} + +.icon-arrow-down { + background-position: -312px -96px; +} + +.icon-share-alt { + background-position: -336px -96px; +} + +.icon-resize-full { + background-position: -360px -96px; +} + +.icon-resize-small { + background-position: -384px -96px; +} + +.icon-plus { + background-position: -408px -96px; +} + +.icon-minus { + background-position: -433px -96px; +} + +.icon-asterisk { + background-position: -456px -96px; +} + +.icon-exclamation-sign { + background-position: 0 -120px; +} + +.icon-gift { + background-position: -24px -120px; +} + +.icon-leaf { + background-position: -48px -120px; +} + +.icon-fire { + background-position: -72px -120px; +} + +.icon-eye-open { + background-position: -96px -120px; +} + +.icon-eye-close { + background-position: -120px -120px; +} + +.icon-warning-sign { + background-position: -144px -120px; +} + +.icon-plane { + background-position: -168px -120px; +} + +.icon-calendar { + background-position: -192px -120px; +} + +.icon-random { + width: 16px; + background-position: -216px -120px; +} + +.icon-comment { + background-position: -240px -120px; +} + +.icon-magnet { + background-position: -264px -120px; +} + +.icon-chevron-up { + background-position: -288px -120px; +} + +.icon-chevron-down { + background-position: -313px -119px; +} + +.icon-retweet { + background-position: -336px -120px; +} + +.icon-shopping-cart { + background-position: -360px -120px; +} + +.icon-folder-close { + width: 16px; + background-position: -384px -120px; +} + +.icon-folder-open { + width: 16px; + background-position: -408px -120px; +} + +.icon-resize-vertical { + background-position: -432px -119px; +} + +.icon-resize-horizontal { + background-position: -456px -118px; +} + +.icon-hdd { + background-position: 0 -144px; +} + +.icon-bullhorn { + background-position: -24px -144px; +} + +.icon-bell { + background-position: -48px -144px; +} + +.icon-certificate { + background-position: -72px -144px; +} + +.icon-thumbs-up { + background-position: -96px -144px; +} + +.icon-thumbs-down { + background-position: -120px -144px; +} + +.icon-hand-right { + background-position: -144px -144px; +} + +.icon-hand-left { + background-position: -168px -144px; +} + +.icon-hand-up { + background-position: -192px -144px; +} + +.icon-hand-down { + background-position: -216px -144px; +} + +.icon-circle-arrow-right { + background-position: -240px -144px; +} + +.icon-circle-arrow-left { + background-position: -264px -144px; +} + +.icon-circle-arrow-up { + background-position: -288px -144px; +} + +.icon-circle-arrow-down { + background-position: -312px -144px; +} + +.icon-globe { + background-position: -336px -144px; +} + +.icon-wrench { + background-position: -360px -144px; +} + +.icon-tasks { + background-position: -384px -144px; +} + +.icon-filter { + background-position: -408px -144px; +} + +.icon-briefcase { + background-position: -432px -144px; +} + +.icon-fullscreen { + background-position: -456px -144px; +} + +.dropup, +.dropdown { + position: relative; +} + +.dropdown-toggle { + *margin-bottom: -3px; +} + +.dropdown-toggle:active, +.open .dropdown-toggle { + outline: 0; +} + +.caret { + display: inline-block; + width: 0; + height: 0; + vertical-align: top; + border-top: 4px solid #000000; + border-right: 4px solid transparent; + border-left: 4px solid transparent; + content: ""; +} + +.dropdown .caret { + margin-top: 8px; + margin-left: 2px; +} + +.dropdown-menu { + position: absolute; + top: 100%; + left: 0; + z-index: 1000; + display: none; + float: left; + min-width: 160px; + padding: 5px 0; + margin: 2px 0 0; + list-style: none; + background-color: #ffffff; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, 0.2); + *border-right-width: 2px; + *border-bottom-width: 2px; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; + -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + -webkit-background-clip: padding-box; + -moz-background-clip: padding; + background-clip: padding-box; +} + +.dropdown-menu.pull-right { + right: 0; + left: auto; +} + +.dropdown-menu .divider { + *width: 100%; + height: 1px; + margin: 9px 1px; + *margin: -5px 0 5px; + overflow: hidden; + background-color: #e5e5e5; + border-bottom: 1px solid #ffffff; +} + +.dropdown-menu > li > a { + display: block; + padding: 3px 20px; + clear: both; + font-weight: normal; + line-height: 20px; + color: #333333; + white-space: nowrap; +} + +.dropdown-menu > li > a:hover, +.dropdown-menu > li > a:focus, +.dropdown-submenu:hover > a, +.dropdown-submenu:focus > a { + color: #ffffff; + text-decoration: none; + background-color: #0081c2; + background-image: -moz-linear-gradient(top, #0088cc, #0077b3); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0077b3)); + background-image: -webkit-linear-gradient(top, #0088cc, #0077b3); + background-image: -o-linear-gradient(top, #0088cc, #0077b3); + background-image: linear-gradient(to bottom, #0088cc, #0077b3); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0077b3', GradientType=0); +} + +.dropdown-menu > .active > a, +.dropdown-menu > .active > a:hover, +.dropdown-menu > .active > a:focus { + color: #ffffff; + text-decoration: none; + background-color: #0081c2; + background-image: -moz-linear-gradient(top, #0088cc, #0077b3); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0077b3)); + background-image: -webkit-linear-gradient(top, #0088cc, #0077b3); + background-image: -o-linear-gradient(top, #0088cc, #0077b3); + background-image: linear-gradient(to bottom, #0088cc, #0077b3); + background-repeat: repeat-x; + outline: 0; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0077b3', GradientType=0); +} + +.dropdown-menu > .disabled > a, +.dropdown-menu > .disabled > a:hover, +.dropdown-menu > .disabled > a:focus { + color: #999999; +} + +.dropdown-menu > .disabled > a:hover, +.dropdown-menu > .disabled > a:focus { + text-decoration: none; + cursor: default; + background-color: transparent; + background-image: none; + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); +} + +.open { + *z-index: 1000; +} + +.open > .dropdown-menu { + display: block; +} + +.pull-right > .dropdown-menu { + right: 0; + left: auto; +} + +.dropup .caret, +.navbar-fixed-bottom .dropdown .caret { + border-top: 0; + border-bottom: 4px solid #000000; + content: ""; +} + +.dropup .dropdown-menu, +.navbar-fixed-bottom .dropdown .dropdown-menu { + top: auto; + bottom: 100%; + margin-bottom: 1px; +} + +.dropdown-submenu { + position: relative; +} + +.dropdown-submenu > .dropdown-menu { + top: 0; + left: 100%; + margin-top: -6px; + margin-left: -1px; + -webkit-border-radius: 0 6px 6px 6px; + -moz-border-radius: 0 6px 6px 6px; + border-radius: 0 6px 6px 6px; +} + +.dropdown-submenu:hover > .dropdown-menu { + display: block; +} + +.dropup .dropdown-submenu > .dropdown-menu { + top: auto; + bottom: 0; + margin-top: 0; + margin-bottom: -2px; + -webkit-border-radius: 5px 5px 5px 0; + -moz-border-radius: 5px 5px 5px 0; + border-radius: 5px 5px 5px 0; +} + +.dropdown-submenu > a:after { + display: block; + float: right; + width: 0; + height: 0; + margin-top: 5px; + margin-right: -10px; + border-color: transparent; + border-left-color: #cccccc; + border-style: solid; + border-width: 5px 0 5px 5px; + content: " "; +} + +.dropdown-submenu:hover > a:after { + border-left-color: #ffffff; +} + +.dropdown-submenu.pull-left { + float: none; +} + +.dropdown-submenu.pull-left > .dropdown-menu { + left: -100%; + margin-left: 10px; + -webkit-border-radius: 6px 0 6px 6px; + -moz-border-radius: 6px 0 6px 6px; + border-radius: 6px 0 6px 6px; +} + +.dropdown .dropdown-menu .nav-header { + padding-right: 20px; + padding-left: 20px; +} + +.typeahead { + z-index: 1051; + margin-top: 2px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.well { + min-height: 20px; + padding: 19px; + margin-bottom: 20px; + background-color: #f5f5f5; + border: 1px solid #e3e3e3; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); +} + +.well blockquote { + border-color: #ddd; + border-color: rgba(0, 0, 0, 0.15); +} + +.well-large { + padding: 24px; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; +} + +.well-small { + padding: 9px; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} + +.fade { + opacity: 0; + -webkit-transition: opacity 0.15s linear; + -moz-transition: opacity 0.15s linear; + -o-transition: opacity 0.15s linear; + transition: opacity 0.15s linear; +} + +.fade.in { + opacity: 1; +} + +.collapse { + position: relative; + height: 0; + overflow: hidden; + -webkit-transition: height 0.35s ease; + -moz-transition: height 0.35s ease; + -o-transition: height 0.35s ease; + transition: height 0.35s ease; +} + +.collapse.in { + height: auto; +} + +.close { + float: right; + font-size: 20px; + font-weight: bold; + line-height: 20px; + color: #000000; + text-shadow: 0 1px 0 #ffffff; + opacity: 0.2; + filter: alpha(opacity=20); +} + +.close:hover, +.close:focus { + color: #000000; + text-decoration: none; + cursor: pointer; + opacity: 0.4; + filter: alpha(opacity=40); +} + +button.close { + padding: 0; + cursor: pointer; + background: transparent; + border: 0; + -webkit-appearance: none; +} + +.btn { + display: inline-block; + *display: inline; + padding: 4px 12px; + margin-bottom: 0; + *margin-left: .3em; + font-size: 14px; + line-height: 20px; + color: #333333; + text-align: center; + text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); + vertical-align: middle; + cursor: pointer; + background-color: #f5f5f5; + *background-color: #e6e6e6; + background-image: -moz-linear-gradient(top, #ffffff, #e6e6e6); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6)); + background-image: -webkit-linear-gradient(top, #ffffff, #e6e6e6); + background-image: -o-linear-gradient(top, #ffffff, #e6e6e6); + background-image: linear-gradient(to bottom, #ffffff, #e6e6e6); + background-repeat: repeat-x; + border: 1px solid #cccccc; + *border: 0; + border-color: #e6e6e6 #e6e6e6 #bfbfbf; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + border-bottom-color: #b3b3b3; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe6e6e6', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); + *zoom: 1; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.btn:hover, +.btn:focus, +.btn:active, +.btn.active, +.btn.disabled, +.btn[disabled] { + color: #333333; + background-color: #e6e6e6; + *background-color: #d9d9d9; +} + +.btn:active, +.btn.active { + background-color: #cccccc \9; +} + +.btn:first-child { + *margin-left: 0; +} + +.btn:hover, +.btn:focus { + color: #333333; + text-decoration: none; + background-position: 0 -15px; + -webkit-transition: background-position 0.1s linear; + -moz-transition: background-position 0.1s linear; + -o-transition: background-position 0.1s linear; + transition: background-position 0.1s linear; +} + +.btn:focus { + outline: thin dotted #333; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +.btn.active, +.btn:active { + background-image: none; + outline: 0; + -webkit-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.btn.disabled, +.btn[disabled] { + cursor: default; + background-image: none; + opacity: 0.65; + filter: alpha(opacity=65); + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; +} + +.btn-large { + padding: 11px 19px; + font-size: 17.5px; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; +} + +.btn-large [class^="icon-"], +.btn-large [class*=" icon-"] { + margin-top: 4px; +} + +.btn-small { + padding: 2px 10px; + font-size: 11.9px; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} + +.btn-small [class^="icon-"], +.btn-small [class*=" icon-"] { + margin-top: 0; +} + +.btn-mini [class^="icon-"], +.btn-mini [class*=" icon-"] { + margin-top: -1px; +} + +.btn-mini { + padding: 0 6px; + font-size: 10.5px; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} + +.btn-block { + display: block; + width: 100%; + padding-right: 0; + padding-left: 0; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +.btn-block + .btn-block { + margin-top: 5px; +} + +input[type="submit"].btn-block, +input[type="reset"].btn-block, +input[type="button"].btn-block { + width: 100%; +} + +.btn-primary.active, +.btn-warning.active, +.btn-danger.active, +.btn-success.active, +.btn-info.active, +.btn-inverse.active { + color: rgba(255, 255, 255, 0.75); +} + +.btn-primary { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #006dcc; + *background-color: #0044cc; + background-image: -moz-linear-gradient(top, #0088cc, #0044cc); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc)); + background-image: -webkit-linear-gradient(top, #0088cc, #0044cc); + background-image: -o-linear-gradient(top, #0088cc, #0044cc); + background-image: linear-gradient(to bottom, #0088cc, #0044cc); + background-repeat: repeat-x; + border-color: #0044cc #0044cc #002a80; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0044cc', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); +} + +.btn-primary:hover, +.btn-primary:focus, +.btn-primary:active, +.btn-primary.active, +.btn-primary.disabled, +.btn-primary[disabled] { + color: #ffffff; + background-color: #0044cc; + *background-color: #003bb3; +} + +.btn-primary:active, +.btn-primary.active { + background-color: #003399 \9; +} + +.btn-warning { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #faa732; + *background-color: #f89406; + background-image: -moz-linear-gradient(top, #fbb450, #f89406); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406)); + background-image: -webkit-linear-gradient(top, #fbb450, #f89406); + background-image: -o-linear-gradient(top, #fbb450, #f89406); + background-image: linear-gradient(to bottom, #fbb450, #f89406); + background-repeat: repeat-x; + border-color: #f89406 #f89406 #ad6704; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450', endColorstr='#fff89406', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); +} + +.btn-warning:hover, +.btn-warning:focus, +.btn-warning:active, +.btn-warning.active, +.btn-warning.disabled, +.btn-warning[disabled] { + color: #ffffff; + background-color: #f89406; + *background-color: #df8505; +} + +.btn-warning:active, +.btn-warning.active { + background-color: #c67605 \9; +} + +.btn-danger { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #da4f49; + *background-color: #bd362f; + background-image: -moz-linear-gradient(top, #ee5f5b, #bd362f); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#bd362f)); + background-image: -webkit-linear-gradient(top, #ee5f5b, #bd362f); + background-image: -o-linear-gradient(top, #ee5f5b, #bd362f); + background-image: linear-gradient(to bottom, #ee5f5b, #bd362f); + background-repeat: repeat-x; + border-color: #bd362f #bd362f #802420; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffee5f5b', endColorstr='#ffbd362f', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); +} + +.btn-danger:hover, +.btn-danger:focus, +.btn-danger:active, +.btn-danger.active, +.btn-danger.disabled, +.btn-danger[disabled] { + color: #ffffff; + background-color: #bd362f; + *background-color: #a9302a; +} + +.btn-danger:active, +.btn-danger.active { + background-color: #942a25 \9; +} + +.btn-success { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #5bb75b; + *background-color: #51a351; + background-image: -moz-linear-gradient(top, #62c462, #51a351); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#51a351)); + background-image: -webkit-linear-gradient(top, #62c462, #51a351); + background-image: -o-linear-gradient(top, #62c462, #51a351); + background-image: linear-gradient(to bottom, #62c462, #51a351); + background-repeat: repeat-x; + border-color: #51a351 #51a351 #387038; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff62c462', endColorstr='#ff51a351', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); +} + +.btn-success:hover, +.btn-success:focus, +.btn-success:active, +.btn-success.active, +.btn-success.disabled, +.btn-success[disabled] { + color: #ffffff; + background-color: #51a351; + *background-color: #499249; +} + +.btn-success:active, +.btn-success.active { + background-color: #408140 \9; +} + +.btn-info { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #49afcd; + *background-color: #2f96b4; + background-image: -moz-linear-gradient(top, #5bc0de, #2f96b4); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#2f96b4)); + background-image: -webkit-linear-gradient(top, #5bc0de, #2f96b4); + background-image: -o-linear-gradient(top, #5bc0de, #2f96b4); + background-image: linear-gradient(to bottom, #5bc0de, #2f96b4); + background-repeat: repeat-x; + border-color: #2f96b4 #2f96b4 #1f6377; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2f96b4', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); +} + +.btn-info:hover, +.btn-info:focus, +.btn-info:active, +.btn-info.active, +.btn-info.disabled, +.btn-info[disabled] { + color: #ffffff; + background-color: #2f96b4; + *background-color: #2a85a0; +} + +.btn-info:active, +.btn-info.active { + background-color: #24748c \9; +} + +.btn-inverse { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #363636; + *background-color: #222222; + background-image: -moz-linear-gradient(top, #444444, #222222); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#444444), to(#222222)); + background-image: -webkit-linear-gradient(top, #444444, #222222); + background-image: -o-linear-gradient(top, #444444, #222222); + background-image: linear-gradient(to bottom, #444444, #222222); + background-repeat: repeat-x; + border-color: #222222 #222222 #000000; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff444444', endColorstr='#ff222222', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); +} + +.btn-inverse:hover, +.btn-inverse:focus, +.btn-inverse:active, +.btn-inverse.active, +.btn-inverse.disabled, +.btn-inverse[disabled] { + color: #ffffff; + background-color: #222222; + *background-color: #151515; +} + +.btn-inverse:active, +.btn-inverse.active { + background-color: #080808 \9; +} + +button.btn, +input[type="submit"].btn { + *padding-top: 3px; + *padding-bottom: 3px; +} + +button.btn::-moz-focus-inner, +input[type="submit"].btn::-moz-focus-inner { + padding: 0; + border: 0; +} + +button.btn.btn-large, +input[type="submit"].btn.btn-large { + *padding-top: 7px; + *padding-bottom: 7px; +} + +button.btn.btn-small, +input[type="submit"].btn.btn-small { + *padding-top: 3px; + *padding-bottom: 3px; +} + +button.btn.btn-mini, +input[type="submit"].btn.btn-mini { + *padding-top: 1px; + *padding-bottom: 1px; +} + +.btn-link, +.btn-link:active, +.btn-link[disabled] { + background-color: transparent; + background-image: none; + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; +} + +.btn-link { + color: #0088cc; + cursor: pointer; + border-color: transparent; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.btn-link:hover, +.btn-link:focus { + color: #005580; + text-decoration: underline; + background-color: transparent; +} + +.btn-link[disabled]:hover, +.btn-link[disabled]:focus { + color: #333333; + text-decoration: none; +} + +.btn-group { + position: relative; + display: inline-block; + *display: inline; + *margin-left: .3em; + font-size: 0; + white-space: nowrap; + vertical-align: middle; + *zoom: 1; +} + +.btn-group:first-child { + *margin-left: 0; +} + +.btn-group + .btn-group { + margin-left: 5px; +} + +.btn-toolbar { + margin-top: 10px; + margin-bottom: 10px; + font-size: 0; +} + +.btn-toolbar > .btn + .btn, +.btn-toolbar > .btn-group + .btn, +.btn-toolbar > .btn + .btn-group { + margin-left: 5px; +} + +.btn-group > .btn { + position: relative; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.btn-group > .btn + .btn { + margin-left: -1px; +} + +.btn-group > .btn, +.btn-group > .dropdown-menu, +.btn-group > .popover { + font-size: 14px; +} + +.btn-group > .btn-mini { + font-size: 10.5px; +} + +.btn-group > .btn-small { + font-size: 11.9px; +} + +.btn-group > .btn-large { + font-size: 17.5px; +} + +.btn-group > .btn:first-child { + margin-left: 0; + -webkit-border-bottom-left-radius: 4px; + border-bottom-left-radius: 4px; + -webkit-border-top-left-radius: 4px; + border-top-left-radius: 4px; + -moz-border-radius-bottomleft: 4px; + -moz-border-radius-topleft: 4px; +} + +.btn-group > .btn:last-child, +.btn-group > .dropdown-toggle { + -webkit-border-top-right-radius: 4px; + border-top-right-radius: 4px; + -webkit-border-bottom-right-radius: 4px; + border-bottom-right-radius: 4px; + -moz-border-radius-topright: 4px; + -moz-border-radius-bottomright: 4px; +} + +.btn-group > .btn.large:first-child { + margin-left: 0; + -webkit-border-bottom-left-radius: 6px; + border-bottom-left-radius: 6px; + -webkit-border-top-left-radius: 6px; + border-top-left-radius: 6px; + -moz-border-radius-bottomleft: 6px; + -moz-border-radius-topleft: 6px; +} + +.btn-group > .btn.large:last-child, +.btn-group > .large.dropdown-toggle { + -webkit-border-top-right-radius: 6px; + border-top-right-radius: 6px; + -webkit-border-bottom-right-radius: 6px; + border-bottom-right-radius: 6px; + -moz-border-radius-topright: 6px; + -moz-border-radius-bottomright: 6px; +} + +.btn-group > .btn:hover, +.btn-group > .btn:focus, +.btn-group > .btn:active, +.btn-group > .btn.active { + z-index: 2; +} + +.btn-group .dropdown-toggle:active, +.btn-group.open .dropdown-toggle { + outline: 0; +} + +.btn-group > .btn + .dropdown-toggle { + *padding-top: 5px; + padding-right: 8px; + *padding-bottom: 5px; + padding-left: 8px; + -webkit-box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.btn-group > .btn-mini + .dropdown-toggle { + *padding-top: 2px; + padding-right: 5px; + *padding-bottom: 2px; + padding-left: 5px; +} + +.btn-group > .btn-small + .dropdown-toggle { + *padding-top: 5px; + *padding-bottom: 4px; +} + +.btn-group > .btn-large + .dropdown-toggle { + *padding-top: 7px; + padding-right: 12px; + *padding-bottom: 7px; + padding-left: 12px; +} + +.btn-group.open .dropdown-toggle { + background-image: none; + -webkit-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.btn-group.open .btn.dropdown-toggle { + background-color: #e6e6e6; +} + +.btn-group.open .btn-primary.dropdown-toggle { + background-color: #0044cc; +} + +.btn-group.open .btn-warning.dropdown-toggle { + background-color: #f89406; +} + +.btn-group.open .btn-danger.dropdown-toggle { + background-color: #bd362f; +} + +.btn-group.open .btn-success.dropdown-toggle { + background-color: #51a351; +} + +.btn-group.open .btn-info.dropdown-toggle { + background-color: #2f96b4; +} + +.btn-group.open .btn-inverse.dropdown-toggle { + background-color: #222222; +} + +.btn .caret { + margin-top: 8px; + margin-left: 0; +} + +.btn-large .caret { + margin-top: 6px; +} + +.btn-large .caret { + border-top-width: 5px; + border-right-width: 5px; + border-left-width: 5px; +} + +.btn-mini .caret, +.btn-small .caret { + margin-top: 8px; +} + +.dropup .btn-large .caret { + border-bottom-width: 5px; +} + +.btn-primary .caret, +.btn-warning .caret, +.btn-danger .caret, +.btn-info .caret, +.btn-success .caret, +.btn-inverse .caret { + border-top-color: #ffffff; + border-bottom-color: #ffffff; +} + +.btn-group-vertical { + display: inline-block; + *display: inline; + /* IE7 inline-block hack */ + + *zoom: 1; +} + +.btn-group-vertical > .btn { + display: block; + float: none; + max-width: 100%; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.btn-group-vertical > .btn + .btn { + margin-top: -1px; + margin-left: 0; +} + +.btn-group-vertical > .btn:first-child { + -webkit-border-radius: 4px 4px 0 0; + -moz-border-radius: 4px 4px 0 0; + border-radius: 4px 4px 0 0; +} + +.btn-group-vertical > .btn:last-child { + -webkit-border-radius: 0 0 4px 4px; + -moz-border-radius: 0 0 4px 4px; + border-radius: 0 0 4px 4px; +} + +.btn-group-vertical > .btn-large:first-child { + -webkit-border-radius: 6px 6px 0 0; + -moz-border-radius: 6px 6px 0 0; + border-radius: 6px 6px 0 0; +} + +.btn-group-vertical > .btn-large:last-child { + -webkit-border-radius: 0 0 6px 6px; + -moz-border-radius: 0 0 6px 6px; + border-radius: 0 0 6px 6px; +} + +.alert { + padding: 8px 35px 8px 14px; + margin-bottom: 20px; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + background-color: #fcf8e3; + border: 1px solid #fbeed5; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.alert, +.alert h4 { + color: #c09853; +} + +.alert h4 { + margin: 0; +} + +.alert .close { + position: relative; + top: -2px; + right: -21px; + line-height: 20px; +} + +.alert-success { + color: #468847; + background-color: #dff0d8; + border-color: #d6e9c6; +} + +.alert-success h4 { + color: #468847; +} + +.alert-danger, +.alert-error { + color: #b94a48; + background-color: #f2dede; + border-color: #eed3d7; +} + +.alert-danger h4, +.alert-error h4 { + color: #b94a48; +} + +.alert-info { + color: #3a87ad; + background-color: #d9edf7; + border-color: #bce8f1; +} + +.alert-info h4 { + color: #3a87ad; +} + +.alert-block { + padding-top: 14px; + padding-bottom: 14px; +} + +.alert-block > p, +.alert-block > ul { + margin-bottom: 0; +} + +.alert-block p + p { + margin-top: 5px; +} + +.nav { + margin-bottom: 20px; + margin-left: 0; + list-style: none; +} + +.nav > li > a { + display: block; +} + +.nav > li > a:hover, +.nav > li > a:focus { + text-decoration: none; + background-color: #eeeeee; +} + +.nav > li > a > img { + max-width: none; +} + +.nav > .pull-right { + float: right; +} + +.nav-header { + display: block; + padding: 3px 15px; + font-size: 11px; + font-weight: bold; + line-height: 20px; + color: #999999; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + text-transform: uppercase; +} + +.nav li + .nav-header { + margin-top: 9px; +} + +.nav-list { + padding-right: 15px; + padding-left: 15px; + margin-bottom: 0; +} + +.nav-list > li > a, +.nav-list .nav-header { + margin-right: -15px; + margin-left: -15px; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); +} + +.nav-list > li > a { + padding: 3px 15px; +} + +.nav-list > .active > a, +.nav-list > .active > a:hover, +.nav-list > .active > a:focus { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2); + background-color: #0088cc; +} + +.nav-list [class^="icon-"], +.nav-list [class*=" icon-"] { + margin-right: 2px; +} + +.nav-list .divider { + *width: 100%; + height: 1px; + margin: 9px 1px; + *margin: -5px 0 5px; + overflow: hidden; + background-color: #e5e5e5; + border-bottom: 1px solid #ffffff; +} + +.nav-tabs, +.nav-pills { + *zoom: 1; +} + +.nav-tabs:before, +.nav-pills:before, +.nav-tabs:after, +.nav-pills:after { + display: table; + line-height: 0; + content: ""; +} + +.nav-tabs:after, +.nav-pills:after { + clear: both; +} + +.nav-tabs > li, +.nav-pills > li { + float: left; +} + +.nav-tabs > li > a, +.nav-pills > li > a { + padding-right: 12px; + padding-left: 12px; + margin-right: 2px; + line-height: 14px; +} + +.nav-tabs { + border-bottom: 1px solid #ddd; +} + +.nav-tabs > li { + margin-bottom: -1px; +} + +.nav-tabs > li > a { + padding-top: 8px; + padding-bottom: 8px; + line-height: 20px; + border: 1px solid transparent; + -webkit-border-radius: 4px 4px 0 0; + -moz-border-radius: 4px 4px 0 0; + border-radius: 4px 4px 0 0; +} + +.nav-tabs > li > a:hover, +.nav-tabs > li > a:focus { + border-color: #eeeeee #eeeeee #dddddd; +} + +.nav-tabs > .active > a, +.nav-tabs > .active > a:hover, +.nav-tabs > .active > a:focus { + color: #555555; + cursor: default; + background-color: #ffffff; + border: 1px solid #ddd; + border-bottom-color: transparent; +} + +.nav-pills > li > a { + padding-top: 8px; + padding-bottom: 8px; + margin-top: 2px; + margin-bottom: 2px; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; +} + +.nav-pills > .active > a, +.nav-pills > .active > a:hover, +.nav-pills > .active > a:focus { + color: #ffffff; + background-color: #0088cc; +} + +.nav-stacked > li { + float: none; +} + +.nav-stacked > li > a { + margin-right: 0; +} + +.nav-tabs.nav-stacked { + border-bottom: 0; +} + +.nav-tabs.nav-stacked > li > a { + border: 1px solid #ddd; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.nav-tabs.nav-stacked > li:first-child > a { + -webkit-border-top-right-radius: 4px; + border-top-right-radius: 4px; + -webkit-border-top-left-radius: 4px; + border-top-left-radius: 4px; + -moz-border-radius-topright: 4px; + -moz-border-radius-topleft: 4px; +} + +.nav-tabs.nav-stacked > li:last-child > a { + -webkit-border-bottom-right-radius: 4px; + border-bottom-right-radius: 4px; + -webkit-border-bottom-left-radius: 4px; + border-bottom-left-radius: 4px; + -moz-border-radius-bottomright: 4px; + -moz-border-radius-bottomleft: 4px; +} + +.nav-tabs.nav-stacked > li > a:hover, +.nav-tabs.nav-stacked > li > a:focus { + z-index: 2; + border-color: #ddd; +} + +.nav-pills.nav-stacked > li > a { + margin-bottom: 3px; +} + +.nav-pills.nav-stacked > li:last-child > a { + margin-bottom: 1px; +} + +.nav-tabs .dropdown-menu { + -webkit-border-radius: 0 0 6px 6px; + -moz-border-radius: 0 0 6px 6px; + border-radius: 0 0 6px 6px; +} + +.nav-pills .dropdown-menu { + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; +} + +.nav .dropdown-toggle .caret { + margin-top: 6px; + border-top-color: #0088cc; + border-bottom-color: #0088cc; +} + +.nav .dropdown-toggle:hover .caret, +.nav .dropdown-toggle:focus .caret { + border-top-color: #005580; + border-bottom-color: #005580; +} + +/* move down carets for tabs */ + +.nav-tabs .dropdown-toggle .caret { + margin-top: 8px; +} + +.nav .active .dropdown-toggle .caret { + border-top-color: #fff; + border-bottom-color: #fff; +} + +.nav-tabs .active .dropdown-toggle .caret { + border-top-color: #555555; + border-bottom-color: #555555; +} + +.nav > .dropdown.active > a:hover, +.nav > .dropdown.active > a:focus { + cursor: pointer; +} + +.nav-tabs .open .dropdown-toggle, +.nav-pills .open .dropdown-toggle, +.nav > li.dropdown.open.active > a:hover, +.nav > li.dropdown.open.active > a:focus { + color: #ffffff; + background-color: #999999; + border-color: #999999; +} + +.nav li.dropdown.open .caret, +.nav li.dropdown.open.active .caret, +.nav li.dropdown.open a:hover .caret, +.nav li.dropdown.open a:focus .caret { + border-top-color: #ffffff; + border-bottom-color: #ffffff; + opacity: 1; + filter: alpha(opacity=100); +} + +.tabs-stacked .open > a:hover, +.tabs-stacked .open > a:focus { + border-color: #999999; +} + +.tabbable { + *zoom: 1; +} + +.tabbable:before, +.tabbable:after { + display: table; + line-height: 0; + content: ""; +} + +.tabbable:after { + clear: both; +} + +.tab-content { + overflow: auto; +} + +.tabs-below > .nav-tabs, +.tabs-right > .nav-tabs, +.tabs-left > .nav-tabs { + border-bottom: 0; +} + +.tab-content > .tab-pane, +.pill-content > .pill-pane { + display: none; +} + +.tab-content > .active, +.pill-content > .active { + display: block; +} + +.tabs-below > .nav-tabs { + border-top: 1px solid #ddd; +} + +.tabs-below > .nav-tabs > li { + margin-top: -1px; + margin-bottom: 0; +} + +.tabs-below > .nav-tabs > li > a { + -webkit-border-radius: 0 0 4px 4px; + -moz-border-radius: 0 0 4px 4px; + border-radius: 0 0 4px 4px; +} + +.tabs-below > .nav-tabs > li > a:hover, +.tabs-below > .nav-tabs > li > a:focus { + border-top-color: #ddd; + border-bottom-color: transparent; +} + +.tabs-below > .nav-tabs > .active > a, +.tabs-below > .nav-tabs > .active > a:hover, +.tabs-below > .nav-tabs > .active > a:focus { + border-color: transparent #ddd #ddd #ddd; +} + +.tabs-left > .nav-tabs > li, +.tabs-right > .nav-tabs > li { + float: none; +} + +.tabs-left > .nav-tabs > li > a, +.tabs-right > .nav-tabs > li > a { + min-width: 74px; + margin-right: 0; + margin-bottom: 3px; +} + +.tabs-left > .nav-tabs { + float: left; + margin-right: 19px; + border-right: 1px solid #ddd; +} + +.tabs-left > .nav-tabs > li > a { + margin-right: -1px; + -webkit-border-radius: 4px 0 0 4px; + -moz-border-radius: 4px 0 0 4px; + border-radius: 4px 0 0 4px; +} + +.tabs-left > .nav-tabs > li > a:hover, +.tabs-left > .nav-tabs > li > a:focus { + border-color: #eeeeee #dddddd #eeeeee #eeeeee; +} + +.tabs-left > .nav-tabs .active > a, +.tabs-left > .nav-tabs .active > a:hover, +.tabs-left > .nav-tabs .active > a:focus { + border-color: #ddd transparent #ddd #ddd; + *border-right-color: #ffffff; +} + +.tabs-right > .nav-tabs { + float: right; + margin-left: 19px; + border-left: 1px solid #ddd; +} + +.tabs-right > .nav-tabs > li > a { + margin-left: -1px; + -webkit-border-radius: 0 4px 4px 0; + -moz-border-radius: 0 4px 4px 0; + border-radius: 0 4px 4px 0; +} + +.tabs-right > .nav-tabs > li > a:hover, +.tabs-right > .nav-tabs > li > a:focus { + border-color: #eeeeee #eeeeee #eeeeee #dddddd; +} + +.tabs-right > .nav-tabs .active > a, +.tabs-right > .nav-tabs .active > a:hover, +.tabs-right > .nav-tabs .active > a:focus { + border-color: #ddd #ddd #ddd transparent; + *border-left-color: #ffffff; +} + +.nav > .disabled > a { + color: #999999; +} + +.nav > .disabled > a:hover, +.nav > .disabled > a:focus { + text-decoration: none; + cursor: default; + background-color: transparent; +} + +.navbar { + *position: relative; + *z-index: 2; + margin-bottom: 20px; + overflow: visible; +} + +.navbar-inner { + min-height: 40px; + padding-right: 20px; + padding-left: 20px; + background-color: #fafafa; + background-image: -moz-linear-gradient(top, #ffffff, #f2f2f2); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#f2f2f2)); + background-image: -webkit-linear-gradient(top, #ffffff, #f2f2f2); + background-image: -o-linear-gradient(top, #ffffff, #f2f2f2); + background-image: linear-gradient(to bottom, #ffffff, #f2f2f2); + background-repeat: repeat-x; + border: 1px solid #d4d4d4; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff2f2f2', GradientType=0); + *zoom: 1; + -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.065); + -moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.065); + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.065); +} + +.navbar-inner:before, +.navbar-inner:after { + display: table; + line-height: 0; + content: ""; +} + +.navbar-inner:after { + clear: both; +} + +.navbar .container { + width: auto; +} + +.nav-collapse.collapse { + height: auto; + overflow: visible; +} + +.navbar .brand { + display: block; + float: left; + padding: 10px 20px 10px; + margin-left: -20px; + font-size: 20px; + font-weight: 200; + color: #777777; + text-shadow: 0 1px 0 #ffffff; +} + +.navbar .brand:hover, +.navbar .brand:focus { + text-decoration: none; +} + +.navbar-text { + margin-bottom: 0; + line-height: 40px; + color: #777777; +} + +.navbar-link { + color: #777777; +} + +.navbar-link:hover, +.navbar-link:focus { + color: #333333; +} + +.navbar .divider-vertical { + height: 40px; + margin: 0 9px; + border-right: 1px solid #ffffff; + border-left: 1px solid #f2f2f2; +} + +.navbar .btn, +.navbar .btn-group { + margin-top: 5px; +} + +.navbar .btn-group .btn, +.navbar .input-prepend .btn, +.navbar .input-append .btn, +.navbar .input-prepend .btn-group, +.navbar .input-append .btn-group { + margin-top: 0; +} + +.navbar-form { + margin-bottom: 0; + *zoom: 1; +} + +.navbar-form:before, +.navbar-form:after { + display: table; + line-height: 0; + content: ""; +} + +.navbar-form:after { + clear: both; +} + +.navbar-form input, +.navbar-form select, +.navbar-form .radio, +.navbar-form .checkbox { + margin-top: 5px; +} + +.navbar-form input, +.navbar-form select, +.navbar-form .btn { + display: inline-block; + margin-bottom: 0; +} + +.navbar-form input[type="image"], +.navbar-form input[type="checkbox"], +.navbar-form input[type="radio"] { + margin-top: 3px; +} + +.navbar-form .input-append, +.navbar-form .input-prepend { + margin-top: 5px; + white-space: nowrap; +} + +.navbar-form .input-append input, +.navbar-form .input-prepend input { + margin-top: 0; +} + +.navbar-search { + position: relative; + float: left; + margin-top: 5px; + margin-bottom: 0; +} + +.navbar-search .search-query { + padding: 4px 14px; + margin-bottom: 0; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 13px; + font-weight: normal; + line-height: 1; + -webkit-border-radius: 15px; + -moz-border-radius: 15px; + border-radius: 15px; +} + +.navbar-static-top { + position: static; + margin-bottom: 0; +} + +.navbar-static-top .navbar-inner { + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.navbar-fixed-top, +.navbar-fixed-bottom { + position: fixed; + right: 0; + left: 0; + z-index: 1030; + margin-bottom: 0; +} + +.navbar-fixed-top .navbar-inner, +.navbar-static-top .navbar-inner { + border-width: 0 0 1px; +} + +.navbar-fixed-bottom .navbar-inner { + border-width: 1px 0 0; +} + +.navbar-fixed-top .navbar-inner, +.navbar-fixed-bottom .navbar-inner { + padding-right: 0; + padding-left: 0; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.navbar-static-top .container, +.navbar-fixed-top .container, +.navbar-fixed-bottom .container { + width: 940px; +} + +.navbar-fixed-top { + top: 0; +} + +.navbar-fixed-top .navbar-inner, +.navbar-static-top .navbar-inner { + -webkit-box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1); + -moz-box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1); + box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1); +} + +.navbar-fixed-bottom { + bottom: 0; +} + +.navbar-fixed-bottom .navbar-inner { + -webkit-box-shadow: 0 -1px 10px rgba(0, 0, 0, 0.1); + -moz-box-shadow: 0 -1px 10px rgba(0, 0, 0, 0.1); + box-shadow: 0 -1px 10px rgba(0, 0, 0, 0.1); +} + +.navbar .nav { + position: relative; + left: 0; + display: block; + float: left; + margin: 0 10px 0 0; +} + +.navbar .nav.pull-right { + float: right; + margin-right: 0; +} + +.navbar .nav > li { + float: left; +} + +.navbar .nav > li > a { + float: none; + padding: 10px 15px 10px; + color: #777777; + text-decoration: none; + text-shadow: 0 1px 0 #ffffff; +} + +.navbar .nav .dropdown-toggle .caret { + margin-top: 8px; +} + +.navbar .nav > li > a:focus, +.navbar .nav > li > a:hover { + color: #333333; + text-decoration: none; + background-color: transparent; +} + +.navbar .nav > .active > a, +.navbar .nav > .active > a:hover, +.navbar .nav > .active > a:focus { + color: #555555; + text-decoration: none; + background-color: #e5e5e5; + -webkit-box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.125); + -moz-box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.125); + box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.125); +} + +.navbar .btn-navbar { + display: none; + float: right; + padding: 7px 10px; + margin-right: 5px; + margin-left: 5px; + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #ededed; + *background-color: #e5e5e5; + background-image: -moz-linear-gradient(top, #f2f2f2, #e5e5e5); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f2f2f2), to(#e5e5e5)); + background-image: -webkit-linear-gradient(top, #f2f2f2, #e5e5e5); + background-image: -o-linear-gradient(top, #f2f2f2, #e5e5e5); + background-image: linear-gradient(to bottom, #f2f2f2, #e5e5e5); + background-repeat: repeat-x; + border-color: #e5e5e5 #e5e5e5 #bfbfbf; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2f2f2', endColorstr='#ffe5e5e5', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); + -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); +} + +.navbar .btn-navbar:hover, +.navbar .btn-navbar:focus, +.navbar .btn-navbar:active, +.navbar .btn-navbar.active, +.navbar .btn-navbar.disabled, +.navbar .btn-navbar[disabled] { + color: #ffffff; + background-color: #e5e5e5; + *background-color: #d9d9d9; +} + +.navbar .btn-navbar:active, +.navbar .btn-navbar.active { + background-color: #cccccc \9; +} + +.navbar .btn-navbar .icon-bar { + display: block; + width: 18px; + height: 2px; + background-color: #f5f5f5; + -webkit-border-radius: 1px; + -moz-border-radius: 1px; + border-radius: 1px; + -webkit-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); + -moz-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); +} + +.btn-navbar .icon-bar + .icon-bar { + margin-top: 3px; +} + +.navbar .nav > li > .dropdown-menu:before { + position: absolute; + top: -7px; + left: 9px; + display: inline-block; + border-right: 7px solid transparent; + border-bottom: 7px solid #ccc; + border-left: 7px solid transparent; + border-bottom-color: rgba(0, 0, 0, 0.2); + content: ''; +} + +.navbar .nav > li > .dropdown-menu:after { + position: absolute; + top: -6px; + left: 10px; + display: inline-block; + border-right: 6px solid transparent; + border-bottom: 6px solid #ffffff; + border-left: 6px solid transparent; + content: ''; +} + +.navbar-fixed-bottom .nav > li > .dropdown-menu:before { + top: auto; + bottom: -7px; + border-top: 7px solid #ccc; + border-bottom: 0; + border-top-color: rgba(0, 0, 0, 0.2); +} + +.navbar-fixed-bottom .nav > li > .dropdown-menu:after { + top: auto; + bottom: -6px; + border-top: 6px solid #ffffff; + border-bottom: 0; +} + +.navbar .nav li.dropdown > a:hover .caret, +.navbar .nav li.dropdown > a:focus .caret { + border-top-color: #333333; + border-bottom-color: #333333; +} + +.navbar .nav li.dropdown.open > .dropdown-toggle, +.navbar .nav li.dropdown.active > .dropdown-toggle, +.navbar .nav li.dropdown.open.active > .dropdown-toggle { + color: #555555; + background-color: #e5e5e5; +} + +.navbar .nav li.dropdown > .dropdown-toggle .caret { + border-top-color: #777777; + border-bottom-color: #777777; +} + +.navbar .nav li.dropdown.open > .dropdown-toggle .caret, +.navbar .nav li.dropdown.active > .dropdown-toggle .caret, +.navbar .nav li.dropdown.open.active > .dropdown-toggle .caret { + border-top-color: #555555; + border-bottom-color: #555555; +} + +.navbar .pull-right > li > .dropdown-menu, +.navbar .nav > li > .dropdown-menu.pull-right { + right: 0; + left: auto; +} + +.navbar .pull-right > li > .dropdown-menu:before, +.navbar .nav > li > .dropdown-menu.pull-right:before { + right: 12px; + left: auto; +} + +.navbar .pull-right > li > .dropdown-menu:after, +.navbar .nav > li > .dropdown-menu.pull-right:after { + right: 13px; + left: auto; +} + +.navbar .pull-right > li > .dropdown-menu .dropdown-menu, +.navbar .nav > li > .dropdown-menu.pull-right .dropdown-menu { + right: 100%; + left: auto; + margin-right: -1px; + margin-left: 0; + -webkit-border-radius: 6px 0 6px 6px; + -moz-border-radius: 6px 0 6px 6px; + border-radius: 6px 0 6px 6px; +} + +.navbar-inverse .navbar-inner { + background-color: #1b1b1b; + background-image: -moz-linear-gradient(top, #222222, #111111); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#222222), to(#111111)); + background-image: -webkit-linear-gradient(top, #222222, #111111); + background-image: -o-linear-gradient(top, #222222, #111111); + background-image: linear-gradient(to bottom, #222222, #111111); + background-repeat: repeat-x; + border-color: #252525; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222', endColorstr='#ff111111', GradientType=0); +} + +.navbar-inverse .brand, +.navbar-inverse .nav > li > a { + color: #999999; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); +} + +.navbar-inverse .brand:hover, +.navbar-inverse .nav > li > a:hover, +.navbar-inverse .brand:focus, +.navbar-inverse .nav > li > a:focus { + color: #ffffff; +} + +.navbar-inverse .brand { + color: #999999; +} + +.navbar-inverse .navbar-text { + color: #999999; +} + +.navbar-inverse .nav > li > a:focus, +.navbar-inverse .nav > li > a:hover { + color: #ffffff; + background-color: transparent; +} + +.navbar-inverse .nav .active > a, +.navbar-inverse .nav .active > a:hover, +.navbar-inverse .nav .active > a:focus { + color: #ffffff; + background-color: #111111; +} + +.navbar-inverse .navbar-link { + color: #999999; +} + +.navbar-inverse .navbar-link:hover, +.navbar-inverse .navbar-link:focus { + color: #ffffff; +} + +.navbar-inverse .divider-vertical { + border-right-color: #222222; + border-left-color: #111111; +} + +.navbar-inverse .nav li.dropdown.open > .dropdown-toggle, +.navbar-inverse .nav li.dropdown.active > .dropdown-toggle, +.navbar-inverse .nav li.dropdown.open.active > .dropdown-toggle { + color: #ffffff; + background-color: #111111; +} + +.navbar-inverse .nav li.dropdown > a:hover .caret, +.navbar-inverse .nav li.dropdown > a:focus .caret { + border-top-color: #ffffff; + border-bottom-color: #ffffff; +} + +.navbar-inverse .nav li.dropdown > .dropdown-toggle .caret { + border-top-color: #999999; + border-bottom-color: #999999; +} + +.navbar-inverse .nav li.dropdown.open > .dropdown-toggle .caret, +.navbar-inverse .nav li.dropdown.active > .dropdown-toggle .caret, +.navbar-inverse .nav li.dropdown.open.active > .dropdown-toggle .caret { + border-top-color: #ffffff; + border-bottom-color: #ffffff; +} + +.navbar-inverse .navbar-search .search-query { + color: #ffffff; + background-color: #515151; + border-color: #111111; + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, 0.15); + -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, 0.15); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, 0.15); + -webkit-transition: none; + -moz-transition: none; + -o-transition: none; + transition: none; +} + +.navbar-inverse .navbar-search .search-query:-moz-placeholder { + color: #cccccc; +} + +.navbar-inverse .navbar-search .search-query:-ms-input-placeholder { + color: #cccccc; +} + +.navbar-inverse .navbar-search .search-query::-webkit-input-placeholder { + color: #cccccc; +} + +.navbar-inverse .navbar-search .search-query:focus, +.navbar-inverse .navbar-search .search-query.focused { + padding: 5px 15px; + color: #333333; + text-shadow: 0 1px 0 #ffffff; + background-color: #ffffff; + border: 0; + outline: 0; + -webkit-box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); + -moz-box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); + box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); +} + +.navbar-inverse .btn-navbar { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #0e0e0e; + *background-color: #040404; + background-image: -moz-linear-gradient(top, #151515, #040404); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#151515), to(#040404)); + background-image: -webkit-linear-gradient(top, #151515, #040404); + background-image: -o-linear-gradient(top, #151515, #040404); + background-image: linear-gradient(to bottom, #151515, #040404); + background-repeat: repeat-x; + border-color: #040404 #040404 #000000; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff151515', endColorstr='#ff040404', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); +} + +.navbar-inverse .btn-navbar:hover, +.navbar-inverse .btn-navbar:focus, +.navbar-inverse .btn-navbar:active, +.navbar-inverse .btn-navbar.active, +.navbar-inverse .btn-navbar.disabled, +.navbar-inverse .btn-navbar[disabled] { + color: #ffffff; + background-color: #040404; + *background-color: #000000; +} + +.navbar-inverse .btn-navbar:active, +.navbar-inverse .btn-navbar.active { + background-color: #000000 \9; +} + +.breadcrumb { + padding: 8px 15px; + margin: 0 0 20px; + list-style: none; + background-color: #f5f5f5; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.breadcrumb > li { + display: inline-block; + *display: inline; + text-shadow: 0 1px 0 #ffffff; + *zoom: 1; +} + +.breadcrumb > li > .divider { + padding: 0 5px; + color: #ccc; +} + +.breadcrumb > .active { + color: #999999; +} + +.pagination { + margin: 20px 0; +} + +.pagination ul { + display: inline-block; + *display: inline; + margin-bottom: 0; + margin-left: 0; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + *zoom: 1; + -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.pagination ul > li { + display: inline; +} + +.pagination ul > li > a, +.pagination ul > li > span { + float: left; + padding: 4px 12px; + line-height: 20px; + text-decoration: none; + background-color: #ffffff; + border: 1px solid #dddddd; + border-left-width: 0; +} + +.pagination ul > li > a:hover, +.pagination ul > li > a:focus, +.pagination ul > .active > a, +.pagination ul > .active > span { + background-color: #f5f5f5; +} + +.pagination ul > .active > a, +.pagination ul > .active > span { + color: #999999; + cursor: default; +} + +.pagination ul > .disabled > span, +.pagination ul > .disabled > a, +.pagination ul > .disabled > a:hover, +.pagination ul > .disabled > a:focus { + color: #999999; + cursor: default; + background-color: transparent; +} + +.pagination ul > li:first-child > a, +.pagination ul > li:first-child > span { + border-left-width: 1px; + -webkit-border-bottom-left-radius: 4px; + border-bottom-left-radius: 4px; + -webkit-border-top-left-radius: 4px; + border-top-left-radius: 4px; + -moz-border-radius-bottomleft: 4px; + -moz-border-radius-topleft: 4px; +} + +.pagination ul > li:last-child > a, +.pagination ul > li:last-child > span { + -webkit-border-top-right-radius: 4px; + border-top-right-radius: 4px; + -webkit-border-bottom-right-radius: 4px; + border-bottom-right-radius: 4px; + -moz-border-radius-topright: 4px; + -moz-border-radius-bottomright: 4px; +} + +.pagination-centered { + text-align: center; +} + +.pagination-right { + text-align: right; +} + +.pagination-large ul > li > a, +.pagination-large ul > li > span { + padding: 11px 19px; + font-size: 17.5px; +} + +.pagination-large ul > li:first-child > a, +.pagination-large ul > li:first-child > span { + -webkit-border-bottom-left-radius: 6px; + border-bottom-left-radius: 6px; + -webkit-border-top-left-radius: 6px; + border-top-left-radius: 6px; + -moz-border-radius-bottomleft: 6px; + -moz-border-radius-topleft: 6px; +} + +.pagination-large ul > li:last-child > a, +.pagination-large ul > li:last-child > span { + -webkit-border-top-right-radius: 6px; + border-top-right-radius: 6px; + -webkit-border-bottom-right-radius: 6px; + border-bottom-right-radius: 6px; + -moz-border-radius-topright: 6px; + -moz-border-radius-bottomright: 6px; +} + +.pagination-mini ul > li:first-child > a, +.pagination-small ul > li:first-child > a, +.pagination-mini ul > li:first-child > span, +.pagination-small ul > li:first-child > span { + -webkit-border-bottom-left-radius: 3px; + border-bottom-left-radius: 3px; + -webkit-border-top-left-radius: 3px; + border-top-left-radius: 3px; + -moz-border-radius-bottomleft: 3px; + -moz-border-radius-topleft: 3px; +} + +.pagination-mini ul > li:last-child > a, +.pagination-small ul > li:last-child > a, +.pagination-mini ul > li:last-child > span, +.pagination-small ul > li:last-child > span { + -webkit-border-top-right-radius: 3px; + border-top-right-radius: 3px; + -webkit-border-bottom-right-radius: 3px; + border-bottom-right-radius: 3px; + -moz-border-radius-topright: 3px; + -moz-border-radius-bottomright: 3px; +} + +.pagination-small ul > li > a, +.pagination-small ul > li > span { + padding: 2px 10px; + font-size: 11.9px; +} + +.pagination-mini ul > li > a, +.pagination-mini ul > li > span { + padding: 0 6px; + font-size: 10.5px; +} + +.pager { + margin: 20px 0; + text-align: center; + list-style: none; + *zoom: 1; +} + +.pager:before, +.pager:after { + display: table; + line-height: 0; + content: ""; +} + +.pager:after { + clear: both; +} + +.pager li { + display: inline; +} + +.pager li > a, +.pager li > span { + display: inline-block; + padding: 5px 14px; + background-color: #fff; + border: 1px solid #ddd; + -webkit-border-radius: 15px; + -moz-border-radius: 15px; + border-radius: 15px; +} + +.pager li > a:hover, +.pager li > a:focus { + text-decoration: none; + background-color: #f5f5f5; +} + +.pager .next > a, +.pager .next > span { + float: right; +} + +.pager .previous > a, +.pager .previous > span { + float: left; +} + +.pager .disabled > a, +.pager .disabled > a:hover, +.pager .disabled > a:focus, +.pager .disabled > span { + color: #999999; + cursor: default; + background-color: #fff; +} + +.modal-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1040; + background-color: #000000; +} + +.modal-backdrop.fade { + opacity: 0; +} + +.modal-backdrop, +.modal-backdrop.fade.in { + opacity: 0.8; + filter: alpha(opacity=80); +} + +.modal { + position: fixed; + top: 10%; + left: 50%; + z-index: 1050; + width: 560px; + margin-left: -280px; + background-color: #ffffff; + border: 1px solid #999; + border: 1px solid rgba(0, 0, 0, 0.3); + *border: 1px solid #999; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; + outline: none; + -webkit-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); + -moz-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); + box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); + -webkit-background-clip: padding-box; + -moz-background-clip: padding-box; + background-clip: padding-box; +} + +.modal.fade { + top: -25%; + -webkit-transition: opacity 0.3s linear, top 0.3s ease-out; + -moz-transition: opacity 0.3s linear, top 0.3s ease-out; + -o-transition: opacity 0.3s linear, top 0.3s ease-out; + transition: opacity 0.3s linear, top 0.3s ease-out; +} + +.modal.fade.in { + top: 10%; +} + +.modal-header { + padding: 9px 15px; + border-bottom: 1px solid #eee; +} + +.modal-header .close { + margin-top: 2px; +} + +.modal-header h3 { + margin: 0; + line-height: 30px; +} + +.modal-body { + position: relative; + max-height: 400px; + padding: 15px; + overflow-y: auto; +} + +.modal-form { + margin-bottom: 0; +} + +.modal-footer { + padding: 14px 15px 15px; + margin-bottom: 0; + text-align: right; + background-color: #f5f5f5; + border-top: 1px solid #ddd; + -webkit-border-radius: 0 0 6px 6px; + -moz-border-radius: 0 0 6px 6px; + border-radius: 0 0 6px 6px; + *zoom: 1; + -webkit-box-shadow: inset 0 1px 0 #ffffff; + -moz-box-shadow: inset 0 1px 0 #ffffff; + box-shadow: inset 0 1px 0 #ffffff; +} + +.modal-footer:before, +.modal-footer:after { + display: table; + line-height: 0; + content: ""; +} + +.modal-footer:after { + clear: both; +} + +.modal-footer .btn + .btn { + margin-bottom: 0; + margin-left: 5px; +} + +.modal-footer .btn-group .btn + .btn { + margin-left: -1px; +} + +.modal-footer .btn-block + .btn-block { + margin-left: 0; +} + +.tooltip { + position: absolute; + z-index: 1030; + display: block; + font-size: 11px; + line-height: 1.4; + opacity: 0; + filter: alpha(opacity=0); + visibility: visible; +} + +.tooltip.in { + opacity: 0.8; + filter: alpha(opacity=80); +} + +.tooltip.top { + padding: 5px 0; + margin-top: -3px; +} + +.tooltip.right { + padding: 0 5px; + margin-left: 3px; +} + +.tooltip.bottom { + padding: 5px 0; + margin-top: 3px; +} + +.tooltip.left { + padding: 0 5px; + margin-left: -3px; +} + +.tooltip-inner { + max-width: 200px; + padding: 8px; + color: #ffffff; + text-align: center; + text-decoration: none; + background-color: #000000; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.tooltip-arrow { + position: absolute; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} + +.tooltip.top .tooltip-arrow { + bottom: 0; + left: 50%; + margin-left: -5px; + border-top-color: #000000; + border-width: 5px 5px 0; +} + +.tooltip.right .tooltip-arrow { + top: 50%; + left: 0; + margin-top: -5px; + border-right-color: #000000; + border-width: 5px 5px 5px 0; +} + +.tooltip.left .tooltip-arrow { + top: 50%; + right: 0; + margin-top: -5px; + border-left-color: #000000; + border-width: 5px 0 5px 5px; +} + +.tooltip.bottom .tooltip-arrow { + top: 0; + left: 50%; + margin-left: -5px; + border-bottom-color: #000000; + border-width: 0 5px 5px; +} + +.popover { + position: absolute; + top: 0; + left: 0; + z-index: 1010; + display: none; + max-width: 276px; + padding: 1px; + text-align: left; + white-space: normal; + background-color: #ffffff; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, 0.2); + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; + -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + -webkit-background-clip: padding-box; + -moz-background-clip: padding; + background-clip: padding-box; +} + +.popover.top { + margin-top: -10px; +} + +.popover.right { + margin-left: 10px; +} + +.popover.bottom { + margin-top: 10px; +} + +.popover.left { + margin-left: -10px; +} + +.popover-title { + padding: 8px 14px; + margin: 0; + font-size: 14px; + font-weight: normal; + line-height: 18px; + background-color: #f7f7f7; + border-bottom: 1px solid #ebebeb; + -webkit-border-radius: 5px 5px 0 0; + -moz-border-radius: 5px 5px 0 0; + border-radius: 5px 5px 0 0; +} + +.popover-title:empty { + display: none; +} + +.popover-content { + padding: 9px 14px; +} + +.popover .arrow, +.popover .arrow:after { + position: absolute; + display: block; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} + +.popover .arrow { + border-width: 11px; +} + +.popover .arrow:after { + border-width: 10px; + content: ""; +} + +.popover.top .arrow { + bottom: -11px; + left: 50%; + margin-left: -11px; + border-top-color: #999; + border-top-color: rgba(0, 0, 0, 0.25); + border-bottom-width: 0; +} + +.popover.top .arrow:after { + bottom: 1px; + margin-left: -10px; + border-top-color: #ffffff; + border-bottom-width: 0; +} + +.popover.right .arrow { + top: 50%; + left: -11px; + margin-top: -11px; + border-right-color: #999; + border-right-color: rgba(0, 0, 0, 0.25); + border-left-width: 0; +} + +.popover.right .arrow:after { + bottom: -10px; + left: 1px; + border-right-color: #ffffff; + border-left-width: 0; +} + +.popover.bottom .arrow { + top: -11px; + left: 50%; + margin-left: -11px; + border-bottom-color: #999; + border-bottom-color: rgba(0, 0, 0, 0.25); + border-top-width: 0; +} + +.popover.bottom .arrow:after { + top: 1px; + margin-left: -10px; + border-bottom-color: #ffffff; + border-top-width: 0; +} + +.popover.left .arrow { + top: 50%; + right: -11px; + margin-top: -11px; + border-left-color: #999; + border-left-color: rgba(0, 0, 0, 0.25); + border-right-width: 0; +} + +.popover.left .arrow:after { + right: 1px; + bottom: -10px; + border-left-color: #ffffff; + border-right-width: 0; +} + +.thumbnails { + margin-left: -20px; + list-style: none; + *zoom: 1; +} + +.thumbnails:before, +.thumbnails:after { + display: table; + line-height: 0; + content: ""; +} + +.thumbnails:after { + clear: both; +} + +.row-fluid .thumbnails { + margin-left: 0; +} + +.thumbnails > li { + float: left; + margin-bottom: 20px; + margin-left: 20px; +} + +.thumbnail { + display: block; + padding: 4px; + line-height: 20px; + border: 1px solid #ddd; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.055); + -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.055); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.055); + -webkit-transition: all 0.2s ease-in-out; + -moz-transition: all 0.2s ease-in-out; + -o-transition: all 0.2s ease-in-out; + transition: all 0.2s ease-in-out; +} + +a.thumbnail:hover, +a.thumbnail:focus { + border-color: #0088cc; + -webkit-box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); + -moz-box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); + box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); +} + +.thumbnail > img { + display: block; + max-width: 100%; + margin-right: auto; + margin-left: auto; +} + +.thumbnail .caption { + padding: 9px; + color: #555555; +} + +.media, +.media-body { + overflow: hidden; + *overflow: visible; + zoom: 1; +} + +.media, +.media .media { + margin-top: 15px; +} + +.media:first-child { + margin-top: 0; +} + +.media-object { + display: block; +} + +.media-heading { + margin: 0 0 5px; +} + +.media > .pull-left { + margin-right: 10px; +} + +.media > .pull-right { + margin-left: 10px; +} + +.media-list { + margin-left: 0; + list-style: none; +} + +.label, +.badge { + display: inline-block; + padding: 2px 4px; + font-size: 11.844px; + font-weight: bold; + line-height: 14px; + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + white-space: nowrap; + vertical-align: baseline; + background-color: #999999; +} + +.label { + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} + +.badge { + padding-right: 9px; + padding-left: 9px; + -webkit-border-radius: 9px; + -moz-border-radius: 9px; + border-radius: 9px; +} + +.label:empty, +.badge:empty { + display: none; +} + +a.label:hover, +a.label:focus, +a.badge:hover, +a.badge:focus { + color: #ffffff; + text-decoration: none; + cursor: pointer; +} + +.label-important, +.badge-important { + background-color: #b94a48; +} + +.label-important[href], +.badge-important[href] { + background-color: #953b39; +} + +.label-warning, +.badge-warning { + background-color: #f89406; +} + +.label-warning[href], +.badge-warning[href] { + background-color: #c67605; +} + +.label-success, +.badge-success { + background-color: #468847; +} + +.label-success[href], +.badge-success[href] { + background-color: #356635; +} + +.label-info, +.badge-info { + background-color: #3a87ad; +} + +.label-info[href], +.badge-info[href] { + background-color: #2d6987; +} + +.label-inverse, +.badge-inverse { + background-color: #333333; +} + +.label-inverse[href], +.badge-inverse[href] { + background-color: #1a1a1a; +} + +.btn .label, +.btn .badge { + position: relative; + top: -1px; +} + +.btn-mini .label, +.btn-mini .badge { + top: 0; +} + +@-webkit-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} + +@-moz-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} + +@-ms-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} + +@-o-keyframes progress-bar-stripes { + from { + background-position: 0 0; + } + to { + background-position: 40px 0; + } +} + +@keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} + +.progress { + height: 20px; + margin-bottom: 20px; + overflow: hidden; + background-color: #f7f7f7; + background-image: -moz-linear-gradient(top, #f5f5f5, #f9f9f9); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f5f5f5), to(#f9f9f9)); + background-image: -webkit-linear-gradient(top, #f5f5f5, #f9f9f9); + background-image: -o-linear-gradient(top, #f5f5f5, #f9f9f9); + background-image: linear-gradient(to bottom, #f5f5f5, #f9f9f9); + background-repeat: repeat-x; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#fff9f9f9', GradientType=0); + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); + -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); +} + +.progress .bar { + float: left; + width: 0; + height: 100%; + font-size: 12px; + color: #ffffff; + text-align: center; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #0e90d2; + background-image: -moz-linear-gradient(top, #149bdf, #0480be); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#149bdf), to(#0480be)); + background-image: -webkit-linear-gradient(top, #149bdf, #0480be); + background-image: -o-linear-gradient(top, #149bdf, #0480be); + background-image: linear-gradient(to bottom, #149bdf, #0480be); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff149bdf', endColorstr='#ff0480be', GradientType=0); + -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + -moz-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + -webkit-transition: width 0.6s ease; + -moz-transition: width 0.6s ease; + -o-transition: width 0.6s ease; + transition: width 0.6s ease; +} + +.progress .bar + .bar { + -webkit-box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.15), inset 0 -1px 0 rgba(0, 0, 0, 0.15); + -moz-box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.15), inset 0 -1px 0 rgba(0, 0, 0, 0.15); + box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.15), inset 0 -1px 0 rgba(0, 0, 0, 0.15); +} + +.progress-striped .bar { + background-color: #149bdf; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + -webkit-background-size: 40px 40px; + -moz-background-size: 40px 40px; + -o-background-size: 40px 40px; + background-size: 40px 40px; +} + +.progress.active .bar { + -webkit-animation: progress-bar-stripes 2s linear infinite; + -moz-animation: progress-bar-stripes 2s linear infinite; + -ms-animation: progress-bar-stripes 2s linear infinite; + -o-animation: progress-bar-stripes 2s linear infinite; + animation: progress-bar-stripes 2s linear infinite; +} + +.progress-danger .bar, +.progress .bar-danger { + background-color: #dd514c; + background-image: -moz-linear-gradient(top, #ee5f5b, #c43c35); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#c43c35)); + background-image: -webkit-linear-gradient(top, #ee5f5b, #c43c35); + background-image: -o-linear-gradient(top, #ee5f5b, #c43c35); + background-image: linear-gradient(to bottom, #ee5f5b, #c43c35); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffee5f5b', endColorstr='#ffc43c35', GradientType=0); +} + +.progress-danger.progress-striped .bar, +.progress-striped .bar-danger { + background-color: #ee5f5b; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} + +.progress-success .bar, +.progress .bar-success { + background-color: #5eb95e; + background-image: -moz-linear-gradient(top, #62c462, #57a957); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#57a957)); + background-image: -webkit-linear-gradient(top, #62c462, #57a957); + background-image: -o-linear-gradient(top, #62c462, #57a957); + background-image: linear-gradient(to bottom, #62c462, #57a957); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff62c462', endColorstr='#ff57a957', GradientType=0); +} + +.progress-success.progress-striped .bar, +.progress-striped .bar-success { + background-color: #62c462; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} + +.progress-info .bar, +.progress .bar-info { + background-color: #4bb1cf; + background-image: -moz-linear-gradient(top, #5bc0de, #339bb9); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#339bb9)); + background-image: -webkit-linear-gradient(top, #5bc0de, #339bb9); + background-image: -o-linear-gradient(top, #5bc0de, #339bb9); + background-image: linear-gradient(to bottom, #5bc0de, #339bb9); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff339bb9', GradientType=0); +} + +.progress-info.progress-striped .bar, +.progress-striped .bar-info { + background-color: #5bc0de; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} + +.progress-warning .bar, +.progress .bar-warning { + background-color: #faa732; + background-image: -moz-linear-gradient(top, #fbb450, #f89406); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406)); + background-image: -webkit-linear-gradient(top, #fbb450, #f89406); + background-image: -o-linear-gradient(top, #fbb450, #f89406); + background-image: linear-gradient(to bottom, #fbb450, #f89406); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450', endColorstr='#fff89406', GradientType=0); +} + +.progress-warning.progress-striped .bar, +.progress-striped .bar-warning { + background-color: #fbb450; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} + +.accordion { + margin-bottom: 20px; +} + +.accordion-group { + margin-bottom: 2px; + border: 1px solid #e5e5e5; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.accordion-heading { + border-bottom: 0; +} + +.accordion-heading .accordion-toggle { + display: block; + padding: 8px 15px; +} + +.accordion-toggle { + cursor: pointer; +} + +.accordion-inner { + padding: 9px 15px; + border-top: 1px solid #e5e5e5; +} + +.carousel { + position: relative; + margin-bottom: 20px; + line-height: 1; +} + +.carousel-inner { + position: relative; + width: 100%; + overflow: hidden; +} + +.carousel-inner > .item { + position: relative; + display: none; + -webkit-transition: 0.6s ease-in-out left; + -moz-transition: 0.6s ease-in-out left; + -o-transition: 0.6s ease-in-out left; + transition: 0.6s ease-in-out left; +} + +.carousel-inner > .item > img, +.carousel-inner > .item > a > img { + display: block; + line-height: 1; +} + +.carousel-inner > .active, +.carousel-inner > .next, +.carousel-inner > .prev { + display: block; +} + +.carousel-inner > .active { + left: 0; +} + +.carousel-inner > .next, +.carousel-inner > .prev { + position: absolute; + top: 0; + width: 100%; +} + +.carousel-inner > .next { + left: 100%; +} + +.carousel-inner > .prev { + left: -100%; +} + +.carousel-inner > .next.left, +.carousel-inner > .prev.right { + left: 0; +} + +.carousel-inner > .active.left { + left: -100%; +} + +.carousel-inner > .active.right { + left: 100%; +} + +.carousel-control { + position: absolute; + top: 40%; + left: 15px; + width: 40px; + height: 40px; + margin-top: -20px; + font-size: 60px; + font-weight: 100; + line-height: 30px; + color: #ffffff; + text-align: center; + background: #222222; + border: 3px solid #ffffff; + -webkit-border-radius: 23px; + -moz-border-radius: 23px; + border-radius: 23px; + opacity: 0.5; + filter: alpha(opacity=50); +} + +.carousel-control.right { + right: 15px; + left: auto; +} + +.carousel-control:hover, +.carousel-control:focus { + color: #ffffff; + text-decoration: none; + opacity: 0.9; + filter: alpha(opacity=90); +} + +.carousel-indicators { + position: absolute; + top: 15px; + right: 15px; + z-index: 5; + margin: 0; + list-style: none; +} + +.carousel-indicators li { + display: block; + float: left; + width: 10px; + height: 10px; + margin-left: 5px; + text-indent: -999px; + background-color: #ccc; + background-color: rgba(255, 255, 255, 0.25); + border-radius: 5px; +} + +.carousel-indicators .active { + background-color: #fff; +} + +.carousel-caption { + position: absolute; + right: 0; + bottom: 0; + left: 0; + padding: 15px; + background: #333333; + background: rgba(0, 0, 0, 0.75); +} + +.carousel-caption h4, +.carousel-caption p { + line-height: 20px; + color: #ffffff; +} + +.carousel-caption h4 { + margin: 0 0 5px; +} + +.carousel-caption p { + margin-bottom: 0; +} + +.hero-unit { + padding: 60px; + margin-bottom: 30px; + font-size: 18px; + font-weight: 200; + line-height: 30px; + color: inherit; + background-color: #eeeeee; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; +} + +.hero-unit h1 { + margin-bottom: 0; + font-size: 60px; + line-height: 1; + letter-spacing: -1px; + color: inherit; +} + +.hero-unit li { + line-height: 30px; +} + +.pull-right { + float: right; +} + +.pull-left { + float: left; +} + +.hide { + display: none; +} + +.show { + display: block; +} + +.invisible { + visibility: hidden; +} + +.affix { + position: fixed; +} -- cgit 1.4.1 From 287f7fda8c441367f4b8b2bb7d13cd905c01a89e Mon Sep 17 00:00:00 2001 From: "\"Vincent Ambo ext:(%22)" <tazjin@gmail.com> Date: Sun, 5 May 2013 18:30:25 +0200 Subject: * fixed date formatting --- src/Locales.hs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Locales.hs b/src/Locales.hs index 09911c302e..834bf7e3de 100644 --- a/src/Locales.hs +++ b/src/Locales.hs @@ -86,8 +86,8 @@ readMore DE = "Weiterlesen" readMore EN = "Read more" eTimeFormat :: BlogLang -> String -eTimeFormat DE = "Geschrieben am %d.%m.%y von " -eTimeFormat EN = "Written on %D by " +eTimeFormat DE = "Geschrieben am %Y-%m-%d von " +eTimeFormat EN = "Written on %Y-%m-%d by " -- contact information contactText :: BlogLang -> Text @@ -121,8 +121,8 @@ cSingle DE = "Kommentar:" --input label cSingle EN = "Comment:" cTimeFormat :: BlogLang -> String --formatTime expects a String -cTimeFormat DE = "[Am %d.%m.%y um %H:%M Uhr]" -cTimeFormat EN = "[On %D at %H:%M]" +cTimeFormat DE = "[Am %Y-%m-%d um %H:%M Uhr]" +cTimeFormat EN = "[On %Y-%m-%d at %H:%M]" cSend :: BlogLang -> Text cSend DE = "Absenden" -- cgit 1.4.1 From 514d2b277715392549affb8d0d9cc699242b0172 Mon Sep 17 00:00:00 2001 From: "\"Vincent Ambo ext:(%22)" <tazjin@gmail.com> Date: Sun, 5 May 2013 19:57:46 +0200 Subject: * further work on Bootstrapping --- src/Blog.hs | 99 ++++++++++++++++++++++++++++++++-------------------------- src/Locales.hs | 2 +- 2 files changed, 56 insertions(+), 45 deletions(-) diff --git a/src/Blog.hs b/src/Blog.hs index 2011a0fb27..5919fb8bfc 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -123,9 +123,10 @@ renderEntries showAll entries topText footerLinks = [shamlet| <div .row> <div .span12> <p> - <span class="innerTitle">#{topText} + <span class="innerTitle"> + <b>#{topText} $forall entry <- elist - <div .row > + <div .row> <div .span2> <a #bar href=#{linkElems entry}> <b>#{title entry} @@ -141,10 +142,9 @@ $forall entry <- elist ^{preEscapedToHtml $ append " " $ btext entry} $if ((/=) (mtext entry) empty) <p> - <a #foo href=#{linkElems entry}>#{readMore $ lang entry} + <a .readmore #foo href=#{linkElems entry}>#{readMore $ lang entry} $else <br>  - <hr> $maybe links <- footerLinks ^{links} |] @@ -174,39 +174,68 @@ showLinks Nothing lang = [shamlet| renderEntry :: Entry -> Html renderEntry e@Entry{..} = [shamlet| -<span class="innerTitle">#{title} -<span class="righttext"> - <i>#{woText} -<div class="innerContainer"> - <article> - <ul style="max-width:57em;"> - <li> - $if (isEntryMarkdown e) - ^{renderEntryMarkdown btext} - <p>^{renderEntryMarkdown $ mtext} - $else - ^{preEscapedToHtml $ btext} - <p>^{preEscapedToHtml $ mtext} - <div class="innerBoxComments"> - <div class="cHead">#{cHead lang}: - <ul style="max-width:57em;">#{renderComments comments lang} - ^{renderCommentBox lang entryId} +<div .row .pusher> + <div .span9> + <span .boldify>#{title} + <div .span3> + <span .righttext><i>#{woText}</i> +<div .row .innerContainer> + <div .span10> + <article> + $if (isEntryMarkdown e) + ^{renderEntryMarkdown btext} + <p>^{renderEntryMarkdown $ mtext} + $else + ^{preEscapedToHtml $ btext} + <p>^{preEscapedToHtml $ mtext} +<div .row .innerBoxComments> + <div .span10> + <div .boldify>#{cHead lang}: +#{renderComments comments lang} +<div .row .innerBoxComments> + <div .span10> + <div .boldify>#{cwHead lang} +^{renderCommentBox lang entryId} |] where woText = flip T.append author $ T.pack $ formatTime defaultTimeLocale (eTimeFormat lang) edate renderComments :: [Comment] -> BlogLang -> Html -renderComments [] lang = [shamlet|<li>#{noComments lang}|] +renderComments [] lang = [shamlet| +<div .row> + <div .span10>#{noComments lang} +|] renderComments comments lang = [shamlet| $forall comment <- comments - <li> - <i>#{append (cauthor comment) ": "} - ^{preEscapedToHtml $ ctext comment} - <p class="tt">#{timeString $ cdate comment} + <div .row> + <div .span1 .commentname> + <i>#{append (cauthor comment) ": "} + <div .span9> + ^{preEscapedToHtml $ ctext comment} + <p .tt>#{timeString $ cdate comment} |] where timeString = formatTime defaultTimeLocale (cTimeFormat lang) +renderCommentBox :: BlogLang -> EntryId -> Html +renderCommentBox cLang cId = [shamlet| +^{captchaOptions cLang} +<div .row> + <div .span10> + <form method="POST" action=#{aLink}> + <fieldset> + <label> + <input .span8 name="cname" placeholder="Name" type="text"> + <label> + <textarea .span8 name="ctext" cols="50" rows="13" placeholder=#{cTextPlaceholder cLang}> + ^{captcha} + <label> + <input .btn type="submit" value=#{cSend cLang}> +|] + where + aLink = T.concat ["/", show' cLang, "/postcomment/", show' cId] + + captcha :: Html captcha = [shamlet| <div class="cCaptcha"> @@ -223,24 +252,6 @@ captchaOptions lang = [shamlet|<script type="text/javascript">^{preEscapedToHtml where options = T.concat ["var RecaptchaOptions = { theme: 'clean', lang: '", showLangText lang, "'};"] - -renderCommentBox :: BlogLang -> EntryId -> Html -renderCommentBox cLang cId = [shamlet| -<div class="cHead">#{cwHead cLang} -^{captchaOptions cLang} -<form method="POST" action=#{aLink}> - <p><input name="cname" placeholder="Name" class="cInput"> - <p> - <label> - <textarea name="ctext" cols="50" rows="13" class="cInput" placeholder=#{cTextPlaceholder cLang}> - <p> - <label> - ^{captcha} - <p><input class="cInput" style="width:120px;" type="submit" value=#{cSend cLang}> -|] - where - aLink = T.concat ["/", show' cLang, "/postcomment/", show' cId] - showSiteNotice :: Html showSiteNotice = [shamlet| $doctype 5 diff --git a/src/Locales.hs b/src/Locales.hs index 834bf7e3de..67c55a81dc 100644 --- a/src/Locales.hs +++ b/src/Locales.hs @@ -160,5 +160,5 @@ rightText EN = "Deutsche Version <a href=\"/de\" class=\"link\">hier verfüg -- static information repoURL :: Text = "https://bitbucket.org/tazjin/tazblog-haskell" -mailTo :: Text = "mailto:tazjin@gmail.com" +mailTo :: Text = "mailto:tazjin+blog@gmail.com" twitter :: Text = "http://twitter.com/#!/tazjin" -- cgit 1.4.1 From 11a51f6abdef96a9c8e49ee7bb23e96aadb55276 Mon Sep 17 00:00:00 2001 From: "\"Vincent Ambo ext:(%22)" <tazjin@gmail.com> Date: Sun, 5 May 2013 20:16:49 +0200 Subject: * fixed comments and footer in Bootstrap --- src/Blog.hs | 53 ++++++++++++++++++++++++++++------------------------- 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/src/Blog.hs b/src/Blog.hs index 5919fb8bfc..8a7d49b921 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -71,23 +71,21 @@ $doctype 5 <link rel="alternate" type="application/rss+xml" title="RSS-Feed" href=#{rssUrl}> <meta http-equiv="content-type" content="text/html;charset=UTF-8"> <body> - <div .container .header> - <div .row> - <div .span12 .blogtitle> - <a class="btitle" href=#{append "/" (show' lang)}>#{blogTitle lang empty} - <div .row> - <br> - <div .span6> - <span .contacts #cosx>^{contactInfo} - <div .span6> - <span .righttext>^{preEscapedToHtml $ rightText lang} + <div .header> + <div .container > + <div .row> + <div .span12 .blogtitle> + <a class="btitle" href=#{append "/" (show' lang)}>#{blogTitle lang empty} + <div .row> + <br> + <div .span6> + <span .contacts #cosx>^{contactInfo} + <div .span6> + <span .righttext>^{preEscapedToHtml $ rightText lang} <div .container> ^{body} - <div .container> - <footer> - ^{showFooter lang $ pack version} - <div class="centerbox"> - <span style="font-size:17px;font-family:Helvetica;">ಠ_ಠ + <footer .footer> + ^{showFooter lang $ pack version} |] where rssUrl = T.concat ["/", show' lang, "/rss.xml"] @@ -100,16 +98,21 @@ $doctype 5 showFooter :: BlogLang -> Text -> Html showFooter l v = [shamlet| -<div class="rightbox" style="text-align:right;"> - Proudly made with # - <a class="link" href="http://haskell.org">Haskell - , # - <a class="link" href="http://hackage.haskell.org/package/acid-state-0.6.3">Acid-State - \ and without PHP, Java, Perl, MySQL and Python. - <p> - <a class="link" href=#{repoURL}>#{append "Version " v} -   - <a class="link" href="/notice">#{noticeText l} +<div .container> + <div .row> + <div .span12 .righttext style="text-align: right;margin-right:-200px"> + Proudly made with # + <a class="link" href="http://haskell.org">Haskell + , # + <a class="link" href="http://hackage.haskell.org/package/acid-state-0.6.3">Acid-State + \ and without PHP, Java, Perl, MySQL and Python. + <p> + <a class="link" href=#{repoURL}>#{append "Version " v} +   + <a class="link" href="/notice">#{noticeText l} + <div .row .text-center> + <div .span12> + <span style="font-size:13px;font-family:Helvetica;">ಠ_ಠ |] isEntryMarkdown :: Entry -> Bool -- cgit 1.4.1 From d7cce6cb413a3a1067ef4e6c72107a21983453a4 Mon Sep 17 00:00:00 2001 From: "\"Vincent Ambo ext:(%22)" <tazjin@gmail.com> Date: Sun, 5 May 2013 20:34:06 +0200 Subject: * more bootstrap-related fixes, nearing completion * version bumped to 4.0 --- TazBlog.cabal | 2 +- src/Blog.hs | 22 ++++++++++++---------- src/Locales.hs | 4 ++-- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/TazBlog.cabal b/TazBlog.cabal index 1295363f60..295d4e8ae3 100644 --- a/TazBlog.cabal +++ b/TazBlog.cabal @@ -1,5 +1,5 @@ Name: TazBlog -Version: 3.5 +Version: 4.0 Synopsis: Tazjin's Blog License-file: LICENSE Author: Vincent Ambo diff --git a/src/Blog.hs b/src/Blog.hs index 8a7d49b921..4c8d19aa31 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -138,7 +138,7 @@ $forall entry <- elist <br> #{linkText $ length $ comments entry} #{cHead $ lang entry} - <div .span10> + <div .span10 .entry> $if (isEntryMarkdown entry) ^{renderEntryMarkdown $ append " " $ btext entry} $else @@ -158,19 +158,21 @@ $maybe links <- footerLinks showLinks :: Maybe Int -> BlogLang -> Html showLinks (Just i) lang = [shamlet| - $if ((>) i 1) - <div class="centerbox"> - <a href=#{nLink $ succ i}>#{backText lang} - \ -- # - <a href=#{nLink $ pred i}>#{nextText lang} - $elseif ((<=) i 1) - ^{showLinks Nothing lang} + $if ((>) i 1) + <div .row .text-center> + <div .span12> + <a href=#{nLink $ succ i}>#{backText lang} + \ -- # + <a href=#{nLink $ pred i}>#{nextText lang} + $elseif ((<=) i 1) + ^{showLinks Nothing lang} |] where nLink page = T.concat ["/", show' lang, "/?page=", show' page] showLinks Nothing lang = [shamlet| -<div class="centerbox"> - <a href=#{nLink}>#{backText lang} +<div .row .text-center> + <div .span12> + <a href=#{nLink}>#{backText lang} |] where nLink = T.concat ["/", show' lang, "/?page=2"] diff --git a/src/Locales.hs b/src/Locales.hs index 67c55a81dc..34d3fadf25 100644 --- a/src/Locales.hs +++ b/src/Locales.hs @@ -18,7 +18,7 @@ import BlogDB (BlogLang (..)) data BlogError = NotFound | DBError -version = "3.5" +version = "4.0" allLang = [EN, DE] @@ -159,6 +159,6 @@ rightText DE = "English version <a href=\"/en\" class=\"link\">available here</a rightText EN = "Deutsche Version <a href=\"/de\" class=\"link\">hier verfügbar</a>." -- static information -repoURL :: Text = "https://bitbucket.org/tazjin/tazblog-haskell" +repoURL :: Text = "http://hg.tazj.in/tazblog-haskell" mailTo :: Text = "mailto:tazjin+blog@gmail.com" twitter :: Text = "http://twitter.com/#!/tazjin" -- cgit 1.4.1 From 67b50be2fe10b7bc829169606fb8def5c808726a Mon Sep 17 00:00:00 2001 From: "\"Vincent Ambo ext:(%22)" <tazjin@gmail.com> Date: Sun, 5 May 2013 20:40:05 +0200 Subject: * smaller HTML fixes --- src/Blog.hs | 4 ++-- src/Main.hs | 2 +- src/RSS.hs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Blog.hs b/src/Blog.hs index 4c8d19aa31..e45d793b27 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -131,7 +131,7 @@ renderEntries showAll entries topText footerLinks = [shamlet| $forall entry <- elist <div .row> <div .span2> - <a #bar href=#{linkElems entry}> + <a href=#{linkElems entry}> <b>#{title entry} <br> <i>#{pack $ formatTime defaultTimeLocale "%Y-%M-%d" $ edate entry} @@ -145,7 +145,7 @@ $forall entry <- elist ^{preEscapedToHtml $ append " " $ btext entry} $if ((/=) (mtext entry) empty) <p> - <a .readmore #foo href=#{linkElems entry}>#{readMore $ lang entry} + <a .readmore href=#{linkElems entry}>#{readMore $ lang entry} $else <br>  $maybe links <- footerLinks diff --git a/src/Main.hs b/src/Main.hs index 82e6c5663a..25e7539a3e 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -164,7 +164,7 @@ addComment acid lang captchakey eId = do -- captcha verification challenge <- look "recaptcha_challenge_field" response <- look "recaptcha_response_field" - (userIp, _) <- liftM rqPeer askRq -- FIXME askRq >>= return . rqPeer + (userIp, _) <- liftM rqPeer askRq validation <- liftIO $ validateCaptcha captchakey userIp challenge response case validation of Right _ -> update' acid (AddComment eId nComment) diff --git a/src/RSS.hs b/src/RSS.hs index 045702ece4..2309b1297c 100644 --- a/src/RSS.hs +++ b/src/RSS.hs @@ -16,7 +16,7 @@ import Locales createChannel :: BlogLang -> UTCTime -> [ChannelElem] createChannel l now = [ Language $ show l , Copyright "Vincent Ambo" - , WebMaster "tazjin@googlemail.com" + , WebMaster "tazjin@gmail.com" , ChannelPubDate now ] -- cgit 1.4.1 From d860239863acd8b559ae914959544d647137d5f9 Mon Sep 17 00:00:00 2001 From: "\"Vincent Ambo ext:(%22)" <tazjin@gmail.com> Date: Sun, 5 May 2013 21:05:05 +0200 Subject: * sticky footer --- src/Blog.hs | 51 ++++++++++++++++++++------------------------------- 1 file changed, 20 insertions(+), 31 deletions(-) diff --git a/src/Blog.hs b/src/Blog.hs index e45d793b27..0defa7ba19 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -45,20 +45,7 @@ markdownCutoff = fromJust $ parseTime defaultTimeLocale "%s" "1367149834" stylesheetSource = $(luciusFile "../res/blogbs.lucius") blogStyle = renderCssUrl undefined stylesheetSource --- <link rel="stylesheet" type="text/css" href="/static/blogv34.css" media="all"> - -{- - <div class="header"> - <p style="clear: both;"> - <span class="contacts" id="cosx">^{contactInfo} - <span class="righttext">^{preEscapedToHtml $ rightText lang} - <div class="middle"> - ^{body} - <div class="footer"> - ^{showFooter lang $ pack version} - <div class="centerbox"> - <span style="font-size:17px;font-family:Helvetica;">ಠ_ಠ --} +-- <link rel="stylesheet" type="text/css" href="/static/blogv34.css" media="all"> FIXME -- blog HTML blogTemplate :: BlogLang -> Text -> Html -> Html @@ -71,19 +58,20 @@ $doctype 5 <link rel="alternate" type="application/rss+xml" title="RSS-Feed" href=#{rssUrl}> <meta http-equiv="content-type" content="text/html;charset=UTF-8"> <body> - <div .header> - <div .container > - <div .row> - <div .span12 .blogtitle> - <a class="btitle" href=#{append "/" (show' lang)}>#{blogTitle lang empty} - <div .row> - <br> - <div .span6> - <span .contacts #cosx>^{contactInfo} - <div .span6> - <span .righttext>^{preEscapedToHtml $ rightText lang} - <div .container> - ^{body} + <div #wrap> + <div .header> + <div .container > + <div .row> + <div .span12 .blogtitle> + <a class="btitle" href=#{append "/" (show' lang)}>#{blogTitle lang empty} + <div .row> + <br> + <div .span6> + <span .contacts #cosx>^{contactInfo} + <div .span6> + <span .righttext>^{preEscapedToHtml $ rightText lang} + <div .container> + ^{body} <footer .footer> ^{showFooter lang $ pack version} |] @@ -398,9 +386,10 @@ commentDeleted eId = adminTemplate "Kommentar gelöscht" $ [shamlet| showError :: BlogError -> BlogLang -> Html showError NotFound l = blogTemplate l (T.append ": " $ notFoundTitle l) $ [shamlet| -<span class="innerTitle">#{notFoundTitle l} -<div class="innerTitle"> - <p class="notFoundFace">:( - <p class="notFoundText">#{notFoundText l} +<div .row .text-center> + <div .span12 .notFoundFace>:( +<div .row .text-center> + <div .span12 .notFoundText> + #{notFoundText l} |] -- cgit 1.4.1 From c7a538e7d19820aab6228197d8dc68a78404940c Mon Sep 17 00:00:00 2001 From: "\"Vincent Ambo ext:(%22)" <tazjin@gmail.com> Date: Sun, 5 May 2013 21:06:41 +0200 Subject: * added bootstrap file * moved CSS to correct paths --- res/blog.lucius | 195 ++++++++++++++++++++++++++++++++++++++++++++ res/blogstyle.lucius | 223 --------------------------------------------------- res/bootstrap.min.js | 6 ++ src/Blog.hs | 6 +- 4 files changed, 203 insertions(+), 227 deletions(-) create mode 100644 res/blog.lucius delete mode 100644 res/blogstyle.lucius create mode 100644 res/bootstrap.min.js diff --git a/res/blog.lucius b/res/blog.lucius new file mode 100644 index 0000000000..ae4c896b78 --- /dev/null +++ b/res/blog.lucius @@ -0,0 +1,195 @@ +@charset "UTF-8"; +/* CSS Document */ +@import url(http://fonts.googleapis.com/css?family=Droid+Sans+Mono); +@import url(http://fonts.googleapis.com/css?family=PT+Sans); + +body { + font-family: 'PT Sans', sans-serif; + min-height: 850px; + background: url(/static/bg.gif); +} + +html, body { + height: 100%; +} + +a { + color: black; +} + +article a, .entry a { + text-decoration: underline; +} + +#wrap { + min-height: 100%; + height: auto !important; + height: 100%; +} + +.readmore { + text-decoration: underline; +} + +.header { + color: #EEE; + background: url(/static/hbg.jpg); + z-index: 4; + padding-left: 20px; + padding-bottom: 30px; + padding-top: 30px; + position: relative; + box-shadow: 0 6px 5px 1px #343537; + margin-bottom: 25px; + padding-left: 0px; +} + +.footer { + background: url(/static/hbg.jpg); + height: 90; + z-index: 4; + position: relative; + background-color: #4A525A; + margin-top: 30px; + padding-top: 20px; + box-shadow: 0 -6px 5px 1px #343537; + color: #EEE; +} + + +.header a, .footer a { + color: #EEE; + text-decoration: none; +} + + +.boldify { + font-size:large; + font-weight:bold; +} + +.pusher { + padding-bottom: 20px; +} + +.innerBoxComments { + padding-top: 20px; +} + +.cCaptcha { + padding: 5px; + border: 1px solid #555; + border-radius: 0.5em; + width: 606px; + background: #F9F9F9; +} + +.tt { + font-family: "courier new",courier,monospace; + font-size: 13px; +} + +.btitle { + text-decoration:none; + //color: #EEE; + font-size:x-large; + font-weight:bold; + margin-top: 15px; + padding-bottom: 25px; + padding-top: 20px; +} + +.contacts { + float: left; + font-weight: bolder; +} + +.righttext { + float:right; + padding-right: 20px; +} + +.commentname { + text-align: right; +} + +.notFoundFace { + height: 100px; + padding-top: 50px; + font-size:100px +} + +.notFoundText { + font-size:24px; + font-weight:bold +} + +/* HsColour style */ + +.code +{ + box-shadow: 3px 3px 5px 1px #888; + border-radius: 10px; + padding: 0.75em; + + font-size: 11pt; + width: 60em; + color: white; + line-height: 1.2em; + font-family: 'Droid Sans Mono', sans-serif; + background: black; + background-image:url('/static/cbg.jpg'); + background-repeat: no-repeat; + } + +.code pre +{ + + font-family: 'Droid Sans Mono', sans-serif; +} + +kbd +{ + font-family: 'Droid Sans Mono', sans-serif; + color: #333; + font-size: 0.8em; +} + +.wide +{ + width: 90em; +} + + +code +{ + line-height: 1.5em; + border: 1px; + } + +.source-code +{ + font-size: 0.75em; + color: #666; + } + +.warning +{ + color: red; + } + + +.hs-keyglyph { color: DarkGoldenrod; } +.hs-layout { color: white;} +.hs-keyword { color: skyblue; } +.hs-comment, .hs-comment a { color: cadetblue;} +.hs-str { color: Darkorange; } +.hs-chr { color: RosyBrown;} +.hs-conid { color: GreenYellow; } +.hs-varid { color: white; } +.hs-num { color: white; } +.hs-varop { color: DarkGoldenrod; } +.hs-conop { color: DarkGoldenrod; } +.hs-sel { color: FireBrick; } +.hs-cpp { color: yellow; } +.hs-definition { color: gold; } diff --git a/res/blogstyle.lucius b/res/blogstyle.lucius deleted file mode 100644 index c2cacfd011..0000000000 --- a/res/blogstyle.lucius +++ /dev/null @@ -1,223 +0,0 @@ -@charset "UTF-8"; -/* CSS Document */ -@import url(http://fonts.googleapis.com/css?family=Droid+Sans+Mono); -@import url(http://fonts.googleapis.com/css?family=PT+Sans); - -html, body{ - margin: 0; - padding: 0; -} - -body { - font-family: 'PT Sans', sans-serif; - min-height: 850px; - color: #EEE; -} - -a { - color: black; -} - -input, textarea, select { - border: 1px solid #555; - padding: 0.5em; - font-size: 15px; - line-height: 1.2em; - width: 550px; - background: #F9F9F9; - -webkit-border-radius: 0.5em; - } - -/* site sections */ -.header { - background: url(/static/hbg.jpg); - z-index: 4; - padding-left: 20px; - padding-bottom: 70px; - padding-top: 30px; - position: relative; - box-shadow: 0 6px 5px 1px #343537; -} - -.link { - color: #EEE; -} - -.middle { - position: relative; - z-index: 2; - display: block; - width: 100%; - padding-top: 40px; - background: url(/static/bg.gif); - color: black; -} - -.footer { - background: url(/static/hbg.jpg); - z-index: 4; - position: relative; - background-color: #4A525A; - margin-top: 30px; - padding-top: 20px; - box-shadow: 0 -6px 5px 1px #343537; - color: #EEE; -} - -/* header elements */ - -.btitle { - text-decoration:none; - color: #EEE; - font-size:x-large; - font-weight:bold; - margin-top: 15px; - margin-bottom: 10px; -} - -.contacts { - float: left; - font-weight: bolder; -} - -.righttext { - float:right; - padding-right: 20px; -} - -.rightbox { - text-align:right; - padding-right: 14px; -} - -/* middle elements */ -.innerTitle { - margin-left: 10px; - font-weight: bold; -} - -.innerBoxComments{ - margin-left: 10px; -} - -.innerContainer { - padding-right: 20px; - -} - -/* common elements */ -.centerbox { - text-align:center; - min-height: 45px; -} - - -/* style elements */ - -.cInput { - margin-left: 15px; -} - -.cCaptcha { - padding: 5px; - border: 1px solid #555; - -webkit-border-radius: 0.5em; - margin-left: 15px; - width: 555px; - background: #F9F9F9; -} - -.tt { - font-family: "courier new",courier,monospace; - font-size: 13px; -} - -.cl { - text-decoration:none;color:black; -} - -.cHead { - font-size:large; - font-weight:bold; -} - -.notFoundFace { - text-align: center; - font-size: 100px; -} - -.notFoundText { - text-align: center; - font-size: 24px; - font-weight: bold; -} - -/* HsColour style */ - -.code -{ - box-shadow: 3px 3px 5px 1px #888; - border-radius: 10px; - padding: 0.75em; - - font-size: 11pt; - width: 60em; - color: white; - line-height: 1.2em; - font-family: 'Droid Sans Mono', sans-serif; - background: black; - background-image:url('/static/cbg.jpg'); - background-repeat: no-repeat; - } - -.code pre -{ - font-family: 'Droid Sans Mono', sans-serif; -} - -kbd -{ - font-family: 'Droid Sans Mono', sans-serif; - color: #333; - font-size: 0.8em; -} - -.wide -{ - width: 90em; -} - - -code -{ - line-height: 1.5em; - border: 1px; - } - -.source-code -{ - font-size: 0.75em; - color: #666; - } - -.warning -{ - color: red; - } - - -.hs-keyglyph { color: DarkGoldenrod; } -.hs-layout { color: white;} -.hs-keyword { color: skyblue; } -.hs-comment, .hs-comment a { color: cadetblue;} -.hs-str { color: Darkorange; } -.hs-chr { color: RosyBrown;} -.hs-conid { color: GreenYellow; } -.hs-varid { color: white; } -.hs-num { color: white; } -.hs-varop { color: DarkGoldenrod; } -.hs-conop { color: DarkGoldenrod; } -.hs-sel { color: FireBrick; } -.hs-cpp { color: yellow; } -.hs-definition { color: gold; } - diff --git a/res/bootstrap.min.js b/res/bootstrap.min.js new file mode 100644 index 0000000000..95c5ac5ee6 --- /dev/null +++ b/res/bootstrap.min.js @@ -0,0 +1,6 @@ +/*! +* Bootstrap.js by @fat & @mdo +* Copyright 2012 Twitter, Inc. +* http://www.apache.org/licenses/LICENSE-2.0.txt +*/ +!function(e){"use strict";e(function(){e.support.transition=function(){var e=function(){var e=document.createElement("bootstrap"),t={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"},n;for(n in t)if(e.style[n]!==undefined)return t[n]}();return e&&{end:e}}()})}(window.jQuery),!function(e){"use strict";var t='[data-dismiss="alert"]',n=function(n){e(n).on("click",t,this.close)};n.prototype.close=function(t){function s(){i.trigger("closed").remove()}var n=e(this),r=n.attr("data-target"),i;r||(r=n.attr("href"),r=r&&r.replace(/.*(?=#[^\s]*$)/,"")),i=e(r),t&&t.preventDefault(),i.length||(i=n.hasClass("alert")?n:n.parent()),i.trigger(t=e.Event("close"));if(t.isDefaultPrevented())return;i.removeClass("in"),e.support.transition&&i.hasClass("fade")?i.on(e.support.transition.end,s):s()};var r=e.fn.alert;e.fn.alert=function(t){return this.each(function(){var r=e(this),i=r.data("alert");i||r.data("alert",i=new n(this)),typeof t=="string"&&i[t].call(r)})},e.fn.alert.Constructor=n,e.fn.alert.noConflict=function(){return e.fn.alert=r,this},e(document).on("click.alert.data-api",t,n.prototype.close)}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=e.extend({},e.fn.button.defaults,n)};t.prototype.setState=function(e){var t="disabled",n=this.$element,r=n.data(),i=n.is("input")?"val":"html";e+="Text",r.resetText||n.data("resetText",n[i]()),n[i](r[e]||this.options[e]),setTimeout(function(){e=="loadingText"?n.addClass(t).attr(t,t):n.removeClass(t).removeAttr(t)},0)},t.prototype.toggle=function(){var e=this.$element.closest('[data-toggle="buttons-radio"]');e&&e.find(".active").removeClass("active"),this.$element.toggleClass("active")};var n=e.fn.button;e.fn.button=function(n){return this.each(function(){var r=e(this),i=r.data("button"),s=typeof n=="object"&&n;i||r.data("button",i=new t(this,s)),n=="toggle"?i.toggle():n&&i.setState(n)})},e.fn.button.defaults={loadingText:"loading..."},e.fn.button.Constructor=t,e.fn.button.noConflict=function(){return e.fn.button=n,this},e(document).on("click.button.data-api","[data-toggle^=button]",function(t){var n=e(t.target);n.hasClass("btn")||(n=n.closest(".btn")),n.button("toggle")})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.$indicators=this.$element.find(".carousel-indicators"),this.options=n,this.options.pause=="hover"&&this.$element.on("mouseenter",e.proxy(this.pause,this)).on("mouseleave",e.proxy(this.cycle,this))};t.prototype={cycle:function(t){return t||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(e.proxy(this.next,this),this.options.interval)),this},getActiveIndex:function(){return this.$active=this.$element.find(".item.active"),this.$items=this.$active.parent().children(),this.$items.index(this.$active)},to:function(t){var n=this.getActiveIndex(),r=this;if(t>this.$items.length-1||t<0)return;return this.sliding?this.$element.one("slid",function(){r.to(t)}):n==t?this.pause().cycle():this.slide(t>n?"next":"prev",e(this.$items[t]))},pause:function(t){return t||(this.paused=!0),this.$element.find(".next, .prev").length&&e.support.transition.end&&(this.$element.trigger(e.support.transition.end),this.cycle(!0)),clearInterval(this.interval),this.interval=null,this},next:function(){if(this.sliding)return;return this.slide("next")},prev:function(){if(this.sliding)return;return this.slide("prev")},slide:function(t,n){var r=this.$element.find(".item.active"),i=n||r[t](),s=this.interval,o=t=="next"?"left":"right",u=t=="next"?"first":"last",a=this,f;this.sliding=!0,s&&this.pause(),i=i.length?i:this.$element.find(".item")[u](),f=e.Event("slide",{relatedTarget:i[0],direction:o});if(i.hasClass("active"))return;this.$indicators.length&&(this.$indicators.find(".active").removeClass("active"),this.$element.one("slid",function(){var t=e(a.$indicators.children()[a.getActiveIndex()]);t&&t.addClass("active")}));if(e.support.transition&&this.$element.hasClass("slide")){this.$element.trigger(f);if(f.isDefaultPrevented())return;i.addClass(t),i[0].offsetWidth,r.addClass(o),i.addClass(o),this.$element.one(e.support.transition.end,function(){i.removeClass([t,o].join(" ")).addClass("active"),r.removeClass(["active",o].join(" ")),a.sliding=!1,setTimeout(function(){a.$element.trigger("slid")},0)})}else{this.$element.trigger(f);if(f.isDefaultPrevented())return;r.removeClass("active"),i.addClass("active"),this.sliding=!1,this.$element.trigger("slid")}return s&&this.cycle(),this}};var n=e.fn.carousel;e.fn.carousel=function(n){return this.each(function(){var r=e(this),i=r.data("carousel"),s=e.extend({},e.fn.carousel.defaults,typeof n=="object"&&n),o=typeof n=="string"?n:s.slide;i||r.data("carousel",i=new t(this,s)),typeof n=="number"?i.to(n):o?i[o]():s.interval&&i.pause().cycle()})},e.fn.carousel.defaults={interval:5e3,pause:"hover"},e.fn.carousel.Constructor=t,e.fn.carousel.noConflict=function(){return e.fn.carousel=n,this},e(document).on("click.carousel.data-api","[data-slide], [data-slide-to]",function(t){var n=e(this),r,i=e(n.attr("data-target")||(r=n.attr("href"))&&r.replace(/.*(?=#[^\s]+$)/,"")),s=e.extend({},i.data(),n.data()),o;i.carousel(s),(o=n.attr("data-slide-to"))&&i.data("carousel").pause().to(o).cycle(),t.preventDefault()})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=e.extend({},e.fn.collapse.defaults,n),this.options.parent&&(this.$parent=e(this.options.parent)),this.options.toggle&&this.toggle()};t.prototype={constructor:t,dimension:function(){var e=this.$element.hasClass("width");return e?"width":"height"},show:function(){var t,n,r,i;if(this.transitioning||this.$element.hasClass("in"))return;t=this.dimension(),n=e.camelCase(["scroll",t].join("-")),r=this.$parent&&this.$parent.find("> .accordion-group > .in");if(r&&r.length){i=r.data("collapse");if(i&&i.transitioning)return;r.collapse("hide"),i||r.data("collapse",null)}this.$element[t](0),this.transition("addClass",e.Event("show"),"shown"),e.support.transition&&this.$element[t](this.$element[0][n])},hide:function(){var t;if(this.transitioning||!this.$element.hasClass("in"))return;t=this.dimension(),this.reset(this.$element[t]()),this.transition("removeClass",e.Event("hide"),"hidden"),this.$element[t](0)},reset:function(e){var t=this.dimension();return this.$element.removeClass("collapse")[t](e||"auto")[0].offsetWidth,this.$element[e!==null?"addClass":"removeClass"]("collapse"),this},transition:function(t,n,r){var i=this,s=function(){n.type=="show"&&i.reset(),i.transitioning=0,i.$element.trigger(r)};this.$element.trigger(n);if(n.isDefaultPrevented())return;this.transitioning=1,this.$element[t]("in"),e.support.transition&&this.$element.hasClass("collapse")?this.$element.one(e.support.transition.end,s):s()},toggle:function(){this[this.$element.hasClass("in")?"hide":"show"]()}};var n=e.fn.collapse;e.fn.collapse=function(n){return this.each(function(){var r=e(this),i=r.data("collapse"),s=e.extend({},e.fn.collapse.defaults,r.data(),typeof n=="object"&&n);i||r.data("collapse",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.collapse.defaults={toggle:!0},e.fn.collapse.Constructor=t,e.fn.collapse.noConflict=function(){return e.fn.collapse=n,this},e(document).on("click.collapse.data-api","[data-toggle=collapse]",function(t){var n=e(this),r,i=n.attr("data-target")||t.preventDefault()||(r=n.attr("href"))&&r.replace(/.*(?=#[^\s]+$)/,""),s=e(i).data("collapse")?"toggle":n.data();n[e(i).hasClass("in")?"addClass":"removeClass"]("collapsed"),e(i).collapse(s)})}(window.jQuery),!function(e){"use strict";function r(){e(t).each(function(){i(e(this)).removeClass("open")})}function i(t){var n=t.attr("data-target"),r;n||(n=t.attr("href"),n=n&&/#/.test(n)&&n.replace(/.*(?=#[^\s]*$)/,"")),r=n&&e(n);if(!r||!r.length)r=t.parent();return r}var t="[data-toggle=dropdown]",n=function(t){var n=e(t).on("click.dropdown.data-api",this.toggle);e("html").on("click.dropdown.data-api",function(){n.parent().removeClass("open")})};n.prototype={constructor:n,toggle:function(t){var n=e(this),s,o;if(n.is(".disabled, :disabled"))return;return s=i(n),o=s.hasClass("open"),r(),o||s.toggleClass("open"),n.focus(),!1},keydown:function(n){var r,s,o,u,a,f;if(!/(38|40|27)/.test(n.keyCode))return;r=e(this),n.preventDefault(),n.stopPropagation();if(r.is(".disabled, :disabled"))return;u=i(r),a=u.hasClass("open");if(!a||a&&n.keyCode==27)return n.which==27&&u.find(t).focus(),r.click();s=e("[role=menu] li:not(.divider):visible a",u);if(!s.length)return;f=s.index(s.filter(":focus")),n.keyCode==38&&f>0&&f--,n.keyCode==40&&f<s.length-1&&f++,~f||(f=0),s.eq(f).focus()}};var s=e.fn.dropdown;e.fn.dropdown=function(t){return this.each(function(){var r=e(this),i=r.data("dropdown");i||r.data("dropdown",i=new n(this)),typeof t=="string"&&i[t].call(r)})},e.fn.dropdown.Constructor=n,e.fn.dropdown.noConflict=function(){return e.fn.dropdown=s,this},e(document).on("click.dropdown.data-api",r).on("click.dropdown.data-api",".dropdown form",function(e){e.stopPropagation()}).on("click.dropdown-menu",function(e){e.stopPropagation()}).on("click.dropdown.data-api",t,n.prototype.toggle).on("keydown.dropdown.data-api",t+", [role=menu]",n.prototype.keydown)}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.options=n,this.$element=e(t).delegate('[data-dismiss="modal"]',"click.dismiss.modal",e.proxy(this.hide,this)),this.options.remote&&this.$element.find(".modal-body").load(this.options.remote)};t.prototype={constructor:t,toggle:function(){return this[this.isShown?"hide":"show"]()},show:function(){var t=this,n=e.Event("show");this.$element.trigger(n);if(this.isShown||n.isDefaultPrevented())return;this.isShown=!0,this.escape(),this.backdrop(function(){var n=e.support.transition&&t.$element.hasClass("fade");t.$element.parent().length||t.$element.appendTo(document.body),t.$element.show(),n&&t.$element[0].offsetWidth,t.$element.addClass("in").attr("aria-hidden",!1),t.enforceFocus(),n?t.$element.one(e.support.transition.end,function(){t.$element.focus().trigger("shown")}):t.$element.focus().trigger("shown")})},hide:function(t){t&&t.preventDefault();var n=this;t=e.Event("hide"),this.$element.trigger(t);if(!this.isShown||t.isDefaultPrevented())return;this.isShown=!1,this.escape(),e(document).off("focusin.modal"),this.$element.removeClass("in").attr("aria-hidden",!0),e.support.transition&&this.$element.hasClass("fade")?this.hideWithTransition():this.hideModal()},enforceFocus:function(){var t=this;e(document).on("focusin.modal",function(e){t.$element[0]!==e.target&&!t.$element.has(e.target).length&&t.$element.focus()})},escape:function(){var e=this;this.isShown&&this.options.keyboard?this.$element.on("keyup.dismiss.modal",function(t){t.which==27&&e.hide()}):this.isShown||this.$element.off("keyup.dismiss.modal")},hideWithTransition:function(){var t=this,n=setTimeout(function(){t.$element.off(e.support.transition.end),t.hideModal()},500);this.$element.one(e.support.transition.end,function(){clearTimeout(n),t.hideModal()})},hideModal:function(){var e=this;this.$element.hide(),this.backdrop(function(){e.removeBackdrop(),e.$element.trigger("hidden")})},removeBackdrop:function(){this.$backdrop&&this.$backdrop.remove(),this.$backdrop=null},backdrop:function(t){var n=this,r=this.$element.hasClass("fade")?"fade":"";if(this.isShown&&this.options.backdrop){var i=e.support.transition&&r;this.$backdrop=e('<div class="modal-backdrop '+r+'" />').appendTo(document.body),this.$backdrop.click(this.options.backdrop=="static"?e.proxy(this.$element[0].focus,this.$element[0]):e.proxy(this.hide,this)),i&&this.$backdrop[0].offsetWidth,this.$backdrop.addClass("in");if(!t)return;i?this.$backdrop.one(e.support.transition.end,t):t()}else!this.isShown&&this.$backdrop?(this.$backdrop.removeClass("in"),e.support.transition&&this.$element.hasClass("fade")?this.$backdrop.one(e.support.transition.end,t):t()):t&&t()}};var n=e.fn.modal;e.fn.modal=function(n){return this.each(function(){var r=e(this),i=r.data("modal"),s=e.extend({},e.fn.modal.defaults,r.data(),typeof n=="object"&&n);i||r.data("modal",i=new t(this,s)),typeof n=="string"?i[n]():s.show&&i.show()})},e.fn.modal.defaults={backdrop:!0,keyboard:!0,show:!0},e.fn.modal.Constructor=t,e.fn.modal.noConflict=function(){return e.fn.modal=n,this},e(document).on("click.modal.data-api",'[data-toggle="modal"]',function(t){var n=e(this),r=n.attr("href"),i=e(n.attr("data-target")||r&&r.replace(/.*(?=#[^\s]+$)/,"")),s=i.data("modal")?"toggle":e.extend({remote:!/#/.test(r)&&r},i.data(),n.data());t.preventDefault(),i.modal(s).one("hide",function(){n.focus()})})}(window.jQuery),!function(e){"use strict";var t=function(e,t){this.init("tooltip",e,t)};t.prototype={constructor:t,init:function(t,n,r){var i,s,o,u,a;this.type=t,this.$element=e(n),this.options=this.getOptions(r),this.enabled=!0,o=this.options.trigger.split(" ");for(a=o.length;a--;)u=o[a],u=="click"?this.$element.on("click."+this.type,this.options.selector,e.proxy(this.toggle,this)):u!="manual"&&(i=u=="hover"?"mouseenter":"focus",s=u=="hover"?"mouseleave":"blur",this.$element.on(i+"."+this.type,this.options.selector,e.proxy(this.enter,this)),this.$element.on(s+"."+this.type,this.options.selector,e.proxy(this.leave,this)));this.options.selector?this._options=e.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},getOptions:function(t){return t=e.extend({},e.fn[this.type].defaults,this.$element.data(),t),t.delay&&typeof t.delay=="number"&&(t.delay={show:t.delay,hide:t.delay}),t},enter:function(t){var n=e.fn[this.type].defaults,r={},i;this._options&&e.each(this._options,function(e,t){n[e]!=t&&(r[e]=t)},this),i=e(t.currentTarget)[this.type](r).data(this.type);if(!i.options.delay||!i.options.delay.show)return i.show();clearTimeout(this.timeout),i.hoverState="in",this.timeout=setTimeout(function(){i.hoverState=="in"&&i.show()},i.options.delay.show)},leave:function(t){var n=e(t.currentTarget)[this.type](this._options).data(this.type);this.timeout&&clearTimeout(this.timeout);if(!n.options.delay||!n.options.delay.hide)return n.hide();n.hoverState="out",this.timeout=setTimeout(function(){n.hoverState=="out"&&n.hide()},n.options.delay.hide)},show:function(){var t,n,r,i,s,o,u=e.Event("show");if(this.hasContent()&&this.enabled){this.$element.trigger(u);if(u.isDefaultPrevented())return;t=this.tip(),this.setContent(),this.options.animation&&t.addClass("fade"),s=typeof this.options.placement=="function"?this.options.placement.call(this,t[0],this.$element[0]):this.options.placement,t.detach().css({top:0,left:0,display:"block"}),this.options.container?t.appendTo(this.options.container):t.insertAfter(this.$element),n=this.getPosition(),r=t[0].offsetWidth,i=t[0].offsetHeight;switch(s){case"bottom":o={top:n.top+n.height,left:n.left+n.width/2-r/2};break;case"top":o={top:n.top-i,left:n.left+n.width/2-r/2};break;case"left":o={top:n.top+n.height/2-i/2,left:n.left-r};break;case"right":o={top:n.top+n.height/2-i/2,left:n.left+n.width}}this.applyPlacement(o,s),this.$element.trigger("shown")}},applyPlacement:function(e,t){var n=this.tip(),r=n[0].offsetWidth,i=n[0].offsetHeight,s,o,u,a;n.offset(e).addClass(t).addClass("in"),s=n[0].offsetWidth,o=n[0].offsetHeight,t=="top"&&o!=i&&(e.top=e.top+i-o,a=!0),t=="bottom"||t=="top"?(u=0,e.left<0&&(u=e.left*-2,e.left=0,n.offset(e),s=n[0].offsetWidth,o=n[0].offsetHeight),this.replaceArrow(u-r+s,s,"left")):this.replaceArrow(o-i,o,"top"),a&&n.offset(e)},replaceArrow:function(e,t,n){this.arrow().css(n,e?50*(1-e/t)+"%":"")},setContent:function(){var e=this.tip(),t=this.getTitle();e.find(".tooltip-inner")[this.options.html?"html":"text"](t),e.removeClass("fade in top bottom left right")},hide:function(){function i(){var t=setTimeout(function(){n.off(e.support.transition.end).detach()},500);n.one(e.support.transition.end,function(){clearTimeout(t),n.detach()})}var t=this,n=this.tip(),r=e.Event("hide");this.$element.trigger(r);if(r.isDefaultPrevented())return;return n.removeClass("in"),e.support.transition&&this.$tip.hasClass("fade")?i():n.detach(),this.$element.trigger("hidden"),this},fixTitle:function(){var e=this.$element;(e.attr("title")||typeof e.attr("data-original-title")!="string")&&e.attr("data-original-title",e.attr("title")||"").attr("title","")},hasContent:function(){return this.getTitle()},getPosition:function(){var t=this.$element[0];return e.extend({},typeof t.getBoundingClientRect=="function"?t.getBoundingClientRect():{width:t.offsetWidth,height:t.offsetHeight},this.$element.offset())},getTitle:function(){var e,t=this.$element,n=this.options;return e=t.attr("data-original-title")||(typeof n.title=="function"?n.title.call(t[0]):n.title),e},tip:function(){return this.$tip=this.$tip||e(this.options.template)},arrow:function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},validate:function(){this.$element[0].parentNode||(this.hide(),this.$element=null,this.options=null)},enable:function(){this.enabled=!0},disable:function(){this.enabled=!1},toggleEnabled:function(){this.enabled=!this.enabled},toggle:function(t){var n=t?e(t.currentTarget)[this.type](this._options).data(this.type):this;n.tip().hasClass("in")?n.hide():n.show()},destroy:function(){this.hide().$element.off("."+this.type).removeData(this.type)}};var n=e.fn.tooltip;e.fn.tooltip=function(n){return this.each(function(){var r=e(this),i=r.data("tooltip"),s=typeof n=="object"&&n;i||r.data("tooltip",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.tooltip.Constructor=t,e.fn.tooltip.defaults={animation:!0,placement:"top",selector:!1,template:'<div class="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',trigger:"hover focus",title:"",delay:0,html:!1,container:!1},e.fn.tooltip.noConflict=function(){return e.fn.tooltip=n,this}}(window.jQuery),!function(e){"use strict";var t=function(e,t){this.init("popover",e,t)};t.prototype=e.extend({},e.fn.tooltip.Constructor.prototype,{constructor:t,setContent:function(){var e=this.tip(),t=this.getTitle(),n=this.getContent();e.find(".popover-title")[this.options.html?"html":"text"](t),e.find(".popover-content")[this.options.html?"html":"text"](n),e.removeClass("fade top bottom left right in")},hasContent:function(){return this.getTitle()||this.getContent()},getContent:function(){var e,t=this.$element,n=this.options;return e=(typeof n.content=="function"?n.content.call(t[0]):n.content)||t.attr("data-content"),e},tip:function(){return this.$tip||(this.$tip=e(this.options.template)),this.$tip},destroy:function(){this.hide().$element.off("."+this.type).removeData(this.type)}});var n=e.fn.popover;e.fn.popover=function(n){return this.each(function(){var r=e(this),i=r.data("popover"),s=typeof n=="object"&&n;i||r.data("popover",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.popover.Constructor=t,e.fn.popover.defaults=e.extend({},e.fn.tooltip.defaults,{placement:"right",trigger:"click",content:"",template:'<div class="popover"><div class="arrow"></div><h3 class="popover-title"></h3><div class="popover-content"></div></div>'}),e.fn.popover.noConflict=function(){return e.fn.popover=n,this}}(window.jQuery),!function(e){"use strict";function t(t,n){var r=e.proxy(this.process,this),i=e(t).is("body")?e(window):e(t),s;this.options=e.extend({},e.fn.scrollspy.defaults,n),this.$scrollElement=i.on("scroll.scroll-spy.data-api",r),this.selector=(this.options.target||(s=e(t).attr("href"))&&s.replace(/.*(?=#[^\s]+$)/,"")||"")+" .nav li > a",this.$body=e("body"),this.refresh(),this.process()}t.prototype={constructor:t,refresh:function(){var t=this,n;this.offsets=e([]),this.targets=e([]),n=this.$body.find(this.selector).map(function(){var n=e(this),r=n.data("target")||n.attr("href"),i=/^#\w/.test(r)&&e(r);return i&&i.length&&[[i.position().top+(!e.isWindow(t.$scrollElement.get(0))&&t.$scrollElement.scrollTop()),r]]||null}).sort(function(e,t){return e[0]-t[0]}).each(function(){t.offsets.push(this[0]),t.targets.push(this[1])})},process:function(){var e=this.$scrollElement.scrollTop()+this.options.offset,t=this.$scrollElement[0].scrollHeight||this.$body[0].scrollHeight,n=t-this.$scrollElement.height(),r=this.offsets,i=this.targets,s=this.activeTarget,o;if(e>=n)return s!=(o=i.last()[0])&&this.activate(o);for(o=r.length;o--;)s!=i[o]&&e>=r[o]&&(!r[o+1]||e<=r[o+1])&&this.activate(i[o])},activate:function(t){var n,r;this.activeTarget=t,e(this.selector).parent(".active").removeClass("active"),r=this.selector+'[data-target="'+t+'"],'+this.selector+'[href="'+t+'"]',n=e(r).parent("li").addClass("active"),n.parent(".dropdown-menu").length&&(n=n.closest("li.dropdown").addClass("active")),n.trigger("activate")}};var n=e.fn.scrollspy;e.fn.scrollspy=function(n){return this.each(function(){var r=e(this),i=r.data("scrollspy"),s=typeof n=="object"&&n;i||r.data("scrollspy",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.scrollspy.Constructor=t,e.fn.scrollspy.defaults={offset:10},e.fn.scrollspy.noConflict=function(){return e.fn.scrollspy=n,this},e(window).on("load",function(){e('[data-spy="scroll"]').each(function(){var t=e(this);t.scrollspy(t.data())})})}(window.jQuery),!function(e){"use strict";var t=function(t){this.element=e(t)};t.prototype={constructor:t,show:function(){var t=this.element,n=t.closest("ul:not(.dropdown-menu)"),r=t.attr("data-target"),i,s,o;r||(r=t.attr("href"),r=r&&r.replace(/.*(?=#[^\s]*$)/,""));if(t.parent("li").hasClass("active"))return;i=n.find(".active:last a")[0],o=e.Event("show",{relatedTarget:i}),t.trigger(o);if(o.isDefaultPrevented())return;s=e(r),this.activate(t.parent("li"),n),this.activate(s,s.parent(),function(){t.trigger({type:"shown",relatedTarget:i})})},activate:function(t,n,r){function o(){i.removeClass("active").find("> .dropdown-menu > .active").removeClass("active"),t.addClass("active"),s?(t[0].offsetWidth,t.addClass("in")):t.removeClass("fade"),t.parent(".dropdown-menu")&&t.closest("li.dropdown").addClass("active"),r&&r()}var i=n.find("> .active"),s=r&&e.support.transition&&i.hasClass("fade");s?i.one(e.support.transition.end,o):o(),i.removeClass("in")}};var n=e.fn.tab;e.fn.tab=function(n){return this.each(function(){var r=e(this),i=r.data("tab");i||r.data("tab",i=new t(this)),typeof n=="string"&&i[n]()})},e.fn.tab.Constructor=t,e.fn.tab.noConflict=function(){return e.fn.tab=n,this},e(document).on("click.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"]',function(t){t.preventDefault(),e(this).tab("show")})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=e.extend({},e.fn.typeahead.defaults,n),this.matcher=this.options.matcher||this.matcher,this.sorter=this.options.sorter||this.sorter,this.highlighter=this.options.highlighter||this.highlighter,this.updater=this.options.updater||this.updater,this.source=this.options.source,this.$menu=e(this.options.menu),this.shown=!1,this.listen()};t.prototype={constructor:t,select:function(){var e=this.$menu.find(".active").attr("data-value");return this.$element.val(this.updater(e)).change(),this.hide()},updater:function(e){return e},show:function(){var t=e.extend({},this.$element.position(),{height:this.$element[0].offsetHeight});return this.$menu.insertAfter(this.$element).css({top:t.top+t.height,left:t.left}).show(),this.shown=!0,this},hide:function(){return this.$menu.hide(),this.shown=!1,this},lookup:function(t){var n;return this.query=this.$element.val(),!this.query||this.query.length<this.options.minLength?this.shown?this.hide():this:(n=e.isFunction(this.source)?this.source(this.query,e.proxy(this.process,this)):this.source,n?this.process(n):this)},process:function(t){var n=this;return t=e.grep(t,function(e){return n.matcher(e)}),t=this.sorter(t),t.length?this.render(t.slice(0,this.options.items)).show():this.shown?this.hide():this},matcher:function(e){return~e.toLowerCase().indexOf(this.query.toLowerCase())},sorter:function(e){var t=[],n=[],r=[],i;while(i=e.shift())i.toLowerCase().indexOf(this.query.toLowerCase())?~i.indexOf(this.query)?n.push(i):r.push(i):t.push(i);return t.concat(n,r)},highlighter:function(e){var t=this.query.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&");return e.replace(new RegExp("("+t+")","ig"),function(e,t){return"<strong>"+t+"</strong>"})},render:function(t){var n=this;return t=e(t).map(function(t,r){return t=e(n.options.item).attr("data-value",r),t.find("a").html(n.highlighter(r)),t[0]}),t.first().addClass("active"),this.$menu.html(t),this},next:function(t){var n=this.$menu.find(".active").removeClass("active"),r=n.next();r.length||(r=e(this.$menu.find("li")[0])),r.addClass("active")},prev:function(e){var t=this.$menu.find(".active").removeClass("active"),n=t.prev();n.length||(n=this.$menu.find("li").last()),n.addClass("active")},listen:function(){this.$element.on("focus",e.proxy(this.focus,this)).on("blur",e.proxy(this.blur,this)).on("keypress",e.proxy(this.keypress,this)).on("keyup",e.proxy(this.keyup,this)),this.eventSupported("keydown")&&this.$element.on("keydown",e.proxy(this.keydown,this)),this.$menu.on("click",e.proxy(this.click,this)).on("mouseenter","li",e.proxy(this.mouseenter,this)).on("mouseleave","li",e.proxy(this.mouseleave,this))},eventSupported:function(e){var t=e in this.$element;return t||(this.$element.setAttribute(e,"return;"),t=typeof this.$element[e]=="function"),t},move:function(e){if(!this.shown)return;switch(e.keyCode){case 9:case 13:case 27:e.preventDefault();break;case 38:e.preventDefault(),this.prev();break;case 40:e.preventDefault(),this.next()}e.stopPropagation()},keydown:function(t){this.suppressKeyPressRepeat=~e.inArray(t.keyCode,[40,38,9,13,27]),this.move(t)},keypress:function(e){if(this.suppressKeyPressRepeat)return;this.move(e)},keyup:function(e){switch(e.keyCode){case 40:case 38:case 16:case 17:case 18:break;case 9:case 13:if(!this.shown)return;this.select();break;case 27:if(!this.shown)return;this.hide();break;default:this.lookup()}e.stopPropagation(),e.preventDefault()},focus:function(e){this.focused=!0},blur:function(e){this.focused=!1,!this.mousedover&&this.shown&&this.hide()},click:function(e){e.stopPropagation(),e.preventDefault(),this.select(),this.$element.focus()},mouseenter:function(t){this.mousedover=!0,this.$menu.find(".active").removeClass("active"),e(t.currentTarget).addClass("active")},mouseleave:function(e){this.mousedover=!1,!this.focused&&this.shown&&this.hide()}};var n=e.fn.typeahead;e.fn.typeahead=function(n){return this.each(function(){var r=e(this),i=r.data("typeahead"),s=typeof n=="object"&&n;i||r.data("typeahead",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.typeahead.defaults={source:[],items:8,menu:'<ul class="typeahead dropdown-menu"></ul>',item:'<li><a href="#"></a></li>',minLength:1},e.fn.typeahead.Constructor=t,e.fn.typeahead.noConflict=function(){return e.fn.typeahead=n,this},e(document).on("focus.typeahead.data-api",'[data-provide="typeahead"]',function(t){var n=e(this);if(n.data("typeahead"))return;n.typeahead(n.data())})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.options=e.extend({},e.fn.affix.defaults,n),this.$window=e(window).on("scroll.affix.data-api",e.proxy(this.checkPosition,this)).on("click.affix.data-api",e.proxy(function(){setTimeout(e.proxy(this.checkPosition,this),1)},this)),this.$element=e(t),this.checkPosition()};t.prototype.checkPosition=function(){if(!this.$element.is(":visible"))return;var t=e(document).height(),n=this.$window.scrollTop(),r=this.$element.offset(),i=this.options.offset,s=i.bottom,o=i.top,u="affix affix-top affix-bottom",a;typeof i!="object"&&(s=o=i),typeof o=="function"&&(o=i.top()),typeof s=="function"&&(s=i.bottom()),a=this.unpin!=null&&n+this.unpin<=r.top?!1:s!=null&&r.top+this.$element.height()>=t-s?"bottom":o!=null&&n<=o?"top":!1;if(this.affixed===a)return;this.affixed=a,this.unpin=a=="bottom"?r.top-n:null,this.$element.removeClass(u).addClass("affix"+(a?"-"+a:""))};var n=e.fn.affix;e.fn.affix=function(n){return this.each(function(){var r=e(this),i=r.data("affix"),s=typeof n=="object"&&n;i||r.data("affix",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.affix.Constructor=t,e.fn.affix.defaults={offset:0},e.fn.affix.noConflict=function(){return e.fn.affix=n,this},e(window).on("load",function(){e('[data-spy="affix"]').each(function(){var t=e(this),n=t.data();n.offset=n.offset||{},n.offsetBottom&&(n.offset.bottom=n.offsetBottom),n.offsetTop&&(n.offset.top=n.offsetTop),t.affix(n)})})}(window.jQuery); \ No newline at end of file diff --git a/src/Blog.hs b/src/Blog.hs index 0defa7ba19..1b6efb138f 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -42,11 +42,9 @@ markdownCutoff = fromJust $ parseTime defaultTimeLocale "%s" "1367149834" -- blog CSS (admin is still static) -stylesheetSource = $(luciusFile "../res/blogbs.lucius") +stylesheetSource = $(luciusFile "../res/blog.lucius") blogStyle = renderCssUrl undefined stylesheetSource --- <link rel="stylesheet" type="text/css" href="/static/blogv34.css" media="all"> FIXME - -- blog HTML blogTemplate :: BlogLang -> Text -> Html -> Html blogTemplate lang t_append body = [shamlet| @@ -54,7 +52,7 @@ $doctype 5 <head> <title>#{blogTitle lang t_append} <link rel="stylesheet" type="text/css" href="/static/bootstrap.css" media="all"> - <link rel="stylesheet" type="text/css" href="/static/blogv300.css" media="all"> + <link rel="stylesheet" type="text/css" href="/static/blogv40.css" media="all"> <link rel="alternate" type="application/rss+xml" title="RSS-Feed" href=#{rssUrl}> <meta http-equiv="content-type" content="text/html;charset=UTF-8"> <body> -- cgit 1.4.1 From df9603369df3ca0d4e3cc382f98ac5e8f302e91b Mon Sep 17 00:00:00 2001 From: "\"Vincent Ambo ext:(%22)" <tazjin@gmail.com> Date: Sun, 5 May 2013 21:06:49 +0200 Subject: * serving correct css location --- src/Main.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Main.hs b/src/Main.hs index 25e7539a3e..1a9331f418 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -95,7 +95,7 @@ tazBlog acid captchakey = do ok $ toResponse $ adminIndex ("tazjin" :: Text) , dir "admin" $ ok $ toResponse adminLogin , dir "dologin" $ processLogin acid - , do dirs "static/blogv300.css" nullDir + , do dirs "static/blogv40.css" nullDir setHeaderM "content-type" "text/css" setHeaderM "cache-control" "max-age=630720000" setHeaderM "expires" "Tue, 20 Jan 2037 04:20:42 GMT" -- cgit 1.4.1 From 1452476015ebf6577c4fb1efa3f76fbefd1ae3a6 Mon Sep 17 00:00:00 2001 From: "\"Vincent Ambo ext:(%22)" <tazjin@gmail.com> Date: Sun, 5 May 2013 21:08:50 +0200 Subject: * minor unimportant fix --- src/Blog.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Blog.hs b/src/Blog.hs index 1b6efb138f..72995e0087 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -42,7 +42,7 @@ markdownCutoff = fromJust $ parseTime defaultTimeLocale "%s" "1367149834" -- blog CSS (admin is still static) -stylesheetSource = $(luciusFile "../res/blog.lucius") +stylesheetSource = $(luciusFile "res/blog.lucius") blogStyle = renderCssUrl undefined stylesheetSource -- blog HTML -- cgit 1.4.1 From 008e333146f0dfc9a8dfe3ec7d0598764c3f068c Mon Sep 17 00:00:00 2001 From: "\"Vincent Ambo ext:(%22)" <tazjin@gmail.com> Date: Sun, 5 May 2013 21:59:49 +0200 Subject: * time hotfix --- src/Blog.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Blog.hs b/src/Blog.hs index 72995e0087..a814a74617 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -120,7 +120,7 @@ $forall entry <- elist <a href=#{linkElems entry}> <b>#{title entry} <br> - <i>#{pack $ formatTime defaultTimeLocale "%Y-%M-%d" $ edate entry} + <i>#{pack $ formatTime defaultTimeLocale "%Y-%m-%d" $ edate entry} <br> #{linkText $ length $ comments entry} #{cHead $ lang entry} -- cgit 1.4.1 From fef78b55fa5d6f6c42b798e298608faba9dffa73 Mon Sep 17 00:00:00 2001 From: Vincent Ambo <tazjin@gmail.com> Date: Mon, 14 Oct 2013 08:34:50 +0200 Subject: Removed comments --- TazBlog.cabal | 3 +-- src/Blog.hs | 64 ---------------------------------------------------------- src/Locales.hs | 2 +- src/Main.hs | 43 ++++++--------------------------------- 4 files changed, 8 insertions(+), 104 deletions(-) diff --git a/TazBlog.cabal b/TazBlog.cabal index 295d4e8ae3..c254361f2f 100644 --- a/TazBlog.cabal +++ b/TazBlog.cabal @@ -1,5 +1,5 @@ Name: TazBlog -Version: 4.0 +Version: 4.1 Synopsis: Tazjin's Blog License-file: LICENSE Author: Vincent Ambo @@ -34,7 +34,6 @@ Executable tazblog network, options, rss, - recaptcha, hamlet, shakespeare-css, markdown diff --git a/src/Blog.hs b/src/Blog.hs index a814a74617..67adf0cd3c 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -17,7 +17,6 @@ import Data.Text (Text, append, empty, pack) import Data.Text.Lazy (fromStrict) import Data.Time import Locales -import Network.Captcha.ReCaptcha import System.Locale (defaultTimeLocale) import Text.Blaze.Html (preEscapedToHtml) import Text.Hamlet @@ -121,9 +120,6 @@ $forall entry <- elist <b>#{title entry} <br> <i>#{pack $ formatTime defaultTimeLocale "%Y-%m-%d" $ edate entry} - <br> - #{linkText $ length $ comments entry} - #{cHead $ lang entry} <div .span10 .entry> $if (isEntryMarkdown entry) ^{renderEntryMarkdown $ append " " $ btext entry} @@ -140,7 +136,6 @@ $maybe links <- footerLinks where elist = if' showAll entries (take 6 entries) linkElems Entry{..} = concat $ intersperse' "/" [show lang, show entryId] - linkText n = T.concat ["[", show' n, "]"] showLinks :: Maybe Int -> BlogLang -> Html showLinks (Just i) lang = [shamlet| @@ -179,69 +174,10 @@ renderEntry e@Entry{..} = [shamlet| $else ^{preEscapedToHtml $ btext} <p>^{preEscapedToHtml $ mtext} -<div .row .innerBoxComments> - <div .span10> - <div .boldify>#{cHead lang}: -#{renderComments comments lang} -<div .row .innerBoxComments> - <div .span10> - <div .boldify>#{cwHead lang} -^{renderCommentBox lang entryId} |] where woText = flip T.append author $ T.pack $ formatTime defaultTimeLocale (eTimeFormat lang) edate -renderComments :: [Comment] -> BlogLang -> Html -renderComments [] lang = [shamlet| -<div .row> - <div .span10>#{noComments lang} -|] -renderComments comments lang = [shamlet| -$forall comment <- comments - <div .row> - <div .span1 .commentname> - <i>#{append (cauthor comment) ": "} - <div .span9> - ^{preEscapedToHtml $ ctext comment} - <p .tt>#{timeString $ cdate comment} -|] - where - timeString = formatTime defaultTimeLocale (cTimeFormat lang) - -renderCommentBox :: BlogLang -> EntryId -> Html -renderCommentBox cLang cId = [shamlet| -^{captchaOptions cLang} -<div .row> - <div .span10> - <form method="POST" action=#{aLink}> - <fieldset> - <label> - <input .span8 name="cname" placeholder="Name" type="text"> - <label> - <textarea .span8 name="ctext" cols="50" rows="13" placeholder=#{cTextPlaceholder cLang}> - ^{captcha} - <label> - <input .btn type="submit" value=#{cSend cLang}> -|] - where - aLink = T.concat ["/", show' cLang, "/postcomment/", show' cId] - - -captcha :: Html -captcha = [shamlet| -<div class="cCaptcha"> - <script src="http://api.recaptcha.net/challenge?k=6LfQXccSAAAAAIjKm26XlFnBMAgvaKlOAjVWEEnM" type="text/javascript"> - <noscript> - <iframe src="http://api.recaptcha.net/noscript?k=6LfQXccSAAAAAIjKm26XlFnBMAgvaKlOAjVWEEnM" height="300" width="500" seamless> - <br> - <textarea name="recaptcha_challenge_field" rows="3" cols="40"> - <input type="hidden" name="recaptcha_response_field" value="manual_challenge"> -|] - -captchaOptions :: BlogLang -> Html -captchaOptions lang = [shamlet|<script type="text/javascript">^{preEscapedToHtml options}|] - where - options = T.concat ["var RecaptchaOptions = { theme: 'clean', lang: '", showLangText lang, "'};"] showSiteNotice :: Html showSiteNotice = [shamlet| diff --git a/src/Locales.hs b/src/Locales.hs index 34d3fadf25..7fd874c1d0 100644 --- a/src/Locales.hs +++ b/src/Locales.hs @@ -18,7 +18,7 @@ import BlogDB (BlogLang (..)) data BlogError = NotFound | DBError -version = "4.0" +version = "4.1" allLang = [EN, DE] diff --git a/src/Main.hs b/src/Main.hs index 1a9331f418..88bf59121e 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -31,7 +31,6 @@ import qualified Data.Text as T import Data.Time import Happstack.Server hiding (Session) import Happstack.Server.Compression -import Network.Captcha.ReCaptcha import Options import System.Locale (defaultTimeLocale) @@ -47,8 +46,6 @@ defineOptions "MainOptions" $ do stringOption "optState" "statedir" "../" "Directory in which the /BlogState dir is located.\ \ The default is ../ (if run from src/)" - stringOption "optCaptcha" "captchakey" "" - "The reCaptcha private key" intOption "optPort" "port" 8000 "The port to run the web server on. Default is 8000" @@ -61,12 +58,12 @@ main = do runCommand $ \opts args -> bracket (openLocalStateFrom (optState opts ++ "BlogState") initialBlogState) createCheckpointAndClose - (\acid -> simpleHTTP nullConf {port = optPort opts} $ tazBlog acid $ optCaptcha opts) + (\acid -> simpleHTTP nullConf {port = optPort opts} $ tazBlog acid) -tazBlog :: AcidState Blog -> String -> ServerPart Response -tazBlog acid captchakey = do +tazBlog :: AcidState Blog -> ServerPart Response +tazBlog acid = do compr <- compressedResponseFilter - msum [ path $ \(lang :: BlogLang) -> blogHandler acid lang captchakey + msum [ path $ \(lang :: BlogLang) -> blogHandler acid lang , nullDir >> showIndex acid EN , dir " " $ nullDir >> seeOther ("https://plus.google.com/115916629925754851590" :: Text) (toResponse ()) @@ -107,12 +104,9 @@ tazBlog acid captchakey = do , notFound $ toResponse $ showError NotFound DE ] -blogHandler :: AcidState Blog -> BlogLang -> String -> ServerPart Response -blogHandler acid lang captchakey = +blogHandler :: AcidState Blog -> BlogLang -> ServerPart Response +blogHandler acid lang = msum [ path $ \(eId :: Integer) -> showEntry acid lang $ EntryId eId - , do decodeBody tmpPolicy - dir "postcomment" $ path $ - \(eId :: Integer) -> addComment acid lang captchakey $ EntryId eId , nullDir >> showIndex acid lang , dir "rss" $ nullDir >> showRSS acid lang , dir "rss.xml" $ nullDir >> showRSS acid lang @@ -154,31 +148,6 @@ showRSS acid lang = do setHeaderM "content-type" "text/xml" ok $ toResponse feed -addComment :: AcidState Blog -> BlogLang -> String -> EntryId -> ServerPart Response -addComment acid lang captchakey eId = do - now <- liftIO getCurrentTime - nCtext <- lookText' "ctext" - nComment <- Comment <$> pure now - <*> lookText' "cname" - <*> pure (commentEscape nCtext) - -- captcha verification - challenge <- look "recaptcha_challenge_field" - response <- look "recaptcha_response_field" - (userIp, _) <- liftM rqPeer askRq - validation <- liftIO $ validateCaptcha captchakey userIp challenge response - case validation of - Right _ -> update' acid (AddComment eId nComment) - >> seeOther ("/" ++ show lang ++ "/" ++ show eId) (toResponse()) - Left _ -> liftIO (putStrLn "Captcha failed") >> seeOther ("/" ++ show lang ++ "/" ++ show eId) (toResponse()) - -commentEscape :: Text -> Text -commentEscape = newlineEscape . ltEscape . gtEscape . ampEscape - where - newlineEscape = T.replace "\n" "<br>" - ampEscape = T.replace "&" "&" - ltEscape = T.replace "<" "<" - gtEscape = T.replace ">" ">" - {- ADMIN stuff -} postEntry :: AcidState Blog -> ServerPart Response -- cgit 1.4.1 From b4b2b053b9313b708e579310a87009ec4ff6eb43 Mon Sep 17 00:00:00 2001 From: Vincent Ambo <dev@tazj.in> Date: Tue, 11 Mar 2014 18:12:51 +0100 Subject: Updated for new options package --- src/Main.hs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/Main.hs b/src/Main.hs index 88bf59121e..a684db4e05 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -42,12 +42,17 @@ import RSS {- Server -} -defineOptions "MainOptions" $ do - stringOption "optState" "statedir" "../" - "Directory in which the /BlogState dir is located.\ - \ The default is ../ (if run from src/)" - intOption "optPort" "port" 8000 - "The port to run the web server on. Default is 8000" +data MainOptions = MainOptions { + optState :: String, + optPort :: Int +} + +instance Options MainOptions where + defineOptions = pure MainOptions + <*> simpleOption "statedir" "/var/tazblog/" + "Directory in which the BlogState is located." + <*> simpleOption "port" 8000 + "Port to run on. Default is 8000." tmpPolicy :: BodyPolicy tmpPolicy = defaultBodyPolicy "./tmp/" 0 200000 1000 -- cgit 1.4.1 From f2b5e14ceedeabf9ee8bb673b2f3c765a9b81670 Mon Sep 17 00:00:00 2001 From: Vincent Ambo <dev@tazj.in> Date: Tue, 11 Mar 2014 18:22:59 +0100 Subject: Add an option for the resource folder --- src/Main.hs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/Main.hs b/src/Main.hs index a684db4e05..67440e6f0e 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -44,7 +44,8 @@ import RSS data MainOptions = MainOptions { optState :: String, - optPort :: Int + optPort :: Int, + optRes :: String } instance Options MainOptions where @@ -53,7 +54,9 @@ instance Options MainOptions where "Directory in which the BlogState is located." <*> simpleOption "port" 8000 "Port to run on. Default is 8000." - + <*> simpleOption "res" "/usr/share/tazblog/res" + "Resources folder location." + tmpPolicy :: BodyPolicy tmpPolicy = defaultBodyPolicy "./tmp/" 0 200000 1000 @@ -63,10 +66,10 @@ main = do runCommand $ \opts args -> bracket (openLocalStateFrom (optState opts ++ "BlogState") initialBlogState) createCheckpointAndClose - (\acid -> simpleHTTP nullConf {port = optPort opts} $ tazBlog acid) + (\acid -> simpleHTTP nullConf {port = optPort opts} $ tazBlog acid (optRes opts)) -tazBlog :: AcidState Blog -> ServerPart Response -tazBlog acid = do +tazBlog :: AcidState Blog -> String -> ServerPart Response +tazBlog acid resDir = do compr <- compressedResponseFilter msum [ path $ \(lang :: BlogLang) -> blogHandler acid lang , nullDir >> showIndex acid EN @@ -104,8 +107,8 @@ tazBlog acid = do ok $ toResponse blogStyle , do setHeaderM "cache-control" "max-age=630720000" setHeaderM "expires" "Tue, 20 Jan 2037 04:20:42 GMT" - dir "static" $ serveDirectory DisableBrowsing [] "../res" - , serveDirectory DisableBrowsing [] "../res" + dir "static" $ serveDirectory DisableBrowsing [] resDir + , serveDirectory DisableBrowsing [] resDir , notFound $ toResponse $ showError NotFound DE ] -- cgit 1.4.1 From 16ef5deace7a51ae711e16b9c67f73c3e0d5b63c Mon Sep 17 00:00:00 2001 From: Vincent Ambo <dev@tazj.in> Date: Tue, 11 Mar 2014 18:27:04 +0100 Subject: Added tag 4.1 for changeset 7e19f2cc8edf --- .hgtags | 1 + 1 file changed, 1 insertion(+) create mode 100644 .hgtags diff --git a/.hgtags b/.hgtags new file mode 100644 index 0000000000..00aa6d325c --- /dev/null +++ b/.hgtags @@ -0,0 +1 @@ +7e19f2cc8edf2c77a8dc7258868c06360e80dcd3 4.1 -- cgit 1.4.1 From 01c49a8ff21da5ce80c1b8456bae796b34a36b0e Mon Sep 17 00:00:00 2001 From: Vincent Ambo <dev@tazj.in> Date: Tue, 11 Mar 2014 19:06:03 +0100 Subject: Added first version of ArchLinux PKGBUILD --- arch/PKGBUILD | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 arch/PKGBUILD diff --git a/arch/PKGBUILD b/arch/PKGBUILD new file mode 100644 index 0000000000..ce1ed66b88 --- /dev/null +++ b/arch/PKGBUILD @@ -0,0 +1,31 @@ +# Maintainer: Vincent Ambo <dev@tazj.in> +pkgname=tazblog +pkgver=4.1 +pkgrel=1 +pkgdesc="Tazjin's blog written in Haskell" +arch=('i686' 'x86_64') +url="http://tazj.in" +makedepends=('ghc' 'cabal-install') +source=(https://bitbucket.org/tazjin/tazblog-haskell/get/$pkgver.tar.gz) +md5sums=('881e1e021b6cc0e95cdbda952520d059') + + +build() { + cd "$srcdir" + cd tazjin-* + + cabal sandbox init + cabal install -j --only-dependencies + cabal build +} + +package() { + cd "$srcdir" + cd tazjin-* + + install -d "${pkgdir}/usr/bin" + install -m755 dist/build/tazblog/tazblog "${pkgdir}/usr/bin/tazblog" + + install -d "${pkgdir}/usr/share/tazblog" + cp -r res/ "${pkgdir}/usr/share/tazblog" +} -- cgit 1.4.1 From d969c6ffc70db8aefa7206e4c10d16f932422792 Mon Sep 17 00:00:00 2001 From: Vincent Ambo <dev@tazj.in> Date: Tue, 11 Mar 2014 19:08:49 +0100 Subject: Run cabal update before building in Arch --- arch/PKGBUILD | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/arch/PKGBUILD b/arch/PKGBUILD index ce1ed66b88..23dc6e5164 100644 --- a/arch/PKGBUILD +++ b/arch/PKGBUILD @@ -13,7 +13,8 @@ md5sums=('881e1e021b6cc0e95cdbda952520d059') build() { cd "$srcdir" cd tazjin-* - + + cabal update cabal sandbox init cabal install -j --only-dependencies cabal build -- cgit 1.4.1 From 3a004b49d8e84895d936f3898972a3cf301f6dd2 Mon Sep 17 00:00:00 2001 From: Vincent Ambo <dev@tazj.in> Date: Tue, 11 Mar 2014 19:32:31 +0100 Subject: Added systemd service file --- arch/PKGBUILD | 6 ++++++ arch/tazblog@.service | 11 +++++++++++ 2 files changed, 17 insertions(+) create mode 100644 arch/tazblog@.service diff --git a/arch/PKGBUILD b/arch/PKGBUILD index 23dc6e5164..ae06220137 100644 --- a/arch/PKGBUILD +++ b/arch/PKGBUILD @@ -24,9 +24,15 @@ package() { cd "$srcdir" cd tazjin-* + # Install blog itself install -d "${pkgdir}/usr/bin" install -m755 dist/build/tazblog/tazblog "${pkgdir}/usr/bin/tazblog" + # Install resources install -d "${pkgdir}/usr/share/tazblog" cp -r res/ "${pkgdir}/usr/share/tazblog" + + # Install service file + install -d "${pkgdir}/usr/lib/systemd/system" + cp "arch/tazblog@.service" "${pkgdir}/usr/lib/systemd/system/" } diff --git a/arch/tazblog@.service b/arch/tazblog@.service new file mode 100644 index 0000000000..2b6f115eb5 --- /dev/null +++ b/arch/tazblog@.service @@ -0,0 +1,11 @@ +[Unit] +Description=tazblog web process + +[Service] +Type=simple +ExecStart=/usr/bin/tazblog +Restart=always +User=%i + +[Install] +WantedBy=multi-user.target -- cgit 1.4.1 From d84c7f4249362fb48e39a73cbc4583cd9fecf937 Mon Sep 17 00:00:00 2001 From: Vincent Ambo <dev@tazj.in> Date: Tue, 11 Mar 2014 19:33:01 +0100 Subject: Added tag 4.1 for changeset be1d1e09f072 --- .hgtags | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.hgtags b/.hgtags index 00aa6d325c..089c798fa6 100644 --- a/.hgtags +++ b/.hgtags @@ -1 +1,3 @@ 7e19f2cc8edf2c77a8dc7258868c06360e80dcd3 4.1 +7e19f2cc8edf2c77a8dc7258868c06360e80dcd3 4.1 +be1d1e09f0727102fc3a41e8218ac1f74c3b4f51 4.1 -- cgit 1.4.1 From d829f0c70c09ff2e4c8617f45cb4c85ac4a46f2c Mon Sep 17 00:00:00 2001 From: Vincent Ambo <dev@tazj.in> Date: Tue, 11 Mar 2014 19:34:44 +0100 Subject: Updated PKGBUILD checksum for moved tag --- arch/PKGBUILD | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arch/PKGBUILD b/arch/PKGBUILD index ae06220137..0500012fbc 100644 --- a/arch/PKGBUILD +++ b/arch/PKGBUILD @@ -7,7 +7,7 @@ arch=('i686' 'x86_64') url="http://tazj.in" makedepends=('ghc' 'cabal-install') source=(https://bitbucket.org/tazjin/tazblog-haskell/get/$pkgver.tar.gz) -md5sums=('881e1e021b6cc0e95cdbda952520d059') +md5sums=('981cdfdd3cba59f13213a644717f5343') build() { -- cgit 1.4.1 From 089f44e8c9d216acf0b2054ef0a214108c6a3c91 Mon Sep 17 00:00:00 2001 From: Vincent Ambo <vincent@spotify.com> Date: Wed, 19 Mar 2014 12:17:41 +0100 Subject: Cleaning up after git move --- .gitignore | 6 + .hgignore | 13 -- .hgtags | 3 - run.sh | 2 - tools/acid-migrate/Acid.hs | 279 ------------------------------ tools/convertdb/Makefile | 13 -- tools/convertdb/convertdb.go | 116 ------------- tools/convertdb/couch.go | 403 ------------------------------------------- tools/fixcomments.hs | 21 --- tools/music/Makefile | 10 -- tools/music/gettitle | 4 - tools/music/iTunes.go | 79 --------- tools/music/start | 1 - update.sh | 5 - 14 files changed, 6 insertions(+), 949 deletions(-) create mode 100644 .gitignore delete mode 100644 .hgignore delete mode 100644 .hgtags delete mode 100644 run.sh delete mode 100644 tools/acid-migrate/Acid.hs delete mode 100644 tools/convertdb/Makefile delete mode 100644 tools/convertdb/convertdb.go delete mode 100644 tools/convertdb/couch.go delete mode 100644 tools/fixcomments.hs delete mode 100644 tools/music/Makefile delete mode 100755 tools/music/gettitle delete mode 100644 tools/music/iTunes.go delete mode 100755 tools/music/start delete mode 100644 update.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..8bfbf2a389 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +*.o +*.hi +BlogState/ +dist/ +.cabal-sandbox/ +*.tar.gz diff --git a/.hgignore b/.hgignore deleted file mode 100644 index f0e6fe0d5e..0000000000 --- a/.hgignore +++ /dev/null @@ -1,13 +0,0 @@ -syntax: glob -.DS_Store -reServe -convertdb -music -*.o -*.hi -*.esproj -*.sublime* -*.8 -*.geany -*.orig -BlogState/ \ No newline at end of file diff --git a/.hgtags b/.hgtags deleted file mode 100644 index 089c798fa6..0000000000 --- a/.hgtags +++ /dev/null @@ -1,3 +0,0 @@ -7e19f2cc8edf2c77a8dc7258868c06360e80dcd3 4.1 -7e19f2cc8edf2c77a8dc7258868c06360e80dcd3 4.1 -be1d1e09f0727102fc3a41e8218ac1f74c3b4f51 4.1 diff --git a/run.sh b/run.sh deleted file mode 100644 index 7ac433b60b..0000000000 --- a/run.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -sudo privbind -u tazjin tazblog --port 80 --statedir $TAZBLOG \ No newline at end of file diff --git a/tools/acid-migrate/Acid.hs b/tools/acid-migrate/Acid.hs deleted file mode 100644 index 10ab3e23d0..0000000000 --- a/tools/acid-migrate/Acid.hs +++ /dev/null @@ -1,279 +0,0 @@ -{-# LANGUAGE DeriveDataTypeable, GeneralizedNewtypeDeriving, RecordWildCards, -TemplateHaskell, TypeFamilies, OverloadedStrings, ScopedTypeVariables, BangPatterns #-} - -module Main where -import Control.Applicative ((<$>), optional) -import Control.Exception (bracket) -import Control.Monad (msum, mzero) -import Control.Monad.IO.Class (MonadIO) -import Control.Monad.Reader (ask) -import Control.Monad.State (get, put) -import Control.Monad.Trans (liftIO) -import Data.Acid -import Data.Acid.Advanced -import Data.Acid.Local -import Data.ByteString (ByteString) -import Data.Data (Data, Typeable) -import Data.IxSet (Indexable(..), IxSet(..), (@=), Proxy(..), getOne, ixFun, ixSet) -import Data.SafeCopy (SafeCopy, base, deriveSafeCopy) -import Data.Text (Text, pack) -import Data.Text.Lazy (toStrict) -import Data.Time -import System.Environment(getEnv) - - -import qualified Crypto.Hash.SHA512 as SHA (hash) -import qualified Data.ByteString.Char8 as B -import qualified Data.ByteString.Base64 as B64 (encode) -import qualified Data.IxSet as IxSet -import qualified Data.Text as Text - - -{-CouchDB imports-} - -import Database.CouchDB hiding (runCouchDB') -import Database.CouchDB.JSON -import Text.JSON -import Data.List (intersperse, (\\)) -import System.Locale (defaultTimeLocale) - --- data types and acid-state setup - -newtype EntryId = EntryId { unEntryId :: Integer } - deriving (Eq, Ord, Data, Enum, Typeable, SafeCopy) - -instance Show EntryId where - show = show . unEntryId - -data BlogLang = EN | DE - deriving (Eq, Ord, Data, Typeable) - -instance Show BlogLang where - show DE = "de" - show EN = "en" - -$(deriveSafeCopy 0 'base ''BlogLang) - -data Comment = Comment { - cdate :: UTCTime, - cauthor :: Text, - ctext :: Text -} deriving (Eq, Ord, Show, Data, Typeable) - -$(deriveSafeCopy 0 'base ''Comment) - -data Entry = Entry { - entryId :: EntryId, - lang :: BlogLang, - author :: Text, - title :: Text, - btext :: Text, - mtext :: Text, - edate :: UTCTime, - tags :: [Text], - comments :: [Comment] -} deriving (Eq, Ord, Show, Data, Typeable) - -$(deriveSafeCopy 0 'base ''Entry) - --- ixSet requires different datatypes for field indexes, so let's define some -newtype Author = Author Text deriving (Eq, Ord, Data, Typeable, SafeCopy) -newtype Title = Title Text deriving (Eq, Ord, Data, Typeable, SafeCopy) -newtype BText = BText Text deriving (Eq, Ord, Data, Typeable, SafeCopy) -- standard text -newtype MText = MText Text deriving (Eq, Ord, Data, Typeable, SafeCopy) -- "read more" text -newtype Tag = Tag Text deriving (Eq, Ord, Data, Typeable, SafeCopy) -newtype EDate = EDate UTCTime deriving (Eq, Ord, Data, Typeable, SafeCopy) -newtype SDate = SDate UTCTime deriving (Eq, Ord, Data, Typeable, SafeCopy) -newtype Username = Username Text deriving (Eq, Ord, Data, Typeable, SafeCopy) -newtype SessionID = SessionID Text deriving (Eq, Ord, Data, Typeable, SafeCopy) - -instance Indexable Entry where - empty = ixSet [ ixFun $ \e -> [ entryId e] - , ixFun $ (:[]) . lang - , ixFun $ \e -> [ Author $ author e ] - , ixFun $ \e -> [ Title $ title e] - , ixFun $ \e -> [ BText $ btext e] - , ixFun $ \e -> [ MText $ mtext e] - , ixFun $ \e -> [ EDate $ edate e] - , ixFun $ \e -> map Tag (tags e) - , ixFun $ comments - ] - -data User = User { - username :: Text, - password :: ByteString -} deriving (Eq, Ord, Data, Typeable) - -$(deriveSafeCopy 0 'base ''User) - -data Session = Session { - sessionID :: Text, - user :: User, - sdate :: UTCTime -} deriving (Eq, Ord, Data, Typeable) - -$(deriveSafeCopy 0 'base ''Session) - -instance Indexable User where - empty = ixSet [ ixFun $ \u -> [Username $ username u] - , ixFun $ (:[]) . password - ] - -instance Indexable Session where - empty = ixSet [ ixFun $ \s -> [SessionID $ sessionID s] - , ixFun $ (:[]) . user - , ixFun $ \s -> [SDate $ sdate s] - ] - -data Blog = Blog { - blogSessions :: IxSet Session, - blogUsers :: IxSet User, - blogEntries :: IxSet Entry -} deriving (Data, Typeable) - -$(deriveSafeCopy 0 'base ''Blog) - -initialBlogState :: Blog -initialBlogState = - Blog { blogSessions = empty - , blogUsers = empty - , blogEntries = empty } - --- acid-state database functions (purity is necessary!) - -insertEntry :: Entry -> Update Blog Entry -insertEntry e = - do b@Blog{..} <- get - put $ b { blogEntries = IxSet.insert e blogEntries } - return e - -updateEntry :: Entry -> Update Blog Entry -updateEntry e = - do b@Blog{..} <- get - put $ b { blogEntries = IxSet.updateIx (entryId e) e blogEntries} - return e - -getPost :: EntryId -> Query Blog (Maybe Entry) -getPost eid = - do b@Blog{..} <- ask - return $ getOne $ blogEntries @= eid - -latestPosts :: Query Blog [Entry] -latestPosts = - do b@Blog{..} <- ask - return $ IxSet.toDescList (Proxy :: Proxy EDate) $ blogEntries - -addSession :: Text -> User -> UTCTime -> Update Blog Session -addSession sId u t = - do b@Blog{..} <- get - let s = Session sId u t - put $ b { blogSessions = IxSet.insert s blogSessions} - return s - -addUser :: Text -> String -> Update Blog User -addUser un pw = - do b@Blog{..} <- get - let u = User un $ hashString pw - put $ b { blogUsers = IxSet.insert u blogUsers} - return u - --- various functions -hashString :: String -> ByteString -hashString = B64.encode . SHA.hash . B.pack - -$(makeAcidic ''Blog - [ 'insertEntry - , 'updateEntry - , 'getPost - , 'latestPosts - , 'addSession - , 'addUser - ]) - --- CouchDB database functions - -runCouchDB' :: CouchMonad a -> IO a -runCouchDB' = runCouchDB "127.0.0.1" 5984 - -instance JSON Comment where - showJSON = undefined - readJSON val = do - obj <- jsonObject val - scauthor <- jsonField "cauthor" obj - jsscdate <- jsonField "cdate" obj :: Result JSValue - let rcdate = stripResult $ jsonInt jsscdate - sctext <- jsonField "ctext" obj - return $ Comment (parseSeconds rcdate) (pack scauthor) (pack sctext) - -instance JSON Entry where - showJSON = undefined - readJSON val = do - obj <- jsonObject val - sauthor <- jsonField "author" obj - stitle <- jsonField "title" obj - day <- jsonField "day" obj - month <- jsonField "month" obj - year <- jsonField "year" obj - stext <- jsonField "text" obj - comments <- jsonField "comments" obj - oldid <- jsonField "_id" obj - let leTime = parseShittyTime year month day oldid - return $ Entry (EntryId $ getUnixTime leTime) DE (pack sauthor) (pack $ stitle \\ "\n") (pack stext) (Text.empty) - leTime [] comments - - -getUnixTime :: UTCTime -> Integer -getUnixTime t = read $ formatTime defaultTimeLocale "%s" t - -parseSeconds :: Integer -> UTCTime -parseSeconds t = readTime defaultTimeLocale "%s" $ show t - -parseShittyTime :: Int -> Int -> Int -> String -> UTCTime -parseShittyTime y m d i = readTime defaultTimeLocale "%Y %m %e %k:%M:%S" newPartTime - where - firstPart = take 2 i - secondPart = take 2 $ drop 2 i - thirdPart = drop 4 i - newPartTime = concat $ intersperse " " [show y, showMonth m, show d, " "] ++ - intersperse ":" [firstPart, secondPart, thirdPart] - showMonth mn - | mn < 10 = "0" ++ show mn - | otherwise = show mn - -getOldEntries = runCouchDB' $ queryView (db "tazblog") (doc "entries") (doc "latestDE") [] - -parseOldEntries :: IO [Entry] -parseOldEntries = do - queryResult <- getOldEntries - let entries = map (stripResult . readJSON . snd) queryResult - return entries - -stripResult :: Result a -> a -stripResult (Ok z) = z -stripResult (Error s) = error $ "JSON error: " ++ s - -pasteToDB :: AcidState Blog -> Entry -> IO (EventResult InsertEntry) -pasteToDB acid !e = update' acid (InsertEntry e) - -main :: IO() -main = do - tbDir <- getEnv "TAZBLOG" - bracket (openLocalStateFrom (tbDir ++ "/BlogState") initialBlogState) - (createCheckpointAndClose) - (\acid -> convertEntries acid) - -convertEntries acid = do - entries <- parseOldEntries - let r = map forceHack entries - rs <- sequence r - putStrLn $ show rs - where - forceHack !x = do - xy <- pasteToDB acid x - return $ show xy - -testThis :: IO () -testThis = do - acid <- openLocalState initialBlogState - allE <- query' acid LatestPosts - putStrLn $ show allE \ No newline at end of file diff --git a/tools/convertdb/Makefile b/tools/convertdb/Makefile deleted file mode 100644 index eba288ec1d..0000000000 --- a/tools/convertdb/Makefile +++ /dev/null @@ -1,13 +0,0 @@ -all: convertdb - -convertdb: couch.8 convertdb.8 - 8l -o convertdb convertdb.8 - -convertdb.8: convertdb.go - 8g convertdb.go - -couch.8: couch.go - 8g couch.go - -clean: - rm -rf *.8 convertdb diff --git a/tools/convertdb/convertdb.go b/tools/convertdb/convertdb.go deleted file mode 100644 index f7b94176b4..0000000000 --- a/tools/convertdb/convertdb.go +++ /dev/null @@ -1,116 +0,0 @@ -package main - -import ( - "strconv" - "fmt" - "io/ioutil" - "json" - "./couch" - "os" - "time" -) - -//old -type OldComment struct { - Author string - Text string - Date string -} - -type OldEntry struct { - Id string - Title string - Author string - Text string - Mtext string - Comments []OldComment -} - -//old -type Comment struct { - Author string `json:"cauthor"` - Text string `json:"ctext"` - Date int64 `json:"cdate"` -} - -type Entry struct { - Id string `json:"_id"` - Year int `json:"year"` - Month int `json:"month"` - Day int `json:"day"` - Lang string `json:"lang"` - Title string `json:"title"` - Author string `json:"author"` - Text string `json:"text"` - Mtext string `json:"mtext"` - Comments []Comment `json:"comments"` -} - -func main() { - getAllByYear("2011", 8, 12) - getAllByYear("2012", 1, 2) -} - -func getAllByYear(year string, minm, maxm int){ - db, _ := couch.NewDatabase("127.0.0.1", "5984", "tazblog") - for i:=minm;i<=maxm;i++{ - dirList, err := ioutil.ReadDir(fmt.Sprintf("data/%s/%02d/", year, i)) - if err != nil { - fmt.Println(err.String()) - os.Exit(1) - } - for d:=len(dirList)-1; d>-1; d--{ - content, cErr := ioutil.ReadFile(fmt.Sprintf("data/%s/%02d/%s", year, i, dirList[d].Name)) - if cErr != nil { - fmt.Println(cErr) - os.Exit(1) - } - var oEntry OldEntry - jErr := json.Unmarshal(content, &oEntry) - if jErr != nil { - fmt.Println(jErr.String()) - os.Exit(1) - } - nEntry := convertEntry(oEntry, fmt.Sprintf("data/%s/%02d/%s", year, i, dirList[d].Name)) - eId, _, err := db.Insert(nEntry) - if err != nil { - fmt.Println(err.String()) - os.Exit(1) - } - fmt.Println("Inserted " + eId) - } - } -} - -func convertEntry(oEntry OldEntry, p string) Entry{ - var nEntry Entry - nComments := make([]Comment, len(oEntry.Comments)) - for i:=0;i<len(oEntry.Comments);i++{ - nComments[i].Author = oEntry.Comments[i].Author - nComments[i].Text = oEntry.Comments[i].Text - nComments[i].Date = parseDumbTime(oEntry.Comments[i].Date) - } - - nEntry.Id = oEntry.Id[3:] - nEntry.Year, _ = strconv.Atoi(p[5:9]) - nEntry.Month, _ = strconv.Atoi(p[10:12]) - nEntry.Day, _ = strconv.Atoi(p[13:15]) - nEntry.Title = oEntry.Title - nEntry.Author = oEntry.Author - nEntry.Mtext = oEntry.Mtext - nEntry.Text = oEntry.Text - nEntry.Comments = nComments - nEntry.Lang = "DE" - - return nEntry -} - -func parseDumbTime(ct string) int64 { - x, err := time.Parse("[Am 02.01.2006 um 15:04 Uhr]", ct) - if err != nil { - fmt.Println(err.String()) - os.Exit(1) - } - - return x.Seconds() -} \ No newline at end of file diff --git a/tools/convertdb/couch.go b/tools/convertdb/couch.go deleted file mode 100644 index 764eb49a60..0000000000 --- a/tools/convertdb/couch.go +++ /dev/null @@ -1,403 +0,0 @@ -// -*- tab-width: 4 -*- -package couch - -import ( - "bytes" - "fmt" - "os" - "json" - "http" - "net" - "io/ioutil" - "url" -) - -var def_hdrs = map[string][]string{} - -type buffer struct { - b *bytes.Buffer -} - -func (b *buffer) Read(out []byte) (int, os.Error) { - return b.b.Read(out) -} - -func (b *buffer) Close() os.Error { return nil } - -// Converts given URL to string containing the body of the response. -func url_to_buf(u string) []byte { - if r, err := http.Get(u); err == nil { - b, err := ioutil.ReadAll(r.Body) - r.Body.Close() - if err == nil { - return b - } - } - return make([]byte, 0) -} - -type IdAndRev struct { - Id string `json:"_id"` - Rev string `json:"_rev"` -} - -// Sends a query to CouchDB and parses the response back. -// method: the name of the HTTP method (POST, PUT,...) -// url: the URL to interact with -// headers: additional headers to pass to the request -// in: body of the request -// out: a structure to fill in with the returned JSON document -func (p Database) interact(method, u string, headers map[string][]string, in []byte, out interface{}) (int, os.Error) { - fullHeaders := map[string][]string{} - for k, v := range headers { - fullHeaders[k] = v - } - bodyLength := 0 - if in != nil { - bodyLength = len(in) - fullHeaders["Content-Type"] = []string{"application/json"} - } - req := http.Request{ - Method: method, - ProtoMajor: 1, - ProtoMinor: 1, - Close: true, - ContentLength: int64(bodyLength), - Header: fullHeaders, - } - req.TransferEncoding = []string{"chunked"} - req.URL, _ = url.Parse(u) - if in != nil { - req.Body = &buffer{bytes.NewBuffer(in)} - } - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%s", p.Host, p.Port)) - if err != nil { - return 0, err - } - http_conn := http.NewClientConn(conn, nil) - defer http_conn.Close() - if err := http_conn.Write(&req); err != nil { - return 0, err - } - r, err := http_conn.Read(&req) - if err != nil { - return 0, err - } - if r.StatusCode < 200 || r.StatusCode >= 300 { - b := []byte{} - r.Body.Read(b) - return r.StatusCode, os.NewError("server said: " + r.Status) - } - decoder := json.NewDecoder(r.Body) - if err = decoder.Decode(out); err != nil { - return 0, err - } - r.Body.Close() - return r.StatusCode, nil -} - -type Database struct { - Host string - Port string - Name string -} - -func (p Database) BaseURL() string { - return fmt.Sprintf("http://%s:%s", p.Host, p.Port) -} - -func (p Database) DBURL() string { - return fmt.Sprintf("%s/%s", p.BaseURL(), p.Name) -} - -// Test whether CouchDB is running (ignores Database.Name) -func (p Database) Running() bool { - u := fmt.Sprintf("%s/%s", p.BaseURL(), "_all_dbs") - s := url_to_buf(u) - if len(s) > 0 { - return true - } - return false -} - -type database_info struct { - Db_name string - // other stuff too, ignore for now -} - -// Test whether specified database exists in specified CouchDB instance -func (p Database) Exists() bool { - di := new(database_info) - if err := json.Unmarshal(url_to_buf(p.DBURL()), di); err != nil { - return false - } - if di.Db_name != p.Name { - return false - } - return true -} - -func (p Database) create_database() os.Error { - ir := response{} - if _, err := p.interact("PUT", p.DBURL(), def_hdrs, nil, &ir); err != nil { - return err - } - if !ir.Ok { - return os.NewError("Create database operation returned not-OK") - } - return nil -} - -// Deletes the given database and all documents -func (p Database) DeleteDatabase() os.Error { - ir := response{} - if _, err := p.interact("DELETE", p.DBURL(), def_hdrs, nil, &ir); err != nil { - return err - } - if !ir.Ok { - return os.NewError("Delete database operation returned not-OK") - } - return nil -} - -func NewDatabase(host, port, name string) (Database, os.Error) { - db := Database{host, port, name} - if !db.Running() { - return db, os.NewError("CouchDB not running") - } - if !db.Exists() { - if err := db.create_database(); err != nil { - return db, err - } - } - return db, nil -} - -// Strip _id and _rev from d, returning them separately if they exist -func clean_JSON(d interface{}) (json_buf []byte, id, rev string, err os.Error) { - json_buf, err = json.Marshal(d) - if err != nil { - return - } - m := map[string]interface{}{} - err = json.Unmarshal(json_buf, &m) - if err != nil { - return - } - id_rev := new(IdAndRev) - err = json.Unmarshal(json_buf, &id_rev) - if err != nil { - return - } - if _, ok := m["_id"]; ok { - id = id_rev.Id - m["_id"] = nil, false - } - if _, ok := m["_rev"]; ok { - rev = id_rev.Rev - m["_rev"] = nil, false - } - json_buf, err = json.Marshal(m) - return -} - -type response struct { - Ok bool - Id string - Rev string - Error string - Reason string -} - -// Inserts document to CouchDB, returning id and rev on success. -// Document may specify both "_id" and "_rev" fields (will overwrite existing) -// or just "_id" (will use that id, but not overwrite existing) -// or neither (will use autogenerated id) -func (p Database) Insert(d interface{}) (string, string, os.Error) { - json_buf, id, rev, err := clean_JSON(d) - if err != nil { - return "", "", err - } - if id != "" && rev != "" { - new_rev, err2 := p.Edit(d) - return id, new_rev, err2 - } else if id != "" { - return p.insert_with(json_buf, id) - } else if id == "" { - return p.insert(json_buf) - } - return "", "", os.NewError("invalid Document") -} - -// Private implementation of simple autogenerated-id insert -func (p Database) insert(json_buf []byte) (string, string, os.Error) { - ir := response{} - if _, err := p.interact("POST", p.DBURL(), def_hdrs, json_buf, &ir); err != nil { - return "", "", err - } - if !ir.Ok { - return "", "", os.NewError(fmt.Sprintf("%s: %s", ir.Error, ir.Reason)) - } - return ir.Id, ir.Rev, nil -} - -// Inserts the given document (shouldn't contain "_id" or "_rev" tagged fields) -// using the passed 'id' as the _id. Will fail if the id already exists. -func (p Database) InsertWith(d interface{}, id string) (string, string, os.Error) { - json_buf, err := json.Marshal(d) - if err != nil { - return "", "", err - } - return p.insert_with(json_buf, id) -} - -// Private implementation of insert with given id -func (p Database) insert_with(json_buf []byte, id string) (string, string, os.Error) { - u := fmt.Sprintf("%s/%s", p.DBURL(), url.QueryEscape(id)) - ir := response{} - if _, err := p.interact("PUT", u, def_hdrs, json_buf, &ir); err != nil { - return "", "", err - } - if !ir.Ok { - return "", "", os.NewError(fmt.Sprintf("%s: %s", ir.Error, ir.Reason)) - } - return ir.Id, ir.Rev, nil -} - -// Edits the given document, returning the new revision. -// d must contain "_id" and "_rev" tagged fields. -func (p Database) Edit(d interface{}) (string, os.Error) { - json_buf, err := json.Marshal(d) - if err != nil { - return "", err - } - id_rev := new(IdAndRev) - err = json.Unmarshal(json_buf, id_rev) - if err != nil { - return "", err - } - if id_rev.Id == "" { - return "", os.NewError("Id not specified in interface") - } - if id_rev.Rev == "" { - return "", os.NewError("Rev not specified in interface (try InsertWith)") - } - u := fmt.Sprintf("%s/%s", p.DBURL(), url.QueryEscape(id_rev.Id)) - ir := response{} - if _, err = p.interact("PUT", u, def_hdrs, json_buf, &ir); err != nil { - return "", err - } - return ir.Rev, nil -} - -// Edits the given document, returning the new revision. -// d should not contain "_id" or "_rev" tagged fields. If it does, they will -// be overwritten with the passed values. -func (p Database) EditWith(d interface{}, id, rev string) (string, os.Error) { - if id == "" || rev == "" { - return "", os.NewError("EditWith: must specify both id and rev") - } - json_buf, err := json.Marshal(d) - if err != nil { - return "", err - } - m := map[string]interface{}{} - err = json.Unmarshal(json_buf, &m) - if err != nil { - return "", err - } - m["_id"] = id - m["_rev"] = rev - return p.Edit(m) -} - -// Unmarshals the document matching id to the given interface, returning rev. -func (p Database) Retrieve(id string, d interface{}) (string, os.Error) { - if id == "" { - return "", os.NewError("no id specified") - } - json_buf := url_to_buf(fmt.Sprintf("%s/%s", p.DBURL(), id)) - id_rev := new(IdAndRev) - if err := json.Unmarshal(json_buf, &id_rev); err != nil { - return "", err - } - if id_rev.Id != id { - return "", os.NewError("invalid id specified") - } - return id_rev.Rev, json.Unmarshal(json_buf, d) -} - -// Deletes document given by id and rev. -func (p Database) Delete(id, rev string) os.Error { - headers := map[string][]string{ - "If-Match": []string{rev}, - } - u := fmt.Sprintf("%s/%s", p.DBURL(), id) - ir := response{} - if _, err := p.interact("DELETE", u, headers, nil, &ir); err != nil { - return err - } - if !ir.Ok { - return os.NewError(fmt.Sprintf("%s: %s", ir.Error, ir.Reason)) - } - return nil -} - -type Row struct { - Id *string -} - -type keyed_view_response struct { - Total_rows uint64 - Offset uint64 - Rows []Row -} - -// Return array of document ids as returned by the given view/options combo. -// view should be eg. "_design/my_foo/_view/my_bar" -// options should be eg. { "limit": 10, "key": "baz" } -func (p Database) QueryIds(view string, options map[string]interface{}) ([]string, os.Error) { - kvr := new(keyed_view_response) - - if err := p.Query(view, options, kvr); err != nil { - fmt.Println("Query error: " + err.String()) - return make([]string, 0), err - } - ids := make([]string, len(kvr.Rows)) - i := 0 - for _, row := range kvr.Rows { - if row.Id != nil { - ids[i] = *row.Id - i++ - } - } - return ids[:i], nil -} - -func (p Database) Query(view string, options map[string]interface{}, results interface{}) os.Error { - if view == "" { - return os.NewError("empty view") - } - - var parameters string - for k, v := range options { - switch t := v.(type) { - case string: - parameters += fmt.Sprintf(`%s=%s&`, k, url.QueryEscape(t)) - case int: - parameters += fmt.Sprintf(`%s=%d&`, k, t) - case bool: - parameters += fmt.Sprintf(`%s=%v&`, k, t) - default: - // TODO more types are supported - panic(fmt.Sprintf("unsupported value-type %T in Query", t)) - } - } - full_url := fmt.Sprintf("%s/%s?%s", p.DBURL(), view, parameters) - json_buf := url_to_buf(full_url) - - if err := json.Unmarshal(json_buf, results); err != nil { - return err - } - return nil -} diff --git a/tools/fixcomments.hs b/tools/fixcomments.hs deleted file mode 100644 index dc89dbdd64..0000000000 --- a/tools/fixcomments.hs +++ /dev/null @@ -1,21 +0,0 @@ - -fixComments :: AcidState Blog -> IO () -fixComments acid = do - entriesDE <- query' acid $ LatestEntries DE - entriesEN <- query' acid $ LatestEntries EN - filterComments entriesDE - filterComments entriesEN - where - (cDate :: UTCTime) = fromJust $ parseTime defaultTimeLocale "%d.%m.%Y %T" "22.04.2012 21:57:35" - foldOp :: [(EntryId, [UTCTime])] -> Entry -> [(EntryId, [UTCTime])] - foldOp l e = let c = map cdate $ filter (\c1 -> cdate c1 > cDate) $ comments e - in if null c then l - else (entryId e, c) : l - pred :: Entry -> Bool - pred e = let f eId [] = False - f eId (c:r) = if (cdate c > cDate) then True - else f eId r - in f (entryId e) (comments e) - filterComments entries = mapM_ removeComments $ foldl foldOp [] $ filter pred entries - removeComments :: (EntryId, [UTCTime]) -> IO () - removeComments (eId, comments) = mapM_ (\c -> update' acid $ DeleteComment eId c) comments \ No newline at end of file diff --git a/tools/music/Makefile b/tools/music/Makefile deleted file mode 100644 index 488c7eb1b0..0000000000 --- a/tools/music/Makefile +++ /dev/null @@ -1,10 +0,0 @@ -all: music - -music: iTunes.8 - 8l -o music iTunes.8 - -iTunes.8: iTunes.go - 8g iTunes.go - -clean: - rm -rf *.8 music \ No newline at end of file diff --git a/tools/music/gettitle b/tools/music/gettitle deleted file mode 100755 index 0bd4cc6979..0000000000 --- a/tools/music/gettitle +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash - -osascript -e 'tell application "iTunes" to get name of current track' -osascript -e 'tell application "iTunes" to get artist of current track' diff --git a/tools/music/iTunes.go b/tools/music/iTunes.go deleted file mode 100644 index 5eb530f6b3..0000000000 --- a/tools/music/iTunes.go +++ /dev/null @@ -1,79 +0,0 @@ -/* This program is free software. It comes without any warranty, to - * the extent permitted by applicable law. You can redistribute it - * and/or modify it under the terms of the Do What The Fuck You Want - * To Do Public License, Version 3, as published by Vincent Ambo. See - * included COPYING file for more details. */ - -package main - -import( "fmt" - "exec" - "strings" - "http" - "url" - "flag" - "os" - "time" -) - -var authkey, host, c_artist, c_title string - -func init(){ - flag.StringVar(&authkey, "key", "none", "http auth key") - flag.StringVar(&host, "host", "http://localhost:8080", "host") -} - -func main(){ - flag.Parse() - fmt.Println("Music updater launching. Update occurs once per minute.") - go updaterThread() - - var cc string - for { - fmt.Println("Type \"exit\" to quit") - fmt.Scanf("%s", &cc) - switch(cc) { - case "exit": - os.Exit(1) - default: - fmt.Println("Type \"exit\" to quit") - - } - } -} - -func updaterThread(){ - rValues := make(url.Values) - rValues.Add("artist", "") - rValues.Add("title", "") - rValues.Add("key", authkey) - - for { - title, artist := getTrack() - if (title != c_title) || (artist != c_artist) { - fmt.Println("Updating to: " + title + " - " + artist) - c_artist = artist; c_title = title - rValues.Set("artist", artist) - rValues.Set("title", title) - _, err := http.PostForm(fmt.Sprint(host + "/setsong"), rValues) - if err != nil { - fmt.Println(err.String()) - } - } - time.Sleep(60000000000) - } -} - -func getTrack() (title, artist string){ - a, err := exec.Command("./gettitle").Output() - if err != nil { - fmt.Println("err: " + err.String()) - title = "" - artist = "" - } else { - trackInfo := strings.Split(string(a), "\n") - title = trackInfo[0] - artist = trackInfo[1] - } - return -} \ No newline at end of file diff --git a/tools/music/start b/tools/music/start deleted file mode 100755 index b9f1358e34..0000000000 --- a/tools/music/start +++ /dev/null @@ -1 +0,0 @@ -./music -host "http://tazj.in" -key "4058ef41bbca252a7b7e675a61dbf935" diff --git a/update.sh b/update.sh deleted file mode 100644 index a4229f941b..0000000000 --- a/update.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash - -hg pull -hg update -cabal install --reinstall \ No newline at end of file -- cgit 1.4.1 From aec8351dda2cdf05331b1dc36b3c95310228123e Mon Sep 17 00:00:00 2001 From: Vincent Ambo <vincent@spotify.com> Date: Wed, 19 Mar 2014 12:21:43 +0100 Subject: Relicense as MIT --- LICENSE | 22 +++++++++++++++++++++- TazBlog.cabal | 1 + arch/PKGBUILD | 1 + 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index c3b19de170..f5c81f7e3a 100644 --- a/LICENSE +++ b/LICENSE @@ -1 +1,21 @@ -I don't feel like writing a license for this. Do whatever you want with this, but credit me. \ No newline at end of file +The MIT License (MIT) + +Copyright (c) 2014 Vincent Ambo + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/TazBlog.cabal b/TazBlog.cabal index c254361f2f..e40b61210a 100644 --- a/TazBlog.cabal +++ b/TazBlog.cabal @@ -1,6 +1,7 @@ Name: TazBlog Version: 4.1 Synopsis: Tazjin's Blog +License: MIT License-file: LICENSE Author: Vincent Ambo Maintainer: tazjin@gmail.com diff --git a/arch/PKGBUILD b/arch/PKGBUILD index 0500012fbc..3d6331b1c3 100644 --- a/arch/PKGBUILD +++ b/arch/PKGBUILD @@ -5,6 +5,7 @@ pkgrel=1 pkgdesc="Tazjin's blog written in Haskell" arch=('i686' 'x86_64') url="http://tazj.in" +license=('MIT') makedepends=('ghc' 'cabal-install') source=(https://bitbucket.org/tazjin/tazblog-haskell/get/$pkgver.tar.gz) md5sums=('981cdfdd3cba59f13213a644717f5343') -- cgit 1.4.1 From 104cc1fc00c765b539243a594cb28cbe4bc429ab Mon Sep 17 00:00:00 2001 From: Vincent Ambo <vincent@spotify.com> Date: Wed, 19 Mar 2014 12:23:56 +0100 Subject: Bump Arch pkgrel --- arch/PKGBUILD | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arch/PKGBUILD b/arch/PKGBUILD index 3d6331b1c3..2b7b9541b2 100644 --- a/arch/PKGBUILD +++ b/arch/PKGBUILD @@ -1,7 +1,7 @@ # Maintainer: Vincent Ambo <dev@tazj.in> pkgname=tazblog pkgver=4.1 -pkgrel=1 +pkgrel=2 pkgdesc="Tazjin's blog written in Haskell" arch=('i686' 'x86_64') url="http://tazj.in" -- cgit 1.4.1 From 33c15489f8150d4e2e13e31621e2010e8d0a29bb Mon Sep 17 00:00:00 2001 From: Vincent Ambo <vincent@spotify.com> Date: Wed, 19 Mar 2014 12:26:21 +0100 Subject: Update md5sum in PKGBUILD --- arch/PKGBUILD | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arch/PKGBUILD b/arch/PKGBUILD index 2b7b9541b2..06365f114f 100644 --- a/arch/PKGBUILD +++ b/arch/PKGBUILD @@ -8,7 +8,7 @@ url="http://tazj.in" license=('MIT') makedepends=('ghc' 'cabal-install') source=(https://bitbucket.org/tazjin/tazblog-haskell/get/$pkgver.tar.gz) -md5sums=('981cdfdd3cba59f13213a644717f5343') +md5sums=('c6f4966785e8fb4c1fb93477403c2731') build() { -- cgit 1.4.1 From 044e8906e417c4faf8a95bbd8ad10cad4d419a6c Mon Sep 17 00:00:00 2001 From: Vincent Ambo <vincent@spotify.com> Date: Wed, 16 Apr 2014 12:25:19 +0200 Subject: 4.2: Added keybase proof --- arch/PKGBUILD | 6 ++--- res/keybase.txt | 68 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 3 deletions(-) create mode 100644 res/keybase.txt diff --git a/arch/PKGBUILD b/arch/PKGBUILD index 06365f114f..d6c7ea46cf 100644 --- a/arch/PKGBUILD +++ b/arch/PKGBUILD @@ -1,14 +1,14 @@ # Maintainer: Vincent Ambo <dev@tazj.in> pkgname=tazblog -pkgver=4.1 -pkgrel=2 +pkgver=4.2 +pkgrel=1 pkgdesc="Tazjin's blog written in Haskell" arch=('i686' 'x86_64') url="http://tazj.in" license=('MIT') makedepends=('ghc' 'cabal-install') source=(https://bitbucket.org/tazjin/tazblog-haskell/get/$pkgver.tar.gz) -md5sums=('c6f4966785e8fb4c1fb93477403c2731') +md5sums=('SKIP') build() { diff --git a/res/keybase.txt b/res/keybase.txt new file mode 100644 index 0000000000..94a8fcc7f9 --- /dev/null +++ b/res/keybase.txt @@ -0,0 +1,68 @@ +================================================================== +https://keybase.io/tazjin +-------------------------------------------------------------------- + +I hereby claim: + + * I am an admin of http://tazj.in + * I am tazjin (https://keybase.io/tazjin) on keybase. + * I have a public key with fingerprint DCF3 4CFA C1AC 44B8 7E26 3331 36EE 3481 4F6D 294A + +To claim this, I am signing this object: + +{ + "body": { + "key": { + "fingerprint": "dcf34cfac1ac44b87e26333136ee34814f6d294a", + "host": "keybase.io", + "key_id": "36EE34814F6D294A", + "uid": "2268b75a56bb9693d3ef077bc1217900", + "username": "tazjin" + }, + "service": { + "hostname": "tazj.in", + "protocol": "http:" + }, + "type": "web_service_binding", + "version": 1 + }, + "ctime": 1397643582, + "expire_in": 157680000, + "prev": "39412fda4ef75c58357b7b073b0432e70d84cefc1e4f7293af0d2c357e6ee3bf", + "seqno": 3, + "tag": "signature" +} + +with the aforementioned key, yielding the PGP signature: + +-----BEGIN PGP MESSAGE----- +Version: GnuPG v2.0.22 (GNU/Linux) +owGbwMvMwMWY9pU1Q3bHF2vG0wdeJTEE+0W6VSsl5adUKllVK2Wngqm0zLz01KKC +osy8EiUrpZTkNGOT5LTEZMPEZBOTJAvzVCMzY2NjQ2Oz1FRjEwtDkzSzFCNLk0Ql +HaWM/GKQDqAxSYnFqXqZ+UAxICc+MwUoamzm6gpW72bmAlTvCJQrBUsYGZlZJJmb +JpqaJSVZmlkapxinphmYmyclGxoZmlsaGIAUFqcW5SXmpgJVlyRWZWXmKdXqKAHF +yjKTU0EuBlmMJK8HVKCjVFCUX5KfnJ8DFMwoKSmwAukpqSwAKSpPTYqHao9PysxL +AXoYqKEstag4Mz9PycoQqDK5JBNknqGxpbmZibGphZGOUmpFQWZRanwmSIWpuZmF +ARCArEktA3nP0sTQKC0l0SQ1zdw02dTC2NQ8yTzJwNw4ycDE2CjV3CDFwiQ5NS3Z +MNUkzdzI0jgxzSDFKBmoKhUUjklpSiAPFeblK1kZA52ZmA40sjgzPS+xpLQoVam2 +k0mGhYGRi4GNlQkUZQxcnAKwiIybyP+/7grfnOLwh9NWuSl90lrVJc9Yf4VdsDXl +7myDtqgv5+a3vI3aPZO//0DEi8Sv//w3eSyJXOa/j+PK20z3bzPcm1OWnWdT6L7/ +/Z/c4a5J1zd/DMmz7pgh1+G9pPvek8mTzGwu9sVH+c6qtepZILU9Zb+Rz3sHVZU8 +haT+VyHNDxxcw+raTv6b9FFEwrp19i3tIM6ml8v5VnLqdex9sudP2MW9hkazX/vb +mZixL6stPnL8mX2gMg/PM/dVVzh07189c8b+mtLxO863mX/W6K8StIwWOrriwqa9 +X/8Fczk3qDwLnrR04i/5fNWWJ6cOuuZYRmTUi/KW/+ctW2c0w1zNxudMwPNv6/c4 +uBziFZEUC/0x33zZvkOeax4o7LWr+vPibpHvpJlRM5qtrUsCXpw/Uh32jlF+t0PL +EjY+RVWxwoP7j/tHbl35Y+rcZlUr9znGVyNf/b9ZLrN3rwlDEcfkyXu7/ReFc7Xf +ZVJ2jD0jO6PivMqm+DNLWZyvXEor8TD+u7NdZvXhm7K9tXOXO6w/8r583q/qngnH +hcQiHrLLG5rNNFR7WrpGfYZAr+PfhMXvak4J3fGLX5IRqbgkvOOpm+1u3e4e8xtb +pwlNvntv/YoLtecM1/P2nOp7y6Opaf10ZrnwvcbSDNZ5GttibG5zH+WT/Pyq62Ro +/EZnvj9FprUfGBzZmh36OM3+3roV+KxG1oT/xyvtv16VqUEA +=fuV3 +-----END PGP MESSAGE----- + +And finally, I am proving ownership of this host by posting or +appending to this document. + +View my publicly-auditable identity here: https://keybase.io/tazjin + +================================================================== -- cgit 1.4.1 From 6cbc1a4448141fc74f610ee5f2beb3608436f486 Mon Sep 17 00:00:00 2001 From: Vincent Ambo <vincent@spotify.com> Date: Wed, 16 Apr 2014 12:40:05 +0200 Subject: Fixed build due to renamed PKG --- TazBlog.cabal | 2 +- res/keybase.txt | 41 +++++++++++++++++++++-------------------- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/TazBlog.cabal b/TazBlog.cabal index e40b61210a..bd903c6763 100644 --- a/TazBlog.cabal +++ b/TazBlog.cabal @@ -36,5 +36,5 @@ Executable tazblog options, rss, hamlet, - shakespeare-css, + shakespeare, markdown diff --git a/res/keybase.txt b/res/keybase.txt index 94a8fcc7f9..661c33e01e 100644 --- a/res/keybase.txt +++ b/res/keybase.txt @@ -26,10 +26,10 @@ To claim this, I am signing this object: "type": "web_service_binding", "version": 1 }, - "ctime": 1397643582, + "ctime": 1397644545, "expire_in": 157680000, - "prev": "39412fda4ef75c58357b7b073b0432e70d84cefc1e4f7293af0d2c357e6ee3bf", - "seqno": 3, + "prev": "4973fdda56a6cfa726a813411c915458c652be45dd19283f7a4ae4f9c217df14", + "seqno": 4, "tag": "signature" } @@ -37,27 +37,28 @@ with the aforementioned key, yielding the PGP signature: -----BEGIN PGP MESSAGE----- Version: GnuPG v2.0.22 (GNU/Linux) -owGbwMvMwMWY9pU1Q3bHF2vG0wdeJTEE+0W6VSsl5adUKllVK2Wngqm0zLz01KKC + +owGbwMvMwMWY9pU1Q3bHF2vG0wdeJTEE+8WyVSsl5adUKllVK2Wngqm0zLz01KKC osy8EiUrpZTkNGOT5LTEZMPEZBOTJAvzVCMzY2NjQ2Oz1FRjEwtDkzSzFCNLk0Ql HaWM/GKQDqAxSYnFqXqZ+UAxICc+MwUoamzm6gpW72bmAlTvCJQrBUsYGZlZJJmb JpqaJSVZmlkapxinphmYmyclGxoZmlsaGIAUFqcW5SXmpgJVlyRWZWXmKdXqKAHF yjKTU0EuBlmMJK8HVKCjVFCUX5KfnJ8DFMwoKSmwAukpqSwAKSpPTYqHao9PysxL -AXoYqKEstag4Mz9PycoQqDK5JBNknqGxpbmZibGphZGOUmpFQWZRanwmSIWpuZmF -ARCArEktA3nP0sTQKC0l0SQ1zdw02dTC2NQ8yTzJwNw4ycDE2CjV3CDFwiQ5NS3Z -MNUkzdzI0jgxzSDFKBmoKhUUjklpSiAPFeblK1kZA52ZmA40sjgzPS+xpLQoVam2 -k0mGhYGRi4GNlQkUZQxcnAKwiIybyP+/7grfnOLwh9NWuSl90lrVJc9Yf4VdsDXl -7myDtqgv5+a3vI3aPZO//0DEi8Sv//w3eSyJXOa/j+PK20z3bzPcm1OWnWdT6L7/ -/Z/c4a5J1zd/DMmz7pgh1+G9pPvek8mTzGwu9sVH+c6qtepZILU9Zb+Rz3sHVZU8 -haT+VyHNDxxcw+raTv6b9FFEwrp19i3tIM6ml8v5VnLqdex9sudP2MW9hkazX/vb -mZixL6stPnL8mX2gMg/PM/dVVzh07189c8b+mtLxO863mX/W6K8StIwWOrriwqa9 -X/8Fczk3qDwLnrR04i/5fNWWJ6cOuuZYRmTUi/KW/+ctW2c0w1zNxudMwPNv6/c4 -uBziFZEUC/0x33zZvkOeax4o7LWr+vPibpHvpJlRM5qtrUsCXpw/Uh32jlF+t0PL -EjY+RVWxwoP7j/tHbl35Y+rcZlUr9znGVyNf/b9ZLrN3rwlDEcfkyXu7/ReFc7Xf -ZVJ2jD0jO6PivMqm+DNLWZyvXEor8TD+u7NdZvXhm7K9tXOXO6w/8r583q/qngnH -hcQiHrLLG5rNNFR7WrpGfYZAr+PfhMXvak4J3fGLX5IRqbgkvOOpm+1u3e4e8xtb -pwlNvntv/YoLtecM1/P2nOp7y6Opaf10ZrnwvcbSDNZ5GttibG5zH+WT/Pyq62Ro -/EZnvj9FprUfGBzZmh36OM3+3roV+KxG1oT/xyvtv16VqUEA -=fuV3 +AXoYqKEstag4Mz9PycoQqDK5JBNknqGxpbmZiYmpiamOUmpFQWZRanwmSIWpuZmF +ARCArEktAxppYmlunJaSAvRFohkwtMyNzBItDI1NDA2TLQ2Bui2SzUyNklJNTFNS +DC2NLIzTzBNNElNN0iyTgZ5MSTM0UQJ5qDAvX8nKBOjMxHSgkcWZ6XmJJaVFqUq1 +nUwyLAyMXAxsrEygKGPg4hSARWSZH/8/0573HMdvfH5XxeayYZ2efPb8bw730i1/ +WBU3qru5pKlf3xKmeK5ihtKeT6VXGm3usV2reZWyvO/0joi83oT9P80s88Q6U/vb +vmycHnB7e110v/3OZadu/Sx6+uXk/ZeCR8u+p/+6dNc8XWqX/68t06pnrGKU/BfU +F7X5S/HUy4ysvyZN+v1Jj6NtMvvN1EvPpCpv3kz2tGU1EzpZFfl8Xujq1OopuxZJ +l5kvDlgZ78ezdLZ1+aOlixbsXra4/3fdbZ8XnQX1DatzV18+e2rmMcPKm6qngqIf +Xp8oKTAz+Mg1v6gHP0wLN/Mf3JKjYHnX5U6L/KIvkbsLArtES0r7w1iWZ3OvvSPr +fW6heune1tOb7j3vP+1XeOyV2ekr6pPO3bdrv9X25HbTaqs7z06f0v35fmtQ3uUZ +Z35eLYmaEmb/x/u3vFh6GsvMDocpCTpPlHa0z+xzOGbhzLFO18v21Zd9ISG3Hqtd +F7jaLlWa2W+TsytNnXudVrfCBSbl8zNMfuk2e0Z8i9ix3PmEVa3rTEfhde3qwgtY +dy8rUbzzd5d9ccF63btqO/VMb4oe04x4uCLB5RD3p+8+s77o/T4WP2cFw+0cviX6 +StlJX5f+U3Or3fZY7dUfPcmMJZ/eSs7m+1d5IUbs3jI27olHFzGVvTcsu7w79aOK +SxmXvnEIUwZXgP6BL4LrPDY1rN2V0q1cZj1/efj880rzeu6+OQYA +=xHfH -----END PGP MESSAGE----- And finally, I am proving ownership of this host by posting or -- cgit 1.4.1 From 19cc93685b2ae499c913a8ff2fc27877d5e213b6 Mon Sep 17 00:00:00 2001 From: Vincent Ambo <vincent@spotify.com> Date: Thu, 8 May 2014 15:57:10 +0200 Subject: (temporary?) fix for builds on GHC 7.8 --- src/BlogDB.hs | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/src/BlogDB.hs b/src/BlogDB.hs index c054d7f17d..21d887297f 100644 --- a/src/BlogDB.hs +++ b/src/BlogDB.hs @@ -34,7 +34,9 @@ import qualified Data.Text as Text newtype EntryId = EntryId { unEntryId :: Integer } - deriving (Eq, Ord, Data, Enum, Typeable, SafeCopy) + deriving (Eq, Ord, Data, Enum, Typeable) + +$(deriveSafeCopy 0 'base ''EntryId) instance Show EntryId where show = show . unEntryId @@ -78,15 +80,25 @@ data Entry = Entry { $(deriveSafeCopy 0 'base ''Entry) -- ixSet requires different datatypes for field indexes, so let's define some -newtype Author = Author Text deriving (Eq, Ord, Data, Typeable, SafeCopy) -newtype Title = Title Text deriving (Eq, Ord, Data, Typeable, SafeCopy) -newtype BText = BText Text deriving (Eq, Ord, Data, Typeable, SafeCopy) -- standard text -newtype MText = MText Text deriving (Eq, Ord, Data, Typeable, SafeCopy) -- "read more" text -newtype Tag = Tag Text deriving (Eq, Ord, Data, Typeable, SafeCopy) -newtype EDate = EDate UTCTime deriving (Eq, Ord, Data, Typeable, SafeCopy) -newtype SDate = SDate UTCTime deriving (Eq, Ord, Data, Typeable, SafeCopy) -newtype Username = Username Text deriving (Eq, Ord, Data, Typeable, SafeCopy) -newtype SessionID = SessionID Text deriving (Eq, Ord, Data, Typeable, SafeCopy) +newtype Author = Author Text deriving (Eq, Ord, Data, Typeable) +newtype Title = Title Text deriving (Eq, Ord, Data, Typeable) +newtype BText = BText Text deriving (Eq, Ord, Data, Typeable) -- standard text +newtype MText = MText Text deriving (Eq, Ord, Data, Typeable) -- "read more" text +newtype Tag = Tag Text deriving (Eq, Ord, Data, Typeable) +newtype EDate = EDate UTCTime deriving (Eq, Ord, Data, Typeable) +newtype SDate = SDate UTCTime deriving (Eq, Ord, Data, Typeable) +newtype Username = Username Text deriving (Eq, Ord, Data, Typeable) +newtype SessionID = SessionID Text deriving (Eq, Ord, Data, Typeable) + +$(deriveSafeCopy 0 'base ''Author) +$(deriveSafeCopy 0 'base ''Title) +$(deriveSafeCopy 0 'base ''BText) +$(deriveSafeCopy 0 'base ''MText) +$(deriveSafeCopy 0 'base ''Tag) +$(deriveSafeCopy 0 'base ''EDate) +$(deriveSafeCopy 0 'base ''SDate) +$(deriveSafeCopy 0 'base ''Username) +$(deriveSafeCopy 0 'base ''SessionID) instance Indexable Entry where empty = ixSet [ ixFun $ \e -> [ entryId e] -- cgit 1.4.1 From 5f6841afa263a7d5938e31eef8df1f8066cd7dd5 Mon Sep 17 00:00:00 2001 From: Vincent Ambo <vincent@spotify.com> Date: Sun, 18 May 2014 21:56:39 +0200 Subject: Move all used GHC extensions to Cabal --- TazBlog.cabal | 12 +++++++++++- src/Blog.hs | 7 ------- src/BlogDB.hs | 8 -------- src/Locales.hs | 4 ---- src/Main.hs | 10 ---------- src/RSS.hs | 2 -- 6 files changed, 11 insertions(+), 32 deletions(-) diff --git a/TazBlog.cabal b/TazBlog.cabal index bd903c6763..2ab0998230 100644 --- a/TazBlog.cabal +++ b/TazBlog.cabal @@ -14,7 +14,6 @@ Executable tazblog hs-source-dirs: src main-is: Main.hs ghc-options: -O2 - Build-depends: base, bytestring, @@ -38,3 +37,14 @@ Executable tazblog hamlet, shakespeare, markdown + extensions: + DeriveDataTypeable + FlexibleContexts + GeneralizedNewtypeDeriving + MultiParamTypeClasses + OverloadedStrings + RecordWildCards + ScopedTypeVariables + TemplateHaskell + TypeFamilies + QuasiQuotes diff --git a/src/Blog.hs b/src/Blog.hs index 67adf0cd3c..00ba6c94d0 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -1,10 +1,3 @@ -{-# LANGUAGE DeriveDataTypeable #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE QuasiQuotes #-} -{-# LANGUAGE RecordWildCards #-} -{-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE TemplateHaskell #-} - module Blog where import BlogDB diff --git a/src/BlogDB.hs b/src/BlogDB.hs index 21d887297f..e2787794c3 100644 --- a/src/BlogDB.hs +++ b/src/BlogDB.hs @@ -1,11 +1,3 @@ -{-# LANGUAGE DeriveDataTypeable #-} -{-# LANGUAGE GeneralizedNewtypeDeriving #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE RecordWildCards #-} -{-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE TypeFamilies #-} - module BlogDB where import Control.Monad.Reader (ask) diff --git a/src/Locales.hs b/src/Locales.hs index 7fd874c1d0..206545d447 100644 --- a/src/Locales.hs +++ b/src/Locales.hs @@ -1,7 +1,3 @@ -{-# LANGUAGE DeriveDataTypeable #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE ScopedTypeVariables #-} - module Locales where import Data.Data (Data, Typeable) diff --git a/src/Main.hs b/src/Main.hs index 67440e6f0e..2579d57696 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -1,13 +1,3 @@ -{-# LANGUAGE DeriveDataTypeable #-} -{-# LANGUAGE FlexibleContexts #-} -{-# LANGUAGE GeneralizedNewtypeDeriving #-} -{-# LANGUAGE MultiParamTypeClasses #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE RecordWildCards #-} -{-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE TypeFamilies #-} - module Main where import Control.Applicative (optional, pure, (<$>), (<*>)) diff --git a/src/RSS.hs b/src/RSS.hs index 2309b1297c..6a244129fe 100644 --- a/src/RSS.hs +++ b/src/RSS.hs @@ -1,5 +1,3 @@ -{-# LANGUAGE RecordWildCards #-} - module RSS (renderFeed) where import qualified Data.Text as T -- cgit 1.4.1 From a5481e70e4213595a9c48e0d73178ed9ffb9a073 Mon Sep 17 00:00:00 2001 From: Vincent Ambo <vincent@spotify.com> Date: Sun, 18 May 2014 22:39:38 +0200 Subject: Refactoring: Moved Happstack things to Server.hs --- .stylish.haskell.yaml | 20 +++++ src/BlogDB.hs | 8 -- src/Main.hs | 219 ++---------------------------------------------- src/Server.hs | 224 ++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 250 insertions(+), 221 deletions(-) create mode 100644 .stylish.haskell.yaml create mode 100644 src/Server.hs diff --git a/.stylish.haskell.yaml b/.stylish.haskell.yaml new file mode 100644 index 0000000000..cb432ce231 --- /dev/null +++ b/.stylish.haskell.yaml @@ -0,0 +1,20 @@ +steps: + - imports: + align: group + - language_pragmas: + style: vertical + remove_redundant: true + - records: {} + - trailing_whitespace: {} +columns: 120 +language_extensions: + - DeriveDataTypeable + - FlexibleContexts + - GeneralizedNewtypeDeriving + - MultiParamTypeClasses + - OverloadedStrings + - RecordWildCards + - ScopedTypeVariables + - TemplateHaskell + - TypeFamilies + - QuasiQuotes diff --git a/src/BlogDB.hs b/src/BlogDB.hs index e2787794c3..ca20aedb09 100644 --- a/src/BlogDB.hs +++ b/src/BlogDB.hs @@ -15,7 +15,6 @@ import Data.SafeCopy (SafeCopy, base, deriveSafeCopy) import Data.Text (Text, pack) import Data.Text.Lazy (toStrict) import Data.Time -import Happstack.Server (FromReqURI (..)) import System.Environment (getEnv) import qualified Crypto.Hash.SHA512 as SHA (hash) @@ -40,13 +39,6 @@ instance Show BlogLang where show DE = "de" show EN = "en" -instance FromReqURI BlogLang where - fromReqURI sub = - case map toLower sub of - "de" -> Just DE - "en" -> Just EN - _ -> Nothing - $(deriveSafeCopy 0 'base ''BlogLang) data Comment = Comment { diff --git a/src/Main.hs b/src/Main.hs index 2579d57696..a50ca67ed1 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -1,34 +1,14 @@ module Main where -import Control.Applicative (optional, pure, (<$>), (<*>)) +import Control.Applicative (pure, (<$>), (<*>)) import Control.Exception (bracket) -import Control.Monad (liftM, msum, mzero, unless, when) -import Control.Monad.IO.Class (liftIO) -import Control.Monad.Reader (ask) -import Control.Monad.State (get, put) -import qualified Crypto.Hash.SHA512 as SHA import Data.Acid -import Data.Acid.Advanced -import Data.Acid.Local -import qualified Data.ByteString.Base64 as B64 (encode) -import Data.ByteString.Char8 (ByteString, pack, unpack) -import Data.Data (Data, Typeable) -import Data.Maybe (fromJust) -import Data.Monoid (mempty) -import Data.SafeCopy (base, deriveSafeCopy) -import Data.Text (Text) -import qualified Data.Text as T -import Data.Time -import Happstack.Server hiding (Session) -import Happstack.Server.Compression +import Data.Acid.Local (createCheckpointAndClose) import Options -import System.Locale (defaultTimeLocale) -import Blog -import BlogDB hiding (addComment, deleteComment, - updateEntry) -import Locales -import RSS +import BlogDB (initialBlogState) +import Locales (version) +import Server {- Server -} @@ -47,199 +27,12 @@ instance Options MainOptions where <*> simpleOption "res" "/usr/share/tazblog/res" "Resources folder location." -tmpPolicy :: BodyPolicy -tmpPolicy = defaultBodyPolicy "./tmp/" 0 200000 1000 - main :: IO() main = do putStrLn ("TazBlog " ++ version ++ " in Haskell starting") runCommand $ \opts args -> bracket (openLocalStateFrom (optState opts ++ "BlogState") initialBlogState) createCheckpointAndClose - (\acid -> simpleHTTP nullConf {port = optPort opts} $ tazBlog acid (optRes opts)) - -tazBlog :: AcidState Blog -> String -> ServerPart Response -tazBlog acid resDir = do - compr <- compressedResponseFilter - msum [ path $ \(lang :: BlogLang) -> blogHandler acid lang - , nullDir >> showIndex acid EN - , dir " " $ nullDir >> - seeOther ("https://plus.google.com/115916629925754851590" :: Text) (toResponse ()) - , path $ \(year :: Int) -> path $ \(month :: Int) -> path $ \(id_ :: String) -> formatOldLink year month id_ - , dir "res" $ serveDirectory DisableBrowsing [] "../res" - , dir "notice" $ ok $ toResponse showSiteNotice - {- :Admin handlers -} - , do dirs "admin/postentry" nullDir - guardSession acid - postEntry acid - , do dirs "admin/entrylist" $ dir (show DE) nullDir - guardSession acid - entryList acid DE - , do dirs "admin/entrylist" $ dir (show EN) nullDir - guardSession acid - entryList acid EN - , do guardSession acid - dirs "admin/edit" $ path $ \(eId :: Integer) -> editEntry acid eId - , do guardSession acid - dirs "admin/updateentry" $ nullDir >> updateEntry acid - , do guardSession acid - dirs "admin/cdelete" $ path $ \(eId :: Integer) -> path $ \(cId :: String) -> - deleteComment acid (EntryId eId) cId - , do dir "admin" nullDir - guardSession acid - ok $ toResponse $ adminIndex ("tazjin" :: Text) - , dir "admin" $ ok $ toResponse adminLogin - , dir "dologin" $ processLogin acid - , do dirs "static/blogv40.css" nullDir - setHeaderM "content-type" "text/css" - setHeaderM "cache-control" "max-age=630720000" - setHeaderM "expires" "Tue, 20 Jan 2037 04:20:42 GMT" - ok $ toResponse blogStyle - , do setHeaderM "cache-control" "max-age=630720000" - setHeaderM "expires" "Tue, 20 Jan 2037 04:20:42 GMT" - dir "static" $ serveDirectory DisableBrowsing [] resDir - , serveDirectory DisableBrowsing [] resDir - , notFound $ toResponse $ showError NotFound DE - ] - -blogHandler :: AcidState Blog -> BlogLang -> ServerPart Response -blogHandler acid lang = - msum [ path $ \(eId :: Integer) -> showEntry acid lang $ EntryId eId - , nullDir >> showIndex acid lang - , dir "rss" $ nullDir >> showRSS acid lang - , dir "rss.xml" $ nullDir >> showRSS acid lang - , notFound $ toResponse $ showError NotFound lang - ] - -formatOldLink :: Int -> Int -> String -> ServerPart Response -formatOldLink y m id_ = - flip seeOther (toResponse ()) $ - concat $ intersperse' "/" ["de", show y, show m, replace '.' '/' id_] - -showEntry :: AcidState Blog -> BlogLang -> EntryId -> ServerPart Response -showEntry acid lang eId = do - entry <- query' acid (GetEntry eId) - tryEntry entry lang - -tryEntry :: Maybe Entry -> BlogLang -> ServerPart Response -tryEntry Nothing lang = notFound $ toResponse $ showError NotFound lang -tryEntry (Just entry) _ = ok $ toResponse $ blogTemplate eLang eTitle $ renderEntry entry - where - eTitle = T.append ": " (title entry) - eLang = lang entry - -showIndex :: AcidState Blog -> BlogLang -> ServerPart Response -showIndex acid lang = do - entries <- query' acid (LatestEntries lang) - (page :: Maybe Int) <- optional $ lookRead "page" - ok $ toResponse $ blogTemplate lang "" $ - renderEntries False (eDrop page entries) (topText lang) (Just $ showLinks page lang) - where - eDrop :: Maybe Int -> [a] -> [a] - eDrop (Just i) = drop ((i-1) * 6) - eDrop Nothing = drop 0 - -showRSS :: AcidState Blog -> BlogLang -> ServerPart Response -showRSS acid lang = do - entries <- query' acid (LatestEntries lang) - feed <- liftIO $ renderFeed lang $ take 6 entries - setHeaderM "content-type" "text/xml" - ok $ toResponse feed - -{- ADMIN stuff -} - -postEntry :: AcidState Blog -> ServerPart Response -postEntry acid = do - decodeBody tmpPolicy - now <- liftIO getCurrentTime - let eId = timeToId now - lang <- look "lang" - nBtext <- lookText' "btext" - nMtext <- lookText' "mtext" - nEntry <- Entry <$> pure eId - <*> getLang lang - <*> readCookieValue "sUser" - <*> lookText' "title" - <*> pure nBtext - <*> pure nMtext - <*> pure now - <*> pure [] -- NYI - <*> pure [] - update' acid (InsertEntry nEntry) - seeOther ("/" ++ lang ++ "/" ++ show eId) (toResponse()) - where - timeToId :: UTCTime -> EntryId - timeToId t = EntryId . read $ formatTime defaultTimeLocale "%s" t - getLang :: String -> ServerPart BlogLang - getLang "de" = return DE - getLang "en" = return EN - -entryList :: AcidState Blog -> BlogLang -> ServerPart Response -entryList acid lang = do - entries <- query' acid (LatestEntries lang) - ok $ toResponse $ adminEntryList entries - -editEntry :: AcidState Blog -> Integer -> ServerPart Response -editEntry acid i = do - (Just entry) <- query' acid (GetEntry eId) - ok $ toResponse $ editPage entry - where - eId = EntryId i - -updateEntry :: AcidState Blog -> ServerPart Response -- TODO: Clean this up -updateEntry acid = do - decodeBody tmpPolicy - (eId :: Integer) <- lookRead "eid" - (Just entry) <- query' acid (GetEntry $ EntryId eId) - nTitle <- lookText' "title" - nBtext <- lookText' "btext" - nMtext <- lookText' "mtext" - let nEntry = entry { title = nTitle - , btext = nBtext - , mtext = nMtext} - update' acid (UpdateEntry nEntry) - seeOther (concat $ intersperse' "/" [show $ lang entry, show eId]) - (toResponse ()) - -deleteComment :: AcidState Blog -> EntryId -> String -> ServerPart Response -deleteComment acid eId cId = do - nEntry <- update' acid (DeleteComment eId cDate) - ok $ toResponse $ commentDeleted eId - where - (cDate :: UTCTime) = fromJust $ parseTime defaultTimeLocale "%s%Q" cId - -guardSession :: AcidState Blog -> ServerPartT IO () -guardSession acid = do - (sId :: Text) <- readCookieValue "session" - (uName :: Text) <- readCookieValue "sUser" - now <- liftIO getCurrentTime - mS <- query' acid (GetSession $ SessionID sId) - case mS of - Nothing -> mzero - (Just Session{..}) -> unless ((uName == username user) && sessionTimeDiff now sdate) - mzero - where - sessionTimeDiff :: UTCTime -> UTCTime -> Bool - sessionTimeDiff now sdate = diffUTCTime now sdate < 43200 - + (\acid -> runBlog acid (optPort opts) (optRes opts)) -processLogin :: AcidState Blog -> ServerPart Response -processLogin acid = do - decodeBody tmpPolicy - account <- lookText' "account" - password <- look "password" - login <- query' acid (CheckUser (Username account) password) - if login - then createSession account - else ok $ toResponse adminLogin - where - createSession account = do - now <- liftIO getCurrentTime - let sId = hashString $ show now - addCookie (MaxAge 43200) (mkCookie "session" $ unpack sId) - addCookie (MaxAge 43200) (mkCookie "sUser" $ T.unpack account) - (Just user) <- query' acid (GetUser $ Username account) - let nSession = Session (T.pack $ unpack sId) user now - update' acid (AddSession nSession) - seeOther ("/admin?do=login" :: Text) (toResponse()) diff --git a/src/Server.hs b/src/Server.hs new file mode 100644 index 0000000000..bc1f51298b --- /dev/null +++ b/src/Server.hs @@ -0,0 +1,224 @@ +-- Server implementation based on Happstack + +module Server where + +import Control.Applicative (optional, pure, (<$>), (<*>)) +import Control.Monad (liftM, msum, mzero, unless, when) +import Control.Monad.IO.Class (liftIO) +import Control.Monad.Reader (ask) +import Data.Acid +import Data.Acid.Advanced +import Data.ByteString.Char8 (ByteString, pack, unpack) +import Data.Char (toLower) +import Data.Maybe (fromJust) +import Data.Text (Text) +import qualified Data.Text as T +import Data.Time +import Happstack.Server hiding (Session) +import Happstack.Server.Compression +import System.Locale (defaultTimeLocale) + +import Blog +import BlogDB hiding (addComment, deleteComment, updateEntry) +import Locales +import RSS + + +instance FromReqURI BlogLang where + fromReqURI sub = + case map toLower sub of + "de" -> Just DE + "en" -> Just EN + _ -> Nothing + +tmpPolicy :: BodyPolicy +tmpPolicy = defaultBodyPolicy "./tmp/" 0 200000 1000 + +runBlog :: AcidState Blog -> Int -> String -> IO () +runBlog acid port respath = + simpleHTTP nullConf {port = port} $ tazBlog acid respath + +tazBlog :: AcidState Blog -> String -> ServerPart Response +tazBlog acid resDir = do + compr <- compressedResponseFilter + msum [ path $ \(lang :: BlogLang) -> blogHandler acid lang + , nullDir >> showIndex acid EN + , dir " " $ nullDir >> + seeOther ("https://plus.google.com/115916629925754851590" :: Text) (toResponse ()) + , path $ \(year :: Int) -> path $ \(month :: Int) -> path $ \(id_ :: String) -> formatOldLink year month id_ + , dir "res" $ serveDirectory DisableBrowsing [] "../res" + , dir "notice" $ ok $ toResponse showSiteNotice + {- :Admin handlers -} + , do dirs "admin/postentry" nullDir + guardSession acid + postEntry acid + , do dirs "admin/entrylist" $ dir (show DE) nullDir + guardSession acid + entryList acid DE + , do dirs "admin/entrylist" $ dir (show EN) nullDir + guardSession acid + entryList acid EN + , do guardSession acid + dirs "admin/edit" $ path $ \(eId :: Integer) -> editEntry acid eId + , do guardSession acid + dirs "admin/updateentry" $ nullDir >> updateEntry acid + , do guardSession acid + dirs "admin/cdelete" $ path $ \(eId :: Integer) -> path $ \(cId :: String) -> + deleteComment acid (EntryId eId) cId + , do dir "admin" nullDir + guardSession acid + ok $ toResponse $ adminIndex ("tazjin" :: Text) + , dir "admin" $ ok $ toResponse adminLogin + , dir "dologin" $ processLogin acid + , do dirs "static/blogv40.css" nullDir + setHeaderM "content-type" "text/css" + setHeaderM "cache-control" "max-age=630720000" + setHeaderM "expires" "Tue, 20 Jan 2037 04:20:42 GMT" + ok $ toResponse blogStyle + , do setHeaderM "cache-control" "max-age=630720000" + setHeaderM "expires" "Tue, 20 Jan 2037 04:20:42 GMT" + dir "static" $ serveDirectory DisableBrowsing [] resDir + , serveDirectory DisableBrowsing [] resDir + , notFound $ toResponse $ showError NotFound DE + ] + +blogHandler :: AcidState Blog -> BlogLang -> ServerPart Response +blogHandler acid lang = + msum [ path $ \(eId :: Integer) -> showEntry acid lang $ EntryId eId + , nullDir >> showIndex acid lang + , dir "rss" $ nullDir >> showRSS acid lang + , dir "rss.xml" $ nullDir >> showRSS acid lang + , notFound $ toResponse $ showError NotFound lang + ] + +formatOldLink :: Int -> Int -> String -> ServerPart Response +formatOldLink y m id_ = + flip seeOther (toResponse ()) $ + concat $ intersperse' "/" ["de", show y, show m, replace '.' '/' id_] + +showEntry :: AcidState Blog -> BlogLang -> EntryId -> ServerPart Response +showEntry acid lang eId = do + entry <- query' acid (GetEntry eId) + tryEntry entry lang + +tryEntry :: Maybe Entry -> BlogLang -> ServerPart Response +tryEntry Nothing lang = notFound $ toResponse $ showError NotFound lang +tryEntry (Just entry) _ = ok $ toResponse $ blogTemplate eLang eTitle $ renderEntry entry + where + eTitle = T.append ": " (title entry) + eLang = lang entry + +showIndex :: AcidState Blog -> BlogLang -> ServerPart Response +showIndex acid lang = do + entries <- query' acid (LatestEntries lang) + (page :: Maybe Int) <- optional $ lookRead "page" + ok $ toResponse $ blogTemplate lang "" $ + renderEntries False (eDrop page entries) (topText lang) (Just $ showLinks page lang) + where + eDrop :: Maybe Int -> [a] -> [a] + eDrop (Just i) = drop ((i-1) * 6) + eDrop Nothing = drop 0 + +showRSS :: AcidState Blog -> BlogLang -> ServerPart Response +showRSS acid lang = do + entries <- query' acid (LatestEntries lang) + feed <- liftIO $ renderFeed lang $ take 6 entries + setHeaderM "content-type" "text/xml" + ok $ toResponse feed + +{- ADMIN stuff -} + +postEntry :: AcidState Blog -> ServerPart Response +postEntry acid = do + decodeBody tmpPolicy + now <- liftIO getCurrentTime + let eId = timeToId now + lang <- look "lang" + nBtext <- lookText' "btext" + nMtext <- lookText' "mtext" + nEntry <- Entry <$> pure eId + <*> getLang lang + <*> readCookieValue "sUser" + <*> lookText' "title" + <*> pure nBtext + <*> pure nMtext + <*> pure now + <*> pure [] -- NYI + <*> pure [] + update' acid (InsertEntry nEntry) + seeOther ("/" ++ lang ++ "/" ++ show eId) (toResponse()) + where + timeToId :: UTCTime -> EntryId + timeToId t = EntryId . read $ formatTime defaultTimeLocale "%s" t + getLang :: String -> ServerPart BlogLang + getLang "de" = return DE + getLang "en" = return EN + +entryList :: AcidState Blog -> BlogLang -> ServerPart Response +entryList acid lang = do + entries <- query' acid (LatestEntries lang) + ok $ toResponse $ adminEntryList entries + +editEntry :: AcidState Blog -> Integer -> ServerPart Response +editEntry acid i = do + (Just entry) <- query' acid (GetEntry eId) + ok $ toResponse $ editPage entry + where + eId = EntryId i + +updateEntry :: AcidState Blog -> ServerPart Response -- TODO: Clean this up +updateEntry acid = do + decodeBody tmpPolicy + (eId :: Integer) <- lookRead "eid" + (Just entry) <- query' acid (GetEntry $ EntryId eId) + nTitle <- lookText' "title" + nBtext <- lookText' "btext" + nMtext <- lookText' "mtext" + let nEntry = entry { title = nTitle + , btext = nBtext + , mtext = nMtext} + update' acid (UpdateEntry nEntry) + seeOther (concat $ intersperse' "/" [show $ lang entry, show eId]) + (toResponse ()) + +deleteComment :: AcidState Blog -> EntryId -> String -> ServerPart Response +deleteComment acid eId cId = do + nEntry <- update' acid (DeleteComment eId cDate) + ok $ toResponse $ commentDeleted eId + where + (cDate :: UTCTime) = fromJust $ parseTime defaultTimeLocale "%s%Q" cId + +guardSession :: AcidState Blog -> ServerPartT IO () +guardSession acid = do + (sId :: Text) <- readCookieValue "session" + (uName :: Text) <- readCookieValue "sUser" + now <- liftIO getCurrentTime + mS <- query' acid (GetSession $ SessionID sId) + case mS of + Nothing -> mzero + (Just Session{..}) -> unless ((uName == username user) && sessionTimeDiff now sdate) + mzero + where + sessionTimeDiff :: UTCTime -> UTCTime -> Bool + sessionTimeDiff now sdate = diffUTCTime now sdate < 43200 + + +processLogin :: AcidState Blog -> ServerPart Response +processLogin acid = do + decodeBody tmpPolicy + account <- lookText' "account" + password <- look "password" + login <- query' acid (CheckUser (Username account) password) + if login + then createSession account + else ok $ toResponse adminLogin + where + createSession account = do + now <- liftIO getCurrentTime + let sId = hashString $ show now + addCookie (MaxAge 43200) (mkCookie "session" $ unpack sId) + addCookie (MaxAge 43200) (mkCookie "sUser" $ T.unpack account) + (Just user) <- query' acid (GetUser $ Username account) + let nSession = Session (T.pack $ unpack sId) user now + update' acid (AddSession nSession) + seeOther ("/admin?do=login" :: Text) (toResponse()) -- cgit 1.4.1 From 41bee335c83ba7eb2f56241d41ccdd373c9a1a1a Mon Sep 17 00:00:00 2001 From: Vincent Ambo <vincent@spotify.com> Date: Sun, 18 May 2014 22:50:28 +0200 Subject: Refactor: Remove leftover comment functionality --- res/blog.lucius | 8 -------- src/Blog.hs | 23 ----------------------- src/BlogDB.hs | 31 +------------------------------ src/Locales.hs | 29 ----------------------------- src/Server.hs | 13 +------------ 5 files changed, 2 insertions(+), 102 deletions(-) diff --git a/res/blog.lucius b/res/blog.lucius index ae4c896b78..95df4eb793 100644 --- a/res/blog.lucius +++ b/res/blog.lucius @@ -72,10 +72,6 @@ article a, .entry a { padding-bottom: 20px; } -.innerBoxComments { - padding-top: 20px; -} - .cCaptcha { padding: 5px; border: 1px solid #555; @@ -109,10 +105,6 @@ article a, .entry a { padding-right: 20px; } -.commentname { - text-align: right; -} - .notFoundFace { height: 100px; padding-top: 50px; diff --git a/src/Blog.hs b/src/Blog.hs index 00ba6c94d0..60d87f2907 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -285,32 +285,9 @@ editPage (Entry{..}) = adminTemplate "Index" $ [shamlet| <textarea name="mtext" cols="100" rows="15">#{mtext} <input type="hidden" name="eid" value=#{unEntryId entryId}> <input type="submit" style="margin-left:20px;" value="Absenden"> - <div class="editComments">#{editComments comments entryId} <p>^{adminFooter} |] -editComments :: [Comment] -> EntryId -> Html -editComments comments eId = [shamlet| -<table> - $forall c <- comments - <tr> - <td>#{cauthor c} - <td>#{cPostTime $ cdate c} - <tr> - <td><a href=#{cDeleteLink $ cdate c}>Löschen -|] - where - cPostTime = formatTime defaultTimeLocale "%c" - cDeleteLink cd = concat ["/admin/cdelete/", show eId, formatTime defaultTimeLocale "/%s%Q" cd] - -commentDeleted :: EntryId -> Html -commentDeleted eId = adminTemplate "Kommentar gelöscht" $ [shamlet| -<div>Der Kommentar wurde gelöscht. -<br> -<a href=#{append "/de/" $ show' eId}>Eintrag ansehen | # -<a href=#{append "/admin/edit/" $ show' eId}>Eintrag bearbeiten -|] - showError :: BlogError -> BlogLang -> Html showError NotFound l = blogTemplate l (T.append ": " $ notFoundTitle l) $ [shamlet| <div .row .text-center> diff --git a/src/BlogDB.hs b/src/BlogDB.hs index ca20aedb09..b75d634479 100644 --- a/src/BlogDB.hs +++ b/src/BlogDB.hs @@ -6,7 +6,6 @@ import Data.Acid import Data.Acid.Advanced import Data.Acid.Local import Data.ByteString (ByteString) -import Data.Char (toLower) import Data.Data (Data, Typeable) import Data.IxSet (Indexable (..), IxSet (..), Proxy (..), getOne, ixFun, ixSet, (@=)) @@ -41,14 +40,6 @@ instance Show BlogLang where $(deriveSafeCopy 0 'base ''BlogLang) -data Comment = Comment { - cdate :: UTCTime, - cauthor :: Text, - ctext :: Text -} deriving (Eq, Ord, Show, Data, Typeable) - -$(deriveSafeCopy 0 'base ''Comment) - data Entry = Entry { entryId :: EntryId, lang :: BlogLang, @@ -57,8 +48,7 @@ data Entry = Entry { btext :: Text, mtext :: Text, edate :: UTCTime, - tags :: [Text], - comments :: [Comment] + tags :: [Text] } deriving (Eq, Ord, Show, Data, Typeable) $(deriveSafeCopy 0 'base ''Entry) @@ -93,7 +83,6 @@ instance Indexable Entry where , ixFun $ \e -> [ MText $ mtext e] , ixFun $ \e -> [ EDate $ edate e] , ixFun $ \e -> map Tag (tags e) - , ixFun comments ] data User = User { @@ -144,22 +133,6 @@ insertEntry e = put $ b { blogEntries = IxSet.insert e blogEntries } return e -addComment :: EntryId -> Comment -> Update Blog Entry -addComment eId c = - do b@Blog{..} <- get - let (Just e) = getOne $ blogEntries @= eId - let newEntry = e { comments = insert c $ comments e } - put $ b { blogEntries = IxSet.updateIx eId newEntry blogEntries } - return newEntry - -deleteComment :: EntryId -> UTCTime -> Update Blog Entry -deleteComment eId cDate = - do b@Blog{..} <- get - let (Just e) = getOne $ blogEntries @= eId - let newEntry = e {comments = filter (\c -> cdate c /= cDate) (comments e)} - put $ b { blogEntries = IxSet.updateIx eId newEntry blogEntries } - return newEntry - updateEntry :: Entry -> Update Blog Entry updateEntry e = do b@Blog{..} <- get @@ -219,8 +192,6 @@ hashString = B64.encode . SHA.hash . B.pack $(makeAcidic ''Blog [ 'insertEntry - , 'addComment - , 'deleteComment , 'updateEntry , 'getEntry , 'latestEntries diff --git a/src/Locales.hs b/src/Locales.hs index 206545d447..796ab9d0df 100644 --- a/src/Locales.hs +++ b/src/Locales.hs @@ -99,35 +99,6 @@ noticeText :: BlogLang -> Text noticeText EN = "site notice" noticeText DE = "Impressum" --- comments -noComments :: BlogLang -> Text -noComments DE = " Keine Kommentare" -noComments EN = " No comments yet" - -cHead :: BlogLang -> Text -cHead DE = "Kommentare" -cHead EN = "Comments" - -cwHead :: BlogLang -> Text -cwHead DE = "Kommentieren:" -cwHead EN = "Comment:" - -cSingle :: BlogLang -> Text -cSingle DE = "Kommentar:" --input label -cSingle EN = "Comment:" - -cTimeFormat :: BlogLang -> String --formatTime expects a String -cTimeFormat DE = "[Am %Y-%m-%d um %H:%M Uhr]" -cTimeFormat EN = "[On %Y-%m-%d at %H:%M]" - -cSend :: BlogLang -> Text -cSend DE = "Absenden" -cSend EN = "Submit" - -cTextPlaceholder :: BlogLang -> Text -cTextPlaceholder DE = "Kommentartext hier eingeben :]" -cTextPlaceholder EN = "Enter your comment here :]" - -- RSS Strings rssTitle :: BlogLang -> String rssTitle DE = "Tazjins Blog" diff --git a/src/Server.hs b/src/Server.hs index bc1f51298b..f11f75c2de 100644 --- a/src/Server.hs +++ b/src/Server.hs @@ -32,7 +32,7 @@ instance FromReqURI BlogLang where _ -> Nothing tmpPolicy :: BodyPolicy -tmpPolicy = defaultBodyPolicy "./tmp/" 0 200000 1000 +tmpPolicy = defaultBodyPolicy "/tmp" 0 200000 1000 runBlog :: AcidState Blog -> Int -> String -> IO () runBlog acid port respath = @@ -62,9 +62,6 @@ tazBlog acid resDir = do dirs "admin/edit" $ path $ \(eId :: Integer) -> editEntry acid eId , do guardSession acid dirs "admin/updateentry" $ nullDir >> updateEntry acid - , do guardSession acid - dirs "admin/cdelete" $ path $ \(eId :: Integer) -> path $ \(cId :: String) -> - deleteComment acid (EntryId eId) cId , do dir "admin" nullDir guardSession acid ok $ toResponse $ adminIndex ("tazjin" :: Text) @@ -144,7 +141,6 @@ postEntry acid = do <*> pure nMtext <*> pure now <*> pure [] -- NYI - <*> pure [] update' acid (InsertEntry nEntry) seeOther ("/" ++ lang ++ "/" ++ show eId) (toResponse()) where @@ -181,13 +177,6 @@ updateEntry acid = do seeOther (concat $ intersperse' "/" [show $ lang entry, show eId]) (toResponse ()) -deleteComment :: AcidState Blog -> EntryId -> String -> ServerPart Response -deleteComment acid eId cId = do - nEntry <- update' acid (DeleteComment eId cDate) - ok $ toResponse $ commentDeleted eId - where - (cDate :: UTCTime) = fromJust $ parseTime defaultTimeLocale "%s%Q" cId - guardSession :: AcidState Blog -> ServerPartT IO () guardSession acid = do (sId :: Text) <- readCookieValue "session" -- cgit 1.4.1 From 2c0eecc9aa0496318a4ccf6205c32a41aa2b9970 Mon Sep 17 00:00:00 2001 From: Vincent Ambo <vincent@spotify.com> Date: Fri, 22 Aug 2014 15:43:51 +0200 Subject: Add the weird safecopy migration tool --- tools/acid-fixer/Main.hs | 228 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 228 insertions(+) create mode 100644 tools/acid-fixer/Main.hs diff --git a/tools/acid-fixer/Main.hs b/tools/acid-fixer/Main.hs new file mode 100644 index 0000000000..8ef3190cc5 --- /dev/null +++ b/tools/acid-fixer/Main.hs @@ -0,0 +1,228 @@ +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeFamilies #-} + +module Main where + +import Control.Monad.Reader (ask) +import Control.Monad.State (get, put) +import Data.Acid +import Data.Acid.Advanced +import Data.Acid.Local +import Data.ByteString (ByteString) +import Data.Char (toLower) +import Data.Data (Data, Typeable) +import Data.IxSet (Indexable (..), IxSet (..), Proxy (..), + getOne, ixFun, ixSet, (@=)) +import Data.List (insert) +import Data.SafeCopy +import Data.Text (Text, pack) +import Data.Text.Lazy (toStrict) +import Data.Time +import Happstack.Server (FromReqURI (..)) +import System.Environment (getEnv) + +import qualified Crypto.Hash.SHA512 as SHA (hash) +import qualified Data.ByteString.Base64 as B64 (encode) +import qualified Data.ByteString.Char8 as B +import qualified Data.IxSet as IxSet +import qualified Data.Text as Text + +newtype EntryId = EntryId { unEntryId :: Integer } + deriving (Eq, Ord, Data, Enum, Typeable) + +$(deriveSafeCopy 2 'base ''EntryId) + +instance Show EntryId where + show = show . unEntryId + +data BlogLang = EN | DE + deriving (Eq, Ord, Data, Typeable) + +instance Show BlogLang where + show DE = "de" + show EN = "en" + +instance FromReqURI BlogLang where + fromReqURI sub = + case map toLower sub of + "de" -> Just DE + "en" -> Just EN + _ -> Nothing + +$(deriveSafeCopy 0 'base ''BlogLang) + +data Comment = Comment { + cdate :: UTCTime, + cauthor :: Text, + ctext :: Text +} deriving (Eq, Ord, Show, Data, Typeable) + +$(deriveSafeCopy 0 'base ''Comment) + +data Entry_v0 = Entry_v0 { + entryId_v0 :: EntryId, + lang_v0 :: BlogLang, + author_v0 :: Text, + title_v0 :: Text, + btext_v0 :: Text, + mtext_v0 :: Text, + edate_v0 :: UTCTime, + tags :: [Text], + comments :: [Comment] +} deriving (Eq, Ord, Show, Data, Typeable) +$(deriveSafeCopy 0 'base ''Entry_v0) + +data Entry = Entry { + entryId :: EntryId, + lang :: BlogLang, + author :: Text, + title :: Text, + btext :: Text, + mtext :: Text, + edate :: UTCTime +} deriving (Eq, Ord, Show, Data, Typeable) + +$(deriveSafeCopy 2 'extension ''Entry) + +instance Migrate Entry where + type MigrateFrom Entry = Entry_v0 + migrate (Entry_v0 ei l a t b m ed _ _) = + Entry ei l a t b m ed + +-- ixSet requires different datatypes for field indexes, so let's define some +newtype Author_v0 = Author_v0 Text deriving (Eq, Ord, Data, Typeable, SafeCopy) +newtype Author = Author Text deriving (Eq, Ord, Data, Typeable) +$(deriveSafeCopy 2 'extension ''Author) +instance Migrate Author where + type MigrateFrom Author = Author_v0 + migrate (Author_v0 x) = Author x + +newtype Title_v0 = Title_v0 Text deriving (Eq, Ord, Data, Typeable, SafeCopy) +newtype Title = Title Text deriving (Eq, Ord, Data, Typeable) +$(deriveSafeCopy 2 'extension ''Title) +instance Migrate Title where + type MigrateFrom Title = Title_v0 + migrate (Title_v0 x) = Title x + +newtype BText_v0 = BText_v0 Text deriving (Eq, Ord, Data, Typeable, SafeCopy) +newtype BText = BText Text deriving (Eq, Ord, Data, Typeable) -- standard text +$(deriveSafeCopy 2 'extension ''BText) +instance Migrate BText where + type MigrateFrom BText = BText_v0 + migrate (BText_v0 x) = BText x + +newtype MText_v0 = MText_v0 Text deriving (Eq, Ord, Data, Typeable, SafeCopy) +newtype MText = MText Text deriving (Eq, Ord, Data, Typeable) -- "read more" text +$(deriveSafeCopy 2 'extension ''MText) +instance Migrate MText where + type MigrateFrom MText = MText_v0 + migrate (MText_v0 x) = MText x + +newtype Tag_v0 = Tag_v0 Text deriving (Eq, Ord, Data, Typeable, SafeCopy) +newtype Tag = Tag Text deriving (Eq, Ord, Data, Typeable) +$(deriveSafeCopy 2 'extension ''Tag) +instance Migrate Tag where + type MigrateFrom Tag = Tag_v0 + migrate (Tag_v0 x) = Tag x + +newtype EDate_v0 = EDate_v0 UTCTime deriving (Eq, Ord, Data, Typeable, SafeCopy) +newtype EDate = EDate UTCTime deriving (Eq, Ord, Data, Typeable) +$(deriveSafeCopy 2 'extension ''EDate) +instance Migrate EDate where + type MigrateFrom EDate = EDate_v0 + migrate (EDate_v0 x) = EDate x + +newtype SDate_v0 = SDate_v0 UTCTime deriving (Eq, Ord, Data, Typeable, SafeCopy) +newtype SDate = SDate UTCTime deriving (Eq, Ord, Data, Typeable) +$(deriveSafeCopy 2 'extension ''SDate) +instance Migrate SDate where + type MigrateFrom SDate = SDate_v0 + migrate (SDate_v0 x) = SDate x + +newtype Username_v0 = Username_v0 Text deriving (Eq, Ord, Data, Typeable, SafeCopy) +newtype Username = Username Text deriving (Eq, Ord, Data, Typeable) +$(deriveSafeCopy 2 'extension ''Username) +instance Migrate Username where + type MigrateFrom Username = Username_v0 + migrate (Username_v0 x) = Username x + +newtype SessionID_v0 = SessionID_v0 Text deriving (Eq, Ord, Data, Typeable, SafeCopy) +newtype SessionID = SessionID Text deriving (Eq, Ord, Data, Typeable) +$(deriveSafeCopy 2 'extension ''SessionID) +instance Migrate SessionID where + type MigrateFrom SessionID = SessionID_v0 + migrate (SessionID_v0 x) = SessionID x + +instance Indexable Entry where + empty = ixSet [ ixFun $ \e -> [ entryId e] + , ixFun $ (:[]) . lang + , ixFun $ \e -> [ Author $ author e ] + , ixFun $ \e -> [ Title $ title e] + , ixFun $ \e -> [ BText $ btext e] + , ixFun $ \e -> [ MText $ mtext e] + , ixFun $ \e -> [ EDate $ edate e] + ] + +data User = User { + username :: Text, + password :: ByteString +} deriving (Eq, Ord, Data, Typeable) + +$(deriveSafeCopy 0 'base ''User) + +data Session = Session { + sessionID :: Text, + user :: User, + sdate :: UTCTime +} deriving (Eq, Ord, Data, Typeable) + +$(deriveSafeCopy 0 'base ''Session) + +instance Indexable User where + empty = ixSet [ ixFun $ \u -> [Username $ username u] + , ixFun $ (:[]) . password + ] + +instance Indexable Session where + empty = ixSet [ ixFun $ \s -> [SessionID $ sessionID s] + , ixFun $ (:[]) . user + , ixFun $ \s -> [SDate $ sdate s] + ] + +data Blog = Blog { + blogSessions :: IxSet Session, + blogUsers :: IxSet User, + blogEntries :: IxSet Entry +} deriving (Data, Typeable) + +latestEntries :: BlogLang -> Query Blog [Entry] +latestEntries lang = + do b@Blog{..} <- ask + return $ IxSet.toDescList (Proxy :: Proxy EDate) $ blogEntries @= lang + +$(deriveSafeCopy 0 'base ''Blog) + +$(makeAcidic ''Blog ['latestEntries]) + +initialBlogState :: Blog +initialBlogState = + Blog { blogSessions = empty + , blogUsers = empty + , blogEntries = empty } + +main :: IO () +main = do + putStrLn "Opening state" + acid <- openLocalStateFrom "/var/tazblog/BlogState" initialBlogState + entries <- query acid (LatestEntries EN) + print $ length entries + print $ head entries + putStrLn "Creating checkpoint" + createCheckpoint acid + putStrLn "Closing state" + closeAcidState acid -- cgit 1.4.1 From 56609f1f59d84d78dfd1a897ee3e9d04768d3bab Mon Sep 17 00:00:00 2001 From: Vincent Ambo <vincent@spotify.com> Date: Fri, 22 Aug 2014 15:56:03 +0200 Subject: v4.2: Bump SafeCopy versions of types --- TazBlog.cabal | 2 +- src/BlogDB.hs | 26 ++++++++++++-------------- src/Locales.hs | 2 +- src/Server.hs | 3 +-- 4 files changed, 15 insertions(+), 18 deletions(-) diff --git a/TazBlog.cabal b/TazBlog.cabal index 2ab0998230..6d0093c0b5 100644 --- a/TazBlog.cabal +++ b/TazBlog.cabal @@ -1,5 +1,5 @@ Name: TazBlog -Version: 4.1 +Version: 4.2 Synopsis: Tazjin's Blog License: MIT License-file: LICENSE diff --git a/src/BlogDB.hs b/src/BlogDB.hs index b75d634479..dd8aaaa66c 100644 --- a/src/BlogDB.hs +++ b/src/BlogDB.hs @@ -26,7 +26,7 @@ import qualified Data.Text as Text newtype EntryId = EntryId { unEntryId :: Integer } deriving (Eq, Ord, Data, Enum, Typeable) -$(deriveSafeCopy 0 'base ''EntryId) +$(deriveSafeCopy 2 'base ''EntryId) instance Show EntryId where show = show . unEntryId @@ -47,11 +47,10 @@ data Entry = Entry { title :: Text, btext :: Text, mtext :: Text, - edate :: UTCTime, - tags :: [Text] + edate :: UTCTime } deriving (Eq, Ord, Show, Data, Typeable) -$(deriveSafeCopy 0 'base ''Entry) +$(deriveSafeCopy 2 'base ''Entry) -- ixSet requires different datatypes for field indexes, so let's define some newtype Author = Author Text deriving (Eq, Ord, Data, Typeable) @@ -64,15 +63,15 @@ newtype SDate = SDate UTCTime deriving (Eq, Ord, Data, Typeable) newtype Username = Username Text deriving (Eq, Ord, Data, Typeable) newtype SessionID = SessionID Text deriving (Eq, Ord, Data, Typeable) -$(deriveSafeCopy 0 'base ''Author) -$(deriveSafeCopy 0 'base ''Title) -$(deriveSafeCopy 0 'base ''BText) -$(deriveSafeCopy 0 'base ''MText) -$(deriveSafeCopy 0 'base ''Tag) -$(deriveSafeCopy 0 'base ''EDate) -$(deriveSafeCopy 0 'base ''SDate) -$(deriveSafeCopy 0 'base ''Username) -$(deriveSafeCopy 0 'base ''SessionID) +$(deriveSafeCopy 2 'base ''Author) +$(deriveSafeCopy 2 'base ''Title) +$(deriveSafeCopy 2 'base ''BText) +$(deriveSafeCopy 2 'base ''MText) +$(deriveSafeCopy 2 'base ''Tag) +$(deriveSafeCopy 2 'base ''EDate) +$(deriveSafeCopy 2 'base ''SDate) +$(deriveSafeCopy 2 'base ''Username) +$(deriveSafeCopy 2 'base ''SessionID) instance Indexable Entry where empty = ixSet [ ixFun $ \e -> [ entryId e] @@ -82,7 +81,6 @@ instance Indexable Entry where , ixFun $ \e -> [ BText $ btext e] , ixFun $ \e -> [ MText $ mtext e] , ixFun $ \e -> [ EDate $ edate e] - , ixFun $ \e -> map Tag (tags e) ] data User = User { diff --git a/src/Locales.hs b/src/Locales.hs index 796ab9d0df..576ef11ce7 100644 --- a/src/Locales.hs +++ b/src/Locales.hs @@ -14,7 +14,7 @@ import BlogDB (BlogLang (..)) data BlogError = NotFound | DBError -version = "4.1" +version = "4.2" allLang = [EN, DE] diff --git a/src/Server.hs b/src/Server.hs index f11f75c2de..5921aead1a 100644 --- a/src/Server.hs +++ b/src/Server.hs @@ -19,7 +19,7 @@ import Happstack.Server.Compression import System.Locale (defaultTimeLocale) import Blog -import BlogDB hiding (addComment, deleteComment, updateEntry) +import BlogDB hiding (updateEntry) import Locales import RSS @@ -140,7 +140,6 @@ postEntry acid = do <*> pure nBtext <*> pure nMtext <*> pure now - <*> pure [] -- NYI update' acid (InsertEntry nEntry) seeOther ("/" ++ lang ++ "/" ++ show eId) (toResponse()) where -- cgit 1.4.1 From 8d2fefec8fea73c680a5c7eb43bf73c0c33e9df0 Mon Sep 17 00:00:00 2001 From: Vincent Ambo <vincent@spotify.com> Date: Fri, 22 Aug 2014 18:35:28 +0200 Subject: Add Dockerfile & Makefile --- Dockerfile.raw | 10 ++++++++++ Makefile | 14 ++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 Dockerfile.raw create mode 100644 Makefile diff --git a/Dockerfile.raw b/Dockerfile.raw new file mode 100644 index 0000000000..f67e3091dd --- /dev/null +++ b/Dockerfile.raw @@ -0,0 +1,10 @@ +FROM base/archlinux +MAINTAINER Vincent Ambo <dev@tazj.in> + +COPY $ARCH_PKG /tmp/tazblog-current.pkg.tar.xz + +RUN pacman -U /tmp/tazblog-current.pkg.tar.xz --noconfirm + +VOLUME /var/tazblog +EXPOSE 8000 +CMD /usr/bin/tazblog diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000..14fd61ae30 --- /dev/null +++ b/Makefile @@ -0,0 +1,14 @@ +VERSION=$(shell bash -c "grep -P -o -e '\d\.\d$$' TazBlog.cabal | head -n1") +ARCH_PKG=arch/tazblog-$(VERSION)-1-x86_64.pkg.tar.xz +export ARCH_PKG + +all: archpkg docker + +archpkg: $(ARCH_PKG) + +$(ARCH_PKG): + cd arch && makepkg + +docker: archpkg + cat Dockerfile.raw | envsubst > Dockerfile; \ + docker build -t tazjin/tazblog . -- cgit 1.4.1 From 3a86d653ce759d6e9daff93d0da8fa588d3b404e Mon Sep 17 00:00:00 2001 From: Vincent Ambo <vincent@spotify.com> Date: Fri, 22 Aug 2014 18:56:57 +0200 Subject: .dockerignore, make clean and new PKGBUILD - Added .dockerignore file - added make target for cleaning up - modified PKGBUILD to build in symlink to checkout (yeah this isn't really how you build Arch packages but that's besides the point! :)) --- .dockerignore | 6 ++++++ Makefile | 3 +++ arch/PKGBUILD | 12 ++++++------ 3 files changed, 15 insertions(+), 6 deletions(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..b6d3d3e812 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +dist +res +src +tools +arch/pkg +arch/src diff --git a/Makefile b/Makefile index 14fd61ae30..00d77dd36c 100644 --- a/Makefile +++ b/Makefile @@ -12,3 +12,6 @@ $(ARCH_PKG): docker: archpkg cat Dockerfile.raw | envsubst > Dockerfile; \ docker build -t tazjin/tazblog . + +clean: + rm -rf dist arch/*.pkg.tar.xz arch/pkg arch/src arch/*. Dockerfile diff --git a/arch/PKGBUILD b/arch/PKGBUILD index d6c7ea46cf..02ff790fd7 100644 --- a/arch/PKGBUILD +++ b/arch/PKGBUILD @@ -7,23 +7,23 @@ arch=('i686' 'x86_64') url="http://tazj.in" license=('MIT') makedepends=('ghc' 'cabal-install') -source=(https://bitbucket.org/tazjin/tazblog-haskell/get/$pkgver.tar.gz) -md5sums=('SKIP') - +source=('tazblog@.service') +sha1sums=('6aeb901a9d0e25763c9c99168a440dd5ac99ffc1') build() { cd "$srcdir" - cd tazjin-* + test -e blog-src || ln -fs ../../ blog-src + cd blog-src - cabal update cabal sandbox init + cabal update cabal install -j --only-dependencies cabal build } package() { cd "$srcdir" - cd tazjin-* + cd blog-src # Install blog itself install -d "${pkgdir}/usr/bin" -- cgit 1.4.1 From 853be2a99c4f4cd8a75f807d37fb97ec1497215d Mon Sep 17 00:00:00 2001 From: Vincent Ambo <tazjin@gmail.com> Date: Thu, 19 Nov 2015 16:21:15 +0100 Subject: [build] Use Stack for building --- .gitignore | 1 + TazBlog.cabal | 66 +++++++++++++++++++++++++++++------------------------------ src/Blog.hs | 3 +-- src/Server.hs | 1 - stack.yaml | 20 ++++++++++++++++++ 5 files changed, 55 insertions(+), 36 deletions(-) create mode 100644 stack.yaml diff --git a/.gitignore b/.gitignore index 8bfbf2a389..a95070c31f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ BlogState/ dist/ .cabal-sandbox/ *.tar.gz +.stack-work/ diff --git a/TazBlog.cabal b/TazBlog.cabal index 6d0093c0b5..f3daba16f7 100644 --- a/TazBlog.cabal +++ b/TazBlog.cabal @@ -14,37 +14,37 @@ Executable tazblog hs-source-dirs: src main-is: Main.hs ghc-options: -O2 - Build-depends: - base, - bytestring, - happstack-server, - text, - blaze-html, - blaze-markup, - crypto-api, - cryptohash, - old-locale, - time, - base64-bytestring, - acid-state, - ixset, - safecopy, - mtl, - transformers, - network, - options, - rss, - hamlet, - shakespeare, - markdown + Build-depends: base, + bytestring, + happstack-server, + text, + blaze-html, + blaze-markup, + crypto-api, + cryptohash, + old-locale, + time, + base64-bytestring, + acid-state, + ixset, + safecopy, + mtl, + transformers, + network, + network-uri, + options, + rss, + hamlet, + shakespeare, + markdown extensions: - DeriveDataTypeable - FlexibleContexts - GeneralizedNewtypeDeriving - MultiParamTypeClasses - OverloadedStrings - RecordWildCards - ScopedTypeVariables - TemplateHaskell - TypeFamilies - QuasiQuotes + DeriveDataTypeable + FlexibleContexts + GeneralizedNewtypeDeriving + MultiParamTypeClasses + OverloadedStrings + RecordWildCards + ScopedTypeVariables + TemplateHaskell + TypeFamilies + QuasiQuotes diff --git a/src/Blog.hs b/src/Blog.hs index 60d87f2907..5659ad5955 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -10,7 +10,6 @@ import Data.Text (Text, append, empty, pack) import Data.Text.Lazy (fromStrict) import Data.Time import Locales -import System.Locale (defaultTimeLocale) import Text.Blaze.Html (preEscapedToHtml) import Text.Hamlet import Text.Lucius @@ -30,7 +29,7 @@ show' = pack . show -- |After this time all entries are Markdown markdownCutoff :: UTCTime -markdownCutoff = fromJust $ parseTime defaultTimeLocale "%s" "1367149834" +markdownCutoff = fromJust $ parseTimeM False defaultTimeLocale "%s" "1367149834" -- blog CSS (admin is still static) diff --git a/src/Server.hs b/src/Server.hs index 5921aead1a..85dc06e536 100644 --- a/src/Server.hs +++ b/src/Server.hs @@ -16,7 +16,6 @@ import qualified Data.Text as T import Data.Time import Happstack.Server hiding (Session) import Happstack.Server.Compression -import System.Locale (defaultTimeLocale) import Blog import BlogDB hiding (updateEntry) diff --git a/stack.yaml b/stack.yaml new file mode 100644 index 0000000000..16b13a65f8 --- /dev/null +++ b/stack.yaml @@ -0,0 +1,20 @@ +# For more information, see: https://github.com/commercialhaskell/stack/blob/release/doc/yaml_configuration.md + +# Specifies the GHC version and set of packages available (e.g., lts-3.5, nightly-2015-09-21, ghc-7.10.2) +resolver: lts-3.14 + +# Local packages, usually specified by relative directory name +packages: +- '.' + +# Packages to be pulled from upstream that are not in the resolver (e.g., acme-missiles-0.3) +extra-deps: + - ixset-1.0.6 + - syb-with-class-0.6.1.6 # needed by ixset + - rss-3000.2.0.5 + +# Override default flag values for local packages and extra-deps +flags: {} + +# Extra package databases containing global packages +extra-package-dbs: [] -- cgit 1.4.1 From fa514a9d3ef2f690294e8026679b83fad3588ab4 Mon Sep 17 00:00:00 2001 From: Vincent Ambo <tazjin@gmail.com> Date: Thu, 19 Nov 2015 16:57:03 +0100 Subject: [build] Prepare new Docker setup --- .dockerignore | 7 +------ Dockerfile | 14 ++++++++++++++ Dockerfile.raw | 10 ---------- arch/PKGBUILD | 39 --------------------------------------- arch/tazblog@.service | 11 ----------- 5 files changed, 15 insertions(+), 66 deletions(-) create mode 100644 Dockerfile delete mode 100644 Dockerfile.raw delete mode 100644 arch/PKGBUILD delete mode 100644 arch/tazblog@.service diff --git a/.dockerignore b/.dockerignore index b6d3d3e812..8ee1bf9489 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,6 +1 @@ -dist -res -src -tools -arch/pkg -arch/src +.stack-work diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..098666934e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM fpco/stack-build +MAINTAINER Vincent Ambo <dev@tazj.in> + +# Base setup +VOLUME /var/tazblog +EXPOSE 8000 + +# Build blog +ADD . /opt/tazblog/src +WORKDIR /opt/tazblog/src +RUN stack build && cp .stack-work/ + +# Done! +CMD /usr/bin/tazblog diff --git a/Dockerfile.raw b/Dockerfile.raw deleted file mode 100644 index f67e3091dd..0000000000 --- a/Dockerfile.raw +++ /dev/null @@ -1,10 +0,0 @@ -FROM base/archlinux -MAINTAINER Vincent Ambo <dev@tazj.in> - -COPY $ARCH_PKG /tmp/tazblog-current.pkg.tar.xz - -RUN pacman -U /tmp/tazblog-current.pkg.tar.xz --noconfirm - -VOLUME /var/tazblog -EXPOSE 8000 -CMD /usr/bin/tazblog diff --git a/arch/PKGBUILD b/arch/PKGBUILD deleted file mode 100644 index 02ff790fd7..0000000000 --- a/arch/PKGBUILD +++ /dev/null @@ -1,39 +0,0 @@ -# Maintainer: Vincent Ambo <dev@tazj.in> -pkgname=tazblog -pkgver=4.2 -pkgrel=1 -pkgdesc="Tazjin's blog written in Haskell" -arch=('i686' 'x86_64') -url="http://tazj.in" -license=('MIT') -makedepends=('ghc' 'cabal-install') -source=('tazblog@.service') -sha1sums=('6aeb901a9d0e25763c9c99168a440dd5ac99ffc1') - -build() { - cd "$srcdir" - test -e blog-src || ln -fs ../../ blog-src - cd blog-src - - cabal sandbox init - cabal update - cabal install -j --only-dependencies - cabal build -} - -package() { - cd "$srcdir" - cd blog-src - - # Install blog itself - install -d "${pkgdir}/usr/bin" - install -m755 dist/build/tazblog/tazblog "${pkgdir}/usr/bin/tazblog" - - # Install resources - install -d "${pkgdir}/usr/share/tazblog" - cp -r res/ "${pkgdir}/usr/share/tazblog" - - # Install service file - install -d "${pkgdir}/usr/lib/systemd/system" - cp "arch/tazblog@.service" "${pkgdir}/usr/lib/systemd/system/" -} diff --git a/arch/tazblog@.service b/arch/tazblog@.service deleted file mode 100644 index 2b6f115eb5..0000000000 --- a/arch/tazblog@.service +++ /dev/null @@ -1,11 +0,0 @@ -[Unit] -Description=tazblog web process - -[Service] -Type=simple -ExecStart=/usr/bin/tazblog -Restart=always -User=%i - -[Install] -WantedBy=multi-user.target -- cgit 1.4.1 From f703c0391640c22e419094b6b27fbab993ef86a3 Mon Sep 17 00:00:00 2001 From: Vincent Ambo <tazjin@gmail.com> Date: Thu, 19 Nov 2015 17:53:05 +0100 Subject: [build] Finish new Dockerfile --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 098666934e..3d40218ab9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,7 +8,7 @@ EXPOSE 8000 # Build blog ADD . /opt/tazblog/src WORKDIR /opt/tazblog/src -RUN stack build && cp .stack-work/ +RUN stack install && cp /root/.local/bin/tazblog /usr/bin/tazblog # Done! CMD /usr/bin/tazblog -- cgit 1.4.1 From 71b2ccd9275bacc789bce7f94d5792a24598460b Mon Sep 17 00:00:00 2001 From: Vincent Ambo <tazjin@gmail.com> Date: Thu, 19 Nov 2015 18:16:34 +0100 Subject: Begin cleaning up old things --- src/Blog.hs | 2 -- src/Locales.hs | 7 +------ src/Server.hs | 19 +++---------------- 3 files changed, 4 insertions(+), 24 deletions(-) diff --git a/src/Blog.hs b/src/Blog.hs index 5659ad5955..34b2aaf526 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -57,8 +57,6 @@ $doctype 5 <br> <div .span6> <span .contacts #cosx>^{contactInfo} - <div .span6> - <span .righttext>^{preEscapedToHtml $ rightText lang} <div .container> ^{body} <footer .footer> diff --git a/src/Locales.hs b/src/Locales.hs index 576ef11ce7..b387171abe 100644 --- a/src/Locales.hs +++ b/src/Locales.hs @@ -118,12 +118,7 @@ notFoundTitle EN = "Not found" notFoundText :: BlogLang -> Text notFoundText DE = "Das gewünschte Objekt wurde leider nicht gefunden." -notFoundText EN = "The requested object could unfortunately not be found." - --- right side text (this is inserted AS IS. Escape HTML!) -rightText :: BlogLang -> Text -rightText DE = "English version <a href=\"/en\" class=\"link\">available here</a>." -rightText EN = "Deutsche Version <a href=\"/de\" class=\"link\">hier verfügbar</a>." +notFoundText EN = "The requested object could not be found." -- static information repoURL :: Text = "http://hg.tazj.in/tazblog-haskell" diff --git a/src/Server.hs b/src/Server.hs index 85dc06e536..b71b070f1b 100644 --- a/src/Server.hs +++ b/src/Server.hs @@ -15,21 +15,12 @@ import Data.Text (Text) import qualified Data.Text as T import Data.Time import Happstack.Server hiding (Session) -import Happstack.Server.Compression import Blog import BlogDB hiding (updateEntry) import Locales import RSS - -instance FromReqURI BlogLang where - fromReqURI sub = - case map toLower sub of - "de" -> Just DE - "en" -> Just EN - _ -> Nothing - tmpPolicy :: BodyPolicy tmpPolicy = defaultBodyPolicy "/tmp" 0 200000 1000 @@ -39,13 +30,9 @@ runBlog acid port respath = tazBlog :: AcidState Blog -> String -> ServerPart Response tazBlog acid resDir = do - compr <- compressedResponseFilter - msum [ path $ \(lang :: BlogLang) -> blogHandler acid lang - , nullDir >> showIndex acid EN - , dir " " $ nullDir >> - seeOther ("https://plus.google.com/115916629925754851590" :: Text) (toResponse ()) + msum [ nullDir >> blogHandler acid EN + , dir "de" $ blogHandler acid DE , path $ \(year :: Int) -> path $ \(month :: Int) -> path $ \(id_ :: String) -> formatOldLink year month id_ - , dir "res" $ serveDirectory DisableBrowsing [] "../res" , dir "notice" $ ok $ toResponse showSiteNotice {- :Admin handlers -} , do dirs "admin/postentry" nullDir @@ -75,7 +62,7 @@ tazBlog acid resDir = do setHeaderM "expires" "Tue, 20 Jan 2037 04:20:42 GMT" dir "static" $ serveDirectory DisableBrowsing [] resDir , serveDirectory DisableBrowsing [] resDir - , notFound $ toResponse $ showError NotFound DE + , notFound $ toResponse $ showError NotFound EN ] blogHandler :: AcidState Blog -> BlogLang -> ServerPart Response -- cgit 1.4.1 From 0f6ff6310ed93b6221120f23ec085c1b7951de5b Mon Sep 17 00:00:00 2001 From: Vincent Ambo <tazjin@gmail.com> Date: Thu, 19 Nov 2015 18:31:06 +0100 Subject: [5.0-beta] Split into library and executable --- TazBlog.cabal | 50 -------------------------------------------------- blog/Main.hs | 38 ++++++++++++++++++++++++++++++++++++++ src/Locales.hs | 2 +- src/Main.hs | 38 -------------------------------------- tazblog.cabal | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 97 insertions(+), 89 deletions(-) delete mode 100644 TazBlog.cabal create mode 100644 blog/Main.hs delete mode 100644 src/Main.hs create mode 100644 tazblog.cabal diff --git a/TazBlog.cabal b/TazBlog.cabal deleted file mode 100644 index f3daba16f7..0000000000 --- a/TazBlog.cabal +++ /dev/null @@ -1,50 +0,0 @@ -Name: TazBlog -Version: 4.2 -Synopsis: Tazjin's Blog -License: MIT -License-file: LICENSE -Author: Vincent Ambo -Maintainer: tazjin@gmail.com -Category: Web blog -Build-type: Simple -cabal-version: >= 1.2 - - -Executable tazblog - hs-source-dirs: src - main-is: Main.hs - ghc-options: -O2 - Build-depends: base, - bytestring, - happstack-server, - text, - blaze-html, - blaze-markup, - crypto-api, - cryptohash, - old-locale, - time, - base64-bytestring, - acid-state, - ixset, - safecopy, - mtl, - transformers, - network, - network-uri, - options, - rss, - hamlet, - shakespeare, - markdown - extensions: - DeriveDataTypeable - FlexibleContexts - GeneralizedNewtypeDeriving - MultiParamTypeClasses - OverloadedStrings - RecordWildCards - ScopedTypeVariables - TemplateHaskell - TypeFamilies - QuasiQuotes diff --git a/blog/Main.hs b/blog/Main.hs new file mode 100644 index 0000000000..a50ca67ed1 --- /dev/null +++ b/blog/Main.hs @@ -0,0 +1,38 @@ +module Main where + +import Control.Applicative (pure, (<$>), (<*>)) +import Control.Exception (bracket) +import Data.Acid +import Data.Acid.Local (createCheckpointAndClose) +import Options + +import BlogDB (initialBlogState) +import Locales (version) +import Server + +{- Server -} + +data MainOptions = MainOptions { + optState :: String, + optPort :: Int, + optRes :: String +} + +instance Options MainOptions where + defineOptions = pure MainOptions + <*> simpleOption "statedir" "/var/tazblog/" + "Directory in which the BlogState is located." + <*> simpleOption "port" 8000 + "Port to run on. Default is 8000." + <*> simpleOption "res" "/usr/share/tazblog/res" + "Resources folder location." + +main :: IO() +main = do + putStrLn ("TazBlog " ++ version ++ " in Haskell starting") + runCommand $ \opts args -> + bracket (openLocalStateFrom (optState opts ++ "BlogState") initialBlogState) + createCheckpointAndClose + (\acid -> runBlog acid (optPort opts) (optRes opts)) + + diff --git a/src/Locales.hs b/src/Locales.hs index b387171abe..e4aaca5b1e 100644 --- a/src/Locales.hs +++ b/src/Locales.hs @@ -14,7 +14,7 @@ import BlogDB (BlogLang (..)) data BlogError = NotFound | DBError -version = "4.2" +version = "5.0-beta" allLang = [EN, DE] diff --git a/src/Main.hs b/src/Main.hs deleted file mode 100644 index a50ca67ed1..0000000000 --- a/src/Main.hs +++ /dev/null @@ -1,38 +0,0 @@ -module Main where - -import Control.Applicative (pure, (<$>), (<*>)) -import Control.Exception (bracket) -import Data.Acid -import Data.Acid.Local (createCheckpointAndClose) -import Options - -import BlogDB (initialBlogState) -import Locales (version) -import Server - -{- Server -} - -data MainOptions = MainOptions { - optState :: String, - optPort :: Int, - optRes :: String -} - -instance Options MainOptions where - defineOptions = pure MainOptions - <*> simpleOption "statedir" "/var/tazblog/" - "Directory in which the BlogState is located." - <*> simpleOption "port" 8000 - "Port to run on. Default is 8000." - <*> simpleOption "res" "/usr/share/tazblog/res" - "Resources folder location." - -main :: IO() -main = do - putStrLn ("TazBlog " ++ version ++ " in Haskell starting") - runCommand $ \opts args -> - bracket (openLocalStateFrom (optState opts ++ "BlogState") initialBlogState) - createCheckpointAndClose - (\acid -> runBlog acid (optPort opts) (optRes opts)) - - diff --git a/tazblog.cabal b/tazblog.cabal new file mode 100644 index 0000000000..6bbe2c01f0 --- /dev/null +++ b/tazblog.cabal @@ -0,0 +1,58 @@ +Name: tazblog +Version: 5.0-beta +Synopsis: Tazjin's Blog +License: MIT +License-file: LICENSE +Author: Vincent Ambo +Maintainer: tazjin@gmail.com +Category: Web blog +Build-type: Simple +cabal-version: >= 1.10 + +library + hs-source-dirs: src + default-language: Haskell2010 + exposed-modules: Blog, BlogDB, Locales, Server, RSS + build-depends: base, + bytestring, + happstack-server, + text, + blaze-html, + blaze-markup, + crypto-api, + cryptohash, + old-locale, + time, + base64-bytestring, + acid-state, + ixset, + safecopy, + mtl, + transformers, + network, + network-uri, + rss, + hamlet, + shakespeare, + markdown + default-extensions: + DeriveDataTypeable + FlexibleContexts + GeneralizedNewtypeDeriving + MultiParamTypeClasses + OverloadedStrings + RecordWildCards + ScopedTypeVariables + TemplateHaskell + TypeFamilies + QuasiQuotes + +executable tazblog + hs-source-dirs: blog + main-is: Main.hs + default-language: Haskell2010 + ghc-options: -threaded -rtsopts -with-rtsopts=-N + build-depends: base, + acid-state, + tazblog, + options -- cgit 1.4.1 From e9f044e6d5a8f3112981e100a37457a75d74b572 Mon Sep 17 00:00:00 2001 From: Vincent Ambo <tazjin@gmail.com> Date: Thu, 19 Nov 2015 19:04:21 +0100 Subject: Add tazblog-db executable --- db/Main.hs | 34 ++++++++++++++++++++++++++++++++++ tazblog.cabal | 11 +++++++++++ 2 files changed, 45 insertions(+) create mode 100644 db/Main.hs diff --git a/db/Main.hs b/db/Main.hs new file mode 100644 index 0000000000..9523041f10 --- /dev/null +++ b/db/Main.hs @@ -0,0 +1,34 @@ +-- | Main module for the database server +module Main where + +import BlogDB (initialBlogState) +import Control.Applicative (pure, (<$>), (<*>)) +import Control.Exception (bracket) +import Data.Acid +import Data.Acid.Local (createCheckpointAndClose) +import Data.Acid.Remote +import Data.Word +import Network (PortID (..)) +import Options + +data DBOptions = DBOptions { + dbPort :: Word16, + stateDirectory :: String +} + +instance Options DBOptions where + defineOptions = pure DBOptions + <*> simpleOption "dbport" 8070 + "Port to serve acid-state on remotely." + <*> simpleOption "state" "/var/tazblog/state" + "Directory in which the acid-state is located." + +main :: IO () +main = do + putStrLn ("Launching TazBlog database server ...") + runCommand $ \opts args -> + bracket (openState opts) createCheckpointAndClose + (acidServer skipAuthenticationCheck $ getPort opts) + where + openState o = openLocalStateFrom (stateDirectory o) initialBlogState + getPort = PortNumber . fromIntegral . dbPort diff --git a/tazblog.cabal b/tazblog.cabal index 6bbe2c01f0..36af795b13 100644 --- a/tazblog.cabal +++ b/tazblog.cabal @@ -56,3 +56,14 @@ executable tazblog acid-state, tazblog, options + +executable tazblog-db + hs-source-dirs: db + main-is: Main.hs + default-language: Haskell2010 + ghc-options: -threaded -rtsopts -with-rtsopts=-N + build-depends: base, + acid-state, + tazblog, + options, + network -- cgit 1.4.1 From fc0bfd470a2b915b193cf8a802a2688bd8a2a4e9 Mon Sep 17 00:00:00 2001 From: Vincent Ambo <tazjin@gmail.com> Date: Thu, 19 Nov 2015 19:28:36 +0100 Subject: Implement remote acid-state support in blog --- blog/Main.hs | 45 ++++++++++++++++++++++++--------------------- tazblog.cabal | 3 ++- 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/blog/Main.hs b/blog/Main.hs index a50ca67ed1..141a8e693c 100644 --- a/blog/Main.hs +++ b/blog/Main.hs @@ -1,38 +1,41 @@ +-- | Main module for the blog's web server module Main where -import Control.Applicative (pure, (<$>), (<*>)) -import Control.Exception (bracket) +import BlogDB (initialBlogState) +import Control.Applicative (pure, (<$>), (<*>)) +import Control.Exception (bracket) import Data.Acid -import Data.Acid.Local (createCheckpointAndClose) +import Data.Acid.Remote +import Data.Word (Word16) +import Locales (version) +import Network (HostName, PortID (..)) import Options - -import BlogDB (initialBlogState) -import Locales (version) import Server -{- Server -} - data MainOptions = MainOptions { - optState :: String, - optPort :: Int, - optRes :: String + dbHost :: String, + dbPort :: Word16, + blogPort :: Int, + resourceDir :: String } instance Options MainOptions where defineOptions = pure MainOptions - <*> simpleOption "statedir" "/var/tazblog/" - "Directory in which the BlogState is located." - <*> simpleOption "port" 8000 - "Port to run on. Default is 8000." - <*> simpleOption "res" "/usr/share/tazblog/res" + <*> simpleOption "dbHost" "localhost" + "Remote acid-state database host. Default is localhost" + <*> simpleOption "dbPort" 8070 + "Remote acid-state database port. Default is 8070" + <*> simpleOption "blogPort" 8000 + "Port to serve the blog on. Default is 8000." + <*> simpleOption "resourceDir" "/opt/tazblog/res" "Resources folder location." - + main :: IO() main = do putStrLn ("TazBlog " ++ version ++ " in Haskell starting") - runCommand $ \opts args -> - bracket (openLocalStateFrom (optState opts ++ "BlogState") initialBlogState) - createCheckpointAndClose - (\acid -> runBlog acid (optPort opts) (optRes opts)) + runCommand $ \opts _ -> + let port = PortNumber $ fromIntegral $ dbPort opts + in openRemoteState skipAuthenticationPerform (dbHost opts) port >>= + (\acid -> runBlog acid (blogPort opts) (resourceDir opts)) diff --git a/tazblog.cabal b/tazblog.cabal index 36af795b13..6fc29176f2 100644 --- a/tazblog.cabal +++ b/tazblog.cabal @@ -55,7 +55,8 @@ executable tazblog build-depends: base, acid-state, tazblog, - options + options, + network executable tazblog-db hs-source-dirs: db -- cgit 1.4.1 From b38216c162a817862dceddd0a86ad60468d79c01 Mon Sep 17 00:00:00 2001 From: Vincent Ambo <tazjin@gmail.com> Date: Thu, 19 Nov 2015 19:31:25 +0100 Subject: Update Dockerfile slightly --- Dockerfile | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 3d40218ab9..73ab1057a2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,12 +3,13 @@ MAINTAINER Vincent Ambo <dev@tazj.in> # Base setup VOLUME /var/tazblog -EXPOSE 8000 +EXPOSE 8000 8070 +ENV PATH /root/.local/bin:$PATH # Build blog -ADD . /opt/tazblog/src -WORKDIR /opt/tazblog/src +ADD . /opt/tazblog +WORKDIR /opt/tazblog RUN stack install && cp /root/.local/bin/tazblog /usr/bin/tazblog # Done! -CMD /usr/bin/tazblog +CMD tazblog -- cgit 1.4.1 From db1ae9930cec8cdf892967c9d7b9c63bf81550ec Mon Sep 17 00:00:00 2001 From: Vincent Ambo <tazjin@gmail.com> Date: Thu, 19 Nov 2015 19:32:02 +0100 Subject: Remove old tools --- tools/acid-fixer/Main.hs | 228 ------------------------------------------ tools/colouriser/LICENSE | 3 - tools/colouriser/colour.cabal | 64 ------------ tools/colouriser/colour.hs | 24 ----- 4 files changed, 319 deletions(-) delete mode 100644 tools/acid-fixer/Main.hs delete mode 100644 tools/colouriser/LICENSE delete mode 100644 tools/colouriser/colour.cabal delete mode 100644 tools/colouriser/colour.hs diff --git a/tools/acid-fixer/Main.hs b/tools/acid-fixer/Main.hs deleted file mode 100644 index 8ef3190cc5..0000000000 --- a/tools/acid-fixer/Main.hs +++ /dev/null @@ -1,228 +0,0 @@ -{-# LANGUAGE DeriveDataTypeable #-} -{-# LANGUAGE GeneralizedNewtypeDeriving #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE RecordWildCards #-} -{-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE TypeFamilies #-} - -module Main where - -import Control.Monad.Reader (ask) -import Control.Monad.State (get, put) -import Data.Acid -import Data.Acid.Advanced -import Data.Acid.Local -import Data.ByteString (ByteString) -import Data.Char (toLower) -import Data.Data (Data, Typeable) -import Data.IxSet (Indexable (..), IxSet (..), Proxy (..), - getOne, ixFun, ixSet, (@=)) -import Data.List (insert) -import Data.SafeCopy -import Data.Text (Text, pack) -import Data.Text.Lazy (toStrict) -import Data.Time -import Happstack.Server (FromReqURI (..)) -import System.Environment (getEnv) - -import qualified Crypto.Hash.SHA512 as SHA (hash) -import qualified Data.ByteString.Base64 as B64 (encode) -import qualified Data.ByteString.Char8 as B -import qualified Data.IxSet as IxSet -import qualified Data.Text as Text - -newtype EntryId = EntryId { unEntryId :: Integer } - deriving (Eq, Ord, Data, Enum, Typeable) - -$(deriveSafeCopy 2 'base ''EntryId) - -instance Show EntryId where - show = show . unEntryId - -data BlogLang = EN | DE - deriving (Eq, Ord, Data, Typeable) - -instance Show BlogLang where - show DE = "de" - show EN = "en" - -instance FromReqURI BlogLang where - fromReqURI sub = - case map toLower sub of - "de" -> Just DE - "en" -> Just EN - _ -> Nothing - -$(deriveSafeCopy 0 'base ''BlogLang) - -data Comment = Comment { - cdate :: UTCTime, - cauthor :: Text, - ctext :: Text -} deriving (Eq, Ord, Show, Data, Typeable) - -$(deriveSafeCopy 0 'base ''Comment) - -data Entry_v0 = Entry_v0 { - entryId_v0 :: EntryId, - lang_v0 :: BlogLang, - author_v0 :: Text, - title_v0 :: Text, - btext_v0 :: Text, - mtext_v0 :: Text, - edate_v0 :: UTCTime, - tags :: [Text], - comments :: [Comment] -} deriving (Eq, Ord, Show, Data, Typeable) -$(deriveSafeCopy 0 'base ''Entry_v0) - -data Entry = Entry { - entryId :: EntryId, - lang :: BlogLang, - author :: Text, - title :: Text, - btext :: Text, - mtext :: Text, - edate :: UTCTime -} deriving (Eq, Ord, Show, Data, Typeable) - -$(deriveSafeCopy 2 'extension ''Entry) - -instance Migrate Entry where - type MigrateFrom Entry = Entry_v0 - migrate (Entry_v0 ei l a t b m ed _ _) = - Entry ei l a t b m ed - --- ixSet requires different datatypes for field indexes, so let's define some -newtype Author_v0 = Author_v0 Text deriving (Eq, Ord, Data, Typeable, SafeCopy) -newtype Author = Author Text deriving (Eq, Ord, Data, Typeable) -$(deriveSafeCopy 2 'extension ''Author) -instance Migrate Author where - type MigrateFrom Author = Author_v0 - migrate (Author_v0 x) = Author x - -newtype Title_v0 = Title_v0 Text deriving (Eq, Ord, Data, Typeable, SafeCopy) -newtype Title = Title Text deriving (Eq, Ord, Data, Typeable) -$(deriveSafeCopy 2 'extension ''Title) -instance Migrate Title where - type MigrateFrom Title = Title_v0 - migrate (Title_v0 x) = Title x - -newtype BText_v0 = BText_v0 Text deriving (Eq, Ord, Data, Typeable, SafeCopy) -newtype BText = BText Text deriving (Eq, Ord, Data, Typeable) -- standard text -$(deriveSafeCopy 2 'extension ''BText) -instance Migrate BText where - type MigrateFrom BText = BText_v0 - migrate (BText_v0 x) = BText x - -newtype MText_v0 = MText_v0 Text deriving (Eq, Ord, Data, Typeable, SafeCopy) -newtype MText = MText Text deriving (Eq, Ord, Data, Typeable) -- "read more" text -$(deriveSafeCopy 2 'extension ''MText) -instance Migrate MText where - type MigrateFrom MText = MText_v0 - migrate (MText_v0 x) = MText x - -newtype Tag_v0 = Tag_v0 Text deriving (Eq, Ord, Data, Typeable, SafeCopy) -newtype Tag = Tag Text deriving (Eq, Ord, Data, Typeable) -$(deriveSafeCopy 2 'extension ''Tag) -instance Migrate Tag where - type MigrateFrom Tag = Tag_v0 - migrate (Tag_v0 x) = Tag x - -newtype EDate_v0 = EDate_v0 UTCTime deriving (Eq, Ord, Data, Typeable, SafeCopy) -newtype EDate = EDate UTCTime deriving (Eq, Ord, Data, Typeable) -$(deriveSafeCopy 2 'extension ''EDate) -instance Migrate EDate where - type MigrateFrom EDate = EDate_v0 - migrate (EDate_v0 x) = EDate x - -newtype SDate_v0 = SDate_v0 UTCTime deriving (Eq, Ord, Data, Typeable, SafeCopy) -newtype SDate = SDate UTCTime deriving (Eq, Ord, Data, Typeable) -$(deriveSafeCopy 2 'extension ''SDate) -instance Migrate SDate where - type MigrateFrom SDate = SDate_v0 - migrate (SDate_v0 x) = SDate x - -newtype Username_v0 = Username_v0 Text deriving (Eq, Ord, Data, Typeable, SafeCopy) -newtype Username = Username Text deriving (Eq, Ord, Data, Typeable) -$(deriveSafeCopy 2 'extension ''Username) -instance Migrate Username where - type MigrateFrom Username = Username_v0 - migrate (Username_v0 x) = Username x - -newtype SessionID_v0 = SessionID_v0 Text deriving (Eq, Ord, Data, Typeable, SafeCopy) -newtype SessionID = SessionID Text deriving (Eq, Ord, Data, Typeable) -$(deriveSafeCopy 2 'extension ''SessionID) -instance Migrate SessionID where - type MigrateFrom SessionID = SessionID_v0 - migrate (SessionID_v0 x) = SessionID x - -instance Indexable Entry where - empty = ixSet [ ixFun $ \e -> [ entryId e] - , ixFun $ (:[]) . lang - , ixFun $ \e -> [ Author $ author e ] - , ixFun $ \e -> [ Title $ title e] - , ixFun $ \e -> [ BText $ btext e] - , ixFun $ \e -> [ MText $ mtext e] - , ixFun $ \e -> [ EDate $ edate e] - ] - -data User = User { - username :: Text, - password :: ByteString -} deriving (Eq, Ord, Data, Typeable) - -$(deriveSafeCopy 0 'base ''User) - -data Session = Session { - sessionID :: Text, - user :: User, - sdate :: UTCTime -} deriving (Eq, Ord, Data, Typeable) - -$(deriveSafeCopy 0 'base ''Session) - -instance Indexable User where - empty = ixSet [ ixFun $ \u -> [Username $ username u] - , ixFun $ (:[]) . password - ] - -instance Indexable Session where - empty = ixSet [ ixFun $ \s -> [SessionID $ sessionID s] - , ixFun $ (:[]) . user - , ixFun $ \s -> [SDate $ sdate s] - ] - -data Blog = Blog { - blogSessions :: IxSet Session, - blogUsers :: IxSet User, - blogEntries :: IxSet Entry -} deriving (Data, Typeable) - -latestEntries :: BlogLang -> Query Blog [Entry] -latestEntries lang = - do b@Blog{..} <- ask - return $ IxSet.toDescList (Proxy :: Proxy EDate) $ blogEntries @= lang - -$(deriveSafeCopy 0 'base ''Blog) - -$(makeAcidic ''Blog ['latestEntries]) - -initialBlogState :: Blog -initialBlogState = - Blog { blogSessions = empty - , blogUsers = empty - , blogEntries = empty } - -main :: IO () -main = do - putStrLn "Opening state" - acid <- openLocalStateFrom "/var/tazblog/BlogState" initialBlogState - entries <- query acid (LatestEntries EN) - print $ length entries - print $ head entries - putStrLn "Creating checkpoint" - createCheckpoint acid - putStrLn "Closing state" - closeAcidState acid diff --git a/tools/colouriser/LICENSE b/tools/colouriser/LICENSE deleted file mode 100644 index 44ade87351..0000000000 --- a/tools/colouriser/LICENSE +++ /dev/null @@ -1,3 +0,0 @@ -This program comes with absolutely no warranty and I can't guarantee that it's not going to explode in your face. - -In addition to this, I don't care what you do with this. \ No newline at end of file diff --git a/tools/colouriser/colour.cabal b/tools/colouriser/colour.cabal deleted file mode 100644 index 99a3c9cc03..0000000000 --- a/tools/colouriser/colour.cabal +++ /dev/null @@ -1,64 +0,0 @@ --- colour.cabal auto-generated by cabal init. For additional options, --- see --- http://www.haskell.org/cabal/release/cabal-latest/doc/users-guide/authors.html#pkg-descr. --- The name of the package. -Name: colour - --- The package version. See the Haskell package versioning policy --- (http://www.haskell.org/haskellwiki/Package_versioning_policy) for --- standards guiding when and how versions should be incremented. -Version: 0.2 - --- A short (one-line) description of the package. -Synopsis: Shortcut program to use HsColour - --- A longer description of the package. --- Description: - --- URL for the project homepage or repository. -Homepage: http://tazj.in/ - --- The license under which the package is released. -License: OtherLicense - --- The file containing the license text. -License-file: LICENSE - --- The package author(s). -Author: tazjin - --- An email address to which users can send suggestions, bug reports, --- and patches. --- Maintainer: - --- A copyright notice. --- Copyright: - -Category: Web - -Build-type: Simple - --- Extra files to be distributed with the package, such as examples or --- a README. --- Extra-source-files: - --- Constraint on the version of Cabal needed to build this package. -Cabal-version: >=1.2 - - -Executable colour - -- .hs or .lhs file containing the Main module. - Main-is: colour.hs - - -- Packages needed in order to build this package. - Build-depends: - base, - options, - hscolour - - -- Modules not exported by this package. - -- Other-modules: - - -- Extra tools (e.g. alex, hsc2hs, ...) needed to build the source. - -- Build-tools: - diff --git a/tools/colouriser/colour.hs b/tools/colouriser/colour.hs deleted file mode 100644 index 3e6e39ba45..0000000000 --- a/tools/colouriser/colour.hs +++ /dev/null @@ -1,24 +0,0 @@ -{-# LANGUAGE TemplateHaskell #-} - -import Control.Monad (unless) -import Language.Haskell.HsColour.Colourise (defaultColourPrefs) -import Language.Haskell.HsColour.CSS -import Options - -defineOptions "MainOptions" $ do - stringOption "optFile" "file" "" - "Name of the .hs file. Will be used for the HTML file as well" - -colorCode :: String -> IO () -colorCode input = do - code <- readFile input - putStr $ concat [ "<div class=\"code\">" - , hscolour False code - , "</div>" - ] - -main :: IO () -main = runCommand $ \opts args -> do - let file = optFile opts - unless (file == "") $ - colorCode file \ No newline at end of file -- cgit 1.4.1 From 850d8d79a7829ca6f4feed0b4e89f1e1e328d4a0 Mon Sep 17 00:00:00 2001 From: Vincent Ambo <tazjin@gmail.com> Date: Thu, 19 Nov 2015 22:23:05 +0100 Subject: [varnish] Add Varnish configuration and Dockerfile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- varnish/Dockerfile | 11 +++++++++++ varnish/default.vcl | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 varnish/Dockerfile create mode 100644 varnish/default.vcl diff --git a/varnish/Dockerfile b/varnish/Dockerfile new file mode 100644 index 0000000000..4a4b7dd7e0 --- /dev/null +++ b/varnish/Dockerfile @@ -0,0 +1,11 @@ +FROM centos:7 +MAINTAINER Vincent Ambo <hej@tazj.in> + +EXPOSE 6081 6082 + +RUN yum install -y epel-release && yum install -y varnish + +ADD default.vcl /etc/varnish/default.vcl + +CMD ulimit -n 131072 && \ + /usr/sbin/varnishd -F -f /etc/varnish/default.vcl -a :6081 -T :6082 -t 120 diff --git a/varnish/default.vcl b/varnish/default.vcl new file mode 100644 index 0000000000..5710a589cc --- /dev/null +++ b/varnish/default.vcl @@ -0,0 +1,49 @@ +vcl 4.0; + +# By default, Varnish will run on the same servers as the blog. Inside of +# Kubernetes this will be inside the same pod. + +backend default { + .host = "localhost"; + .port = "8000"; +} + +# Purge requests should be accepted from localhost +acl purge { + "localhost"; +} + +sub vcl_recv { + # Allow HTTP PURGE from ACL above + if (req.method == "PURGE" && client.ip ~ purge) { + return (purge); + } + + # Redirect /en to / (no more multi-language support) + if (req.url ~ "^/en") { + set req.url = regsub(req.url, "^/en/", "/"); + return (synth(301, "")); + } + + # Don't cache admin page + if (req.url ~ "^/admin") { + return (pass); + } +} + +sub vcl_backend_response { + # Cache everything for at least 1 minute. + if (beresp.ttl < 1m) { + set beresp.ttl = 1m; + } + + # Add an HSTS header to our response +} + +sub vcl_synth { + # Execute redirects + if (resp.status == 301) { + set resp.http.Location = req.url; + return (deliver); + } +} -- cgit 1.4.1 From c60a85638820ac94ee7232515fe87d76f54893a7 Mon Sep 17 00:00:00 2001 From: Vincent Ambo <tazjin@gmail.com> Date: Thu, 19 Nov 2015 22:34:04 +0100 Subject: Version 5.0 \o/ --- src/Locales.hs | 2 +- tazblog.cabal | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Locales.hs b/src/Locales.hs index e4aaca5b1e..10cce8389f 100644 --- a/src/Locales.hs +++ b/src/Locales.hs @@ -14,7 +14,7 @@ import BlogDB (BlogLang (..)) data BlogError = NotFound | DBError -version = "5.0-beta" +version = "5.0" allLang = [EN, DE] diff --git a/tazblog.cabal b/tazblog.cabal index 6fc29176f2..ee759dfb04 100644 --- a/tazblog.cabal +++ b/tazblog.cabal @@ -1,5 +1,5 @@ Name: tazblog -Version: 5.0-beta +Version: 5.0 Synopsis: Tazjin's Blog License: MIT License-file: LICENSE -- cgit 1.4.1 From 1342e8fb1d86c9f1349dff2c587be5b4f67b6b86 Mon Sep 17 00:00:00 2001 From: Vincent Ambo <tazjin@gmail.com> Date: Fri, 20 Nov 2015 01:53:38 +0100 Subject: Reinstate some language handling --- src/Locales.hs | 4 +--- src/Server.hs | 18 ++++++++++-------- tazblog.cabal | 4 ++-- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Locales.hs b/src/Locales.hs index 10cce8389f..b326da648f 100644 --- a/src/Locales.hs +++ b/src/Locales.hs @@ -5,8 +5,6 @@ import Data.Maybe (fromMaybe) import Data.Text (Text) import qualified Data.Text as T import Network.URI - - import BlogDB (BlogLang (..)) {- to add a language simply define its abbreviation and Show instance then @@ -14,7 +12,7 @@ import BlogDB (BlogLang (..)) data BlogError = NotFound | DBError -version = "5.0" +version = "5.0.1" allLang = [EN, DE] diff --git a/src/Server.hs b/src/Server.hs index b71b070f1b..3b70f348ac 100644 --- a/src/Server.hs +++ b/src/Server.hs @@ -21,6 +21,14 @@ import BlogDB hiding (updateEntry) import Locales import RSS + +instance FromReqURI BlogLang where + fromReqURI sub = + case map toLower sub of + "de" -> Just DE + "en" -> Just EN + _ -> Nothing + tmpPolicy :: BodyPolicy tmpPolicy = defaultBodyPolicy "/tmp" 0 200000 1000 @@ -31,8 +39,7 @@ runBlog acid port respath = tazBlog :: AcidState Blog -> String -> ServerPart Response tazBlog acid resDir = do msum [ nullDir >> blogHandler acid EN - , dir "de" $ blogHandler acid DE - , path $ \(year :: Int) -> path $ \(month :: Int) -> path $ \(id_ :: String) -> formatOldLink year month id_ + , path $ \(lang :: BlogLang) -> blogHandler acid lang , dir "notice" $ ok $ toResponse showSiteNotice {- :Admin handlers -} , do dirs "admin/postentry" nullDir @@ -62,7 +69,7 @@ tazBlog acid resDir = do setHeaderM "expires" "Tue, 20 Jan 2037 04:20:42 GMT" dir "static" $ serveDirectory DisableBrowsing [] resDir , serveDirectory DisableBrowsing [] resDir - , notFound $ toResponse $ showError NotFound EN + , notFound $ toResponse $ showError NotFound DE ] blogHandler :: AcidState Blog -> BlogLang -> ServerPart Response @@ -74,11 +81,6 @@ blogHandler acid lang = , notFound $ toResponse $ showError NotFound lang ] -formatOldLink :: Int -> Int -> String -> ServerPart Response -formatOldLink y m id_ = - flip seeOther (toResponse ()) $ - concat $ intersperse' "/" ["de", show y, show m, replace '.' '/' id_] - showEntry :: AcidState Blog -> BlogLang -> EntryId -> ServerPart Response showEntry acid lang eId = do entry <- query' acid (GetEntry eId) diff --git a/tazblog.cabal b/tazblog.cabal index ee759dfb04..5de69e26b4 100644 --- a/tazblog.cabal +++ b/tazblog.cabal @@ -1,5 +1,5 @@ Name: tazblog -Version: 5.0 +Version: 5.0.1 Synopsis: Tazjin's Blog License: MIT License-file: LICENSE @@ -11,7 +11,7 @@ cabal-version: >= 1.10 library hs-source-dirs: src - default-language: Haskell2010 + default-language: Haskell2010 exposed-modules: Blog, BlogDB, Locales, Server, RSS build-depends: base, bytestring, -- cgit 1.4.1 From df93cead2801ac6fa28d21ef46751bcc4f95dc2b Mon Sep 17 00:00:00 2001 From: Vincent Ambo <tazjin@gmail.com> Date: Fri, 20 Nov 2015 01:59:52 +0100 Subject: [k8s] Add Kubernetes service & RC files --- k8s/tazblog-db-rc.yaml | 26 ++++++++++++++++++++++++++ k8s/tazblog-db-service.yaml | 12 ++++++++++++ k8s/tazblog-rc.yaml | 39 +++++++++++++++++++++++++++++++++++++++ k8s/tazblog-svc.yaml | 17 +++++++++++++++++ 4 files changed, 94 insertions(+) create mode 100644 k8s/tazblog-db-rc.yaml create mode 100644 k8s/tazblog-db-service.yaml create mode 100644 k8s/tazblog-rc.yaml create mode 100644 k8s/tazblog-svc.yaml diff --git a/k8s/tazblog-db-rc.yaml b/k8s/tazblog-db-rc.yaml new file mode 100644 index 0000000000..26d730c4df --- /dev/null +++ b/k8s/tazblog-db-rc.yaml @@ -0,0 +1,26 @@ +apiVersion: v1 +kind: ReplicationController +metadata: + name: tazblog-db +spec: + selector: + app: tazblog-db + template: + metadata: + labels: + app: tazblog-db + spec: + containers: + - image: tazjin/tazblog-haskell:master + name: tazblog-db + command: ["tazblog-db"] + ports: + - containerPort: 8070 + volumeMounts: + - name: tazblog-state + mountPath: /var/tazblog + volumes: + - name: tazblog-state + gcePersistentDisk: + pdName: tazblog-state + fsType: ext4 diff --git a/k8s/tazblog-db-service.yaml b/k8s/tazblog-db-service.yaml new file mode 100644 index 0000000000..6d5d429469 --- /dev/null +++ b/k8s/tazblog-db-service.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: tazblog-db + labels: + app: tazblog-db +spec: + selector: + app: tazblog-db + ports: + - port: 8070 + name: tazblog-db diff --git a/k8s/tazblog-rc.yaml b/k8s/tazblog-rc.yaml new file mode 100644 index 0000000000..ed291392a7 --- /dev/null +++ b/k8s/tazblog-rc.yaml @@ -0,0 +1,39 @@ +apiVersion: v1 +kind: ReplicationController +metadata: + name: tazblog +spec: + selector: + app: tazblog + template: + metadata: + labels: + app: tazblog + spec: + containers: + - image: tazjin/tazblog-haskell:master + imagePullPolicy: Always + name: tazblog + command: ["tazblog", "--dbHost", "tazblog-db.default.svc.cluster.local"] + ports: + - containerPort: 8000 + - image: tazjin/varnish + imagePullPolicy: Always + name: tazblog-varnish + ports: + - containerPort: 6081 + - containerPort: 6082 + - image: tazjin/hitch:master + imagePullPolicy: Always + name: tazblog-hitch + command: ["hitch", "--user=hitch", "/etc/hitch/ssl/tazblog-tls"] + ports: + - containerPort: 8443 + volumeMounts: + - name: tazblog-tls + readOnly: true + mountPath: /etc/hitch/ssl + volumes: + - name: tazblog-tls + secret: + secretName: tazblog-tls diff --git a/k8s/tazblog-svc.yaml b/k8s/tazblog-svc.yaml new file mode 100644 index 0000000000..6a2d9a4223 --- /dev/null +++ b/k8s/tazblog-svc.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + name: tazblog + labels: + app: tazblog +spec: + type: LoadBalancer + selector: + app: tazblog + ports: + - port: 80 + targetPort: 6081 + name: tazblog-http + - port: 443 + targetPort: 8443 + name: tazblog-https -- cgit 1.4.1 From fdc20d3c825c8a8d8760c4f4778fb3f7d0d691cc Mon Sep 17 00:00:00 2001 From: Vincent Ambo <tazjin@gmail.com> Date: Fri, 20 Nov 2015 10:58:15 +0100 Subject: [k8s] Hitch should connect to Varnish, not the blog --- k8s/tazblog-rc.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/k8s/tazblog-rc.yaml b/k8s/tazblog-rc.yaml index ed291392a7..4549ab1b39 100644 --- a/k8s/tazblog-rc.yaml +++ b/k8s/tazblog-rc.yaml @@ -3,6 +3,7 @@ kind: ReplicationController metadata: name: tazblog spec: + replicas: 2 selector: app: tazblog template: @@ -26,7 +27,7 @@ spec: - image: tazjin/hitch:master imagePullPolicy: Always name: tazblog-hitch - command: ["hitch", "--user=hitch", "/etc/hitch/ssl/tazblog-tls"] + command: ["hitch", "--backend=:6081", "--user=hitch", "/etc/hitch/ssl/tazblog-tls"] ports: - containerPort: 8443 volumeMounts: -- cgit 1.4.1 From c1bdfe5a66a9d6f2f0a0c1e9d1f3ae28a12e8feb Mon Sep 17 00:00:00 2001 From: Vincent Ambo <tazjin@gmail.com> Date: Fri, 20 Nov 2015 10:58:33 +0100 Subject: [varnish] Don't remove the /en/ for now --- varnish/default.vcl | 6 ------ 1 file changed, 6 deletions(-) diff --git a/varnish/default.vcl b/varnish/default.vcl index 5710a589cc..de08f4f646 100644 --- a/varnish/default.vcl +++ b/varnish/default.vcl @@ -19,12 +19,6 @@ sub vcl_recv { return (purge); } - # Redirect /en to / (no more multi-language support) - if (req.url ~ "^/en") { - set req.url = regsub(req.url, "^/en/", "/"); - return (synth(301, "")); - } - # Don't cache admin page if (req.url ~ "^/admin") { return (pass); -- cgit 1.4.1 From 4fa4e20f9bae085ba129b96121ec6dbd9e9f27fb Mon Sep 17 00:00:00 2001 From: Vincent Ambo <vincent@kivra.com> Date: Sat, 21 Nov 2015 02:56:05 +0100 Subject: [5.1-beta] Begin beta branch --- src/Locales.hs | 2 +- tazblog.cabal | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Locales.hs b/src/Locales.hs index b326da648f..2b578e094c 100644 --- a/src/Locales.hs +++ b/src/Locales.hs @@ -12,7 +12,7 @@ import BlogDB (BlogLang (..)) data BlogError = NotFound | DBError -version = "5.0.1" +version = "5.1-beta" allLang = [EN, DE] diff --git a/tazblog.cabal b/tazblog.cabal index 5de69e26b4..9220241ba6 100644 --- a/tazblog.cabal +++ b/tazblog.cabal @@ -1,5 +1,5 @@ Name: tazblog -Version: 5.0.1 +Version: 5.1-beta Synopsis: Tazjin's Blog License: MIT License-file: LICENSE -- cgit 1.4.1 From 30e9f29fe19d29c39fc315e6e43cde3998d377cb Mon Sep 17 00:00:00 2001 From: Vincent Ambo <vincent@kivra.com> Date: Sat, 21 Nov 2015 02:56:51 +0100 Subject: Remove site notice --- res/admin.css | 2 +- src/Blog.hs | 20 -------------------- src/Server.hs | 1 - 3 files changed, 1 insertion(+), 22 deletions(-) diff --git a/res/admin.css b/res/admin.css index 5225fe1033..2eb375dd14 100644 --- a/res/admin.css +++ b/res/admin.css @@ -47,4 +47,4 @@ body { padding-right: 10px; min-height:200px; width:378px; -} \ No newline at end of file +} diff --git a/src/Blog.hs b/src/Blog.hs index 34b2aaf526..973382bc92 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -83,8 +83,6 @@ showFooter l v = [shamlet| \ and without PHP, Java, Perl, MySQL and Python. <p> <a class="link" href=#{repoURL}>#{append "Version " v} -   - <a class="link" href="/notice">#{noticeText l} <div .row .text-center> <div .span12> <span style="font-size:13px;font-family:Helvetica;">ಠ_ಠ @@ -168,24 +166,6 @@ renderEntry e@Entry{..} = [shamlet| where woText = flip T.append author $ T.pack $ formatTime defaultTimeLocale (eTimeFormat lang) edate - -showSiteNotice :: Html -showSiteNotice = [shamlet| -$doctype 5 -<head> - <title>Impressum -<body> - <h2>Impressum - <br> - <p> - Vincent Ambo - <br> - Gyllenborgsgatan 8, LGH 1306 - <br> - 11243 Stockholm - <p><a href="/" style="color:black;">Back -|] - {- Administration pages -} adminTemplate :: Text -> Html -> Html diff --git a/src/Server.hs b/src/Server.hs index 3b70f348ac..0522d9d7a8 100644 --- a/src/Server.hs +++ b/src/Server.hs @@ -21,7 +21,6 @@ import BlogDB hiding (updateEntry) import Locales import RSS - instance FromReqURI BlogLang where fromReqURI sub = case map toLower sub of -- cgit 1.4.1 From 7610e790139717bf87ff25c9d694d7d589d5c420 Mon Sep 17 00:00:00 2001 From: Vincent Ambo <vincent@kivra.com> Date: Sat, 21 Nov 2015 02:59:03 +0100 Subject: [db] Update interactiveUserAdd for remote state --- src/BlogDB.hs | 52 +++++++++++++++++++++++++--------------------------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/src/BlogDB.hs b/src/BlogDB.hs index dd8aaaa66c..316c2fdc08 100644 --- a/src/BlogDB.hs +++ b/src/BlogDB.hs @@ -1,20 +1,20 @@ module BlogDB where -import Control.Monad.Reader (ask) -import Control.Monad.State (get, put) -import Data.Acid -import Data.Acid.Advanced -import Data.Acid.Local -import Data.ByteString (ByteString) -import Data.Data (Data, Typeable) -import Data.IxSet (Indexable (..), IxSet (..), Proxy (..), - getOne, ixFun, ixSet, (@=)) -import Data.List (insert) -import Data.SafeCopy (SafeCopy, base, deriveSafeCopy) -import Data.Text (Text, pack) -import Data.Text.Lazy (toStrict) -import Data.Time -import System.Environment (getEnv) +import Control.Monad.Reader (ask) +import Control.Monad.State (get, put) +import Data.Acid +import Data.Acid.Advanced +import Data.Acid.Remote +import Data.ByteString (ByteString) +import Data.Data (Data, Typeable) +import Data.IxSet (Indexable (..), IxSet (..), Proxy (..), getOne, ixFun, ixSet, (@=)) +import Data.List (insert) +import Data.SafeCopy (SafeCopy, base, deriveSafeCopy) +import Data.Text (Text, pack) +import Data.Text.Lazy (toStrict) +import Data.Time +import Network (PortID (..)) +import System.Environment (getEnv) import qualified Crypto.Hash.SHA512 as SHA (hash) import qualified Data.ByteString.Base64 as B64 (encode) @@ -22,7 +22,6 @@ import qualified Data.ByteString.Char8 as B import qualified Data.IxSet as IxSet import qualified Data.Text as Text - newtype EntryId = EntryId { unEntryId :: Integer } deriving (Eq, Ord, Data, Enum, Typeable) @@ -41,13 +40,13 @@ instance Show BlogLang where $(deriveSafeCopy 0 'base ''BlogLang) data Entry = Entry { - entryId :: EntryId, - lang :: BlogLang, - author :: Text, - title :: Text, - btext :: Text, - mtext :: Text, - edate :: UTCTime + entryId :: EntryId, + lang :: BlogLang, + author :: Text, + title :: Text, + btext :: Text, + mtext :: Text, + edate :: UTCTime } deriving (Eq, Ord, Show, Data, Typeable) $(deriveSafeCopy 2 'base ''Entry) @@ -201,10 +200,9 @@ $(makeAcidic ''Blog , 'clearSessions ]) -interactiveUserAdd :: IO () -interactiveUserAdd = do - tbDir <- getEnv "TAZBLOG" - acid <- openLocalStateFrom (tbDir ++ "/BlogState") initialBlogState +interactiveUserAdd :: String -> IO () +interactiveUserAdd dbHost = do + acid <- openRemoteState skipAuthenticationPerform dbHost (PortNumber 8070) putStrLn "Username:" un <- getLine putStrLn "Password:" -- cgit 1.4.1 From 308e859d56a56d2f625f2f2fe5c88331e35a8a25 Mon Sep 17 00:00:00 2001 From: Vincent Ambo <vincent@kivra.com> Date: Sat, 21 Nov 2015 02:59:34 +0100 Subject: [blog] Split request handling, do HTTP better * request handling split into multiple smaller handlers * use request methods in various places instead of different routes * some minor updates to admin page --- src/Blog.hs | 38 ++++++++++++------------- src/Server.hs | 89 +++++++++++++++++++++++++++++------------------------------ 2 files changed, 62 insertions(+), 65 deletions(-) diff --git a/src/Blog.hs b/src/Blog.hs index 973382bc92..f4bdaa6986 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -184,7 +184,7 @@ adminLogin = adminTemplate "Login" $ [shamlet| <div class="loginBox"> <div class="loginBoxTop">TazBlog Admin: Login <div class="loginBoxMiddle"> - <form action="/dologin" method="POST"> + <form action="/admin" method="POST"> <p>Account ID <p><input type="text" style="font-size:2;" name="account" value="tazjin" readonly="1"> <p>Passwort @@ -195,40 +195,39 @@ adminLogin = adminTemplate "Login" $ [shamlet| adminIndex :: Text -> Html adminIndex sUser = adminTemplate "Index" $ [shamlet| <div style="float:center;"> - <form action="/admin/postentry" method="POST"> + <form action="/admin/entry" method="POST"> <table> <tr> - <thead><td>Titel: + <thead><td>Title: <td><input type="text" name="title"> <tr> - <thead><td>Sprache: + <thead><td>Language: <td><select name="lang"> + <option value="en">English <option value="de">Deutsch - <option value="en">Englisch <tr> <thead><td>Text: <td> <textarea name="btext" cols="100" rows="15"> <tr> <thead> - <td style="vertical-align:top;">Mehr Text: + <td style="vertical-align:top;">Read more: <td> <textarea name="mtext" cols="100" rows="15"> <input type="hidden" name="author" value=#{sUser}> - <input style="margin-left:20px;" type="submit" value="Absenden"> + <input style="margin-left:20px;" type="submit" value="Submit"> ^{adminFooter} |] adminFooter :: Html adminFooter = [shamlet| -<a href="/">Startseite -\ -- Entrylist: # -<a href="/admin/entrylist/de">DE -\ & # -<a href="/admin/entrylist/en">EN +<a href="/">Front page \ -- # -<a href="#">Backup -\ (NYI) + <a href="/admin">New article +\ -- Entry list: # + <a href="/admin/entrylist/en">EN +\ & # +<a href="/admin/entrylist/de">DE |] adminEntryList :: [Entry] -> Html @@ -237,7 +236,7 @@ adminEntryList entries = adminTemplate "EntryList" $ [shamlet| <table> $forall entry <- entries <tr> - <td><a href=#{append "/admin/edit/" (show' $ entryId entry)}>#{title entry} + <td><a href=#{append "/admin/entry/" (show' $ entryId entry)}>#{title entry} <td>#{formatPostDate $ edate entry} |] where @@ -246,10 +245,10 @@ adminEntryList entries = adminTemplate "EntryList" $ [shamlet| editPage :: Entry -> Html editPage (Entry{..}) = adminTemplate "Index" $ [shamlet| <div style="float:center;"> - <form action="/admin/updateentry" method="POST"> + <form action=#{append "/admin/entry/" (show' entryId)} method="POST"> <table> <tr> - <td>Titel: + <td>Title: <td> <input type="text" name="title" value=#{title}> <tr> @@ -257,11 +256,10 @@ editPage (Entry{..}) = adminTemplate "Index" $ [shamlet| <td> <textarea name="btext" cols="100" rows="15">#{btext} <tr> - <td style="vertical-align:top;">Mehr Text: + <td style="vertical-align:top;">Read more: <td> <textarea name="mtext" cols="100" rows="15">#{mtext} - <input type="hidden" name="eid" value=#{unEntryId entryId}> - <input type="submit" style="margin-left:20px;" value="Absenden"> + <input type="submit" style="margin-left:20px;" value="Submit"> <p>^{adminFooter} |] diff --git a/src/Server.hs b/src/Server.hs index 0522d9d7a8..69eff3a78f 100644 --- a/src/Server.hs +++ b/src/Server.hs @@ -37,37 +37,14 @@ runBlog acid port respath = tazBlog :: AcidState Blog -> String -> ServerPart Response tazBlog acid resDir = do - msum [ nullDir >> blogHandler acid EN - , path $ \(lang :: BlogLang) -> blogHandler acid lang - , dir "notice" $ ok $ toResponse showSiteNotice - {- :Admin handlers -} - , do dirs "admin/postentry" nullDir - guardSession acid - postEntry acid - , do dirs "admin/entrylist" $ dir (show DE) nullDir - guardSession acid - entryList acid DE - , do dirs "admin/entrylist" $ dir (show EN) nullDir - guardSession acid - entryList acid EN - , do guardSession acid - dirs "admin/edit" $ path $ \(eId :: Integer) -> editEntry acid eId - , do guardSession acid - dirs "admin/updateentry" $ nullDir >> updateEntry acid - , do dir "admin" nullDir - guardSession acid - ok $ toResponse $ adminIndex ("tazjin" :: Text) - , dir "admin" $ ok $ toResponse adminLogin - , dir "dologin" $ processLogin acid - , do dirs "static/blogv40.css" nullDir - setHeaderM "content-type" "text/css" - setHeaderM "cache-control" "max-age=630720000" - setHeaderM "expires" "Tue, 20 Jan 2037 04:20:42 GMT" - ok $ toResponse blogStyle - , do setHeaderM "cache-control" "max-age=630720000" - setHeaderM "expires" "Tue, 20 Jan 2037 04:20:42 GMT" - dir "static" $ serveDirectory DisableBrowsing [] resDir - , serveDirectory DisableBrowsing [] resDir + msum [ path $ \(lang :: BlogLang) -> blogHandler acid lang + , dir "admin" $ msum [ + adminHandler acid -- this checks auth + , method GET >> (ok $ toResponse adminLogin) + , method POST >> processLogin acid ] + , dirs "static/blogv40.css" $ serveBlogStyle + , dir "static" $ staticHandler resDir + , blogHandler acid EN , notFound $ toResponse $ showError NotFound DE ] @@ -80,6 +57,30 @@ blogHandler acid lang = , notFound $ toResponse $ showError NotFound lang ] +staticHandler :: String -> ServerPart Response +staticHandler resDir = do + setHeaderM "cache-control" "max-age=630720000" + setHeaderM "expires" "Tue, 20 Jan 2037 04:20:42 GMT" + serveDirectory DisableBrowsing [] resDir + +serveBlogStyle :: ServerPart Response +serveBlogStyle = do + setHeaderM "content-type" "text/css" + setHeaderM "cache-control" "max-age=630720000" + setHeaderM "expires" "Tue, 20 Jan 2037 04:20:42 GMT" + ok $ toResponse $ blogStyle + +adminHandler :: AcidState Blog -> ServerPart Response +adminHandler acid = do + guardSession acid + msum [ dir "entry" $ method POST >> postEntry acid + , dir "entry" $ path $ \(entry :: Integer) -> msum [ + method GET >> editEntry acid entry + , method POST >> updateEntry acid entry ] + , dir "entrylist" $ path $ \(lang :: BlogLang) -> entryList acid lang + , ok $ toResponse $ adminIndex "tazjin" + ] + showEntry :: AcidState Blog -> BlogLang -> EntryId -> ServerPart Response showEntry acid lang eId = do entry <- query' acid (GetEntry eId) @@ -114,6 +115,7 @@ showRSS acid lang = do postEntry :: AcidState Blog -> ServerPart Response postEntry acid = do + nullDir decodeBody tmpPolicy now <- liftIO getCurrentTime let eId = timeToId now @@ -142,25 +144,22 @@ entryList acid lang = do ok $ toResponse $ adminEntryList entries editEntry :: AcidState Blog -> Integer -> ServerPart Response -editEntry acid i = do - (Just entry) <- query' acid (GetEntry eId) +editEntry acid entryId = do + (Just entry) <- query' acid (GetEntry $ EntryId entryId) ok $ toResponse $ editPage entry - where - eId = EntryId i -updateEntry :: AcidState Blog -> ServerPart Response -- TODO: Clean this up -updateEntry acid = do +updateEntry :: AcidState Blog -> Integer -> ServerPart Response +updateEntry acid entryId = do decodeBody tmpPolicy - (eId :: Integer) <- lookRead "eid" - (Just entry) <- query' acid (GetEntry $ EntryId eId) + (Just entry) <- query' acid (GetEntry $ EntryId entryId) nTitle <- lookText' "title" nBtext <- lookText' "btext" nMtext <- lookText' "mtext" - let nEntry = entry { title = nTitle - , btext = nBtext - , mtext = nMtext} - update' acid (UpdateEntry nEntry) - seeOther (concat $ intersperse' "/" [show $ lang entry, show eId]) + let newEntry = entry { title = nTitle + , btext = nBtext + , mtext = nMtext} + update' acid (UpdateEntry newEntry) + seeOther (concat $ intersperse' "/" [show $ lang entry, show entryId]) (toResponse ()) guardSession :: AcidState Blog -> ServerPartT IO () @@ -186,7 +185,7 @@ processLogin acid = do login <- query' acid (CheckUser (Username account) password) if login then createSession account - else ok $ toResponse adminLogin + else unauthorized $ toResponse adminLogin where createSession account = do now <- liftIO getCurrentTime -- cgit 1.4.1 From 3b3f6497bd30a2b6bfdf2ed6261b8cc4ac76f8da Mon Sep 17 00:00:00 2001 From: Vincent Ambo <vincent@kivra.com> Date: Sat, 21 Nov 2015 03:02:05 +0100 Subject: [blog] Always link title to / --- src/Blog.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Blog.hs b/src/Blog.hs index f4bdaa6986..cef163fa53 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -52,7 +52,7 @@ $doctype 5 <div .container > <div .row> <div .span12 .blogtitle> - <a class="btitle" href=#{append "/" (show' lang)}>#{blogTitle lang empty} + <a class="btitle" href="/">#{blogTitle lang empty} <div .row> <br> <div .span6> -- cgit 1.4.1 From 68824a41569cfe0eece74e0edaceeff25d421061 Mon Sep 17 00:00:00 2001 From: Vincent Ambo <vincent@kivra.com> Date: Sat, 21 Nov 2015 03:02:56 +0100 Subject: [all] Stylish Haskell import formatting --- src/Blog.hs | 30 +++++++++++++++--------------- src/Locales.hs | 2 +- src/RSS.hs | 16 ++++++++-------- src/Server.hs | 20 ++++++++++---------- 4 files changed, 34 insertions(+), 34 deletions(-) diff --git a/src/Blog.hs b/src/Blog.hs index cef163fa53..70adcc1ac7 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -1,21 +1,21 @@ module Blog where -import BlogDB -import Control.Monad (unless, when) -import Data.Data (Data, Typeable) -import Data.List (intersperse) -import Data.Maybe (fromJust) -import Data.Monoid (mempty) -import Data.Text (Text, append, empty, pack) -import Data.Text.Lazy (fromStrict) -import Data.Time -import Locales -import Text.Blaze.Html (preEscapedToHtml) -import Text.Hamlet -import Text.Lucius -import Text.Markdown +import BlogDB +import Control.Monad (unless, when) +import Data.Data (Data, Typeable) +import Data.List (intersperse) +import Data.Maybe (fromJust) +import Data.Monoid (mempty) +import Data.Text (Text, append, empty, pack) +import Data.Text.Lazy (fromStrict) +import Data.Time +import Locales +import Text.Blaze.Html (preEscapedToHtml) +import Text.Hamlet +import Text.Lucius +import Text.Markdown -import qualified Data.Text as T +import qualified Data.Text as T -- custom list functions intersperse' :: a -> [a] -> [a] diff --git a/src/Locales.hs b/src/Locales.hs index 2b578e094c..a05379d410 100644 --- a/src/Locales.hs +++ b/src/Locales.hs @@ -1,11 +1,11 @@ module Locales where +import BlogDB (BlogLang (..)) import Data.Data (Data, Typeable) import Data.Maybe (fromMaybe) import Data.Text (Text) import qualified Data.Text as T import Network.URI -import BlogDB (BlogLang (..)) {- to add a language simply define its abbreviation and Show instance then - translate the appropriate strings and add CouchDB views in Server.hs -} diff --git a/src/RSS.hs b/src/RSS.hs index 6a244129fe..34804cbf0a 100644 --- a/src/RSS.hs +++ b/src/RSS.hs @@ -1,15 +1,15 @@ module RSS (renderFeed) where -import qualified Data.Text as T +import qualified Data.Text as T -import Control.Monad (liftM) -import Data.Maybe (fromMaybe) -import Data.Time (UTCTime, getCurrentTime) -import Network.URI -import Text.RSS +import Control.Monad (liftM) +import Data.Maybe (fromMaybe) +import Data.Time (UTCTime, getCurrentTime) +import Network.URI +import Text.RSS -import BlogDB hiding (Title) -import Locales +import BlogDB hiding (Title) +import Locales createChannel :: BlogLang -> UTCTime -> [ChannelElem] createChannel l now = [ Language $ show l diff --git a/src/Server.hs b/src/Server.hs index 69eff3a78f..4eef611edc 100644 --- a/src/Server.hs +++ b/src/Server.hs @@ -2,19 +2,19 @@ module Server where -import Control.Applicative (optional, pure, (<$>), (<*>)) -import Control.Monad (liftM, msum, mzero, unless, when) -import Control.Monad.IO.Class (liftIO) -import Control.Monad.Reader (ask) +import Control.Applicative (optional, pure, (<$>), (<*>)) +import Control.Monad (liftM, msum, mzero, unless, when) +import Control.Monad.IO.Class (liftIO) +import Control.Monad.Reader (ask) import Data.Acid import Data.Acid.Advanced -import Data.ByteString.Char8 (ByteString, pack, unpack) -import Data.Char (toLower) -import Data.Maybe (fromJust) -import Data.Text (Text) -import qualified Data.Text as T +import Data.ByteString.Char8 (ByteString, pack, unpack) +import Data.Char (toLower) +import Data.Maybe (fromJust) +import Data.Text (Text) +import qualified Data.Text as T import Data.Time -import Happstack.Server hiding (Session) +import Happstack.Server hiding (Session) import Blog import BlogDB hiding (updateEntry) -- cgit 1.4.1 From c2fe73b02715f05cae8721d0e8474e6baa683adf Mon Sep 17 00:00:00 2001 From: Vincent Ambo <vincent@kivra.com> Date: Sat, 21 Nov 2015 03:04:43 +0100 Subject: [cabal] Enable warnings --- tazblog.cabal | 1 + 1 file changed, 1 insertion(+) diff --git a/tazblog.cabal b/tazblog.cabal index 9220241ba6..9e41ad5d7b 100644 --- a/tazblog.cabal +++ b/tazblog.cabal @@ -12,6 +12,7 @@ cabal-version: >= 1.10 library hs-source-dirs: src default-language: Haskell2010 + ghc-options: -W exposed-modules: Blog, BlogDB, Locales, Server, RSS build-depends: base, bytestring, -- cgit 1.4.1 From 77c376e283e5dd2bde32ab1f20555c28c4adad89 Mon Sep 17 00:00:00 2001 From: Vincent Ambo <vincent@kivra.com> Date: Sat, 21 Nov 2015 03:18:08 +0100 Subject: [all] Fix all warnings --- src/Blog.hs | 17 ++++++++++------- src/BlogDB.hs | 17 +++++++---------- src/Locales.hs | 45 +++++++++++++++++++++++++-------------------- src/Server.hs | 10 ++++------ 4 files changed, 46 insertions(+), 43 deletions(-) diff --git a/src/Blog.hs b/src/Blog.hs index 70adcc1ac7..602fe59894 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -1,11 +1,8 @@ module Blog where import BlogDB -import Control.Monad (unless, when) -import Data.Data (Data, Typeable) import Data.List (intersperse) import Data.Maybe (fromJust) -import Data.Monoid (mempty) import Data.Text (Text, append, empty, pack) import Data.Text.Lazy (fromStrict) import Data.Time @@ -60,7 +57,7 @@ $doctype 5 <div .container> ^{body} <footer .footer> - ^{showFooter lang $ pack version} + ^{showFooter $ pack version} |] where rssUrl = T.concat ["/", show' lang, "/rss.xml"] @@ -71,8 +68,8 @@ $doctype 5 <a class="link" href=#{twitter} target="_blank">Twitter |] -showFooter :: BlogLang -> Text -> Html -showFooter l v = [shamlet| +showFooter :: Text -> Html +showFooter v = [shamlet| <div .container> <div .row> <div .span12 .righttext style="text-align: right;margin-right:-200px"> @@ -271,4 +268,10 @@ showError NotFound l = blogTemplate l (T.append ": " $ notFoundTitle l) $ [shaml <div .span12 .notFoundText> #{notFoundText l} |] - +showError UnknownError l = blogTemplate l "" $ [shamlet| +<div .row .text-center> + <div .span12 .notFoundFace>:( +<div .row .text-center> + <div .span12 .notFoundText> + #{unknownErrorText l} +|] diff --git a/src/BlogDB.hs b/src/BlogDB.hs index 316c2fdc08..52e4e80c39 100644 --- a/src/BlogDB.hs +++ b/src/BlogDB.hs @@ -7,11 +7,9 @@ import Data.Acid.Advanced import Data.Acid.Remote import Data.ByteString (ByteString) import Data.Data (Data, Typeable) -import Data.IxSet (Indexable (..), IxSet (..), Proxy (..), getOne, ixFun, ixSet, (@=)) -import Data.List (insert) -import Data.SafeCopy (SafeCopy, base, deriveSafeCopy) +import Data.IxSet (Indexable (..), IxSet, Proxy (..), getOne, ixFun, ixSet, (@=)) +import Data.SafeCopy (base, deriveSafeCopy) import Data.Text (Text, pack) -import Data.Text.Lazy (toStrict) import Data.Time import Network (PortID (..)) import System.Environment (getEnv) @@ -20,7 +18,6 @@ import qualified Crypto.Hash.SHA512 as SHA (hash) import qualified Data.ByteString.Base64 as B64 (encode) import qualified Data.ByteString.Char8 as B import qualified Data.IxSet as IxSet -import qualified Data.Text as Text newtype EntryId = EntryId { unEntryId :: Integer } deriving (Eq, Ord, Data, Enum, Typeable) @@ -138,12 +135,12 @@ updateEntry e = getEntry :: EntryId -> Query Blog (Maybe Entry) getEntry eId = - do b@Blog{..} <- ask + do Blog{..} <- ask return $ getOne $ blogEntries @= eId latestEntries :: BlogLang -> Query Blog [Entry] latestEntries lang = - do b@Blog{..} <- ask + do Blog{..} <- ask return $ IxSet.toDescList (Proxy :: Proxy EDate) $ blogEntries @= lang addSession :: Session -> Update Blog Session @@ -154,7 +151,7 @@ addSession nSession = getSession :: SessionID -> Query Blog (Maybe Session) getSession sId = - do b@Blog{..} <- ask + do Blog{..} <- ask return $ getOne $ blogSessions @= sId clearSessions :: Update Blog [Session] @@ -172,12 +169,12 @@ addUser un pw = getUser :: Username -> Query Blog (Maybe User) getUser uN = - do b@Blog{..} <- ask + do Blog{..} <- ask return $ getOne $ blogUsers @= uN checkUser :: Username -> String -> Query Blog Bool checkUser uN pw = - do b@Blog{..} <- ask + do Blog{..} <- ask let user = getOne $ blogUsers @= uN case user of Nothing -> return False diff --git a/src/Locales.hs b/src/Locales.hs index a05379d410..e4ac9767c3 100644 --- a/src/Locales.hs +++ b/src/Locales.hs @@ -1,7 +1,6 @@ module Locales where import BlogDB (BlogLang (..)) -import Data.Data (Data, Typeable) import Data.Maybe (fromMaybe) import Data.Text (Text) import qualified Data.Text as T @@ -10,7 +9,7 @@ import Network.URI {- to add a language simply define its abbreviation and Show instance then - translate the appropriate strings and add CouchDB views in Server.hs -} -data BlogError = NotFound | DBError +data BlogError = NotFound | UnknownError version = "5.1-beta" @@ -37,31 +36,33 @@ getMonth l y m = T.append (monthName l m) $ T.pack $ show y where monthName :: BlogLang -> Int -> Text monthName DE m = case m of - 1 -> "Januar " - 2 -> "Februar " - 3 -> "März " - 4 -> "April " - 5 -> "Mai " - 6 -> "Juni " - 7 -> "Juli " - 8 -> "August " - 9 -> "September " + 1 -> "Januar " + 2 -> "Februar " + 3 -> "März " + 4 -> "April " + 5 -> "Mai " + 6 -> "Juni " + 7 -> "Juli " + 8 -> "August " + 9 -> "September " 10 -> "Oktober " 11 -> "November " 12 -> "Dezember " + _ -> "Unbekannt " monthName EN m = case m of - 1 -> "January " - 2 -> "February " - 3 -> "March " - 4 -> "April " - 5 -> "May " - 6 -> "June " - 7 -> "July " - 8 -> "August " - 9 -> "September " + 1 -> "January " + 2 -> "February " + 3 -> "March " + 4 -> "April " + 5 -> "May " + 6 -> "June " + 7 -> "July " + 8 -> "August " + 9 -> "September " 10 -> "October " 11 -> "November " 12 -> "December " + _ -> "Unknown " entireMonth :: BlogLang -> Text entireMonth DE = "Ganzer Monat" @@ -118,6 +119,10 @@ notFoundText :: BlogLang -> Text notFoundText DE = "Das gewünschte Objekt wurde leider nicht gefunden." notFoundText EN = "The requested object could not be found." +unknownErrorText :: BlogLang -> Text +unknownErrorText DE = "Ein unbekannter Fehler ist aufgetreten." +unknownErrorText EN = "An unknown error has occured." + -- static information repoURL :: Text = "http://hg.tazj.in/tazblog-haskell" mailTo :: Text = "mailto:tazjin+blog@gmail.com" diff --git a/src/Server.hs b/src/Server.hs index 4eef611edc..30cf422a83 100644 --- a/src/Server.hs +++ b/src/Server.hs @@ -2,15 +2,13 @@ module Server where -import Control.Applicative (optional, pure, (<$>), (<*>)) -import Control.Monad (liftM, msum, mzero, unless, when) +import Control.Applicative (optional) +import Control.Monad (msum, mzero, unless) import Control.Monad.IO.Class (liftIO) -import Control.Monad.Reader (ask) import Data.Acid import Data.Acid.Advanced -import Data.ByteString.Char8 (ByteString, pack, unpack) +import Data.ByteString.Char8 (unpack) import Data.Char (toLower) -import Data.Maybe (fromJust) import Data.Text (Text) import qualified Data.Text as T import Data.Time @@ -136,7 +134,7 @@ postEntry acid = do timeToId t = EntryId . read $ formatTime defaultTimeLocale "%s" t getLang :: String -> ServerPart BlogLang getLang "de" = return DE - getLang "en" = return EN + getLang _ = return EN -- English is default entryList :: AcidState Blog -> BlogLang -> ServerPart Response entryList acid lang = do -- cgit 1.4.1 From 6cc143a6ffdc19d14b623384c72701f1accc51fc Mon Sep 17 00:00:00 2001 From: Vincent Ambo <vincent@kivra.com> Date: Sat, 21 Nov 2015 03:18:46 +0100 Subject: [locales] Update repoURL --- src/Locales.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Locales.hs b/src/Locales.hs index e4ac9767c3..0ee5f0e535 100644 --- a/src/Locales.hs +++ b/src/Locales.hs @@ -124,6 +124,6 @@ unknownErrorText DE = "Ein unbekannter Fehler ist aufgetreten." unknownErrorText EN = "An unknown error has occured." -- static information -repoURL :: Text = "http://hg.tazj.in/tazblog-haskell" +repoURL :: Text = "https://bitbucket.org/tazjin/tazblog-haskell" mailTo :: Text = "mailto:tazjin+blog@gmail.com" twitter :: Text = "http://twitter.com/#!/tazjin" -- cgit 1.4.1 From cfea17dc0d69de8e5e2e27433a068d36efa2d1b1 Mon Sep 17 00:00:00 2001 From: Vincent Ambo <vincent@kivra.com> Date: Sat, 21 Nov 2015 03:20:04 +0100 Subject: [blog] Update acid-state URL --- src/Blog.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Blog.hs b/src/Blog.hs index 602fe59894..7d985c4008 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -76,7 +76,7 @@ showFooter v = [shamlet| Proudly made with # <a class="link" href="http://haskell.org">Haskell , # - <a class="link" href="http://hackage.haskell.org/package/acid-state-0.6.3">Acid-State + <a class="link" href="https://hackage.haskell.org/package/acid-state">Acid-State \ and without PHP, Java, Perl, MySQL and Python. <p> <a class="link" href=#{repoURL}>#{append "Version " v} -- cgit 1.4.1 From 9f33d98db59d6c5c1997ce55eb7be219089bd8b0 Mon Sep 17 00:00:00 2001 From: Vincent Ambo <vincent@kivra.com> Date: Sat, 21 Nov 2015 17:24:01 +0100 Subject: [blog] Implement new and simpler design --- res/blog.css | 35 +++++++++++ res/blog.lucius | 187 -------------------------------------------------------- src/Blog.hs | 152 ++++++++++++++++++--------------------------- src/Locales.hs | 8 +-- src/Server.hs | 3 +- 5 files changed, 98 insertions(+), 287 deletions(-) create mode 100644 res/blog.css delete mode 100644 res/blog.lucius diff --git a/res/blog.css b/res/blog.css new file mode 100644 index 0000000000..e6e4ae3c2b --- /dev/null +++ b/res/blog.css @@ -0,0 +1,35 @@ +body { + margin: 40px auto; + max-width: 650px; + line-height: 1.6; + font-size: 18px; + color: #383838; + padding: 0 10px +} +h1, h2, h3 { + line-height: 1.2 +} +.footer { + text-align: right; +} +.lod { + text-align: center; +} +.unstyled-link { + color: inherit; + text-decoration: none; +} +.uncoloured-link { + color: inherit; +} +.date { + text-align: right; + font-style: italic; + float: right; +} +.inline { + display: inline; +} +.navigation { + text-align: center; +} diff --git a/res/blog.lucius b/res/blog.lucius deleted file mode 100644 index 95df4eb793..0000000000 --- a/res/blog.lucius +++ /dev/null @@ -1,187 +0,0 @@ -@charset "UTF-8"; -/* CSS Document */ -@import url(http://fonts.googleapis.com/css?family=Droid+Sans+Mono); -@import url(http://fonts.googleapis.com/css?family=PT+Sans); - -body { - font-family: 'PT Sans', sans-serif; - min-height: 850px; - background: url(/static/bg.gif); -} - -html, body { - height: 100%; -} - -a { - color: black; -} - -article a, .entry a { - text-decoration: underline; -} - -#wrap { - min-height: 100%; - height: auto !important; - height: 100%; -} - -.readmore { - text-decoration: underline; -} - -.header { - color: #EEE; - background: url(/static/hbg.jpg); - z-index: 4; - padding-left: 20px; - padding-bottom: 30px; - padding-top: 30px; - position: relative; - box-shadow: 0 6px 5px 1px #343537; - margin-bottom: 25px; - padding-left: 0px; -} - -.footer { - background: url(/static/hbg.jpg); - height: 90; - z-index: 4; - position: relative; - background-color: #4A525A; - margin-top: 30px; - padding-top: 20px; - box-shadow: 0 -6px 5px 1px #343537; - color: #EEE; -} - - -.header a, .footer a { - color: #EEE; - text-decoration: none; -} - - -.boldify { - font-size:large; - font-weight:bold; -} - -.pusher { - padding-bottom: 20px; -} - -.cCaptcha { - padding: 5px; - border: 1px solid #555; - border-radius: 0.5em; - width: 606px; - background: #F9F9F9; -} - -.tt { - font-family: "courier new",courier,monospace; - font-size: 13px; -} - -.btitle { - text-decoration:none; - //color: #EEE; - font-size:x-large; - font-weight:bold; - margin-top: 15px; - padding-bottom: 25px; - padding-top: 20px; -} - -.contacts { - float: left; - font-weight: bolder; -} - -.righttext { - float:right; - padding-right: 20px; -} - -.notFoundFace { - height: 100px; - padding-top: 50px; - font-size:100px -} - -.notFoundText { - font-size:24px; - font-weight:bold -} - -/* HsColour style */ - -.code -{ - box-shadow: 3px 3px 5px 1px #888; - border-radius: 10px; - padding: 0.75em; - - font-size: 11pt; - width: 60em; - color: white; - line-height: 1.2em; - font-family: 'Droid Sans Mono', sans-serif; - background: black; - background-image:url('/static/cbg.jpg'); - background-repeat: no-repeat; - } - -.code pre -{ - - font-family: 'Droid Sans Mono', sans-serif; -} - -kbd -{ - font-family: 'Droid Sans Mono', sans-serif; - color: #333; - font-size: 0.8em; -} - -.wide -{ - width: 90em; -} - - -code -{ - line-height: 1.5em; - border: 1px; - } - -.source-code -{ - font-size: 0.75em; - color: #666; - } - -.warning -{ - color: red; - } - - -.hs-keyglyph { color: DarkGoldenrod; } -.hs-layout { color: white;} -.hs-keyword { color: skyblue; } -.hs-comment, .hs-comment a { color: cadetblue;} -.hs-str { color: Darkorange; } -.hs-chr { color: RosyBrown;} -.hs-conid { color: GreenYellow; } -.hs-varid { color: white; } -.hs-num { color: white; } -.hs-varop { color: DarkGoldenrod; } -.hs-conop { color: DarkGoldenrod; } -.hs-sel { color: FireBrick; } -.hs-cpp { color: yellow; } -.hs-definition { color: gold; } diff --git a/src/Blog.hs b/src/Blog.hs index 7d985c4008..97c7fa9bc9 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -28,61 +28,39 @@ show' = pack . show markdownCutoff :: UTCTime markdownCutoff = fromJust $ parseTimeM False defaultTimeLocale "%s" "1367149834" - --- blog CSS (admin is still static) -stylesheetSource = $(luciusFile "res/blog.lucius") -blogStyle = renderCssUrl undefined stylesheetSource - -- blog HTML blogTemplate :: BlogLang -> Text -> Html -> Html blogTemplate lang t_append body = [shamlet| $doctype 5 <head> - <title>#{blogTitle lang t_append} - <link rel="stylesheet" type="text/css" href="/static/bootstrap.css" media="all"> - <link rel="stylesheet" type="text/css" href="/static/blogv40.css" media="all"> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <link rel="stylesheet" type="text/css" href="/static/blog.css" media="all"> <link rel="alternate" type="application/rss+xml" title="RSS-Feed" href=#{rssUrl}> - <meta http-equiv="content-type" content="text/html;charset=UTF-8"> + <title>#{blogTitle lang t_append} <body> - <div #wrap> - <div .header> - <div .container > - <div .row> - <div .span12 .blogtitle> - <a class="btitle" href="/">#{blogTitle lang empty} - <div .row> - <br> - <div .span6> - <span .contacts #cosx>^{contactInfo} - <div .container> - ^{body} - <footer .footer> - ^{showFooter $ pack version} + <header> + <h1> + <a href="/" .unstyled-link>#{blogTitle lang empty} + <hr> + ^{body} + ^{showFooter} |] where rssUrl = T.concat ["/", show' lang, "/rss.xml"] - contactInfo = [shamlet| -#{contactText lang} -<a class="link" href=#{mailTo}>Mail -#{orText lang} -<a class="link" href=#{twitter} target="_blank">Twitter -|] -showFooter :: Text -> Html -showFooter v = [shamlet| -<div .container> - <div .row> - <div .span12 .righttext style="text-align: right;margin-right:-200px"> - Proudly made with # - <a class="link" href="http://haskell.org">Haskell - , # - <a class="link" href="https://hackage.haskell.org/package/acid-state">Acid-State - \ and without PHP, Java, Perl, MySQL and Python. - <p> - <a class="link" href=#{repoURL}>#{append "Version " v} - <div .row .text-center> - <div .span12> - <span style="font-size:13px;font-family:Helvetica;">ಠ_ಠ +showFooter :: Html +showFooter = [shamlet| +<footer> + <p .footer>Served without any dynamic languages. + <p .footer> + <a href=#{repoURL} .uncoloured-link>Version #{version} + | + <a href=#{twitter} .uncoloured-link>Twitter + | + <a href=#{mailTo} .uncoloured-link>Mail + <p .lod> + ಠ_ಠ |] isEntryMarkdown :: Entry -> Bool @@ -91,77 +69,63 @@ isEntryMarkdown e = edate e > markdownCutoff renderEntryMarkdown :: Text -> Html renderEntryMarkdown = markdown def {msXssProtect = False} . fromStrict -renderEntries :: Bool -> [Entry] -> Text -> Maybe Html -> Html -renderEntries showAll entries topText footerLinks = [shamlet| -<div .row> - <div .span12> - <p> - <span class="innerTitle"> - <b>#{topText} -$forall entry <- elist - <div .row> - <div .span2> - <a href=#{linkElems entry}> - <b>#{title entry} - <br> - <i>#{pack $ formatTime defaultTimeLocale "%Y-%m-%d" $ edate entry} - <div .span10 .entry> - $if (isEntryMarkdown entry) - ^{renderEntryMarkdown $ append " " $ btext entry} - $else - ^{preEscapedToHtml $ append " " $ btext entry} - $if ((/=) (mtext entry) empty) - <p> - <a .readmore href=#{linkElems entry}>#{readMore $ lang entry} - $else - <br>  -$maybe links <- footerLinks +renderEntries :: Bool -> [Entry] -> Maybe Html -> Html +renderEntries showAll entries pageLinks = [shamlet| +$forall entry <- toDisplay + <article> + <h2> + <a href=#{linkElems entry} .unstyled-link> + #{title entry} + <aside .date> + #{pack $ formatTime defaultTimeLocale "%Y-%m-%d" $ edate entry} + $if (isEntryMarkdown entry) + ^{renderEntryMarkdown $ btext entry} + $else + ^{preEscapedToHtml $ btext entry} + $if ((/=) (mtext entry) empty) + <a .uncoloured-link href=#{linkElems entry}> + #{readMore $ lang entry} + <hr> +$maybe links <- pageLinks ^{links} |] where - elist = if' showAll entries (take 6 entries) + toDisplay = if' showAll entries (take 6 entries) linkElems Entry{..} = concat $ intersperse' "/" [show lang, show entryId] showLinks :: Maybe Int -> BlogLang -> Html showLinks (Just i) lang = [shamlet| $if ((>) i 1) - <div .row .text-center> - <div .span12> - <a href=#{nLink $ succ i}>#{backText lang} - \ -- # - <a href=#{nLink $ pred i}>#{nextText lang} + <div .navigation> + <a href=#{nLink $ succ i} .uncoloured-link>#{backText lang} + | + <a href=#{nLink $ pred i} .uncoloured-link>#{nextText lang} $elseif ((<=) i 1) ^{showLinks Nothing lang} |] where nLink page = T.concat ["/", show' lang, "/?page=", show' page] showLinks Nothing lang = [shamlet| -<div .row .text-center> - <div .span12> - <a href=#{nLink}>#{backText lang} +<div .navigation> + <a href=#{nLink} .uncoloured-link>#{backText lang} |] where nLink = T.concat ["/", show' lang, "/?page=2"] renderEntry :: Entry -> Html renderEntry e@Entry{..} = [shamlet| -<div .row .pusher> - <div .span9> - <span .boldify>#{title} - <div .span3> - <span .righttext><i>#{woText}</i> -<div .row .innerContainer> - <div .span10> - <article> - $if (isEntryMarkdown e) - ^{renderEntryMarkdown btext} - <p>^{renderEntryMarkdown $ mtext} - $else - ^{preEscapedToHtml $ btext} - <p>^{preEscapedToHtml $ mtext} +<article> + <h2> + #{title} + <aside .date> + #{pack $ formatTime defaultTimeLocale "%Y-%m-%d" edate} + $if (isEntryMarkdown e) + ^{renderEntryMarkdown btext} + <p>^{renderEntryMarkdown $ mtext} + $else + ^{preEscapedToHtml $ btext} +<hr> |] - where - woText = flip T.append author $ T.pack $ formatTime defaultTimeLocale (eTimeFormat lang) edate {- Administration pages -} diff --git a/src/Locales.hs b/src/Locales.hs index 0ee5f0e535..00b2b871d1 100644 --- a/src/Locales.hs +++ b/src/Locales.hs @@ -20,8 +20,8 @@ if' True x _ = x if' False _ y = y blogTitle :: BlogLang -> Text -> Text -blogTitle DE s = T.concat ["Tazjins Blog", s] -blogTitle EN s = T.concat ["Tazjin's Blog", s] +blogTitle DE s = T.concat ["Tazjins blog", s] +blogTitle EN s = T.concat ["Tazjin's blog", s] showLangText :: BlogLang -> Text showLangText EN = "en" @@ -77,8 +77,8 @@ nextText DE = "Später" nextText EN = "Later" readMore :: BlogLang -> Text -readMore DE = "Weiterlesen" -readMore EN = "Read more" +readMore DE = "[Weiterlesen]" +readMore EN = "[Read more]" eTimeFormat :: BlogLang -> String eTimeFormat DE = "Geschrieben am %Y-%m-%d von " diff --git a/src/Server.hs b/src/Server.hs index 30cf422a83..6996583f5d 100644 --- a/src/Server.hs +++ b/src/Server.hs @@ -40,7 +40,6 @@ tazBlog acid resDir = do adminHandler acid -- this checks auth , method GET >> (ok $ toResponse adminLogin) , method POST >> processLogin acid ] - , dirs "static/blogv40.css" $ serveBlogStyle , dir "static" $ staticHandler resDir , blogHandler acid EN , notFound $ toResponse $ showError NotFound DE @@ -96,7 +95,7 @@ showIndex acid lang = do entries <- query' acid (LatestEntries lang) (page :: Maybe Int) <- optional $ lookRead "page" ok $ toResponse $ blogTemplate lang "" $ - renderEntries False (eDrop page entries) (topText lang) (Just $ showLinks page lang) + renderEntries False (eDrop page entries) (Just $ showLinks page lang) where eDrop :: Maybe Int -> [a] -> [a] eDrop (Just i) = drop ((i-1) * 6) -- cgit 1.4.1 From 6201e24c0bcd45c7ee8916127dab8e25aae78050 Mon Sep 17 00:00:00 2001 From: Vincent Ambo <vincent@kivra.com> Date: Sat, 21 Nov 2015 17:25:07 +0100 Subject: [res] Remove some unnecessary files --- res/admin.css | 5 +- res/bootstrap.css | 6155 -------------------------------------------------- res/bootstrap.min.js | 6 - 3 files changed, 2 insertions(+), 6164 deletions(-) delete mode 100644 res/bootstrap.css delete mode 100644 res/bootstrap.min.js diff --git a/res/admin.css b/res/admin.css index 2eb375dd14..10980dc9e4 100644 --- a/res/admin.css +++ b/res/admin.css @@ -1,7 +1,6 @@ @charset "UTF-8"; /* CSS Document */ - body { padding-top: 20px; font-family: 'PT Sans', sans-serif; @@ -19,7 +18,7 @@ body { background-color: rgb(245,245,245); } -.loginBox { +.loginBox { width: 400px; margin: 0 auto; } @@ -31,7 +30,7 @@ body { font-size: 12px; padding-left: 20px; padding-top: 11px; - background: url(/res/loginBoxTop.png); + background: url(/static/loginBoxTop.png); } .loginBoxMiddle { diff --git a/res/bootstrap.css b/res/bootstrap.css deleted file mode 100644 index 5233f1312d..0000000000 --- a/res/bootstrap.css +++ /dev/null @@ -1,6155 +0,0 @@ -/*! - * Bootstrap v2.3.1 - * - * Copyright 2012 Twitter, Inc - * Licensed under the Apache License v2.0 - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Designed and built with all the love in the world @twitter by @mdo and @fat. - */ - -.clearfix { - *zoom: 1; -} - -.clearfix:before, -.clearfix:after { - display: table; - line-height: 0; - content: ""; -} - -.clearfix:after { - clear: both; -} - -.hide-text { - font: 0/0 a; - color: transparent; - text-shadow: none; - background-color: transparent; - border: 0; -} - -.input-block-level { - display: block; - width: 100%; - min-height: 30px; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; -} - -article, -aside, -details, -figcaption, -figure, -footer, -header, -hgroup, -nav, -section { - display: block; -} - -audio, -canvas, -video { - display: inline-block; - *display: inline; - *zoom: 1; -} - -audio:not([controls]) { - display: none; -} - -html { - font-size: 100%; - -webkit-text-size-adjust: 100%; - -ms-text-size-adjust: 100%; -} - -a:focus { - outline: thin dotted #333; - outline: 5px auto -webkit-focus-ring-color; - outline-offset: -2px; -} - -a:hover, -a:active { - outline: 0; -} - -sub, -sup { - position: relative; - font-size: 75%; - line-height: 0; - vertical-align: baseline; -} - -sup { - top: -0.5em; -} - -sub { - bottom: -0.25em; -} - -img { - width: auto\9; - height: auto; - max-width: 100%; - vertical-align: middle; - border: 0; - -ms-interpolation-mode: bicubic; -} - -#map_canvas img, -.google-maps img { - max-width: none; -} - -button, -input, -select, -textarea { - margin: 0; - font-size: 100%; - vertical-align: middle; -} - -button, -input { - *overflow: visible; - line-height: normal; -} - -button::-moz-focus-inner, -input::-moz-focus-inner { - padding: 0; - border: 0; -} - -button, -html input[type="button"], -input[type="reset"], -input[type="submit"] { - cursor: pointer; - -webkit-appearance: button; -} - -label, -select, -button, -input[type="button"], -input[type="reset"], -input[type="submit"], -input[type="radio"], -input[type="checkbox"] { - cursor: pointer; -} - -input[type="search"] { - -webkit-box-sizing: content-box; - -moz-box-sizing: content-box; - box-sizing: content-box; - -webkit-appearance: textfield; -} - -input[type="search"]::-webkit-search-decoration, -input[type="search"]::-webkit-search-cancel-button { - -webkit-appearance: none; -} - -textarea { - overflow: auto; - vertical-align: top; -} - -@media print { - * { - color: #000 !important; - text-shadow: none !important; - background: transparent !important; - box-shadow: none !important; - } - a, - a:visited { - text-decoration: underline; - } - a[href]:after { - content: " (" attr(href) ")"; - } - abbr[title]:after { - content: " (" attr(title) ")"; - } - .ir a:after, - a[href^="javascript:"]:after, - a[href^="#"]:after { - content: ""; - } - pre, - blockquote { - border: 1px solid #999; - page-break-inside: avoid; - } - thead { - display: table-header-group; - } - tr, - img { - page-break-inside: avoid; - } - img { - max-width: 100% !important; - } - @page { - margin: 0.5cm; - } - p, - h2, - h3 { - orphans: 3; - widows: 3; - } - h2, - h3 { - page-break-after: avoid; - } -} - -body { - margin: 0; - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - font-size: 14px; - line-height: 20px; - color: #333333; - background-color: #ffffff; -} - -a { - color: #0088cc; - text-decoration: none; -} - -a:hover, -a:focus { - color: #005580; - text-decoration: underline; -} - -.img-rounded { - -webkit-border-radius: 6px; - -moz-border-radius: 6px; - border-radius: 6px; -} - -.img-polaroid { - padding: 4px; - background-color: #fff; - border: 1px solid #ccc; - border: 1px solid rgba(0, 0, 0, 0.2); - -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); - -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); -} - -.img-circle { - -webkit-border-radius: 500px; - -moz-border-radius: 500px; - border-radius: 500px; -} - -.row { - margin-left: -20px; - *zoom: 1; -} - -.row:before, -.row:after { - display: table; - line-height: 0; - content: ""; -} - -.row:after { - clear: both; -} - -[class*="span"] { - float: left; - min-height: 1px; - margin-left: 20px; -} - -.container, -.navbar-static-top .container, -.navbar-fixed-top .container, -.navbar-fixed-bottom .container { - width: 940px; -} - -.span12 { - width: 940px; -} - -.span11 { - width: 860px; -} - -.span10 { - width: 780px; -} - -.span9 { - width: 700px; -} - -.span8 { - width: 620px; -} - -.span7 { - width: 540px; -} - -.span6 { - width: 460px; -} - -.span5 { - width: 380px; -} - -.span4 { - width: 300px; -} - -.span3 { - width: 220px; -} - -.span2 { - width: 140px; -} - -.span1 { - width: 60px; -} - -.offset12 { - margin-left: 980px; -} - -.offset11 { - margin-left: 900px; -} - -.offset10 { - margin-left: 820px; -} - -.offset9 { - margin-left: 740px; -} - -.offset8 { - margin-left: 660px; -} - -.offset7 { - margin-left: 580px; -} - -.offset6 { - margin-left: 500px; -} - -.offset5 { - margin-left: 420px; -} - -.offset4 { - margin-left: 340px; -} - -.offset3 { - margin-left: 260px; -} - -.offset2 { - margin-left: 180px; -} - -.offset1 { - margin-left: 100px; -} - -.row-fluid { - width: 100%; - *zoom: 1; -} - -.row-fluid:before, -.row-fluid:after { - display: table; - line-height: 0; - content: ""; -} - -.row-fluid:after { - clear: both; -} - -.row-fluid [class*="span"] { - display: block; - float: left; - width: 100%; - min-height: 30px; - margin-left: 2.127659574468085%; - *margin-left: 2.074468085106383%; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; -} - -.row-fluid [class*="span"]:first-child { - margin-left: 0; -} - -.row-fluid .controls-row [class*="span"] + [class*="span"] { - margin-left: 2.127659574468085%; -} - -.row-fluid .span12 { - width: 100%; - *width: 99.94680851063829%; -} - -.row-fluid .span11 { - width: 91.48936170212765%; - *width: 91.43617021276594%; -} - -.row-fluid .span10 { - width: 82.97872340425532%; - *width: 82.92553191489361%; -} - -.row-fluid .span9 { - width: 74.46808510638297%; - *width: 74.41489361702126%; -} - -.row-fluid .span8 { - width: 65.95744680851064%; - *width: 65.90425531914893%; -} - -.row-fluid .span7 { - width: 57.44680851063829%; - *width: 57.39361702127659%; -} - -.row-fluid .span6 { - width: 48.93617021276595%; - *width: 48.88297872340425%; -} - -.row-fluid .span5 { - width: 40.42553191489362%; - *width: 40.37234042553192%; -} - -.row-fluid .span4 { - width: 31.914893617021278%; - *width: 31.861702127659576%; -} - -.row-fluid .span3 { - width: 23.404255319148934%; - *width: 23.351063829787233%; -} - -.row-fluid .span2 { - width: 14.893617021276595%; - *width: 14.840425531914894%; -} - -.row-fluid .span1 { - width: 6.382978723404255%; - *width: 6.329787234042553%; -} - -.row-fluid .offset12 { - margin-left: 104.25531914893617%; - *margin-left: 104.14893617021275%; -} - -.row-fluid .offset12:first-child { - margin-left: 102.12765957446808%; - *margin-left: 102.02127659574467%; -} - -.row-fluid .offset11 { - margin-left: 95.74468085106382%; - *margin-left: 95.6382978723404%; -} - -.row-fluid .offset11:first-child { - margin-left: 93.61702127659574%; - *margin-left: 93.51063829787232%; -} - -.row-fluid .offset10 { - margin-left: 87.23404255319149%; - *margin-left: 87.12765957446807%; -} - -.row-fluid .offset10:first-child { - margin-left: 85.1063829787234%; - *margin-left: 84.99999999999999%; -} - -.row-fluid .offset9 { - margin-left: 78.72340425531914%; - *margin-left: 78.61702127659572%; -} - -.row-fluid .offset9:first-child { - margin-left: 76.59574468085106%; - *margin-left: 76.48936170212764%; -} - -.row-fluid .offset8 { - margin-left: 70.2127659574468%; - *margin-left: 70.10638297872339%; -} - -.row-fluid .offset8:first-child { - margin-left: 68.08510638297872%; - *margin-left: 67.9787234042553%; -} - -.row-fluid .offset7 { - margin-left: 61.70212765957446%; - *margin-left: 61.59574468085106%; -} - -.row-fluid .offset7:first-child { - margin-left: 59.574468085106375%; - *margin-left: 59.46808510638297%; -} - -.row-fluid .offset6 { - margin-left: 53.191489361702125%; - *margin-left: 53.085106382978715%; -} - -.row-fluid .offset6:first-child { - margin-left: 51.063829787234035%; - *margin-left: 50.95744680851063%; -} - -.row-fluid .offset5 { - margin-left: 44.68085106382979%; - *margin-left: 44.57446808510638%; -} - -.row-fluid .offset5:first-child { - margin-left: 42.5531914893617%; - *margin-left: 42.4468085106383%; -} - -.row-fluid .offset4 { - margin-left: 36.170212765957444%; - *margin-left: 36.06382978723405%; -} - -.row-fluid .offset4:first-child { - margin-left: 34.04255319148936%; - *margin-left: 33.93617021276596%; -} - -.row-fluid .offset3 { - margin-left: 27.659574468085104%; - *margin-left: 27.5531914893617%; -} - -.row-fluid .offset3:first-child { - margin-left: 25.53191489361702%; - *margin-left: 25.425531914893618%; -} - -.row-fluid .offset2 { - margin-left: 19.148936170212764%; - *margin-left: 19.04255319148936%; -} - -.row-fluid .offset2:first-child { - margin-left: 17.02127659574468%; - *margin-left: 16.914893617021278%; -} - -.row-fluid .offset1 { - margin-left: 10.638297872340425%; - *margin-left: 10.53191489361702%; -} - -.row-fluid .offset1:first-child { - margin-left: 8.51063829787234%; - *margin-left: 8.404255319148938%; -} - -[class*="span"].hide, -.row-fluid [class*="span"].hide { - display: none; -} - -[class*="span"].pull-right, -.row-fluid [class*="span"].pull-right { - float: right; -} - -.container { - margin-right: auto; - margin-left: auto; - *zoom: 1; -} - -.container:before, -.container:after { - display: table; - line-height: 0; - content: ""; -} - -.container:after { - clear: both; -} - -.container-fluid { - padding-right: 20px; - padding-left: 20px; - *zoom: 1; -} - -.container-fluid:before, -.container-fluid:after { - display: table; - line-height: 0; - content: ""; -} - -.container-fluid:after { - clear: both; -} - -p { - margin: 0 0 10px; -} - -.lead { - margin-bottom: 20px; - font-size: 21px; - font-weight: 200; - line-height: 30px; -} - -small { - font-size: 85%; -} - -strong { - font-weight: bold; -} - -em { - font-style: italic; -} - -cite { - font-style: normal; -} - -.muted { - color: #999999; -} - -a.muted:hover, -a.muted:focus { - color: #808080; -} - -.text-warning { - color: #c09853; -} - -a.text-warning:hover, -a.text-warning:focus { - color: #a47e3c; -} - -.text-error { - color: #b94a48; -} - -a.text-error:hover, -a.text-error:focus { - color: #953b39; -} - -.text-info { - color: #3a87ad; -} - -a.text-info:hover, -a.text-info:focus { - color: #2d6987; -} - -.text-success { - color: #468847; -} - -a.text-success:hover, -a.text-success:focus { - color: #356635; -} - -.text-left { - text-align: left; -} - -.text-right { - text-align: right; -} - -.text-center { - text-align: center; -} - -h1, -h2, -h3, -h4, -h5, -h6 { - margin: 10px 0; - font-family: inherit; - font-weight: bold; - line-height: 20px; - color: inherit; - text-rendering: optimizelegibility; -} - -h1 small, -h2 small, -h3 small, -h4 small, -h5 small, -h6 small { - font-weight: normal; - line-height: 1; - color: #999999; -} - -h1, -h2, -h3 { - line-height: 40px; -} - -h1 { - font-size: 38.5px; -} - -h2 { - font-size: 31.5px; -} - -h3 { - font-size: 24.5px; -} - -h4 { - font-size: 17.5px; -} - -h5 { - font-size: 14px; -} - -h6 { - font-size: 11.9px; -} - -h1 small { - font-size: 24.5px; -} - -h2 small { - font-size: 17.5px; -} - -h3 small { - font-size: 14px; -} - -h4 small { - font-size: 14px; -} - -.page-header { - padding-bottom: 9px; - margin: 20px 0 30px; - border-bottom: 1px solid #eeeeee; -} - -ul, -ol { - padding: 0; - margin: 0 0 10px 25px; -} - -ul ul, -ul ol, -ol ol, -ol ul { - margin-bottom: 0; -} - -li { - line-height: 20px; -} - -ul.unstyled, -ol.unstyled { - margin-left: 0; - list-style: none; -} - -ul.inline, -ol.inline { - margin-left: 0; - list-style: none; -} - -ul.inline > li, -ol.inline > li { - display: inline-block; - *display: inline; - padding-right: 5px; - padding-left: 5px; - *zoom: 1; -} - -dl { - margin-bottom: 20px; -} - -dt, -dd { - line-height: 20px; -} - -dt { - font-weight: bold; -} - -dd { - margin-left: 10px; -} - -.dl-horizontal { - *zoom: 1; -} - -.dl-horizontal:before, -.dl-horizontal:after { - display: table; - line-height: 0; - content: ""; -} - -.dl-horizontal:after { - clear: both; -} - -.dl-horizontal dt { - float: left; - width: 160px; - overflow: hidden; - clear: left; - text-align: right; - text-overflow: ellipsis; - white-space: nowrap; -} - -.dl-horizontal dd { - margin-left: 180px; -} - -hr { - margin: 20px 0; - border: 0; - border-top: 1px solid #eeeeee; - border-bottom: 1px solid #ffffff; -} - -abbr[title], -abbr[data-original-title] { - cursor: help; - border-bottom: 1px dotted #999999; -} - -abbr.initialism { - font-size: 90%; - text-transform: uppercase; -} - -blockquote { - padding: 0 0 0 15px; - margin: 0 0 20px; - border-left: 5px solid #eeeeee; -} - -blockquote p { - margin-bottom: 0; - font-size: 17.5px; - font-weight: 300; - line-height: 1.25; -} - -blockquote small { - display: block; - line-height: 20px; - color: #999999; -} - -blockquote small:before { - content: '\2014 \00A0'; -} - -blockquote.pull-right { - float: right; - padding-right: 15px; - padding-left: 0; - border-right: 5px solid #eeeeee; - border-left: 0; -} - -blockquote.pull-right p, -blockquote.pull-right small { - text-align: right; -} - -blockquote.pull-right small:before { - content: ''; -} - -blockquote.pull-right small:after { - content: '\00A0 \2014'; -} - -q:before, -q:after, -blockquote:before, -blockquote:after { - content: ""; -} - -address { - display: block; - margin-bottom: 20px; - font-style: normal; - line-height: 20px; -} - -code, -pre { - padding: 0 3px 2px; - font-family: Monaco, Menlo, Consolas, "Courier New", monospace; - font-size: 12px; - color: #333333; - -webkit-border-radius: 3px; - -moz-border-radius: 3px; - border-radius: 3px; -} - -code { - padding: 2px 4px; - color: #d14; - white-space: nowrap; - background-color: #f7f7f9; - border: 1px solid #e1e1e8; -} - -pre { - display: block; - padding: 9.5px; - margin: 0 0 10px; - font-size: 13px; - line-height: 20px; - word-break: break-all; - word-wrap: break-word; - white-space: pre; - white-space: pre-wrap; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; -} - -pre.prettyprint { - margin-bottom: 20px; -} - -pre code { - padding: 0; - color: inherit; - white-space: pre; - white-space: pre-wrap; - background-color: transparent; - border: 0; -} - -.pre-scrollable { - max-height: 340px; - overflow-y: scroll; -} - -form { - margin: 0 0 20px; -} - -fieldset { - padding: 0; - margin: 0; - border: 0; -} - -legend { - display: block; - width: 100%; - padding: 0; - margin-bottom: 20px; - font-size: 21px; - line-height: 40px; - color: #333333; - border: 0; - border-bottom: 1px solid #e5e5e5; -} - -legend small { - font-size: 15px; - color: #999999; -} - -label, -input, -button, -select, -textarea { - font-size: 14px; - font-weight: normal; - line-height: 20px; -} - -input, -button, -select, -textarea { - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; -} - -label { - display: block; - margin-bottom: 5px; -} - -select, -textarea, -input[type="text"], -input[type="password"], -input[type="datetime"], -input[type="datetime-local"], -input[type="date"], -input[type="month"], -input[type="time"], -input[type="week"], -input[type="number"], -input[type="email"], -input[type="url"], -input[type="search"], -input[type="tel"], -input[type="color"], -.uneditable-input { - display: inline-block; - height: 20px; - padding: 4px 6px; - margin-bottom: 10px; - font-size: 14px; - line-height: 20px; - color: #555555; - vertical-align: middle; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; -} - -input, -textarea, -.uneditable-input { - width: 206px; -} - -textarea { - height: auto; -} - -textarea, -input[type="text"], -input[type="password"], -input[type="datetime"], -input[type="datetime-local"], -input[type="date"], -input[type="month"], -input[type="time"], -input[type="week"], -input[type="number"], -input[type="email"], -input[type="url"], -input[type="search"], -input[type="tel"], -input[type="color"], -.uneditable-input { - background-color: #ffffff; - border: 1px solid #cccccc; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - -webkit-transition: border linear 0.2s, box-shadow linear 0.2s; - -moz-transition: border linear 0.2s, box-shadow linear 0.2s; - -o-transition: border linear 0.2s, box-shadow linear 0.2s; - transition: border linear 0.2s, box-shadow linear 0.2s; -} - -textarea:focus, -input[type="text"]:focus, -input[type="password"]:focus, -input[type="datetime"]:focus, -input[type="datetime-local"]:focus, -input[type="date"]:focus, -input[type="month"]:focus, -input[type="time"]:focus, -input[type="week"]:focus, -input[type="number"]:focus, -input[type="email"]:focus, -input[type="url"]:focus, -input[type="search"]:focus, -input[type="tel"]:focus, -input[type="color"]:focus, -.uneditable-input:focus { - border-color: rgba(82, 168, 236, 0.8); - outline: 0; - outline: thin dotted \9; - /* IE6-9 */ - - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); - -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); -} - -input[type="radio"], -input[type="checkbox"] { - margin: 4px 0 0; - margin-top: 1px \9; - *margin-top: 0; - line-height: normal; -} - -input[type="file"], -input[type="image"], -input[type="submit"], -input[type="reset"], -input[type="button"], -input[type="radio"], -input[type="checkbox"] { - width: auto; -} - -select, -input[type="file"] { - height: 30px; - /* In IE7, the height of the select element cannot be changed by height, only font-size */ - - *margin-top: 4px; - /* For IE7, add top margin to align select with labels */ - - line-height: 30px; -} - -select { - width: 220px; - background-color: #ffffff; - border: 1px solid #cccccc; -} - -select[multiple], -select[size] { - height: auto; -} - -select:focus, -input[type="file"]:focus, -input[type="radio"]:focus, -input[type="checkbox"]:focus { - outline: thin dotted #333; - outline: 5px auto -webkit-focus-ring-color; - outline-offset: -2px; -} - -.uneditable-input, -.uneditable-textarea { - color: #999999; - cursor: not-allowed; - background-color: #fcfcfc; - border-color: #cccccc; - -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); - -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); - box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); -} - -.uneditable-input { - overflow: hidden; - white-space: nowrap; -} - -.uneditable-textarea { - width: auto; - height: auto; -} - -input:-moz-placeholder, -textarea:-moz-placeholder { - color: #999999; -} - -input:-ms-input-placeholder, -textarea:-ms-input-placeholder { - color: #999999; -} - -input::-webkit-input-placeholder, -textarea::-webkit-input-placeholder { - color: #999999; -} - -.radio, -.checkbox { - min-height: 20px; - padding-left: 20px; -} - -.radio input[type="radio"], -.checkbox input[type="checkbox"] { - float: left; - margin-left: -20px; -} - -.controls > .radio:first-child, -.controls > .checkbox:first-child { - padding-top: 5px; -} - -.radio.inline, -.checkbox.inline { - display: inline-block; - padding-top: 5px; - margin-bottom: 0; - vertical-align: middle; -} - -.radio.inline + .radio.inline, -.checkbox.inline + .checkbox.inline { - margin-left: 10px; -} - -.input-mini { - width: 60px; -} - -.input-small { - width: 90px; -} - -.input-medium { - width: 150px; -} - -.input-large { - width: 210px; -} - -.input-xlarge { - width: 270px; -} - -.input-xxlarge { - width: 530px; -} - -input[class*="span"], -select[class*="span"], -textarea[class*="span"], -.uneditable-input[class*="span"], -.row-fluid input[class*="span"], -.row-fluid select[class*="span"], -.row-fluid textarea[class*="span"], -.row-fluid .uneditable-input[class*="span"] { - float: none; - margin-left: 0; -} - -.input-append input[class*="span"], -.input-append .uneditable-input[class*="span"], -.input-prepend input[class*="span"], -.input-prepend .uneditable-input[class*="span"], -.row-fluid input[class*="span"], -.row-fluid select[class*="span"], -.row-fluid textarea[class*="span"], -.row-fluid .uneditable-input[class*="span"], -.row-fluid .input-prepend [class*="span"], -.row-fluid .input-append [class*="span"] { - display: inline-block; -} - -input, -textarea, -.uneditable-input { - margin-left: 0; -} - -.controls-row [class*="span"] + [class*="span"] { - margin-left: 20px; -} - -input.span12, -textarea.span12, -.uneditable-input.span12 { - width: 926px; -} - -input.span11, -textarea.span11, -.uneditable-input.span11 { - width: 846px; -} - -input.span10, -textarea.span10, -.uneditable-input.span10 { - width: 766px; -} - -input.span9, -textarea.span9, -.uneditable-input.span9 { - width: 686px; -} - -input.span8, -textarea.span8, -.uneditable-input.span8 { - width: 606px; -} - -input.span7, -textarea.span7, -.uneditable-input.span7 { - width: 526px; -} - -input.span6, -textarea.span6, -.uneditable-input.span6 { - width: 446px; -} - -input.span5, -textarea.span5, -.uneditable-input.span5 { - width: 366px; -} - -input.span4, -textarea.span4, -.uneditable-input.span4 { - width: 286px; -} - -input.span3, -textarea.span3, -.uneditable-input.span3 { - width: 206px; -} - -input.span2, -textarea.span2, -.uneditable-input.span2 { - width: 126px; -} - -input.span1, -textarea.span1, -.uneditable-input.span1 { - width: 46px; -} - -.controls-row { - *zoom: 1; -} - -.controls-row:before, -.controls-row:after { - display: table; - line-height: 0; - content: ""; -} - -.controls-row:after { - clear: both; -} - -.controls-row [class*="span"], -.row-fluid .controls-row [class*="span"] { - float: left; -} - -.controls-row .checkbox[class*="span"], -.controls-row .radio[class*="span"] { - padding-top: 5px; -} - -input[disabled], -select[disabled], -textarea[disabled], -input[readonly], -select[readonly], -textarea[readonly] { - cursor: not-allowed; - background-color: #eeeeee; -} - -input[type="radio"][disabled], -input[type="checkbox"][disabled], -input[type="radio"][readonly], -input[type="checkbox"][readonly] { - background-color: transparent; -} - -.control-group.warning .control-label, -.control-group.warning .help-block, -.control-group.warning .help-inline { - color: #c09853; -} - -.control-group.warning .checkbox, -.control-group.warning .radio, -.control-group.warning input, -.control-group.warning select, -.control-group.warning textarea { - color: #c09853; -} - -.control-group.warning input, -.control-group.warning select, -.control-group.warning textarea { - border-color: #c09853; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); -} - -.control-group.warning input:focus, -.control-group.warning select:focus, -.control-group.warning textarea:focus { - border-color: #a47e3c; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e; - -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e; -} - -.control-group.warning .input-prepend .add-on, -.control-group.warning .input-append .add-on { - color: #c09853; - background-color: #fcf8e3; - border-color: #c09853; -} - -.control-group.error .control-label, -.control-group.error .help-block, -.control-group.error .help-inline { - color: #b94a48; -} - -.control-group.error .checkbox, -.control-group.error .radio, -.control-group.error input, -.control-group.error select, -.control-group.error textarea { - color: #b94a48; -} - -.control-group.error input, -.control-group.error select, -.control-group.error textarea { - border-color: #b94a48; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); -} - -.control-group.error input:focus, -.control-group.error select:focus, -.control-group.error textarea:focus { - border-color: #953b39; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392; - -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392; -} - -.control-group.error .input-prepend .add-on, -.control-group.error .input-append .add-on { - color: #b94a48; - background-color: #f2dede; - border-color: #b94a48; -} - -.control-group.success .control-label, -.control-group.success .help-block, -.control-group.success .help-inline { - color: #468847; -} - -.control-group.success .checkbox, -.control-group.success .radio, -.control-group.success input, -.control-group.success select, -.control-group.success textarea { - color: #468847; -} - -.control-group.success input, -.control-group.success select, -.control-group.success textarea { - border-color: #468847; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); -} - -.control-group.success input:focus, -.control-group.success select:focus, -.control-group.success textarea:focus { - border-color: #356635; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b; - -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b; -} - -.control-group.success .input-prepend .add-on, -.control-group.success .input-append .add-on { - color: #468847; - background-color: #dff0d8; - border-color: #468847; -} - -.control-group.info .control-label, -.control-group.info .help-block, -.control-group.info .help-inline { - color: #3a87ad; -} - -.control-group.info .checkbox, -.control-group.info .radio, -.control-group.info input, -.control-group.info select, -.control-group.info textarea { - color: #3a87ad; -} - -.control-group.info input, -.control-group.info select, -.control-group.info textarea { - border-color: #3a87ad; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); -} - -.control-group.info input:focus, -.control-group.info select:focus, -.control-group.info textarea:focus { - border-color: #2d6987; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7ab5d3; - -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7ab5d3; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7ab5d3; -} - -.control-group.info .input-prepend .add-on, -.control-group.info .input-append .add-on { - color: #3a87ad; - background-color: #d9edf7; - border-color: #3a87ad; -} - -input:focus:invalid, -textarea:focus:invalid, -select:focus:invalid { - color: #b94a48; - border-color: #ee5f5b; -} - -input:focus:invalid:focus, -textarea:focus:invalid:focus, -select:focus:invalid:focus { - border-color: #e9322d; - -webkit-box-shadow: 0 0 6px #f8b9b7; - -moz-box-shadow: 0 0 6px #f8b9b7; - box-shadow: 0 0 6px #f8b9b7; -} - -.form-actions { - padding: 19px 20px 20px; - margin-top: 20px; - margin-bottom: 20px; - background-color: #f5f5f5; - border-top: 1px solid #e5e5e5; - *zoom: 1; -} - -.form-actions:before, -.form-actions:after { - display: table; - line-height: 0; - content: ""; -} - -.form-actions:after { - clear: both; -} - -.help-block, -.help-inline { - color: #595959; -} - -.help-block { - display: block; - margin-bottom: 10px; -} - -.help-inline { - display: inline-block; - *display: inline; - padding-left: 5px; - vertical-align: middle; - *zoom: 1; -} - -.input-append, -.input-prepend { - display: inline-block; - margin-bottom: 10px; - font-size: 0; - white-space: nowrap; - vertical-align: middle; -} - -.input-append input, -.input-prepend input, -.input-append select, -.input-prepend select, -.input-append .uneditable-input, -.input-prepend .uneditable-input, -.input-append .dropdown-menu, -.input-prepend .dropdown-menu, -.input-append .popover, -.input-prepend .popover { - font-size: 14px; -} - -.input-append input, -.input-prepend input, -.input-append select, -.input-prepend select, -.input-append .uneditable-input, -.input-prepend .uneditable-input { - position: relative; - margin-bottom: 0; - *margin-left: 0; - vertical-align: top; - -webkit-border-radius: 0 4px 4px 0; - -moz-border-radius: 0 4px 4px 0; - border-radius: 0 4px 4px 0; -} - -.input-append input:focus, -.input-prepend input:focus, -.input-append select:focus, -.input-prepend select:focus, -.input-append .uneditable-input:focus, -.input-prepend .uneditable-input:focus { - z-index: 2; -} - -.input-append .add-on, -.input-prepend .add-on { - display: inline-block; - width: auto; - height: 20px; - min-width: 16px; - padding: 4px 5px; - font-size: 14px; - font-weight: normal; - line-height: 20px; - text-align: center; - text-shadow: 0 1px 0 #ffffff; - background-color: #eeeeee; - border: 1px solid #ccc; -} - -.input-append .add-on, -.input-prepend .add-on, -.input-append .btn, -.input-prepend .btn, -.input-append .btn-group > .dropdown-toggle, -.input-prepend .btn-group > .dropdown-toggle { - vertical-align: top; - -webkit-border-radius: 0; - -moz-border-radius: 0; - border-radius: 0; -} - -.input-append .active, -.input-prepend .active { - background-color: #a9dba9; - border-color: #46a546; -} - -.input-prepend .add-on, -.input-prepend .btn { - margin-right: -1px; -} - -.input-prepend .add-on:first-child, -.input-prepend .btn:first-child { - -webkit-border-radius: 4px 0 0 4px; - -moz-border-radius: 4px 0 0 4px; - border-radius: 4px 0 0 4px; -} - -.input-append input, -.input-append select, -.input-append .uneditable-input { - -webkit-border-radius: 4px 0 0 4px; - -moz-border-radius: 4px 0 0 4px; - border-radius: 4px 0 0 4px; -} - -.input-append input + .btn-group .btn:last-child, -.input-append select + .btn-group .btn:last-child, -.input-append .uneditable-input + .btn-group .btn:last-child { - -webkit-border-radius: 0 4px 4px 0; - -moz-border-radius: 0 4px 4px 0; - border-radius: 0 4px 4px 0; -} - -.input-append .add-on, -.input-append .btn, -.input-append .btn-group { - margin-left: -1px; -} - -.input-append .add-on:last-child, -.input-append .btn:last-child, -.input-append .btn-group:last-child > .dropdown-toggle { - -webkit-border-radius: 0 4px 4px 0; - -moz-border-radius: 0 4px 4px 0; - border-radius: 0 4px 4px 0; -} - -.input-prepend.input-append input, -.input-prepend.input-append select, -.input-prepend.input-append .uneditable-input { - -webkit-border-radius: 0; - -moz-border-radius: 0; - border-radius: 0; -} - -.input-prepend.input-append input + .btn-group .btn, -.input-prepend.input-append select + .btn-group .btn, -.input-prepend.input-append .uneditable-input + .btn-group .btn { - -webkit-border-radius: 0 4px 4px 0; - -moz-border-radius: 0 4px 4px 0; - border-radius: 0 4px 4px 0; -} - -.input-prepend.input-append .add-on:first-child, -.input-prepend.input-append .btn:first-child { - margin-right: -1px; - -webkit-border-radius: 4px 0 0 4px; - -moz-border-radius: 4px 0 0 4px; - border-radius: 4px 0 0 4px; -} - -.input-prepend.input-append .add-on:last-child, -.input-prepend.input-append .btn:last-child { - margin-left: -1px; - -webkit-border-radius: 0 4px 4px 0; - -moz-border-radius: 0 4px 4px 0; - border-radius: 0 4px 4px 0; -} - -.input-prepend.input-append .btn-group:first-child { - margin-left: 0; -} - -input.search-query { - padding-right: 14px; - padding-right: 4px \9; - padding-left: 14px; - padding-left: 4px \9; - /* IE7-8 doesn't have border-radius, so don't indent the padding */ - - margin-bottom: 0; - -webkit-border-radius: 15px; - -moz-border-radius: 15px; - border-radius: 15px; -} - -/* Allow for input prepend/append in search forms */ - -.form-search .input-append .search-query, -.form-search .input-prepend .search-query { - -webkit-border-radius: 0; - -moz-border-radius: 0; - border-radius: 0; -} - -.form-search .input-append .search-query { - -webkit-border-radius: 14px 0 0 14px; - -moz-border-radius: 14px 0 0 14px; - border-radius: 14px 0 0 14px; -} - -.form-search .input-append .btn { - -webkit-border-radius: 0 14px 14px 0; - -moz-border-radius: 0 14px 14px 0; - border-radius: 0 14px 14px 0; -} - -.form-search .input-prepend .search-query { - -webkit-border-radius: 0 14px 14px 0; - -moz-border-radius: 0 14px 14px 0; - border-radius: 0 14px 14px 0; -} - -.form-search .input-prepend .btn { - -webkit-border-radius: 14px 0 0 14px; - -moz-border-radius: 14px 0 0 14px; - border-radius: 14px 0 0 14px; -} - -.form-search input, -.form-inline input, -.form-horizontal input, -.form-search textarea, -.form-inline textarea, -.form-horizontal textarea, -.form-search select, -.form-inline select, -.form-horizontal select, -.form-search .help-inline, -.form-inline .help-inline, -.form-horizontal .help-inline, -.form-search .uneditable-input, -.form-inline .uneditable-input, -.form-horizontal .uneditable-input, -.form-search .input-prepend, -.form-inline .input-prepend, -.form-horizontal .input-prepend, -.form-search .input-append, -.form-inline .input-append, -.form-horizontal .input-append { - display: inline-block; - *display: inline; - margin-bottom: 0; - vertical-align: middle; - *zoom: 1; -} - -.form-search .hide, -.form-inline .hide, -.form-horizontal .hide { - display: none; -} - -.form-search label, -.form-inline label, -.form-search .btn-group, -.form-inline .btn-group { - display: inline-block; -} - -.form-search .input-append, -.form-inline .input-append, -.form-search .input-prepend, -.form-inline .input-prepend { - margin-bottom: 0; -} - -.form-search .radio, -.form-search .checkbox, -.form-inline .radio, -.form-inline .checkbox { - padding-left: 0; - margin-bottom: 0; - vertical-align: middle; -} - -.form-search .radio input[type="radio"], -.form-search .checkbox input[type="checkbox"], -.form-inline .radio input[type="radio"], -.form-inline .checkbox input[type="checkbox"] { - float: left; - margin-right: 3px; - margin-left: 0; -} - -.control-group { - margin-bottom: 10px; -} - -legend + .control-group { - margin-top: 20px; - -webkit-margin-top-collapse: separate; -} - -.form-horizontal .control-group { - margin-bottom: 20px; - *zoom: 1; -} - -.form-horizontal .control-group:before, -.form-horizontal .control-group:after { - display: table; - line-height: 0; - content: ""; -} - -.form-horizontal .control-group:after { - clear: both; -} - -.form-horizontal .control-label { - float: left; - width: 160px; - padding-top: 5px; - text-align: right; -} - -.form-horizontal .controls { - *display: inline-block; - *padding-left: 20px; - margin-left: 180px; - *margin-left: 0; -} - -.form-horizontal .controls:first-child { - *padding-left: 180px; -} - -.form-horizontal .help-block { - margin-bottom: 0; -} - -.form-horizontal input + .help-block, -.form-horizontal select + .help-block, -.form-horizontal textarea + .help-block, -.form-horizontal .uneditable-input + .help-block, -.form-horizontal .input-prepend + .help-block, -.form-horizontal .input-append + .help-block { - margin-top: 10px; -} - -.form-horizontal .form-actions { - padding-left: 180px; -} - -table { - max-width: 100%; - background-color: transparent; - border-collapse: collapse; - border-spacing: 0; -} - -.table { - width: 100%; - margin-bottom: 20px; -} - -.table th, -.table td { - padding: 8px; - line-height: 20px; - text-align: left; - vertical-align: top; - border-top: 1px solid #dddddd; -} - -.table th { - font-weight: bold; -} - -.table thead th { - vertical-align: bottom; -} - -.table caption + thead tr:first-child th, -.table caption + thead tr:first-child td, -.table colgroup + thead tr:first-child th, -.table colgroup + thead tr:first-child td, -.table thead:first-child tr:first-child th, -.table thead:first-child tr:first-child td { - border-top: 0; -} - -.table tbody + tbody { - border-top: 2px solid #dddddd; -} - -.table .table { - background-color: #ffffff; -} - -.table-condensed th, -.table-condensed td { - padding: 4px 5px; -} - -.table-bordered { - border: 1px solid #dddddd; - border-collapse: separate; - *border-collapse: collapse; - border-left: 0; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; -} - -.table-bordered th, -.table-bordered td { - border-left: 1px solid #dddddd; -} - -.table-bordered caption + thead tr:first-child th, -.table-bordered caption + tbody tr:first-child th, -.table-bordered caption + tbody tr:first-child td, -.table-bordered colgroup + thead tr:first-child th, -.table-bordered colgroup + tbody tr:first-child th, -.table-bordered colgroup + tbody tr:first-child td, -.table-bordered thead:first-child tr:first-child th, -.table-bordered tbody:first-child tr:first-child th, -.table-bordered tbody:first-child tr:first-child td { - border-top: 0; -} - -.table-bordered thead:first-child tr:first-child > th:first-child, -.table-bordered tbody:first-child tr:first-child > td:first-child, -.table-bordered tbody:first-child tr:first-child > th:first-child { - -webkit-border-top-left-radius: 4px; - border-top-left-radius: 4px; - -moz-border-radius-topleft: 4px; -} - -.table-bordered thead:first-child tr:first-child > th:last-child, -.table-bordered tbody:first-child tr:first-child > td:last-child, -.table-bordered tbody:first-child tr:first-child > th:last-child { - -webkit-border-top-right-radius: 4px; - border-top-right-radius: 4px; - -moz-border-radius-topright: 4px; -} - -.table-bordered thead:last-child tr:last-child > th:first-child, -.table-bordered tbody:last-child tr:last-child > td:first-child, -.table-bordered tbody:last-child tr:last-child > th:first-child, -.table-bordered tfoot:last-child tr:last-child > td:first-child, -.table-bordered tfoot:last-child tr:last-child > th:first-child { - -webkit-border-bottom-left-radius: 4px; - border-bottom-left-radius: 4px; - -moz-border-radius-bottomleft: 4px; -} - -.table-bordered thead:last-child tr:last-child > th:last-child, -.table-bordered tbody:last-child tr:last-child > td:last-child, -.table-bordered tbody:last-child tr:last-child > th:last-child, -.table-bordered tfoot:last-child tr:last-child > td:last-child, -.table-bordered tfoot:last-child tr:last-child > th:last-child { - -webkit-border-bottom-right-radius: 4px; - border-bottom-right-radius: 4px; - -moz-border-radius-bottomright: 4px; -} - -.table-bordered tfoot + tbody:last-child tr:last-child td:first-child { - -webkit-border-bottom-left-radius: 0; - border-bottom-left-radius: 0; - -moz-border-radius-bottomleft: 0; -} - -.table-bordered tfoot + tbody:last-child tr:last-child td:last-child { - -webkit-border-bottom-right-radius: 0; - border-bottom-right-radius: 0; - -moz-border-radius-bottomright: 0; -} - -.table-bordered caption + thead tr:first-child th:first-child, -.table-bordered caption + tbody tr:first-child td:first-child, -.table-bordered colgroup + thead tr:first-child th:first-child, -.table-bordered colgroup + tbody tr:first-child td:first-child { - -webkit-border-top-left-radius: 4px; - border-top-left-radius: 4px; - -moz-border-radius-topleft: 4px; -} - -.table-bordered caption + thead tr:first-child th:last-child, -.table-bordered caption + tbody tr:first-child td:last-child, -.table-bordered colgroup + thead tr:first-child th:last-child, -.table-bordered colgroup + tbody tr:first-child td:last-child { - -webkit-border-top-right-radius: 4px; - border-top-right-radius: 4px; - -moz-border-radius-topright: 4px; -} - -.table-striped tbody > tr:nth-child(odd) > td, -.table-striped tbody > tr:nth-child(odd) > th { - background-color: #f9f9f9; -} - -.table-hover tbody tr:hover > td, -.table-hover tbody tr:hover > th { - background-color: #f5f5f5; -} - -table td[class*="span"], -table th[class*="span"], -.row-fluid table td[class*="span"], -.row-fluid table th[class*="span"] { - display: table-cell; - float: none; - margin-left: 0; -} - -.table td.span1, -.table th.span1 { - float: none; - width: 44px; - margin-left: 0; -} - -.table td.span2, -.table th.span2 { - float: none; - width: 124px; - margin-left: 0; -} - -.table td.span3, -.table th.span3 { - float: none; - width: 204px; - margin-left: 0; -} - -.table td.span4, -.table th.span4 { - float: none; - width: 284px; - margin-left: 0; -} - -.table td.span5, -.table th.span5 { - float: none; - width: 364px; - margin-left: 0; -} - -.table td.span6, -.table th.span6 { - float: none; - width: 444px; - margin-left: 0; -} - -.table td.span7, -.table th.span7 { - float: none; - width: 524px; - margin-left: 0; -} - -.table td.span8, -.table th.span8 { - float: none; - width: 604px; - margin-left: 0; -} - -.table td.span9, -.table th.span9 { - float: none; - width: 684px; - margin-left: 0; -} - -.table td.span10, -.table th.span10 { - float: none; - width: 764px; - margin-left: 0; -} - -.table td.span11, -.table th.span11 { - float: none; - width: 844px; - margin-left: 0; -} - -.table td.span12, -.table th.span12 { - float: none; - width: 924px; - margin-left: 0; -} - -.table tbody tr.success > td { - background-color: #dff0d8; -} - -.table tbody tr.error > td { - background-color: #f2dede; -} - -.table tbody tr.warning > td { - background-color: #fcf8e3; -} - -.table tbody tr.info > td { - background-color: #d9edf7; -} - -.table-hover tbody tr.success:hover > td { - background-color: #d0e9c6; -} - -.table-hover tbody tr.error:hover > td { - background-color: #ebcccc; -} - -.table-hover tbody tr.warning:hover > td { - background-color: #faf2cc; -} - -.table-hover tbody tr.info:hover > td { - background-color: #c4e3f3; -} - -[class^="icon-"], -[class*=" icon-"] { - display: inline-block; - width: 14px; - height: 14px; - margin-top: 1px; - *margin-right: .3em; - line-height: 14px; - vertical-align: text-top; - background-image: url("../img/glyphicons-halflings.png"); - background-position: 14px 14px; - background-repeat: no-repeat; -} - -/* White icons with optional class, or on hover/focus/active states of certain elements */ - -.icon-white, -.nav-pills > .active > a > [class^="icon-"], -.nav-pills > .active > a > [class*=" icon-"], -.nav-list > .active > a > [class^="icon-"], -.nav-list > .active > a > [class*=" icon-"], -.navbar-inverse .nav > .active > a > [class^="icon-"], -.navbar-inverse .nav > .active > a > [class*=" icon-"], -.dropdown-menu > li > a:hover > [class^="icon-"], -.dropdown-menu > li > a:focus > [class^="icon-"], -.dropdown-menu > li > a:hover > [class*=" icon-"], -.dropdown-menu > li > a:focus > [class*=" icon-"], -.dropdown-menu > .active > a > [class^="icon-"], -.dropdown-menu > .active > a > [class*=" icon-"], -.dropdown-submenu:hover > a > [class^="icon-"], -.dropdown-submenu:focus > a > [class^="icon-"], -.dropdown-submenu:hover > a > [class*=" icon-"], -.dropdown-submenu:focus > a > [class*=" icon-"] { - background-image: url("../img/glyphicons-halflings-white.png"); -} - -.icon-glass { - background-position: 0 0; -} - -.icon-music { - background-position: -24px 0; -} - -.icon-search { - background-position: -48px 0; -} - -.icon-envelope { - background-position: -72px 0; -} - -.icon-heart { - background-position: -96px 0; -} - -.icon-star { - background-position: -120px 0; -} - -.icon-star-empty { - background-position: -144px 0; -} - -.icon-user { - background-position: -168px 0; -} - -.icon-film { - background-position: -192px 0; -} - -.icon-th-large { - background-position: -216px 0; -} - -.icon-th { - background-position: -240px 0; -} - -.icon-th-list { - background-position: -264px 0; -} - -.icon-ok { - background-position: -288px 0; -} - -.icon-remove { - background-position: -312px 0; -} - -.icon-zoom-in { - background-position: -336px 0; -} - -.icon-zoom-out { - background-position: -360px 0; -} - -.icon-off { - background-position: -384px 0; -} - -.icon-signal { - background-position: -408px 0; -} - -.icon-cog { - background-position: -432px 0; -} - -.icon-trash { - background-position: -456px 0; -} - -.icon-home { - background-position: 0 -24px; -} - -.icon-file { - background-position: -24px -24px; -} - -.icon-time { - background-position: -48px -24px; -} - -.icon-road { - background-position: -72px -24px; -} - -.icon-download-alt { - background-position: -96px -24px; -} - -.icon-download { - background-position: -120px -24px; -} - -.icon-upload { - background-position: -144px -24px; -} - -.icon-inbox { - background-position: -168px -24px; -} - -.icon-play-circle { - background-position: -192px -24px; -} - -.icon-repeat { - background-position: -216px -24px; -} - -.icon-refresh { - background-position: -240px -24px; -} - -.icon-list-alt { - background-position: -264px -24px; -} - -.icon-lock { - background-position: -287px -24px; -} - -.icon-flag { - background-position: -312px -24px; -} - -.icon-headphones { - background-position: -336px -24px; -} - -.icon-volume-off { - background-position: -360px -24px; -} - -.icon-volume-down { - background-position: -384px -24px; -} - -.icon-volume-up { - background-position: -408px -24px; -} - -.icon-qrcode { - background-position: -432px -24px; -} - -.icon-barcode { - background-position: -456px -24px; -} - -.icon-tag { - background-position: 0 -48px; -} - -.icon-tags { - background-position: -25px -48px; -} - -.icon-book { - background-position: -48px -48px; -} - -.icon-bookmark { - background-position: -72px -48px; -} - -.icon-print { - background-position: -96px -48px; -} - -.icon-camera { - background-position: -120px -48px; -} - -.icon-font { - background-position: -144px -48px; -} - -.icon-bold { - background-position: -167px -48px; -} - -.icon-italic { - background-position: -192px -48px; -} - -.icon-text-height { - background-position: -216px -48px; -} - -.icon-text-width { - background-position: -240px -48px; -} - -.icon-align-left { - background-position: -264px -48px; -} - -.icon-align-center { - background-position: -288px -48px; -} - -.icon-align-right { - background-position: -312px -48px; -} - -.icon-align-justify { - background-position: -336px -48px; -} - -.icon-list { - background-position: -360px -48px; -} - -.icon-indent-left { - background-position: -384px -48px; -} - -.icon-indent-right { - background-position: -408px -48px; -} - -.icon-facetime-video { - background-position: -432px -48px; -} - -.icon-picture { - background-position: -456px -48px; -} - -.icon-pencil { - background-position: 0 -72px; -} - -.icon-map-marker { - background-position: -24px -72px; -} - -.icon-adjust { - background-position: -48px -72px; -} - -.icon-tint { - background-position: -72px -72px; -} - -.icon-edit { - background-position: -96px -72px; -} - -.icon-share { - background-position: -120px -72px; -} - -.icon-check { - background-position: -144px -72px; -} - -.icon-move { - background-position: -168px -72px; -} - -.icon-step-backward { - background-position: -192px -72px; -} - -.icon-fast-backward { - background-position: -216px -72px; -} - -.icon-backward { - background-position: -240px -72px; -} - -.icon-play { - background-position: -264px -72px; -} - -.icon-pause { - background-position: -288px -72px; -} - -.icon-stop { - background-position: -312px -72px; -} - -.icon-forward { - background-position: -336px -72px; -} - -.icon-fast-forward { - background-position: -360px -72px; -} - -.icon-step-forward { - background-position: -384px -72px; -} - -.icon-eject { - background-position: -408px -72px; -} - -.icon-chevron-left { - background-position: -432px -72px; -} - -.icon-chevron-right { - background-position: -456px -72px; -} - -.icon-plus-sign { - background-position: 0 -96px; -} - -.icon-minus-sign { - background-position: -24px -96px; -} - -.icon-remove-sign { - background-position: -48px -96px; -} - -.icon-ok-sign { - background-position: -72px -96px; -} - -.icon-question-sign { - background-position: -96px -96px; -} - -.icon-info-sign { - background-position: -120px -96px; -} - -.icon-screenshot { - background-position: -144px -96px; -} - -.icon-remove-circle { - background-position: -168px -96px; -} - -.icon-ok-circle { - background-position: -192px -96px; -} - -.icon-ban-circle { - background-position: -216px -96px; -} - -.icon-arrow-left { - background-position: -240px -96px; -} - -.icon-arrow-right { - background-position: -264px -96px; -} - -.icon-arrow-up { - background-position: -289px -96px; -} - -.icon-arrow-down { - background-position: -312px -96px; -} - -.icon-share-alt { - background-position: -336px -96px; -} - -.icon-resize-full { - background-position: -360px -96px; -} - -.icon-resize-small { - background-position: -384px -96px; -} - -.icon-plus { - background-position: -408px -96px; -} - -.icon-minus { - background-position: -433px -96px; -} - -.icon-asterisk { - background-position: -456px -96px; -} - -.icon-exclamation-sign { - background-position: 0 -120px; -} - -.icon-gift { - background-position: -24px -120px; -} - -.icon-leaf { - background-position: -48px -120px; -} - -.icon-fire { - background-position: -72px -120px; -} - -.icon-eye-open { - background-position: -96px -120px; -} - -.icon-eye-close { - background-position: -120px -120px; -} - -.icon-warning-sign { - background-position: -144px -120px; -} - -.icon-plane { - background-position: -168px -120px; -} - -.icon-calendar { - background-position: -192px -120px; -} - -.icon-random { - width: 16px; - background-position: -216px -120px; -} - -.icon-comment { - background-position: -240px -120px; -} - -.icon-magnet { - background-position: -264px -120px; -} - -.icon-chevron-up { - background-position: -288px -120px; -} - -.icon-chevron-down { - background-position: -313px -119px; -} - -.icon-retweet { - background-position: -336px -120px; -} - -.icon-shopping-cart { - background-position: -360px -120px; -} - -.icon-folder-close { - width: 16px; - background-position: -384px -120px; -} - -.icon-folder-open { - width: 16px; - background-position: -408px -120px; -} - -.icon-resize-vertical { - background-position: -432px -119px; -} - -.icon-resize-horizontal { - background-position: -456px -118px; -} - -.icon-hdd { - background-position: 0 -144px; -} - -.icon-bullhorn { - background-position: -24px -144px; -} - -.icon-bell { - background-position: -48px -144px; -} - -.icon-certificate { - background-position: -72px -144px; -} - -.icon-thumbs-up { - background-position: -96px -144px; -} - -.icon-thumbs-down { - background-position: -120px -144px; -} - -.icon-hand-right { - background-position: -144px -144px; -} - -.icon-hand-left { - background-position: -168px -144px; -} - -.icon-hand-up { - background-position: -192px -144px; -} - -.icon-hand-down { - background-position: -216px -144px; -} - -.icon-circle-arrow-right { - background-position: -240px -144px; -} - -.icon-circle-arrow-left { - background-position: -264px -144px; -} - -.icon-circle-arrow-up { - background-position: -288px -144px; -} - -.icon-circle-arrow-down { - background-position: -312px -144px; -} - -.icon-globe { - background-position: -336px -144px; -} - -.icon-wrench { - background-position: -360px -144px; -} - -.icon-tasks { - background-position: -384px -144px; -} - -.icon-filter { - background-position: -408px -144px; -} - -.icon-briefcase { - background-position: -432px -144px; -} - -.icon-fullscreen { - background-position: -456px -144px; -} - -.dropup, -.dropdown { - position: relative; -} - -.dropdown-toggle { - *margin-bottom: -3px; -} - -.dropdown-toggle:active, -.open .dropdown-toggle { - outline: 0; -} - -.caret { - display: inline-block; - width: 0; - height: 0; - vertical-align: top; - border-top: 4px solid #000000; - border-right: 4px solid transparent; - border-left: 4px solid transparent; - content: ""; -} - -.dropdown .caret { - margin-top: 8px; - margin-left: 2px; -} - -.dropdown-menu { - position: absolute; - top: 100%; - left: 0; - z-index: 1000; - display: none; - float: left; - min-width: 160px; - padding: 5px 0; - margin: 2px 0 0; - list-style: none; - background-color: #ffffff; - border: 1px solid #ccc; - border: 1px solid rgba(0, 0, 0, 0.2); - *border-right-width: 2px; - *border-bottom-width: 2px; - -webkit-border-radius: 6px; - -moz-border-radius: 6px; - border-radius: 6px; - -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); - -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); - box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); - -webkit-background-clip: padding-box; - -moz-background-clip: padding; - background-clip: padding-box; -} - -.dropdown-menu.pull-right { - right: 0; - left: auto; -} - -.dropdown-menu .divider { - *width: 100%; - height: 1px; - margin: 9px 1px; - *margin: -5px 0 5px; - overflow: hidden; - background-color: #e5e5e5; - border-bottom: 1px solid #ffffff; -} - -.dropdown-menu > li > a { - display: block; - padding: 3px 20px; - clear: both; - font-weight: normal; - line-height: 20px; - color: #333333; - white-space: nowrap; -} - -.dropdown-menu > li > a:hover, -.dropdown-menu > li > a:focus, -.dropdown-submenu:hover > a, -.dropdown-submenu:focus > a { - color: #ffffff; - text-decoration: none; - background-color: #0081c2; - background-image: -moz-linear-gradient(top, #0088cc, #0077b3); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0077b3)); - background-image: -webkit-linear-gradient(top, #0088cc, #0077b3); - background-image: -o-linear-gradient(top, #0088cc, #0077b3); - background-image: linear-gradient(to bottom, #0088cc, #0077b3); - background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0077b3', GradientType=0); -} - -.dropdown-menu > .active > a, -.dropdown-menu > .active > a:hover, -.dropdown-menu > .active > a:focus { - color: #ffffff; - text-decoration: none; - background-color: #0081c2; - background-image: -moz-linear-gradient(top, #0088cc, #0077b3); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0077b3)); - background-image: -webkit-linear-gradient(top, #0088cc, #0077b3); - background-image: -o-linear-gradient(top, #0088cc, #0077b3); - background-image: linear-gradient(to bottom, #0088cc, #0077b3); - background-repeat: repeat-x; - outline: 0; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0077b3', GradientType=0); -} - -.dropdown-menu > .disabled > a, -.dropdown-menu > .disabled > a:hover, -.dropdown-menu > .disabled > a:focus { - color: #999999; -} - -.dropdown-menu > .disabled > a:hover, -.dropdown-menu > .disabled > a:focus { - text-decoration: none; - cursor: default; - background-color: transparent; - background-image: none; - filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); -} - -.open { - *z-index: 1000; -} - -.open > .dropdown-menu { - display: block; -} - -.pull-right > .dropdown-menu { - right: 0; - left: auto; -} - -.dropup .caret, -.navbar-fixed-bottom .dropdown .caret { - border-top: 0; - border-bottom: 4px solid #000000; - content: ""; -} - -.dropup .dropdown-menu, -.navbar-fixed-bottom .dropdown .dropdown-menu { - top: auto; - bottom: 100%; - margin-bottom: 1px; -} - -.dropdown-submenu { - position: relative; -} - -.dropdown-submenu > .dropdown-menu { - top: 0; - left: 100%; - margin-top: -6px; - margin-left: -1px; - -webkit-border-radius: 0 6px 6px 6px; - -moz-border-radius: 0 6px 6px 6px; - border-radius: 0 6px 6px 6px; -} - -.dropdown-submenu:hover > .dropdown-menu { - display: block; -} - -.dropup .dropdown-submenu > .dropdown-menu { - top: auto; - bottom: 0; - margin-top: 0; - margin-bottom: -2px; - -webkit-border-radius: 5px 5px 5px 0; - -moz-border-radius: 5px 5px 5px 0; - border-radius: 5px 5px 5px 0; -} - -.dropdown-submenu > a:after { - display: block; - float: right; - width: 0; - height: 0; - margin-top: 5px; - margin-right: -10px; - border-color: transparent; - border-left-color: #cccccc; - border-style: solid; - border-width: 5px 0 5px 5px; - content: " "; -} - -.dropdown-submenu:hover > a:after { - border-left-color: #ffffff; -} - -.dropdown-submenu.pull-left { - float: none; -} - -.dropdown-submenu.pull-left > .dropdown-menu { - left: -100%; - margin-left: 10px; - -webkit-border-radius: 6px 0 6px 6px; - -moz-border-radius: 6px 0 6px 6px; - border-radius: 6px 0 6px 6px; -} - -.dropdown .dropdown-menu .nav-header { - padding-right: 20px; - padding-left: 20px; -} - -.typeahead { - z-index: 1051; - margin-top: 2px; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; -} - -.well { - min-height: 20px; - padding: 19px; - margin-bottom: 20px; - background-color: #f5f5f5; - border: 1px solid #e3e3e3; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); - -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); -} - -.well blockquote { - border-color: #ddd; - border-color: rgba(0, 0, 0, 0.15); -} - -.well-large { - padding: 24px; - -webkit-border-radius: 6px; - -moz-border-radius: 6px; - border-radius: 6px; -} - -.well-small { - padding: 9px; - -webkit-border-radius: 3px; - -moz-border-radius: 3px; - border-radius: 3px; -} - -.fade { - opacity: 0; - -webkit-transition: opacity 0.15s linear; - -moz-transition: opacity 0.15s linear; - -o-transition: opacity 0.15s linear; - transition: opacity 0.15s linear; -} - -.fade.in { - opacity: 1; -} - -.collapse { - position: relative; - height: 0; - overflow: hidden; - -webkit-transition: height 0.35s ease; - -moz-transition: height 0.35s ease; - -o-transition: height 0.35s ease; - transition: height 0.35s ease; -} - -.collapse.in { - height: auto; -} - -.close { - float: right; - font-size: 20px; - font-weight: bold; - line-height: 20px; - color: #000000; - text-shadow: 0 1px 0 #ffffff; - opacity: 0.2; - filter: alpha(opacity=20); -} - -.close:hover, -.close:focus { - color: #000000; - text-decoration: none; - cursor: pointer; - opacity: 0.4; - filter: alpha(opacity=40); -} - -button.close { - padding: 0; - cursor: pointer; - background: transparent; - border: 0; - -webkit-appearance: none; -} - -.btn { - display: inline-block; - *display: inline; - padding: 4px 12px; - margin-bottom: 0; - *margin-left: .3em; - font-size: 14px; - line-height: 20px; - color: #333333; - text-align: center; - text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); - vertical-align: middle; - cursor: pointer; - background-color: #f5f5f5; - *background-color: #e6e6e6; - background-image: -moz-linear-gradient(top, #ffffff, #e6e6e6); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6)); - background-image: -webkit-linear-gradient(top, #ffffff, #e6e6e6); - background-image: -o-linear-gradient(top, #ffffff, #e6e6e6); - background-image: linear-gradient(to bottom, #ffffff, #e6e6e6); - background-repeat: repeat-x; - border: 1px solid #cccccc; - *border: 0; - border-color: #e6e6e6 #e6e6e6 #bfbfbf; - border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); - border-bottom-color: #b3b3b3; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe6e6e6', GradientType=0); - filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); - *zoom: 1; - -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); - -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); -} - -.btn:hover, -.btn:focus, -.btn:active, -.btn.active, -.btn.disabled, -.btn[disabled] { - color: #333333; - background-color: #e6e6e6; - *background-color: #d9d9d9; -} - -.btn:active, -.btn.active { - background-color: #cccccc \9; -} - -.btn:first-child { - *margin-left: 0; -} - -.btn:hover, -.btn:focus { - color: #333333; - text-decoration: none; - background-position: 0 -15px; - -webkit-transition: background-position 0.1s linear; - -moz-transition: background-position 0.1s linear; - -o-transition: background-position 0.1s linear; - transition: background-position 0.1s linear; -} - -.btn:focus { - outline: thin dotted #333; - outline: 5px auto -webkit-focus-ring-color; - outline-offset: -2px; -} - -.btn.active, -.btn:active { - background-image: none; - outline: 0; - -webkit-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); - -moz-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); - box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); -} - -.btn.disabled, -.btn[disabled] { - cursor: default; - background-image: none; - opacity: 0.65; - filter: alpha(opacity=65); - -webkit-box-shadow: none; - -moz-box-shadow: none; - box-shadow: none; -} - -.btn-large { - padding: 11px 19px; - font-size: 17.5px; - -webkit-border-radius: 6px; - -moz-border-radius: 6px; - border-radius: 6px; -} - -.btn-large [class^="icon-"], -.btn-large [class*=" icon-"] { - margin-top: 4px; -} - -.btn-small { - padding: 2px 10px; - font-size: 11.9px; - -webkit-border-radius: 3px; - -moz-border-radius: 3px; - border-radius: 3px; -} - -.btn-small [class^="icon-"], -.btn-small [class*=" icon-"] { - margin-top: 0; -} - -.btn-mini [class^="icon-"], -.btn-mini [class*=" icon-"] { - margin-top: -1px; -} - -.btn-mini { - padding: 0 6px; - font-size: 10.5px; - -webkit-border-radius: 3px; - -moz-border-radius: 3px; - border-radius: 3px; -} - -.btn-block { - display: block; - width: 100%; - padding-right: 0; - padding-left: 0; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; -} - -.btn-block + .btn-block { - margin-top: 5px; -} - -input[type="submit"].btn-block, -input[type="reset"].btn-block, -input[type="button"].btn-block { - width: 100%; -} - -.btn-primary.active, -.btn-warning.active, -.btn-danger.active, -.btn-success.active, -.btn-info.active, -.btn-inverse.active { - color: rgba(255, 255, 255, 0.75); -} - -.btn-primary { - color: #ffffff; - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); - background-color: #006dcc; - *background-color: #0044cc; - background-image: -moz-linear-gradient(top, #0088cc, #0044cc); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc)); - background-image: -webkit-linear-gradient(top, #0088cc, #0044cc); - background-image: -o-linear-gradient(top, #0088cc, #0044cc); - background-image: linear-gradient(to bottom, #0088cc, #0044cc); - background-repeat: repeat-x; - border-color: #0044cc #0044cc #002a80; - border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0044cc', GradientType=0); - filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); -} - -.btn-primary:hover, -.btn-primary:focus, -.btn-primary:active, -.btn-primary.active, -.btn-primary.disabled, -.btn-primary[disabled] { - color: #ffffff; - background-color: #0044cc; - *background-color: #003bb3; -} - -.btn-primary:active, -.btn-primary.active { - background-color: #003399 \9; -} - -.btn-warning { - color: #ffffff; - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); - background-color: #faa732; - *background-color: #f89406; - background-image: -moz-linear-gradient(top, #fbb450, #f89406); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406)); - background-image: -webkit-linear-gradient(top, #fbb450, #f89406); - background-image: -o-linear-gradient(top, #fbb450, #f89406); - background-image: linear-gradient(to bottom, #fbb450, #f89406); - background-repeat: repeat-x; - border-color: #f89406 #f89406 #ad6704; - border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450', endColorstr='#fff89406', GradientType=0); - filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); -} - -.btn-warning:hover, -.btn-warning:focus, -.btn-warning:active, -.btn-warning.active, -.btn-warning.disabled, -.btn-warning[disabled] { - color: #ffffff; - background-color: #f89406; - *background-color: #df8505; -} - -.btn-warning:active, -.btn-warning.active { - background-color: #c67605 \9; -} - -.btn-danger { - color: #ffffff; - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); - background-color: #da4f49; - *background-color: #bd362f; - background-image: -moz-linear-gradient(top, #ee5f5b, #bd362f); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#bd362f)); - background-image: -webkit-linear-gradient(top, #ee5f5b, #bd362f); - background-image: -o-linear-gradient(top, #ee5f5b, #bd362f); - background-image: linear-gradient(to bottom, #ee5f5b, #bd362f); - background-repeat: repeat-x; - border-color: #bd362f #bd362f #802420; - border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffee5f5b', endColorstr='#ffbd362f', GradientType=0); - filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); -} - -.btn-danger:hover, -.btn-danger:focus, -.btn-danger:active, -.btn-danger.active, -.btn-danger.disabled, -.btn-danger[disabled] { - color: #ffffff; - background-color: #bd362f; - *background-color: #a9302a; -} - -.btn-danger:active, -.btn-danger.active { - background-color: #942a25 \9; -} - -.btn-success { - color: #ffffff; - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); - background-color: #5bb75b; - *background-color: #51a351; - background-image: -moz-linear-gradient(top, #62c462, #51a351); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#51a351)); - background-image: -webkit-linear-gradient(top, #62c462, #51a351); - background-image: -o-linear-gradient(top, #62c462, #51a351); - background-image: linear-gradient(to bottom, #62c462, #51a351); - background-repeat: repeat-x; - border-color: #51a351 #51a351 #387038; - border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff62c462', endColorstr='#ff51a351', GradientType=0); - filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); -} - -.btn-success:hover, -.btn-success:focus, -.btn-success:active, -.btn-success.active, -.btn-success.disabled, -.btn-success[disabled] { - color: #ffffff; - background-color: #51a351; - *background-color: #499249; -} - -.btn-success:active, -.btn-success.active { - background-color: #408140 \9; -} - -.btn-info { - color: #ffffff; - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); - background-color: #49afcd; - *background-color: #2f96b4; - background-image: -moz-linear-gradient(top, #5bc0de, #2f96b4); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#2f96b4)); - background-image: -webkit-linear-gradient(top, #5bc0de, #2f96b4); - background-image: -o-linear-gradient(top, #5bc0de, #2f96b4); - background-image: linear-gradient(to bottom, #5bc0de, #2f96b4); - background-repeat: repeat-x; - border-color: #2f96b4 #2f96b4 #1f6377; - border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2f96b4', GradientType=0); - filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); -} - -.btn-info:hover, -.btn-info:focus, -.btn-info:active, -.btn-info.active, -.btn-info.disabled, -.btn-info[disabled] { - color: #ffffff; - background-color: #2f96b4; - *background-color: #2a85a0; -} - -.btn-info:active, -.btn-info.active { - background-color: #24748c \9; -} - -.btn-inverse { - color: #ffffff; - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); - background-color: #363636; - *background-color: #222222; - background-image: -moz-linear-gradient(top, #444444, #222222); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#444444), to(#222222)); - background-image: -webkit-linear-gradient(top, #444444, #222222); - background-image: -o-linear-gradient(top, #444444, #222222); - background-image: linear-gradient(to bottom, #444444, #222222); - background-repeat: repeat-x; - border-color: #222222 #222222 #000000; - border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff444444', endColorstr='#ff222222', GradientType=0); - filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); -} - -.btn-inverse:hover, -.btn-inverse:focus, -.btn-inverse:active, -.btn-inverse.active, -.btn-inverse.disabled, -.btn-inverse[disabled] { - color: #ffffff; - background-color: #222222; - *background-color: #151515; -} - -.btn-inverse:active, -.btn-inverse.active { - background-color: #080808 \9; -} - -button.btn, -input[type="submit"].btn { - *padding-top: 3px; - *padding-bottom: 3px; -} - -button.btn::-moz-focus-inner, -input[type="submit"].btn::-moz-focus-inner { - padding: 0; - border: 0; -} - -button.btn.btn-large, -input[type="submit"].btn.btn-large { - *padding-top: 7px; - *padding-bottom: 7px; -} - -button.btn.btn-small, -input[type="submit"].btn.btn-small { - *padding-top: 3px; - *padding-bottom: 3px; -} - -button.btn.btn-mini, -input[type="submit"].btn.btn-mini { - *padding-top: 1px; - *padding-bottom: 1px; -} - -.btn-link, -.btn-link:active, -.btn-link[disabled] { - background-color: transparent; - background-image: none; - -webkit-box-shadow: none; - -moz-box-shadow: none; - box-shadow: none; -} - -.btn-link { - color: #0088cc; - cursor: pointer; - border-color: transparent; - -webkit-border-radius: 0; - -moz-border-radius: 0; - border-radius: 0; -} - -.btn-link:hover, -.btn-link:focus { - color: #005580; - text-decoration: underline; - background-color: transparent; -} - -.btn-link[disabled]:hover, -.btn-link[disabled]:focus { - color: #333333; - text-decoration: none; -} - -.btn-group { - position: relative; - display: inline-block; - *display: inline; - *margin-left: .3em; - font-size: 0; - white-space: nowrap; - vertical-align: middle; - *zoom: 1; -} - -.btn-group:first-child { - *margin-left: 0; -} - -.btn-group + .btn-group { - margin-left: 5px; -} - -.btn-toolbar { - margin-top: 10px; - margin-bottom: 10px; - font-size: 0; -} - -.btn-toolbar > .btn + .btn, -.btn-toolbar > .btn-group + .btn, -.btn-toolbar > .btn + .btn-group { - margin-left: 5px; -} - -.btn-group > .btn { - position: relative; - -webkit-border-radius: 0; - -moz-border-radius: 0; - border-radius: 0; -} - -.btn-group > .btn + .btn { - margin-left: -1px; -} - -.btn-group > .btn, -.btn-group > .dropdown-menu, -.btn-group > .popover { - font-size: 14px; -} - -.btn-group > .btn-mini { - font-size: 10.5px; -} - -.btn-group > .btn-small { - font-size: 11.9px; -} - -.btn-group > .btn-large { - font-size: 17.5px; -} - -.btn-group > .btn:first-child { - margin-left: 0; - -webkit-border-bottom-left-radius: 4px; - border-bottom-left-radius: 4px; - -webkit-border-top-left-radius: 4px; - border-top-left-radius: 4px; - -moz-border-radius-bottomleft: 4px; - -moz-border-radius-topleft: 4px; -} - -.btn-group > .btn:last-child, -.btn-group > .dropdown-toggle { - -webkit-border-top-right-radius: 4px; - border-top-right-radius: 4px; - -webkit-border-bottom-right-radius: 4px; - border-bottom-right-radius: 4px; - -moz-border-radius-topright: 4px; - -moz-border-radius-bottomright: 4px; -} - -.btn-group > .btn.large:first-child { - margin-left: 0; - -webkit-border-bottom-left-radius: 6px; - border-bottom-left-radius: 6px; - -webkit-border-top-left-radius: 6px; - border-top-left-radius: 6px; - -moz-border-radius-bottomleft: 6px; - -moz-border-radius-topleft: 6px; -} - -.btn-group > .btn.large:last-child, -.btn-group > .large.dropdown-toggle { - -webkit-border-top-right-radius: 6px; - border-top-right-radius: 6px; - -webkit-border-bottom-right-radius: 6px; - border-bottom-right-radius: 6px; - -moz-border-radius-topright: 6px; - -moz-border-radius-bottomright: 6px; -} - -.btn-group > .btn:hover, -.btn-group > .btn:focus, -.btn-group > .btn:active, -.btn-group > .btn.active { - z-index: 2; -} - -.btn-group .dropdown-toggle:active, -.btn-group.open .dropdown-toggle { - outline: 0; -} - -.btn-group > .btn + .dropdown-toggle { - *padding-top: 5px; - padding-right: 8px; - *padding-bottom: 5px; - padding-left: 8px; - -webkit-box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); - -moz-box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); - box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); -} - -.btn-group > .btn-mini + .dropdown-toggle { - *padding-top: 2px; - padding-right: 5px; - *padding-bottom: 2px; - padding-left: 5px; -} - -.btn-group > .btn-small + .dropdown-toggle { - *padding-top: 5px; - *padding-bottom: 4px; -} - -.btn-group > .btn-large + .dropdown-toggle { - *padding-top: 7px; - padding-right: 12px; - *padding-bottom: 7px; - padding-left: 12px; -} - -.btn-group.open .dropdown-toggle { - background-image: none; - -webkit-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); - -moz-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); - box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); -} - -.btn-group.open .btn.dropdown-toggle { - background-color: #e6e6e6; -} - -.btn-group.open .btn-primary.dropdown-toggle { - background-color: #0044cc; -} - -.btn-group.open .btn-warning.dropdown-toggle { - background-color: #f89406; -} - -.btn-group.open .btn-danger.dropdown-toggle { - background-color: #bd362f; -} - -.btn-group.open .btn-success.dropdown-toggle { - background-color: #51a351; -} - -.btn-group.open .btn-info.dropdown-toggle { - background-color: #2f96b4; -} - -.btn-group.open .btn-inverse.dropdown-toggle { - background-color: #222222; -} - -.btn .caret { - margin-top: 8px; - margin-left: 0; -} - -.btn-large .caret { - margin-top: 6px; -} - -.btn-large .caret { - border-top-width: 5px; - border-right-width: 5px; - border-left-width: 5px; -} - -.btn-mini .caret, -.btn-small .caret { - margin-top: 8px; -} - -.dropup .btn-large .caret { - border-bottom-width: 5px; -} - -.btn-primary .caret, -.btn-warning .caret, -.btn-danger .caret, -.btn-info .caret, -.btn-success .caret, -.btn-inverse .caret { - border-top-color: #ffffff; - border-bottom-color: #ffffff; -} - -.btn-group-vertical { - display: inline-block; - *display: inline; - /* IE7 inline-block hack */ - - *zoom: 1; -} - -.btn-group-vertical > .btn { - display: block; - float: none; - max-width: 100%; - -webkit-border-radius: 0; - -moz-border-radius: 0; - border-radius: 0; -} - -.btn-group-vertical > .btn + .btn { - margin-top: -1px; - margin-left: 0; -} - -.btn-group-vertical > .btn:first-child { - -webkit-border-radius: 4px 4px 0 0; - -moz-border-radius: 4px 4px 0 0; - border-radius: 4px 4px 0 0; -} - -.btn-group-vertical > .btn:last-child { - -webkit-border-radius: 0 0 4px 4px; - -moz-border-radius: 0 0 4px 4px; - border-radius: 0 0 4px 4px; -} - -.btn-group-vertical > .btn-large:first-child { - -webkit-border-radius: 6px 6px 0 0; - -moz-border-radius: 6px 6px 0 0; - border-radius: 6px 6px 0 0; -} - -.btn-group-vertical > .btn-large:last-child { - -webkit-border-radius: 0 0 6px 6px; - -moz-border-radius: 0 0 6px 6px; - border-radius: 0 0 6px 6px; -} - -.alert { - padding: 8px 35px 8px 14px; - margin-bottom: 20px; - text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); - background-color: #fcf8e3; - border: 1px solid #fbeed5; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; -} - -.alert, -.alert h4 { - color: #c09853; -} - -.alert h4 { - margin: 0; -} - -.alert .close { - position: relative; - top: -2px; - right: -21px; - line-height: 20px; -} - -.alert-success { - color: #468847; - background-color: #dff0d8; - border-color: #d6e9c6; -} - -.alert-success h4 { - color: #468847; -} - -.alert-danger, -.alert-error { - color: #b94a48; - background-color: #f2dede; - border-color: #eed3d7; -} - -.alert-danger h4, -.alert-error h4 { - color: #b94a48; -} - -.alert-info { - color: #3a87ad; - background-color: #d9edf7; - border-color: #bce8f1; -} - -.alert-info h4 { - color: #3a87ad; -} - -.alert-block { - padding-top: 14px; - padding-bottom: 14px; -} - -.alert-block > p, -.alert-block > ul { - margin-bottom: 0; -} - -.alert-block p + p { - margin-top: 5px; -} - -.nav { - margin-bottom: 20px; - margin-left: 0; - list-style: none; -} - -.nav > li > a { - display: block; -} - -.nav > li > a:hover, -.nav > li > a:focus { - text-decoration: none; - background-color: #eeeeee; -} - -.nav > li > a > img { - max-width: none; -} - -.nav > .pull-right { - float: right; -} - -.nav-header { - display: block; - padding: 3px 15px; - font-size: 11px; - font-weight: bold; - line-height: 20px; - color: #999999; - text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); - text-transform: uppercase; -} - -.nav li + .nav-header { - margin-top: 9px; -} - -.nav-list { - padding-right: 15px; - padding-left: 15px; - margin-bottom: 0; -} - -.nav-list > li > a, -.nav-list .nav-header { - margin-right: -15px; - margin-left: -15px; - text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); -} - -.nav-list > li > a { - padding: 3px 15px; -} - -.nav-list > .active > a, -.nav-list > .active > a:hover, -.nav-list > .active > a:focus { - color: #ffffff; - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2); - background-color: #0088cc; -} - -.nav-list [class^="icon-"], -.nav-list [class*=" icon-"] { - margin-right: 2px; -} - -.nav-list .divider { - *width: 100%; - height: 1px; - margin: 9px 1px; - *margin: -5px 0 5px; - overflow: hidden; - background-color: #e5e5e5; - border-bottom: 1px solid #ffffff; -} - -.nav-tabs, -.nav-pills { - *zoom: 1; -} - -.nav-tabs:before, -.nav-pills:before, -.nav-tabs:after, -.nav-pills:after { - display: table; - line-height: 0; - content: ""; -} - -.nav-tabs:after, -.nav-pills:after { - clear: both; -} - -.nav-tabs > li, -.nav-pills > li { - float: left; -} - -.nav-tabs > li > a, -.nav-pills > li > a { - padding-right: 12px; - padding-left: 12px; - margin-right: 2px; - line-height: 14px; -} - -.nav-tabs { - border-bottom: 1px solid #ddd; -} - -.nav-tabs > li { - margin-bottom: -1px; -} - -.nav-tabs > li > a { - padding-top: 8px; - padding-bottom: 8px; - line-height: 20px; - border: 1px solid transparent; - -webkit-border-radius: 4px 4px 0 0; - -moz-border-radius: 4px 4px 0 0; - border-radius: 4px 4px 0 0; -} - -.nav-tabs > li > a:hover, -.nav-tabs > li > a:focus { - border-color: #eeeeee #eeeeee #dddddd; -} - -.nav-tabs > .active > a, -.nav-tabs > .active > a:hover, -.nav-tabs > .active > a:focus { - color: #555555; - cursor: default; - background-color: #ffffff; - border: 1px solid #ddd; - border-bottom-color: transparent; -} - -.nav-pills > li > a { - padding-top: 8px; - padding-bottom: 8px; - margin-top: 2px; - margin-bottom: 2px; - -webkit-border-radius: 5px; - -moz-border-radius: 5px; - border-radius: 5px; -} - -.nav-pills > .active > a, -.nav-pills > .active > a:hover, -.nav-pills > .active > a:focus { - color: #ffffff; - background-color: #0088cc; -} - -.nav-stacked > li { - float: none; -} - -.nav-stacked > li > a { - margin-right: 0; -} - -.nav-tabs.nav-stacked { - border-bottom: 0; -} - -.nav-tabs.nav-stacked > li > a { - border: 1px solid #ddd; - -webkit-border-radius: 0; - -moz-border-radius: 0; - border-radius: 0; -} - -.nav-tabs.nav-stacked > li:first-child > a { - -webkit-border-top-right-radius: 4px; - border-top-right-radius: 4px; - -webkit-border-top-left-radius: 4px; - border-top-left-radius: 4px; - -moz-border-radius-topright: 4px; - -moz-border-radius-topleft: 4px; -} - -.nav-tabs.nav-stacked > li:last-child > a { - -webkit-border-bottom-right-radius: 4px; - border-bottom-right-radius: 4px; - -webkit-border-bottom-left-radius: 4px; - border-bottom-left-radius: 4px; - -moz-border-radius-bottomright: 4px; - -moz-border-radius-bottomleft: 4px; -} - -.nav-tabs.nav-stacked > li > a:hover, -.nav-tabs.nav-stacked > li > a:focus { - z-index: 2; - border-color: #ddd; -} - -.nav-pills.nav-stacked > li > a { - margin-bottom: 3px; -} - -.nav-pills.nav-stacked > li:last-child > a { - margin-bottom: 1px; -} - -.nav-tabs .dropdown-menu { - -webkit-border-radius: 0 0 6px 6px; - -moz-border-radius: 0 0 6px 6px; - border-radius: 0 0 6px 6px; -} - -.nav-pills .dropdown-menu { - -webkit-border-radius: 6px; - -moz-border-radius: 6px; - border-radius: 6px; -} - -.nav .dropdown-toggle .caret { - margin-top: 6px; - border-top-color: #0088cc; - border-bottom-color: #0088cc; -} - -.nav .dropdown-toggle:hover .caret, -.nav .dropdown-toggle:focus .caret { - border-top-color: #005580; - border-bottom-color: #005580; -} - -/* move down carets for tabs */ - -.nav-tabs .dropdown-toggle .caret { - margin-top: 8px; -} - -.nav .active .dropdown-toggle .caret { - border-top-color: #fff; - border-bottom-color: #fff; -} - -.nav-tabs .active .dropdown-toggle .caret { - border-top-color: #555555; - border-bottom-color: #555555; -} - -.nav > .dropdown.active > a:hover, -.nav > .dropdown.active > a:focus { - cursor: pointer; -} - -.nav-tabs .open .dropdown-toggle, -.nav-pills .open .dropdown-toggle, -.nav > li.dropdown.open.active > a:hover, -.nav > li.dropdown.open.active > a:focus { - color: #ffffff; - background-color: #999999; - border-color: #999999; -} - -.nav li.dropdown.open .caret, -.nav li.dropdown.open.active .caret, -.nav li.dropdown.open a:hover .caret, -.nav li.dropdown.open a:focus .caret { - border-top-color: #ffffff; - border-bottom-color: #ffffff; - opacity: 1; - filter: alpha(opacity=100); -} - -.tabs-stacked .open > a:hover, -.tabs-stacked .open > a:focus { - border-color: #999999; -} - -.tabbable { - *zoom: 1; -} - -.tabbable:before, -.tabbable:after { - display: table; - line-height: 0; - content: ""; -} - -.tabbable:after { - clear: both; -} - -.tab-content { - overflow: auto; -} - -.tabs-below > .nav-tabs, -.tabs-right > .nav-tabs, -.tabs-left > .nav-tabs { - border-bottom: 0; -} - -.tab-content > .tab-pane, -.pill-content > .pill-pane { - display: none; -} - -.tab-content > .active, -.pill-content > .active { - display: block; -} - -.tabs-below > .nav-tabs { - border-top: 1px solid #ddd; -} - -.tabs-below > .nav-tabs > li { - margin-top: -1px; - margin-bottom: 0; -} - -.tabs-below > .nav-tabs > li > a { - -webkit-border-radius: 0 0 4px 4px; - -moz-border-radius: 0 0 4px 4px; - border-radius: 0 0 4px 4px; -} - -.tabs-below > .nav-tabs > li > a:hover, -.tabs-below > .nav-tabs > li > a:focus { - border-top-color: #ddd; - border-bottom-color: transparent; -} - -.tabs-below > .nav-tabs > .active > a, -.tabs-below > .nav-tabs > .active > a:hover, -.tabs-below > .nav-tabs > .active > a:focus { - border-color: transparent #ddd #ddd #ddd; -} - -.tabs-left > .nav-tabs > li, -.tabs-right > .nav-tabs > li { - float: none; -} - -.tabs-left > .nav-tabs > li > a, -.tabs-right > .nav-tabs > li > a { - min-width: 74px; - margin-right: 0; - margin-bottom: 3px; -} - -.tabs-left > .nav-tabs { - float: left; - margin-right: 19px; - border-right: 1px solid #ddd; -} - -.tabs-left > .nav-tabs > li > a { - margin-right: -1px; - -webkit-border-radius: 4px 0 0 4px; - -moz-border-radius: 4px 0 0 4px; - border-radius: 4px 0 0 4px; -} - -.tabs-left > .nav-tabs > li > a:hover, -.tabs-left > .nav-tabs > li > a:focus { - border-color: #eeeeee #dddddd #eeeeee #eeeeee; -} - -.tabs-left > .nav-tabs .active > a, -.tabs-left > .nav-tabs .active > a:hover, -.tabs-left > .nav-tabs .active > a:focus { - border-color: #ddd transparent #ddd #ddd; - *border-right-color: #ffffff; -} - -.tabs-right > .nav-tabs { - float: right; - margin-left: 19px; - border-left: 1px solid #ddd; -} - -.tabs-right > .nav-tabs > li > a { - margin-left: -1px; - -webkit-border-radius: 0 4px 4px 0; - -moz-border-radius: 0 4px 4px 0; - border-radius: 0 4px 4px 0; -} - -.tabs-right > .nav-tabs > li > a:hover, -.tabs-right > .nav-tabs > li > a:focus { - border-color: #eeeeee #eeeeee #eeeeee #dddddd; -} - -.tabs-right > .nav-tabs .active > a, -.tabs-right > .nav-tabs .active > a:hover, -.tabs-right > .nav-tabs .active > a:focus { - border-color: #ddd #ddd #ddd transparent; - *border-left-color: #ffffff; -} - -.nav > .disabled > a { - color: #999999; -} - -.nav > .disabled > a:hover, -.nav > .disabled > a:focus { - text-decoration: none; - cursor: default; - background-color: transparent; -} - -.navbar { - *position: relative; - *z-index: 2; - margin-bottom: 20px; - overflow: visible; -} - -.navbar-inner { - min-height: 40px; - padding-right: 20px; - padding-left: 20px; - background-color: #fafafa; - background-image: -moz-linear-gradient(top, #ffffff, #f2f2f2); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#f2f2f2)); - background-image: -webkit-linear-gradient(top, #ffffff, #f2f2f2); - background-image: -o-linear-gradient(top, #ffffff, #f2f2f2); - background-image: linear-gradient(to bottom, #ffffff, #f2f2f2); - background-repeat: repeat-x; - border: 1px solid #d4d4d4; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff2f2f2', GradientType=0); - *zoom: 1; - -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.065); - -moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.065); - box-shadow: 0 1px 4px rgba(0, 0, 0, 0.065); -} - -.navbar-inner:before, -.navbar-inner:after { - display: table; - line-height: 0; - content: ""; -} - -.navbar-inner:after { - clear: both; -} - -.navbar .container { - width: auto; -} - -.nav-collapse.collapse { - height: auto; - overflow: visible; -} - -.navbar .brand { - display: block; - float: left; - padding: 10px 20px 10px; - margin-left: -20px; - font-size: 20px; - font-weight: 200; - color: #777777; - text-shadow: 0 1px 0 #ffffff; -} - -.navbar .brand:hover, -.navbar .brand:focus { - text-decoration: none; -} - -.navbar-text { - margin-bottom: 0; - line-height: 40px; - color: #777777; -} - -.navbar-link { - color: #777777; -} - -.navbar-link:hover, -.navbar-link:focus { - color: #333333; -} - -.navbar .divider-vertical { - height: 40px; - margin: 0 9px; - border-right: 1px solid #ffffff; - border-left: 1px solid #f2f2f2; -} - -.navbar .btn, -.navbar .btn-group { - margin-top: 5px; -} - -.navbar .btn-group .btn, -.navbar .input-prepend .btn, -.navbar .input-append .btn, -.navbar .input-prepend .btn-group, -.navbar .input-append .btn-group { - margin-top: 0; -} - -.navbar-form { - margin-bottom: 0; - *zoom: 1; -} - -.navbar-form:before, -.navbar-form:after { - display: table; - line-height: 0; - content: ""; -} - -.navbar-form:after { - clear: both; -} - -.navbar-form input, -.navbar-form select, -.navbar-form .radio, -.navbar-form .checkbox { - margin-top: 5px; -} - -.navbar-form input, -.navbar-form select, -.navbar-form .btn { - display: inline-block; - margin-bottom: 0; -} - -.navbar-form input[type="image"], -.navbar-form input[type="checkbox"], -.navbar-form input[type="radio"] { - margin-top: 3px; -} - -.navbar-form .input-append, -.navbar-form .input-prepend { - margin-top: 5px; - white-space: nowrap; -} - -.navbar-form .input-append input, -.navbar-form .input-prepend input { - margin-top: 0; -} - -.navbar-search { - position: relative; - float: left; - margin-top: 5px; - margin-bottom: 0; -} - -.navbar-search .search-query { - padding: 4px 14px; - margin-bottom: 0; - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - font-size: 13px; - font-weight: normal; - line-height: 1; - -webkit-border-radius: 15px; - -moz-border-radius: 15px; - border-radius: 15px; -} - -.navbar-static-top { - position: static; - margin-bottom: 0; -} - -.navbar-static-top .navbar-inner { - -webkit-border-radius: 0; - -moz-border-radius: 0; - border-radius: 0; -} - -.navbar-fixed-top, -.navbar-fixed-bottom { - position: fixed; - right: 0; - left: 0; - z-index: 1030; - margin-bottom: 0; -} - -.navbar-fixed-top .navbar-inner, -.navbar-static-top .navbar-inner { - border-width: 0 0 1px; -} - -.navbar-fixed-bottom .navbar-inner { - border-width: 1px 0 0; -} - -.navbar-fixed-top .navbar-inner, -.navbar-fixed-bottom .navbar-inner { - padding-right: 0; - padding-left: 0; - -webkit-border-radius: 0; - -moz-border-radius: 0; - border-radius: 0; -} - -.navbar-static-top .container, -.navbar-fixed-top .container, -.navbar-fixed-bottom .container { - width: 940px; -} - -.navbar-fixed-top { - top: 0; -} - -.navbar-fixed-top .navbar-inner, -.navbar-static-top .navbar-inner { - -webkit-box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1); - -moz-box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1); - box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1); -} - -.navbar-fixed-bottom { - bottom: 0; -} - -.navbar-fixed-bottom .navbar-inner { - -webkit-box-shadow: 0 -1px 10px rgba(0, 0, 0, 0.1); - -moz-box-shadow: 0 -1px 10px rgba(0, 0, 0, 0.1); - box-shadow: 0 -1px 10px rgba(0, 0, 0, 0.1); -} - -.navbar .nav { - position: relative; - left: 0; - display: block; - float: left; - margin: 0 10px 0 0; -} - -.navbar .nav.pull-right { - float: right; - margin-right: 0; -} - -.navbar .nav > li { - float: left; -} - -.navbar .nav > li > a { - float: none; - padding: 10px 15px 10px; - color: #777777; - text-decoration: none; - text-shadow: 0 1px 0 #ffffff; -} - -.navbar .nav .dropdown-toggle .caret { - margin-top: 8px; -} - -.navbar .nav > li > a:focus, -.navbar .nav > li > a:hover { - color: #333333; - text-decoration: none; - background-color: transparent; -} - -.navbar .nav > .active > a, -.navbar .nav > .active > a:hover, -.navbar .nav > .active > a:focus { - color: #555555; - text-decoration: none; - background-color: #e5e5e5; - -webkit-box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.125); - -moz-box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.125); - box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.125); -} - -.navbar .btn-navbar { - display: none; - float: right; - padding: 7px 10px; - margin-right: 5px; - margin-left: 5px; - color: #ffffff; - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); - background-color: #ededed; - *background-color: #e5e5e5; - background-image: -moz-linear-gradient(top, #f2f2f2, #e5e5e5); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f2f2f2), to(#e5e5e5)); - background-image: -webkit-linear-gradient(top, #f2f2f2, #e5e5e5); - background-image: -o-linear-gradient(top, #f2f2f2, #e5e5e5); - background-image: linear-gradient(to bottom, #f2f2f2, #e5e5e5); - background-repeat: repeat-x; - border-color: #e5e5e5 #e5e5e5 #bfbfbf; - border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2f2f2', endColorstr='#ffe5e5e5', GradientType=0); - filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); - -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); - -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); -} - -.navbar .btn-navbar:hover, -.navbar .btn-navbar:focus, -.navbar .btn-navbar:active, -.navbar .btn-navbar.active, -.navbar .btn-navbar.disabled, -.navbar .btn-navbar[disabled] { - color: #ffffff; - background-color: #e5e5e5; - *background-color: #d9d9d9; -} - -.navbar .btn-navbar:active, -.navbar .btn-navbar.active { - background-color: #cccccc \9; -} - -.navbar .btn-navbar .icon-bar { - display: block; - width: 18px; - height: 2px; - background-color: #f5f5f5; - -webkit-border-radius: 1px; - -moz-border-radius: 1px; - border-radius: 1px; - -webkit-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); - -moz-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); - box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); -} - -.btn-navbar .icon-bar + .icon-bar { - margin-top: 3px; -} - -.navbar .nav > li > .dropdown-menu:before { - position: absolute; - top: -7px; - left: 9px; - display: inline-block; - border-right: 7px solid transparent; - border-bottom: 7px solid #ccc; - border-left: 7px solid transparent; - border-bottom-color: rgba(0, 0, 0, 0.2); - content: ''; -} - -.navbar .nav > li > .dropdown-menu:after { - position: absolute; - top: -6px; - left: 10px; - display: inline-block; - border-right: 6px solid transparent; - border-bottom: 6px solid #ffffff; - border-left: 6px solid transparent; - content: ''; -} - -.navbar-fixed-bottom .nav > li > .dropdown-menu:before { - top: auto; - bottom: -7px; - border-top: 7px solid #ccc; - border-bottom: 0; - border-top-color: rgba(0, 0, 0, 0.2); -} - -.navbar-fixed-bottom .nav > li > .dropdown-menu:after { - top: auto; - bottom: -6px; - border-top: 6px solid #ffffff; - border-bottom: 0; -} - -.navbar .nav li.dropdown > a:hover .caret, -.navbar .nav li.dropdown > a:focus .caret { - border-top-color: #333333; - border-bottom-color: #333333; -} - -.navbar .nav li.dropdown.open > .dropdown-toggle, -.navbar .nav li.dropdown.active > .dropdown-toggle, -.navbar .nav li.dropdown.open.active > .dropdown-toggle { - color: #555555; - background-color: #e5e5e5; -} - -.navbar .nav li.dropdown > .dropdown-toggle .caret { - border-top-color: #777777; - border-bottom-color: #777777; -} - -.navbar .nav li.dropdown.open > .dropdown-toggle .caret, -.navbar .nav li.dropdown.active > .dropdown-toggle .caret, -.navbar .nav li.dropdown.open.active > .dropdown-toggle .caret { - border-top-color: #555555; - border-bottom-color: #555555; -} - -.navbar .pull-right > li > .dropdown-menu, -.navbar .nav > li > .dropdown-menu.pull-right { - right: 0; - left: auto; -} - -.navbar .pull-right > li > .dropdown-menu:before, -.navbar .nav > li > .dropdown-menu.pull-right:before { - right: 12px; - left: auto; -} - -.navbar .pull-right > li > .dropdown-menu:after, -.navbar .nav > li > .dropdown-menu.pull-right:after { - right: 13px; - left: auto; -} - -.navbar .pull-right > li > .dropdown-menu .dropdown-menu, -.navbar .nav > li > .dropdown-menu.pull-right .dropdown-menu { - right: 100%; - left: auto; - margin-right: -1px; - margin-left: 0; - -webkit-border-radius: 6px 0 6px 6px; - -moz-border-radius: 6px 0 6px 6px; - border-radius: 6px 0 6px 6px; -} - -.navbar-inverse .navbar-inner { - background-color: #1b1b1b; - background-image: -moz-linear-gradient(top, #222222, #111111); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#222222), to(#111111)); - background-image: -webkit-linear-gradient(top, #222222, #111111); - background-image: -o-linear-gradient(top, #222222, #111111); - background-image: linear-gradient(to bottom, #222222, #111111); - background-repeat: repeat-x; - border-color: #252525; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222', endColorstr='#ff111111', GradientType=0); -} - -.navbar-inverse .brand, -.navbar-inverse .nav > li > a { - color: #999999; - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); -} - -.navbar-inverse .brand:hover, -.navbar-inverse .nav > li > a:hover, -.navbar-inverse .brand:focus, -.navbar-inverse .nav > li > a:focus { - color: #ffffff; -} - -.navbar-inverse .brand { - color: #999999; -} - -.navbar-inverse .navbar-text { - color: #999999; -} - -.navbar-inverse .nav > li > a:focus, -.navbar-inverse .nav > li > a:hover { - color: #ffffff; - background-color: transparent; -} - -.navbar-inverse .nav .active > a, -.navbar-inverse .nav .active > a:hover, -.navbar-inverse .nav .active > a:focus { - color: #ffffff; - background-color: #111111; -} - -.navbar-inverse .navbar-link { - color: #999999; -} - -.navbar-inverse .navbar-link:hover, -.navbar-inverse .navbar-link:focus { - color: #ffffff; -} - -.navbar-inverse .divider-vertical { - border-right-color: #222222; - border-left-color: #111111; -} - -.navbar-inverse .nav li.dropdown.open > .dropdown-toggle, -.navbar-inverse .nav li.dropdown.active > .dropdown-toggle, -.navbar-inverse .nav li.dropdown.open.active > .dropdown-toggle { - color: #ffffff; - background-color: #111111; -} - -.navbar-inverse .nav li.dropdown > a:hover .caret, -.navbar-inverse .nav li.dropdown > a:focus .caret { - border-top-color: #ffffff; - border-bottom-color: #ffffff; -} - -.navbar-inverse .nav li.dropdown > .dropdown-toggle .caret { - border-top-color: #999999; - border-bottom-color: #999999; -} - -.navbar-inverse .nav li.dropdown.open > .dropdown-toggle .caret, -.navbar-inverse .nav li.dropdown.active > .dropdown-toggle .caret, -.navbar-inverse .nav li.dropdown.open.active > .dropdown-toggle .caret { - border-top-color: #ffffff; - border-bottom-color: #ffffff; -} - -.navbar-inverse .navbar-search .search-query { - color: #ffffff; - background-color: #515151; - border-color: #111111; - -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, 0.15); - -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, 0.15); - box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, 0.15); - -webkit-transition: none; - -moz-transition: none; - -o-transition: none; - transition: none; -} - -.navbar-inverse .navbar-search .search-query:-moz-placeholder { - color: #cccccc; -} - -.navbar-inverse .navbar-search .search-query:-ms-input-placeholder { - color: #cccccc; -} - -.navbar-inverse .navbar-search .search-query::-webkit-input-placeholder { - color: #cccccc; -} - -.navbar-inverse .navbar-search .search-query:focus, -.navbar-inverse .navbar-search .search-query.focused { - padding: 5px 15px; - color: #333333; - text-shadow: 0 1px 0 #ffffff; - background-color: #ffffff; - border: 0; - outline: 0; - -webkit-box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); - -moz-box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); - box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); -} - -.navbar-inverse .btn-navbar { - color: #ffffff; - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); - background-color: #0e0e0e; - *background-color: #040404; - background-image: -moz-linear-gradient(top, #151515, #040404); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#151515), to(#040404)); - background-image: -webkit-linear-gradient(top, #151515, #040404); - background-image: -o-linear-gradient(top, #151515, #040404); - background-image: linear-gradient(to bottom, #151515, #040404); - background-repeat: repeat-x; - border-color: #040404 #040404 #000000; - border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff151515', endColorstr='#ff040404', GradientType=0); - filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); -} - -.navbar-inverse .btn-navbar:hover, -.navbar-inverse .btn-navbar:focus, -.navbar-inverse .btn-navbar:active, -.navbar-inverse .btn-navbar.active, -.navbar-inverse .btn-navbar.disabled, -.navbar-inverse .btn-navbar[disabled] { - color: #ffffff; - background-color: #040404; - *background-color: #000000; -} - -.navbar-inverse .btn-navbar:active, -.navbar-inverse .btn-navbar.active { - background-color: #000000 \9; -} - -.breadcrumb { - padding: 8px 15px; - margin: 0 0 20px; - list-style: none; - background-color: #f5f5f5; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; -} - -.breadcrumb > li { - display: inline-block; - *display: inline; - text-shadow: 0 1px 0 #ffffff; - *zoom: 1; -} - -.breadcrumb > li > .divider { - padding: 0 5px; - color: #ccc; -} - -.breadcrumb > .active { - color: #999999; -} - -.pagination { - margin: 20px 0; -} - -.pagination ul { - display: inline-block; - *display: inline; - margin-bottom: 0; - margin-left: 0; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; - *zoom: 1; - -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); - -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); -} - -.pagination ul > li { - display: inline; -} - -.pagination ul > li > a, -.pagination ul > li > span { - float: left; - padding: 4px 12px; - line-height: 20px; - text-decoration: none; - background-color: #ffffff; - border: 1px solid #dddddd; - border-left-width: 0; -} - -.pagination ul > li > a:hover, -.pagination ul > li > a:focus, -.pagination ul > .active > a, -.pagination ul > .active > span { - background-color: #f5f5f5; -} - -.pagination ul > .active > a, -.pagination ul > .active > span { - color: #999999; - cursor: default; -} - -.pagination ul > .disabled > span, -.pagination ul > .disabled > a, -.pagination ul > .disabled > a:hover, -.pagination ul > .disabled > a:focus { - color: #999999; - cursor: default; - background-color: transparent; -} - -.pagination ul > li:first-child > a, -.pagination ul > li:first-child > span { - border-left-width: 1px; - -webkit-border-bottom-left-radius: 4px; - border-bottom-left-radius: 4px; - -webkit-border-top-left-radius: 4px; - border-top-left-radius: 4px; - -moz-border-radius-bottomleft: 4px; - -moz-border-radius-topleft: 4px; -} - -.pagination ul > li:last-child > a, -.pagination ul > li:last-child > span { - -webkit-border-top-right-radius: 4px; - border-top-right-radius: 4px; - -webkit-border-bottom-right-radius: 4px; - border-bottom-right-radius: 4px; - -moz-border-radius-topright: 4px; - -moz-border-radius-bottomright: 4px; -} - -.pagination-centered { - text-align: center; -} - -.pagination-right { - text-align: right; -} - -.pagination-large ul > li > a, -.pagination-large ul > li > span { - padding: 11px 19px; - font-size: 17.5px; -} - -.pagination-large ul > li:first-child > a, -.pagination-large ul > li:first-child > span { - -webkit-border-bottom-left-radius: 6px; - border-bottom-left-radius: 6px; - -webkit-border-top-left-radius: 6px; - border-top-left-radius: 6px; - -moz-border-radius-bottomleft: 6px; - -moz-border-radius-topleft: 6px; -} - -.pagination-large ul > li:last-child > a, -.pagination-large ul > li:last-child > span { - -webkit-border-top-right-radius: 6px; - border-top-right-radius: 6px; - -webkit-border-bottom-right-radius: 6px; - border-bottom-right-radius: 6px; - -moz-border-radius-topright: 6px; - -moz-border-radius-bottomright: 6px; -} - -.pagination-mini ul > li:first-child > a, -.pagination-small ul > li:first-child > a, -.pagination-mini ul > li:first-child > span, -.pagination-small ul > li:first-child > span { - -webkit-border-bottom-left-radius: 3px; - border-bottom-left-radius: 3px; - -webkit-border-top-left-radius: 3px; - border-top-left-radius: 3px; - -moz-border-radius-bottomleft: 3px; - -moz-border-radius-topleft: 3px; -} - -.pagination-mini ul > li:last-child > a, -.pagination-small ul > li:last-child > a, -.pagination-mini ul > li:last-child > span, -.pagination-small ul > li:last-child > span { - -webkit-border-top-right-radius: 3px; - border-top-right-radius: 3px; - -webkit-border-bottom-right-radius: 3px; - border-bottom-right-radius: 3px; - -moz-border-radius-topright: 3px; - -moz-border-radius-bottomright: 3px; -} - -.pagination-small ul > li > a, -.pagination-small ul > li > span { - padding: 2px 10px; - font-size: 11.9px; -} - -.pagination-mini ul > li > a, -.pagination-mini ul > li > span { - padding: 0 6px; - font-size: 10.5px; -} - -.pager { - margin: 20px 0; - text-align: center; - list-style: none; - *zoom: 1; -} - -.pager:before, -.pager:after { - display: table; - line-height: 0; - content: ""; -} - -.pager:after { - clear: both; -} - -.pager li { - display: inline; -} - -.pager li > a, -.pager li > span { - display: inline-block; - padding: 5px 14px; - background-color: #fff; - border: 1px solid #ddd; - -webkit-border-radius: 15px; - -moz-border-radius: 15px; - border-radius: 15px; -} - -.pager li > a:hover, -.pager li > a:focus { - text-decoration: none; - background-color: #f5f5f5; -} - -.pager .next > a, -.pager .next > span { - float: right; -} - -.pager .previous > a, -.pager .previous > span { - float: left; -} - -.pager .disabled > a, -.pager .disabled > a:hover, -.pager .disabled > a:focus, -.pager .disabled > span { - color: #999999; - cursor: default; - background-color: #fff; -} - -.modal-backdrop { - position: fixed; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: 1040; - background-color: #000000; -} - -.modal-backdrop.fade { - opacity: 0; -} - -.modal-backdrop, -.modal-backdrop.fade.in { - opacity: 0.8; - filter: alpha(opacity=80); -} - -.modal { - position: fixed; - top: 10%; - left: 50%; - z-index: 1050; - width: 560px; - margin-left: -280px; - background-color: #ffffff; - border: 1px solid #999; - border: 1px solid rgba(0, 0, 0, 0.3); - *border: 1px solid #999; - -webkit-border-radius: 6px; - -moz-border-radius: 6px; - border-radius: 6px; - outline: none; - -webkit-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); - -moz-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); - box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); - -webkit-background-clip: padding-box; - -moz-background-clip: padding-box; - background-clip: padding-box; -} - -.modal.fade { - top: -25%; - -webkit-transition: opacity 0.3s linear, top 0.3s ease-out; - -moz-transition: opacity 0.3s linear, top 0.3s ease-out; - -o-transition: opacity 0.3s linear, top 0.3s ease-out; - transition: opacity 0.3s linear, top 0.3s ease-out; -} - -.modal.fade.in { - top: 10%; -} - -.modal-header { - padding: 9px 15px; - border-bottom: 1px solid #eee; -} - -.modal-header .close { - margin-top: 2px; -} - -.modal-header h3 { - margin: 0; - line-height: 30px; -} - -.modal-body { - position: relative; - max-height: 400px; - padding: 15px; - overflow-y: auto; -} - -.modal-form { - margin-bottom: 0; -} - -.modal-footer { - padding: 14px 15px 15px; - margin-bottom: 0; - text-align: right; - background-color: #f5f5f5; - border-top: 1px solid #ddd; - -webkit-border-radius: 0 0 6px 6px; - -moz-border-radius: 0 0 6px 6px; - border-radius: 0 0 6px 6px; - *zoom: 1; - -webkit-box-shadow: inset 0 1px 0 #ffffff; - -moz-box-shadow: inset 0 1px 0 #ffffff; - box-shadow: inset 0 1px 0 #ffffff; -} - -.modal-footer:before, -.modal-footer:after { - display: table; - line-height: 0; - content: ""; -} - -.modal-footer:after { - clear: both; -} - -.modal-footer .btn + .btn { - margin-bottom: 0; - margin-left: 5px; -} - -.modal-footer .btn-group .btn + .btn { - margin-left: -1px; -} - -.modal-footer .btn-block + .btn-block { - margin-left: 0; -} - -.tooltip { - position: absolute; - z-index: 1030; - display: block; - font-size: 11px; - line-height: 1.4; - opacity: 0; - filter: alpha(opacity=0); - visibility: visible; -} - -.tooltip.in { - opacity: 0.8; - filter: alpha(opacity=80); -} - -.tooltip.top { - padding: 5px 0; - margin-top: -3px; -} - -.tooltip.right { - padding: 0 5px; - margin-left: 3px; -} - -.tooltip.bottom { - padding: 5px 0; - margin-top: 3px; -} - -.tooltip.left { - padding: 0 5px; - margin-left: -3px; -} - -.tooltip-inner { - max-width: 200px; - padding: 8px; - color: #ffffff; - text-align: center; - text-decoration: none; - background-color: #000000; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; -} - -.tooltip-arrow { - position: absolute; - width: 0; - height: 0; - border-color: transparent; - border-style: solid; -} - -.tooltip.top .tooltip-arrow { - bottom: 0; - left: 50%; - margin-left: -5px; - border-top-color: #000000; - border-width: 5px 5px 0; -} - -.tooltip.right .tooltip-arrow { - top: 50%; - left: 0; - margin-top: -5px; - border-right-color: #000000; - border-width: 5px 5px 5px 0; -} - -.tooltip.left .tooltip-arrow { - top: 50%; - right: 0; - margin-top: -5px; - border-left-color: #000000; - border-width: 5px 0 5px 5px; -} - -.tooltip.bottom .tooltip-arrow { - top: 0; - left: 50%; - margin-left: -5px; - border-bottom-color: #000000; - border-width: 0 5px 5px; -} - -.popover { - position: absolute; - top: 0; - left: 0; - z-index: 1010; - display: none; - max-width: 276px; - padding: 1px; - text-align: left; - white-space: normal; - background-color: #ffffff; - border: 1px solid #ccc; - border: 1px solid rgba(0, 0, 0, 0.2); - -webkit-border-radius: 6px; - -moz-border-radius: 6px; - border-radius: 6px; - -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); - -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); - box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); - -webkit-background-clip: padding-box; - -moz-background-clip: padding; - background-clip: padding-box; -} - -.popover.top { - margin-top: -10px; -} - -.popover.right { - margin-left: 10px; -} - -.popover.bottom { - margin-top: 10px; -} - -.popover.left { - margin-left: -10px; -} - -.popover-title { - padding: 8px 14px; - margin: 0; - font-size: 14px; - font-weight: normal; - line-height: 18px; - background-color: #f7f7f7; - border-bottom: 1px solid #ebebeb; - -webkit-border-radius: 5px 5px 0 0; - -moz-border-radius: 5px 5px 0 0; - border-radius: 5px 5px 0 0; -} - -.popover-title:empty { - display: none; -} - -.popover-content { - padding: 9px 14px; -} - -.popover .arrow, -.popover .arrow:after { - position: absolute; - display: block; - width: 0; - height: 0; - border-color: transparent; - border-style: solid; -} - -.popover .arrow { - border-width: 11px; -} - -.popover .arrow:after { - border-width: 10px; - content: ""; -} - -.popover.top .arrow { - bottom: -11px; - left: 50%; - margin-left: -11px; - border-top-color: #999; - border-top-color: rgba(0, 0, 0, 0.25); - border-bottom-width: 0; -} - -.popover.top .arrow:after { - bottom: 1px; - margin-left: -10px; - border-top-color: #ffffff; - border-bottom-width: 0; -} - -.popover.right .arrow { - top: 50%; - left: -11px; - margin-top: -11px; - border-right-color: #999; - border-right-color: rgba(0, 0, 0, 0.25); - border-left-width: 0; -} - -.popover.right .arrow:after { - bottom: -10px; - left: 1px; - border-right-color: #ffffff; - border-left-width: 0; -} - -.popover.bottom .arrow { - top: -11px; - left: 50%; - margin-left: -11px; - border-bottom-color: #999; - border-bottom-color: rgba(0, 0, 0, 0.25); - border-top-width: 0; -} - -.popover.bottom .arrow:after { - top: 1px; - margin-left: -10px; - border-bottom-color: #ffffff; - border-top-width: 0; -} - -.popover.left .arrow { - top: 50%; - right: -11px; - margin-top: -11px; - border-left-color: #999; - border-left-color: rgba(0, 0, 0, 0.25); - border-right-width: 0; -} - -.popover.left .arrow:after { - right: 1px; - bottom: -10px; - border-left-color: #ffffff; - border-right-width: 0; -} - -.thumbnails { - margin-left: -20px; - list-style: none; - *zoom: 1; -} - -.thumbnails:before, -.thumbnails:after { - display: table; - line-height: 0; - content: ""; -} - -.thumbnails:after { - clear: both; -} - -.row-fluid .thumbnails { - margin-left: 0; -} - -.thumbnails > li { - float: left; - margin-bottom: 20px; - margin-left: 20px; -} - -.thumbnail { - display: block; - padding: 4px; - line-height: 20px; - border: 1px solid #ddd; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; - -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.055); - -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.055); - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.055); - -webkit-transition: all 0.2s ease-in-out; - -moz-transition: all 0.2s ease-in-out; - -o-transition: all 0.2s ease-in-out; - transition: all 0.2s ease-in-out; -} - -a.thumbnail:hover, -a.thumbnail:focus { - border-color: #0088cc; - -webkit-box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); - -moz-box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); - box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); -} - -.thumbnail > img { - display: block; - max-width: 100%; - margin-right: auto; - margin-left: auto; -} - -.thumbnail .caption { - padding: 9px; - color: #555555; -} - -.media, -.media-body { - overflow: hidden; - *overflow: visible; - zoom: 1; -} - -.media, -.media .media { - margin-top: 15px; -} - -.media:first-child { - margin-top: 0; -} - -.media-object { - display: block; -} - -.media-heading { - margin: 0 0 5px; -} - -.media > .pull-left { - margin-right: 10px; -} - -.media > .pull-right { - margin-left: 10px; -} - -.media-list { - margin-left: 0; - list-style: none; -} - -.label, -.badge { - display: inline-block; - padding: 2px 4px; - font-size: 11.844px; - font-weight: bold; - line-height: 14px; - color: #ffffff; - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); - white-space: nowrap; - vertical-align: baseline; - background-color: #999999; -} - -.label { - -webkit-border-radius: 3px; - -moz-border-radius: 3px; - border-radius: 3px; -} - -.badge { - padding-right: 9px; - padding-left: 9px; - -webkit-border-radius: 9px; - -moz-border-radius: 9px; - border-radius: 9px; -} - -.label:empty, -.badge:empty { - display: none; -} - -a.label:hover, -a.label:focus, -a.badge:hover, -a.badge:focus { - color: #ffffff; - text-decoration: none; - cursor: pointer; -} - -.label-important, -.badge-important { - background-color: #b94a48; -} - -.label-important[href], -.badge-important[href] { - background-color: #953b39; -} - -.label-warning, -.badge-warning { - background-color: #f89406; -} - -.label-warning[href], -.badge-warning[href] { - background-color: #c67605; -} - -.label-success, -.badge-success { - background-color: #468847; -} - -.label-success[href], -.badge-success[href] { - background-color: #356635; -} - -.label-info, -.badge-info { - background-color: #3a87ad; -} - -.label-info[href], -.badge-info[href] { - background-color: #2d6987; -} - -.label-inverse, -.badge-inverse { - background-color: #333333; -} - -.label-inverse[href], -.badge-inverse[href] { - background-color: #1a1a1a; -} - -.btn .label, -.btn .badge { - position: relative; - top: -1px; -} - -.btn-mini .label, -.btn-mini .badge { - top: 0; -} - -@-webkit-keyframes progress-bar-stripes { - from { - background-position: 40px 0; - } - to { - background-position: 0 0; - } -} - -@-moz-keyframes progress-bar-stripes { - from { - background-position: 40px 0; - } - to { - background-position: 0 0; - } -} - -@-ms-keyframes progress-bar-stripes { - from { - background-position: 40px 0; - } - to { - background-position: 0 0; - } -} - -@-o-keyframes progress-bar-stripes { - from { - background-position: 0 0; - } - to { - background-position: 40px 0; - } -} - -@keyframes progress-bar-stripes { - from { - background-position: 40px 0; - } - to { - background-position: 0 0; - } -} - -.progress { - height: 20px; - margin-bottom: 20px; - overflow: hidden; - background-color: #f7f7f7; - background-image: -moz-linear-gradient(top, #f5f5f5, #f9f9f9); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f5f5f5), to(#f9f9f9)); - background-image: -webkit-linear-gradient(top, #f5f5f5, #f9f9f9); - background-image: -o-linear-gradient(top, #f5f5f5, #f9f9f9); - background-image: linear-gradient(to bottom, #f5f5f5, #f9f9f9); - background-repeat: repeat-x; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#fff9f9f9', GradientType=0); - -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); - -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); - box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); -} - -.progress .bar { - float: left; - width: 0; - height: 100%; - font-size: 12px; - color: #ffffff; - text-align: center; - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); - background-color: #0e90d2; - background-image: -moz-linear-gradient(top, #149bdf, #0480be); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#149bdf), to(#0480be)); - background-image: -webkit-linear-gradient(top, #149bdf, #0480be); - background-image: -o-linear-gradient(top, #149bdf, #0480be); - background-image: linear-gradient(to bottom, #149bdf, #0480be); - background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff149bdf', endColorstr='#ff0480be', GradientType=0); - -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); - -moz-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); - box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - -webkit-transition: width 0.6s ease; - -moz-transition: width 0.6s ease; - -o-transition: width 0.6s ease; - transition: width 0.6s ease; -} - -.progress .bar + .bar { - -webkit-box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.15), inset 0 -1px 0 rgba(0, 0, 0, 0.15); - -moz-box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.15), inset 0 -1px 0 rgba(0, 0, 0, 0.15); - box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.15), inset 0 -1px 0 rgba(0, 0, 0, 0.15); -} - -.progress-striped .bar { - background-color: #149bdf; - background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); - background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - -webkit-background-size: 40px 40px; - -moz-background-size: 40px 40px; - -o-background-size: 40px 40px; - background-size: 40px 40px; -} - -.progress.active .bar { - -webkit-animation: progress-bar-stripes 2s linear infinite; - -moz-animation: progress-bar-stripes 2s linear infinite; - -ms-animation: progress-bar-stripes 2s linear infinite; - -o-animation: progress-bar-stripes 2s linear infinite; - animation: progress-bar-stripes 2s linear infinite; -} - -.progress-danger .bar, -.progress .bar-danger { - background-color: #dd514c; - background-image: -moz-linear-gradient(top, #ee5f5b, #c43c35); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#c43c35)); - background-image: -webkit-linear-gradient(top, #ee5f5b, #c43c35); - background-image: -o-linear-gradient(top, #ee5f5b, #c43c35); - background-image: linear-gradient(to bottom, #ee5f5b, #c43c35); - background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffee5f5b', endColorstr='#ffc43c35', GradientType=0); -} - -.progress-danger.progress-striped .bar, -.progress-striped .bar-danger { - background-color: #ee5f5b; - background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); - background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); -} - -.progress-success .bar, -.progress .bar-success { - background-color: #5eb95e; - background-image: -moz-linear-gradient(top, #62c462, #57a957); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#57a957)); - background-image: -webkit-linear-gradient(top, #62c462, #57a957); - background-image: -o-linear-gradient(top, #62c462, #57a957); - background-image: linear-gradient(to bottom, #62c462, #57a957); - background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff62c462', endColorstr='#ff57a957', GradientType=0); -} - -.progress-success.progress-striped .bar, -.progress-striped .bar-success { - background-color: #62c462; - background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); - background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); -} - -.progress-info .bar, -.progress .bar-info { - background-color: #4bb1cf; - background-image: -moz-linear-gradient(top, #5bc0de, #339bb9); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#339bb9)); - background-image: -webkit-linear-gradient(top, #5bc0de, #339bb9); - background-image: -o-linear-gradient(top, #5bc0de, #339bb9); - background-image: linear-gradient(to bottom, #5bc0de, #339bb9); - background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff339bb9', GradientType=0); -} - -.progress-info.progress-striped .bar, -.progress-striped .bar-info { - background-color: #5bc0de; - background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); - background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); -} - -.progress-warning .bar, -.progress .bar-warning { - background-color: #faa732; - background-image: -moz-linear-gradient(top, #fbb450, #f89406); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406)); - background-image: -webkit-linear-gradient(top, #fbb450, #f89406); - background-image: -o-linear-gradient(top, #fbb450, #f89406); - background-image: linear-gradient(to bottom, #fbb450, #f89406); - background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450', endColorstr='#fff89406', GradientType=0); -} - -.progress-warning.progress-striped .bar, -.progress-striped .bar-warning { - background-color: #fbb450; - background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); - background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); -} - -.accordion { - margin-bottom: 20px; -} - -.accordion-group { - margin-bottom: 2px; - border: 1px solid #e5e5e5; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; -} - -.accordion-heading { - border-bottom: 0; -} - -.accordion-heading .accordion-toggle { - display: block; - padding: 8px 15px; -} - -.accordion-toggle { - cursor: pointer; -} - -.accordion-inner { - padding: 9px 15px; - border-top: 1px solid #e5e5e5; -} - -.carousel { - position: relative; - margin-bottom: 20px; - line-height: 1; -} - -.carousel-inner { - position: relative; - width: 100%; - overflow: hidden; -} - -.carousel-inner > .item { - position: relative; - display: none; - -webkit-transition: 0.6s ease-in-out left; - -moz-transition: 0.6s ease-in-out left; - -o-transition: 0.6s ease-in-out left; - transition: 0.6s ease-in-out left; -} - -.carousel-inner > .item > img, -.carousel-inner > .item > a > img { - display: block; - line-height: 1; -} - -.carousel-inner > .active, -.carousel-inner > .next, -.carousel-inner > .prev { - display: block; -} - -.carousel-inner > .active { - left: 0; -} - -.carousel-inner > .next, -.carousel-inner > .prev { - position: absolute; - top: 0; - width: 100%; -} - -.carousel-inner > .next { - left: 100%; -} - -.carousel-inner > .prev { - left: -100%; -} - -.carousel-inner > .next.left, -.carousel-inner > .prev.right { - left: 0; -} - -.carousel-inner > .active.left { - left: -100%; -} - -.carousel-inner > .active.right { - left: 100%; -} - -.carousel-control { - position: absolute; - top: 40%; - left: 15px; - width: 40px; - height: 40px; - margin-top: -20px; - font-size: 60px; - font-weight: 100; - line-height: 30px; - color: #ffffff; - text-align: center; - background: #222222; - border: 3px solid #ffffff; - -webkit-border-radius: 23px; - -moz-border-radius: 23px; - border-radius: 23px; - opacity: 0.5; - filter: alpha(opacity=50); -} - -.carousel-control.right { - right: 15px; - left: auto; -} - -.carousel-control:hover, -.carousel-control:focus { - color: #ffffff; - text-decoration: none; - opacity: 0.9; - filter: alpha(opacity=90); -} - -.carousel-indicators { - position: absolute; - top: 15px; - right: 15px; - z-index: 5; - margin: 0; - list-style: none; -} - -.carousel-indicators li { - display: block; - float: left; - width: 10px; - height: 10px; - margin-left: 5px; - text-indent: -999px; - background-color: #ccc; - background-color: rgba(255, 255, 255, 0.25); - border-radius: 5px; -} - -.carousel-indicators .active { - background-color: #fff; -} - -.carousel-caption { - position: absolute; - right: 0; - bottom: 0; - left: 0; - padding: 15px; - background: #333333; - background: rgba(0, 0, 0, 0.75); -} - -.carousel-caption h4, -.carousel-caption p { - line-height: 20px; - color: #ffffff; -} - -.carousel-caption h4 { - margin: 0 0 5px; -} - -.carousel-caption p { - margin-bottom: 0; -} - -.hero-unit { - padding: 60px; - margin-bottom: 30px; - font-size: 18px; - font-weight: 200; - line-height: 30px; - color: inherit; - background-color: #eeeeee; - -webkit-border-radius: 6px; - -moz-border-radius: 6px; - border-radius: 6px; -} - -.hero-unit h1 { - margin-bottom: 0; - font-size: 60px; - line-height: 1; - letter-spacing: -1px; - color: inherit; -} - -.hero-unit li { - line-height: 30px; -} - -.pull-right { - float: right; -} - -.pull-left { - float: left; -} - -.hide { - display: none; -} - -.show { - display: block; -} - -.invisible { - visibility: hidden; -} - -.affix { - position: fixed; -} diff --git a/res/bootstrap.min.js b/res/bootstrap.min.js deleted file mode 100644 index 95c5ac5ee6..0000000000 --- a/res/bootstrap.min.js +++ /dev/null @@ -1,6 +0,0 @@ -/*! -* Bootstrap.js by @fat & @mdo -* Copyright 2012 Twitter, Inc. -* http://www.apache.org/licenses/LICENSE-2.0.txt -*/ -!function(e){"use strict";e(function(){e.support.transition=function(){var e=function(){var e=document.createElement("bootstrap"),t={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"},n;for(n in t)if(e.style[n]!==undefined)return t[n]}();return e&&{end:e}}()})}(window.jQuery),!function(e){"use strict";var t='[data-dismiss="alert"]',n=function(n){e(n).on("click",t,this.close)};n.prototype.close=function(t){function s(){i.trigger("closed").remove()}var n=e(this),r=n.attr("data-target"),i;r||(r=n.attr("href"),r=r&&r.replace(/.*(?=#[^\s]*$)/,"")),i=e(r),t&&t.preventDefault(),i.length||(i=n.hasClass("alert")?n:n.parent()),i.trigger(t=e.Event("close"));if(t.isDefaultPrevented())return;i.removeClass("in"),e.support.transition&&i.hasClass("fade")?i.on(e.support.transition.end,s):s()};var r=e.fn.alert;e.fn.alert=function(t){return this.each(function(){var r=e(this),i=r.data("alert");i||r.data("alert",i=new n(this)),typeof t=="string"&&i[t].call(r)})},e.fn.alert.Constructor=n,e.fn.alert.noConflict=function(){return e.fn.alert=r,this},e(document).on("click.alert.data-api",t,n.prototype.close)}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=e.extend({},e.fn.button.defaults,n)};t.prototype.setState=function(e){var t="disabled",n=this.$element,r=n.data(),i=n.is("input")?"val":"html";e+="Text",r.resetText||n.data("resetText",n[i]()),n[i](r[e]||this.options[e]),setTimeout(function(){e=="loadingText"?n.addClass(t).attr(t,t):n.removeClass(t).removeAttr(t)},0)},t.prototype.toggle=function(){var e=this.$element.closest('[data-toggle="buttons-radio"]');e&&e.find(".active").removeClass("active"),this.$element.toggleClass("active")};var n=e.fn.button;e.fn.button=function(n){return this.each(function(){var r=e(this),i=r.data("button"),s=typeof n=="object"&&n;i||r.data("button",i=new t(this,s)),n=="toggle"?i.toggle():n&&i.setState(n)})},e.fn.button.defaults={loadingText:"loading..."},e.fn.button.Constructor=t,e.fn.button.noConflict=function(){return e.fn.button=n,this},e(document).on("click.button.data-api","[data-toggle^=button]",function(t){var n=e(t.target);n.hasClass("btn")||(n=n.closest(".btn")),n.button("toggle")})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.$indicators=this.$element.find(".carousel-indicators"),this.options=n,this.options.pause=="hover"&&this.$element.on("mouseenter",e.proxy(this.pause,this)).on("mouseleave",e.proxy(this.cycle,this))};t.prototype={cycle:function(t){return t||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(e.proxy(this.next,this),this.options.interval)),this},getActiveIndex:function(){return this.$active=this.$element.find(".item.active"),this.$items=this.$active.parent().children(),this.$items.index(this.$active)},to:function(t){var n=this.getActiveIndex(),r=this;if(t>this.$items.length-1||t<0)return;return this.sliding?this.$element.one("slid",function(){r.to(t)}):n==t?this.pause().cycle():this.slide(t>n?"next":"prev",e(this.$items[t]))},pause:function(t){return t||(this.paused=!0),this.$element.find(".next, .prev").length&&e.support.transition.end&&(this.$element.trigger(e.support.transition.end),this.cycle(!0)),clearInterval(this.interval),this.interval=null,this},next:function(){if(this.sliding)return;return this.slide("next")},prev:function(){if(this.sliding)return;return this.slide("prev")},slide:function(t,n){var r=this.$element.find(".item.active"),i=n||r[t](),s=this.interval,o=t=="next"?"left":"right",u=t=="next"?"first":"last",a=this,f;this.sliding=!0,s&&this.pause(),i=i.length?i:this.$element.find(".item")[u](),f=e.Event("slide",{relatedTarget:i[0],direction:o});if(i.hasClass("active"))return;this.$indicators.length&&(this.$indicators.find(".active").removeClass("active"),this.$element.one("slid",function(){var t=e(a.$indicators.children()[a.getActiveIndex()]);t&&t.addClass("active")}));if(e.support.transition&&this.$element.hasClass("slide")){this.$element.trigger(f);if(f.isDefaultPrevented())return;i.addClass(t),i[0].offsetWidth,r.addClass(o),i.addClass(o),this.$element.one(e.support.transition.end,function(){i.removeClass([t,o].join(" ")).addClass("active"),r.removeClass(["active",o].join(" ")),a.sliding=!1,setTimeout(function(){a.$element.trigger("slid")},0)})}else{this.$element.trigger(f);if(f.isDefaultPrevented())return;r.removeClass("active"),i.addClass("active"),this.sliding=!1,this.$element.trigger("slid")}return s&&this.cycle(),this}};var n=e.fn.carousel;e.fn.carousel=function(n){return this.each(function(){var r=e(this),i=r.data("carousel"),s=e.extend({},e.fn.carousel.defaults,typeof n=="object"&&n),o=typeof n=="string"?n:s.slide;i||r.data("carousel",i=new t(this,s)),typeof n=="number"?i.to(n):o?i[o]():s.interval&&i.pause().cycle()})},e.fn.carousel.defaults={interval:5e3,pause:"hover"},e.fn.carousel.Constructor=t,e.fn.carousel.noConflict=function(){return e.fn.carousel=n,this},e(document).on("click.carousel.data-api","[data-slide], [data-slide-to]",function(t){var n=e(this),r,i=e(n.attr("data-target")||(r=n.attr("href"))&&r.replace(/.*(?=#[^\s]+$)/,"")),s=e.extend({},i.data(),n.data()),o;i.carousel(s),(o=n.attr("data-slide-to"))&&i.data("carousel").pause().to(o).cycle(),t.preventDefault()})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=e.extend({},e.fn.collapse.defaults,n),this.options.parent&&(this.$parent=e(this.options.parent)),this.options.toggle&&this.toggle()};t.prototype={constructor:t,dimension:function(){var e=this.$element.hasClass("width");return e?"width":"height"},show:function(){var t,n,r,i;if(this.transitioning||this.$element.hasClass("in"))return;t=this.dimension(),n=e.camelCase(["scroll",t].join("-")),r=this.$parent&&this.$parent.find("> .accordion-group > .in");if(r&&r.length){i=r.data("collapse");if(i&&i.transitioning)return;r.collapse("hide"),i||r.data("collapse",null)}this.$element[t](0),this.transition("addClass",e.Event("show"),"shown"),e.support.transition&&this.$element[t](this.$element[0][n])},hide:function(){var t;if(this.transitioning||!this.$element.hasClass("in"))return;t=this.dimension(),this.reset(this.$element[t]()),this.transition("removeClass",e.Event("hide"),"hidden"),this.$element[t](0)},reset:function(e){var t=this.dimension();return this.$element.removeClass("collapse")[t](e||"auto")[0].offsetWidth,this.$element[e!==null?"addClass":"removeClass"]("collapse"),this},transition:function(t,n,r){var i=this,s=function(){n.type=="show"&&i.reset(),i.transitioning=0,i.$element.trigger(r)};this.$element.trigger(n);if(n.isDefaultPrevented())return;this.transitioning=1,this.$element[t]("in"),e.support.transition&&this.$element.hasClass("collapse")?this.$element.one(e.support.transition.end,s):s()},toggle:function(){this[this.$element.hasClass("in")?"hide":"show"]()}};var n=e.fn.collapse;e.fn.collapse=function(n){return this.each(function(){var r=e(this),i=r.data("collapse"),s=e.extend({},e.fn.collapse.defaults,r.data(),typeof n=="object"&&n);i||r.data("collapse",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.collapse.defaults={toggle:!0},e.fn.collapse.Constructor=t,e.fn.collapse.noConflict=function(){return e.fn.collapse=n,this},e(document).on("click.collapse.data-api","[data-toggle=collapse]",function(t){var n=e(this),r,i=n.attr("data-target")||t.preventDefault()||(r=n.attr("href"))&&r.replace(/.*(?=#[^\s]+$)/,""),s=e(i).data("collapse")?"toggle":n.data();n[e(i).hasClass("in")?"addClass":"removeClass"]("collapsed"),e(i).collapse(s)})}(window.jQuery),!function(e){"use strict";function r(){e(t).each(function(){i(e(this)).removeClass("open")})}function i(t){var n=t.attr("data-target"),r;n||(n=t.attr("href"),n=n&&/#/.test(n)&&n.replace(/.*(?=#[^\s]*$)/,"")),r=n&&e(n);if(!r||!r.length)r=t.parent();return r}var t="[data-toggle=dropdown]",n=function(t){var n=e(t).on("click.dropdown.data-api",this.toggle);e("html").on("click.dropdown.data-api",function(){n.parent().removeClass("open")})};n.prototype={constructor:n,toggle:function(t){var n=e(this),s,o;if(n.is(".disabled, :disabled"))return;return s=i(n),o=s.hasClass("open"),r(),o||s.toggleClass("open"),n.focus(),!1},keydown:function(n){var r,s,o,u,a,f;if(!/(38|40|27)/.test(n.keyCode))return;r=e(this),n.preventDefault(),n.stopPropagation();if(r.is(".disabled, :disabled"))return;u=i(r),a=u.hasClass("open");if(!a||a&&n.keyCode==27)return n.which==27&&u.find(t).focus(),r.click();s=e("[role=menu] li:not(.divider):visible a",u);if(!s.length)return;f=s.index(s.filter(":focus")),n.keyCode==38&&f>0&&f--,n.keyCode==40&&f<s.length-1&&f++,~f||(f=0),s.eq(f).focus()}};var s=e.fn.dropdown;e.fn.dropdown=function(t){return this.each(function(){var r=e(this),i=r.data("dropdown");i||r.data("dropdown",i=new n(this)),typeof t=="string"&&i[t].call(r)})},e.fn.dropdown.Constructor=n,e.fn.dropdown.noConflict=function(){return e.fn.dropdown=s,this},e(document).on("click.dropdown.data-api",r).on("click.dropdown.data-api",".dropdown form",function(e){e.stopPropagation()}).on("click.dropdown-menu",function(e){e.stopPropagation()}).on("click.dropdown.data-api",t,n.prototype.toggle).on("keydown.dropdown.data-api",t+", [role=menu]",n.prototype.keydown)}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.options=n,this.$element=e(t).delegate('[data-dismiss="modal"]',"click.dismiss.modal",e.proxy(this.hide,this)),this.options.remote&&this.$element.find(".modal-body").load(this.options.remote)};t.prototype={constructor:t,toggle:function(){return this[this.isShown?"hide":"show"]()},show:function(){var t=this,n=e.Event("show");this.$element.trigger(n);if(this.isShown||n.isDefaultPrevented())return;this.isShown=!0,this.escape(),this.backdrop(function(){var n=e.support.transition&&t.$element.hasClass("fade");t.$element.parent().length||t.$element.appendTo(document.body),t.$element.show(),n&&t.$element[0].offsetWidth,t.$element.addClass("in").attr("aria-hidden",!1),t.enforceFocus(),n?t.$element.one(e.support.transition.end,function(){t.$element.focus().trigger("shown")}):t.$element.focus().trigger("shown")})},hide:function(t){t&&t.preventDefault();var n=this;t=e.Event("hide"),this.$element.trigger(t);if(!this.isShown||t.isDefaultPrevented())return;this.isShown=!1,this.escape(),e(document).off("focusin.modal"),this.$element.removeClass("in").attr("aria-hidden",!0),e.support.transition&&this.$element.hasClass("fade")?this.hideWithTransition():this.hideModal()},enforceFocus:function(){var t=this;e(document).on("focusin.modal",function(e){t.$element[0]!==e.target&&!t.$element.has(e.target).length&&t.$element.focus()})},escape:function(){var e=this;this.isShown&&this.options.keyboard?this.$element.on("keyup.dismiss.modal",function(t){t.which==27&&e.hide()}):this.isShown||this.$element.off("keyup.dismiss.modal")},hideWithTransition:function(){var t=this,n=setTimeout(function(){t.$element.off(e.support.transition.end),t.hideModal()},500);this.$element.one(e.support.transition.end,function(){clearTimeout(n),t.hideModal()})},hideModal:function(){var e=this;this.$element.hide(),this.backdrop(function(){e.removeBackdrop(),e.$element.trigger("hidden")})},removeBackdrop:function(){this.$backdrop&&this.$backdrop.remove(),this.$backdrop=null},backdrop:function(t){var n=this,r=this.$element.hasClass("fade")?"fade":"";if(this.isShown&&this.options.backdrop){var i=e.support.transition&&r;this.$backdrop=e('<div class="modal-backdrop '+r+'" />').appendTo(document.body),this.$backdrop.click(this.options.backdrop=="static"?e.proxy(this.$element[0].focus,this.$element[0]):e.proxy(this.hide,this)),i&&this.$backdrop[0].offsetWidth,this.$backdrop.addClass("in");if(!t)return;i?this.$backdrop.one(e.support.transition.end,t):t()}else!this.isShown&&this.$backdrop?(this.$backdrop.removeClass("in"),e.support.transition&&this.$element.hasClass("fade")?this.$backdrop.one(e.support.transition.end,t):t()):t&&t()}};var n=e.fn.modal;e.fn.modal=function(n){return this.each(function(){var r=e(this),i=r.data("modal"),s=e.extend({},e.fn.modal.defaults,r.data(),typeof n=="object"&&n);i||r.data("modal",i=new t(this,s)),typeof n=="string"?i[n]():s.show&&i.show()})},e.fn.modal.defaults={backdrop:!0,keyboard:!0,show:!0},e.fn.modal.Constructor=t,e.fn.modal.noConflict=function(){return e.fn.modal=n,this},e(document).on("click.modal.data-api",'[data-toggle="modal"]',function(t){var n=e(this),r=n.attr("href"),i=e(n.attr("data-target")||r&&r.replace(/.*(?=#[^\s]+$)/,"")),s=i.data("modal")?"toggle":e.extend({remote:!/#/.test(r)&&r},i.data(),n.data());t.preventDefault(),i.modal(s).one("hide",function(){n.focus()})})}(window.jQuery),!function(e){"use strict";var t=function(e,t){this.init("tooltip",e,t)};t.prototype={constructor:t,init:function(t,n,r){var i,s,o,u,a;this.type=t,this.$element=e(n),this.options=this.getOptions(r),this.enabled=!0,o=this.options.trigger.split(" ");for(a=o.length;a--;)u=o[a],u=="click"?this.$element.on("click."+this.type,this.options.selector,e.proxy(this.toggle,this)):u!="manual"&&(i=u=="hover"?"mouseenter":"focus",s=u=="hover"?"mouseleave":"blur",this.$element.on(i+"."+this.type,this.options.selector,e.proxy(this.enter,this)),this.$element.on(s+"."+this.type,this.options.selector,e.proxy(this.leave,this)));this.options.selector?this._options=e.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},getOptions:function(t){return t=e.extend({},e.fn[this.type].defaults,this.$element.data(),t),t.delay&&typeof t.delay=="number"&&(t.delay={show:t.delay,hide:t.delay}),t},enter:function(t){var n=e.fn[this.type].defaults,r={},i;this._options&&e.each(this._options,function(e,t){n[e]!=t&&(r[e]=t)},this),i=e(t.currentTarget)[this.type](r).data(this.type);if(!i.options.delay||!i.options.delay.show)return i.show();clearTimeout(this.timeout),i.hoverState="in",this.timeout=setTimeout(function(){i.hoverState=="in"&&i.show()},i.options.delay.show)},leave:function(t){var n=e(t.currentTarget)[this.type](this._options).data(this.type);this.timeout&&clearTimeout(this.timeout);if(!n.options.delay||!n.options.delay.hide)return n.hide();n.hoverState="out",this.timeout=setTimeout(function(){n.hoverState=="out"&&n.hide()},n.options.delay.hide)},show:function(){var t,n,r,i,s,o,u=e.Event("show");if(this.hasContent()&&this.enabled){this.$element.trigger(u);if(u.isDefaultPrevented())return;t=this.tip(),this.setContent(),this.options.animation&&t.addClass("fade"),s=typeof this.options.placement=="function"?this.options.placement.call(this,t[0],this.$element[0]):this.options.placement,t.detach().css({top:0,left:0,display:"block"}),this.options.container?t.appendTo(this.options.container):t.insertAfter(this.$element),n=this.getPosition(),r=t[0].offsetWidth,i=t[0].offsetHeight;switch(s){case"bottom":o={top:n.top+n.height,left:n.left+n.width/2-r/2};break;case"top":o={top:n.top-i,left:n.left+n.width/2-r/2};break;case"left":o={top:n.top+n.height/2-i/2,left:n.left-r};break;case"right":o={top:n.top+n.height/2-i/2,left:n.left+n.width}}this.applyPlacement(o,s),this.$element.trigger("shown")}},applyPlacement:function(e,t){var n=this.tip(),r=n[0].offsetWidth,i=n[0].offsetHeight,s,o,u,a;n.offset(e).addClass(t).addClass("in"),s=n[0].offsetWidth,o=n[0].offsetHeight,t=="top"&&o!=i&&(e.top=e.top+i-o,a=!0),t=="bottom"||t=="top"?(u=0,e.left<0&&(u=e.left*-2,e.left=0,n.offset(e),s=n[0].offsetWidth,o=n[0].offsetHeight),this.replaceArrow(u-r+s,s,"left")):this.replaceArrow(o-i,o,"top"),a&&n.offset(e)},replaceArrow:function(e,t,n){this.arrow().css(n,e?50*(1-e/t)+"%":"")},setContent:function(){var e=this.tip(),t=this.getTitle();e.find(".tooltip-inner")[this.options.html?"html":"text"](t),e.removeClass("fade in top bottom left right")},hide:function(){function i(){var t=setTimeout(function(){n.off(e.support.transition.end).detach()},500);n.one(e.support.transition.end,function(){clearTimeout(t),n.detach()})}var t=this,n=this.tip(),r=e.Event("hide");this.$element.trigger(r);if(r.isDefaultPrevented())return;return n.removeClass("in"),e.support.transition&&this.$tip.hasClass("fade")?i():n.detach(),this.$element.trigger("hidden"),this},fixTitle:function(){var e=this.$element;(e.attr("title")||typeof e.attr("data-original-title")!="string")&&e.attr("data-original-title",e.attr("title")||"").attr("title","")},hasContent:function(){return this.getTitle()},getPosition:function(){var t=this.$element[0];return e.extend({},typeof t.getBoundingClientRect=="function"?t.getBoundingClientRect():{width:t.offsetWidth,height:t.offsetHeight},this.$element.offset())},getTitle:function(){var e,t=this.$element,n=this.options;return e=t.attr("data-original-title")||(typeof n.title=="function"?n.title.call(t[0]):n.title),e},tip:function(){return this.$tip=this.$tip||e(this.options.template)},arrow:function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},validate:function(){this.$element[0].parentNode||(this.hide(),this.$element=null,this.options=null)},enable:function(){this.enabled=!0},disable:function(){this.enabled=!1},toggleEnabled:function(){this.enabled=!this.enabled},toggle:function(t){var n=t?e(t.currentTarget)[this.type](this._options).data(this.type):this;n.tip().hasClass("in")?n.hide():n.show()},destroy:function(){this.hide().$element.off("."+this.type).removeData(this.type)}};var n=e.fn.tooltip;e.fn.tooltip=function(n){return this.each(function(){var r=e(this),i=r.data("tooltip"),s=typeof n=="object"&&n;i||r.data("tooltip",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.tooltip.Constructor=t,e.fn.tooltip.defaults={animation:!0,placement:"top",selector:!1,template:'<div class="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',trigger:"hover focus",title:"",delay:0,html:!1,container:!1},e.fn.tooltip.noConflict=function(){return e.fn.tooltip=n,this}}(window.jQuery),!function(e){"use strict";var t=function(e,t){this.init("popover",e,t)};t.prototype=e.extend({},e.fn.tooltip.Constructor.prototype,{constructor:t,setContent:function(){var e=this.tip(),t=this.getTitle(),n=this.getContent();e.find(".popover-title")[this.options.html?"html":"text"](t),e.find(".popover-content")[this.options.html?"html":"text"](n),e.removeClass("fade top bottom left right in")},hasContent:function(){return this.getTitle()||this.getContent()},getContent:function(){var e,t=this.$element,n=this.options;return e=(typeof n.content=="function"?n.content.call(t[0]):n.content)||t.attr("data-content"),e},tip:function(){return this.$tip||(this.$tip=e(this.options.template)),this.$tip},destroy:function(){this.hide().$element.off("."+this.type).removeData(this.type)}});var n=e.fn.popover;e.fn.popover=function(n){return this.each(function(){var r=e(this),i=r.data("popover"),s=typeof n=="object"&&n;i||r.data("popover",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.popover.Constructor=t,e.fn.popover.defaults=e.extend({},e.fn.tooltip.defaults,{placement:"right",trigger:"click",content:"",template:'<div class="popover"><div class="arrow"></div><h3 class="popover-title"></h3><div class="popover-content"></div></div>'}),e.fn.popover.noConflict=function(){return e.fn.popover=n,this}}(window.jQuery),!function(e){"use strict";function t(t,n){var r=e.proxy(this.process,this),i=e(t).is("body")?e(window):e(t),s;this.options=e.extend({},e.fn.scrollspy.defaults,n),this.$scrollElement=i.on("scroll.scroll-spy.data-api",r),this.selector=(this.options.target||(s=e(t).attr("href"))&&s.replace(/.*(?=#[^\s]+$)/,"")||"")+" .nav li > a",this.$body=e("body"),this.refresh(),this.process()}t.prototype={constructor:t,refresh:function(){var t=this,n;this.offsets=e([]),this.targets=e([]),n=this.$body.find(this.selector).map(function(){var n=e(this),r=n.data("target")||n.attr("href"),i=/^#\w/.test(r)&&e(r);return i&&i.length&&[[i.position().top+(!e.isWindow(t.$scrollElement.get(0))&&t.$scrollElement.scrollTop()),r]]||null}).sort(function(e,t){return e[0]-t[0]}).each(function(){t.offsets.push(this[0]),t.targets.push(this[1])})},process:function(){var e=this.$scrollElement.scrollTop()+this.options.offset,t=this.$scrollElement[0].scrollHeight||this.$body[0].scrollHeight,n=t-this.$scrollElement.height(),r=this.offsets,i=this.targets,s=this.activeTarget,o;if(e>=n)return s!=(o=i.last()[0])&&this.activate(o);for(o=r.length;o--;)s!=i[o]&&e>=r[o]&&(!r[o+1]||e<=r[o+1])&&this.activate(i[o])},activate:function(t){var n,r;this.activeTarget=t,e(this.selector).parent(".active").removeClass("active"),r=this.selector+'[data-target="'+t+'"],'+this.selector+'[href="'+t+'"]',n=e(r).parent("li").addClass("active"),n.parent(".dropdown-menu").length&&(n=n.closest("li.dropdown").addClass("active")),n.trigger("activate")}};var n=e.fn.scrollspy;e.fn.scrollspy=function(n){return this.each(function(){var r=e(this),i=r.data("scrollspy"),s=typeof n=="object"&&n;i||r.data("scrollspy",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.scrollspy.Constructor=t,e.fn.scrollspy.defaults={offset:10},e.fn.scrollspy.noConflict=function(){return e.fn.scrollspy=n,this},e(window).on("load",function(){e('[data-spy="scroll"]').each(function(){var t=e(this);t.scrollspy(t.data())})})}(window.jQuery),!function(e){"use strict";var t=function(t){this.element=e(t)};t.prototype={constructor:t,show:function(){var t=this.element,n=t.closest("ul:not(.dropdown-menu)"),r=t.attr("data-target"),i,s,o;r||(r=t.attr("href"),r=r&&r.replace(/.*(?=#[^\s]*$)/,""));if(t.parent("li").hasClass("active"))return;i=n.find(".active:last a")[0],o=e.Event("show",{relatedTarget:i}),t.trigger(o);if(o.isDefaultPrevented())return;s=e(r),this.activate(t.parent("li"),n),this.activate(s,s.parent(),function(){t.trigger({type:"shown",relatedTarget:i})})},activate:function(t,n,r){function o(){i.removeClass("active").find("> .dropdown-menu > .active").removeClass("active"),t.addClass("active"),s?(t[0].offsetWidth,t.addClass("in")):t.removeClass("fade"),t.parent(".dropdown-menu")&&t.closest("li.dropdown").addClass("active"),r&&r()}var i=n.find("> .active"),s=r&&e.support.transition&&i.hasClass("fade");s?i.one(e.support.transition.end,o):o(),i.removeClass("in")}};var n=e.fn.tab;e.fn.tab=function(n){return this.each(function(){var r=e(this),i=r.data("tab");i||r.data("tab",i=new t(this)),typeof n=="string"&&i[n]()})},e.fn.tab.Constructor=t,e.fn.tab.noConflict=function(){return e.fn.tab=n,this},e(document).on("click.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"]',function(t){t.preventDefault(),e(this).tab("show")})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=e.extend({},e.fn.typeahead.defaults,n),this.matcher=this.options.matcher||this.matcher,this.sorter=this.options.sorter||this.sorter,this.highlighter=this.options.highlighter||this.highlighter,this.updater=this.options.updater||this.updater,this.source=this.options.source,this.$menu=e(this.options.menu),this.shown=!1,this.listen()};t.prototype={constructor:t,select:function(){var e=this.$menu.find(".active").attr("data-value");return this.$element.val(this.updater(e)).change(),this.hide()},updater:function(e){return e},show:function(){var t=e.extend({},this.$element.position(),{height:this.$element[0].offsetHeight});return this.$menu.insertAfter(this.$element).css({top:t.top+t.height,left:t.left}).show(),this.shown=!0,this},hide:function(){return this.$menu.hide(),this.shown=!1,this},lookup:function(t){var n;return this.query=this.$element.val(),!this.query||this.query.length<this.options.minLength?this.shown?this.hide():this:(n=e.isFunction(this.source)?this.source(this.query,e.proxy(this.process,this)):this.source,n?this.process(n):this)},process:function(t){var n=this;return t=e.grep(t,function(e){return n.matcher(e)}),t=this.sorter(t),t.length?this.render(t.slice(0,this.options.items)).show():this.shown?this.hide():this},matcher:function(e){return~e.toLowerCase().indexOf(this.query.toLowerCase())},sorter:function(e){var t=[],n=[],r=[],i;while(i=e.shift())i.toLowerCase().indexOf(this.query.toLowerCase())?~i.indexOf(this.query)?n.push(i):r.push(i):t.push(i);return t.concat(n,r)},highlighter:function(e){var t=this.query.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&");return e.replace(new RegExp("("+t+")","ig"),function(e,t){return"<strong>"+t+"</strong>"})},render:function(t){var n=this;return t=e(t).map(function(t,r){return t=e(n.options.item).attr("data-value",r),t.find("a").html(n.highlighter(r)),t[0]}),t.first().addClass("active"),this.$menu.html(t),this},next:function(t){var n=this.$menu.find(".active").removeClass("active"),r=n.next();r.length||(r=e(this.$menu.find("li")[0])),r.addClass("active")},prev:function(e){var t=this.$menu.find(".active").removeClass("active"),n=t.prev();n.length||(n=this.$menu.find("li").last()),n.addClass("active")},listen:function(){this.$element.on("focus",e.proxy(this.focus,this)).on("blur",e.proxy(this.blur,this)).on("keypress",e.proxy(this.keypress,this)).on("keyup",e.proxy(this.keyup,this)),this.eventSupported("keydown")&&this.$element.on("keydown",e.proxy(this.keydown,this)),this.$menu.on("click",e.proxy(this.click,this)).on("mouseenter","li",e.proxy(this.mouseenter,this)).on("mouseleave","li",e.proxy(this.mouseleave,this))},eventSupported:function(e){var t=e in this.$element;return t||(this.$element.setAttribute(e,"return;"),t=typeof this.$element[e]=="function"),t},move:function(e){if(!this.shown)return;switch(e.keyCode){case 9:case 13:case 27:e.preventDefault();break;case 38:e.preventDefault(),this.prev();break;case 40:e.preventDefault(),this.next()}e.stopPropagation()},keydown:function(t){this.suppressKeyPressRepeat=~e.inArray(t.keyCode,[40,38,9,13,27]),this.move(t)},keypress:function(e){if(this.suppressKeyPressRepeat)return;this.move(e)},keyup:function(e){switch(e.keyCode){case 40:case 38:case 16:case 17:case 18:break;case 9:case 13:if(!this.shown)return;this.select();break;case 27:if(!this.shown)return;this.hide();break;default:this.lookup()}e.stopPropagation(),e.preventDefault()},focus:function(e){this.focused=!0},blur:function(e){this.focused=!1,!this.mousedover&&this.shown&&this.hide()},click:function(e){e.stopPropagation(),e.preventDefault(),this.select(),this.$element.focus()},mouseenter:function(t){this.mousedover=!0,this.$menu.find(".active").removeClass("active"),e(t.currentTarget).addClass("active")},mouseleave:function(e){this.mousedover=!1,!this.focused&&this.shown&&this.hide()}};var n=e.fn.typeahead;e.fn.typeahead=function(n){return this.each(function(){var r=e(this),i=r.data("typeahead"),s=typeof n=="object"&&n;i||r.data("typeahead",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.typeahead.defaults={source:[],items:8,menu:'<ul class="typeahead dropdown-menu"></ul>',item:'<li><a href="#"></a></li>',minLength:1},e.fn.typeahead.Constructor=t,e.fn.typeahead.noConflict=function(){return e.fn.typeahead=n,this},e(document).on("focus.typeahead.data-api",'[data-provide="typeahead"]',function(t){var n=e(this);if(n.data("typeahead"))return;n.typeahead(n.data())})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.options=e.extend({},e.fn.affix.defaults,n),this.$window=e(window).on("scroll.affix.data-api",e.proxy(this.checkPosition,this)).on("click.affix.data-api",e.proxy(function(){setTimeout(e.proxy(this.checkPosition,this),1)},this)),this.$element=e(t),this.checkPosition()};t.prototype.checkPosition=function(){if(!this.$element.is(":visible"))return;var t=e(document).height(),n=this.$window.scrollTop(),r=this.$element.offset(),i=this.options.offset,s=i.bottom,o=i.top,u="affix affix-top affix-bottom",a;typeof i!="object"&&(s=o=i),typeof o=="function"&&(o=i.top()),typeof s=="function"&&(s=i.bottom()),a=this.unpin!=null&&n+this.unpin<=r.top?!1:s!=null&&r.top+this.$element.height()>=t-s?"bottom":o!=null&&n<=o?"top":!1;if(this.affixed===a)return;this.affixed=a,this.unpin=a=="bottom"?r.top-n:null,this.$element.removeClass(u).addClass("affix"+(a?"-"+a:""))};var n=e.fn.affix;e.fn.affix=function(n){return this.each(function(){var r=e(this),i=r.data("affix"),s=typeof n=="object"&&n;i||r.data("affix",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.affix.Constructor=t,e.fn.affix.defaults={offset:0},e.fn.affix.noConflict=function(){return e.fn.affix=n,this},e(window).on("load",function(){e('[data-spy="affix"]').each(function(){var t=e(this),n=t.data();n.offset=n.offset||{},n.offsetBottom&&(n.offset.bottom=n.offsetBottom),n.offsetTop&&(n.offset.top=n.offsetTop),t.affix(n)})})}(window.jQuery); \ No newline at end of file -- cgit 1.4.1 From 4a6dcafaad323664824819d60ba879f8f043b9f4 Mon Sep 17 00:00:00 2001 From: Vincent Ambo <tazjin@gmail.com> Date: Sat, 21 Nov 2015 18:25:12 +0100 Subject: [k8s] Specify Hitch backend correctly --- k8s/tazblog-rc.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/k8s/tazblog-rc.yaml b/k8s/tazblog-rc.yaml index 4549ab1b39..e8d9f8bb42 100644 --- a/k8s/tazblog-rc.yaml +++ b/k8s/tazblog-rc.yaml @@ -27,7 +27,7 @@ spec: - image: tazjin/hitch:master imagePullPolicy: Always name: tazblog-hitch - command: ["hitch", "--backend=:6081", "--user=hitch", "/etc/hitch/ssl/tazblog-tls"] + command: ["hitch", "--backend=[127.0.0.1]:6081", "--user=hitch", "/etc/hitch/ssl/tazblog-tls"] ports: - containerPort: 8443 volumeMounts: -- cgit 1.4.1 From 8f8cb132fb805561a538c94e9f6ed809695bc8c9 Mon Sep 17 00:00:00 2001 From: Vincent Ambo <tazjin@gmail.com> Date: Sat, 21 Nov 2015 18:25:22 +0100 Subject: [varnish] Add HSTS header to every response --- varnish/default.vcl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/varnish/default.vcl b/varnish/default.vcl index de08f4f646..ebf1854df8 100644 --- a/varnish/default.vcl +++ b/varnish/default.vcl @@ -30,8 +30,11 @@ sub vcl_backend_response { if (beresp.ttl < 1m) { set beresp.ttl = 1m; } +} - # Add an HSTS header to our response +sub vcl_deliver { + # Add an HSTS header to everything + set resp.http.Strict-Transport-Security = "max-age=31536000;includeSubdomains;preload"; } sub vcl_synth { -- cgit 1.4.1 From e6a20995f59f1cf098976345ff737281bb771072 Mon Sep 17 00:00:00 2001 From: Vincent Ambo <tazjin@gmail.com> Date: Sat, 21 Nov 2015 18:27:25 +0100 Subject: [blog] Remove unneeded Lucius usage --- src/Blog.hs | 1 - src/Server.hs | 7 ------- 2 files changed, 8 deletions(-) diff --git a/src/Blog.hs b/src/Blog.hs index 97c7fa9bc9..f44cfe1f42 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -9,7 +9,6 @@ import Data.Time import Locales import Text.Blaze.Html (preEscapedToHtml) import Text.Hamlet -import Text.Lucius import Text.Markdown import qualified Data.Text as T diff --git a/src/Server.hs b/src/Server.hs index 6996583f5d..df8916d85d 100644 --- a/src/Server.hs +++ b/src/Server.hs @@ -60,13 +60,6 @@ staticHandler resDir = do setHeaderM "expires" "Tue, 20 Jan 2037 04:20:42 GMT" serveDirectory DisableBrowsing [] resDir -serveBlogStyle :: ServerPart Response -serveBlogStyle = do - setHeaderM "content-type" "text/css" - setHeaderM "cache-control" "max-age=630720000" - setHeaderM "expires" "Tue, 20 Jan 2037 04:20:42 GMT" - ok $ toResponse $ blogStyle - adminHandler :: AcidState Blog -> ServerPart Response adminHandler acid = do guardSession acid -- cgit 1.4.1 From 28b00ef3d4925302f25b65f4735aa01a9fc5240d Mon Sep 17 00:00:00 2001 From: Vincent Ambo <tazjin@gmail.com> Date: Sat, 21 Nov 2015 18:31:39 +0100 Subject: [blog] Minor design fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Blog.hs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Blog.hs b/src/Blog.hs index f44cfe1f42..f6ed6e54f9 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -82,8 +82,9 @@ $forall entry <- toDisplay $else ^{preEscapedToHtml $ btext entry} $if ((/=) (mtext entry) empty) - <a .uncoloured-link href=#{linkElems entry}> - #{readMore $ lang entry} + <p> + <a .uncoloured-link href=#{linkElems entry}> + #{readMore $ lang entry} <hr> $maybe links <- pageLinks ^{links} @@ -123,6 +124,7 @@ renderEntry e@Entry{..} = [shamlet| <p>^{renderEntryMarkdown $ mtext} $else ^{preEscapedToHtml $ btext} + <p>^{preEscapedToHtml $ mtext} <hr> |] -- cgit 1.4.1 From 80b6f2ca1f78f5b8a54cedee5ee1275af230bdbf Mon Sep 17 00:00:00 2001 From: Vincent Ambo <tazjin@gmail.com> Date: Sat, 21 Nov 2015 18:37:29 +0100 Subject: [blog] Add inline class on headers --- src/Blog.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Blog.hs b/src/Blog.hs index f6ed6e54f9..62e7d45754 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -72,7 +72,7 @@ renderEntries :: Bool -> [Entry] -> Maybe Html -> Html renderEntries showAll entries pageLinks = [shamlet| $forall entry <- toDisplay <article> - <h2> + <h2 .inline> <a href=#{linkElems entry} .unstyled-link> #{title entry} <aside .date> @@ -115,7 +115,7 @@ showLinks Nothing lang = [shamlet| renderEntry :: Entry -> Html renderEntry e@Entry{..} = [shamlet| <article> - <h2> + <h2 .inline> #{title} <aside .date> #{pack $ formatTime defaultTimeLocale "%Y-%m-%d" edate} -- cgit 1.4.1 From 6bc3802646d02ae7b2e86590d1914aa5d9455c2f Mon Sep 17 00:00:00 2001 From: Vincent Ambo <tazjin@gmail.com> Date: Sat, 21 Nov 2015 18:54:57 +0100 Subject: [res] Remove old background images MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- res/bg.gif | Bin 15015 -> 0 bytes res/cbg.jpg | Bin 12124 -> 0 bytes res/hbg.jpg | Bin 6039 -> 0 bytes 3 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 res/bg.gif delete mode 100644 res/cbg.jpg delete mode 100644 res/hbg.jpg diff --git a/res/bg.gif b/res/bg.gif deleted file mode 100644 index 62c5cba41c..0000000000 Binary files a/res/bg.gif and /dev/null differ diff --git a/res/cbg.jpg b/res/cbg.jpg deleted file mode 100644 index c83bb4c295..0000000000 Binary files a/res/cbg.jpg and /dev/null differ diff --git a/res/hbg.jpg b/res/hbg.jpg deleted file mode 100644 index c03636fd1d..0000000000 Binary files a/res/hbg.jpg and /dev/null differ -- cgit 1.4.1 From 4c961c4ac6948f6fed0b6ec420c9e0522d592263 Mon Sep 17 00:00:00 2001 From: Vincent Ambo <tazjin@gmail.com> Date: Sat, 21 Nov 2015 18:55:21 +0100 Subject: Rename res/ to static/ --- blog/Main.hs | 2 +- res/admin.css | 49 ------------------------------- res/apple-touch-icon.png | Bin 9756 -> 0 bytes res/blog.css | 35 ---------------------- res/favicon.ico | Bin 4354 -> 0 bytes res/keybase.txt | 69 -------------------------------------------- res/loginBoxTop.png | Bin 606 -> 0 bytes res/signin.gif | Bin 1850 -> 0 bytes static/admin.css | 49 +++++++++++++++++++++++++++++++ static/apple-touch-icon.png | Bin 0 -> 9756 bytes static/blog.css | 35 ++++++++++++++++++++++ static/favicon.ico | Bin 0 -> 4354 bytes static/keybase.txt | 69 ++++++++++++++++++++++++++++++++++++++++++++ static/loginBoxTop.png | Bin 0 -> 606 bytes static/signin.gif | Bin 0 -> 1850 bytes 15 files changed, 154 insertions(+), 154 deletions(-) delete mode 100644 res/admin.css delete mode 100644 res/apple-touch-icon.png delete mode 100644 res/blog.css delete mode 100644 res/favicon.ico delete mode 100644 res/keybase.txt delete mode 100644 res/loginBoxTop.png delete mode 100644 res/signin.gif create mode 100644 static/admin.css create mode 100644 static/apple-touch-icon.png create mode 100644 static/blog.css create mode 100644 static/favicon.ico create mode 100644 static/keybase.txt create mode 100644 static/loginBoxTop.png create mode 100644 static/signin.gif diff --git a/blog/Main.hs b/blog/Main.hs index 141a8e693c..cfe068a8d9 100644 --- a/blog/Main.hs +++ b/blog/Main.hs @@ -27,7 +27,7 @@ instance Options MainOptions where "Remote acid-state database port. Default is 8070" <*> simpleOption "blogPort" 8000 "Port to serve the blog on. Default is 8000." - <*> simpleOption "resourceDir" "/opt/tazblog/res" + <*> simpleOption "resourceDir" "/opt/tazblog/static" "Resources folder location." main :: IO() diff --git a/res/admin.css b/res/admin.css deleted file mode 100644 index 10980dc9e4..0000000000 --- a/res/admin.css +++ /dev/null @@ -1,49 +0,0 @@ -@charset "UTF-8"; -/* CSS Document */ - -body { - padding-top: 20px; - font-family: 'PT Sans', sans-serif; - background-image: linear-gradient(bottom, rgb(245,245,245) 66%, rgb(239,239,239) 83%); - background-image: -o-linear-gradient(bottom, rgb(245,245,245) 66%, rgb(239,239,239) 83%); - background-image: -webkit-linear-gradient(bottom, rgb(245,245,245) 66%, rgb(239,239,239) 83%); - background-image: -webkit-gradient( - linear, - left bottom, - left top, - color-stop(0.66, rgb(245,245,245)), - color-stop(0.83, rgb(239,239,239)) - ); - background-repeat: no-repeat; - background-color: rgb(245,245,245); -} - -.loginBox { - width: 400px; - margin: 0 auto; -} - -.loginBoxTop { - width: 380px; - height: 28px; - color: #FFFFFF; - font-size: 12px; - padding-left: 20px; - padding-top: 11px; - background: url(/static/loginBoxTop.png); -} - -.loginBoxMiddle { - background-color: #F3F3F3; - border-top: 0px hidden; - border:1px solid #D2D2D2; - border-bottom-left-radius: 12px; - border-bottom-right-radius: 12px; - text-align: center; - font-size:12px; - height:auto; - padding-left: 10px; - padding-right: 10px; - min-height:200px; - width:378px; -} diff --git a/res/apple-touch-icon.png b/res/apple-touch-icon.png deleted file mode 100644 index 22ba058cdd..0000000000 Binary files a/res/apple-touch-icon.png and /dev/null differ diff --git a/res/blog.css b/res/blog.css deleted file mode 100644 index e6e4ae3c2b..0000000000 --- a/res/blog.css +++ /dev/null @@ -1,35 +0,0 @@ -body { - margin: 40px auto; - max-width: 650px; - line-height: 1.6; - font-size: 18px; - color: #383838; - padding: 0 10px -} -h1, h2, h3 { - line-height: 1.2 -} -.footer { - text-align: right; -} -.lod { - text-align: center; -} -.unstyled-link { - color: inherit; - text-decoration: none; -} -.uncoloured-link { - color: inherit; -} -.date { - text-align: right; - font-style: italic; - float: right; -} -.inline { - display: inline; -} -.navigation { - text-align: center; -} diff --git a/res/favicon.ico b/res/favicon.ico deleted file mode 100644 index 2958dd3afc..0000000000 Binary files a/res/favicon.ico and /dev/null differ diff --git a/res/keybase.txt b/res/keybase.txt deleted file mode 100644 index 661c33e01e..0000000000 --- a/res/keybase.txt +++ /dev/null @@ -1,69 +0,0 @@ -================================================================== -https://keybase.io/tazjin --------------------------------------------------------------------- - -I hereby claim: - - * I am an admin of http://tazj.in - * I am tazjin (https://keybase.io/tazjin) on keybase. - * I have a public key with fingerprint DCF3 4CFA C1AC 44B8 7E26 3331 36EE 3481 4F6D 294A - -To claim this, I am signing this object: - -{ - "body": { - "key": { - "fingerprint": "dcf34cfac1ac44b87e26333136ee34814f6d294a", - "host": "keybase.io", - "key_id": "36EE34814F6D294A", - "uid": "2268b75a56bb9693d3ef077bc1217900", - "username": "tazjin" - }, - "service": { - "hostname": "tazj.in", - "protocol": "http:" - }, - "type": "web_service_binding", - "version": 1 - }, - "ctime": 1397644545, - "expire_in": 157680000, - "prev": "4973fdda56a6cfa726a813411c915458c652be45dd19283f7a4ae4f9c217df14", - "seqno": 4, - "tag": "signature" -} - -with the aforementioned key, yielding the PGP signature: - ------BEGIN PGP MESSAGE----- -Version: GnuPG v2.0.22 (GNU/Linux) - -owGbwMvMwMWY9pU1Q3bHF2vG0wdeJTEE+8WyVSsl5adUKllVK2Wngqm0zLz01KKC -osy8EiUrpZTkNGOT5LTEZMPEZBOTJAvzVCMzY2NjQ2Oz1FRjEwtDkzSzFCNLk0Ql -HaWM/GKQDqAxSYnFqXqZ+UAxICc+MwUoamzm6gpW72bmAlTvCJQrBUsYGZlZJJmb -JpqaJSVZmlkapxinphmYmyclGxoZmlsaGIAUFqcW5SXmpgJVlyRWZWXmKdXqKAHF -yjKTU0EuBlmMJK8HVKCjVFCUX5KfnJ8DFMwoKSmwAukpqSwAKSpPTYqHao9PysxL -AXoYqKEstag4Mz9PycoQqDK5JBNknqGxpbmZiYmpiamOUmpFQWZRanwmSIWpuZmF -ARCArEktAxppYmlunJaSAvRFohkwtMyNzBItDI1NDA2TLQ2Bui2SzUyNklJNTFNS -DC2NLIzTzBNNElNN0iyTgZ5MSTM0UQJ5qDAvX8nKBOjMxHSgkcWZ6XmJJaVFqUq1 -nUwyLAyMXAxsrEygKGPg4hSARWSZH/8/0573HMdvfH5XxeayYZ2efPb8bw730i1/ -WBU3qru5pKlf3xKmeK5ihtKeT6VXGm3usV2reZWyvO/0joi83oT9P80s88Q6U/vb -vmycHnB7e110v/3OZadu/Sx6+uXk/ZeCR8u+p/+6dNc8XWqX/68t06pnrGKU/BfU -F7X5S/HUy4ysvyZN+v1Jj6NtMvvN1EvPpCpv3kz2tGU1EzpZFfl8Xujq1OopuxZJ -l5kvDlgZ78ezdLZ1+aOlixbsXra4/3fdbZ8XnQX1DatzV18+e2rmMcPKm6qngqIf -Xp8oKTAz+Mg1v6gHP0wLN/Mf3JKjYHnX5U6L/KIvkbsLArtES0r7w1iWZ3OvvSPr -fW6heune1tOb7j3vP+1XeOyV2ekr6pPO3bdrv9X25HbTaqs7z06f0v35fmtQ3uUZ -Z35eLYmaEmb/x/u3vFh6GsvMDocpCTpPlHa0z+xzOGbhzLFO18v21Zd9ISG3Hqtd -F7jaLlWa2W+TsytNnXudVrfCBSbl8zNMfuk2e0Z8i9ix3PmEVa3rTEfhde3qwgtY -dy8rUbzzd5d9ccF63btqO/VMb4oe04x4uCLB5RD3p+8+s77o/T4WP2cFw+0cviX6 -StlJX5f+U3Or3fZY7dUfPcmMJZ/eSs7m+1d5IUbs3jI27olHFzGVvTcsu7w79aOK -SxmXvnEIUwZXgP6BL4LrPDY1rN2V0q1cZj1/efj880rzeu6+OQYA -=xHfH ------END PGP MESSAGE----- - -And finally, I am proving ownership of this host by posting or -appending to this document. - -View my publicly-auditable identity here: https://keybase.io/tazjin - -================================================================== diff --git a/res/loginBoxTop.png b/res/loginBoxTop.png deleted file mode 100644 index 8a0ee3ba8d..0000000000 Binary files a/res/loginBoxTop.png and /dev/null differ diff --git a/res/signin.gif b/res/signin.gif deleted file mode 100644 index bbe282bae0..0000000000 Binary files a/res/signin.gif and /dev/null differ diff --git a/static/admin.css b/static/admin.css new file mode 100644 index 0000000000..10980dc9e4 --- /dev/null +++ b/static/admin.css @@ -0,0 +1,49 @@ +@charset "UTF-8"; +/* CSS Document */ + +body { + padding-top: 20px; + font-family: 'PT Sans', sans-serif; + background-image: linear-gradient(bottom, rgb(245,245,245) 66%, rgb(239,239,239) 83%); + background-image: -o-linear-gradient(bottom, rgb(245,245,245) 66%, rgb(239,239,239) 83%); + background-image: -webkit-linear-gradient(bottom, rgb(245,245,245) 66%, rgb(239,239,239) 83%); + background-image: -webkit-gradient( + linear, + left bottom, + left top, + color-stop(0.66, rgb(245,245,245)), + color-stop(0.83, rgb(239,239,239)) + ); + background-repeat: no-repeat; + background-color: rgb(245,245,245); +} + +.loginBox { + width: 400px; + margin: 0 auto; +} + +.loginBoxTop { + width: 380px; + height: 28px; + color: #FFFFFF; + font-size: 12px; + padding-left: 20px; + padding-top: 11px; + background: url(/static/loginBoxTop.png); +} + +.loginBoxMiddle { + background-color: #F3F3F3; + border-top: 0px hidden; + border:1px solid #D2D2D2; + border-bottom-left-radius: 12px; + border-bottom-right-radius: 12px; + text-align: center; + font-size:12px; + height:auto; + padding-left: 10px; + padding-right: 10px; + min-height:200px; + width:378px; +} diff --git a/static/apple-touch-icon.png b/static/apple-touch-icon.png new file mode 100644 index 0000000000..22ba058cdd Binary files /dev/null and b/static/apple-touch-icon.png differ diff --git a/static/blog.css b/static/blog.css new file mode 100644 index 0000000000..e6e4ae3c2b --- /dev/null +++ b/static/blog.css @@ -0,0 +1,35 @@ +body { + margin: 40px auto; + max-width: 650px; + line-height: 1.6; + font-size: 18px; + color: #383838; + padding: 0 10px +} +h1, h2, h3 { + line-height: 1.2 +} +.footer { + text-align: right; +} +.lod { + text-align: center; +} +.unstyled-link { + color: inherit; + text-decoration: none; +} +.uncoloured-link { + color: inherit; +} +.date { + text-align: right; + font-style: italic; + float: right; +} +.inline { + display: inline; +} +.navigation { + text-align: center; +} diff --git a/static/favicon.ico b/static/favicon.ico new file mode 100644 index 0000000000..2958dd3afc Binary files /dev/null and b/static/favicon.ico differ diff --git a/static/keybase.txt b/static/keybase.txt new file mode 100644 index 0000000000..661c33e01e --- /dev/null +++ b/static/keybase.txt @@ -0,0 +1,69 @@ +================================================================== +https://keybase.io/tazjin +-------------------------------------------------------------------- + +I hereby claim: + + * I am an admin of http://tazj.in + * I am tazjin (https://keybase.io/tazjin) on keybase. + * I have a public key with fingerprint DCF3 4CFA C1AC 44B8 7E26 3331 36EE 3481 4F6D 294A + +To claim this, I am signing this object: + +{ + "body": { + "key": { + "fingerprint": "dcf34cfac1ac44b87e26333136ee34814f6d294a", + "host": "keybase.io", + "key_id": "36EE34814F6D294A", + "uid": "2268b75a56bb9693d3ef077bc1217900", + "username": "tazjin" + }, + "service": { + "hostname": "tazj.in", + "protocol": "http:" + }, + "type": "web_service_binding", + "version": 1 + }, + "ctime": 1397644545, + "expire_in": 157680000, + "prev": "4973fdda56a6cfa726a813411c915458c652be45dd19283f7a4ae4f9c217df14", + "seqno": 4, + "tag": "signature" +} + +with the aforementioned key, yielding the PGP signature: + +-----BEGIN PGP MESSAGE----- +Version: GnuPG v2.0.22 (GNU/Linux) + +owGbwMvMwMWY9pU1Q3bHF2vG0wdeJTEE+8WyVSsl5adUKllVK2Wngqm0zLz01KKC +osy8EiUrpZTkNGOT5LTEZMPEZBOTJAvzVCMzY2NjQ2Oz1FRjEwtDkzSzFCNLk0Ql +HaWM/GKQDqAxSYnFqXqZ+UAxICc+MwUoamzm6gpW72bmAlTvCJQrBUsYGZlZJJmb +JpqaJSVZmlkapxinphmYmyclGxoZmlsaGIAUFqcW5SXmpgJVlyRWZWXmKdXqKAHF +yjKTU0EuBlmMJK8HVKCjVFCUX5KfnJ8DFMwoKSmwAukpqSwAKSpPTYqHao9PysxL +AXoYqKEstag4Mz9PycoQqDK5JBNknqGxpbmZiYmpiamOUmpFQWZRanwmSIWpuZmF +ARCArEktAxppYmlunJaSAvRFohkwtMyNzBItDI1NDA2TLQ2Bui2SzUyNklJNTFNS +DC2NLIzTzBNNElNN0iyTgZ5MSTM0UQJ5qDAvX8nKBOjMxHSgkcWZ6XmJJaVFqUq1 +nUwyLAyMXAxsrEygKGPg4hSARWSZH/8/0573HMdvfH5XxeayYZ2efPb8bw730i1/ +WBU3qru5pKlf3xKmeK5ihtKeT6VXGm3usV2reZWyvO/0joi83oT9P80s88Q6U/vb +vmycHnB7e110v/3OZadu/Sx6+uXk/ZeCR8u+p/+6dNc8XWqX/68t06pnrGKU/BfU +F7X5S/HUy4ysvyZN+v1Jj6NtMvvN1EvPpCpv3kz2tGU1EzpZFfl8Xujq1OopuxZJ +l5kvDlgZ78ezdLZ1+aOlixbsXra4/3fdbZ8XnQX1DatzV18+e2rmMcPKm6qngqIf +Xp8oKTAz+Mg1v6gHP0wLN/Mf3JKjYHnX5U6L/KIvkbsLArtES0r7w1iWZ3OvvSPr +fW6heune1tOb7j3vP+1XeOyV2ekr6pPO3bdrv9X25HbTaqs7z06f0v35fmtQ3uUZ +Z35eLYmaEmb/x/u3vFh6GsvMDocpCTpPlHa0z+xzOGbhzLFO18v21Zd9ISG3Hqtd +F7jaLlWa2W+TsytNnXudVrfCBSbl8zNMfuk2e0Z8i9ix3PmEVa3rTEfhde3qwgtY +dy8rUbzzd5d9ccF63btqO/VMb4oe04x4uCLB5RD3p+8+s77o/T4WP2cFw+0cviX6 +StlJX5f+U3Or3fZY7dUfPcmMJZ/eSs7m+1d5IUbs3jI27olHFzGVvTcsu7w79aOK +SxmXvnEIUwZXgP6BL4LrPDY1rN2V0q1cZj1/efj880rzeu6+OQYA +=xHfH +-----END PGP MESSAGE----- + +And finally, I am proving ownership of this host by posting or +appending to this document. + +View my publicly-auditable identity here: https://keybase.io/tazjin + +================================================================== diff --git a/static/loginBoxTop.png b/static/loginBoxTop.png new file mode 100644 index 0000000000..8a0ee3ba8d Binary files /dev/null and b/static/loginBoxTop.png differ diff --git a/static/signin.gif b/static/signin.gif new file mode 100644 index 0000000000..bbe282bae0 Binary files /dev/null and b/static/signin.gif differ -- cgit 1.4.1 From a68d6cfa31ca9fa4e2773c5f3e375c21079c358b Mon Sep 17 00:00:00 2001 From: Vincent Ambo <tazjin@gmail.com> Date: Sat, 21 Nov 2015 19:01:05 +0100 Subject: [locale/blog] Remove unused locale strings --- src/Blog.hs | 2 +- src/Locales.hs | 70 +--------------------------------------------------------- 2 files changed, 2 insertions(+), 70 deletions(-) diff --git a/src/Blog.hs b/src/Blog.hs index 62e7d45754..a78b911eda 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -90,7 +90,7 @@ $maybe links <- pageLinks ^{links} |] where - toDisplay = if' showAll entries (take 6 entries) + toDisplay = if showAll then entries else (take 6 entries) linkElems Entry{..} = concat $ intersperse' "/" [show lang, show entryId] showLinks :: Maybe Int -> BlogLang -> Html diff --git a/src/Locales.hs b/src/Locales.hs index 00b2b871d1..a800bbf847 100644 --- a/src/Locales.hs +++ b/src/Locales.hs @@ -6,19 +6,10 @@ import Data.Text (Text) import qualified Data.Text as T import Network.URI -{- to add a language simply define its abbreviation and Show instance then - - translate the appropriate strings and add CouchDB views in Server.hs -} - data BlogError = NotFound | UnknownError version = "5.1-beta" -allLang = [EN, DE] - -if' :: Bool -> a -> a -> a -if' True x _ = x -if' False _ y = y - blogTitle :: BlogLang -> Text -> Text blogTitle DE s = T.concat ["Tazjins blog", s] blogTitle EN s = T.concat ["Tazjin's blog", s] @@ -27,47 +18,6 @@ showLangText :: BlogLang -> Text showLangText EN = "en" showLangText DE = "de" --- index site headline -topText DE = "Aktuelle Einträge" -topText EN = "Latest entries" - -getMonth :: BlogLang -> Int -> Int -> Text -getMonth l y m = T.append (monthName l m) $ T.pack $ show y - where - monthName :: BlogLang -> Int -> Text - monthName DE m = case m of - 1 -> "Januar " - 2 -> "Februar " - 3 -> "März " - 4 -> "April " - 5 -> "Mai " - 6 -> "Juni " - 7 -> "Juli " - 8 -> "August " - 9 -> "September " - 10 -> "Oktober " - 11 -> "November " - 12 -> "Dezember " - _ -> "Unbekannt " - monthName EN m = case m of - 1 -> "January " - 2 -> "February " - 3 -> "March " - 4 -> "April " - 5 -> "May " - 6 -> "June " - 7 -> "July " - 8 -> "August " - 9 -> "September " - 10 -> "October " - 11 -> "November " - 12 -> "December " - _ -> "Unknown " - -entireMonth :: BlogLang -> Text -entireMonth DE = "Ganzer Monat" -entireMonth EN = "Entire month" - backText :: BlogLang -> Text backText DE = "Früher" backText EN = "Earlier" @@ -80,24 +30,6 @@ readMore :: BlogLang -> Text readMore DE = "[Weiterlesen]" readMore EN = "[Read more]" -eTimeFormat :: BlogLang -> String -eTimeFormat DE = "Geschrieben am %Y-%m-%d von " -eTimeFormat EN = "Written on %Y-%m-%d by " - --- contact information -contactText :: BlogLang -> Text -contactText DE = "Wer mich kontaktieren will: " -contactText EN = "Get in touch with me: " - -orText :: BlogLang -> Text -orText DE = " oder " -orText EN = " or " - --- footer -noticeText :: BlogLang -> Text -noticeText EN = "site notice" -noticeText DE = "Impressum" - -- RSS Strings rssTitle :: BlogLang -> String rssTitle DE = "Tazjins Blog" @@ -126,4 +58,4 @@ unknownErrorText EN = "An unknown error has occured." -- static information repoURL :: Text = "https://bitbucket.org/tazjin/tazblog-haskell" mailTo :: Text = "mailto:tazjin+blog@gmail.com" -twitter :: Text = "http://twitter.com/#!/tazjin" +twitter :: Text = "https://twitter.com/tazjin" -- cgit 1.4.1 From d7d428d597ebede6d7d423abcecb930cdca8a88c Mon Sep 17 00:00:00 2001 From: Vincent Ambo <tazjin@gmail.com> Date: Sat, 21 Nov 2015 19:05:17 +0100 Subject: [locale/blog] Remove other unused code --- src/Blog.hs | 7 +------ src/Server.hs | 2 +- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/Blog.hs b/src/Blog.hs index a78b911eda..6cc00510c0 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -1,7 +1,6 @@ module Blog where import BlogDB -import Data.List (intersperse) import Data.Maybe (fromJust) import Data.Text (Text, append, empty, pack) import Data.Text.Lazy (fromStrict) @@ -13,10 +12,6 @@ import Text.Markdown import qualified Data.Text as T --- custom list functions -intersperse' :: a -> [a] -> [a] -intersperse' sep l = sep : intersperse sep l - replace :: Eq a => a -> a -> [a] -> [a] replace x y = map (\z -> if z == x then y else z) @@ -91,7 +86,7 @@ $maybe links <- pageLinks |] where toDisplay = if showAll then entries else (take 6 entries) - linkElems Entry{..} = concat $ intersperse' "/" [show lang, show entryId] + linkElems Entry{..} = concat $ ["/", show lang, "/", show entryId] showLinks :: Maybe Int -> BlogLang -> Html showLinks (Just i) lang = [shamlet| diff --git a/src/Server.hs b/src/Server.hs index df8916d85d..373bb7d6b5 100644 --- a/src/Server.hs +++ b/src/Server.hs @@ -149,7 +149,7 @@ updateEntry acid entryId = do , btext = nBtext , mtext = nMtext} update' acid (UpdateEntry newEntry) - seeOther (concat $ intersperse' "/" [show $ lang entry, show entryId]) + seeOther (concat $ ["/", show $ lang entry, "/", show entryId]) (toResponse ()) guardSession :: AcidState Blog -> ServerPartT IO () -- cgit 1.4.1 From 41599161faf4ff8627f75a4acaa57d6f0b7f0928 Mon Sep 17 00:00:00 2001 From: Vincent Ambo <tazjin@gmail.com> Date: Sat, 21 Nov 2015 19:07:30 +0100 Subject: [blog] Update error page for new design --- src/Blog.hs | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/Blog.hs b/src/Blog.hs index 6cc00510c0..ee87cf8d5f 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -222,16 +222,12 @@ editPage (Entry{..}) = adminTemplate "Index" $ [shamlet| showError :: BlogError -> BlogLang -> Html showError NotFound l = blogTemplate l (T.append ": " $ notFoundTitle l) $ [shamlet| -<div .row .text-center> - <div .span12 .notFoundFace>:( -<div .row .text-center> - <div .span12 .notFoundText> - #{notFoundText l} +<p>:( +<p>#{notFoundText l} +<hr> |] showError UnknownError l = blogTemplate l "" $ [shamlet| -<div .row .text-center> - <div .span12 .notFoundFace>:( -<div .row .text-center> - <div .span12 .notFoundText> - #{unknownErrorText l} +<p>:( +<p>#{unknownErrorText l} +<hr> |] -- cgit 1.4.1 From 9b403a625f0f3cef650cb860725d8dec3a3b8919 Mon Sep 17 00:00:00 2001 From: Vincent Ambo <tazjin@gmail.com> Date: Sat, 21 Nov 2015 19:09:56 +0100 Subject: [5.1] Version 5.1 --- src/Locales.hs | 2 +- tazblog.cabal | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Locales.hs b/src/Locales.hs index a800bbf847..b0467d76dc 100644 --- a/src/Locales.hs +++ b/src/Locales.hs @@ -8,7 +8,7 @@ import Network.URI data BlogError = NotFound | UnknownError -version = "5.1-beta" +version = "5.1" blogTitle :: BlogLang -> Text -> Text blogTitle DE s = T.concat ["Tazjins blog", s] diff --git a/tazblog.cabal b/tazblog.cabal index 9e41ad5d7b..f687f1217c 100644 --- a/tazblog.cabal +++ b/tazblog.cabal @@ -1,5 +1,5 @@ Name: tazblog -Version: 5.1-beta +Version: 5.1 Synopsis: Tazjin's Blog License: MIT License-file: LICENSE -- cgit 1.4.1 From a3a2afdc597eb00121191ca42f01a9e04555f684 Mon Sep 17 00:00:00 2001 From: Vincent Ambo <tazjin@gmail.com> Date: Sat, 21 Nov 2015 19:49:36 +0100 Subject: [varnish] Use Varnish 4.1, redirect to HTTPS --- varnish/Dockerfile | 9 ++++++--- varnish/default.vcl | 13 ++++++++++--- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/varnish/Dockerfile b/varnish/Dockerfile index 4a4b7dd7e0..54a8afe278 100644 --- a/varnish/Dockerfile +++ b/varnish/Dockerfile @@ -1,11 +1,14 @@ FROM centos:7 MAINTAINER Vincent Ambo <hej@tazj.in> -EXPOSE 6081 6082 +EXPOSE 6081 6082 6083 -RUN yum install -y epel-release && yum install -y varnish +RUN yum install -y epel-release && \ + rpm --nosignature -i https://repo.varnish-cache.org/redhat/varnish-4.1.el7.rpm && \ + yum install -y varnish ADD default.vcl /etc/varnish/default.vcl CMD ulimit -n 131072 && \ - /usr/sbin/varnishd -F -f /etc/varnish/default.vcl -a :6081 -T :6082 -t 120 + /usr/sbin/varnishd -F -f /etc/varnish/default.vcl \ + -a :6081 -T :6082 -a :6083,PROXY -t 120 diff --git a/varnish/default.vcl b/varnish/default.vcl index ebf1854df8..066b1a9b24 100644 --- a/varnish/default.vcl +++ b/varnish/default.vcl @@ -1,4 +1,5 @@ vcl 4.0; +import std; # By default, Varnish will run on the same servers as the blog. Inside of # Kubernetes this will be inside the same pod. @@ -23,6 +24,11 @@ sub vcl_recv { if (req.url ~ "^/admin") { return (pass); } + + # Redirect non-www to www and non-HTTPS to HTTPS + if (req.http.host ~ "tazj.in" || std.port(local.ip) == 6081) { + return (synth (750, "")); + } } sub vcl_backend_response { @@ -38,9 +44,10 @@ sub vcl_deliver { } sub vcl_synth { - # Execute redirects - if (resp.status == 301) { - set resp.http.Location = req.url; + # Execute TLS or www. redirect + if (resp.status == 750) { + set resp.http.Location = "https://www.tazj.in" + req.url; + set resp.status = 301; return (deliver); } } -- cgit 1.4.1 From 8390d148774ba917fdff5a37095c6816ed6c16ce Mon Sep 17 00:00:00 2001 From: Vincent Ambo <tazjin@gmail.com> Date: Sat, 21 Nov 2015 20:30:50 +0100 Subject: [varnish] Fix www. redirect --- varnish/default.vcl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/varnish/default.vcl b/varnish/default.vcl index 066b1a9b24..cd726aff36 100644 --- a/varnish/default.vcl +++ b/varnish/default.vcl @@ -26,7 +26,7 @@ sub vcl_recv { } # Redirect non-www to www and non-HTTPS to HTTPS - if (req.http.host ~ "tazj.in" || std.port(local.ip) == 6081) { + if (req.http.host ~ "^tazj.in" || std.port(local.ip) == 6081) { return (synth (750, "")); } } -- cgit 1.4.1 From 7458430485a84519bc243eb98e08489ab52d92a0 Mon Sep 17 00:00:00 2001 From: Vincent Ambo <tazjin@gmail.com> Date: Sat, 21 Nov 2015 20:31:05 +0100 Subject: [k8s] Update hitch flags to use PROXY to Varnish --- k8s/tazblog-rc.yaml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/k8s/tazblog-rc.yaml b/k8s/tazblog-rc.yaml index e8d9f8bb42..4a075a52fa 100644 --- a/k8s/tazblog-rc.yaml +++ b/k8s/tazblog-rc.yaml @@ -1,15 +1,17 @@ apiVersion: v1 kind: ReplicationController metadata: - name: tazblog + name: tazblog-5.1 spec: replicas: 2 selector: app: tazblog + version: v5.1 template: metadata: labels: app: tazblog + version: v5.1 spec: containers: - image: tazjin/tazblog-haskell:master @@ -27,7 +29,7 @@ spec: - image: tazjin/hitch:master imagePullPolicy: Always name: tazblog-hitch - command: ["hitch", "--backend=[127.0.0.1]:6081", "--user=hitch", "/etc/hitch/ssl/tazblog-tls"] + command: ["hitch", "--backend=[127.0.0.1]:6083", "--write-proxy", "--user=hitch", "/etc/hitch/ssl/tazblog-tls"] ports: - containerPort: 8443 volumeMounts: -- cgit 1.4.1 From 813c273e687d57e284aae8daec9ec7bd1196eecc Mon Sep 17 00:00:00 2001 From: Vincent Ambo <tazjin@gmail.com> Date: Sat, 21 Nov 2015 20:50:02 +0100 Subject: [varnish] Add HSTS header on redirect --- varnish/default.vcl | 1 + 1 file changed, 1 insertion(+) diff --git a/varnish/default.vcl b/varnish/default.vcl index cd726aff36..a06bb744b7 100644 --- a/varnish/default.vcl +++ b/varnish/default.vcl @@ -47,6 +47,7 @@ sub vcl_synth { # Execute TLS or www. redirect if (resp.status == 750) { set resp.http.Location = "https://www.tazj.in" + req.url; + set resp.http.Strict-Transport-Security = "max-age=31536000;includeSubdomains;preload"; set resp.status = 301; return (deliver); } -- cgit 1.4.1 From 6138a8b0f8bd57c8793e8cfd1ee0da5d76a472d6 Mon Sep 17 00:00:00 2001 From: Vincent Ambo <tazjin@gmail.com> Date: Sat, 21 Nov 2015 21:20:12 +0100 Subject: [varnish] Add X-Cache headers --- varnish/default.vcl | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/varnish/default.vcl b/varnish/default.vcl index a06bb744b7..5a15d21a9c 100644 --- a/varnish/default.vcl +++ b/varnish/default.vcl @@ -41,6 +41,12 @@ sub vcl_backend_response { sub vcl_deliver { # Add an HSTS header to everything set resp.http.Strict-Transport-Security = "max-age=31536000;includeSubdomains;preload"; + + if (obj.hits > 0) { + set resp.http.X-Cache = "HIT"; + } else { + set resp.http.X-Cache = "MISS"; + } } sub vcl_synth { -- cgit 1.4.1 From 35dba3b2114248dabe23a3ad2a177b4de909994f Mon Sep 17 00:00:00 2001 From: Vincent Ambo <tazjin@gmail.com> Date: Mon, 23 Nov 2015 16:36:42 +0100 Subject: [5.1.1] Add meta-description tag --- src/Blog.hs | 1 + src/Locales.hs | 2 +- tazblog.cabal | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Blog.hs b/src/Blog.hs index ee87cf8d5f..f35e3d9080 100644 --- a/src/Blog.hs +++ b/src/Blog.hs @@ -29,6 +29,7 @@ $doctype 5 <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> + <meta name="description" content=#{blogTitle lang t_append}> <link rel="stylesheet" type="text/css" href="/static/blog.css" media="all"> <link rel="alternate" type="application/rss+xml" title="RSS-Feed" href=#{rssUrl}> <title>#{blogTitle lang t_append} diff --git a/src/Locales.hs b/src/Locales.hs index b0467d76dc..92acb215f8 100644 --- a/src/Locales.hs +++ b/src/Locales.hs @@ -8,7 +8,7 @@ import Network.URI data BlogError = NotFound | UnknownError -version = "5.1" +version = "5.1.1" blogTitle :: BlogLang -> Text -> Text blogTitle DE s = T.concat ["Tazjins blog", s] diff --git a/tazblog.cabal b/tazblog.cabal index f687f1217c..4fdffd2108 100644 --- a/tazblog.cabal +++ b/tazblog.cabal @@ -1,5 +1,5 @@ Name: tazblog -Version: 5.1 +Version: 5.1.1 Synopsis: Tazjin's Blog License: MIT License-file: LICENSE -- cgit 1.4.1 From 94788f1f92b0d4d3733e044b015b72c0857d02cd Mon Sep 17 00:00:00 2001 From: Vincent Ambo <tazjin@gmail.com> Date: Tue, 24 Nov 2015 14:07:20 +0100 Subject: [varnish & k8s] Minor config updates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- k8s/tazblog-rc.yaml | 9 ++++++--- varnish/Dockerfile | 4 +++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/k8s/tazblog-rc.yaml b/k8s/tazblog-rc.yaml index 4a075a52fa..019d1bee06 100644 --- a/k8s/tazblog-rc.yaml +++ b/k8s/tazblog-rc.yaml @@ -1,17 +1,17 @@ apiVersion: v1 kind: ReplicationController metadata: - name: tazblog-5.1 + name: tazblog-5.1.1 spec: replicas: 2 selector: app: tazblog - version: v5.1 + version: v5.1.1 template: metadata: labels: app: tazblog - version: v5.1 + version: v5.1.1 spec: containers: - image: tazjin/tazblog-haskell:master @@ -36,6 +36,9 @@ spec: - name: tazblog-tls readOnly: true mountPath: /etc/hitch/ssl + resources: + requests: + memory: "1024Mi" volumes: - name: tazblog-tls secret: diff --git a/varnish/Dockerfile b/varnish/Dockerfile index 54a8afe278..83733b527d 100644 --- a/varnish/Dockerfile +++ b/varnish/Dockerfile @@ -11,4 +11,6 @@ ADD default.vcl /etc/varnish/default.vcl CMD ulimit -n 131072 && \ /usr/sbin/varnishd -F -f /etc/varnish/default.vcl \ - -a :6081 -T :6082 -a :6083,PROXY -t 120 + -a :6081 -T :6082 -a :6083,PROXY -t 120 \ + -p thread_pool_min=5 -p thread_pool_max=500\ + -p thread_pool_timeout=300 -- cgit 1.4.1 From 599d7659037477bb080a10f9027a39a4203c8ec8 Mon Sep 17 00:00:00 2001 From: Vincent Ambo <tazjin@gmail.com> Date: Tue, 24 Nov 2015 14:07:31 +0100 Subject: [blog] Add a deleteEntry function, only for CLI --- src/BlogDB.hs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/BlogDB.hs b/src/BlogDB.hs index 52e4e80c39..bc9c243933 100644 --- a/src/BlogDB.hs +++ b/src/BlogDB.hs @@ -130,9 +130,15 @@ insertEntry e = updateEntry :: Entry -> Update Blog Entry updateEntry e = do b@Blog{..} <- get - put $ b { blogEntries = IxSet.updateIx (entryId e) e blogEntries} + put $ b { blogEntries = IxSet.updateIx (entryId e) e blogEntries } return e +deleteEntry :: EntryId -> Update Blog EntryId +deleteEntry entry = + do b@Blog{..} <- get + put $ b { blogEntries = IxSet.deleteIx entry blogEntries } + return entry + getEntry :: EntryId -> Query Blog (Maybe Entry) getEntry eId = do Blog{..} <- ask @@ -187,6 +193,7 @@ hashString = B64.encode . SHA.hash . B.pack $(makeAcidic ''Blog [ 'insertEntry , 'updateEntry + , 'deleteEntry , 'getEntry , 'latestEntries , 'addSession -- cgit 1.4.1 From 792fe17f359b63a352e981dd3683179483d51236 Mon Sep 17 00:00:00 2001 From: Vincent Ambo <tazjin@gmail.com> Date: Tue, 24 Nov 2015 14:07:57 +0100 Subject: Version 5.1.2 --- src/Locales.hs | 2 +- tazblog.cabal | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Locales.hs b/src/Locales.hs index 92acb215f8..c1ddcb38fa 100644 --- a/src/Locales.hs +++ b/src/Locales.hs @@ -8,7 +8,7 @@ import Network.URI data BlogError = NotFound | UnknownError -version = "5.1.1" +version = "5.1.2" blogTitle :: BlogLang -> Text -> Text blogTitle DE s = T.concat ["Tazjins blog", s] diff --git a/tazblog.cabal b/tazblog.cabal index 4fdffd2108..da624ef904 100644 --- a/tazblog.cabal +++ b/tazblog.cabal @@ -1,5 +1,5 @@ Name: tazblog -Version: 5.1.1 +Version: 5.1.2 Synopsis: Tazjin's Blog License: MIT License-file: LICENSE -- cgit 1.4.1 From e949c5e812dbe06f441ac5381492b08f683e9f25 Mon Sep 17 00:00:00 2001 From: Vincent Ambo <tazjin@gmail.com> Date: Sun, 29 Nov 2015 16:52:17 +0100 Subject: [build] Cache Docker dependencies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 73ab1057a2..7d8b605826 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,11 @@ FROM fpco/stack-build MAINTAINER Vincent Ambo <dev@tazj.in> +# Cache dependencies +ADD stack.yaml tazblog.cabal /opt/tazblog/ +WORKDIR /opt/tazblog +RUN stack build --only-dependencies + # Base setup VOLUME /var/tazblog EXPOSE 8000 8070 @@ -8,8 +13,7 @@ ENV PATH /root/.local/bin:$PATH # Build blog ADD . /opt/tazblog -WORKDIR /opt/tazblog -RUN stack install && cp /root/.local/bin/tazblog /usr/bin/tazblog +RUN stack install && cp /root/.local/bin/tazblog* /usr/bin/ # Done! CMD tazblog -- cgit 1.4.1 From 8addea105871151453a70e2fec5a91fe2ed13937 Mon Sep 17 00:00:00 2001 From: Vincent Ambo <tazjin@gmail.com> Date: Sat, 13 Feb 2016 09:20:43 +0100 Subject: [server] Serve static files at root --- src/Server.hs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Server.hs b/src/Server.hs index 373bb7d6b5..c025be009a 100644 --- a/src/Server.hs +++ b/src/Server.hs @@ -42,6 +42,7 @@ tazBlog acid resDir = do , method POST >> processLogin acid ] , dir "static" $ staticHandler resDir , blogHandler acid EN + , staticHandler resDir , notFound $ toResponse $ showError NotFound DE ] -- cgit 1.4.1 From b5425938d4c9e1882c90c2aa2d7a5e5515258fb3 Mon Sep 17 00:00:00 2001 From: Vincent Ambo <tazjin@gmail.com> Date: Sat, 13 Feb 2016 11:47:34 +0100 Subject: [stack] Bump to LTS 5.2 --- stack.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stack.yaml b/stack.yaml index 16b13a65f8..5242b88aa7 100644 --- a/stack.yaml +++ b/stack.yaml @@ -1,7 +1,7 @@ # For more information, see: https://github.com/commercialhaskell/stack/blob/release/doc/yaml_configuration.md # Specifies the GHC version and set of packages available (e.g., lts-3.5, nightly-2015-09-21, ghc-7.10.2) -resolver: lts-3.14 +resolver: lts-5.2 # Local packages, usually specified by relative directory name packages: -- cgit 1.4.1 From ca0d71b630dc84a644a907de456c02f3aca58253 Mon Sep 17 00:00:00 2001 From: Vincent Ambo <tazjin@gmail.com> Date: Sun, 14 Feb 2016 01:43:22 +0100 Subject: [k8s/cabal] Bump to 5.1.3 --- k8s/tazblog-rc.yaml | 6 +++--- tazblog.cabal | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/k8s/tazblog-rc.yaml b/k8s/tazblog-rc.yaml index 019d1bee06..b29a4d5d75 100644 --- a/k8s/tazblog-rc.yaml +++ b/k8s/tazblog-rc.yaml @@ -1,17 +1,17 @@ apiVersion: v1 kind: ReplicationController metadata: - name: tazblog-5.1.1 + name: tazblog-5.1.3 spec: replicas: 2 selector: app: tazblog - version: v5.1.1 + version: v5.1.3 template: metadata: labels: app: tazblog - version: v5.1.1 + version: v5.1.3 spec: containers: - image: tazjin/tazblog-haskell:master diff --git a/tazblog.cabal b/tazblog.cabal index da624ef904..3ca9d373b2 100644 --- a/tazblog.cabal +++ b/tazblog.cabal @@ -1,5 +1,5 @@ Name: tazblog -Version: 5.1.2 +Version: 5.1.3 Synopsis: Tazjin's Blog License: MIT License-file: LICENSE -- cgit 1.4.1 From eadf75d86b04b5fb7a7ef286af9c4621e9e58e31 Mon Sep 17 00:00:00 2001 From: Vincent Ambo <tazjin@gmail.com> Date: Fri, 22 Dec 2017 10:45:33 +0100 Subject: Bump to LTS 9.11 --- stack.yaml | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/stack.yaml b/stack.yaml index 5242b88aa7..9f00e8a31b 100644 --- a/stack.yaml +++ b/stack.yaml @@ -1,20 +1,12 @@ # For more information, see: https://github.com/commercialhaskell/stack/blob/release/doc/yaml_configuration.md -# Specifies the GHC version and set of packages available (e.g., lts-3.5, nightly-2015-09-21, ghc-7.10.2) -resolver: lts-5.2 - -# Local packages, usually specified by relative directory name +resolver: lts-9.11 packages: - '.' - -# Packages to be pulled from upstream that are not in the resolver (e.g., acme-missiles-0.3) extra-deps: - - ixset-1.0.6 - - syb-with-class-0.6.1.6 # needed by ixset - - rss-3000.2.0.5 - -# Override default flag values for local packages and extra-deps + - acid-state-0.14.3 + - ixset-1.0.7 + - rss-3000.2.0.6 + - syb-with-class-0.6.1.8 flags: {} - -# Extra package databases containing global packages extra-package-dbs: [] -- cgit 1.4.1 From aeeb11f1b76729115c4db98f419cbcda1a0f7660 Mon Sep 17 00:00:00 2001 From: Vincent Ambo <tazjin@gmail.com> Date: Fri, 22 Dec 2017 11:21:06 +0100 Subject: Bump to LTS 9.20 --- stack.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stack.yaml b/stack.yaml index 9f00e8a31b..8841429aa0 100644 --- a/stack.yaml +++ b/stack.yaml @@ -1,6 +1,6 @@ # For more information, see: https://github.com/commercialhaskell/stack/blob/release/doc/yaml_configuration.md -resolver: lts-9.11 +resolver: lts-9.20 packages: - '.' extra-deps: -- cgit 1.4.1 From 2373c925e18963c6f2698ba4c45d74196f3aaaf3 Mon Sep 17 00:00:00 2001 From: Vincent Ambo <tazjin@google.com> Date: Fri, 28 Jun 2019 22:55:13 +0100 Subject: refactor: Move tazblog into monorepo structure It's happening! --- .dockerignore | 1 - .gitignore | 7 - .stylish.haskell.yaml | 20 --- Dockerfile | 19 --- LICENSE | 21 --- Makefile | 17 -- TODO | 1 - backup.sh | 2 - blog/Main.hs | 41 ----- db/Main.hs | 34 ---- k8s/tazblog-db-rc.yaml | 26 --- k8s/tazblog-db-service.yaml | 12 -- k8s/tazblog-rc.yaml | 45 ------ k8s/tazblog-svc.yaml | 17 -- services/tazblog/.gitignore | 7 + services/tazblog/.stylish.haskell.yaml | 20 +++ services/tazblog/Dockerfile | 19 +++ services/tazblog/LICENSE | 21 +++ services/tazblog/Makefile | 17 ++ services/tazblog/TODO | 1 + services/tazblog/backup.sh | 2 + services/tazblog/blog/Main.hs | 41 +++++ services/tazblog/db/Main.hs | 34 ++++ services/tazblog/k8s/tazblog-db-rc.yaml | 26 +++ services/tazblog/k8s/tazblog-db-service.yaml | 12 ++ services/tazblog/k8s/tazblog-rc.yaml | 45 ++++++ services/tazblog/k8s/tazblog-svc.yaml | 17 ++ services/tazblog/src/Blog.hs | 234 +++++++++++++++++++++++++++ services/tazblog/src/BlogDB.hs | 229 ++++++++++++++++++++++++++ services/tazblog/src/Locales.hs | 61 +++++++ services/tazblog/src/RSS.hs | 41 +++++ services/tazblog/src/Server.hs | 189 ++++++++++++++++++++++ services/tazblog/stack.yaml | 12 ++ services/tazblog/static/admin.css | 49 ++++++ services/tazblog/static/apple-touch-icon.png | Bin 0 -> 9756 bytes services/tazblog/static/blog.css | 35 ++++ services/tazblog/static/favicon.ico | Bin 0 -> 4354 bytes services/tazblog/static/keybase.txt | 69 ++++++++ services/tazblog/static/loginBoxTop.png | Bin 0 -> 606 bytes services/tazblog/static/signin.gif | Bin 0 -> 1850 bytes services/tazblog/tazblog.cabal | 71 ++++++++ services/tazblog/varnish/Dockerfile | 16 ++ services/tazblog/varnish/default.vcl | 60 +++++++ src/Blog.hs | 234 --------------------------- src/BlogDB.hs | 229 -------------------------- src/Locales.hs | 61 ------- src/RSS.hs | 41 ----- src/Server.hs | 189 ---------------------- stack.yaml | 12 -- static/admin.css | 49 ------ static/apple-touch-icon.png | Bin 9756 -> 0 bytes static/blog.css | 35 ---- static/favicon.ico | Bin 4354 -> 0 bytes static/keybase.txt | 69 -------- static/loginBoxTop.png | Bin 606 -> 0 bytes static/signin.gif | Bin 1850 -> 0 bytes tazblog.cabal | 71 -------- varnish/Dockerfile | 16 -- varnish/default.vcl | 60 ------- 59 files changed, 1328 insertions(+), 1329 deletions(-) delete mode 100644 .dockerignore delete mode 100644 .gitignore delete mode 100644 .stylish.haskell.yaml delete mode 100644 Dockerfile delete mode 100644 LICENSE delete mode 100644 Makefile delete mode 100644 TODO delete mode 100644 backup.sh delete mode 100644 blog/Main.hs delete mode 100644 db/Main.hs delete mode 100644 k8s/tazblog-db-rc.yaml delete mode 100644 k8s/tazblog-db-service.yaml delete mode 100644 k8s/tazblog-rc.yaml delete mode 100644 k8s/tazblog-svc.yaml create mode 100644 services/tazblog/.gitignore create mode 100644 services/tazblog/.stylish.haskell.yaml create mode 100644 services/tazblog/Dockerfile create mode 100644 services/tazblog/LICENSE create mode 100644 services/tazblog/Makefile create mode 100644 services/tazblog/TODO create mode 100644 services/tazblog/backup.sh create mode 100644 services/tazblog/blog/Main.hs create mode 100644 services/tazblog/db/Main.hs create mode 100644 services/tazblog/k8s/tazblog-db-rc.yaml create mode 100644 services/tazblog/k8s/tazblog-db-service.yaml create mode 100644 services/tazblog/k8s/tazblog-rc.yaml create mode 100644 services/tazblog/k8s/tazblog-svc.yaml create mode 100644 services/tazblog/src/Blog.hs create mode 100644 services/tazblog/src/BlogDB.hs create mode 100644 services/tazblog/src/Locales.hs create mode 100644 services/tazblog/src/RSS.hs create mode 100644 services/tazblog/src/Server.hs create mode 100644 services/tazblog/stack.yaml create mode 100644 services/tazblog/static/admin.css create mode 100644 services/tazblog/static/apple-touch-icon.png create mode 100644 services/tazblog/static/blog.css create mode 100644 services/tazblog/static/favicon.ico create mode 100644 services/tazblog/static/keybase.txt create mode 100644 services/tazblog/static/loginBoxTop.png create mode 100644 services/tazblog/static/signin.gif create mode 100644 services/tazblog/tazblog.cabal create mode 100644 services/tazblog/varnish/Dockerfile create mode 100644 services/tazblog/varnish/default.vcl delete mode 100644 src/Blog.hs delete mode 100644 src/BlogDB.hs delete mode 100644 src/Locales.hs delete mode 100644 src/RSS.hs delete mode 100644 src/Server.hs delete mode 100644 stack.yaml delete mode 100644 static/admin.css delete mode 100644 static/apple-touch-icon.png delete mode 100644 static/blog.css delete mode 100644 static/favicon.ico delete mode 100644 static/keybase.txt delete mode 100644 static/loginBoxTop.png delete mode 100644 static/signin.gif delete mode 100644 tazblog.cabal delete mode 100644 varnish/Dockerfile delete mode 100644 varnish/default.vcl diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 8ee1bf9489..0000000000 --- a/.dockerignore +++ /dev/null @@ -1 +0,0 @@ -.stack-work diff --git a/.gitignore b/.gitignore deleted file mode 100644 index a95070c31f..0000000000 --- a/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -*.o -*.hi -BlogState/ -dist/ -.cabal-sandbox/ -*.tar.gz -.stack-work/ diff --git a/.stylish.haskell.yaml b/.stylish.haskell.yaml deleted file mode 100644 index cb432ce231..0000000000 --- a/.stylish.haskell.yaml +++ /dev/null @@ -1,20 +0,0 @@ -steps: - - imports: - align: group - - language_pragmas: - style: vertical - remove_redundant: true - - records: {} - - trailing_whitespace: {} -columns: 120 -language_extensions: - - DeriveDataTypeable - - FlexibleContexts - - GeneralizedNewtypeDeriving - - MultiParamTypeClasses - - OverloadedStrings - - RecordWildCards - - ScopedTypeVariables - - TemplateHaskell - - TypeFamilies - - QuasiQuotes diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 7d8b605826..0000000000 --- a/Dockerfile +++ /dev/null @@ -1,19 +0,0 @@ -FROM fpco/stack-build -MAINTAINER Vincent Ambo <dev@tazj.in> - -# Cache dependencies -ADD stack.yaml tazblog.cabal /opt/tazblog/ -WORKDIR /opt/tazblog -RUN stack build --only-dependencies - -# Base setup -VOLUME /var/tazblog -EXPOSE 8000 8070 -ENV PATH /root/.local/bin:$PATH - -# Build blog -ADD . /opt/tazblog -RUN stack install && cp /root/.local/bin/tazblog* /usr/bin/ - -# Done! -CMD tazblog diff --git a/LICENSE b/LICENSE deleted file mode 100644 index f5c81f7e3a..0000000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2014 Vincent Ambo - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/Makefile b/Makefile deleted file mode 100644 index 00d77dd36c..0000000000 --- a/Makefile +++ /dev/null @@ -1,17 +0,0 @@ -VERSION=$(shell bash -c "grep -P -o -e '\d\.\d$$' TazBlog.cabal | head -n1") -ARCH_PKG=arch/tazblog-$(VERSION)-1-x86_64.pkg.tar.xz -export ARCH_PKG - -all: archpkg docker - -archpkg: $(ARCH_PKG) - -$(ARCH_PKG): - cd arch && makepkg - -docker: archpkg - cat Dockerfile.raw | envsubst > Dockerfile; \ - docker build -t tazjin/tazblog . - -clean: - rm -rf dist arch/*.pkg.tar.xz arch/pkg arch/src arch/*. Dockerfile diff --git a/TODO b/TODO deleted file mode 100644 index fdb963dd79..0000000000 --- a/TODO +++ /dev/null @@ -1 +0,0 @@ -* Bootstrap: http://twitter.github.com/bootstrap/index.html diff --git a/backup.sh b/backup.sh deleted file mode 100644 index bbc3167324..0000000000 --- a/backup.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -tar cf backup.tar BlogState/ diff --git a/blog/Main.hs b/blog/Main.hs deleted file mode 100644 index cfe068a8d9..0000000000 --- a/blog/Main.hs +++ /dev/null @@ -1,41 +0,0 @@ --- | Main module for the blog's web server -module Main where - -import BlogDB (initialBlogState) -import Control.Applicative (pure, (<$>), (<*>)) -import Control.Exception (bracket) -import Data.Acid -import Data.Acid.Remote -import Data.Word (Word16) -import Locales (version) -import Network (HostName, PortID (..)) -import Options -import Server - -data MainOptions = MainOptions { - dbHost :: String, - dbPort :: Word16, - blogPort :: Int, - resourceDir :: String -} - -instance Options MainOptions where - defineOptions = pure MainOptions - <*> simpleOption "dbHost" "localhost" - "Remote acid-state database host. Default is localhost" - <*> simpleOption "dbPort" 8070 - "Remote acid-state database port. Default is 8070" - <*> simpleOption "blogPort" 8000 - "Port to serve the blog on. Default is 8000." - <*> simpleOption "resourceDir" "/opt/tazblog/static" - "Resources folder location." - -main :: IO() -main = do - putStrLn ("TazBlog " ++ version ++ " in Haskell starting") - runCommand $ \opts _ -> - let port = PortNumber $ fromIntegral $ dbPort opts - in openRemoteState skipAuthenticationPerform (dbHost opts) port >>= - (\acid -> runBlog acid (blogPort opts) (resourceDir opts)) - - diff --git a/db/Main.hs b/db/Main.hs deleted file mode 100644 index 9523041f10..0000000000 --- a/db/Main.hs +++ /dev/null @@ -1,34 +0,0 @@ --- | Main module for the database server -module Main where - -import BlogDB (initialBlogState) -import Control.Applicative (pure, (<$>), (<*>)) -import Control.Exception (bracket) -import Data.Acid -import Data.Acid.Local (createCheckpointAndClose) -import Data.Acid.Remote -import Data.Word -import Network (PortID (..)) -import Options - -data DBOptions = DBOptions { - dbPort :: Word16, - stateDirectory :: String -} - -instance Options DBOptions where - defineOptions = pure DBOptions - <*> simpleOption "dbport" 8070 - "Port to serve acid-state on remotely." - <*> simpleOption "state" "/var/tazblog/state" - "Directory in which the acid-state is located." - -main :: IO () -main = do - putStrLn ("Launching TazBlog database server ...") - runCommand $ \opts args -> - bracket (openState opts) createCheckpointAndClose - (acidServer skipAuthenticationCheck $ getPort opts) - where - openState o = openLocalStateFrom (stateDirectory o) initialBlogState - getPort = PortNumber . fromIntegral . dbPort diff --git a/k8s/tazblog-db-rc.yaml b/k8s/tazblog-db-rc.yaml deleted file mode 100644 index 26d730c4df..0000000000 --- a/k8s/tazblog-db-rc.yaml +++ /dev/null @@ -1,26 +0,0 @@ -apiVersion: v1 -kind: ReplicationController -metadata: - name: tazblog-db -spec: - selector: - app: tazblog-db - template: - metadata: - labels: - app: tazblog-db - spec: - containers: - - image: tazjin/tazblog-haskell:master - name: tazblog-db - command: ["tazblog-db"] - ports: - - containerPort: 8070 - volumeMounts: - - name: tazblog-state - mountPath: /var/tazblog - volumes: - - name: tazblog-state - gcePersistentDisk: - pdName: tazblog-state - fsType: ext4 diff --git a/k8s/tazblog-db-service.yaml b/k8s/tazblog-db-service.yaml deleted file mode 100644 index 6d5d429469..0000000000 --- a/k8s/tazblog-db-service.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: tazblog-db - labels: - app: tazblog-db -spec: - selector: - app: tazblog-db - ports: - - port: 8070 - name: tazblog-db diff --git a/k8s/tazblog-rc.yaml b/k8s/tazblog-rc.yaml deleted file mode 100644 index b29a4d5d75..0000000000 --- a/k8s/tazblog-rc.yaml +++ /dev/null @@ -1,45 +0,0 @@ -apiVersion: v1 -kind: ReplicationController -metadata: - name: tazblog-5.1.3 -spec: - replicas: 2 - selector: - app: tazblog - version: v5.1.3 - template: - metadata: - labels: - app: tazblog - version: v5.1.3 - spec: - containers: - - image: tazjin/tazblog-haskell:master - imagePullPolicy: Always - name: tazblog - command: ["tazblog", "--dbHost", "tazblog-db.default.svc.cluster.local"] - ports: - - containerPort: 8000 - - image: tazjin/varnish - imagePullPolicy: Always - name: tazblog-varnish - ports: - - containerPort: 6081 - - containerPort: 6082 - - image: tazjin/hitch:master - imagePullPolicy: Always - name: tazblog-hitch - command: ["hitch", "--backend=[127.0.0.1]:6083", "--write-proxy", "--user=hitch", "/etc/hitch/ssl/tazblog-tls"] - ports: - - containerPort: 8443 - volumeMounts: - - name: tazblog-tls - readOnly: true - mountPath: /etc/hitch/ssl - resources: - requests: - memory: "1024Mi" - volumes: - - name: tazblog-tls - secret: - secretName: tazblog-tls diff --git a/k8s/tazblog-svc.yaml b/k8s/tazblog-svc.yaml deleted file mode 100644 index 6a2d9a4223..0000000000 --- a/k8s/tazblog-svc.yaml +++ /dev/null @@ -1,17 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: tazblog - labels: - app: tazblog -spec: - type: LoadBalancer - selector: - app: tazblog - ports: - - port: 80 - targetPort: 6081 - name: tazblog-http - - port: 443 - targetPort: 8443 - name: tazblog-https diff --git a/services/tazblog/.gitignore b/services/tazblog/.gitignore new file mode 100644 index 0000000000..a95070c31f --- /dev/null +++ b/services/tazblog/.gitignore @@ -0,0 +1,7 @@ +*.o +*.hi +BlogState/ +dist/ +.cabal-sandbox/ +*.tar.gz +.stack-work/ diff --git a/services/tazblog/.stylish.haskell.yaml b/services/tazblog/.stylish.haskell.yaml new file mode 100644 index 0000000000..cb432ce231 --- /dev/null +++ b/services/tazblog/.stylish.haskell.yaml @@ -0,0 +1,20 @@ +steps: + - imports: + align: group + - language_pragmas: + style: vertical + remove_redundant: true + - records: {} + - trailing_whitespace: {} +columns: 120 +language_extensions: + - DeriveDataTypeable + - FlexibleContexts + - GeneralizedNewtypeDeriving + - MultiParamTypeClasses + - OverloadedStrings + - RecordWildCards + - ScopedTypeVariables + - TemplateHaskell + - TypeFamilies + - QuasiQuotes diff --git a/services/tazblog/Dockerfile b/services/tazblog/Dockerfile new file mode 100644 index 0000000000..7d8b605826 --- /dev/null +++ b/services/tazblog/Dockerfile @@ -0,0 +1,19 @@ +FROM fpco/stack-build +MAINTAINER Vincent Ambo <dev@tazj.in> + +# Cache dependencies +ADD stack.yaml tazblog.cabal /opt/tazblog/ +WORKDIR /opt/tazblog +RUN stack build --only-dependencies + +# Base setup +VOLUME /var/tazblog +EXPOSE 8000 8070 +ENV PATH /root/.local/bin:$PATH + +# Build blog +ADD . /opt/tazblog +RUN stack install && cp /root/.local/bin/tazblog* /usr/bin/ + +# Done! +CMD tazblog diff --git a/services/tazblog/LICENSE b/services/tazblog/LICENSE new file mode 100644 index 0000000000..f5c81f7e3a --- /dev/null +++ b/services/tazblog/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Vincent Ambo + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/services/tazblog/Makefile b/services/tazblog/Makefile new file mode 100644 index 0000000000..00d77dd36c --- /dev/null +++ b/services/tazblog/Makefile @@ -0,0 +1,17 @@ +VERSION=$(shell bash -c "grep -P -o -e '\d\.\d$$' TazBlog.cabal | head -n1") +ARCH_PKG=arch/tazblog-$(VERSION)-1-x86_64.pkg.tar.xz +export ARCH_PKG + +all: archpkg docker + +archpkg: $(ARCH_PKG) + +$(ARCH_PKG): + cd arch && makepkg + +docker: archpkg + cat Dockerfile.raw | envsubst > Dockerfile; \ + docker build -t tazjin/tazblog . + +clean: + rm -rf dist arch/*.pkg.tar.xz arch/pkg arch/src arch/*. Dockerfile diff --git a/services/tazblog/TODO b/services/tazblog/TODO new file mode 100644 index 0000000000..fdb963dd79 --- /dev/null +++ b/services/tazblog/TODO @@ -0,0 +1 @@ +* Bootstrap: http://twitter.github.com/bootstrap/index.html diff --git a/services/tazblog/backup.sh b/services/tazblog/backup.sh new file mode 100644 index 0000000000..bbc3167324 --- /dev/null +++ b/services/tazblog/backup.sh @@ -0,0 +1,2 @@ +#!/bin/bash +tar cf backup.tar BlogState/ diff --git a/services/tazblog/blog/Main.hs b/services/tazblog/blog/Main.hs new file mode 100644 index 0000000000..cfe068a8d9 --- /dev/null +++ b/services/tazblog/blog/Main.hs @@ -0,0 +1,41 @@ +-- | Main module for the blog's web server +module Main where + +import BlogDB (initialBlogState) +import Control.Applicative (pure, (<$>), (<*>)) +import Control.Exception (bracket) +import Data.Acid +import Data.Acid.Remote +import Data.Word (Word16) +import Locales (version) +import Network (HostName, PortID (..)) +import Options +import Server + +data MainOptions = MainOptions { + dbHost :: String, + dbPort :: Word16, + blogPort :: Int, + resourceDir :: String +} + +instance Options MainOptions where + defineOptions = pure MainOptions + <*> simpleOption "dbHost" "localhost" + "Remote acid-state database host. Default is localhost" + <*> simpleOption "dbPort" 8070 + "Remote acid-state database port. Default is 8070" + <*> simpleOption "blogPort" 8000 + "Port to serve the blog on. Default is 8000." + <*> simpleOption "resourceDir" "/opt/tazblog/static" + "Resources folder location." + +main :: IO() +main = do + putStrLn ("TazBlog " ++ version ++ " in Haskell starting") + runCommand $ \opts _ -> + let port = PortNumber $ fromIntegral $ dbPort opts + in openRemoteState skipAuthenticationPerform (dbHost opts) port >>= + (\acid -> runBlog acid (blogPort opts) (resourceDir opts)) + + diff --git a/services/tazblog/db/Main.hs b/services/tazblog/db/Main.hs new file mode 100644 index 0000000000..9523041f10 --- /dev/null +++ b/services/tazblog/db/Main.hs @@ -0,0 +1,34 @@ +-- | Main module for the database server +module Main where + +import BlogDB (initialBlogState) +import Control.Applicative (pure, (<$>), (<*>)) +import Control.Exception (bracket) +import Data.Acid +import Data.Acid.Local (createCheckpointAndClose) +import Data.Acid.Remote +import Data.Word +import Network (PortID (..)) +import Options + +data DBOptions = DBOptions { + dbPort :: Word16, + stateDirectory :: String +} + +instance Options DBOptions where + defineOptions = pure DBOptions + <*> simpleOption "dbport" 8070 + "Port to serve acid-state on remotely." + <*> simpleOption "state" "/var/tazblog/state" + "Directory in which the acid-state is located." + +main :: IO () +main = do + putStrLn ("Launching TazBlog database server ...") + runCommand $ \opts args -> + bracket (openState opts) createCheckpointAndClose + (acidServer skipAuthenticationCheck $ getPort opts) + where + openState o = openLocalStateFrom (stateDirectory o) initialBlogState + getPort = PortNumber . fromIntegral . dbPort diff --git a/services/tazblog/k8s/tazblog-db-rc.yaml b/services/tazblog/k8s/tazblog-db-rc.yaml new file mode 100644 index 0000000000..26d730c4df --- /dev/null +++ b/services/tazblog/k8s/tazblog-db-rc.yaml @@ -0,0 +1,26 @@ +apiVersion: v1 +kind: ReplicationController +metadata: + name: tazblog-db +spec: + selector: + app: tazblog-db + template: + metadata: + labels: + app: tazblog-db + spec: + containers: + - image: tazjin/tazblog-haskell:master + name: tazblog-db + command: ["tazblog-db"] + ports: + - containerPort: 8070 + volumeMounts: + - name: tazblog-state + mountPath: /var/tazblog + volumes: + - name: tazblog-state + gcePersistentDisk: + pdName: tazblog-state + fsType: ext4 diff --git a/services/tazblog/k8s/tazblog-db-service.yaml b/services/tazblog/k8s/tazblog-db-service.yaml new file mode 100644 index 0000000000..6d5d429469 --- /dev/null +++ b/services/tazblog/k8s/tazblog-db-service.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: tazblog-db + labels: + app: tazblog-db +spec: + selector: + app: tazblog-db + ports: + - port: 8070 + name: tazblog-db diff --git a/services/tazblog/k8s/tazblog-rc.yaml b/services/tazblog/k8s/tazblog-rc.yaml new file mode 100644 index 0000000000..b29a4d5d75 --- /dev/null +++ b/services/tazblog/k8s/tazblog-rc.yaml @@ -0,0 +1,45 @@ +apiVersion: v1 +kind: ReplicationController +metadata: + name: tazblog-5.1.3 +spec: + replicas: 2 + selector: + app: tazblog + version: v5.1.3 + template: + metadata: + labels: + app: tazblog + version: v5.1.3 + spec: + containers: + - image: tazjin/tazblog-haskell:master + imagePullPolicy: Always + name: tazblog + command: ["tazblog", "--dbHost", "tazblog-db.default.svc.cluster.local"] + ports: + - containerPort: 8000 + - image: tazjin/varnish + imagePullPolicy: Always + name: tazblog-varnish + ports: + - containerPort: 6081 + - containerPort: 6082 + - image: tazjin/hitch:master + imagePullPolicy: Always + name: tazblog-hitch + command: ["hitch", "--backend=[127.0.0.1]:6083", "--write-proxy", "--user=hitch", "/etc/hitch/ssl/tazblog-tls"] + ports: + - containerPort: 8443 + volumeMounts: + - name: tazblog-tls + readOnly: true + mountPath: /etc/hitch/ssl + resources: + requests: + memory: "1024Mi" + volumes: + - name: tazblog-tls + secret: + secretName: tazblog-tls diff --git a/services/tazblog/k8s/tazblog-svc.yaml b/services/tazblog/k8s/tazblog-svc.yaml new file mode 100644 index 0000000000..6a2d9a4223 --- /dev/null +++ b/services/tazblog/k8s/tazblog-svc.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + name: tazblog + labels: + app: tazblog +spec: + type: LoadBalancer + selector: + app: tazblog + ports: + - port: 80 + targetPort: 6081 + name: tazblog-http + - port: 443 + targetPort: 8443 + name: tazblog-https diff --git a/services/tazblog/src/Blog.hs b/services/tazblog/src/Blog.hs new file mode 100644 index 0000000000..f35e3d9080 --- /dev/null +++ b/services/tazblog/src/Blog.hs @@ -0,0 +1,234 @@ +module Blog where + +import BlogDB +import Data.Maybe (fromJust) +import Data.Text (Text, append, empty, pack) +import Data.Text.Lazy (fromStrict) +import Data.Time +import Locales +import Text.Blaze.Html (preEscapedToHtml) +import Text.Hamlet +import Text.Markdown + +import qualified Data.Text as T + +replace :: Eq a => a -> a -> [a] -> [a] +replace x y = map (\z -> if z == x then y else z) + +show' :: Show a => a -> Text +show' = pack . show + +-- |After this time all entries are Markdown +markdownCutoff :: UTCTime +markdownCutoff = fromJust $ parseTimeM False defaultTimeLocale "%s" "1367149834" + +-- blog HTML +blogTemplate :: BlogLang -> Text -> Html -> Html +blogTemplate lang t_append body = [shamlet| +$doctype 5 + <head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <meta name="description" content=#{blogTitle lang t_append}> + <link rel="stylesheet" type="text/css" href="/static/blog.css" media="all"> + <link rel="alternate" type="application/rss+xml" title="RSS-Feed" href=#{rssUrl}> + <title>#{blogTitle lang t_append} + <body> + <header> + <h1> + <a href="/" .unstyled-link>#{blogTitle lang empty} + <hr> + ^{body} + ^{showFooter} +|] + where + rssUrl = T.concat ["/", show' lang, "/rss.xml"] + +showFooter :: Html +showFooter = [shamlet| +<footer> + <p .footer>Served without any dynamic languages. + <p .footer> + <a href=#{repoURL} .uncoloured-link>Version #{version} + | + <a href=#{twitter} .uncoloured-link>Twitter + | + <a href=#{mailTo} .uncoloured-link>Mail + <p .lod> + ಠ_ಠ +|] + +isEntryMarkdown :: Entry -> Bool +isEntryMarkdown e = edate e > markdownCutoff + +renderEntryMarkdown :: Text -> Html +renderEntryMarkdown = markdown def {msXssProtect = False} . fromStrict + +renderEntries :: Bool -> [Entry] -> Maybe Html -> Html +renderEntries showAll entries pageLinks = [shamlet| +$forall entry <- toDisplay + <article> + <h2 .inline> + <a href=#{linkElems entry} .unstyled-link> + #{title entry} + <aside .date> + #{pack $ formatTime defaultTimeLocale "%Y-%m-%d" $ edate entry} + $if (isEntryMarkdown entry) + ^{renderEntryMarkdown $ btext entry} + $else + ^{preEscapedToHtml $ btext entry} + $if ((/=) (mtext entry) empty) + <p> + <a .uncoloured-link href=#{linkElems entry}> + #{readMore $ lang entry} + <hr> +$maybe links <- pageLinks + ^{links} +|] + where + toDisplay = if showAll then entries else (take 6 entries) + linkElems Entry{..} = concat $ ["/", show lang, "/", show entryId] + +showLinks :: Maybe Int -> BlogLang -> Html +showLinks (Just i) lang = [shamlet| + $if ((>) i 1) + <div .navigation> + <a href=#{nLink $ succ i} .uncoloured-link>#{backText lang} + | + <a href=#{nLink $ pred i} .uncoloured-link>#{nextText lang} + $elseif ((<=) i 1) + ^{showLinks Nothing lang} +|] + where + nLink page = T.concat ["/", show' lang, "/?page=", show' page] +showLinks Nothing lang = [shamlet| +<div .navigation> + <a href=#{nLink} .uncoloured-link>#{backText lang} +|] + where + nLink = T.concat ["/", show' lang, "/?page=2"] + +renderEntry :: Entry -> Html +renderEntry e@Entry{..} = [shamlet| +<article> + <h2 .inline> + #{title} + <aside .date> + #{pack $ formatTime defaultTimeLocale "%Y-%m-%d" edate} + $if (isEntryMarkdown e) + ^{renderEntryMarkdown btext} + <p>^{renderEntryMarkdown $ mtext} + $else + ^{preEscapedToHtml $ btext} + <p>^{preEscapedToHtml $ mtext} +<hr> +|] + +{- Administration pages -} + +adminTemplate :: Text -> Html -> Html +adminTemplate title body = [shamlet| +$doctype 5 +<head> + <link rel="stylesheet" type="text/css" href="/static/admin.css" media="all"> + <meta http-equiv="content-type" content="text/html;charset=UTF-8"> + <title>#{append "TazBlog Admin: " title} +<body> + ^{body} +|] + +adminLogin :: Html +adminLogin = adminTemplate "Login" $ [shamlet| +<div class="loginBox"> + <div class="loginBoxTop">TazBlog Admin: Login + <div class="loginBoxMiddle"> + <form action="/admin" method="POST"> + <p>Account ID + <p><input type="text" style="font-size:2;" name="account" value="tazjin" readonly="1"> + <p>Passwort + <p><input type="password" style="font-size:2;" name="password"> + <p><input alt="Anmelden" type="image" src="/static/signin.gif"> +|] + +adminIndex :: Text -> Html +adminIndex sUser = adminTemplate "Index" $ [shamlet| +<div style="float:center;"> + <form action="/admin/entry" method="POST"> + <table> + <tr> + <thead><td>Title: + <td><input type="text" name="title"> + <tr> + <thead><td>Language: + <td><select name="lang"> + <option value="en">English + <option value="de">Deutsch + <tr> + <thead><td>Text: + <td> + <textarea name="btext" cols="100" rows="15"> + <tr> + <thead> + <td style="vertical-align:top;">Read more: + <td> + <textarea name="mtext" cols="100" rows="15"> + <input type="hidden" name="author" value=#{sUser}> + <input style="margin-left:20px;" type="submit" value="Submit"> + ^{adminFooter} +|] + +adminFooter :: Html +adminFooter = [shamlet| +<a href="/">Front page +\ -- # + <a href="/admin">New article +\ -- Entry list: # + <a href="/admin/entrylist/en">EN +\ & # +<a href="/admin/entrylist/de">DE +|] + +adminEntryList :: [Entry] -> Html +adminEntryList entries = adminTemplate "EntryList" $ [shamlet| +<div style="float: center;"> + <table> + $forall entry <- entries + <tr> + <td><a href=#{append "/admin/entry/" (show' $ entryId entry)}>#{title entry} + <td>#{formatPostDate $ edate entry} +|] + where + formatPostDate = formatTime defaultTimeLocale "[On %D at %H:%M]" + +editPage :: Entry -> Html +editPage (Entry{..}) = adminTemplate "Index" $ [shamlet| +<div style="float:center;"> + <form action=#{append "/admin/entry/" (show' entryId)} method="POST"> + <table> + <tr> + <td>Title: + <td> + <input type="text" name="title" value=#{title}> + <tr> + <td style="vertical-align:top;">Text: + <td> + <textarea name="btext" cols="100" rows="15">#{btext} + <tr> + <td style="vertical-align:top;">Read more: + <td> + <textarea name="mtext" cols="100" rows="15">#{mtext} + <input type="submit" style="margin-left:20px;" value="Submit"> + <p>^{adminFooter} +|] + +showError :: BlogError -> BlogLang -> Html +showError NotFound l = blogTemplate l (T.append ": " $ notFoundTitle l) $ [shamlet| +<p>:( +<p>#{notFoundText l} +<hr> +|] +showError UnknownError l = blogTemplate l "" $ [shamlet| +<p>:( +<p>#{unknownErrorText l} +<hr> +|] diff --git a/services/tazblog/src/BlogDB.hs b/services/tazblog/src/BlogDB.hs new file mode 100644 index 0000000000..bc9c243933 --- /dev/null +++ b/services/tazblog/src/BlogDB.hs @@ -0,0 +1,229 @@ +module BlogDB where + +import Control.Monad.Reader (ask) +import Control.Monad.State (get, put) +import Data.Acid +import Data.Acid.Advanced +import Data.Acid.Remote +import Data.ByteString (ByteString) +import Data.Data (Data, Typeable) +import Data.IxSet (Indexable (..), IxSet, Proxy (..), getOne, ixFun, ixSet, (@=)) +import Data.SafeCopy (base, deriveSafeCopy) +import Data.Text (Text, pack) +import Data.Time +import Network (PortID (..)) +import System.Environment (getEnv) + +import qualified Crypto.Hash.SHA512 as SHA (hash) +import qualified Data.ByteString.Base64 as B64 (encode) +import qualified Data.ByteString.Char8 as B +import qualified Data.IxSet as IxSet + +newtype EntryId = EntryId { unEntryId :: Integer } + deriving (Eq, Ord, Data, Enum, Typeable) + +$(deriveSafeCopy 2 'base ''EntryId) + +instance Show EntryId where + show = show . unEntryId + +data BlogLang = EN | DE + deriving (Eq, Ord, Data, Typeable) + +instance Show BlogLang where + show DE = "de" + show EN = "en" + +$(deriveSafeCopy 0 'base ''BlogLang) + +data Entry = Entry { + entryId :: EntryId, + lang :: BlogLang, + author :: Text, + title :: Text, + btext :: Text, + mtext :: Text, + edate :: UTCTime +} deriving (Eq, Ord, Show, Data, Typeable) + +$(deriveSafeCopy 2 'base ''Entry) + +-- ixSet requires different datatypes for field indexes, so let's define some +newtype Author = Author Text deriving (Eq, Ord, Data, Typeable) +newtype Title = Title Text deriving (Eq, Ord, Data, Typeable) +newtype BText = BText Text deriving (Eq, Ord, Data, Typeable) -- standard text +newtype MText = MText Text deriving (Eq, Ord, Data, Typeable) -- "read more" text +newtype Tag = Tag Text deriving (Eq, Ord, Data, Typeable) +newtype EDate = EDate UTCTime deriving (Eq, Ord, Data, Typeable) +newtype SDate = SDate UTCTime deriving (Eq, Ord, Data, Typeable) +newtype Username = Username Text deriving (Eq, Ord, Data, Typeable) +newtype SessionID = SessionID Text deriving (Eq, Ord, Data, Typeable) + +$(deriveSafeCopy 2 'base ''Author) +$(deriveSafeCopy 2 'base ''Title) +$(deriveSafeCopy 2 'base ''BText) +$(deriveSafeCopy 2 'base ''MText) +$(deriveSafeCopy 2 'base ''Tag) +$(deriveSafeCopy 2 'base ''EDate) +$(deriveSafeCopy 2 'base ''SDate) +$(deriveSafeCopy 2 'base ''Username) +$(deriveSafeCopy 2 'base ''SessionID) + +instance Indexable Entry where + empty = ixSet [ ixFun $ \e -> [ entryId e] + , ixFun $ (:[]) . lang + , ixFun $ \e -> [ Author $ author e ] + , ixFun $ \e -> [ Title $ title e] + , ixFun $ \e -> [ BText $ btext e] + , ixFun $ \e -> [ MText $ mtext e] + , ixFun $ \e -> [ EDate $ edate e] + ] + +data User = User { + username :: Text, + password :: ByteString +} deriving (Eq, Ord, Data, Typeable) + +$(deriveSafeCopy 0 'base ''User) + +data Session = Session { + sessionID :: Text, + user :: User, + sdate :: UTCTime +} deriving (Eq, Ord, Data, Typeable) + +$(deriveSafeCopy 0 'base ''Session) + +instance Indexable User where + empty = ixSet [ ixFun $ \u -> [Username $ username u] + , ixFun $ (:[]) . password + ] + +instance Indexable Session where + empty = ixSet [ ixFun $ \s -> [SessionID $ sessionID s] + , ixFun $ (:[]) . user + , ixFun $ \s -> [SDate $ sdate s] + ] + +data Blog = Blog { + blogSessions :: IxSet Session, + blogUsers :: IxSet User, + blogEntries :: IxSet Entry +} deriving (Data, Typeable) + +$(deriveSafeCopy 0 'base ''Blog) + +initialBlogState :: Blog +initialBlogState = + Blog { blogSessions = empty + , blogUsers = empty + , blogEntries = empty } + +-- acid-state database functions (purity is necessary!) + +insertEntry :: Entry -> Update Blog Entry +insertEntry e = + do b@Blog{..} <- get + put $ b { blogEntries = IxSet.insert e blogEntries } + return e + +updateEntry :: Entry -> Update Blog Entry +updateEntry e = + do b@Blog{..} <- get + put $ b { blogEntries = IxSet.updateIx (entryId e) e blogEntries } + return e + +deleteEntry :: EntryId -> Update Blog EntryId +deleteEntry entry = + do b@Blog{..} <- get + put $ b { blogEntries = IxSet.deleteIx entry blogEntries } + return entry + +getEntry :: EntryId -> Query Blog (Maybe Entry) +getEntry eId = + do Blog{..} <- ask + return $ getOne $ blogEntries @= eId + +latestEntries :: BlogLang -> Query Blog [Entry] +latestEntries lang = + do Blog{..} <- ask + return $ IxSet.toDescList (Proxy :: Proxy EDate) $ blogEntries @= lang + +addSession :: Session -> Update Blog Session +addSession nSession = + do b@Blog{..} <- get + put $ b { blogSessions = IxSet.insert nSession blogSessions} + return nSession + +getSession :: SessionID -> Query Blog (Maybe Session) +getSession sId = + do Blog{..} <- ask + return $ getOne $ blogSessions @= sId + +clearSessions :: Update Blog [Session] +clearSessions = + do b@Blog{..} <- get + put $ b { blogSessions = empty } + return [] + +addUser :: Text -> String -> Update Blog User +addUser un pw = + do b@Blog{..} <- get + let u = User un $ hashString pw + put $ b { blogUsers = IxSet.insert u blogUsers} + return u + +getUser :: Username -> Query Blog (Maybe User) +getUser uN = + do Blog{..} <- ask + return $ getOne $ blogUsers @= uN + +checkUser :: Username -> String -> Query Blog Bool +checkUser uN pw = + do Blog{..} <- ask + let user = getOne $ blogUsers @= uN + case user of + Nothing -> return False + (Just u) -> return $ password u == hashString pw + +-- various functions +hashString :: String -> ByteString +hashString = B64.encode . SHA.hash . B.pack + +$(makeAcidic ''Blog + [ 'insertEntry + , 'updateEntry + , 'deleteEntry + , 'getEntry + , 'latestEntries + , 'addSession + , 'getSession + , 'addUser + , 'getUser + , 'checkUser + , 'clearSessions + ]) + +interactiveUserAdd :: String -> IO () +interactiveUserAdd dbHost = do + acid <- openRemoteState skipAuthenticationPerform dbHost (PortNumber 8070) + putStrLn "Username:" + un <- getLine + putStrLn "Password:" + pw <- getLine + update' acid (AddUser (pack un) pw) + closeAcidState acid + +flushSessions :: IO () +flushSessions = do + tbDir <- getEnv "TAZBLOG" + acid <- openLocalStateFrom (tbDir ++ "/BlogState") initialBlogState + update' acid ClearSessions + closeAcidState acid + +archiveState :: IO () +archiveState = do + tbDir <- getEnv "TAZBLOG" + acid <- openLocalStateFrom (tbDir ++ "/BlogState") initialBlogState + createArchive acid + closeAcidState acid diff --git a/services/tazblog/src/Locales.hs b/services/tazblog/src/Locales.hs new file mode 100644 index 0000000000..c1ddcb38fa --- /dev/null +++ b/services/tazblog/src/Locales.hs @@ -0,0 +1,61 @@ +module Locales where + +import BlogDB (BlogLang (..)) +import Data.Maybe (fromMaybe) +import Data.Text (Text) +import qualified Data.Text as T +import Network.URI + +data BlogError = NotFound | UnknownError + +version = "5.1.2" + +blogTitle :: BlogLang -> Text -> Text +blogTitle DE s = T.concat ["Tazjins blog", s] +blogTitle EN s = T.concat ["Tazjin's blog", s] + +showLangText :: BlogLang -> Text +showLangText EN = "en" +showLangText DE = "de" + +backText :: BlogLang -> Text +backText DE = "Früher" +backText EN = "Earlier" + +nextText :: BlogLang -> Text +nextText DE = "Später" +nextText EN = "Later" + +readMore :: BlogLang -> Text +readMore DE = "[Weiterlesen]" +readMore EN = "[Read more]" + +-- RSS Strings +rssTitle :: BlogLang -> String +rssTitle DE = "Tazjins Blog" +rssTitle EN = "Tazjin's Blog" + +rssDesc :: BlogLang -> String +rssDesc DE = "Feed zu Tazjins Blog" +rssDesc EN = "Feed for Tazjin's Blog" + +rssLink :: BlogLang -> URI +rssLink l = fromMaybe nullURI $ parseURI ("http://tazj.in/" ++ show l) + +-- errors +notFoundTitle :: BlogLang -> Text +notFoundTitle DE = "Nicht gefunden" +notFoundTitle EN = "Not found" + +notFoundText :: BlogLang -> Text +notFoundText DE = "Das gewünschte Objekt wurde leider nicht gefunden." +notFoundText EN = "The requested object could not be found." + +unknownErrorText :: BlogLang -> Text +unknownErrorText DE = "Ein unbekannter Fehler ist aufgetreten." +unknownErrorText EN = "An unknown error has occured." + +-- static information +repoURL :: Text = "https://bitbucket.org/tazjin/tazblog-haskell" +mailTo :: Text = "mailto:tazjin+blog@gmail.com" +twitter :: Text = "https://twitter.com/tazjin" diff --git a/services/tazblog/src/RSS.hs b/services/tazblog/src/RSS.hs new file mode 100644 index 0000000000..34804cbf0a --- /dev/null +++ b/services/tazblog/src/RSS.hs @@ -0,0 +1,41 @@ +module RSS (renderFeed) where + +import qualified Data.Text as T + +import Control.Monad (liftM) +import Data.Maybe (fromMaybe) +import Data.Time (UTCTime, getCurrentTime) +import Network.URI +import Text.RSS + +import BlogDB hiding (Title) +import Locales + +createChannel :: BlogLang -> UTCTime -> [ChannelElem] +createChannel l now = [ Language $ show l + , Copyright "Vincent Ambo" + , WebMaster "tazjin@gmail.com" + , ChannelPubDate now + ] + +createRSS :: BlogLang -> UTCTime -> [Item] -> RSS +createRSS l t = RSS (rssTitle l) (rssLink l) (rssDesc l) (createChannel l t) + +createItem :: Entry -> Item +createItem Entry{..} = [ Title $ T.unpack title + , Link $ makeLink lang entryId + , Description $ T.unpack btext + , PubDate edate] + +makeLink :: BlogLang -> EntryId -> URI +makeLink l i = let url = "http://tazj.in/" ++ show l ++ "/" ++ show i + in fromMaybe nullURI $ parseURI url + +createItems :: [Entry] -> [Item] +createItems = map createItem + +createFeed :: BlogLang -> [Entry] -> IO RSS +createFeed l e = getCurrentTime >>= (\t -> return $ createRSS l t $ createItems e ) + +renderFeed :: BlogLang -> [Entry] -> IO String +renderFeed l e = liftM (showXML . rssToXML) (createFeed l e) diff --git a/services/tazblog/src/Server.hs b/services/tazblog/src/Server.hs new file mode 100644 index 0000000000..c025be009a --- /dev/null +++ b/services/tazblog/src/Server.hs @@ -0,0 +1,189 @@ +-- Server implementation based on Happstack + +module Server where + +import Control.Applicative (optional) +import Control.Monad (msum, mzero, unless) +import Control.Monad.IO.Class (liftIO) +import Data.Acid +import Data.Acid.Advanced +import Data.ByteString.Char8 (unpack) +import Data.Char (toLower) +import Data.Text (Text) +import qualified Data.Text as T +import Data.Time +import Happstack.Server hiding (Session) + +import Blog +import BlogDB hiding (updateEntry) +import Locales +import RSS + +instance FromReqURI BlogLang where + fromReqURI sub = + case map toLower sub of + "de" -> Just DE + "en" -> Just EN + _ -> Nothing + +tmpPolicy :: BodyPolicy +tmpPolicy = defaultBodyPolicy "/tmp" 0 200000 1000 + +runBlog :: AcidState Blog -> Int -> String -> IO () +runBlog acid port respath = + simpleHTTP nullConf {port = port} $ tazBlog acid respath + +tazBlog :: AcidState Blog -> String -> ServerPart Response +tazBlog acid resDir = do + msum [ path $ \(lang :: BlogLang) -> blogHandler acid lang + , dir "admin" $ msum [ + adminHandler acid -- this checks auth + , method GET >> (ok $ toResponse adminLogin) + , method POST >> processLogin acid ] + , dir "static" $ staticHandler resDir + , blogHandler acid EN + , staticHandler resDir + , notFound $ toResponse $ showError NotFound DE + ] + +blogHandler :: AcidState Blog -> BlogLang -> ServerPart Response +blogHandler acid lang = + msum [ path $ \(eId :: Integer) -> showEntry acid lang $ EntryId eId + , nullDir >> showIndex acid lang + , dir "rss" $ nullDir >> showRSS acid lang + , dir "rss.xml" $ nullDir >> showRSS acid lang + , notFound $ toResponse $ showError NotFound lang + ] + +staticHandler :: String -> ServerPart Response +staticHandler resDir = do + setHeaderM "cache-control" "max-age=630720000" + setHeaderM "expires" "Tue, 20 Jan 2037 04:20:42 GMT" + serveDirectory DisableBrowsing [] resDir + +adminHandler :: AcidState Blog -> ServerPart Response +adminHandler acid = do + guardSession acid + msum [ dir "entry" $ method POST >> postEntry acid + , dir "entry" $ path $ \(entry :: Integer) -> msum [ + method GET >> editEntry acid entry + , method POST >> updateEntry acid entry ] + , dir "entrylist" $ path $ \(lang :: BlogLang) -> entryList acid lang + , ok $ toResponse $ adminIndex "tazjin" + ] + +showEntry :: AcidState Blog -> BlogLang -> EntryId -> ServerPart Response +showEntry acid lang eId = do + entry <- query' acid (GetEntry eId) + tryEntry entry lang + +tryEntry :: Maybe Entry -> BlogLang -> ServerPart Response +tryEntry Nothing lang = notFound $ toResponse $ showError NotFound lang +tryEntry (Just entry) _ = ok $ toResponse $ blogTemplate eLang eTitle $ renderEntry entry + where + eTitle = T.append ": " (title entry) + eLang = lang entry + +showIndex :: AcidState Blog -> BlogLang -> ServerPart Response +showIndex acid lang = do + entries <- query' acid (LatestEntries lang) + (page :: Maybe Int) <- optional $ lookRead "page" + ok $ toResponse $ blogTemplate lang "" $ + renderEntries False (eDrop page entries) (Just $ showLinks page lang) + where + eDrop :: Maybe Int -> [a] -> [a] + eDrop (Just i) = drop ((i-1) * 6) + eDrop Nothing = drop 0 + +showRSS :: AcidState Blog -> BlogLang -> ServerPart Response +showRSS acid lang = do + entries <- query' acid (LatestEntries lang) + feed <- liftIO $ renderFeed lang $ take 6 entries + setHeaderM "content-type" "text/xml" + ok $ toResponse feed + +{- ADMIN stuff -} + +postEntry :: AcidState Blog -> ServerPart Response +postEntry acid = do + nullDir + decodeBody tmpPolicy + now <- liftIO getCurrentTime + let eId = timeToId now + lang <- look "lang" + nBtext <- lookText' "btext" + nMtext <- lookText' "mtext" + nEntry <- Entry <$> pure eId + <*> getLang lang + <*> readCookieValue "sUser" + <*> lookText' "title" + <*> pure nBtext + <*> pure nMtext + <*> pure now + update' acid (InsertEntry nEntry) + seeOther ("/" ++ lang ++ "/" ++ show eId) (toResponse()) + where + timeToId :: UTCTime -> EntryId + timeToId t = EntryId . read $ formatTime defaultTimeLocale "%s" t + getLang :: String -> ServerPart BlogLang + getLang "de" = return DE + getLang _ = return EN -- English is default + +entryList :: AcidState Blog -> BlogLang -> ServerPart Response +entryList acid lang = do + entries <- query' acid (LatestEntries lang) + ok $ toResponse $ adminEntryList entries + +editEntry :: AcidState Blog -> Integer -> ServerPart Response +editEntry acid entryId = do + (Just entry) <- query' acid (GetEntry $ EntryId entryId) + ok $ toResponse $ editPage entry + +updateEntry :: AcidState Blog -> Integer -> ServerPart Response +updateEntry acid entryId = do + decodeBody tmpPolicy + (Just entry) <- query' acid (GetEntry $ EntryId entryId) + nTitle <- lookText' "title" + nBtext <- lookText' "btext" + nMtext <- lookText' "mtext" + let newEntry = entry { title = nTitle + , btext = nBtext + , mtext = nMtext} + update' acid (UpdateEntry newEntry) + seeOther (concat $ ["/", show $ lang entry, "/", show entryId]) + (toResponse ()) + +guardSession :: AcidState Blog -> ServerPartT IO () +guardSession acid = do + (sId :: Text) <- readCookieValue "session" + (uName :: Text) <- readCookieValue "sUser" + now <- liftIO getCurrentTime + mS <- query' acid (GetSession $ SessionID sId) + case mS of + Nothing -> mzero + (Just Session{..}) -> unless ((uName == username user) && sessionTimeDiff now sdate) + mzero + where + sessionTimeDiff :: UTCTime -> UTCTime -> Bool + sessionTimeDiff now sdate = diffUTCTime now sdate < 43200 + + +processLogin :: AcidState Blog -> ServerPart Response +processLogin acid = do + decodeBody tmpPolicy + account <- lookText' "account" + password <- look "password" + login <- query' acid (CheckUser (Username account) password) + if login + then createSession account + else unauthorized $ toResponse adminLogin + where + createSession account = do + now <- liftIO getCurrentTime + let sId = hashString $ show now + addCookie (MaxAge 43200) (mkCookie "session" $ unpack sId) + addCookie (MaxAge 43200) (mkCookie "sUser" $ T.unpack account) + (Just user) <- query' acid (GetUser $ Username account) + let nSession = Session (T.pack $ unpack sId) user now + update' acid (AddSession nSession) + seeOther ("/admin?do=login" :: Text) (toResponse()) diff --git a/services/tazblog/stack.yaml b/services/tazblog/stack.yaml new file mode 100644 index 0000000000..8841429aa0 --- /dev/null +++ b/services/tazblog/stack.yaml @@ -0,0 +1,12 @@ +# For more information, see: https://github.com/commercialhaskell/stack/blob/release/doc/yaml_configuration.md + +resolver: lts-9.20 +packages: +- '.' +extra-deps: + - acid-state-0.14.3 + - ixset-1.0.7 + - rss-3000.2.0.6 + - syb-with-class-0.6.1.8 +flags: {} +extra-package-dbs: [] diff --git a/services/tazblog/static/admin.css b/services/tazblog/static/admin.css new file mode 100644 index 0000000000..10980dc9e4 --- /dev/null +++ b/services/tazblog/static/admin.css @@ -0,0 +1,49 @@ +@charset "UTF-8"; +/* CSS Document */ + +body { + padding-top: 20px; + font-family: 'PT Sans', sans-serif; + background-image: linear-gradient(bottom, rgb(245,245,245) 66%, rgb(239,239,239) 83%); + background-image: -o-linear-gradient(bottom, rgb(245,245,245) 66%, rgb(239,239,239) 83%); + background-image: -webkit-linear-gradient(bottom, rgb(245,245,245) 66%, rgb(239,239,239) 83%); + background-image: -webkit-gradient( + linear, + left bottom, + left top, + color-stop(0.66, rgb(245,245,245)), + color-stop(0.83, rgb(239,239,239)) + ); + background-repeat: no-repeat; + background-color: rgb(245,245,245); +} + +.loginBox { + width: 400px; + margin: 0 auto; +} + +.loginBoxTop { + width: 380px; + height: 28px; + color: #FFFFFF; + font-size: 12px; + padding-left: 20px; + padding-top: 11px; + background: url(/static/loginBoxTop.png); +} + +.loginBoxMiddle { + background-color: #F3F3F3; + border-top: 0px hidden; + border:1px solid #D2D2D2; + border-bottom-left-radius: 12px; + border-bottom-right-radius: 12px; + text-align: center; + font-size:12px; + height:auto; + padding-left: 10px; + padding-right: 10px; + min-height:200px; + width:378px; +} diff --git a/services/tazblog/static/apple-touch-icon.png b/services/tazblog/static/apple-touch-icon.png new file mode 100644 index 0000000000..22ba058cdd Binary files /dev/null and b/services/tazblog/static/apple-touch-icon.png differ diff --git a/services/tazblog/static/blog.css b/services/tazblog/static/blog.css new file mode 100644 index 0000000000..e6e4ae3c2b --- /dev/null +++ b/services/tazblog/static/blog.css @@ -0,0 +1,35 @@ +body { + margin: 40px auto; + max-width: 650px; + line-height: 1.6; + font-size: 18px; + color: #383838; + padding: 0 10px +} +h1, h2, h3 { + line-height: 1.2 +} +.footer { + text-align: right; +} +.lod { + text-align: center; +} +.unstyled-link { + color: inherit; + text-decoration: none; +} +.uncoloured-link { + color: inherit; +} +.date { + text-align: right; + font-style: italic; + float: right; +} +.inline { + display: inline; +} +.navigation { + text-align: center; +} diff --git a/services/tazblog/static/favicon.ico b/services/tazblog/static/favicon.ico new file mode 100644 index 0000000000..2958dd3afc Binary files /dev/null and b/services/tazblog/static/favicon.ico differ diff --git a/services/tazblog/static/keybase.txt b/services/tazblog/static/keybase.txt new file mode 100644 index 0000000000..661c33e01e --- /dev/null +++ b/services/tazblog/static/keybase.txt @@ -0,0 +1,69 @@ +================================================================== +https://keybase.io/tazjin +-------------------------------------------------------------------- + +I hereby claim: + + * I am an admin of http://tazj.in + * I am tazjin (https://keybase.io/tazjin) on keybase. + * I have a public key with fingerprint DCF3 4CFA C1AC 44B8 7E26 3331 36EE 3481 4F6D 294A + +To claim this, I am signing this object: + +{ + "body": { + "key": { + "fingerprint": "dcf34cfac1ac44b87e26333136ee34814f6d294a", + "host": "keybase.io", + "key_id": "36EE34814F6D294A", + "uid": "2268b75a56bb9693d3ef077bc1217900", + "username": "tazjin" + }, + "service": { + "hostname": "tazj.in", + "protocol": "http:" + }, + "type": "web_service_binding", + "version": 1 + }, + "ctime": 1397644545, + "expire_in": 157680000, + "prev": "4973fdda56a6cfa726a813411c915458c652be45dd19283f7a4ae4f9c217df14", + "seqno": 4, + "tag": "signature" +} + +with the aforementioned key, yielding the PGP signature: + +-----BEGIN PGP MESSAGE----- +Version: GnuPG v2.0.22 (GNU/Linux) + +owGbwMvMwMWY9pU1Q3bHF2vG0wdeJTEE+8WyVSsl5adUKllVK2Wngqm0zLz01KKC +osy8EiUrpZTkNGOT5LTEZMPEZBOTJAvzVCMzY2NjQ2Oz1FRjEwtDkzSzFCNLk0Ql +HaWM/GKQDqAxSYnFqXqZ+UAxICc+MwUoamzm6gpW72bmAlTvCJQrBUsYGZlZJJmb +JpqaJSVZmlkapxinphmYmyclGxoZmlsaGIAUFqcW5SXmpgJVlyRWZWXmKdXqKAHF +yjKTU0EuBlmMJK8HVKCjVFCUX5KfnJ8DFMwoKSmwAukpqSwAKSpPTYqHao9PysxL +AXoYqKEstag4Mz9PycoQqDK5JBNknqGxpbmZiYmpiamOUmpFQWZRanwmSIWpuZmF +ARCArEktAxppYmlunJaSAvRFohkwtMyNzBItDI1NDA2TLQ2Bui2SzUyNklJNTFNS +DC2NLIzTzBNNElNN0iyTgZ5MSTM0UQJ5qDAvX8nKBOjMxHSgkcWZ6XmJJaVFqUq1 +nUwyLAyMXAxsrEygKGPg4hSARWSZH/8/0573HMdvfH5XxeayYZ2efPb8bw730i1/ +WBU3qru5pKlf3xKmeK5ihtKeT6VXGm3usV2reZWyvO/0joi83oT9P80s88Q6U/vb +vmycHnB7e110v/3OZadu/Sx6+uXk/ZeCR8u+p/+6dNc8XWqX/68t06pnrGKU/BfU +F7X5S/HUy4ysvyZN+v1Jj6NtMvvN1EvPpCpv3kz2tGU1EzpZFfl8Xujq1OopuxZJ +l5kvDlgZ78ezdLZ1+aOlixbsXra4/3fdbZ8XnQX1DatzV18+e2rmMcPKm6qngqIf +Xp8oKTAz+Mg1v6gHP0wLN/Mf3JKjYHnX5U6L/KIvkbsLArtES0r7w1iWZ3OvvSPr +fW6heune1tOb7j3vP+1XeOyV2ekr6pPO3bdrv9X25HbTaqs7z06f0v35fmtQ3uUZ +Z35eLYmaEmb/x/u3vFh6GsvMDocpCTpPlHa0z+xzOGbhzLFO18v21Zd9ISG3Hqtd +F7jaLlWa2W+TsytNnXudVrfCBSbl8zNMfuk2e0Z8i9ix3PmEVa3rTEfhde3qwgtY +dy8rUbzzd5d9ccF63btqO/VMb4oe04x4uCLB5RD3p+8+s77o/T4WP2cFw+0cviX6 +StlJX5f+U3Or3fZY7dUfPcmMJZ/eSs7m+1d5IUbs3jI27olHFzGVvTcsu7w79aOK +SxmXvnEIUwZXgP6BL4LrPDY1rN2V0q1cZj1/efj880rzeu6+OQYA +=xHfH +-----END PGP MESSAGE----- + +And finally, I am proving ownership of this host by posting or +appending to this document. + +View my publicly-auditable identity here: https://keybase.io/tazjin + +================================================================== diff --git a/services/tazblog/static/loginBoxTop.png b/services/tazblog/static/loginBoxTop.png new file mode 100644 index 0000000000..8a0ee3ba8d Binary files /dev/null and b/services/tazblog/static/loginBoxTop.png differ diff --git a/services/tazblog/static/signin.gif b/services/tazblog/static/signin.gif new file mode 100644 index 0000000000..bbe282bae0 Binary files /dev/null and b/services/tazblog/static/signin.gif differ diff --git a/services/tazblog/tazblog.cabal b/services/tazblog/tazblog.cabal new file mode 100644 index 0000000000..3ca9d373b2 --- /dev/null +++ b/services/tazblog/tazblog.cabal @@ -0,0 +1,71 @@ +Name: tazblog +Version: 5.1.3 +Synopsis: Tazjin's Blog +License: MIT +License-file: LICENSE +Author: Vincent Ambo +Maintainer: tazjin@gmail.com +Category: Web blog +Build-type: Simple +cabal-version: >= 1.10 + +library + hs-source-dirs: src + default-language: Haskell2010 + ghc-options: -W + exposed-modules: Blog, BlogDB, Locales, Server, RSS + build-depends: base, + bytestring, + happstack-server, + text, + blaze-html, + blaze-markup, + crypto-api, + cryptohash, + old-locale, + time, + base64-bytestring, + acid-state, + ixset, + safecopy, + mtl, + transformers, + network, + network-uri, + rss, + hamlet, + shakespeare, + markdown + default-extensions: + DeriveDataTypeable + FlexibleContexts + GeneralizedNewtypeDeriving + MultiParamTypeClasses + OverloadedStrings + RecordWildCards + ScopedTypeVariables + TemplateHaskell + TypeFamilies + QuasiQuotes + +executable tazblog + hs-source-dirs: blog + main-is: Main.hs + default-language: Haskell2010 + ghc-options: -threaded -rtsopts -with-rtsopts=-N + build-depends: base, + acid-state, + tazblog, + options, + network + +executable tazblog-db + hs-source-dirs: db + main-is: Main.hs + default-language: Haskell2010 + ghc-options: -threaded -rtsopts -with-rtsopts=-N + build-depends: base, + acid-state, + tazblog, + options, + network diff --git a/services/tazblog/varnish/Dockerfile b/services/tazblog/varnish/Dockerfile new file mode 100644 index 0000000000..83733b527d --- /dev/null +++ b/services/tazblog/varnish/Dockerfile @@ -0,0 +1,16 @@ +FROM centos:7 +MAINTAINER Vincent Ambo <hej@tazj.in> + +EXPOSE 6081 6082 6083 + +RUN yum install -y epel-release && \ + rpm --nosignature -i https://repo.varnish-cache.org/redhat/varnish-4.1.el7.rpm && \ + yum install -y varnish + +ADD default.vcl /etc/varnish/default.vcl + +CMD ulimit -n 131072 && \ + /usr/sbin/varnishd -F -f /etc/varnish/default.vcl \ + -a :6081 -T :6082 -a :6083,PROXY -t 120 \ + -p thread_pool_min=5 -p thread_pool_max=500\ + -p thread_pool_timeout=300 diff --git a/services/tazblog/varnish/default.vcl b/services/tazblog/varnish/default.vcl new file mode 100644 index 0000000000..5a15d21a9c --- /dev/null +++ b/services/tazblog/varnish/default.vcl @@ -0,0 +1,60 @@ +vcl 4.0; +import std; + +# By default, Varnish will run on the same servers as the blog. Inside of +# Kubernetes this will be inside the same pod. + +backend default { + .host = "localhost"; + .port = "8000"; +} + +# Purge requests should be accepted from localhost +acl purge { + "localhost"; +} + +sub vcl_recv { + # Allow HTTP PURGE from ACL above + if (req.method == "PURGE" && client.ip ~ purge) { + return (purge); + } + + # Don't cache admin page + if (req.url ~ "^/admin") { + return (pass); + } + + # Redirect non-www to www and non-HTTPS to HTTPS + if (req.http.host ~ "^tazj.in" || std.port(local.ip) == 6081) { + return (synth (750, "")); + } +} + +sub vcl_backend_response { + # Cache everything for at least 1 minute. + if (beresp.ttl < 1m) { + set beresp.ttl = 1m; + } +} + +sub vcl_deliver { + # Add an HSTS header to everything + set resp.http.Strict-Transport-Security = "max-age=31536000;includeSubdomains;preload"; + + if (obj.hits > 0) { + set resp.http.X-Cache = "HIT"; + } else { + set resp.http.X-Cache = "MISS"; + } +} + +sub vcl_synth { + # Execute TLS or www. redirect + if (resp.status == 750) { + set resp.http.Location = "https://www.tazj.in" + req.url; + set resp.http.Strict-Transport-Security = "max-age=31536000;includeSubdomains;preload"; + set resp.status = 301; + return (deliver); + } +} diff --git a/src/Blog.hs b/src/Blog.hs deleted file mode 100644 index f35e3d9080..0000000000 --- a/src/Blog.hs +++ /dev/null @@ -1,234 +0,0 @@ -module Blog where - -import BlogDB -import Data.Maybe (fromJust) -import Data.Text (Text, append, empty, pack) -import Data.Text.Lazy (fromStrict) -import Data.Time -import Locales -import Text.Blaze.Html (preEscapedToHtml) -import Text.Hamlet -import Text.Markdown - -import qualified Data.Text as T - -replace :: Eq a => a -> a -> [a] -> [a] -replace x y = map (\z -> if z == x then y else z) - -show' :: Show a => a -> Text -show' = pack . show - --- |After this time all entries are Markdown -markdownCutoff :: UTCTime -markdownCutoff = fromJust $ parseTimeM False defaultTimeLocale "%s" "1367149834" - --- blog HTML -blogTemplate :: BlogLang -> Text -> Html -> Html -blogTemplate lang t_append body = [shamlet| -$doctype 5 - <head> - <meta charset="utf-8"> - <meta name="viewport" content="width=device-width, initial-scale=1"> - <meta name="description" content=#{blogTitle lang t_append}> - <link rel="stylesheet" type="text/css" href="/static/blog.css" media="all"> - <link rel="alternate" type="application/rss+xml" title="RSS-Feed" href=#{rssUrl}> - <title>#{blogTitle lang t_append} - <body> - <header> - <h1> - <a href="/" .unstyled-link>#{blogTitle lang empty} - <hr> - ^{body} - ^{showFooter} -|] - where - rssUrl = T.concat ["/", show' lang, "/rss.xml"] - -showFooter :: Html -showFooter = [shamlet| -<footer> - <p .footer>Served without any dynamic languages. - <p .footer> - <a href=#{repoURL} .uncoloured-link>Version #{version} - | - <a href=#{twitter} .uncoloured-link>Twitter - | - <a href=#{mailTo} .uncoloured-link>Mail - <p .lod> - ಠ_ಠ -|] - -isEntryMarkdown :: Entry -> Bool -isEntryMarkdown e = edate e > markdownCutoff - -renderEntryMarkdown :: Text -> Html -renderEntryMarkdown = markdown def {msXssProtect = False} . fromStrict - -renderEntries :: Bool -> [Entry] -> Maybe Html -> Html -renderEntries showAll entries pageLinks = [shamlet| -$forall entry <- toDisplay - <article> - <h2 .inline> - <a href=#{linkElems entry} .unstyled-link> - #{title entry} - <aside .date> - #{pack $ formatTime defaultTimeLocale "%Y-%m-%d" $ edate entry} - $if (isEntryMarkdown entry) - ^{renderEntryMarkdown $ btext entry} - $else - ^{preEscapedToHtml $ btext entry} - $if ((/=) (mtext entry) empty) - <p> - <a .uncoloured-link href=#{linkElems entry}> - #{readMore $ lang entry} - <hr> -$maybe links <- pageLinks - ^{links} -|] - where - toDisplay = if showAll then entries else (take 6 entries) - linkElems Entry{..} = concat $ ["/", show lang, "/", show entryId] - -showLinks :: Maybe Int -> BlogLang -> Html -showLinks (Just i) lang = [shamlet| - $if ((>) i 1) - <div .navigation> - <a href=#{nLink $ succ i} .uncoloured-link>#{backText lang} - | - <a href=#{nLink $ pred i} .uncoloured-link>#{nextText lang} - $elseif ((<=) i 1) - ^{showLinks Nothing lang} -|] - where - nLink page = T.concat ["/", show' lang, "/?page=", show' page] -showLinks Nothing lang = [shamlet| -<div .navigation> - <a href=#{nLink} .uncoloured-link>#{backText lang} -|] - where - nLink = T.concat ["/", show' lang, "/?page=2"] - -renderEntry :: Entry -> Html -renderEntry e@Entry{..} = [shamlet| -<article> - <h2 .inline> - #{title} - <aside .date> - #{pack $ formatTime defaultTimeLocale "%Y-%m-%d" edate} - $if (isEntryMarkdown e) - ^{renderEntryMarkdown btext} - <p>^{renderEntryMarkdown $ mtext} - $else - ^{preEscapedToHtml $ btext} - <p>^{preEscapedToHtml $ mtext} -<hr> -|] - -{- Administration pages -} - -adminTemplate :: Text -> Html -> Html -adminTemplate title body = [shamlet| -$doctype 5 -<head> - <link rel="stylesheet" type="text/css" href="/static/admin.css" media="all"> - <meta http-equiv="content-type" content="text/html;charset=UTF-8"> - <title>#{append "TazBlog Admin: " title} -<body> - ^{body} -|] - -adminLogin :: Html -adminLogin = adminTemplate "Login" $ [shamlet| -<div class="loginBox"> - <div class="loginBoxTop">TazBlog Admin: Login - <div class="loginBoxMiddle"> - <form action="/admin" method="POST"> - <p>Account ID - <p><input type="text" style="font-size:2;" name="account" value="tazjin" readonly="1"> - <p>Passwort - <p><input type="password" style="font-size:2;" name="password"> - <p><input alt="Anmelden" type="image" src="/static/signin.gif"> -|] - -adminIndex :: Text -> Html -adminIndex sUser = adminTemplate "Index" $ [shamlet| -<div style="float:center;"> - <form action="/admin/entry" method="POST"> - <table> - <tr> - <thead><td>Title: - <td><input type="text" name="title"> - <tr> - <thead><td>Language: - <td><select name="lang"> - <option value="en">English - <option value="de">Deutsch - <tr> - <thead><td>Text: - <td> - <textarea name="btext" cols="100" rows="15"> - <tr> - <thead> - <td style="vertical-align:top;">Read more: - <td> - <textarea name="mtext" cols="100" rows="15"> - <input type="hidden" name="author" value=#{sUser}> - <input style="margin-left:20px;" type="submit" value="Submit"> - ^{adminFooter} -|] - -adminFooter :: Html -adminFooter = [shamlet| -<a href="/">Front page -\ -- # - <a href="/admin">New article -\ -- Entry list: # - <a href="/admin/entrylist/en">EN -\ & # -<a href="/admin/entrylist/de">DE -|] - -adminEntryList :: [Entry] -> Html -adminEntryList entries = adminTemplate "EntryList" $ [shamlet| -<div style="float: center;"> - <table> - $forall entry <- entries - <tr> - <td><a href=#{append "/admin/entry/" (show' $ entryId entry)}>#{title entry} - <td>#{formatPostDate $ edate entry} -|] - where - formatPostDate = formatTime defaultTimeLocale "[On %D at %H:%M]" - -editPage :: Entry -> Html -editPage (Entry{..}) = adminTemplate "Index" $ [shamlet| -<div style="float:center;"> - <form action=#{append "/admin/entry/" (show' entryId)} method="POST"> - <table> - <tr> - <td>Title: - <td> - <input type="text" name="title" value=#{title}> - <tr> - <td style="vertical-align:top;">Text: - <td> - <textarea name="btext" cols="100" rows="15">#{btext} - <tr> - <td style="vertical-align:top;">Read more: - <td> - <textarea name="mtext" cols="100" rows="15">#{mtext} - <input type="submit" style="margin-left:20px;" value="Submit"> - <p>^{adminFooter} -|] - -showError :: BlogError -> BlogLang -> Html -showError NotFound l = blogTemplate l (T.append ": " $ notFoundTitle l) $ [shamlet| -<p>:( -<p>#{notFoundText l} -<hr> -|] -showError UnknownError l = blogTemplate l "" $ [shamlet| -<p>:( -<p>#{unknownErrorText l} -<hr> -|] diff --git a/src/BlogDB.hs b/src/BlogDB.hs deleted file mode 100644 index bc9c243933..0000000000 --- a/src/BlogDB.hs +++ /dev/null @@ -1,229 +0,0 @@ -module BlogDB where - -import Control.Monad.Reader (ask) -import Control.Monad.State (get, put) -import Data.Acid -import Data.Acid.Advanced -import Data.Acid.Remote -import Data.ByteString (ByteString) -import Data.Data (Data, Typeable) -import Data.IxSet (Indexable (..), IxSet, Proxy (..), getOne, ixFun, ixSet, (@=)) -import Data.SafeCopy (base, deriveSafeCopy) -import Data.Text (Text, pack) -import Data.Time -import Network (PortID (..)) -import System.Environment (getEnv) - -import qualified Crypto.Hash.SHA512 as SHA (hash) -import qualified Data.ByteString.Base64 as B64 (encode) -import qualified Data.ByteString.Char8 as B -import qualified Data.IxSet as IxSet - -newtype EntryId = EntryId { unEntryId :: Integer } - deriving (Eq, Ord, Data, Enum, Typeable) - -$(deriveSafeCopy 2 'base ''EntryId) - -instance Show EntryId where - show = show . unEntryId - -data BlogLang = EN | DE - deriving (Eq, Ord, Data, Typeable) - -instance Show BlogLang where - show DE = "de" - show EN = "en" - -$(deriveSafeCopy 0 'base ''BlogLang) - -data Entry = Entry { - entryId :: EntryId, - lang :: BlogLang, - author :: Text, - title :: Text, - btext :: Text, - mtext :: Text, - edate :: UTCTime -} deriving (Eq, Ord, Show, Data, Typeable) - -$(deriveSafeCopy 2 'base ''Entry) - --- ixSet requires different datatypes for field indexes, so let's define some -newtype Author = Author Text deriving (Eq, Ord, Data, Typeable) -newtype Title = Title Text deriving (Eq, Ord, Data, Typeable) -newtype BText = BText Text deriving (Eq, Ord, Data, Typeable) -- standard text -newtype MText = MText Text deriving (Eq, Ord, Data, Typeable) -- "read more" text -newtype Tag = Tag Text deriving (Eq, Ord, Data, Typeable) -newtype EDate = EDate UTCTime deriving (Eq, Ord, Data, Typeable) -newtype SDate = SDate UTCTime deriving (Eq, Ord, Data, Typeable) -newtype Username = Username Text deriving (Eq, Ord, Data, Typeable) -newtype SessionID = SessionID Text deriving (Eq, Ord, Data, Typeable) - -$(deriveSafeCopy 2 'base ''Author) -$(deriveSafeCopy 2 'base ''Title) -$(deriveSafeCopy 2 'base ''BText) -$(deriveSafeCopy 2 'base ''MText) -$(deriveSafeCopy 2 'base ''Tag) -$(deriveSafeCopy 2 'base ''EDate) -$(deriveSafeCopy 2 'base ''SDate) -$(deriveSafeCopy 2 'base ''Username) -$(deriveSafeCopy 2 'base ''SessionID) - -instance Indexable Entry where - empty = ixSet [ ixFun $ \e -> [ entryId e] - , ixFun $ (:[]) . lang - , ixFun $ \e -> [ Author $ author e ] - , ixFun $ \e -> [ Title $ title e] - , ixFun $ \e -> [ BText $ btext e] - , ixFun $ \e -> [ MText $ mtext e] - , ixFun $ \e -> [ EDate $ edate e] - ] - -data User = User { - username :: Text, - password :: ByteString -} deriving (Eq, Ord, Data, Typeable) - -$(deriveSafeCopy 0 'base ''User) - -data Session = Session { - sessionID :: Text, - user :: User, - sdate :: UTCTime -} deriving (Eq, Ord, Data, Typeable) - -$(deriveSafeCopy 0 'base ''Session) - -instance Indexable User where - empty = ixSet [ ixFun $ \u -> [Username $ username u] - , ixFun $ (:[]) . password - ] - -instance Indexable Session where - empty = ixSet [ ixFun $ \s -> [SessionID $ sessionID s] - , ixFun $ (:[]) . user - , ixFun $ \s -> [SDate $ sdate s] - ] - -data Blog = Blog { - blogSessions :: IxSet Session, - blogUsers :: IxSet User, - blogEntries :: IxSet Entry -} deriving (Data, Typeable) - -$(deriveSafeCopy 0 'base ''Blog) - -initialBlogState :: Blog -initialBlogState = - Blog { blogSessions = empty - , blogUsers = empty - , blogEntries = empty } - --- acid-state database functions (purity is necessary!) - -insertEntry :: Entry -> Update Blog Entry -insertEntry e = - do b@Blog{..} <- get - put $ b { blogEntries = IxSet.insert e blogEntries } - return e - -updateEntry :: Entry -> Update Blog Entry -updateEntry e = - do b@Blog{..} <- get - put $ b { blogEntries = IxSet.updateIx (entryId e) e blogEntries } - return e - -deleteEntry :: EntryId -> Update Blog EntryId -deleteEntry entry = - do b@Blog{..} <- get - put $ b { blogEntries = IxSet.deleteIx entry blogEntries } - return entry - -getEntry :: EntryId -> Query Blog (Maybe Entry) -getEntry eId = - do Blog{..} <- ask - return $ getOne $ blogEntries @= eId - -latestEntries :: BlogLang -> Query Blog [Entry] -latestEntries lang = - do Blog{..} <- ask - return $ IxSet.toDescList (Proxy :: Proxy EDate) $ blogEntries @= lang - -addSession :: Session -> Update Blog Session -addSession nSession = - do b@Blog{..} <- get - put $ b { blogSessions = IxSet.insert nSession blogSessions} - return nSession - -getSession :: SessionID -> Query Blog (Maybe Session) -getSession sId = - do Blog{..} <- ask - return $ getOne $ blogSessions @= sId - -clearSessions :: Update Blog [Session] -clearSessions = - do b@Blog{..} <- get - put $ b { blogSessions = empty } - return [] - -addUser :: Text -> String -> Update Blog User -addUser un pw = - do b@Blog{..} <- get - let u = User un $ hashString pw - put $ b { blogUsers = IxSet.insert u blogUsers} - return u - -getUser :: Username -> Query Blog (Maybe User) -getUser uN = - do Blog{..} <- ask - return $ getOne $ blogUsers @= uN - -checkUser :: Username -> String -> Query Blog Bool -checkUser uN pw = - do Blog{..} <- ask - let user = getOne $ blogUsers @= uN - case user of - Nothing -> return False - (Just u) -> return $ password u == hashString pw - --- various functions -hashString :: String -> ByteString -hashString = B64.encode . SHA.hash . B.pack - -$(makeAcidic ''Blog - [ 'insertEntry - , 'updateEntry - , 'deleteEntry - , 'getEntry - , 'latestEntries - , 'addSession - , 'getSession - , 'addUser - , 'getUser - , 'checkUser - , 'clearSessions - ]) - -interactiveUserAdd :: String -> IO () -interactiveUserAdd dbHost = do - acid <- openRemoteState skipAuthenticationPerform dbHost (PortNumber 8070) - putStrLn "Username:" - un <- getLine - putStrLn "Password:" - pw <- getLine - update' acid (AddUser (pack un) pw) - closeAcidState acid - -flushSessions :: IO () -flushSessions = do - tbDir <- getEnv "TAZBLOG" - acid <- openLocalStateFrom (tbDir ++ "/BlogState") initialBlogState - update' acid ClearSessions - closeAcidState acid - -archiveState :: IO () -archiveState = do - tbDir <- getEnv "TAZBLOG" - acid <- openLocalStateFrom (tbDir ++ "/BlogState") initialBlogState - createArchive acid - closeAcidState acid diff --git a/src/Locales.hs b/src/Locales.hs deleted file mode 100644 index c1ddcb38fa..0000000000 --- a/src/Locales.hs +++ /dev/null @@ -1,61 +0,0 @@ -module Locales where - -import BlogDB (BlogLang (..)) -import Data.Maybe (fromMaybe) -import Data.Text (Text) -import qualified Data.Text as T -import Network.URI - -data BlogError = NotFound | UnknownError - -version = "5.1.2" - -blogTitle :: BlogLang -> Text -> Text -blogTitle DE s = T.concat ["Tazjins blog", s] -blogTitle EN s = T.concat ["Tazjin's blog", s] - -showLangText :: BlogLang -> Text -showLangText EN = "en" -showLangText DE = "de" - -backText :: BlogLang -> Text -backText DE = "Früher" -backText EN = "Earlier" - -nextText :: BlogLang -> Text -nextText DE = "Später" -nextText EN = "Later" - -readMore :: BlogLang -> Text -readMore DE = "[Weiterlesen]" -readMore EN = "[Read more]" - --- RSS Strings -rssTitle :: BlogLang -> String -rssTitle DE = "Tazjins Blog" -rssTitle EN = "Tazjin's Blog" - -rssDesc :: BlogLang -> String -rssDesc DE = "Feed zu Tazjins Blog" -rssDesc EN = "Feed for Tazjin's Blog" - -rssLink :: BlogLang -> URI -rssLink l = fromMaybe nullURI $ parseURI ("http://tazj.in/" ++ show l) - --- errors -notFoundTitle :: BlogLang -> Text -notFoundTitle DE = "Nicht gefunden" -notFoundTitle EN = "Not found" - -notFoundText :: BlogLang -> Text -notFoundText DE = "Das gewünschte Objekt wurde leider nicht gefunden." -notFoundText EN = "The requested object could not be found." - -unknownErrorText :: BlogLang -> Text -unknownErrorText DE = "Ein unbekannter Fehler ist aufgetreten." -unknownErrorText EN = "An unknown error has occured." - --- static information -repoURL :: Text = "https://bitbucket.org/tazjin/tazblog-haskell" -mailTo :: Text = "mailto:tazjin+blog@gmail.com" -twitter :: Text = "https://twitter.com/tazjin" diff --git a/src/RSS.hs b/src/RSS.hs deleted file mode 100644 index 34804cbf0a..0000000000 --- a/src/RSS.hs +++ /dev/null @@ -1,41 +0,0 @@ -module RSS (renderFeed) where - -import qualified Data.Text as T - -import Control.Monad (liftM) -import Data.Maybe (fromMaybe) -import Data.Time (UTCTime, getCurrentTime) -import Network.URI -import Text.RSS - -import BlogDB hiding (Title) -import Locales - -createChannel :: BlogLang -> UTCTime -> [ChannelElem] -createChannel l now = [ Language $ show l - , Copyright "Vincent Ambo" - , WebMaster "tazjin@gmail.com" - , ChannelPubDate now - ] - -createRSS :: BlogLang -> UTCTime -> [Item] -> RSS -createRSS l t = RSS (rssTitle l) (rssLink l) (rssDesc l) (createChannel l t) - -createItem :: Entry -> Item -createItem Entry{..} = [ Title $ T.unpack title - , Link $ makeLink lang entryId - , Description $ T.unpack btext - , PubDate edate] - -makeLink :: BlogLang -> EntryId -> URI -makeLink l i = let url = "http://tazj.in/" ++ show l ++ "/" ++ show i - in fromMaybe nullURI $ parseURI url - -createItems :: [Entry] -> [Item] -createItems = map createItem - -createFeed :: BlogLang -> [Entry] -> IO RSS -createFeed l e = getCurrentTime >>= (\t -> return $ createRSS l t $ createItems e ) - -renderFeed :: BlogLang -> [Entry] -> IO String -renderFeed l e = liftM (showXML . rssToXML) (createFeed l e) diff --git a/src/Server.hs b/src/Server.hs deleted file mode 100644 index c025be009a..0000000000 --- a/src/Server.hs +++ /dev/null @@ -1,189 +0,0 @@ --- Server implementation based on Happstack - -module Server where - -import Control.Applicative (optional) -import Control.Monad (msum, mzero, unless) -import Control.Monad.IO.Class (liftIO) -import Data.Acid -import Data.Acid.Advanced -import Data.ByteString.Char8 (unpack) -import Data.Char (toLower) -import Data.Text (Text) -import qualified Data.Text as T -import Data.Time -import Happstack.Server hiding (Session) - -import Blog -import BlogDB hiding (updateEntry) -import Locales -import RSS - -instance FromReqURI BlogLang where - fromReqURI sub = - case map toLower sub of - "de" -> Just DE - "en" -> Just EN - _ -> Nothing - -tmpPolicy :: BodyPolicy -tmpPolicy = defaultBodyPolicy "/tmp" 0 200000 1000 - -runBlog :: AcidState Blog -> Int -> String -> IO () -runBlog acid port respath = - simpleHTTP nullConf {port = port} $ tazBlog acid respath - -tazBlog :: AcidState Blog -> String -> ServerPart Response -tazBlog acid resDir = do - msum [ path $ \(lang :: BlogLang) -> blogHandler acid lang - , dir "admin" $ msum [ - adminHandler acid -- this checks auth - , method GET >> (ok $ toResponse adminLogin) - , method POST >> processLogin acid ] - , dir "static" $ staticHandler resDir - , blogHandler acid EN - , staticHandler resDir - , notFound $ toResponse $ showError NotFound DE - ] - -blogHandler :: AcidState Blog -> BlogLang -> ServerPart Response -blogHandler acid lang = - msum [ path $ \(eId :: Integer) -> showEntry acid lang $ EntryId eId - , nullDir >> showIndex acid lang - , dir "rss" $ nullDir >> showRSS acid lang - , dir "rss.xml" $ nullDir >> showRSS acid lang - , notFound $ toResponse $ showError NotFound lang - ] - -staticHandler :: String -> ServerPart Response -staticHandler resDir = do - setHeaderM "cache-control" "max-age=630720000" - setHeaderM "expires" "Tue, 20 Jan 2037 04:20:42 GMT" - serveDirectory DisableBrowsing [] resDir - -adminHandler :: AcidState Blog -> ServerPart Response -adminHandler acid = do - guardSession acid - msum [ dir "entry" $ method POST >> postEntry acid - , dir "entry" $ path $ \(entry :: Integer) -> msum [ - method GET >> editEntry acid entry - , method POST >> updateEntry acid entry ] - , dir "entrylist" $ path $ \(lang :: BlogLang) -> entryList acid lang - , ok $ toResponse $ adminIndex "tazjin" - ] - -showEntry :: AcidState Blog -> BlogLang -> EntryId -> ServerPart Response -showEntry acid lang eId = do - entry <- query' acid (GetEntry eId) - tryEntry entry lang - -tryEntry :: Maybe Entry -> BlogLang -> ServerPart Response -tryEntry Nothing lang = notFound $ toResponse $ showError NotFound lang -tryEntry (Just entry) _ = ok $ toResponse $ blogTemplate eLang eTitle $ renderEntry entry - where - eTitle = T.append ": " (title entry) - eLang = lang entry - -showIndex :: AcidState Blog -> BlogLang -> ServerPart Response -showIndex acid lang = do - entries <- query' acid (LatestEntries lang) - (page :: Maybe Int) <- optional $ lookRead "page" - ok $ toResponse $ blogTemplate lang "" $ - renderEntries False (eDrop page entries) (Just $ showLinks page lang) - where - eDrop :: Maybe Int -> [a] -> [a] - eDrop (Just i) = drop ((i-1) * 6) - eDrop Nothing = drop 0 - -showRSS :: AcidState Blog -> BlogLang -> ServerPart Response -showRSS acid lang = do - entries <- query' acid (LatestEntries lang) - feed <- liftIO $ renderFeed lang $ take 6 entries - setHeaderM "content-type" "text/xml" - ok $ toResponse feed - -{- ADMIN stuff -} - -postEntry :: AcidState Blog -> ServerPart Response -postEntry acid = do - nullDir - decodeBody tmpPolicy - now <- liftIO getCurrentTime - let eId = timeToId now - lang <- look "lang" - nBtext <- lookText' "btext" - nMtext <- lookText' "mtext" - nEntry <- Entry <$> pure eId - <*> getLang lang - <*> readCookieValue "sUser" - <*> lookText' "title" - <*> pure nBtext - <*> pure nMtext - <*> pure now - update' acid (InsertEntry nEntry) - seeOther ("/" ++ lang ++ "/" ++ show eId) (toResponse()) - where - timeToId :: UTCTime -> EntryId - timeToId t = EntryId . read $ formatTime defaultTimeLocale "%s" t - getLang :: String -> ServerPart BlogLang - getLang "de" = return DE - getLang _ = return EN -- English is default - -entryList :: AcidState Blog -> BlogLang -> ServerPart Response -entryList acid lang = do - entries <- query' acid (LatestEntries lang) - ok $ toResponse $ adminEntryList entries - -editEntry :: AcidState Blog -> Integer -> ServerPart Response -editEntry acid entryId = do - (Just entry) <- query' acid (GetEntry $ EntryId entryId) - ok $ toResponse $ editPage entry - -updateEntry :: AcidState Blog -> Integer -> ServerPart Response -updateEntry acid entryId = do - decodeBody tmpPolicy - (Just entry) <- query' acid (GetEntry $ EntryId entryId) - nTitle <- lookText' "title" - nBtext <- lookText' "btext" - nMtext <- lookText' "mtext" - let newEntry = entry { title = nTitle - , btext = nBtext - , mtext = nMtext} - update' acid (UpdateEntry newEntry) - seeOther (concat $ ["/", show $ lang entry, "/", show entryId]) - (toResponse ()) - -guardSession :: AcidState Blog -> ServerPartT IO () -guardSession acid = do - (sId :: Text) <- readCookieValue "session" - (uName :: Text) <- readCookieValue "sUser" - now <- liftIO getCurrentTime - mS <- query' acid (GetSession $ SessionID sId) - case mS of - Nothing -> mzero - (Just Session{..}) -> unless ((uName == username user) && sessionTimeDiff now sdate) - mzero - where - sessionTimeDiff :: UTCTime -> UTCTime -> Bool - sessionTimeDiff now sdate = diffUTCTime now sdate < 43200 - - -processLogin :: AcidState Blog -> ServerPart Response -processLogin acid = do - decodeBody tmpPolicy - account <- lookText' "account" - password <- look "password" - login <- query' acid (CheckUser (Username account) password) - if login - then createSession account - else unauthorized $ toResponse adminLogin - where - createSession account = do - now <- liftIO getCurrentTime - let sId = hashString $ show now - addCookie (MaxAge 43200) (mkCookie "session" $ unpack sId) - addCookie (MaxAge 43200) (mkCookie "sUser" $ T.unpack account) - (Just user) <- query' acid (GetUser $ Username account) - let nSession = Session (T.pack $ unpack sId) user now - update' acid (AddSession nSession) - seeOther ("/admin?do=login" :: Text) (toResponse()) diff --git a/stack.yaml b/stack.yaml deleted file mode 100644 index 8841429aa0..0000000000 --- a/stack.yaml +++ /dev/null @@ -1,12 +0,0 @@ -# For more information, see: https://github.com/commercialhaskell/stack/blob/release/doc/yaml_configuration.md - -resolver: lts-9.20 -packages: -- '.' -extra-deps: - - acid-state-0.14.3 - - ixset-1.0.7 - - rss-3000.2.0.6 - - syb-with-class-0.6.1.8 -flags: {} -extra-package-dbs: [] diff --git a/static/admin.css b/static/admin.css deleted file mode 100644 index 10980dc9e4..0000000000 --- a/static/admin.css +++ /dev/null @@ -1,49 +0,0 @@ -@charset "UTF-8"; -/* CSS Document */ - -body { - padding-top: 20px; - font-family: 'PT Sans', sans-serif; - background-image: linear-gradient(bottom, rgb(245,245,245) 66%, rgb(239,239,239) 83%); - background-image: -o-linear-gradient(bottom, rgb(245,245,245) 66%, rgb(239,239,239) 83%); - background-image: -webkit-linear-gradient(bottom, rgb(245,245,245) 66%, rgb(239,239,239) 83%); - background-image: -webkit-gradient( - linear, - left bottom, - left top, - color-stop(0.66, rgb(245,245,245)), - color-stop(0.83, rgb(239,239,239)) - ); - background-repeat: no-repeat; - background-color: rgb(245,245,245); -} - -.loginBox { - width: 400px; - margin: 0 auto; -} - -.loginBoxTop { - width: 380px; - height: 28px; - color: #FFFFFF; - font-size: 12px; - padding-left: 20px; - padding-top: 11px; - background: url(/static/loginBoxTop.png); -} - -.loginBoxMiddle { - background-color: #F3F3F3; - border-top: 0px hidden; - border:1px solid #D2D2D2; - border-bottom-left-radius: 12px; - border-bottom-right-radius: 12px; - text-align: center; - font-size:12px; - height:auto; - padding-left: 10px; - padding-right: 10px; - min-height:200px; - width:378px; -} diff --git a/static/apple-touch-icon.png b/static/apple-touch-icon.png deleted file mode 100644 index 22ba058cdd..0000000000 Binary files a/static/apple-touch-icon.png and /dev/null differ diff --git a/static/blog.css b/static/blog.css deleted file mode 100644 index e6e4ae3c2b..0000000000 --- a/static/blog.css +++ /dev/null @@ -1,35 +0,0 @@ -body { - margin: 40px auto; - max-width: 650px; - line-height: 1.6; - font-size: 18px; - color: #383838; - padding: 0 10px -} -h1, h2, h3 { - line-height: 1.2 -} -.footer { - text-align: right; -} -.lod { - text-align: center; -} -.unstyled-link { - color: inherit; - text-decoration: none; -} -.uncoloured-link { - color: inherit; -} -.date { - text-align: right; - font-style: italic; - float: right; -} -.inline { - display: inline; -} -.navigation { - text-align: center; -} diff --git a/static/favicon.ico b/static/favicon.ico deleted file mode 100644 index 2958dd3afc..0000000000 Binary files a/static/favicon.ico and /dev/null differ diff --git a/static/keybase.txt b/static/keybase.txt deleted file mode 100644 index 661c33e01e..0000000000 --- a/static/keybase.txt +++ /dev/null @@ -1,69 +0,0 @@ -================================================================== -https://keybase.io/tazjin --------------------------------------------------------------------- - -I hereby claim: - - * I am an admin of http://tazj.in - * I am tazjin (https://keybase.io/tazjin) on keybase. - * I have a public key with fingerprint DCF3 4CFA C1AC 44B8 7E26 3331 36EE 3481 4F6D 294A - -To claim this, I am signing this object: - -{ - "body": { - "key": { - "fingerprint": "dcf34cfac1ac44b87e26333136ee34814f6d294a", - "host": "keybase.io", - "key_id": "36EE34814F6D294A", - "uid": "2268b75a56bb9693d3ef077bc1217900", - "username": "tazjin" - }, - "service": { - "hostname": "tazj.in", - "protocol": "http:" - }, - "type": "web_service_binding", - "version": 1 - }, - "ctime": 1397644545, - "expire_in": 157680000, - "prev": "4973fdda56a6cfa726a813411c915458c652be45dd19283f7a4ae4f9c217df14", - "seqno": 4, - "tag": "signature" -} - -with the aforementioned key, yielding the PGP signature: - ------BEGIN PGP MESSAGE----- -Version: GnuPG v2.0.22 (GNU/Linux) - -owGbwMvMwMWY9pU1Q3bHF2vG0wdeJTEE+8WyVSsl5adUKllVK2Wngqm0zLz01KKC -osy8EiUrpZTkNGOT5LTEZMPEZBOTJAvzVCMzY2NjQ2Oz1FRjEwtDkzSzFCNLk0Ql -HaWM/GKQDqAxSYnFqXqZ+UAxICc+MwUoamzm6gpW72bmAlTvCJQrBUsYGZlZJJmb -JpqaJSVZmlkapxinphmYmyclGxoZmlsaGIAUFqcW5SXmpgJVlyRWZWXmKdXqKAHF -yjKTU0EuBlmMJK8HVKCjVFCUX5KfnJ8DFMwoKSmwAukpqSwAKSpPTYqHao9PysxL -AXoYqKEstag4Mz9PycoQqDK5JBNknqGxpbmZiYmpiamOUmpFQWZRanwmSIWpuZmF -ARCArEktAxppYmlunJaSAvRFohkwtMyNzBItDI1NDA2TLQ2Bui2SzUyNklJNTFNS -DC2NLIzTzBNNElNN0iyTgZ5MSTM0UQJ5qDAvX8nKBOjMxHSgkcWZ6XmJJaVFqUq1 -nUwyLAyMXAxsrEygKGPg4hSARWSZH/8/0573HMdvfH5XxeayYZ2efPb8bw730i1/ -WBU3qru5pKlf3xKmeK5ihtKeT6VXGm3usV2reZWyvO/0joi83oT9P80s88Q6U/vb -vmycHnB7e110v/3OZadu/Sx6+uXk/ZeCR8u+p/+6dNc8XWqX/68t06pnrGKU/BfU -F7X5S/HUy4ysvyZN+v1Jj6NtMvvN1EvPpCpv3kz2tGU1EzpZFfl8Xujq1OopuxZJ -l5kvDlgZ78ezdLZ1+aOlixbsXra4/3fdbZ8XnQX1DatzV18+e2rmMcPKm6qngqIf -Xp8oKTAz+Mg1v6gHP0wLN/Mf3JKjYHnX5U6L/KIvkbsLArtES0r7w1iWZ3OvvSPr -fW6heune1tOb7j3vP+1XeOyV2ekr6pPO3bdrv9X25HbTaqs7z06f0v35fmtQ3uUZ -Z35eLYmaEmb/x/u3vFh6GsvMDocpCTpPlHa0z+xzOGbhzLFO18v21Zd9ISG3Hqtd -F7jaLlWa2W+TsytNnXudVrfCBSbl8zNMfuk2e0Z8i9ix3PmEVa3rTEfhde3qwgtY -dy8rUbzzd5d9ccF63btqO/VMb4oe04x4uCLB5RD3p+8+s77o/T4WP2cFw+0cviX6 -StlJX5f+U3Or3fZY7dUfPcmMJZ/eSs7m+1d5IUbs3jI27olHFzGVvTcsu7w79aOK -SxmXvnEIUwZXgP6BL4LrPDY1rN2V0q1cZj1/efj880rzeu6+OQYA -=xHfH ------END PGP MESSAGE----- - -And finally, I am proving ownership of this host by posting or -appending to this document. - -View my publicly-auditable identity here: https://keybase.io/tazjin - -================================================================== diff --git a/static/loginBoxTop.png b/static/loginBoxTop.png deleted file mode 100644 index 8a0ee3ba8d..0000000000 Binary files a/static/loginBoxTop.png and /dev/null differ diff --git a/static/signin.gif b/static/signin.gif deleted file mode 100644 index bbe282bae0..0000000000 Binary files a/static/signin.gif and /dev/null differ diff --git a/tazblog.cabal b/tazblog.cabal deleted file mode 100644 index 3ca9d373b2..0000000000 --- a/tazblog.cabal +++ /dev/null @@ -1,71 +0,0 @@ -Name: tazblog -Version: 5.1.3 -Synopsis: Tazjin's Blog -License: MIT -License-file: LICENSE -Author: Vincent Ambo -Maintainer: tazjin@gmail.com -Category: Web blog -Build-type: Simple -cabal-version: >= 1.10 - -library - hs-source-dirs: src - default-language: Haskell2010 - ghc-options: -W - exposed-modules: Blog, BlogDB, Locales, Server, RSS - build-depends: base, - bytestring, - happstack-server, - text, - blaze-html, - blaze-markup, - crypto-api, - cryptohash, - old-locale, - time, - base64-bytestring, - acid-state, - ixset, - safecopy, - mtl, - transformers, - network, - network-uri, - rss, - hamlet, - shakespeare, - markdown - default-extensions: - DeriveDataTypeable - FlexibleContexts - GeneralizedNewtypeDeriving - MultiParamTypeClasses - OverloadedStrings - RecordWildCards - ScopedTypeVariables - TemplateHaskell - TypeFamilies - QuasiQuotes - -executable tazblog - hs-source-dirs: blog - main-is: Main.hs - default-language: Haskell2010 - ghc-options: -threaded -rtsopts -with-rtsopts=-N - build-depends: base, - acid-state, - tazblog, - options, - network - -executable tazblog-db - hs-source-dirs: db - main-is: Main.hs - default-language: Haskell2010 - ghc-options: -threaded -rtsopts -with-rtsopts=-N - build-depends: base, - acid-state, - tazblog, - options, - network diff --git a/varnish/Dockerfile b/varnish/Dockerfile deleted file mode 100644 index 83733b527d..0000000000 --- a/varnish/Dockerfile +++ /dev/null @@ -1,16 +0,0 @@ -FROM centos:7 -MAINTAINER Vincent Ambo <hej@tazj.in> - -EXPOSE 6081 6082 6083 - -RUN yum install -y epel-release && \ - rpm --nosignature -i https://repo.varnish-cache.org/redhat/varnish-4.1.el7.rpm && \ - yum install -y varnish - -ADD default.vcl /etc/varnish/default.vcl - -CMD ulimit -n 131072 && \ - /usr/sbin/varnishd -F -f /etc/varnish/default.vcl \ - -a :6081 -T :6082 -a :6083,PROXY -t 120 \ - -p thread_pool_min=5 -p thread_pool_max=500\ - -p thread_pool_timeout=300 diff --git a/varnish/default.vcl b/varnish/default.vcl deleted file mode 100644 index 5a15d21a9c..0000000000 --- a/varnish/default.vcl +++ /dev/null @@ -1,60 +0,0 @@ -vcl 4.0; -import std; - -# By default, Varnish will run on the same servers as the blog. Inside of -# Kubernetes this will be inside the same pod. - -backend default { - .host = "localhost"; - .port = "8000"; -} - -# Purge requests should be accepted from localhost -acl purge { - "localhost"; -} - -sub vcl_recv { - # Allow HTTP PURGE from ACL above - if (req.method == "PURGE" && client.ip ~ purge) { - return (purge); - } - - # Don't cache admin page - if (req.url ~ "^/admin") { - return (pass); - } - - # Redirect non-www to www and non-HTTPS to HTTPS - if (req.http.host ~ "^tazj.in" || std.port(local.ip) == 6081) { - return (synth (750, "")); - } -} - -sub vcl_backend_response { - # Cache everything for at least 1 minute. - if (beresp.ttl < 1m) { - set beresp.ttl = 1m; - } -} - -sub vcl_deliver { - # Add an HSTS header to everything - set resp.http.Strict-Transport-Security = "max-age=31536000;includeSubdomains;preload"; - - if (obj.hits > 0) { - set resp.http.X-Cache = "HIT"; - } else { - set resp.http.X-Cache = "MISS"; - } -} - -sub vcl_synth { - # Execute TLS or www. redirect - if (resp.status == 750) { - set resp.http.Location = "https://www.tazj.in" + req.url; - set resp.http.Strict-Transport-Security = "max-age=31536000;includeSubdomains;preload"; - set resp.status = 301; - return (deliver); - } -} -- cgit 1.4.1 From 85dbb4cc238c1e4b1b49806c0728c891a9a0acf7 Mon Sep 17 00:00:00 2001 From: Vincent Ambo <tazjin@google.com> Date: Fri, 28 Jun 2019 22:56:48 +0100 Subject: chore: Keep project root under MIT license To comply with Google's open-source patching rules :) --- LICENSE | 21 +++++++++++++++++++++ services/tazblog/LICENSE | 21 --------------------- 2 files changed, 21 insertions(+), 21 deletions(-) create mode 100644 LICENSE delete mode 100644 services/tazblog/LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000..904a76ed04 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2019 Vincent Ambo + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/services/tazblog/LICENSE b/services/tazblog/LICENSE deleted file mode 100644 index f5c81f7e3a..0000000000 --- a/services/tazblog/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2014 Vincent Ambo - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -- cgit 1.4.1 From 47f2145b5b39b63500fe2d9d0a64edc44edaa793 Mon Sep 17 00:00:00 2001 From: Vincent Ambo <tazjin@google.com> Date: Sat, 29 Jun 2019 14:01:28 +0100 Subject: chore(tazblog): Remove files from ye olde times --- services/tazblog/Dockerfile | 19 --------- services/tazblog/Makefile | 17 -------- services/tazblog/TODO | 1 - services/tazblog/backup.sh | 2 - services/tazblog/k8s/tazblog-db-rc.yaml | 26 ------------ services/tazblog/k8s/tazblog-db-service.yaml | 12 ------ services/tazblog/k8s/tazblog-rc.yaml | 45 --------------------- services/tazblog/k8s/tazblog-svc.yaml | 17 -------- services/tazblog/stack.yaml | 12 ------ services/tazblog/varnish/Dockerfile | 16 -------- services/tazblog/varnish/default.vcl | 60 ---------------------------- 11 files changed, 227 deletions(-) delete mode 100644 services/tazblog/Dockerfile delete mode 100644 services/tazblog/Makefile delete mode 100644 services/tazblog/TODO delete mode 100644 services/tazblog/backup.sh delete mode 100644 services/tazblog/k8s/tazblog-db-rc.yaml delete mode 100644 services/tazblog/k8s/tazblog-db-service.yaml delete mode 100644 services/tazblog/k8s/tazblog-rc.yaml delete mode 100644 services/tazblog/k8s/tazblog-svc.yaml delete mode 100644 services/tazblog/stack.yaml delete mode 100644 services/tazblog/varnish/Dockerfile delete mode 100644 services/tazblog/varnish/default.vcl diff --git a/services/tazblog/Dockerfile b/services/tazblog/Dockerfile deleted file mode 100644 index 7d8b605826..0000000000 --- a/services/tazblog/Dockerfile +++ /dev/null @@ -1,19 +0,0 @@ -FROM fpco/stack-build -MAINTAINER Vincent Ambo <dev@tazj.in> - -# Cache dependencies -ADD stack.yaml tazblog.cabal /opt/tazblog/ -WORKDIR /opt/tazblog -RUN stack build --only-dependencies - -# Base setup -VOLUME /var/tazblog -EXPOSE 8000 8070 -ENV PATH /root/.local/bin:$PATH - -# Build blog -ADD . /opt/tazblog -RUN stack install && cp /root/.local/bin/tazblog* /usr/bin/ - -# Done! -CMD tazblog diff --git a/services/tazblog/Makefile b/services/tazblog/Makefile deleted file mode 100644 index 00d77dd36c..0000000000 --- a/services/tazblog/Makefile +++ /dev/null @@ -1,17 +0,0 @@ -VERSION=$(shell bash -c "grep -P -o -e '\d\.\d$$' TazBlog.cabal | head -n1") -ARCH_PKG=arch/tazblog-$(VERSION)-1-x86_64.pkg.tar.xz -export ARCH_PKG - -all: archpkg docker - -archpkg: $(ARCH_PKG) - -$(ARCH_PKG): - cd arch && makepkg - -docker: archpkg - cat Dockerfile.raw | envsubst > Dockerfile; \ - docker build -t tazjin/tazblog . - -clean: - rm -rf dist arch/*.pkg.tar.xz arch/pkg arch/src arch/*. Dockerfile diff --git a/services/tazblog/TODO b/services/tazblog/TODO deleted file mode 100644 index fdb963dd79..0000000000 --- a/services/tazblog/TODO +++ /dev/null @@ -1 +0,0 @@ -* Bootstrap: http://twitter.github.com/bootstrap/index.html diff --git a/services/tazblog/backup.sh b/services/tazblog/backup.sh deleted file mode 100644 index bbc3167324..0000000000 --- a/services/tazblog/backup.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -tar cf backup.tar BlogState/ diff --git a/services/tazblog/k8s/tazblog-db-rc.yaml b/services/tazblog/k8s/tazblog-db-rc.yaml deleted file mode 100644 index 26d730c4df..0000000000 --- a/services/tazblog/k8s/tazblog-db-rc.yaml +++ /dev/null @@ -1,26 +0,0 @@ -apiVersion: v1 -kind: ReplicationController -metadata: - name: tazblog-db -spec: - selector: - app: tazblog-db - template: - metadata: - labels: - app: tazblog-db - spec: - containers: - - image: tazjin/tazblog-haskell:master - name: tazblog-db - command: ["tazblog-db"] - ports: - - containerPort: 8070 - volumeMounts: - - name: tazblog-state - mountPath: /var/tazblog - volumes: - - name: tazblog-state - gcePersistentDisk: - pdName: tazblog-state - fsType: ext4 diff --git a/services/tazblog/k8s/tazblog-db-service.yaml b/services/tazblog/k8s/tazblog-db-service.yaml deleted file mode 100644 index 6d5d429469..0000000000 --- a/services/tazblog/k8s/tazblog-db-service.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: tazblog-db - labels: - app: tazblog-db -spec: - selector: - app: tazblog-db - ports: - - port: 8070 - name: tazblog-db diff --git a/services/tazblog/k8s/tazblog-rc.yaml b/services/tazblog/k8s/tazblog-rc.yaml deleted file mode 100644 index b29a4d5d75..0000000000 --- a/services/tazblog/k8s/tazblog-rc.yaml +++ /dev/null @@ -1,45 +0,0 @@ -apiVersion: v1 -kind: ReplicationController -metadata: - name: tazblog-5.1.3 -spec: - replicas: 2 - selector: - app: tazblog - version: v5.1.3 - template: - metadata: - labels: - app: tazblog - version: v5.1.3 - spec: - containers: - - image: tazjin/tazblog-haskell:master - imagePullPolicy: Always - name: tazblog - command: ["tazblog", "--dbHost", "tazblog-db.default.svc.cluster.local"] - ports: - - containerPort: 8000 - - image: tazjin/varnish - imagePullPolicy: Always - name: tazblog-varnish - ports: - - containerPort: 6081 - - containerPort: 6082 - - image: tazjin/hitch:master - imagePullPolicy: Always - name: tazblog-hitch - command: ["hitch", "--backend=[127.0.0.1]:6083", "--write-proxy", "--user=hitch", "/etc/hitch/ssl/tazblog-tls"] - ports: - - containerPort: 8443 - volumeMounts: - - name: tazblog-tls - readOnly: true - mountPath: /etc/hitch/ssl - resources: - requests: - memory: "1024Mi" - volumes: - - name: tazblog-tls - secret: - secretName: tazblog-tls diff --git a/services/tazblog/k8s/tazblog-svc.yaml b/services/tazblog/k8s/tazblog-svc.yaml deleted file mode 100644 index 6a2d9a4223..0000000000 --- a/services/tazblog/k8s/tazblog-svc.yaml +++ /dev/null @@ -1,17 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: tazblog - labels: - app: tazblog -spec: - type: LoadBalancer - selector: - app: tazblog - ports: - - port: 80 - targetPort: 6081 - name: tazblog-http - - port: 443 - targetPort: 8443 - name: tazblog-https diff --git a/services/tazblog/stack.yaml b/services/tazblog/stack.yaml deleted file mode 100644 index 8841429aa0..0000000000 --- a/services/tazblog/stack.yaml +++ /dev/null @@ -1,12 +0,0 @@ -# For more information, see: https://github.com/commercialhaskell/stack/blob/release/doc/yaml_configuration.md - -resolver: lts-9.20 -packages: -- '.' -extra-deps: - - acid-state-0.14.3 - - ixset-1.0.7 - - rss-3000.2.0.6 - - syb-with-class-0.6.1.8 -flags: {} -extra-package-dbs: [] diff --git a/services/tazblog/varnish/Dockerfile b/services/tazblog/varnish/Dockerfile deleted file mode 100644 index 83733b527d..0000000000 --- a/services/tazblog/varnish/Dockerfile +++ /dev/null @@ -1,16 +0,0 @@ -FROM centos:7 -MAINTAINER Vincent Ambo <hej@tazj.in> - -EXPOSE 6081 6082 6083 - -RUN yum install -y epel-release && \ - rpm --nosignature -i https://repo.varnish-cache.org/redhat/varnish-4.1.el7.rpm && \ - yum install -y varnish - -ADD default.vcl /etc/varnish/default.vcl - -CMD ulimit -n 131072 && \ - /usr/sbin/varnishd -F -f /etc/varnish/default.vcl \ - -a :6081 -T :6082 -a :6083,PROXY -t 120 \ - -p thread_pool_min=5 -p thread_pool_max=500\ - -p thread_pool_timeout=300 diff --git a/services/tazblog/varnish/default.vcl b/services/tazblog/varnish/default.vcl deleted file mode 100644 index 5a15d21a9c..0000000000 --- a/services/tazblog/varnish/default.vcl +++ /dev/null @@ -1,60 +0,0 @@ -vcl 4.0; -import std; - -# By default, Varnish will run on the same servers as the blog. Inside of -# Kubernetes this will be inside the same pod. - -backend default { - .host = "localhost"; - .port = "8000"; -} - -# Purge requests should be accepted from localhost -acl purge { - "localhost"; -} - -sub vcl_recv { - # Allow HTTP PURGE from ACL above - if (req.method == "PURGE" && client.ip ~ purge) { - return (purge); - } - - # Don't cache admin page - if (req.url ~ "^/admin") { - return (pass); - } - - # Redirect non-www to www and non-HTTPS to HTTPS - if (req.http.host ~ "^tazj.in" || std.port(local.ip) == 6081) { - return (synth (750, "")); - } -} - -sub vcl_backend_response { - # Cache everything for at least 1 minute. - if (beresp.ttl < 1m) { - set beresp.ttl = 1m; - } -} - -sub vcl_deliver { - # Add an HSTS header to everything - set resp.http.Strict-Transport-Security = "max-age=31536000;includeSubdomains;preload"; - - if (obj.hits > 0) { - set resp.http.X-Cache = "HIT"; - } else { - set resp.http.X-Cache = "MISS"; - } -} - -sub vcl_synth { - # Execute TLS or www. redirect - if (resp.status == 750) { - set resp.http.Location = "https://www.tazj.in" + req.url; - set resp.http.Strict-Transport-Security = "max-age=31536000;includeSubdomains;preload"; - set resp.status = 301; - return (deliver); - } -} -- cgit 1.4.1