Update blog example.

This commit is contained in:
Dillon Kearns 2023-02-04 08:19:06 -08:00
parent 219e4b3a12
commit 48b92722e5
4 changed files with 544 additions and 18 deletions

View File

@ -196,6 +196,11 @@ action routeParams =
[ ( "slug", Encode.string okForm.slug )
, ( "title", Encode.string okForm.title )
, ( "body", Encode.string okForm.body )
, ( "publish"
, okForm.publish
|> Maybe.map (Date.toIsoString >> Encode.string)
|> Maybe.withDefault Encode.null
)
]
)
(Decode.oneOf
@ -208,7 +213,7 @@ action routeParams =
(\result ->
case result of
Ok () ->
Route.redirectTo Route.Index
Route.redirectTo (Route.Posts__Slug___Edit { slug = okForm.slug })
Err errorMessage ->
Server.Response.render { errors = formResponse, errorMessage = Just errorMessage }

View File

@ -0,0 +1,187 @@
module Route.Posts.Slug_ exposing (ActionData, Data, route, RouteParams, Msg, Model)
{-|
@docs ActionData, Data, route, RouteParams, Msg, Model
-}
import BackendTask
import BackendTask.Custom
import Date exposing (Date)
import Effect
import ErrorPage
import FatalError
import Head
import Html
import Json.Decode as Decode exposing (Decoder)
import Json.Encode as Encode
import Markdown.Block exposing (Block)
import Markdown.Parser
import Markdown.Renderer
import Pages.Msg
import Pages.PageUrl
import Path
import Platform.Sub
import Route
import RouteBuilder
import Server.Request
import Server.Response
import Shared
import Time
import View
type alias Model =
{}
type Msg
= NoOp
type alias RouteParams =
{ slug : String }
route : RouteBuilder.StatefulRoute RouteParams Data ActionData Model Msg
route =
RouteBuilder.buildWithLocalState
{ view = view
, init = init
, update = update
, subscriptions = subscriptions
}
(RouteBuilder.serverRender { data = data, action = action, head = head })
init :
Maybe Pages.PageUrl.PageUrl
-> Shared.Model
-> RouteBuilder.StaticPayload Data ActionData RouteParams
-> ( Model, Effect.Effect Msg )
init pageUrl sharedModel app =
( {}, Effect.none )
update :
Pages.PageUrl.PageUrl
-> Shared.Model
-> RouteBuilder.StaticPayload Data ActionData RouteParams
-> Msg
-> Model
-> ( Model, Effect.Effect msg )
update pageUrl sharedModel app msg model =
case msg of
NoOp ->
( model, Effect.none )
subscriptions :
Maybe Pages.PageUrl.PageUrl
-> RouteParams
-> Path.Path
-> Shared.Model
-> Model
-> Sub Msg
subscriptions maybePageUrl routeParams path sharedModel model =
Platform.Sub.none
type alias Data =
{ body : List Block
}
type alias ActionData =
{}
data :
RouteParams
-> Server.Request.Parser (BackendTask.BackendTask FatalError.FatalError (Server.Response.Response Data ErrorPage.ErrorPage))
data routeParams =
Server.Request.succeed
(BackendTask.Custom.run "getPost"
(Encode.string routeParams.slug)
(Decode.nullable postDecoder)
|> BackendTask.allowFatal
|> BackendTask.andThen
(\maybePost ->
case maybePost of
Just post ->
let
parsed : Result String (List Block)
parsed =
post.body
|> Markdown.Parser.parse
|> Result.mapError (\_ -> "Invalid markdown.")
in
parsed
|> Result.mapError FatalError.fromString
|> Result.map
(\parsedMarkdown ->
Server.Response.render
{ body = parsedMarkdown
}
)
|> BackendTask.fromResult
Nothing ->
Route.redirectTo Route.Index
|> BackendTask.succeed
)
)
type alias Post =
{ title : String
, body : String
, slug : String
, publish : Maybe Date
}
postDecoder : Decoder Post
postDecoder =
Decode.map4 Post
(Decode.field "title" Decode.string)
(Decode.field "body" Decode.string)
(Decode.field "slug" Decode.string)
(Decode.field "publish"
(Decode.nullable
(Decode.int |> Decode.map (Time.millisToPosix >> Date.fromPosix Time.utc))
)
)
|> Decode.map (Debug.log "postDecoder")
head : RouteBuilder.StaticPayload Data ActionData RouteParams -> List Head.Tag
head app =
[]
view :
Maybe Pages.PageUrl.PageUrl
-> Shared.Model
-> Model
-> RouteBuilder.StaticPayload Data ActionData RouteParams
-> View.View (Pages.Msg.Msg Msg)
view maybeUrl sharedModel model app =
{ title = "Posts.Slug_"
, body =
[ Html.text "Here is your generated page!!!"
, Html.div []
(app.data.body
|> Markdown.Renderer.render Markdown.Renderer.defaultHtmlRenderer
|> Result.withDefault []
)
]
}
action :
RouteParams
-> Server.Request.Parser (BackendTask.BackendTask FatalError.FatalError (Server.Response.Response ActionData ErrorPage.ErrorPage))
action routeParams =
Server.Request.succeed (BackendTask.succeed (Server.Response.render {}))

View File

@ -0,0 +1,298 @@
module Route.Posts.Slug_.Edit exposing (ActionData, Data, route, RouteParams, Msg, Model)
{-|
@docs ActionData, Data, route, RouteParams, Msg, Model
-}
import BackendTask
import BackendTask.Custom
import Date exposing (Date)
import Debug
import Effect
import ErrorPage
import FatalError
import Form
import Form.Field
import Form.FieldView
import Form.Validation
import Form.Value
import Head
import Html
import Html.Attributes
import Json.Decode as Decode exposing (Decoder)
import Json.Encode as Encode
import Pages.Msg
import Pages.PageUrl
import Pages.Script
import Path
import Platform.Sub
import Route
import RouteBuilder
import Server.Request
import Server.Response
import Shared
import Time
import View
type alias Model =
{}
type Msg
= NoOp
type alias RouteParams =
{ slug : String }
route : RouteBuilder.StatefulRoute RouteParams Data ActionData Model Msg
route =
RouteBuilder.buildWithLocalState
{ view = view
, init = init
, update = update
, subscriptions = subscriptions
}
(RouteBuilder.serverRender { data = data, action = action, head = head })
init :
Maybe Pages.PageUrl.PageUrl
-> Shared.Model
-> RouteBuilder.StaticPayload Data ActionData RouteParams
-> ( Model, Effect.Effect Msg )
init pageUrl sharedModel app =
( {}, Effect.none )
update :
Pages.PageUrl.PageUrl
-> Shared.Model
-> RouteBuilder.StaticPayload Data ActionData RouteParams
-> Msg
-> Model
-> ( Model, Effect.Effect msg )
update pageUrl sharedModel app msg model =
case msg of
NoOp ->
( model, Effect.none )
subscriptions :
Maybe Pages.PageUrl.PageUrl
-> RouteParams
-> Path.Path
-> Shared.Model
-> Model
-> Sub Msg
subscriptions maybePageUrl routeParams path sharedModel model =
Platform.Sub.none
type alias Data =
{ post : Post
}
type alias ActionData =
{ errors : Form.Response String }
type alias Post =
{ title : String
, body : String
, slug : String
, publish : Maybe Date
}
postDecoder : Decoder Post
postDecoder =
Decode.map4 Post
(Decode.field "title" Decode.string)
(Decode.field "body" Decode.string)
(Decode.field "slug" Decode.string)
(Decode.field "publish"
(Decode.nullable
(Decode.int |> Decode.map (Time.millisToPosix >> Date.fromPosix Time.utc))
)
)
|> Decode.map (Debug.log "postDecoder")
data :
RouteParams
-> Server.Request.Parser (BackendTask.BackendTask FatalError.FatalError (Server.Response.Response Data ErrorPage.ErrorPage))
data routeParams =
Server.Request.succeed
(BackendTask.Custom.run "getPost"
(Encode.string routeParams.slug)
(Decode.nullable postDecoder)
|> BackendTask.allowFatal
|> BackendTask.map
(\maybePost ->
case maybePost of
Just post ->
Server.Response.render
{ post = post
}
Nothing ->
Route.redirectTo Route.Index
)
)
head : RouteBuilder.StaticPayload Data ActionData RouteParams -> List Head.Tag
head app =
[]
view :
Maybe Pages.PageUrl.PageUrl
-> Shared.Model
-> Model
-> RouteBuilder.StaticPayload Data ActionData RouteParams
-> View.View (Pages.Msg.Msg Msg)
view maybeUrl sharedModel model app =
{ title = "Posts.Slug_.Edit"
, body =
[ Html.h2 [] [ Html.text "Form" ]
, Form.renderHtml
[]
(\renderStyledHtmlUnpack -> Just renderStyledHtmlUnpack.errors)
app
app.data.post
(Form.toDynamicTransition "form" form)
]
}
action :
RouteParams
-> Server.Request.Parser (BackendTask.BackendTask FatalError.FatalError (Server.Response.Response ActionData ErrorPage.ErrorPage))
action routeParams =
Server.Request.map
(\( formResponse, parsedForm ) ->
case parsedForm of
Ok okForm ->
BackendTask.Custom.run "updatePost"
(Encode.object
[ ( "slug", Encode.string okForm.slug )
, ( "title", Encode.string okForm.title )
, ( "body", Encode.string okForm.body )
, ( "publish"
, okForm.publish
|> Maybe.map (Date.toIsoString >> Encode.string)
|> Maybe.withDefault Encode.null
)
]
)
(Decode.succeed ())
|> BackendTask.allowFatal
|> BackendTask.map
(\() ->
Server.Response.render
{ errors = formResponse }
)
Err invalidForm ->
BackendTask.map
(\parsed ->
Server.Response.render
{ errors = formResponse }
)
(Pages.Script.log
(Debug.toString parsedForm)
)
)
(Server.Request.formData (Form.initCombined Basics.identity form))
form : Form.DoneForm String ParsedForm Post (List (Html.Html (Pages.Msg.Msg Msg)))
form =
(\title slug body publish ->
{ combine =
ParsedForm
|> Form.Validation.succeed
|> Form.Validation.andMap title
|> Form.Validation.andMap slug
|> Form.Validation.andMap body
|> Form.Validation.andMap publish
, view =
\formState ->
let
fieldView label field =
Html.div []
[ Html.label []
[ Html.text (label ++ " ")
, Form.FieldView.input [] field
, errorsView formState.errors field
]
]
in
[ fieldView "title" title
, fieldView "slug" slug
, fieldView "body" body
, fieldView "publish" publish
, if formState.isTransitioning then
Html.button [ Html.Attributes.disabled True ]
[ Html.text "Submitting..." ]
else
Html.button []
[ Html.text "Submit" ]
]
}
)
|> Form.init
|> Form.field "title"
(Form.Field.required "Required" Form.Field.text
|> Form.Field.withInitialValue (.title >> Form.Value.string)
)
|> Form.field "slug"
(Form.Field.required "Required" Form.Field.text
|> Form.Field.withInitialValue (.slug >> Form.Value.string)
)
|> Form.field "body"
(Form.Field.required "Required" Form.Field.text
|> Form.Field.textarea { rows = Just 30, cols = Just 80 }
|> Form.Field.withInitialValue (.body >> Form.Value.string)
)
|> Form.field "publish"
(Form.Field.date { invalid = \dateUnpack -> "" }
|> Form.Field.withOptionalInitialValue
(.publish >> Maybe.map Form.Value.date)
)
type alias ParsedForm =
{ title : String, slug : String, body : String, publish : Maybe Date }
errorsView :
Form.Errors String
-> Form.Validation.Field String parsed kind
-> Html.Html (Pages.Msg.Msg Msg)
errorsView errors field =
if List.isEmpty (Form.errorsForField field errors) then
Html.div [] []
else
Html.div
[]
[ Html.ul
[]
(List.map
(\error ->
Html.li
[ Html.Attributes.style "color" "red" ]
[ Html.text error ]
)
(Form.errorsForField field errors)
)
]

View File

@ -3,13 +3,14 @@ import { PrismaClientKnownRequestError } from "@prisma/client/runtime/index.js";
const prisma = new PrismaClient();
export async function createPost({ slug, title, body }) {
export async function createPost({ slug, title, body, publish }) {
try {
await prisma.post.create({
data: {
slug,
title,
body,
publish,
},
});
} catch (e) {
@ -27,23 +28,58 @@ export async function createPost({ slug, title, body }) {
}
}
export async function posts() {
return (await prisma.post.findMany()).map(transformDates);
}
export async function updatePost({ slug, title, body, publish }) {
try {
const data = {
slug,
title,
body,
publish: new Date(publish),
};
await prisma.post.upsert({
where: {
slug,
},
create: data,
update: data,
});
return null;
} catch (e) {
if (e instanceof PrismaClientKnownRequestError) {
// https://www.prisma.io/docs/reference/api-reference/error-reference
console.log("MESSAGE:", e.message, e.meta, e.code, e.name);
console.dir(e);
function transformDates(item) {
return Object.fromEntries(
Object.entries(item).map(([key, value]) => [key, transformValue(value)])
);
}
return { errorMessage: e.message };
function transformValue(value) {
// if (typeof value === "bigint") {
// obj[key] = toNumber(value);
// }
if (value instanceof Date) {
return value.getMilliseconds();
} else {
return value;
// specific error
} else {
console.trace(e);
throw e;
}
}
}
export async function getPost(slug) {
try {
return await prisma.post.findFirst({
where: {
slug,
},
select: {
body: true,
title: true,
slug: true,
publish: true,
},
});
} catch (e) {
console.log("ERROR");
console.trace(e);
return null;
}
}
export async function posts() {
return await prisma.post.findMany();
}