about summary refs log tree commit diff stats
path: root/elm/cost-of-meeting/src
diff options
context:
space:
mode:
Diffstat (limited to 'elm/cost-of-meeting/src')
-rw-r--r--elm/cost-of-meeting/src/Main.elm217
-rw-r--r--elm/cost-of-meeting/src/styles.css121
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