diff of 5b1a3cd09e91dcd10a13141a3a6f1fa6d98f4fc7

5b1a3cd09e91dcd10a13141a3a6f1fa6d98f4fc7
diff --git a/.gitignore b/.gitignore
index 72b0d98..b594379 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,9 @@
 *~
-*.fasl
\ No newline at end of file
+*.fasl
+elm-frontti/*.html
+elm-frontti/*.js
+elm-frontti/elm-stuff/*
+*cookies
+*input.json
+/elm-frontti/src/elm.js
+/elm-frontti/TAGS
diff --git a/elm-frontti/elm.json b/elm-frontti/elm.json
new file mode 100644
index 0000000..a61175b
--- /dev/null
+++ b/elm-frontti/elm.json
@@ -0,0 +1,46 @@
+{
+    "type": "application",
+    "source-directories": [
+        "src"
+    ],
+    "elm-version": "0.19.1",
+    "dependencies": {
+        "direct": {
+            "NoRedInk/elm-json-decode-pipeline": "1.0.0",
+            "PanagiotisGeorgiadis/elm-datetime": "1.3.0",
+            "TSFoster/elm-uuid": "4.2.0",
+            "danhandrea/elm-date-format": "2.0.1",
+            "elm/browser": "1.0.2",
+            "elm/core": "1.0.5",
+            "elm/file": "1.0.5",
+            "elm/html": "1.0.0",
+            "elm/http": "2.0.0",
+            "elm/json": "1.1.3",
+            "elm/time": "1.0.0",
+            "elm/url": "1.0.0",
+            "elm-community/dict-extra": "2.4.0",
+            "elm-community/string-extra": "4.0.1",
+            "mhoare/elm-stack": "3.1.2",
+            "waratuman/json-extra": "1.0.2"
+        },
+        "indirect": {
+            "TSFoster/elm-bytes-extra": "1.3.0",
+            "TSFoster/elm-md5": "2.0.1",
+            "TSFoster/elm-sha1": "2.1.1",
+            "danfishgold/base64-bytes": "1.1.0",
+            "elm/bytes": "1.0.8",
+            "elm/parser": "1.1.0",
+            "elm/random": "1.0.0",
+            "elm/regex": "1.0.0",
+            "elm/virtual-dom": "1.0.2",
+            "justinmimbs/timezone-data": "2.1.4",
+            "rtfeldman/elm-hex": "1.0.0",
+            "rtfeldman/elm-iso8601-date-strings": "1.1.3",
+            "waratuman/time-extra": "1.1.0"
+        }
+    },
+    "test-dependencies": {
+        "direct": {},
+        "indirect": {}
+    }
+}
diff --git a/elm-frontti/src/Ajax_cmds.elm b/elm-frontti/src/Ajax_cmds.elm
new file mode 100644
index 0000000..75d976b
--- /dev/null
+++ b/elm-frontti/src/Ajax_cmds.elm
@@ -0,0 +1,114 @@
+module Ajax_cmds exposing (..)
+
+import Article
+import User
+import Page
+import Message exposing (..)
+import Http exposing (..)
+import Image as Image
+import Settings
+import Json.Decode as Json
+
+getSession =
+    Http.get
+        { url = "/api/login/session"
+        , expect = Http.expectJson GotSession User.userDecoder}
+
+getEditablePosts : Cmd Msg
+getEditablePosts =
+    Http.get
+        { url = "/api/posts/all-titles"
+        , expect = Http.expectJson EditableTitlesReceived (Json.list Article.sidebarTitleDecoder) }
+
+getPage : Int -> Cmd Msg
+getPage page_id =
+    Http.get
+        { url = "/api/posts/page/" ++ (String.fromInt page_id) ++ "/page-size/6"
+        , expect = Http.expectJson PageReceived Page.pageDecoder}
+
+getPost : Int -> Cmd Msg
+getPost post_id =
+    Http.get
+        { url = "/api/posts/post/" ++ (String.fromInt post_id)
+        , expect = Http.expectJson PostReceived Article.articleDecoder}
+
+getSettings : Cmd Msg
+getSettings =
+    Http.get
+        { url = "/api/settings/client-settings"
+        , expect = Http.expectJson SettingsReceived Settings.settingsDecoder}
+
+getTitles =
+    Http.get
+        { url = "/api/posts/titles"
+        , expect = Http.expectJson TitlesReceived (Json.list Article.sidebarTitleDecoder)}
+
+postLogin username password =
+    Http.post
+       { url = "/api/login/login"
+       , expect = Http.expectJson LoginSuccess User.userDecoder
+       , body = Http.jsonBody <| User.encodeLoggingIn <| User.UserLoggingIn username password}
+
+getPostEditorData post_id =
+    Http.get
+        { url = "/api/posts/post/" ++ (String.fromInt post_id) ++ "/allow-hidden/true"
+        , expect = Http.expectJson EditorPostReceived Article.articleDecoder}
+
+postArticle : Article.Article -> Cmd Msg        
+postArticle article =
+    Http.post
+        { url = "/api/posts/post"
+        , body = Http.jsonBody <| Article.encode article
+        , expect = Http.expectString HttpIgnoreResponse }
+        
+putArticle : Article.Article -> Cmd Msg        
+putArticle article =
+    case article.id of
+        Just id ->
+            Http.request
+                { method = "PUT"
+                , headers = []
+                , url = "/api/posts/post"
+                , body = Http.jsonBody <| Article.encode article
+                , expect = Http.expectString HttpGoHome
+                , timeout = Nothing
+                , tracker = Nothing
+                }
+        Nothing -> Cmd.none
+
+-- returns { :id :name }
+getListOfImages : Bool -> Cmd Msg
+getListOfImages managerCalled = Http.get
+                  { url = "/api/pictures/list/all"
+                  , expect = Http.expectJson (GotListOfImages managerCalled) (Json.list Image.imageDecoder)}
+
+
+postPicture pictureFile = Http.post 
+                          { url = "/api/pictures"
+                          , body = Http.multipartBody [ Http.filePart "file" pictureFile ]
+                          , expect = Http.expectJson UploadedImage Image.imageResponseDecoder }
+
+
+deletePictures ids = Http.request
+                     { url = "/api/pictures"
+                     , method = "DELETE"
+                     , headers = []
+                     , expect = Http.expectString HttpManagerGetListOfImages
+                     , body = Http.jsonBody <| (Image.list_of_uuids_encode ids)
+                     , timeout = Nothing
+                     , tracker = Nothing}
+
+getReferencingPosts id = Http.get
+                         { url = "/api/pictures/referencing/" ++ id
+                         , expect = Http.expectJson GotReferencingPosts (Json.list Image.referencingPostDecoder)}
+
+loadTaggedPosts tags = Http.get
+                       { url = "/api/posts/tagged/" ++ tags
+                       , expect = Http.expectJson GotTaggedPosts (Json.list Article.articleDecoder)}
+
+loadPostVersion post_id_int version_id_int =
+    let post_id = String.fromInt post_id_int
+        version_id = String.fromInt version_id_int in
+    Http.get
+        { url = "/api/posts/post/" ++ post_id ++ "/version/" ++ version_id
+        , expect = Http.expectJson GotOldPost Article.articleDecoder}
diff --git a/elm-frontti/src/Article.elm b/elm-frontti/src/Article.elm
new file mode 100644
index 0000000..24a371c
--- /dev/null
+++ b/elm-frontti/src/Article.elm
@@ -0,0 +1,119 @@
+module Article exposing (..)
+
+import Creator exposing (encode)
+
+import DateTime exposing (DateTime)
+import Json.Encode as Json exposing (..)
+import Json.Encode.Extra exposing (..)
+import Json.Decode as Decode exposing (Decoder, succeed)
+import Json.Decode.Pipeline exposing (required)
+import Json.Decode.Extra as Extra
+import Time
+
+-- {
+--   "tags": [],
+--   "creator": {
+--     "username": "feuer",
+--     "nickname": "Feuer",
+--     "img_location": "https://feuerx.net/etc/feuer.jpeg"
+--   },
+--   "content": "<p>Tämä on testi posti :D</p>\n\n<p>Uusi paragraaaaaaaafffi</p>",
+--   "comments": [],
+--   "amount-of-comments": 0,
+--   "title": "Testi Posti",
+--   "prev-post-id": null,
+--   "id": 1,
+--   "versions": [],
+--   "version": null,
+--   "next-post-id": null,
+--   "created_at": "2020-10-16T07:52:59Z"
+-- }
+
+import Creator exposing (Creator, creatorDecoder)
+
+decodeApply : Decode.Decoder a -> Decode.Decoder (a -> b) -> Decode.Decoder b
+decodeApply value partial =
+    Decode.andThen (\p -> Decode.map p value) partial
+
+
+type alias Article =
+    { creator : Creator
+    , tags : List String
+    , content : String
+    -- TODO make a comment type
+    , comments : Maybe (List String)
+    -- , amount_of_comments : Int
+    , title : String
+    , pre_post_id : Maybe Int
+    , id : Maybe Int
+    , versions: Maybe (List Int)
+    , version : Maybe Int
+    -- , next_post_id: Maybe Int
+    , created_at: Maybe Time.Posix
+    }
+
+-- encoder
+encode : Article -> Json.Value
+encode article =
+    object
+        [ ( "creator", Creator.encode article.creator )
+        , ( "tags", list string article.tags)
+        , ( "content", string article.content)
+        , ( "comments", (list string (case article.comments of
+                                         Just comments -> comments
+                                         Nothing -> [])))
+        , ( "title", string article.title)
+        , ( "pre_post_id", (maybe int) article.pre_post_id)
+        , ( "id", (maybe int) article.id)
+        , ( "version", (maybe int) article.version)
+        , ( "created_at", (maybe iso8601) article.created_at)
+        ]
+
+
+-- decoder
+
+    
+tagsDecoder = Decode.field "tags" (Decode.list Decode.string)
+contentDecoder = Decode.field "content" Decode.string
+commentsDecoder = Decode.maybe (Decode.field "comments" (Decode.list Decode.string))
+-- amount_of_commentsDecoder = Decode.field "amount-of-comments" Decode.int                  
+titleDecoder = Decode.field "title" Decode.string
+pre_post_idDecoder = Decode.maybe (Decode.field "prev-post-id"  Decode.int)
+idDecoder = Decode.maybe ( Decode.field "id" Decode.int)
+versionsDecoder = Decode.maybe (Decode.field "versions" (Decode.list Decode.int))
+versionDecoder = Decode.maybe (Decode.field "version" Decode.int)
+-- next_post_idDecoder = Decode.field "next-post-id" (Decode.maybe Decode.int)
+created_atDecoder = Decode.field "created_at" (Decode.maybe Extra.iso8601)
+creator_Decoder = Decode.field "creator" creatorDecoder                    
+
+-- |> == clojure's ->>
+articleDecoder : Decoder Article                    
+articleDecoder =
+    Decode.succeed Article
+        |> decodeApply creator_Decoder
+        |> decodeApply tagsDecoder
+        |> decodeApply contentDecoder
+        |> decodeApply commentsDecoder
+        |> decodeApply titleDecoder
+        |> decodeApply pre_post_idDecoder
+        |> decodeApply idDecoder
+        |> decodeApply versionsDecoder
+        |> decodeApply versionDecoder
+        |> decodeApply created_atDecoder
+
+type alias Title =
+    { title : String
+    , id : Int
+    , year : Int
+    , month: Int
+    , tags: List String
+    }
+    
+
+sidebarTitleDecoder =
+    Decode.succeed Title
+        |> decodeApply (Decode.field "Title" Decode.string)
+        |> decodeApply (Decode.field "Id" Decode.int)
+        |> decodeApply (Decode.field "Year" Decode.int)
+        |> decodeApply (Decode.field "Month" Decode.int)
+        |> decodeApply (Decode.field "Tags" (Decode.list Decode.string))
diff --git a/elm-frontti/src/Article_view.elm b/elm-frontti/src/Article_view.elm
new file mode 100644
index 0000000..b252983
--- /dev/null
+++ b/elm-frontti/src/Article_view.elm
@@ -0,0 +1,50 @@
+module Article_view exposing (..)
+
+import DateFormat as Df
+import Html exposing (..)
+import Html.Attributes exposing (..)
+import Html.Events exposing (onInput, onClick)
+import User
+import Message exposing (..)
+import Settings
+import Time
+import Article
+
+
+formatDateTime formatString zone posixTime =
+    Df.format formatString zone posixTime
+
+articleView settings loginstate zone the_actual_post =
+    let versions = Maybe.withDefault [] the_actual_post.versions
+    in
+    div [class "post"] [ case the_actual_post.id of
+                             Just post_id -> a [href ("/blog/post/" ++ String.fromInt post_id)] [ text the_actual_post.title ]
+                             Nothing -> span [] [ text the_actual_post.title ]
+                       , div [class "meta"] (List.append [ User.user_avatar the_actual_post.creator
+                                                         , p [] [text ("By " ++ the_actual_post.creator.nickname)]
+                                                         , case the_actual_post.created_at of
+                                                               Just writing_time ->
+                                                                   p [] [text ("Written at " ++ (formatDateTime settings.time_format zone writing_time))]
+                                                               Nothing ->
+                                                                   p [] [text ("No idea when it's written")]]
+                                                 (case the_actual_post.id of
+                                                     Just post_id ->
+                                                         (List.map (\version -> a [ href ("/blog/post/" ++ String.fromInt post_id ++ "/version/" ++ String.fromInt version) ] [ text ((String.fromInt version) ++ ", ")]) versions)
+                                                     Nothing -> []))
+                             
+                       , (case the_actual_post.id of
+                              Just post_id ->
+                                  case loginstate of
+                                      LoggedIn _ -> a [ href ("/blog/post/edit/" ++ String.fromInt post_id)
+                                                      , attribute "data-testid" "edit-post-btn"
+                                                      , onClick (OpenPostEditor post_id)] [text "Edit this post"]
+                                      _ -> div [] []
+                              _ -> div [] [])
+                                                    
+                       , article [ class "content"
+                                 , dangerouslySetInnerHTML the_actual_post.content] []
+                       , div [] ( the_actual_post.tags
+                                |> List.filter ((/=) "")
+                                |> List.map ( \tag -> span [] [ a [ href ("/blog/tags/" ++ tag)
+                                                                  , class "tag" ] [text tag]
+                                                              , text ", "]))]
diff --git a/elm-frontti/src/Button.elm b/elm-frontti/src/Button.elm
new file mode 100644
index 0000000..f56df8f
--- /dev/null
+++ b/elm-frontti/src/Button.elm
@@ -0,0 +1,9 @@
+module Button exposing (murja_button)
+
+import Html exposing (..)
+import Html.Attributes exposing (..)
+import Html.Events exposing (..)
+
+murja_button params contents =
+    span (List.append params [ class "murja-button" ])
+        contents
diff --git a/elm-frontti/src/Creator.elm b/elm-frontti/src/Creator.elm
new file mode 100644
index 0000000..fee57f6
--- /dev/null
+++ b/elm-frontti/src/Creator.elm
@@ -0,0 +1,26 @@
+module Creator exposing (..)
+
+import Json.Encode as Json exposing (..)
+import Json.Decode as Decode exposing (Decoder, succeed)
+import Json.Decode.Pipeline exposing (required)
+
+type alias Creator =
+    { username : String
+    , nickname : String
+    , img_location : String}
+
+usernameDecoder = Decode.field "username" Decode.string
+nicknameDecoder = Decode.field "nickname" Decode.string
+img_locationDecoder = Decode.field "img_location" Decode.string
+
+creatorDecoder = Decode.map3 Creator usernameDecoder nicknameDecoder img_locationDecoder                      
+
+-- encoder
+
+encode : Creator -> Json.Value
+encode creator =
+    object
+        [ ( "username", string creator.username)
+        , ( "nickname", string creator.nickname)
+        , ( "img_location", string creator.img_location)
+        ]
diff --git a/elm-frontti/src/Date_utils.elm b/elm-frontti/src/Date_utils.elm
new file mode 100644
index 0000000..1ae2823
--- /dev/null
+++ b/elm-frontti/src/Date_utils.elm
@@ -0,0 +1,8 @@
+module Date_utils exposing (int_to_month_string)
+import Array
+
+-- import Dict exposing (Dict)
+
+months = Array.fromList ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]
+
+int_to_month_string i = Array.get (i - 1) months         
diff --git a/elm-frontti/src/Image.elm b/elm-frontti/src/Image.elm
new file mode 100644
index 0000000..d11abca
--- /dev/null
+++ b/elm-frontti/src/Image.elm
@@ -0,0 +1,49 @@
+module Image exposing (..)
+
+import Json.Encode as Json exposing (..)
+import Json.Encode.Extra exposing (..)
+import Json.Decode as Decode exposing (Decoder, succeed)
+import Json.Decode.Pipeline exposing (required)
+import Json.Decode.Extra as Extra
+
+import UUID exposing (UUID)
+
+import Article exposing (decodeApply)
+
+type alias Image =
+    { id: UUID
+    , name: String }
+
+type alias PostImageResponse =
+    { id: UUID }
+
+type alias ReferencingPost =
+    { post_id : Int
+    , post_title : String
+    , media_id : String
+    , media_name : String}
+
+encode img =
+    object
+        [ ("id", UUID.toValue img.id)
+        , ("name", string img.name) ]
+
+idDecoder = Decode.field "id" UUID.jsonDecoder
+nameDecoder = Decode.field "name" Decode.string
+
+imageDecoder =
+    Decode.succeed Image
+        |> decodeApply idDecoder
+        |> decodeApply nameDecoder
+imageResponseDecoder = Decode.succeed PostImageResponse
+                       |> decodeApply idDecoder
+
+list_of_uuids_encode ids = Json.object
+                           [ ( "ids", Json.list UUID.toValue ids)]
+
+referencingPostDecoder =
+    Decode.succeed ReferencingPost
+        |> decodeApply (Decode.field "post_id" Decode.int)
+        |> decodeApply (Decode.field "post_title" Decode.string)
+        |> decodeApply (Decode.field "media_id" Decode.string)
+        |> decodeApply (Decode.field "media_name" Decode.string)
diff --git a/elm-frontti/src/ImageSelector.elm b/elm-frontti/src/ImageSelector.elm
new file mode 100644
index 0000000..1a41159
--- /dev/null
+++ b/elm-frontti/src/ImageSelector.elm
@@ -0,0 +1,18 @@
+module ImageSelector exposing (..)
+
+import Html exposing (..)
+import Html.Attributes exposing (..)
+import Html.Events exposing (onClick)
+
+import UUID
+
+import Image exposing (Image)
+import Message exposing (..)
+
+image the_actual_img = img [ src ("/api/pictures/" ++ (UUID.toString the_actual_img.id))
+                           , onClick (SelectedImage the_actual_img.id)] []
+
+imageSelector : List Image -> Html Msg
+imageSelector img_list =
+    div [ id "selector-div" ]
+        (List.map image img_list)
diff --git a/elm-frontti/src/Main.elm b/elm-frontti/src/Main.elm
new file mode 100644
index 0000000..82c03da
--- /dev/null
+++ b/elm-frontti/src/Main.elm
@@ -0,0 +1,538 @@
+port module Main exposing (..)
+
+import Browser
+import Html exposing (..)
+import Html.Attributes exposing (..)
+import Html.Events exposing (onInput, onClick)
+
+import Http
+
+import Article
+import Article_view exposing (articleView)
+import Ajax_cmds exposing (..)
+import Creator as C
+import Page as P
+import Settings
+import Message exposing (..)
+import User
+import Topbar
+import PostsAdmin
+import PostEditor
+import Medialist exposing (medialist)
+import Image
+import ImageSelector exposing (imageSelector)
+
+import DateTime exposing (DateTime)
+import Json.Decode as Decode
+import Json.Encode
+import Time
+import Task
+import Dict.Extra exposing (groupBy)
+import Dict exposing (toList, keys, get)
+import String exposing (fromInt)
+import String.Extra exposing (toSentenceCase)
+import Stack exposing (push, top, pop)
+
+import Browser.Navigation as Nav
+
+import RouteParser
+import Url
+import Date_utils exposing (int_to_month_string)
+
+import UUID
+import File exposing (mime)
+
+
+-- MAIN
+
+main : Program () Model Msg
+main =
+  Browser.application
+    { init = init
+    , view = view
+    , update = update
+    , subscriptions = subscriptions
+    , onUrlChange = UrlChanged
+    , onUrlRequest = LinkClicked
+    }
+
+-- SUBSCRIPTIONS
+
+
+subscriptions : Model -> Sub Msg
+subscriptions _ = Sub.batch 
+                  [ tags ReceivedTag
+                  , aceStateUpdate AceStateUpdate
+                  , fromLocalStorage PostFromLocalStorage]
+
+initialModel url key viewstate = Model viewstate Nothing False False [] Nothing LoggedOut key url Nothing Time.utc Nothing
+    
+viewStatePerUrl : Url.Url -> (ViewState, List (Cmd Msg))
+viewStatePerUrl url =
+    case RouteParser.url_to_route url of
+        RouteParser.Page page_id -> (Loading, [ getSettings
+                                              , getTitles
+                                              , getSession
+                                              , getPage page_id
+                                              ])
+        RouteParser.Post post_id -> (Loading, [ getSettings
+                                              , getTitles
+                                              , getSession
+                                              , getPost post_id])
+        RouteParser.Home -> (Loading, [ getSettings
+                                      , getTitles
+                                      , getSession
+                                      , getPage 1
+                                      ])
+        RouteParser.PostEditor post_id -> (Loading, [ getSettings
+                                                    , getTitles
+                                                    , getSession
+                                                    , getPostEditorData post_id])
+        RouteParser.PostAdmin -> (Loading, [ getSettings
+                                           , getSession
+                                           , getTitles
+                                           , getEditablePosts ])
+        RouteParser.MediaManager -> (Loading, [ getSettings
+                                              , getSession
+                                              , getTitles
+                                              , getListOfImages True] )
+        RouteParser.TaggedPosts tags_ -> (Loading, [ getSession
+                                                   , getSettings
+                                                   , getTitles
+                                                   , loadTaggedPosts tags_])
+        RouteParser.NewPost ->
+            (PostEditor, [ getSettings
+                         , getTitles
+                         , getSession
+                         , loadPostFromLocalStorage ()])
+
+        RouteParser.PostVersion post_id version_id -> (Loading, [ getSession
+                                                                , getSettings
+                                                                , getTitles
+                                                                , loadPostVersion post_id version_id])
+                                                                      
+        RouteParser.NotFound -> (ShowError ("Couldn't parse url " ++ (Url.toString url)), [Cmd.none])
+    
+init _ url key =
+    let (viewstate, cmds) = (viewStatePerUrl url)
+        model = initialModel url key viewstate
+    in
+        ( model
+        , Cmd.batch (List.append cmds [ Task.perform AdjustTimeZone Time.here]))
+
+
+-- UPDATE
+
+
+-- PORTS --
+port prompt : String -> Cmd msg
+port alert : String -> Cmd msg
+port tags : (String -> msg) -> Sub msg
+port aceStateUpdate : (String -> msg) -> Sub msg
+
+port savePostToLocalStorage: String -> Cmd msg
+port loadPostFromLocalStorage: () -> Cmd msg                         
+port fromLocalStorage: (String -> msg) -> Sub msg
+
+update : Msg -> Model -> (Model, Cmd Msg)
+update msg model =
+    case msg of
+        SettingsReceived result ->
+            case result of
+                Ok new_settings ->
+                    ({model | settings = Just new_settings}, Cmd.none)
+                        
+                Err http_error ->
+                    ( model
+                    , alert ("Error loading settings " ++ Debug.toString http_error))
+        PostReceived result ->
+            case result of
+                Ok post -> ( {model | view_state = PostView post}
+                           , Cmd.none)
+                Err error -> ( model
+                             , alert ("Error loading post " ++ Debug.toString error))
+        PageReceived result ->
+            case result of
+                Ok page -> 
+                    ( {model | view_state = PageView page}
+                    , Cmd.none)
+                Err error ->
+                    ( model
+                    , alert ("Error loading page " ++ Debug.toString error))
+        TitlesReceived result ->
+            case result of
+                Ok decoded_titles ->
+                    ({ model | settings = Maybe.map (\s -> {s | titles = Just decoded_titles}) model.settings}
+                    ,  Cmd.none)
+                Err error ->
+                    ( model
+                    , alert ("Error loading titles " ++ Debug.toString error))
+        UrlChanged url ->
+            let (view_state, cmds) = viewStatePerUrl url in 
+            ({model | url = url, view_state = view_state}, Cmd.batch cmds)
+        LinkClicked urlRequest ->
+            case urlRequest of
+                Browser.Internal url ->
+                    (model, Nav.pushUrl model.key (Url.toString url))
+                Browser.External href ->
+                    (model, Nav.load href)
+        LoginFocus ->
+            ({model | loginState = LoggingIn "" ""}, Cmd.none)
+        ChangeUsername username ->
+            case model.loginState of
+                LoggingIn old_username password ->
+                    ({model | loginState = LoggingIn username password}, Cmd.none)
+                _ -> (model, Cmd.none)
+        ChangePassword password ->
+            case model.loginState of
+                LoggingIn username old_password ->
+                    ({model | loginState = LoggingIn username password}, Cmd.none)
+                _ -> (model, Cmd.none)
+        DoLogIn -> case model.loginState of
+                       LoggingIn username password ->
+                           (model, postLogin username password)
+                       _ -> (model, Cmd.none)
+        LoginSuccess result ->
+            case result of
+                Ok user ->
+                    ({model | loginState = LoggedIn user}, Cmd.none)
+                Err error ->
+                    ({model | loginState = LoginFailed}, Cmd.none)
+        GotSession result ->
+            case result of
+                Ok user ->
+                    if model.view_state == PostEditor then
+                        ({ model | loginState = LoggedIn user 
+                         , postEditorSettings = Just (PostEditorSettings
+                                                          (Maybe.withDefault 
+                                                               (Article.Article (C.Creator user.username user.nickname user.img_location) [""] "" Nothing "New post" Nothing Nothing (Just []) Nothing Nothing)
+                                                               model.postFromLocalStorage)
+                                                          "" False)}
+                        , Cmd.none)
+                    else 
+                        ({model | loginState = LoggedIn user}, Cmd.none)
+                Err error ->
+                    case error of
+                        Http.BadStatus status ->
+                            if status == 401 then
+                                -- no valid session
+                                (model, Cmd.none)
+                            else
+                                ( model
+                                , alert ("Error (" ++ String.fromInt status ++ ") when loading session"))
+                        Http.BadBody err ->
+                            ( model
+                            , alert ("Error when loading session: " ++ err))
+                        _ -> ( model
+                             , alert ("Error when loading session"))
+        EditableTitlesReceived result ->
+            case result of
+                Ok titles ->
+                    ({model | view_state = PostEditorList titles}
+                    , Cmd.none)
+                Err error ->
+                    ( model
+                    , alert ("Coudln't load titles " ++ Debug.toString error))
+        OpenPostEditor post_id ->
+            (model, getPostEditorData post_id)
+        EditorPostReceived result ->
+            case result of
+                Ok post ->
+                    ({ model | view_state = PostEditor
+                     , postEditorSettings = Just (PostEditorSettings post "" False)}
+                    , Cmd.none)
+                Err error ->
+                    ( model
+                    , alert ("Error loading post editor " ++ Debug.toString error))
+        PromptTag prompt_message ->
+            (model, prompt prompt_message)
+        Alert alert_msg ->
+            (model, alert alert_msg)
+        RunAce content ->
+            (model, reallySetupAce content)
+        SelectTag tag ->
+            case model.postEditorSettings of
+                Just settings ->
+                    ({ model | postEditorSettings = Just
+                           { settings | selected_tag = tag}}
+                    , Cmd.none)
+                _ -> (model, Cmd.none)
+        ReceivedTag tag ->
+            case model.postEditorSettings of
+                Just settings ->
+                    let old_article = settings.article
+                        article = { old_article | tags = tag :: settings.article.tags} in
+                    ({ model | postEditorSettings = Just
+                           { settings | article = article}}
+                    , savePostToLocalStorage (Json.Encode.encode 0 (Article.encode article)))
+                Nothing -> (model, alert "ReceivedTag called even though postEditorSettings is nil")
+        DropTag tag ->
+            case model.postEditorSettings of
+                Just settings ->
+                    let old_article = settings.article
+                        article = { old_article | tags = List.filter ((/=) settings.selected_tag) old_article.tags} in
+                    ({ model | postEditorSettings = Just
+                           { settings | article = article}}
+                    , savePostToLocalStorage (Json.Encode.encode 0 (Article.encode article)))
+                Nothing -> (model, alert "DropTag called even though postEditorSettings is nil")
+        HttpIgnoreResponse result ->
+            (model, Cmd.none)
+        SavePost article ->
+            let new_post_p = article.id == Nothing in
+            doGoHome_
+              { model | postEditorSettings = Nothing}
+              [ if new_post_p then postArticle article else putArticle article ]
+                    
+
+        GoHome -> doGoHome model
+        HttpGoHome _ -> doGoHome model
+
+        AceStateUpdate content ->
+            case model.postEditorSettings of
+                Just settings ->
+                    let article = settings.article in
+                    ({ model | postEditorSettings = Just
+                           { settings | article =
+                                 { article | content = content}}}
+                    , savePostToLocalStorage (Json.Encode.encode 0 (Article.encode article)))
+                Nothing -> (model, alert "AceStateUpdate called even though postEditorSettings is nil")
+                    
+        ChangeTitle new_title ->
+            case model.postEditorSettings of
+                Just settings ->
+                    let article = settings.article in
+                    ({ model | postEditorSettings = Just
+                           { settings | article =
+                                 { article | title = new_title}}}
+                    , savePostToLocalStorage (Json.Encode.encode 0 (Article.encode article)))
+                Nothing -> (model, alert "ChangeTitle called even though postEditorSettings is nil")            
+        HttpManagerGetListOfImages _ -> (model, getListOfImages True)                                  
+        GetListOfImages -> ( { model | showImageModal = True }
+                           , getListOfImages False)
+        GotListOfImages managerCalled result ->
+            case result of
+
+                Ok images ->
+                    case managerCalled of
+                        True ->
+                            ({ model
+                                 | loadedImages = images
+                                 , view_state = MediaList
+                                 , medialist_state = Just (MediaListState [] Dict.empty)}
+                            , Cmd.batch (List.map (\image -> getReferencingPosts (UUID.toString image.id)) images))
+                        False -> 
+                            ({model | showImageModal = True, loadedImages = images}, Cmd.none)
+                Err error ->
+                    ( model
+                    , alert (Debug.toString error))
+        SelectedImage img_id ->
+            ( {model | showImageModal = False, loadedImages = [] }
+            , addImgToAce (UUID.toString img_id))
+        EditorDragEnter ->
+            ( {model | draggingImages = True}
+            , Cmd.none)
+        EditorDragLeave ->
+            ( {model | draggingImages = False}
+            , Cmd.none)
+        GotFiles file files ->
+            if String.startsWith "image" (mime file) then
+                ( { model | draggingImages = False }
+                , postPicture file)
+            else
+                ( { model | draggingImages = False }
+                , alert ("Got " ++ (mime file) ++ ", expected an image"))
+        GotInputFiles files ->
+            if List.all (\file -> String.startsWith "image" (mime file)) files then
+                ( model
+                , Cmd.batch (List.map (\file -> postPicture file) files))
+            else
+                ( model
+                , alert ("Expected images, got " ++ (String.join ", " (List.map mime files))))
+        UploadedImage imgResponse ->
+            case imgResponse of
+                Ok actualResponse ->
+                    ( model
+                    , addImgToAce (UUID.toString actualResponse.id ))
+                Err err ->
+                    (model, alert ("Error uploading image " ++ Debug.toString err))
+        MarkImageForRemoval img_id ->
+            case model.medialist_state of
+                Just state ->
+                    if List.member img_id state.selected_ids_for_removal then
+                        ({ model | medialist_state = Just {state | selected_ids_for_removal =
+                                                               List.filter ((/=) img_id) state.selected_ids_for_removal}}
+                        , Cmd.none)
+                    else 
+                        ({ model | medialist_state = Just {state | selected_ids_for_removal = img_id :: state.selected_ids_for_removal}}
+                        , Cmd.none)
+                        
+                Nothing ->
+                    ( model
+                    , alert "Medialist state is uninitialized")
+        MarkAllImages ids ->
+            case model.medialist_state of
+                Just state ->
+                    ({ model | medialist_state = Just {state | selected_ids_for_removal = ids}}
+                    , Cmd.none)
+                Nothing -> ( model
+                           , alert "Medialist state is uninitialized")
+        RemoveSelectedImages ->
+            case model.medialist_state of
+                Just state -> 
+                    (model, deletePictures state.selected_ids_for_removal)
+                Nothing -> (model, Cmd.none)
+        GotReferencingPosts response ->
+            case response of
+                Ok posts ->
+                    case model.medialist_state of
+                        Just state -> ({ model | medialist_state = Just {state | referencing_posts =
+                                                                             Dict.union state.referencing_posts (groupBy .media_id posts)}}
+                                      , Cmd.none)
+                        Nothing -> ( model
+                                   , Cmd.none)
+                Err err ->
+                    ( model
+                    , alert "Error while downloading info about referencing posts, check your devtools' network log")
+        PushUrl url ->
+            ( model, Nav.pushUrl model.key url )
+        AdjustTimeZone zone ->
+            ( {model | zone = zone}
+            , Cmd.none)
+        GotTaggedPosts result ->
+            case result of
+                Ok posts ->
+                    ({ model | view_state = TaggedPostsView posts}
+                    , Cmd.none)
+                Err err ->
+                    ( model , alert ( "Error loading tagged posts " ++ (Debug.toString err)))
+        ToggleArticlePreview ->
+            ({ model | postEditorSettings = Maybe.map (\settings ->
+                                                           {settings | show_preview = not settings.show_preview}) model.postEditorSettings}
+            , Cmd.none)
+        GotOldPost result ->
+            case result of
+                Ok post ->
+                    ({ model | view_state = PostView post}
+                    , Cmd.none)
+                Err err ->
+                    (model , alert ("Error loading post version " ++ Debug.toString err))
+        PostFromLocalStorage post_json ->
+            case (Decode.decodeString Article.articleDecoder post_json) of
+                Ok saved_article ->
+                    ({ model | postFromLocalStorage = Just saved_article} 
+                    , Cmd.none)
+                Err err ->
+                    ( model
+                    , alert ("json decoding failed" ++ Debug.toString err))
+        ClearLocalStorage ->
+            case model.loginState of
+                LoggedIn user ->
+                    ({ model | postEditorSettings = Just (PostEditorSettings
+                                                              (Maybe.withDefault 
+                                                                   (Article.Article (C.Creator user.username user.nickname user.img_location) [""] "" Nothing "New post" Nothing Nothing (Just []) Nothing Nothing)
+                                                                   model.postFromLocalStorage)
+                                                              "" False)}
+                    , clearPostFromLS ())
+                _ -> (model, Cmd.none)
+            
+            
+                  
+            
+doGoHome_ model other_cmds =
+    (model, Cmd.batch (List.append [ getSettings
+                                   , getTitles
+                                   , getSession
+                                   , getPage 1
+                                   , Nav.pushUrl model.key "/blog/"]
+                           other_cmds))
+
+doGoHome model = doGoHome_ model []        
+                           
+                
+getContentCmd viewState =
+    case viewState of
+        PostEditorList _ -> getEditablePosts
+        _ -> Cmd.none
+                        
+
+-- VIEW
+
+
+
+sidebarHistory : List Article.Title -> Html Msg
+sidebarHistory titles =
+    let grouped_by_year = groupBy .year titles in
+      div [id "grouper"]
+          [ul []
+               (List.concat (List.map (\year ->
+                                           case get year grouped_by_year of
+                                               Just per_year ->
+                                                   [li [] [details [] [summary [] [text ((fromInt year) ++ " (" ++ (fromInt (List.length per_year)) ++ ")")],
+                                                                        let grouped_by_month = groupBy .month per_year in
+                                                                          ul [] (List.concat (List.map (\month ->
+                                                                                                            case (int_to_month_string month) of
+                                                                                                                Just month_str ->
+                                                                                                                    let month_titles = titles |> List.filter (\title ->
+                                                                                                                                                                  title.year == year && title.month == month)
+                                                                                                                    in
+                                                                                                                        [li [] [details [] [summary [] [text ((toSentenceCase month_str) ++ " (" ++ (fromInt (List.length month_titles)) ++ ")")]
+                                                                                                                               , ul [class "title-list"] (month_titles
+                                                                                                                                                         |> List.map (\title ->
+                                                                                                                                                                          [li [class "title-list"]
+                                                                                                                                                                               [a [href ("/blog/post/" ++ (fromInt title.id))] [text title.title]]])
+                                                                                                                                                         |> List.concat)]]]
+                                                                                                                Nothing -> [li [] [details [] [summary [] [text ("Couldn't decode month " ++ (String.fromInt month))]]]]
+                                                                                                       ) (keys grouped_by_month) |> List.reverse))]]]
+
+                                               Nothing ->
+                                                        [li [] [text ("There's no year " ++ (fromInt year) ++ " in titles")]]) (keys grouped_by_year |> List.reverse)))]
+
+
+
+view : Model -> Browser.Document Msg
+view model =
+    case model.settings of
+        Nothing ->
+            { title = "Error loading murja"
+            , body = 
+                  [div [] [text "Couldn't load settings"]]}
+        Just settings ->
+            { title = settings.blog_title
+            , body = 
+                  [ header [] [a [href "/"] [text settings.blog_title ]]
+                  , Topbar.topbar model.loginState
+                  , div [class "flex-container"] 
+                        [ div [class "page"]
+                              (case model.view_state of
+                                           Loading ->
+                                               [div [] [text "LOADING"]]
+                                           PostView article ->
+                                               [ articleView settings model.loginState model.zone article ]
+                                           PageView page ->
+                                               (List.concat [(List.map (articleView settings model.loginState model.zone) page.posts),
+                                                                 [footer [(attribute "data-testid" "page-changer")] (if page.id > 1 then [ a [href ("/blog/page/" ++ fromInt (page.id + 1))] [text "Older posts"]
+                                                                                                 , a [href ("/blog/page/" ++ fromInt (page.id - 1)), class "newer-post"] [text "Newer posts"]]
+                                                                             else [a [href ("/blog/page/" ++ fromInt (page.id + 1))] [text "Next page"]])]])
+                                           ShowError err ->
+                                               [pre [] [text err]]
+                                           PostEditorList titles -> [ PostsAdmin.view titles ]
+                                           TaggedPostsView articles ->
+                                               (List.map (articleView settings model.loginState model.zone) articles)
+                                           PostEditor ->
+                                               case model.postEditorSettings of
+                                                   Just editorSettings ->
+                                                       let post = editorSettings.article
+                                                           tag_index = editorSettings.selected_tag in
+                                                       PostEditor.postEditor post tag_index model.showImageModal model.loadedImages model.draggingImages editorSettings settings model.zone model.loginState
+                                                   Nothing -> [ div [] [ text "No post loaded" ]]
+                                           MediaList -> [ medialist model.loadedImages model.medialist_state ])
+                        , div [id "sidebar"] [ User.loginView model.loginState
+                                             , (case settings.titles of
+                                                    Just titles ->
+                                                        sidebarHistory titles 
+                                                    Nothing ->
+                                                        div [] [text "Loading history failed"])
+                                             , (case model.view_state of
+                                                    PostEditorList titles -> PostsAdmin.tagList titles
+                                                    
+                                                    _ -> div [] [])]]]}
diff --git a/elm-frontti/src/Medialist.elm b/elm-frontti/src/Medialist.elm
new file mode 100644
index 0000000..90b7c71
--- /dev/null
+++ b/elm-frontti/src/Medialist.elm
@@ -0,0 +1,66 @@
+module Medialist exposing (..)
+
+import Html exposing (..)
+import Html.Attributes exposing (..)
+import Html.Events exposing (..)
+import Json.Decode as D
+import Dict
+    
+
+import Http
+
+import Article
+import Ajax_cmds exposing (..)
+import Creator as C
+import Page as P
+import Message exposing (..)
+import ImageSelector exposing (imageSelector)
+import Image
+
+import UUID
+
+referencing_post_view post = li []
+                             [ a [ href ("/blog/post/" ++ (String.fromInt post.post_id)) ]
+                                 [ text post.post_title ]]
+
+medialist images medialist_state =
+    case medialist_state of
+        Just state ->
+            div [ class "vertical-flex-container" ]
+                (List.append
+                     [div [class "title-flex-container"]
+                          [ button [ class "post-admin-title"
+                                   , onClick RemoveSelectedImages ] [ text "Remove selected" ]
+                          , div [ class "post-admin-title" ] []
+                          , button [ class "post-admin-title"
+                                   , onClick (MarkAllImages (List.map .id images))] [ text "Select all" ]]]
+                     (List.map (\image ->
+                                    let checkbox_id = "delete" ++ (UUID.toString image.id)
+                                    in 
+                                        div [ class "title-flex-container" ]
+                                    [ details [ ]
+                                          [ summary [ class "post-admin-title" ] [h2 [] [ text image.name ]
+                                                                                 , div [] [ text (UUID.toString image.id)]]
+                                          , ImageSelector.image image]
+                                    , details [ class "post-admin-title"]
+                                        [ summary [ attribute "data-testid" "referencing-post"]
+                                              [ text "Referencing posts" ]
+                                        , case (Dict.get (UUID.toString image.id) state.referencing_posts) of
+                                              Just referencing_posts ->
+                                                  if referencing_posts == [] then
+                                                      div [] [ text "No referencing posts" ]
+                                                  else
+                                                       ul []
+                                                           (List.map referencing_post_view referencing_posts)
+                                              Nothing -> div [] [ text "No referencing posts" ]]
+                                    , div [ class "post-admin-title" ]
+                                        [ label [for checkbox_id] [text "Choose for deletion"]
+                                        , input [ type_ "checkbox"
+                                                , id checkbox_id
+                                                , checked (List.member image.id state.selected_ids_for_removal)
+                                                , onClick (MarkImageForRemoval image.id)] []]])
+                          images))
+        Nothing ->
+            div [] [ text "lol et sit oo initialisoinu medialist_statea" ]
+                       
+                       
diff --git a/elm-frontti/src/Message.elm b/elm-frontti/src/Message.elm
new file mode 100644
index 0000000..1dfee98
--- /dev/null
+++ b/elm-frontti/src/Message.elm
@@ -0,0 +1,135 @@
+port module Message exposing (..)
+
+import Http
+import Html
+import Html.Attributes
+import Json.Encode
+import Browser
+import Time
+import Page as P
+import Article
+import Browser.Navigation as Nav
+import Settings
+import Url
+import Title
+import Image exposing (Image, ReferencingPost)
+
+import File exposing (File)
+import UUID exposing (UUID)
+import Stack exposing (..)
+import Dict exposing (Dict)
+    
+type ViewState
+    = PageView P.Page
+    | PostView Article.Article
+    | Loading 
+    | ShowError String
+    | PostEditorList (List Title.Title)                     -- list all the posts in db
+    | PostEditor
+    | MediaList                     -- list all the image blobs in db
+    | TaggedPostsView (List Article.Article)
+      
+type alias User =
+    { username : String
+    , nickname : String
+    , img_location : String
+    }
+
+type LoginState
+    = LoggedIn LoginUser
+    | LoggingIn String String
+    | LoginFailed
+    | LoggedOut      
+
+
+type alias LoginUser =
+    { nickname : String
+    , username : String
+    , img_location : String
+    , primary_group_name : String
+    , permissions : List String
+    }
+
+type alias MediaListState =
+    { selected_ids_for_removal : List UUID
+    , referencing_posts : Dict String (List ReferencingPost)}
+
+type alias PostEditorSettings =
+    { article : Article.Article
+    , selected_tag : String
+    , show_preview : Bool}
+    
+type alias Model =
+    { view_state : ViewState
+    , settings : Maybe Settings.Settings
+    , showImageModal : Bool
+    , draggingImages : Bool
+    , loadedImages : List Image
+    , medialist_state : Maybe MediaListState
+    , loginState : LoginState
+    , key : Nav.Key
+    , url : Url.Url
+    , postEditorSettings: Maybe PostEditorSettings
+    , zone : Time.Zone
+    , postFromLocalStorage : Maybe Article.Article}
+    
+type Msg
+  = PageReceived (Result Http.Error P.Page)
+  | PostReceived (Result Http.Error Article.Article)
+  | SettingsReceived (Result Http.Error Settings.Settings)
+  | TitlesReceived (Result Http.Error (List Article.Title))
+  | EditableTitlesReceived (Result Http.Error (List Article.Title))
+  | UrlChanged Url.Url
+  | LinkClicked Browser.UrlRequest
+  | LoginFocus
+  | ChangeUsername String
+  | ChangePassword String
+  | DoLogIn
+  | LoginSuccess (Result Http.Error LoginUser)
+  | GotSession (Result Http.Error LoginUser)
+  | OpenPostEditor Int
+  | EditorPostReceived (Result Http.Error Article.Article)
+  | PromptTag String
+  | ReceivedTag String
+  | AceStateUpdate String
+  | SelectTag String
+  | Alert String
+  | DropTag String
+  | SavePost Article.Article
+  | HttpIgnoreResponse (Result Http.Error String)
+  | HttpGoHome (Result Http.Error String)
+  | GoHome
+  | ChangeTitle String
+  | RunAce String
+  | GetListOfImages
+  | GotListOfImages Bool (Result Http.Error (List Image.Image))
+  | SelectedImage UUID
+  | EditorDragEnter
+  | EditorDragLeave
+  | GotFiles File (List File)
+  | GotInputFiles (List File)
+  | UploadedImage (Result Http.Error Image.PostImageResponse)
+  | MarkImageForRemoval UUID
+  | MarkAllImages (List UUID)
+  | RemoveSelectedImages
+  | HttpManagerGetListOfImages (Result Http.Error String)
+  | GotReferencingPosts (Result Http.Error (List Image.ReferencingPost))
+  | PushUrl String
+  | AdjustTimeZone Time.Zone
+  | GotTaggedPosts  (Result Http.Error (List Article.Article))
+  | ToggleArticlePreview
+  | GotOldPost (Result Http.Error Article.Article)
+  | PostFromLocalStorage String
+  | ClearLocalStorage 
+  
+
+
+-- ports
+port reallySetupAce : String -> Cmd msg
+port addImgToAce : String -> Cmd msg
+port clearPostFromLS : () -> Cmd msg                   
+
+
+-- dumb shit that would deserve its own module
+dangerouslySetInnerHTML: String -> Html.Attribute msg
+dangerouslySetInnerHTML = Json.Encode.string >> Html.Attributes.property "dangerouslySetInnerHTML"
diff --git a/elm-frontti/src/Page.elm b/elm-frontti/src/Page.elm
new file mode 100644
index 0000000..c9d51db
--- /dev/null
+++ b/elm-frontti/src/Page.elm
@@ -0,0 +1,22 @@
+module Page exposing (..)
+
+
+import Http
+import Html exposing (Html, text, pre)
+import Article as A
+
+import Json.Decode as Decode exposing (Decoder, succeed)
+import Json.Decode.Pipeline exposing (required)
+import Json.Decode.Extra as Extra
+
+type alias Page =
+    { last_page: Bool
+    , id : Int
+    , posts: List A.Article}
+
+pageDecoder : Decoder Page
+pageDecoder =
+    Decode.map3 Page
+        (Decode.field "last-page?" Decode.bool)
+        (Decode.field "id" Decode.int)
+        (Decode.field "posts" (Decode.list A.articleDecoder))
diff --git a/elm-frontti/src/PostEditor.elm b/elm-frontti/src/PostEditor.elm
new file mode 100644
index 0000000..2364142
--- /dev/null
+++ b/elm-frontti/src/PostEditor.elm
@@ -0,0 +1,111 @@
+module PostEditor exposing (..)
+
+import Html exposing (..)
+import Html.Attributes exposing (..)
+import Html.Events exposing (..)
+import Json.Decode as D
+    
+
+import Http
+
+import Article_view
+import Ajax_cmds exposing (..)
+import Creator as C
+import Page as P
+import Message exposing (..)
+import ImageSelector exposing (imageSelector)
+import Button exposing (murja_button)
+
+import File exposing (File)
+import File.Select as Select
+
+dropDecoder : D.Decoder Msg
+dropDecoder =
+  D.at ["dataTransfer","files"] (D.oneOrMore GotFiles File.decoder)
+
+
+hijackOn : String -> D.Decoder msg -> Attribute msg
+hijackOn event decoder =
+  preventDefaultOn event (D.map hijack decoder)
+
+
+hijack : msg -> (msg, Bool)
+hijack msg =
+  (msg, True)
+      
+
+optionize tag = option [value tag] [text tag]
+
+tagView post selectedTag = div [class "tagview"]
+                           [ select [ multiple True
+                                    , class "tag-select"
+                                    , id "tag-select"
+                                    , onInput SelectTag
+                                    , attribute "data-tags" (String.join "," post.tags)] (List.map optionize post.tags)
+                           , murja_button [ onClick (PromptTag "New tag? ")
+                                          , id "new-tag-btn"]
+                                 [ text "Add tag"]
+                           , murja_button [ onClick (DropTag selectedTag)
+                                          , attribute "data-testid" "remove-tag"]
+                               [text "Remove selected tag"]]
+
+third_column = div [class "tagview" ]
+               [ murja_button [ onClick ClearLocalStorage
+                              , attribute "data-testid" "clear-editor" ]
+                     [ text "Clear post in the editor" ] ]
+
+editor params =
+    node "ace-editor"
+    (  params
+    ++ [ attribute "theme" "ace/theme/monokai"
+       , attribute "mode" "ace/mode/html"])
+    []
+
+filesDecoder : D.Decoder (List File)
+filesDecoder =
+  D.at ["target","files"] (D.list File.decoder)
+      
+postEditor post tag showImageModal loadedImages draggingImages editorSettings app_settings tz loginState
+    = [ div [ id "editor-buttons"]
+            [ input [ name "title"
+                    , id "editor-post-title"
+                    , value post.title
+                    , onInput ChangeTitle] []
+            , murja_button [ id "editor-post-save"
+                           , onClick (SavePost post) ] [text "Save version"]
+            , label [ for "file-pictures-input"
+                    , class "murja-button"] [ text "Add pictures from device"]
+            , input [ type_ "file"
+                    , multiple False
+                    , style "display" "none"
+                    , id "file-pictures-input"
+                    , on "change" (D.map GotInputFiles filesDecoder)] []
+            , murja_button [ id "image-insert-btn"
+                           , onClick GetListOfImages]
+                  [text "Insert image"]
+            , label [for "show-preview-cb"]
+                [text "Show article preview"]
+            , input [ type_ "checkbox"
+                    , id "show-preview-cb"
+                    , checked editorSettings.show_preview
+                    , onClick ToggleArticlePreview] []]
+            
+      , tagView post tag
+      , third_column
+      , if showImageModal then imageSelector loadedImages else div [] []
+      , div [ attribute "data-testid" "article-id" ] [ text ("Article: " ++ (Maybe.withDefault "No id" (Maybe.map String.fromInt post.id)))]
+
+      , if editorSettings.show_preview then
+            case loginState of
+                LoggedIn user ->
+                    Article_view.articleView app_settings loginState tz post
+                _ -> div [] [text "You're not logged in"]
+                        
+        else editor [ id "editor-post-content"
+                    , style "background-color" (if draggingImages then "#880088" else "")
+                    , hijackOn "dragenter" (D.succeed EditorDragEnter)
+                    , hijackOn "dragend" (D.succeed EditorDragLeave)
+                    , hijackOn "dragover" (D.succeed EditorDragEnter)
+                    , hijackOn "dragleave" (D.succeed EditorDragLeave)
+                    , hijackOn "drop" dropDecoder
+                    , hijackOn "ready" (D.succeed (RunAce post.content))]]
diff --git a/elm-frontti/src/PostsAdmin.elm b/elm-frontti/src/PostsAdmin.elm
new file mode 100644
index 0000000..a1dfdc5
--- /dev/null
+++ b/elm-frontti/src/PostsAdmin.elm
@@ -0,0 +1,49 @@
+module PostsAdmin exposing (..)
+
+import Message exposing (..)
+import Set
+
+import Html exposing (..)
+import Html.Attributes exposing (..)
+import Html.Events exposing (..)
+
+import Date_utils exposing (int_to_month_string)
+
+tagListElement allTags tag =
+    let count = (  allTags
+                |> List.filter ((==) tag)
+                |> List.length) in
+    a [ href ("/blog/tags/" ++ tag)
+      , style "display" "block" ]
+    [ text (tag ++ " (" ++ (String.fromInt count) ++ ")")]
+
+tagList titles =
+    let allTags = (  titles
+                  |> List.concatMap (\title -> title.tags))
+        tags = (  allTags
+               |> Set.fromList
+               |> Set.toList
+               |> List.filter ((/=) "")) in
+    div [] (List.append [h3 [] [text ("Tags in the system (" ++ String.fromInt (List.length tags) ++ "): ")]]
+                (  tags 
+                |> List.map (tagListElement allTags)))
+
+titleView title =  case (int_to_month_string title.month) of
+                       Just month -> 
+                           div [ class "title-flex-container" ] 
+                               [ span [class "post-admin-title" ] [text ( title.title ++ " - " ++ month  ++ ", " ++ (String.fromInt title.year))]
+                               , a [ href ("/blog/post/edit/" ++ String.fromInt title.id)
+                                   , attribute "data-testid" "manager-edit-post-btn"
+                                   , onClick (OpenPostEditor title.id)] [text "Edit"]
+                               , a [href ("/blog/post/remove/" ++ String.fromInt title.id)] [text "Remove"]
+                               , div [class "post-admin-title" ]
+                                   (List.append  [ h3 [] [text "Tags: "]]
+                                        (List.map (\tag -> a [ href ("/blog/tags/" ++ tag)
+                                                             , style "display" "block" ]
+                                                       [ text tag ]) title.tags))]
+                       Nothing -> div [] [text ("Parsing month " ++ (String.fromInt title.month) ++ " failed")]
+
+view titles = (div [class "vertical-flex-container"]
+               (titles |>
+                List.map titleView))
+                              
diff --git a/elm-frontti/src/RouteParser.elm b/elm-frontti/src/RouteParser.elm
new file mode 100644
index 0000000..f60d76e
--- /dev/null
+++ b/elm-frontti/src/RouteParser.elm
@@ -0,0 +1,33 @@
+module RouteParser exposing (..)
+
+import Url
+import Url.Parser exposing (..)
+import String exposing (fromInt)
+-- http://localhost:3000/blog/post/edit/21
+type Route
+    = Page Int
+    | Post Int
+    | NewPost 
+    | PostAdmin
+    | MediaManager
+    | PostEditor Int
+    | TaggedPosts String
+    | PostVersion Int Int
+    | Home
+    | NotFound
+
+routeParser =
+    oneOf
+        [ map Page (s "blog" </> (s "page" </> int))
+        , map Home Url.Parser.top
+        , map Home (s "blog")
+        , map PostVersion (s "blog" </> (s "post" </> (int </> (s "version" </> int))))
+        , map Post (s "blog" </> (s "post" </> int))
+        , map PostEditor (s "blog" </> (s "post" </> (s "edit" </> int)))
+        , map MediaManager (s "blog" </> (s "mediamanager"))
+        , map NewPost (s "blog" </> (s "new_post"))
+        , map TaggedPosts (s "blog" </> (s "tags" </> string))
+        , map PostAdmin (s "blog" </> (s "postadmin"))]
+
+url_to_route url =
+            Maybe.withDefault NotFound (parse routeParser url)
diff --git a/elm-frontti/src/Settings.elm b/elm-frontti/src/Settings.elm
new file mode 100644
index 0000000..06d0ddd
--- /dev/null
+++ b/elm-frontti/src/Settings.elm
@@ -0,0 +1,32 @@
+-- {
+--   "time-format": "dd.MM.yyyy HH:mm",
+--   "blog-title": "Murja.dev @ roland",
+--   "recent-post-count": 6,
+--   "xss-filter-posts?": false
+-- }
+
+
+module Settings exposing (..)
+
+import Json.Decode as Decode exposing (Decoder, succeed)
+import Json.Decode.Pipeline exposing (required)
+import Json.Decode.Extra as Extra
+
+import Article
+
+type alias Settings =
+    { time_format : String
+    , blog_title : String
+    , recent_post_count : Int
+    , xss_filter_posts : Bool
+
+    , titles : Maybe (List Article.Title)     --for reasons fucking unknown, growing Main.Model beoynd 2 fields breaks everything. 
+    }
+
+settingsDecoder = Decode.map5 Settings
+                  (Decode.field "time-format" Decode.string)
+                  (Decode.field "blog-title" Decode.string)
+                  (Decode.field "recent-post-count" Decode.int)
+                  (Decode.field "xss-filter-posts?" Decode.bool)
+                  (Decode.maybe (Decode.list (Decode.field "does-not-exist" Article.sidebarTitleDecoder)))
+                     
diff --git a/elm-frontti/src/Title.elm b/elm-frontti/src/Title.elm
new file mode 100644
index 0000000..b5ffe05
--- /dev/null
+++ b/elm-frontti/src/Title.elm
@@ -0,0 +1,8 @@
+module Title exposing (..)
+
+type alias Title =
+    { title: String
+    , id: Int
+    , year: Int
+    , month: Int
+    , tags: List String}
diff --git a/elm-frontti/src/Topbar.elm b/elm-frontti/src/Topbar.elm
new file mode 100644
index 0000000..fc70da6
--- /dev/null
+++ b/elm-frontti/src/Topbar.elm
@@ -0,0 +1,25 @@
+module Topbar exposing (..)
+
+import Message exposing (..)
+import User
+import Article exposing (..)
+import Creator exposing (..)
+import Ajax_cmds exposing (..)
+
+import Html exposing (..)
+import Html.Attributes exposing (..)
+import Html.Events exposing (..)
+import Browser.Navigation as Nav
+
+import Button exposing (murja_button)
+
+topbar state =
+    case state of
+        LoggedIn user ->                               
+            div [class "left-sidebar"] [ span [] [text ("Welcome, " ++ user.nickname)]
+                                       , User.user_avatar user
+                                       , ul [] [ li [] [ murja_button [ onClick GoHome, attribute "data-testid" "home"] [text "Home"]]
+                                               , li [] [ murja_button [ onClick (PushUrl "/blog/postadmin"), attribute "data-testid" "manage-posts-btn" ] [text "Manage posts"]]
+                                               , li [] [ murja_button [ onClick (PushUrl "/blog/mediamanager")] [text "Manage media"]]
+                                               , li [] [ murja_button [ onClick (PushUrl "/blog/new_post"), attribute "data-testid" "new-post-btn" ] [text "New post!"]]]]
+        _ -> div [] []
diff --git a/elm-frontti/src/User.elm b/elm-frontti/src/User.elm
new file mode 100644
index 0000000..53b012f
--- /dev/null
+++ b/elm-frontti/src/User.elm
@@ -0,0 +1,88 @@
+module User exposing (..)
+
+import Html exposing (..)
+import Html.Attributes exposing (..)
+import Html.Events exposing (..)
+
+import Message exposing (..)
+import Article exposing (decodeApply)
+import Json.Decode as Decode exposing (Decoder, succeed)
+import Json.Decode.Pipeline exposing (required)
+import Json.Decode.Extra as Extra
+import Json.Encode as Json
+
+
+    -- {
+    --       "nickname": "Feuer",
+    --             "img_location": "https://feuerx.net/etc/feuer.jpeg",
+    --             "userid": 1,
+    --             "primary-group-name": "Admins",
+    --             "permissions": [
+    --                  "edit-self",
+    --                          "comment-post",
+    --                          "edit-user",
+    --                          "create-comment",
+    --                          "edit-post",
+    --                          "delete-post",
+    --                          "create-post",
+    --                          "create-page",
+    --                          "delete-comment",
+    --                          "delete-user",
+    --                          "can-import",
+    --                          "edit-comment"
+    --                        ]
+
+
+nicknameDecoder = Decode.field "nickname" Decode.string
+imgDecoder = Decode.field "img_location" Decode.string
+group_name_decoder = Decode.field "primary-group-name" Decode.string
+permissionsDecoder = Decode.field "permissions" (Decode.list Decode.string)
+usernameDecoder = Decode.field "username" Decode.string                  
+                     
+-- |> == clojure's ->>
+userDecoder : Decoder LoginUser
+userDecoder =
+    Decode.succeed LoginUser
+        |> decodeApply nicknameDecoder
+        |> decodeApply usernameDecoder
+        |> decodeApply imgDecoder
+        |> decodeApply group_name_decoder
+        |> decodeApply permissionsDecoder
+    
+stateToText state =
+    case state of
+        LoggedIn _ -> "LoggedIn"
+        LoggingIn _ _ -> "LoggingIn"
+        LoggedOut -> "LoggedOut"
+        LoginFailed -> "LoginFailed"
+           
+loginView loginstate =
+    let actual_view = [label [for "username"] [text "Username"],
+                       input [name "username", id "username", attribute "data-testid" "username-input-field", onInput ChangeUsername, onFocus LoginFocus ] [],
+                       label [for "password"] [text "Password"],
+                       input [name "password", attribute "data-testid" "password-input-field", id "password", type_ "password", onInput ChangePassword ] []
+                           -- , label [] [text ("Loginstate: " ++ stateToText loginstate)]
+                      ] in
+    div [] (case loginstate of
+                                  LoggedIn usr ->
+                                      [p [attribute "data-testid" "welcome-user-label"] [text ("Welcome, " ++ usr.nickname)]]
+                                  LoggingIn username password ->
+                                      (List.concat [actual_view,
+                                                    [button [attribute "data-testid" "dologin", onClick DoLogIn] [text "Login!"]]])
+                                  LoggedOut ->
+                                      actual_view
+                                  LoginFailed ->
+                                      (List.concat [actual_view,
+                                                    [button [onClick DoLogIn] [text "Login!"],
+                                                     div [attribute "data-testid" "loginfailed"] [text "Login failed! Check username and password!"]]]))
+
+user_avatar creator = img [class "user_avatar", src creator.img_location] []
+
+type alias UserLoggingIn =
+    { username : String
+    , password : String}
+
+encodeLoggingIn user =
+    Json.object
+        [ ("username", Json.string user.username)
+        , ("password", Json.string user.password)]
diff --git a/resources/css/murja.css b/resources/css/murja.css
deleted file mode 120000
index 809d42b..0000000
--- a/resources/css/murja.css
+++ /dev/null
@@ -1 +0,0 @@
-../../old-murja//murja/resources/public/css/murja.css
\ No newline at end of file
diff --git a/resources/css/murja.css b/resources/css/murja.css
new file mode 100644
index 0000000..e7b4f1a
--- /dev/null
+++ b/resources/css/murja.css
@@ -0,0 +1,245 @@
+html, body {
+    height: 100%
+}
+
+#editor-post-title {
+    flex: 1 1;	    
+}
+
+.tagview {
+    display: table;
+}
+
+.tagview > button {
+    display: table-cell;
+}
+
+.tagview > select {
+    display: block;
+}
+
+.tag {
+    font-size: 0.7em;
+}
+
+#editor-post-content {
+    box-sizing: border-box;
+    height: 100%;
+    min-width: 100%;
+    display: block;
+    flex: 10 1;	    
+}
+
+/* #editor-post-save { */
+/*     flex: 1 1; */
+/* } */
+
+#editor-buttons * {
+    display: block;
+}
+
+#selector-div {
+    
+}
+
+#selector-div img {
+    width: 300px;
+    height: 300px;
+    padding: 1em;
+}
+
+#selector-div img:hover {
+    background-color: #880088;
+}
+
+.left-sidebar {
+    flex: 1 1;
+    border: 2px solid #666666;
+}
+
+.left-sidebar > ul {
+    display: flex;
+    flex-flow: row wrap;
+}
+
+.left-sidebar > ul > li {
+    flex: 1 1;
+    list-style: none;
+}
+
+.page {
+    flex: 8 1;
+    bottom: 0;
+
+
+    display: flex;
+    flex-flow: row wrap;
+}
+
+.post {
+    border: 2px solid #666666;
+    width: 100%;
+}
+
+body {
+    background-color: #000000;
+    font-family: 'Helvetica Neue', Verdana, Helvetica, Arial, sans-serif;
+    color: #00CC00;
+}
+
+.meta {
+    color: #666666;
+}
+
+.flex-container {
+    display: flex;
+    flex-flow: row wrap;
+    height: 100%;
+}
+
+.title-flex-container {
+    display: flex;
+    flex-flow: row wrap;
+    margin-bottom: 10%;
+}
+
+.title-flex-container > * {
+
+}
+
+.title-flex-container > a {
+    flex: 1 4;
+}
+
+.vertical-flex-container {
+    display: flex;
+    width: 100%;
+    flex-flow: column wrap;
+}
+
+.post-admin-title {
+    flex: 3 1;
+}
+
+#sidebar {
+    border: 2px solid #666666;
+    flex: 1 1;
+}
+
+#loginview {
+    border-top: 2px solid #666666;
+    border-bottom: 2px solid #666666;
+}
+
+#loginview button {
+    display: block;
+    margin-bottom: 30px;
+}
+
+#loginview a {
+    display: block;
+}
+
+.user_avatar {
+    width: 50px;
+    height: 50px;
+    margin-left: 10px;
+    margin-right: 10px;
+    display: block;
+}
+
+label {
+    display: block;
+}
+
+textarea {
+    width: 100%;
+    height: 100%;
+}
+
+.blog-title {
+    color: #FFFFFF;
+}
+
+.blog-title:hover {
+    color: #0000FF;
+}
+
+.commenting-area {
+    margin-top: 60px;
+}
+
+#form button {
+    display: block;
+}
+
+#import-form input {
+    display: block;
+}
+
+.newer-post {
+    float: right;
+}
+
+a {
+    color: #0DF;
+}
+
+a:hover {
+    color: #FFF;
+}
+
+#grouper ul {
+    list-style: none;
+}
+
+.title-list li {
+    margin: 10px;
+}
+
+header {
+    font-size: 3em;
+    color: #00CC00;
+    display: table;
+    margin: 0 auto;
+    max-widht: 70%;
+}
+
+.version-container {
+    float: right;
+    position: relative;
+    right: 0px;
+}
+
+.version-container a {
+    padding-right: 5px;
+}
+
+.big_font {
+    font-size: 2em;
+}
+
+.media-row {
+    padding-bottom: 10%;
+}
+
+.murja-button
+{
+    border: 5px solid #00CC00;
+    margin: 5px;
+    border-radius: 15px;
+
+}
+
+#new-tag-btn
+{
+    display: block;
+}
+
+
+@media only screen and (max-device-width:480px)
+{
+    body {
+	font-size: 3em;
+    }
+}
diff --git a/resources/js/murja-helper.js b/resources/js/murja-helper.js
deleted file mode 120000
index 18d439d..0000000
--- a/resources/js/murja-helper.js
+++ /dev/null
@@ -1 +0,0 @@
-../../old-murja/murja/resources/public/js/murja-helper.js
\ No newline at end of file
diff --git a/resources/js/murja-helper.js b/resources/js/murja-helper.js
new file mode 100644
index 0000000..5b9660d
--- /dev/null
+++ b/resources/js/murja-helper.js
@@ -0,0 +1,62 @@
+var app = Elm.Main.init({
+    node: document.getElementById("app")
+});
+app.ports.alert.subscribe( (prompt) => {
+    window.alert(prompt);
+});
+
+app.ports.prompt.subscribe( (prompt) => {
+    let value = window.prompt(prompt);
+    app.ports.tags.send(value);
+});
+
+app.ports.reallySetupAce.subscribe( (content) => {
+    let editor = ace.edit("editor-post-content");
+
+    if(!editor) {
+	alert("Didn't find ace");
+	return;
+    }
+
+    editor.setKeyboardHandler("ace/keyboard/emacs");
+    editor.session.setValue(content);
+    editor.on('change', event => {
+     	let value = editor.getSession().getValue();
+	     app.ports.aceStateUpdate.send(value);
+    });
+});
+
+app.ports.addImgToAce.subscribe(img_id => {
+    let editor = ace.edit("editor-post-content");
+
+    if (editor) {
+	editor.insert('<img src="/api/pictures/' + img_id +'" />');
+
+    } else alert("Didn't find ace editor");
+})
+
+Object.defineProperty(HTMLElement.prototype, "dangerouslySetInnerHTML", {
+    get () {
+        return this.innerHTML
+    },
+    set (value) {
+        this.innerHTML = value
+    }
+})
+
+app.ports.savePostToLocalStorage.subscribe( v => {
+    localStorage.setItem("post", v)
+});
+
+app.ports.loadPostFromLocalStorage.subscribe( () => {
+    const post = localStorage.getItem("post");
+    if (post) 
+	app.ports.fromLocalStorage.send(post);
+});
+
+app.ports.clearPostFromLS.subscribe( () => {
+    if (window.confirm("Are you sure to clear the editor?")) {
+	localStorage.removeItem("post");
+	location.reload();
+    }
+});
diff --git a/resources/js/murja.js b/resources/js/murja.js
index 68c2e15..027acb5 120000
--- a/resources/js/murja.js
+++ b/resources/js/murja.js
@@ -1 +1 @@
-../../old-murja/elm-frontti/elm.js
\ No newline at end of file
+../../elm-frontti/elm.js
\ No newline at end of file
diff --git a/src/main.lisp b/src/main.lisp
index 3f0cb5b..d9c88d3 100644
--- a/src/main.lisp
+++ b/src/main.lisp
@@ -16,4 +16,4 @@
     (format t "Started murja server on ~a ~%" port)
     server))
 
-;;(start-server :port 3010)
+(start-server :port 3010)