elm-pages-v3-beta/examples/pokedex/app/Route/TailwindForm.elm

1149 lines
34 KiB
Elm
Raw Normal View History

2022-05-03 21:51:32 +03:00
module Route.TailwindForm exposing (ActionData, Data, Model, Msg, route)
import Browser.Dom
import Css exposing (Color)
import Css.Global
import DataSource exposing (DataSource)
2022-01-04 08:17:07 +03:00
import Date exposing (Date)
import Dict exposing (Dict)
2022-03-29 01:11:07 +03:00
import Effect exposing (Effect)
2022-03-29 21:48:04 +03:00
import ErrorPage exposing (ErrorPage)
2022-07-09 03:56:52 +03:00
import Form.Field as Field
2022-07-09 03:51:26 +03:00
import Form.Validation as Validation
import Form.Value
import Head
import Head.Seo as Seo
import Html.Styled as Html exposing (Html)
import Html.Styled.Attributes as Attr exposing (css)
import Http
import Icon
2022-07-09 03:40:01 +03:00
import Pages.FieldView
import Pages.Form as Form exposing (Form)
import Pages.FormState
import Pages.Msg
import Pages.PageUrl exposing (PageUrl)
import Pages.Url
2022-03-05 20:50:01 +03:00
import RouteBuilder exposing (StatefulRoute, StatelessRoute, StaticPayload)
import Server.Request as Request exposing (Parser)
import Server.Response as Response exposing (Response)
import Shared
import Tailwind.Breakpoints as Bp
import Tailwind.Utilities as Tw
import Task
2022-01-04 08:17:07 +03:00
import Time
2022-04-28 20:47:11 +03:00
import Url exposing (Url)
import View exposing (View)
type alias Model =
{}
2022-01-06 02:12:58 +03:00
type Msg
= MovedToTop
type alias RouteParams =
{}
type alias User =
{ first : String
, last : String
, username : String
, email : String
2022-01-04 08:17:07 +03:00
, birthDay : Date
, checkIn : Date
, checkOut : Date
2022-01-05 22:40:35 +03:00
, rating : Int
, password : ( String, String )
, notificationPreferences : NotificationPreferences
}
type alias NotificationPreferences =
{ comments : Bool
, candidates : Bool
, offers : Bool
, pushNotificationsSetting : PushNotificationsSetting
}
defaultUser : User
defaultUser =
{ first = "jane"
, last = "Doe"
, username = "janedoe"
, email = "janedoe@example.com"
2022-01-04 08:17:07 +03:00
, birthDay = Date.fromCalendarDate 1969 Time.Jul 20
, checkIn = Date.fromCalendarDate 2022 Time.Jan 11
, checkOut = Date.fromCalendarDate 2022 Time.Jan 12
2022-01-05 22:40:35 +03:00
, rating = 5
, password = ( "", "" )
, notificationPreferences =
{ comments = False
, candidates = False
, offers = False
, pushNotificationsSetting = PushNone
}
}
styleAttrs attrs =
List.map Attr.fromUnstyled attrs
usernameInput formState field =
Html.div []
[ Html.div
[ css
[ Bp.sm
[ Tw.grid
, Tw.grid_cols_3
, Tw.gap_4
, Tw.items_start
, Tw.border_t
, Tw.border_gray_200
, Tw.pt_5
]
]
]
[ Html.label
[ Attr.for "username"
, css
[ Tw.block
, Tw.text_sm
, Tw.font_medium
, Tw.text_gray_700
, Bp.sm
[ Tw.mt_px
, Tw.pt_2
]
]
]
[ Html.text "Username" ]
, Html.div
[ css
[ Tw.mt_1
, Bp.sm
[ Tw.mt_0
, Tw.col_span_2
]
]
]
[ Html.div
[ css
[ Tw.max_w_lg
, Tw.flex
, Tw.rounded_md
, Tw.shadow_sm
, Tw.relative
]
]
[ Html.span
[ css
[ Tw.inline_flex
, Tw.items_center
, Tw.px_3
, Tw.rounded_l_md
, Tw.border
, Tw.border_r_0
, Tw.border_gray_300
, Tw.bg_gray_50
, Tw.text_gray_500
, Bp.sm
[ Tw.text_sm
]
]
]
[ Html.text "workcation.com/" ]
2022-07-09 03:40:01 +03:00
, Pages.FieldView.inputStyled
[ Attr.type_ "text"
, Attr.name "username"
, Attr.id "username"
, Attr.attribute "autocomplete" "username"
, css
[ Tw.flex_1
, Tw.block
, Tw.w_full
, Tw.min_w_0
, Tw.rounded_none
, Tw.rounded_r_md
, Tw.border_gray_300
, Css.focus
[ Tw.ring_indigo_500
, Tw.border_indigo_500
]
, Bp.sm
[ Tw.text_sm
]
]
]
field
, Html.div
[ css
[ Tw.absolute
, Tw.inset_y_0
, Tw.right_0
, Tw.pr_3
, Tw.flex
, Tw.items_center
, Tw.pointer_events_none
]
]
[ if formState.errors |> Form.errorsForField field |> List.isEmpty then
Html.text ""
else
Icon.error
]
]
]
]
, errorsView formState field
]
validateCapitalized : String -> ( Maybe String, List String )
validateCapitalized string =
if string |> String.toList |> List.head |> Maybe.withDefault 'a' |> Char.isUpper then
( Just string, [] )
else
( Nothing, [ "Needs to be capitalized" ] )
form : Form.StyledHtmlForm String User data msg
form =
Form.init
(\first last username email dob checkin checkout rating password passwordConfirmation comments candidates offers pushNotifications acceptTerms ->
Validation.succeed User
|> Validation.andMap first
|> Validation.andMap last
|> Validation.andMap username
|> Validation.andMap email
|> Validation.andMap dob
|> Validation.andMap checkin
|> Validation.andMap checkout
|> Validation.andMap rating
|> Validation.andMap
(Validation.map2
(\passwordValue passwordConfirmationValue ->
if passwordValue == passwordConfirmationValue then
Validation.succeed ( passwordValue, passwordConfirmationValue )
else
Validation.fail passwordConfirmation "Must match password"
)
password
passwordConfirmation
|> Validation.andThen identity
)
|> Validation.andMap
(Validation.succeed NotificationPreferences
|> Validation.andMap comments
|> Validation.andMap candidates
|> Validation.andMap offers
|> Validation.andMap pushNotifications
)
|> Validation.andThen
(\validated ->
if Date.toRataDie validated.checkIn >= Date.toRataDie validated.checkOut then
Validation.succeed validated |> Validation.withError checkin "Must be before checkout"
else
Validation.succeed validated
)
)
(\formState first last username email dob checkin checkout rating password passwordConfirmation comments candidates offers pushNotifications acceptTerms ->
let
fieldView labelText field =
textInput formState labelText field
in
[ wrapSection
[ fieldView "First name" first
, fieldView "Last name" last
, usernameInput formState username
, fieldView "Email" email
, fieldView "Date of Birth" dob
, fieldView "Check-in" checkin
, fieldView "Check-out" checkout
, fieldView "Rating" rating
]
, fieldView "Password" password
, fieldView "Password Confirmation" passwordConfirmation
, wrapEmailSection
[ checkboxInput { name = "Comments", description = "Get notified when someones posts a comment on a posting." } formState comments
, checkboxInput { name = "Candidates", description = "Get notified when a candidate applies for a job." } formState candidates
, checkboxInput { name = "Offers", description = "Get notified when a candidate accepts or rejects an offer." } formState offers
]
, wrapNotificationsSections
[ wrapPushNotificationsSection formState
pushNotifications
2022-07-09 03:40:01 +03:00
[ Pages.FieldView.radioStyled
[ css
[ Tw.mt_4
, Tw.space_y_4
]
]
(radioInput [])
pushNotifications
]
]
, checkboxInput { name = "Accept terms", description = "Please read the terms before proceeding." } formState acceptTerms
, Html.div
[ css
[ Tw.pt_5
]
]
[ Html.div
[ css
[ Tw.flex
, Tw.justify_end
]
]
[ cancelButton
, saveButton False []
]
]
]
)
|> Form.field "first"
(Field.text
|> Field.required "Required"
|> Field.withInitialValue (always defaultUser.first >> Form.Value.string)
|> Field.withClientValidation validateCapitalized
)
|> Form.field "last"
(Field.text
|> Field.required "Required"
|> Field.withInitialValue (always defaultUser.last >> Form.Value.string)
|> Field.withClientValidation validateCapitalized
)
|> Form.field "username"
(Field.text
|> Field.withInitialValue (always defaultUser.username >> Form.Value.string)
|> Field.required "Required"
|> Field.withClientValidation
(\username ->
( Just username
, if username |> String.contains "@" then
[ "Cannot contain @ symbol" ]
else
[]
)
)
|> Field.withClientValidation
(\username ->
( Just username
, if username |> String.contains "#" then
[ "Cannot contain # symbol" ]
else
[]
)
)
|> Field.withClientValidation
(\username ->
( Just username
, if (username |> String.length) < 3 then
[ "Must be at least 3 characters long" ]
else
[]
)
)
|> Field.withServerValidation
(\username ->
if username == "asdf" then
DataSource.succeed [ "username is taken" ]
else
DataSource.succeed []
)
)
|> Form.field "email"
(Field.text
|> Field.withInitialValue (always defaultUser.email >> Form.Value.string)
|> Field.email
|> Field.required "Required"
)
|> Form.field "dob"
(Field.date
{ invalid = \_ -> "Invalid date" }
|> Field.required "Required"
|> Field.withMin (Date.fromCalendarDate 1900 Time.Jan 1 |> Form.Value.date) "Choose a later date"
|> Field.withMax (Date.fromCalendarDate 2022 Time.Jan 1 |> Form.Value.date) "Choose an earlier date"
|> Field.withInitialValue (always defaultUser.birthDay >> Form.Value.date)
|> Field.withServerValidation
2022-01-04 08:17:07 +03:00
(\birthDate ->
2022-01-05 03:20:44 +03:00
if birthDate == Date.fromCalendarDate 1969 Time.Jul 20 then
2022-01-04 08:17:07 +03:00
DataSource.succeed [ "No way, that's when the moon landing happened!" ]
else
DataSource.succeed []
)
)
|> Form.field "checkin"
(Field.date
{ invalid = \_ -> "Invalid date" }
|> Field.required "Required"
|> Field.withInitialValue (always defaultUser.checkIn >> Form.Value.date)
)
|> Form.field "checkout"
(Field.date
{ invalid = \_ -> "Invalid date" }
|> Field.required "Required"
|> Field.withInitialValue (always defaultUser.checkOut >> Form.Value.date)
)
|> Form.field "rating"
(Field.int { invalid = \_ -> "Invalid number" }
|> Field.range
{ missing = "Required"
, invalid = \_ -> "Outside range"
, initial = \_ -> Form.Value.int 3
, min = Form.Value.int 1
, max = Form.Value.int 5
}
2022-01-05 22:40:35 +03:00
)
|> Form.field "password"
(Field.text |> Field.password |> Field.required "Required")
|> Form.field "password-confirmation"
(Field.text |> Field.password |> Field.required "Required")
|> Form.field "comments"
Field.checkbox
|> Form.field "candidates"
Field.checkbox
|> Form.field "offers"
Field.checkbox
|> Form.field
"push-notifications"
(Field.select
[ ( "PushAll", PushAll )
, ( "PushEmail", PushEmail )
, ( "PushNone", PushNone )
]
(\_ -> "Invalid option")
|> Field.required "Please select your notification preference."
)
|> Form.field "acceptTerms"
(Field.checkbox
|> Field.withClientValidation
(\checked ->
( Just ()
, if checked then
[]
else
[ "Please agree to terms to proceed." ]
)
2022-01-11 06:01:47 +03:00
)
)
2022-01-05 04:55:37 +03:00
type PushNotificationsSetting
= PushAll
| PushEmail
| PushNone
saveButton formHasErrors formAttrs =
2022-01-04 01:26:04 +03:00
Html.button
(styleAttrs formAttrs
++ [ css
[ Tw.ml_3
, Tw.inline_flex
, Tw.justify_center
, Tw.py_2
, Tw.px_4
, Tw.border
, Tw.border_transparent
, Tw.shadow_sm
, Tw.text_sm
, Tw.font_medium
, Tw.rounded_md
, Tw.text_white
, Tw.bg_indigo_600
, Css.focus
[ Tw.outline_none
, Tw.ring_2
, Tw.ring_offset_2
, Tw.ring_indigo_500
]
, --if formHasErrors then
-- Css.batch
-- [ Tw.text_gray_200
-- , Tw.bg_indigo_500
-- , Tw.cursor_default
-- ]
--
-- else
Css.hover
[ Tw.bg_indigo_700
]
2022-01-04 01:26:04 +03:00
]
]
)
[ Html.text "Save" ]
cancelButton : Html msg
cancelButton =
Html.button
[ Attr.type_ "button"
, css
[ Tw.bg_white
, Tw.py_2
, Tw.px_4
, Tw.border
, Tw.border_gray_300
, Tw.rounded_md
, Tw.shadow_sm
, Tw.text_sm
, Tw.font_medium
, Tw.text_gray_700
, Css.focus
[ Tw.outline_none
, Tw.ring_2
, Tw.ring_offset_2
, Tw.ring_indigo_500
]
, Css.hover
[ Tw.bg_gray_50
]
]
]
[ Html.text "Cancel" ]
2022-05-03 21:51:32 +03:00
route : StatefulRoute RouteParams Data ActionData Model Msg
route =
2022-03-05 20:50:01 +03:00
RouteBuilder.serverRender
{ head = head
, data = data
, action = action
}
2022-03-05 20:50:01 +03:00
|> RouteBuilder.buildWithLocalState
2022-01-06 02:12:58 +03:00
{ view = view
, update = update
, init = init
, subscriptions = \_ _ _ _ _ -> Sub.none
}
action : RouteParams -> Parser (DataSource (Response ActionData ErrorPage))
action routeParams =
Request.formParserResultNew2 [ form ]
|> Request.map
(\toDataSource ->
toDataSource
|> DataSource.andThen
(\result ->
case result of
Ok user ->
DataSource.succeed
{ user = user
, flashMessage =
Ok ("Successfully updated profile for user " ++ user.first ++ " " ++ user.last)
, formResponse = Nothing
}
|> DataSource.map Response.render
Err error ->
DataSource.succeed
{ flashMessage = Err "Got errors"
, user = defaultUser
, formResponse = Just error
}
|> DataSource.map Response.render
)
)
update : a -> b -> c -> Msg -> Model -> ( Model, Effect Msg )
update _ _ _ msg model =
2022-01-06 02:12:58 +03:00
case msg of
MovedToTop ->
2022-03-29 01:11:07 +03:00
( model, Effect.none )
2022-01-06 03:11:15 +03:00
init _ _ static =
( {}, Effect.none )
type alias Data =
{}
2022-05-03 21:51:32 +03:00
type alias ActionData =
{ user : User
, flashMessage : Result String String
, formResponse : Maybe { fields : List ( String, String ), errors : Dict String (List String) }
}
2022-05-03 21:51:32 +03:00
2022-03-29 21:48:04 +03:00
data : RouteParams -> Parser (DataSource (Response Data ErrorPage))
data routeParams =
Request.oneOf
[ {}
|> Response.render
|> DataSource.succeed
|> Request.succeed
]
head :
2022-05-03 21:51:32 +03:00
StaticPayload Data ActionData RouteParams
-> List Head.Tag
head static =
Seo.summary
{ canonicalUrlOverride = Nothing
, siteName = "elm-pages"
, image =
{ url = Pages.Url.external "TODO"
, alt = "elm-pages logo"
, dimensions = Nothing
, mimeType = Nothing
}
, description = "TODO"
, locale = Nothing
, title = "TODO title" -- metadata.title -- TODO
}
|> Seo.website
wrapSection : List (Html msg) -> Html msg
wrapSection children =
2022-01-03 22:55:45 +03:00
Html.div []
[ Html.div []
[ Html.h3
[ css
[ Tw.text_lg
, Tw.leading_6
, Tw.font_medium
, Tw.text_gray_900
]
]
[ Html.text "Profile" ]
, Html.p
[ css
[ Tw.mt_1
, Tw.max_w_2xl
, Tw.text_sm
, Tw.text_gray_500
]
]
[ Html.text "This information will be displayed publicly so be careful what you share." ]
]
, Html.div
[ css
[ Tw.mt_6
, Tw.space_y_6
, Bp.sm
[ Tw.mt_5
, Tw.space_y_5
]
]
]
children
2022-01-03 22:55:45 +03:00
]
2022-03-31 19:50:45 +03:00
--formModelView formModel =
-- formModel
-- |> Debug.toString
-- |> Html.text
-- |> List.singleton
-- |> Html.pre
-- [ Attr.style "white-space" "break-spaces"
-- ]
2022-01-06 02:12:58 +03:00
view :
Maybe PageUrl
-> Shared.Model
2022-01-06 02:12:58 +03:00
-> Model
2022-05-03 21:51:32 +03:00
-> StaticPayload Data ActionData RouteParams
-> View (Pages.Msg.Msg Msg)
2022-01-06 02:12:58 +03:00
view maybeUrl sharedModel model static =
let
user : User
user =
static.action
|> Maybe.map .user
|> Maybe.withDefault defaultUser
in
{ title = "Form Example"
, body =
[ Html.div []
[ Css.Global.global Tw.globalStyles
, static.action
|> Maybe.map .flashMessage
|> Maybe.map flashView
|> Maybe.withDefault (Html.p [] [])
, Html.p []
[ -- TODO should this be calling a function in Form and passing in the form, like `Form.isSubmitting form`?
if static.transition /= Nothing then
Html.text "Submitting..."
else
Html.text ""
]
, Html.div
2022-01-03 22:14:39 +03:00
[ css
[ Tw.flex
, Tw.flex_col
, Tw.items_center
, Tw.mt_8
, Tw.border_gray_700
, Tw.rounded_lg
]
2022-01-03 22:14:39 +03:00
]
[ Html.text
(static.action
|> Maybe.andThen .formResponse
|> Debug.toString
)
, form
|> Form.toDynamicTransition "test"
2022-07-08 19:59:59 +03:00
|> Form.renderStyledHtml []
(static.action
|> Maybe.andThen .formResponse
)
static
never
2022-01-03 22:14:39 +03:00
]
]
|> Html.toUnstyled
]
}
2022-01-03 22:14:39 +03:00
successColor : Color
successColor =
Css.rgb 163 251 163
errorColor : Color
errorColor =
Css.rgb 251 163 163
flashView : Result String String -> Html msg
flashView message =
Html.p
[ css
[ Css.backgroundColor
(case message of
Ok _ ->
successColor
Err _ ->
errorColor
)
, Tw.p_4
]
]
[ Html.text <|
case message of
Ok okMessage ->
okMessage
Err error ->
"Something went wrong: " ++ error
]
textInput info labelText field =
2022-01-03 22:14:39 +03:00
Html.div
[ css
[ Bp.sm
[ Tw.grid
, Tw.grid_cols_3
, Tw.gap_4
, Tw.items_start
, Tw.border_t
, Tw.border_gray_200
, Tw.pt_5
]
]
2022-01-03 22:14:39 +03:00
]
[ --Html.text (Debug.toString submitStatus),
Html.span
[ css
[ Tw.font_bold
]
]
[ Html.text (Pages.FormState.fieldStatusToString field.status)
]
, Html.label
2022-01-03 22:14:39 +03:00
([ css
[ Tw.block
, Tw.text_sm
, Tw.font_medium
, Tw.text_gray_700
, Bp.sm
[ Tw.mt_px
, Tw.pt_2
]
]
2022-01-03 22:14:39 +03:00
]
-- TODO need for="..." attribute on label
--++ styleAttrs toLabel
2022-01-03 22:14:39 +03:00
)
[ Html.text labelText ]
, Html.div
[ css
[ Tw.mt_1
, Bp.sm
[ Tw.mt_0
, Tw.col_span_2
]
2022-01-03 22:14:39 +03:00
]
]
[ field
2022-07-09 03:40:01 +03:00
|> Pages.FieldView.inputStyled
[ --Attr.attribute "autocomplete" "given-name",
css
[ Tw.max_w_lg
, Tw.block
, Tw.w_full
, Tw.shadow_sm
, Tw.border_gray_300
, Tw.rounded_md
, Css.focus
[ Tw.ring_indigo_500
, Tw.border_indigo_500
]
, Bp.sm
[ Tw.max_w_xs
, Tw.text_sm
]
]
]
2022-01-03 22:14:39 +03:00
]
, errorsView info field
]
errorsView : Form.Context String data -> Form.ViewField String parsed kind -> Html msg
errorsView formState field =
let
showErrors : Bool
showErrors =
--formState.submitAttempted
True
in
Html.ul
[ css
[ Tw.mt_2
, Tw.text_sm
, Tw.text_red_600
2022-01-04 08:17:07 +03:00
]
]
(if showErrors then
formState.errors
|> Form.errorsForField field
|> List.map
(\error ->
Html.li
[ css [ Tw.list_disc ]
]
[ Html.text error ]
)
2022-01-03 22:14:39 +03:00
else
[]
)
2022-01-03 22:14:39 +03:00
checkboxInput { name, description } info field =
Html.div
[ css
[ Tw.max_w_lg
, Tw.space_y_4
]
]
[ Html.label
[ css
[ Tw.relative
, Tw.flex
, Tw.items_start
]
]
[ Html.div
[ css
[ Tw.flex
, Tw.items_center
, Tw.h_5
]
]
[ field
2022-07-09 03:40:01 +03:00
|> Pages.FieldView.inputStyled
[ css
[ Tw.h_4
, Tw.w_4
, Tw.text_indigo_600
, Tw.border_gray_300
, Tw.rounded
, Css.focus
[ Tw.ring_indigo_500
]
]
]
]
, Html.div
[ css
[ Tw.ml_3
, Tw.text_sm
]
]
[ Html.div
[ css
[ Tw.font_medium
, Tw.text_gray_700
]
]
[ Html.text name ]
, Html.p
[ css
[ Tw.text_gray_500
]
]
[ Html.text description ]
]
]
, errorsView info field
]
2022-01-03 22:14:39 +03:00
2022-01-05 03:20:44 +03:00
wrapNotificationsSections children =
Html.div
[ css
[ Tw.divide_y
, Tw.divide_gray_200
, Tw.pt_8
, Tw.space_y_6
, Bp.sm
[ Tw.pt_10
, Tw.space_y_5
]
]
]
[ Html.div []
[ Html.h3
[ css
[ Tw.text_lg
, Tw.leading_6
, Tw.font_medium
, Tw.text_gray_900
]
]
[ Html.text "Notifications" ]
, Html.p
[ css
[ Tw.mt_1
, Tw.max_w_2xl
, Tw.text_sm
, Tw.text_gray_500
]
]
[ Html.text "We'll always let you know about important changes, but you pick what else you want to hear about." ]
]
, Html.div
[ css
[ Tw.space_y_6
, Tw.divide_y
, Tw.divide_gray_200
, Bp.sm
[ Tw.space_y_5
]
]
]
children
]
wrapEmailSection children =
Html.div
[ css
[ Tw.pt_6
, Bp.sm
[ Tw.pt_5
]
]
]
[ Html.div
[ Attr.attribute "role" "group"
, Attr.attribute "aria-labelledby" "label-email"
]
[ Html.div
[ css
[ Bp.sm
[ Tw.grid
, Tw.grid_cols_3
, Tw.gap_4
, Tw.items_baseline
]
]
]
[ Html.div []
[ Html.div
[ css
[ Tw.text_base
, Tw.font_medium
, Tw.text_gray_900
, Bp.sm
[ Tw.text_sm
, Tw.text_gray_700
]
]
, Attr.id "label-email"
]
[ Html.text "By Email" ]
]
, Html.div
[ css
[ Tw.mt_4
, Bp.sm
[ Tw.mt_0
, Tw.col_span_2
]
]
]
[ Html.div
[ css
[ Tw.max_w_lg
, Tw.space_y_4
]
]
children
]
]
]
]
radioInput errors item toRadio =
Html.label
2022-01-05 04:55:37 +03:00
[ css
[ Tw.ml_3
, Tw.block
, Tw.text_sm
, Tw.font_medium
, Tw.text_gray_700
2022-01-05 04:55:37 +03:00
]
]
[ Html.div
[ css
[ Tw.flex
, Tw.items_center
]
]
[ toRadio
[ css
[ Tw.h_4
, Tw.w_4
, Tw.text_indigo_600
, Tw.border_gray_300
, Tw.mr_2
, Css.focus
[ Tw.ring_indigo_500
]
]
]
, (case item of
2022-01-05 04:55:37 +03:00
PushAll ->
"Everything"
PushEmail ->
"Same as email"
PushNone ->
"No push notifications"
)
|> Html.text
]
]
wrapPushNotificationsSection formState field children =
2022-01-05 03:20:44 +03:00
Html.div
[ css
[ Tw.pt_6
, Bp.sm
[ Tw.pt_5
]
]
]
[ Html.div
[ Attr.attribute "role" "group"
, Attr.attribute "aria-labelledby" "label-notifications"
]
[ Html.span
[ css
[ Tw.font_bold
]
]
[ Html.text (Pages.FormState.fieldStatusToString field.status)
]
, Html.div
2022-01-05 03:20:44 +03:00
[ css
[ Bp.sm
[ Tw.grid
, Tw.grid_cols_3
, Tw.gap_4
, Tw.items_baseline
]
]
]
[ Html.div []
[ Html.div
[ css
[ Tw.text_base
, Tw.font_medium
, Tw.text_gray_900
, Bp.sm
[ Tw.text_sm
, Tw.text_gray_700
]
]
, Attr.id "label-notifications"
]
[ Html.text "Push Notifications" ]
]
, Html.div
[ css
[ Bp.sm
[ Tw.col_span_2
]
]
]
[ Html.div
[ css
[ Tw.max_w_lg
]
]
[ Html.p
[ css
[ Tw.text_sm
, Tw.text_gray_500
]
]
[ Html.text "These are delivered via SMS to your mobile phone." ]
, Html.div
[ css
[ Tw.mt_4
, Tw.space_y_4
]
]
2022-01-05 04:55:37 +03:00
children
2022-01-05 03:20:44 +03:00
]
]
]
]
, errorsView formState field
2022-01-05 03:20:44 +03:00
]