Support multiple HabitTypes
I could have and should have broken this change into smaller pieces, but when I came up for air, I had changed too much, and most of the changes are intermingled. Oh well... this is an exciting change! Include habits for: - Morning - Evening - Payday (the 25th) - First of the Month - First of the Year Since the Morning and Evening routines might be a bit noisy, I'm excluding them from the output using a flag, `include{Morning,Evening}`, which I support in the UI to toggle their visibility. I made *much* more progress on this app that I expected to today, and I *think* -- short of supporting a database and a server -- I'm close to being *completely* finished. Wahoo!
This commit is contained in:
parent
abf1875934
commit
767fed75c3
2 changed files with 197 additions and 74 deletions
|
@ -1,11 +1,12 @@
|
||||||
module Habits exposing (render)
|
module Habits exposing (render)
|
||||||
|
|
||||||
import Browser
|
import Browser
|
||||||
|
import Date exposing (Date)
|
||||||
import Html exposing (..)
|
import Html exposing (..)
|
||||||
import Html.Attributes exposing (..)
|
import Html.Attributes exposing (..)
|
||||||
import Html.Events exposing (..)
|
import Html.Events exposing (..)
|
||||||
import Set exposing (Set)
|
import Set exposing (Set)
|
||||||
import State
|
import State exposing (HabitType(..))
|
||||||
import Time exposing (Weekday(..))
|
import Time exposing (Weekday(..))
|
||||||
import UI
|
import UI
|
||||||
import Utils exposing (Strategy(..))
|
import Utils exposing (Strategy(..))
|
||||||
|
@ -38,7 +39,7 @@ evening =
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
[ ( 30, "Read" )
|
[ ( 30, "Read" )
|
||||||
, ( 1, "Record in State.Habit Journal" )
|
, ( 1, "Record in habit Journal" )
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -145,8 +146,84 @@ firstOfTheYear =
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
weekdayName : Weekday -> String
|
habitTypes :
|
||||||
weekdayName weekday =
|
{ includeMorning : Bool
|
||||||
|
, includeEvening : Bool
|
||||||
|
, date : Date
|
||||||
|
}
|
||||||
|
-> List State.HabitType
|
||||||
|
habitTypes { includeMorning, includeEvening, date } =
|
||||||
|
let
|
||||||
|
habitTypePredicates : List ( State.HabitType, Date -> Bool )
|
||||||
|
habitTypePredicates =
|
||||||
|
[ ( Morning, \_ -> includeMorning )
|
||||||
|
, ( DayOfWeek, \_ -> True )
|
||||||
|
, ( Payday, \x -> Date.day x == 25 )
|
||||||
|
, ( FirstOfTheMonth, \x -> Date.day x == 1 )
|
||||||
|
, ( FirstOfTheYear, \x -> Date.day x == 1 && Date.monthNumber x == 1 )
|
||||||
|
, ( Evening, \_ -> includeEvening )
|
||||||
|
]
|
||||||
|
in
|
||||||
|
habitTypePredicates
|
||||||
|
|> List.filter (\( _, predicate ) -> predicate date)
|
||||||
|
|> List.map (\( habitType, _ ) -> habitType)
|
||||||
|
|
||||||
|
|
||||||
|
habitsFor : State.HabitType -> Weekday -> List State.Habit
|
||||||
|
habitsFor habitType weekday =
|
||||||
|
case habitType of
|
||||||
|
Morning ->
|
||||||
|
morning
|
||||||
|
|
||||||
|
Evening ->
|
||||||
|
evening
|
||||||
|
|
||||||
|
DayOfWeek ->
|
||||||
|
let
|
||||||
|
toHabit : List ( Int, String ) -> List State.Habit
|
||||||
|
toHabit =
|
||||||
|
List.map
|
||||||
|
(\( duration, x ) ->
|
||||||
|
{ label = x
|
||||||
|
, habitType = State.DayOfWeek
|
||||||
|
, minutesDuration = duration
|
||||||
|
}
|
||||||
|
)
|
||||||
|
in
|
||||||
|
case weekday of
|
||||||
|
Mon ->
|
||||||
|
toHabit monday
|
||||||
|
|
||||||
|
Tue ->
|
||||||
|
toHabit tuesday
|
||||||
|
|
||||||
|
Wed ->
|
||||||
|
toHabit wednesday
|
||||||
|
|
||||||
|
Thu ->
|
||||||
|
toHabit thursday
|
||||||
|
|
||||||
|
Fri ->
|
||||||
|
toHabit friday
|
||||||
|
|
||||||
|
Sat ->
|
||||||
|
toHabit saturday
|
||||||
|
|
||||||
|
Sun ->
|
||||||
|
toHabit sunday
|
||||||
|
|
||||||
|
Payday ->
|
||||||
|
payday
|
||||||
|
|
||||||
|
FirstOfTheMonth ->
|
||||||
|
firstOfTheMonth
|
||||||
|
|
||||||
|
FirstOfTheYear ->
|
||||||
|
firstOfTheYear
|
||||||
|
|
||||||
|
|
||||||
|
weekdayLabelFor : Weekday -> State.WeekdayLabel
|
||||||
|
weekdayLabelFor weekday =
|
||||||
case weekday of
|
case weekday of
|
||||||
Mon ->
|
Mon ->
|
||||||
"Monday"
|
"Monday"
|
||||||
|
@ -170,47 +247,12 @@ weekdayName weekday =
|
||||||
"Sunday"
|
"Sunday"
|
||||||
|
|
||||||
|
|
||||||
habitsFor : Weekday -> List State.Habit
|
timeRemaining : State.WeekdayLabel -> State.CompletedHabits -> List State.Habit -> Int
|
||||||
habitsFor weekday =
|
timeRemaining weekdayLabel completed habits =
|
||||||
let
|
|
||||||
toHabit =
|
|
||||||
List.map
|
|
||||||
(\( duration, x ) ->
|
|
||||||
{ label = x
|
|
||||||
, habitType = State.DayOfWeek
|
|
||||||
, minutesDuration = duration
|
|
||||||
}
|
|
||||||
)
|
|
||||||
in
|
|
||||||
case weekday of
|
|
||||||
Mon ->
|
|
||||||
toHabit monday
|
|
||||||
|
|
||||||
Tue ->
|
|
||||||
toHabit tuesday
|
|
||||||
|
|
||||||
Wed ->
|
|
||||||
toHabit wednesday
|
|
||||||
|
|
||||||
Thu ->
|
|
||||||
toHabit thursday
|
|
||||||
|
|
||||||
Fri ->
|
|
||||||
toHabit friday
|
|
||||||
|
|
||||||
Sat ->
|
|
||||||
toHabit saturday
|
|
||||||
|
|
||||||
Sun ->
|
|
||||||
toHabit sunday
|
|
||||||
|
|
||||||
|
|
||||||
timeRemaining : Set Int -> List State.Habit -> Int
|
|
||||||
timeRemaining completed habits =
|
|
||||||
habits
|
habits
|
||||||
|> List.indexedMap
|
|> List.indexedMap
|
||||||
(\i { minutesDuration } ->
|
(\i { label, minutesDuration } ->
|
||||||
if Set.member i completed then
|
if Set.member ( weekdayLabel, label ) completed then
|
||||||
0
|
0
|
||||||
|
|
||||||
else
|
else
|
||||||
|
@ -220,24 +262,32 @@ timeRemaining completed habits =
|
||||||
|
|
||||||
|
|
||||||
render : State.Model -> Html State.Msg
|
render : State.Model -> Html State.Msg
|
||||||
render { today, visibleDayOfWeek, completed } =
|
render { today, visibleDayOfWeek, completed, includeMorning, includeEvening } =
|
||||||
case visibleDayOfWeek of
|
case ( today, visibleDayOfWeek ) of
|
||||||
Nothing ->
|
( Just todaysDate, Just visibleWeekday ) ->
|
||||||
p [] [ text "Unable to display habits because we do not know what day of the week it is." ]
|
|
||||||
|
|
||||||
Just weekday ->
|
|
||||||
let
|
let
|
||||||
|
todaysWeekday : Weekday
|
||||||
|
todaysWeekday =
|
||||||
|
Date.weekday todaysDate
|
||||||
|
|
||||||
|
habits : List State.Habit
|
||||||
habits =
|
habits =
|
||||||
habitsFor weekday
|
habitTypes
|
||||||
|
{ includeMorning = includeMorning
|
||||||
|
, includeEvening = includeEvening
|
||||||
|
, date = todaysDate
|
||||||
|
}
|
||||||
|
|> List.map (\habitType -> habitsFor habitType todaysWeekday)
|
||||||
|
|> List.concat
|
||||||
in
|
in
|
||||||
div
|
div
|
||||||
[ Utils.class
|
[ Utils.class
|
||||||
[ Always "container mx-auto py-6 px-6"
|
[ Always "container mx-auto py-6 px-6"
|
||||||
, When (today /= visibleDayOfWeek) "pt-20"
|
, When (todaysWeekday /= visibleWeekday) "pt-20"
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
[ header []
|
[ header []
|
||||||
[ if today /= visibleDayOfWeek then
|
[ if todaysWeekday /= visibleWeekday then
|
||||||
div [ class "text-center w-full bg-blue-600 text-white fixed top-0 left-0 px-3 py-4" ]
|
div [ class "text-center w-full bg-blue-600 text-white fixed top-0 left-0 px-3 py-4" ]
|
||||||
[ p [ class "py-2 inline pr-5" ]
|
[ p [ class "py-2 inline pr-5" ]
|
||||||
[ text "As you are not viewing today's habits, the UI is in read-only mode" ]
|
[ text "As you are not viewing today's habits, the UI is in read-only mode" ]
|
||||||
|
@ -257,7 +307,7 @@ render { today, visibleDayOfWeek, completed } =
|
||||||
]
|
]
|
||||||
[ text "‹ previous" ]
|
[ text "‹ previous" ]
|
||||||
, h1 [ class "font-bold text-blue-500 text-3xl text-center w-full" ]
|
, h1 [ class "font-bold text-blue-500 text-3xl text-center w-full" ]
|
||||||
[ text (weekdayName weekday) ]
|
[ text (weekdayLabelFor visibleWeekday) ]
|
||||||
, UI.button
|
, UI.button
|
||||||
[ class "w-1/4 text-gray-500"
|
[ class "w-1/4 text-gray-500"
|
||||||
, onClick State.ViewNext
|
, onClick State.ViewNext
|
||||||
|
@ -265,11 +315,12 @@ render { today, visibleDayOfWeek, completed } =
|
||||||
[ text "next ›" ]
|
[ text "next ›" ]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
, if today == visibleDayOfWeek then
|
, if todaysWeekday == visibleWeekday then
|
||||||
p [ class "text-center" ]
|
p [ class "text-center" ]
|
||||||
[ let
|
[ let
|
||||||
|
t : Int
|
||||||
t =
|
t =
|
||||||
timeRemaining completed habits
|
timeRemaining (weekdayLabelFor todaysWeekday) completed habits
|
||||||
in
|
in
|
||||||
if t == 0 then
|
if t == 0 then
|
||||||
text "Nothing to do!"
|
text "Nothing to do!"
|
||||||
|
@ -277,7 +328,7 @@ render { today, visibleDayOfWeek, completed } =
|
||||||
else
|
else
|
||||||
text
|
text
|
||||||
((habits
|
((habits
|
||||||
|> timeRemaining completed
|
|> timeRemaining (weekdayLabelFor todaysWeekday) completed
|
||||||
|> String.fromInt
|
|> String.fromInt
|
||||||
)
|
)
|
||||||
++ " minutes remaining"
|
++ " minutes remaining"
|
||||||
|
@ -286,7 +337,7 @@ render { today, visibleDayOfWeek, completed } =
|
||||||
|
|
||||||
else
|
else
|
||||||
text ""
|
text ""
|
||||||
, if today == visibleDayOfWeek then
|
, if todaysWeekday == visibleWeekday then
|
||||||
div []
|
div []
|
||||||
[ UI.button
|
[ UI.button
|
||||||
[ onClick
|
[ onClick
|
||||||
|
@ -304,10 +355,16 @@ render { today, visibleDayOfWeek, completed } =
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
[ let
|
[ let
|
||||||
|
numCompleted : Int
|
||||||
numCompleted =
|
numCompleted =
|
||||||
habits
|
habits
|
||||||
|> List.indexedMap (\i _ -> i)
|
|> List.indexedMap (\i { label } -> ( i, label ))
|
||||||
|> List.filter (\i -> Set.member i completed)
|
|> List.filter
|
||||||
|
(\( i, label ) ->
|
||||||
|
Set.member
|
||||||
|
( weekdayLabelFor todaysWeekday, label )
|
||||||
|
completed
|
||||||
|
)
|
||||||
|> List.length
|
|> List.length
|
||||||
in
|
in
|
||||||
if numCompleted == 0 then
|
if numCompleted == 0 then
|
||||||
|
@ -316,6 +373,40 @@ render { today, visibleDayOfWeek, completed } =
|
||||||
else
|
else
|
||||||
text ("Clear (" ++ String.fromInt numCompleted ++ ")")
|
text ("Clear (" ++ String.fromInt numCompleted ++ ")")
|
||||||
]
|
]
|
||||||
|
, UI.button
|
||||||
|
[ onClick State.ToggleMorning
|
||||||
|
, Utils.class
|
||||||
|
[ Always "px-3 underline"
|
||||||
|
, If includeMorning
|
||||||
|
"text-gray-600"
|
||||||
|
"text-blue-600"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
[ text
|
||||||
|
(if includeMorning then
|
||||||
|
"Hide Morning"
|
||||||
|
|
||||||
|
else
|
||||||
|
"Show Morning"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
, UI.button
|
||||||
|
[ Utils.class
|
||||||
|
[ Always "px-3 underline"
|
||||||
|
, If includeEvening
|
||||||
|
"text-gray-600"
|
||||||
|
"text-blue-600"
|
||||||
|
]
|
||||||
|
, onClick State.ToggleEvening
|
||||||
|
]
|
||||||
|
[ text
|
||||||
|
(if includeEvening then
|
||||||
|
"Hide Evening"
|
||||||
|
|
||||||
|
else
|
||||||
|
"Show Evening"
|
||||||
|
)
|
||||||
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
else
|
else
|
||||||
|
@ -325,14 +416,19 @@ render { today, visibleDayOfWeek, completed } =
|
||||||
|> List.indexedMap
|
|> List.indexedMap
|
||||||
(\i { label, minutesDuration } ->
|
(\i { label, minutesDuration } ->
|
||||||
let
|
let
|
||||||
|
isCompleted : Bool
|
||||||
isCompleted =
|
isCompleted =
|
||||||
Set.member i completed
|
Set.member ( weekdayLabelFor todaysWeekday, label ) completed
|
||||||
in
|
in
|
||||||
li [ class "text-xl list-disc ml-6" ]
|
li [ class "text-xl list-disc ml-6" ]
|
||||||
[ if today == visibleDayOfWeek then
|
[ if todaysWeekday == visibleWeekday then
|
||||||
UI.button
|
UI.button
|
||||||
[ class "py-5 px-3"
|
[ class "py-5 px-3"
|
||||||
, onClick (State.ToggleHabit i)
|
, onClick
|
||||||
|
(State.ToggleHabit
|
||||||
|
(weekdayLabelFor todaysWeekday)
|
||||||
|
label
|
||||||
|
)
|
||||||
]
|
]
|
||||||
[ span
|
[ span
|
||||||
[ Utils.class
|
[ Utils.class
|
||||||
|
@ -364,3 +460,6 @@ render { today, visibleDayOfWeek, completed } =
|
||||||
, p [] [ text "Client: Elm; Server: n/a" ]
|
, p [] [ text "Client: Elm; Server: n/a" ]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
( _, _ ) ->
|
||||||
|
p [] [ text "Unable to display habits because we do not know what day of the week it is." ]
|
||||||
|
|
|
@ -1,21 +1,31 @@
|
||||||
module State exposing (..)
|
module State exposing (..)
|
||||||
|
|
||||||
import Date
|
import Date exposing (Date)
|
||||||
import Set exposing (Set)
|
import Set exposing (Set)
|
||||||
import Task
|
import Task
|
||||||
import Time exposing (Weekday(..))
|
import Time exposing (Weekday(..))
|
||||||
|
|
||||||
|
|
||||||
|
type alias WeekdayLabel =
|
||||||
|
String
|
||||||
|
|
||||||
|
|
||||||
|
type alias HabitLabel =
|
||||||
|
String
|
||||||
|
|
||||||
|
|
||||||
type Msg
|
type Msg
|
||||||
= DoNothing
|
= DoNothing
|
||||||
| SetView View
|
| SetView View
|
||||||
| ReceiveDate Date.Date
|
| ReceiveDate Date
|
||||||
| ToggleHabit Int
|
| ToggleHabit WeekdayLabel HabitLabel
|
||||||
| MaybeAdjustWeekday
|
| MaybeAdjustWeekday
|
||||||
| ViewToday
|
| ViewToday
|
||||||
| ViewPrevious
|
| ViewPrevious
|
||||||
| ViewNext
|
| ViewNext
|
||||||
| ClearAll
|
| ClearAll
|
||||||
|
| ToggleMorning
|
||||||
|
| ToggleEvening
|
||||||
|
|
||||||
|
|
||||||
type View
|
type View
|
||||||
|
@ -32,18 +42,24 @@ type HabitType
|
||||||
|
|
||||||
|
|
||||||
type alias Habit =
|
type alias Habit =
|
||||||
{ label : String
|
{ label : HabitLabel
|
||||||
, habitType : HabitType
|
, habitType : HabitType
|
||||||
, minutesDuration : Int
|
, minutesDuration : Int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type alias CompletedHabits =
|
||||||
|
Set ( WeekdayLabel, HabitLabel )
|
||||||
|
|
||||||
|
|
||||||
type alias Model =
|
type alias Model =
|
||||||
{ isLoading : Bool
|
{ isLoading : Bool
|
||||||
, view : View
|
, view : View
|
||||||
, today : Maybe Weekday
|
, today : Maybe Date
|
||||||
, completed : Set Int
|
, completed : CompletedHabits
|
||||||
, visibleDayOfWeek : Maybe Weekday
|
, visibleDayOfWeek : Maybe Weekday
|
||||||
|
, includeMorning : Bool
|
||||||
|
, includeEvening : Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -106,6 +122,8 @@ init =
|
||||||
, today = Nothing
|
, today = Nothing
|
||||||
, completed = Set.empty
|
, completed = Set.empty
|
||||||
, visibleDayOfWeek = Nothing
|
, visibleDayOfWeek = Nothing
|
||||||
|
, includeMorning = False
|
||||||
|
, includeEvening = False
|
||||||
}
|
}
|
||||||
, Date.today |> Task.perform ReceiveDate
|
, Date.today |> Task.perform ReceiveDate
|
||||||
)
|
)
|
||||||
|
@ -129,20 +147,20 @@ update msg ({ today, visibleDayOfWeek, completed } as model) =
|
||||||
|
|
||||||
ReceiveDate x ->
|
ReceiveDate x ->
|
||||||
( { model
|
( { model
|
||||||
| today = Just (Date.weekday x)
|
| today = Just x
|
||||||
, visibleDayOfWeek = Just (Date.weekday x)
|
, visibleDayOfWeek = Just (Date.weekday x)
|
||||||
}
|
}
|
||||||
, Cmd.none
|
, Cmd.none
|
||||||
)
|
)
|
||||||
|
|
||||||
ToggleHabit i ->
|
ToggleHabit weekdayLabel habitLabel ->
|
||||||
( { model
|
( { model
|
||||||
| completed =
|
| completed =
|
||||||
if Set.member i completed then
|
if Set.member ( weekdayLabel, habitLabel ) completed then
|
||||||
Set.remove i completed
|
Set.remove ( weekdayLabel, habitLabel ) completed
|
||||||
|
|
||||||
else
|
else
|
||||||
Set.insert i completed
|
Set.insert ( weekdayLabel, habitLabel ) completed
|
||||||
}
|
}
|
||||||
, Cmd.none
|
, Cmd.none
|
||||||
)
|
)
|
||||||
|
@ -151,7 +169,7 @@ update msg ({ today, visibleDayOfWeek, completed } as model) =
|
||||||
( model, Date.today |> Task.perform ReceiveDate )
|
( model, Date.today |> Task.perform ReceiveDate )
|
||||||
|
|
||||||
ViewToday ->
|
ViewToday ->
|
||||||
( { model | visibleDayOfWeek = today }, Cmd.none )
|
( { model | visibleDayOfWeek = today |> Maybe.map Date.weekday }, Cmd.none )
|
||||||
|
|
||||||
ViewPrevious ->
|
ViewPrevious ->
|
||||||
( { model
|
( { model
|
||||||
|
@ -169,3 +187,9 @@ update msg ({ today, visibleDayOfWeek, completed } as model) =
|
||||||
|
|
||||||
ClearAll ->
|
ClearAll ->
|
||||||
( { model | completed = Set.empty }, Cmd.none )
|
( { model | completed = Set.empty }, Cmd.none )
|
||||||
|
|
||||||
|
ToggleMorning ->
|
||||||
|
( { model | includeMorning = not model.includeMorning }, Cmd.none )
|
||||||
|
|
||||||
|
ToggleEvening ->
|
||||||
|
( { model | includeEvening = not model.includeEvening }, Cmd.none )
|
||||||
|
|
Loading…
Reference in a new issue