module Main exposing (..) import Http import Json.Decode exposing (Decoder, field, string, map2, map3, map4, map5) import Browser import Browser.Navigation exposing (..) import Url import Html exposing (..) import Html.Attributes exposing (..) import Html.Events exposing (onInput, onClick) import Url.Parser as P exposing (()) import Http exposing (multipartBody) import Http exposing (stringPart) main : Program () Model Msg main = Browser.application { init = init, subscriptions = (\_ -> Sub.none), update = update, view = view, onUrlRequest = LinkClinked, onUrlChange = UrlChanged } type alias Category = { id: String, name: String } type alias Post = { id: String, category: Category, title: String, subtext: String, content: String } type Route = Home | CategoriesView | CategoryView String | PostView String | LoginView | LogoutView | CreatePostView | EditPostView routeParser : P.Parser (Route -> a) a routeParser = P.oneOf [ P.map Home P.top , P.map CategoriesView (P.s "categories") , P.map CategoryView (P.s "category" P.string) , P.map PostView (P.s "post" P.string) , P.map LoginView (P.s "login") , P.map LogoutView (P.s "logout") , P.map CreatePostView (P.s "create") ] processPostListing : Decoder (List Post) processPostListing = Json.Decode.list ( map5(\a->\b->\c->\d->\e-> Post a (Category b c) d e "") (field "id" string) (field "catid" string) (field "category" string) (field "title" string) (field "subtext" string) ) processCategoryListing : Decoder (List Category) processCategoryListing = Json.Decode.list (map2 Category (field "id" string) (field "name" string)) processPost : Decoder {title: String, content: String, subtext: String, category: Category} processPost = map5 (\a -> \b -> \c -> \d -> \e -> {title=a,content=b,subtext=e,category=(Category c d)}) (field "title" string) (field "content" string) (field "catid" string) (field "category" string) (field "subtext" string) processCategory : Decoder (String, List Post) processCategory = map2 (\a -> \b -> (a,b)) (field "category" string) (field "posts" processPostListing) processLogout : Decoder (Bool) processLogout = Json.Decode.map (\a -> case a of "true" -> True _ -> False) (field "success" string) processDeletePost : Decoder (Bool) processDeletePost = Json.Decode.map (\a -> case a of "true" -> True _ -> False) (field "success" string) processLogin : Decoder (Maybe User) processLogin = map2 (\a->\b->case a of "true" -> Just (User b) _ -> Nothing) (field "success" string) (field "username" string) processCreatePost : Decoder (Maybe String) processCreatePost = map2 (\a->\b->case a of "true" -> Just b _ -> Nothing) (field "success" string) (field "postid" string) processEditPost : Decoder (Maybe String) processEditPost = map2 (\a->\b->case a of "true" -> Just b _ -> Nothing) (field "success" string) (field "postid" string) processWhoami : Decoder (Maybe String) processWhoami = map2 (\a->\b->case a of "true" -> Just b _ -> Nothing) (field "success" string) (field "username" string) messageOfRoute : Maybe Route -> Cmd Msg messageOfRoute r = case r of Just route -> case route of Home -> Http.get {url = "/v1/posts", expect = Http.expectJson GotPosts processPostListing} CategoriesView -> Http.get {url = "/v1/categories", expect = Http.expectJson GotCategories processCategoryListing} PostView p -> Http.get {url = "/v1/post/" ++ p, expect = Http.expectJson GotPost processPost} CategoryView c -> Http.get {url = "/v1/category/" ++ c, expect = Http.expectJson GotCategory processCategory} _ -> Cmd.none Nothing -> Cmd.none type alias User = { username: String } type alias Model = { header: String, body: String, footer: String, pinnedPosts: List Post, posts: List Post, route: Maybe Route, key: Browser.Navigation.Key, errMessage: Maybe (String), username: String, password: String, user: Maybe User, post: Post } type Msg = LinkClinked Browser.UrlRequest | UrlChanged Url.Url | GotPosts (Result Http.Error (List Post)) | GotCategories (Result Http.Error (List Category)) | GotPost (Result Http.Error {title: String, content: String, subtext: String, category: Category}) | GotCategory (Result Http.Error (String, List Post)) | LoginResult (Result Http.Error (Maybe User)) | UsernameUpdate String | PasswordUpdate String | Login | Logout | LogoutResult (Result Http.Error Bool) | TitleUpdate String | ContentUpdate String | SubtextUpdate String | CategoryUpdate String | CreatePost | CreatePostResult (Result Http.Error (Maybe String)) | DeletePostResult (Result Http.Error Bool) | EditPostResult (Result Http.Error (Maybe String)) | GotWhoami (Result Http.Error (Maybe String)) | GotoEditPost | GotoDeletePost | EditPost | DeletePost init : () -> Url.Url -> Browser.Navigation.Key -> (Model, Cmd Msg) init _ url key = let r = P.parse routeParser url in ({ header = "", body = "", footer = "", pinnedPosts = [], posts = [], route = r, key = key, errMessage = Nothing, username = "", password = "", user = Nothing, post = Post "" (Category "" "") "" "" "" }, Http.get {url = "/v1/whoami", expect = Http.expectJson GotWhoami processWhoami} ) update : Msg -> Model -> (Model, Cmd Msg) update msg model = case msg of LinkClinked req -> case req of Browser.Internal url -> (model, Browser.Navigation.pushUrl model.key (Url.toString url)) Browser.External url -> (model, Browser.Navigation.load url) UrlChanged req -> ( let r = P.parse routeParser req in let imodel = case r of Just CreatePostView -> {model | post = Post "" (Category "" "") "" "" ""} Just (PostView p) -> let post = model.post in {model | post = {post | id = p}} _ -> model in ({imodel | route = r}, messageOfRoute r) ) GotPosts (Ok l) -> ({model | errMessage = Nothing, posts = l}, Cmd.none) GotPosts (Err _) -> ({model | errMessage = Just "Failed to load posts"}, Cmd.none) GotPost (Ok {title, content, subtext, category}) -> let post = model.post in ({model | post = {post | title = title, content = content, category = category, subtext = subtext}}, Cmd.none) GotCategory (Ok (title, posts)) -> ({model | errMessage = Nothing, header = title, posts = posts}, Cmd.none) Login -> (model, handleLogin model) LoginResult (Ok (Just _ as u)) -> ({model | user = u}, load "/") LoginResult (Ok (Nothing)) -> ({model | errMessage = Just "Invalid username or password."}, Cmd.none) Logout -> (model, Http.get {url = "/v1/logout", expect = Http.expectJson LogoutResult processLogout}) LogoutResult _ -> (model, load "/") UsernameUpdate u -> ({model | username = u}, Cmd.none) PasswordUpdate p -> ({model | password = p}, Cmd.none) TitleUpdate t -> let post = model.post in ({model | post = {post | title = t}}, Cmd.none) SubtextUpdate s -> let post = model.post in ({model | post = {post | subtext = s}}, Cmd.none) ContentUpdate c -> let post = model.post in ({model | post = {post | content = c}}, Cmd.none) CategoryUpdate c -> let post = model.post in let category = model.post.category in ({model | post = {post | category = {category | name = c}}}, Cmd.none) CreatePost -> (model, handleCreatePost model) CreatePostResult (Ok (Just postid)) -> (model, load ("/post/" ++ postid)) CreatePostResult (Ok Nothing) -> ({model | errMessage = Just "Failed to create post"}, Cmd.none) DeletePostResult (Ok True) -> (model, load "/") DeletePostResult (Ok False) -> ({model | errMessage = Just "Failed to delete post"}, Cmd.none) GotWhoami (Ok (Just u)) -> ({model | user = Just (User u)}, messageOfRoute model.route) GotWhoami _ -> (model, messageOfRoute model.route) GotoEditPost -> ({model | route = Just EditPostView}, Cmd.none) EditPost -> (model, handleEditPost model) EditPostResult (Ok (Just postid)) -> (model, load ("/post/" ++ postid)) DeletePost -> (model, handleDeletePost model) _ -> ({model | errMessage = Just "Something has gone horribly wrong."}, Cmd.none) handleLogin : Model -> Cmd Msg handleLogin model = Http.post { url = "/v1/login", body = multipartBody [stringPart "username" model.username, stringPart "password" model.password], expect = Http.expectJson LoginResult processLogin } handleCreatePost : Model -> Cmd Msg handleCreatePost model = case model.user of Nothing -> Cmd.none Just user -> Http.post { url = "/v1/createPost", body = multipartBody [ stringPart "username" user.username, stringPart "title" model.post.title, stringPart "subtext" model.post.subtext, stringPart "content" model.post.content, stringPart "category" model.post.category.name ], expect = Http.expectJson CreatePostResult processCreatePost } handleEditPost : Model -> Cmd Msg handleEditPost model = case model.user of Nothing -> Cmd.none Just user -> Http.post { url = "/v1/editPost", body = multipartBody [ stringPart "username" user.username, stringPart "id" model.post.id, stringPart "title" model.post.title, stringPart "subtext" model.post.subtext, stringPart "content" model.post.content, stringPart "category" model.post.category.name ], expect = Http.expectJson EditPostResult processEditPost } handleDeletePost : Model -> Cmd Msg handleDeletePost model = case model.user of Nothing -> Cmd.none Just user -> Http.post { url = "/v1/deletePost", body = multipartBody [ stringPart "username" user.username, stringPart "id" model.post.id ], expect = Http.expectJson DeletePostResult processDeletePost } type alias Document msg = { title: String, body: List (Html msg) } view : Model -> Document Msg view model = {title = "XNOEBLOG", body = [htmlView model]} htmlView : Model -> Html Msg htmlView model = div [ style "flex-direction" "column", style "margin" "0", style "padding" "0", style "font-family" "sans-serif" ] [ div [ style "border-bottom" "2px solid black", style "height" "1.5em", style "padding" "5px" ] ([ xa [href "/"] [text "Home"], text " - " ] ++ case model.user of Nothing -> [xa [href "/login"] [text "Login"]] Just u -> [xa [onClick Logout] [text "Logout"], text " - ", xa [href "/create"] [text "Create Post"]] ), renderModel model ] renderModel : Model -> Html Msg renderModel model = case (model.errMessage) of Just e -> h1 [] [text (e)] Nothing -> case model.route of Just route -> case route of Home -> div [style "padding" "5px"] [h1 [] [text "Welcome to my blog."],cardListing model] PostView _ -> div [] ([h1 [] [text (model.post.title)], p [] [text(model.post.content)], h3 [] [text(model.post.category.name ++ " ")]] ++ case model.user of Nothing -> [] Just u -> [xa [href "", onClick DeletePost] [text "Delete Post"], text " - ", xa [href "", onClick GotoEditPost] [text "Edit Post"]]) CategoryView _ -> div [style "padding" "5px"] [h1 [] [text (model.header)], cardListing model] LoginView -> loginForm model CreatePostView -> createPostForm model CreatePost "Create Post" EditPostView -> createPostForm model EditPost "Edit Post" _ -> h1 [] [ text ("Not found."), xa [ href "/"] [text("Return home")] ] _ -> h1 [] [ text ("Not found."), xa [ href "/"] [text("Return home")] ] cardListing : Model -> Html Msg cardListing model = div [ style "display" "grid", style "grid-template-columns" "repeat(auto-fill, minmax(16rem, 1fr))" ] (List.map renderPost model.posts) renderPost : Post -> Html Msg renderPost post = div [ style "max-width" "32rem", style "height" "auto", style "box-sizing" "border-box", style "padding" "15px" ] [ div [ style "flex-direction" "column", style "border-radius" "5px", style "display" "flex", style "height" "100%", style "border" "2px solid black", style "padding" "5px" ] [ header [ style "min-height" "75px", style "display" "flex", style "flex-direction" "column", style "align-items" "center", style "box-shadow" "none", style "box-sizing" "border-box", style "font-size" "20px", style "font-weight" "bold", style "border-radius" "10px 10px 0 0" ] [ xa [style "text-align" "center", href ("/post/" ++ post.id)] [text (post.title)] ], div [ style "flex-grow" "1", style "padding" "10px" ] [ text (post.subtext) ], footer [ style "min-height" "50px", style "display" "flex", style "flex-direction" "row", style "justify-content" "space-around", style "align-items" "center", style "box-shadow" "none", style "box-sizing" "border-box", style "padding" "10px", style "border-radius" "0 0 10px 10px" ] [ text ("Category: "), xa [href ("/category/" ++ post.category.id)] [text (post.category.name)] ] ] ] createPostForm : Model -> Msg -> String -> Html Msg createPostForm model msg buttonText = div [ style "display" "flex", style "flex-direction" "column" ] [ viewInput "text" "title" "Title" model.post.title TitleUpdate, viewInput "text" "subtext" "Subtext" model.post.subtext SubtextUpdate, viewTextarea "content" "Content" model.post.content ContentUpdate, viewInput "text" "category" "Blog Post" model.post.category.name CategoryUpdate, button [onClick msg] [text buttonText] ] loginForm : Model -> Html Msg loginForm model = div [ style "display" "flex", style "flex-direction" "column" ] [ viewInput "text" "username" "Username" model.username UsernameUpdate, viewInput "password" "password" "Password" model.password PasswordUpdate, button [onClick Login] [text "Login"] ] viewInput : String -> String -> String -> String -> (String -> msg) -> Html msg viewInput t n p v toMsg = input [ name n, type_ t, placeholder p, value v, onInput toMsg ] [] viewTextarea : String -> String -> String -> (String -> msg) -> Html msg viewTextarea n p v toMsg = textarea [ name n, placeholder p, value v, onInput toMsg ] [] xa : List (Attribute msg) -> List (Html msg) -> Html msg xa x y = a (x ++ [ style "text-decoration" "none", style "color" "#aa66ff" ]) y