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)