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

1136 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
2022-03-29 01:11:07 +03:00
import Effect exposing (Effect)
2022-03-29 21:48:04 +03:00
import ErrorPage exposing (ErrorPage)
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
import Pages.Field as Field
import Pages.FieldRenderer
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 Validation
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/" ]
, Pages.FieldRenderer.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 Dict.get field.name formState.errors |> Maybe.withDefault [] |> 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 User msg
form =
Form.init
(\first last username email dob checkin checkout rating password passwordConfirmation comments candidates offers pushNotifications acceptTerms ->
Validation.succeed User
|> Validation.withField first
|> Validation.withField last
|> Validation.withField username
|> Validation.withField email
|> Validation.withField dob
|> Validation.withField checkin
|> Validation.withField checkout
|> Validation.withField rating
|> Validation.andMap
(Validation.map2
(\passwordValue passwordConfirmationValue ->
if passwordValue == passwordConfirmationValue then
Validation.succeed ( passwordValue, passwordConfirmationValue )
else
Validation.fail passwordConfirmation.name "Must match password"
)
password.value
passwordConfirmation.value
|> Validation.andThen identity
)
|> Validation.andMap
(Validation.succeed NotificationPreferences
|> Validation.withField comments
|> Validation.withField candidates
|> Validation.withField offers
|> Validation.withField pushNotifications
)
|> Validation.andThen
(\validated ->
if Date.toRataDie validated.checkIn >= Date.toRataDie validated.checkOut then
Validation.succeed validated |> Validation.withError checkin.name "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
[ Pages.FieldRenderer.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 (.first >> Form.Value.string)
|> Field.withClientValidation validateCapitalized
)
|> Form.field "last"
(Field.text
|> Field.required "Required"
|> Field.withInitialValue (.last >> Form.Value.string)
|> Field.withClientValidation validateCapitalized
)
|> Form.field "username"
(Field.text
|> Field.withInitialValue (.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 (.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 (.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 (.checkIn >> Form.Value.date)
)
|> Form.field "checkout"
(Field.date
{ invalid = \_ -> "Invalid date" }
|> Field.required "Required"
|> Field.withInitialValue (.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.formParserResultNew [ form ]
|> Request.map
(\result ->
case result of
Ok user ->
DataSource.succeed
{ user = user
, flashMessage =
Ok ("Successfully updated profile for user " ++ user.first ++ " " ++ user.last)
}
|> DataSource.map Response.render
Err error ->
DataSource.succeed
{ flashMessage = Err "Got errors"
, user = defaultUser
}
|> 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
-- @@@@@@@ TODO migrate
--, initialForm : Form.Model
, flashMessage : Result String 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
]
[ Form.renderStyledHtml { submitStrategy = Form.TransitionStrategy, method = Form.Post } static user form
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
|> Pages.FieldRenderer.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
|> Dict.get field.name
|> Maybe.withDefault []
|> 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
|> Pages.FieldRenderer.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
]