diff options
Diffstat (limited to 'elm/cost-of-meeting/src')
-rw-r--r-- | elm/cost-of-meeting/src/Main.elm | 217 | ||||
-rw-r--r-- | elm/cost-of-meeting/src/styles.css | 121 |
2 files changed, 338 insertions, 0 deletions
diff --git a/elm/cost-of-meeting/src/Main.elm b/elm/cost-of-meeting/src/Main.elm new file mode 100644 index 0000000..fdc16d5 --- /dev/null +++ b/elm/cost-of-meeting/src/Main.elm @@ -0,0 +1,217 @@ +module Main exposing (main) + +import Browser +import Html exposing (..) +import Html.Attributes as Attr +import Html.Events exposing (onInput) +import String + +-- MODEL + +type alias Model = + { participants : String + , salary : String + , minutes : String + , errors : List String + } + + +init : Model +init = + { participants = "" + , salary = "" + , minutes = "" + , errors = [] + } + + +-- UPDATE + +type Msg + = UpdateParticipants String + | UpdateSalary String + | UpdateMinutes String + + +update : Msg -> Model -> Model +update msg model = + case msg of + UpdateParticipants value -> + { model | participants = value, errors = validateInputs { model | participants = value } } + + UpdateSalary value -> + { model | salary = value, errors = validateInputs { model | salary = value } } + + UpdateMinutes value -> + { model | minutes = value, errors = validateInputs { model | minutes = value } } + + +-- VALIDATION + +validateInputs : Model -> List String +validateInputs model = + let + validateParticipants value = + case String.toFloat (String.replace "," "" value) of + Just num -> + if num <= 0 then + [ "Number of participants must be greater than 0" ] + else if num > 1000 then + [ "Number of participants seems unusually high" ] + else if num /= toFloat (round num) then + [ "Number of participants must be a whole number" ] + else + [] + + Nothing -> + if String.isEmpty value then + [] + else + [ "Number of participants must be a valid number" ] + + validateSalary value = + case String.toFloat (String.replace "," "" value) of + Just num -> + if num <= 0 then + [ "Salary must be greater than 0" ] + else if num < 10 then + [ "Salary seems unusually low" ] + else if num > 1000000 then + [ "Salary seems unusually high" ] + else + [] + + Nothing -> + if String.isEmpty value then + [] + else + [ "Salary must be a valid number" ] + + validateMinutes value = + case String.toFloat (String.replace "," "" value) of + Just num -> + if num <= 0 then + [ "Meeting length must be greater than 0" ] + else if num > 480 then + [ "Meeting length seems unusually long (over 8 hours)" ] + else if num /= toFloat (round num) then + [ "Meeting length must be a whole number" ] + else + [] + + Nothing -> + if String.isEmpty value then + [] + else + [ "Meeting length must be a valid number" ] + in + validateParticipants model.participants + ++ validateSalary model.salary + ++ validateMinutes model.minutes + + +-- VIEW + +view : Model -> Html Msg +view model = + div [ Attr.class "container" ] + [ h1 [] [ text "Meeting Cost Calculator" ] + , div [ Attr.class "input-group" ] + [ label [] [ text "Number of Participants" ] + , input + [ Attr.type_ "number" + , Attr.value model.participants + , onInput UpdateParticipants + , Attr.placeholder "Enter number of participants" + , Attr.min "1" + , Attr.max "1000" + ] [] + ] + , div [ Attr.class "input-group" ] + [ label [] [ text "Average Annual Salary ($)" ] + , input + [ Attr.type_ "text" + , Attr.value model.salary + , onInput UpdateSalary + , Attr.placeholder "Enter average annual salary" + ] [] + ] + , div [ Attr.class "input-group" ] + [ label [] [ text "Meeting Length (minutes)" ] + , input + [ Attr.type_ "number" + , Attr.value model.minutes + , onInput UpdateMinutes + , Attr.placeholder "Enter meeting length in minutes" + , Attr.min "1" + , Attr.max "480" + ] [] + ] + , viewErrors model.errors + , div [ Attr.class "results" ] + [ viewResults model ] + ] + + +viewErrors : List String -> Html msg +viewErrors errors = + if List.isEmpty errors then + div [] [] + else + div [ Attr.class "errors" ] + (List.map (\error -> p [] [ text error ]) errors) + + +viewResults : Model -> Html msg +viewResults model = + let + participants = + String.toFloat (String.replace "," "" model.participants) |> Maybe.withDefault 0 + + salary = + String.toFloat (String.replace "," "" model.salary) |> Maybe.withDefault 0 + + minutes = + String.toFloat (String.replace "," "" model.minutes) |> Maybe.withDefault 0 + + -- Calculate working hours per year (50 weeks * 5 days * 8 hours) + workingHoursPerYear = + 50 * 5 * 8 + + -- Calculate hourly rate + hourlyRate = + salary / workingHoursPerYear + + -- Calculate cost per minute + costPerMinute = + hourlyRate / 60 + + -- Calculate total meeting cost + totalCost = + costPerMinute * minutes * participants + in + if participants > 0 && salary > 0 && minutes > 0 && List.isEmpty model.errors then + div [] + [ h2 [] [ text "Approximate Meeting Cost" ] + , p [] + [ span [] [ text "Total Meeting Cost: " ] + , strong [] [ text ("$" ++ String.fromFloat (toFloat (round (totalCost * 100)) / 100)) ] + ] + , p [] + [ span [] [ text "Cost per Minute: " ] + , strong [] [ text ("$" ++ String.fromFloat (toFloat (round (costPerMinute * 100)) / 100)) ] + ] + ] + else + div [] [] + + +-- MAIN + +main : Program () Model Msg +main = + Browser.sandbox + { init = init + , update = update + , view = view + } \ No newline at end of file diff --git a/elm/cost-of-meeting/src/styles.css b/elm/cost-of-meeting/src/styles.css new file mode 100644 index 0000000..5d0427b --- /dev/null +++ b/elm/cost-of-meeting/src/styles.css @@ -0,0 +1,121 @@ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + background-color: white; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + line-height: 1.6; +} + +.container { + max-width: 100%; + margin: 0; + padding: 1rem; + border: 3px solid black; +} + +@media (min-width: 600px) { + .container { + max-width: 600px; + margin: 2rem auto; + padding: 2rem; + } +} + +.input-group { + margin-bottom: 2rem; + border: 2px solid black; + padding: 1rem; +} + +.input-group label { + display: block; + margin-bottom: 0.5rem; + font-weight: bold; + text-transform: uppercase; + font-size: 0.9rem; + letter-spacing: 0.05em; +} + +.input-group input { + width: 100%; + padding: 0.75rem; + font-size: 16px; + border: 2px solid black; + background: white; + -webkit-appearance: none; + -moz-appearance: textfield; +} + +.input-group input:focus { + outline: none; + border-width: 3px; +} + +.results { + margin-top: 2rem; + padding: 1.5rem; + border: 2px solid black; + background-color: white; +} + +.errors { + margin: 1rem 0; + padding: 1rem; + border: 2px solid black; + background-color: white; +} + +.errors p { + color: black; + font-size: 0.9rem; + margin: 0.5rem 0; + font-weight: normal; +} + +h1 { + color: black; + margin-bottom: 2rem; + font-size: 1.5rem; + text-transform: uppercase; + letter-spacing: 0.1em; + border-bottom: 3px solid black; + padding-bottom: 0.5rem; +} + +h2 { + color: black; + font-size: 1.2rem; + margin-bottom: 1rem; + text-transform: uppercase; + letter-spacing: 0.05em; + border-bottom: 2px solid black; + padding-bottom: 0.5rem; +} + +p { + margin: 1rem 0; + font-size: 1.1rem; + font-weight: 500; + display: flex; + justify-content: space-between; + align-items: baseline; +} + +p span { + font-weight: normal; +} + +p strong { + min-width: 120px; + text-align: right; +} + +input[type=number]::-webkit-inner-spin-button, +input[type=number]::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; +} \ No newline at end of file |