diff options
author | William Carroll <wpcarro@gmail.com> | 2020-12-11T22·47+0000 |
---|---|---|
committer | William Carroll <wpcarro@gmail.com> | 2020-12-11T22·47+0000 |
commit | 9e2fbfde8e9c2256e1817f79d119e0dd7bed7b7d (patch) | |
tree | 6010b135c9c466e5dee21c8e4e5c85f47529a35f /website/habit-screens | |
parent | 3feb8ceb9a6bbaa48927188de4806c8573f499a5 (diff) |
Move the habit-screens project into //website
I'd like to eventually deploy this to wpcarro.dev. Coming soon!
Diffstat (limited to 'website/habit-screens')
-rw-r--r-- | website/habit-screens/.envrc | 2 | ||||
-rw-r--r-- | website/habit-screens/.gitignore | 3 | ||||
-rw-r--r-- | website/habit-screens/README.md | 31 | ||||
-rw-r--r-- | website/habit-screens/default.nix | 53 | ||||
-rw-r--r-- | website/habit-screens/design.md | 43 | ||||
-rw-r--r-- | website/habit-screens/elm-srcs.nix | 77 | ||||
-rw-r--r-- | website/habit-screens/elm.json | 32 | ||||
-rw-r--r-- | website/habit-screens/index.css | 3 | ||||
-rw-r--r-- | website/habit-screens/index.html | 21 | ||||
-rw-r--r-- | website/habit-screens/registry.dat | bin | 0 -> 103324 bytes | |||
-rw-r--r-- | website/habit-screens/shell.nix | 10 | ||||
-rw-r--r-- | website/habit-screens/src/Habits.elm | 465 | ||||
-rw-r--r-- | website/habit-screens/src/Main.elm | 29 | ||||
-rw-r--r-- | website/habit-screens/src/State.elm | 195 | ||||
-rw-r--r-- | website/habit-screens/src/UI.elm | 9 | ||||
-rw-r--r-- | website/habit-screens/src/Utils.elm | 37 |
16 files changed, 1010 insertions, 0 deletions
diff --git a/website/habit-screens/.envrc b/website/habit-screens/.envrc new file mode 100644 index 000000000000..a4a62da526d3 --- /dev/null +++ b/website/habit-screens/.envrc @@ -0,0 +1,2 @@ +source_up +use_nix diff --git a/website/habit-screens/.gitignore b/website/habit-screens/.gitignore new file mode 100644 index 000000000000..1cb4f3034cc3 --- /dev/null +++ b/website/habit-screens/.gitignore @@ -0,0 +1,3 @@ +/elm-stuff +/Main.min.js +/output.css diff --git a/website/habit-screens/README.md b/website/habit-screens/README.md new file mode 100644 index 000000000000..506cdf9c4ac8 --- /dev/null +++ b/website/habit-screens/README.md @@ -0,0 +1,31 @@ +# Habit Screens + +Problem: I would like to increase the rate at which I complete my daily, weekly, +monthly, yearly habits. + +Solution: Habit Screens are mounted in strategic locations throughout my +apartment. Each Habit Screen displays the habits that I should complete that +day, and I can tap each item to mark it as complete. I will encounter the Habit +Screens in my bedroom, kitchen, and bathroom, so I will have adequate "cues" to +focus my attention. By marking each item as complete and tracking the results +over time, I will have more incentive to maintain my consistency +(i.e. "reward"). + +## Elm + +Elm has one of the best developer experiences that I'm aware of. The error +messages are helpful and the entire experience is optimized to improve the ease +of writing web applications. + +### Developing + +If you're interested in contributing, the following will create an environment +in which you can develop: + +```shell +$ nix-shell +$ npx tailwindcss build index.css -o output.css +$ elm-live -- src/Main.elm --output=Main.min.js +``` + +You can now view your web client at `http://localhost:8000`! diff --git a/website/habit-screens/default.nix b/website/habit-screens/default.nix new file mode 100644 index 000000000000..19fd1b37c2ed --- /dev/null +++ b/website/habit-screens/default.nix @@ -0,0 +1,53 @@ +{ nixpkgs ? <nixpkgs> +, config ? {} +}: + +with (import nixpkgs config); + +let + mkDerivation = + { srcs ? ./elm-srcs.nix + , src + , name + , srcdir ? "./src" + , targets ? [] + , registryDat ? ./registry.dat + , outputJavaScript ? false + }: + stdenv.mkDerivation { + inherit name src; + + buildInputs = [ elmPackages.elm ] + ++ lib.optional outputJavaScript nodePackages_10_x.uglify-js; + + buildPhase = pkgs.elmPackages.fetchElmDeps { + elmPackages = import srcs; + elmVersion = "0.19.1"; + inherit registryDat; + }; + + installPhase = let + elmfile = module: "${srcdir}/${builtins.replaceStrings ["."] ["/"] module}.elm"; + extension = if outputJavaScript then "js" else "html"; + in '' + mkdir -p $out/share/doc + ${lib.concatStrings (map (module: '' + echo "compiling ${elmfile module}" + elm make ${elmfile module} --output $out/${module}.${extension} --docs $out/share/doc/${module}.json + ${lib.optionalString outputJavaScript '' + echo "minifying ${elmfile module}" + uglifyjs $out/${module}.${extension} --compress 'pure_funcs="F2,F3,F4,F5,F6,F7,F8,F9,A2,A3,A4,A5,A6,A7,A8,A9",pure_getters,keep_fargs=false,unsafe_comps,unsafe' \ + | uglifyjs --mangle --output=$out/${module}.min.${extension} + ''} + '') targets)} + ''; + }; +in mkDerivation { + name = "elm-app-0.1.0"; + srcs = ./elm-srcs.nix; + src = ./.; + targets = ["Main"]; + srcdir = "./src"; + outputJavaScript = false; +} + diff --git a/website/habit-screens/design.md b/website/habit-screens/design.md new file mode 100644 index 000000000000..f16361ac4358 --- /dev/null +++ b/website/habit-screens/design.md @@ -0,0 +1,43 @@ +# Habit Screens + +## MVP + +One Android tablet mounted on my bedroom wall displaying habits for that day. I +can toggle the done/todo states on each item by tapping it. There is no +server. All of the habits are defined in the client-side codebase. The +application is available online at wpcarro.dev. + +## Ideal + +Three Android tablets: one mounted in my bedroom, another in my bathroom, and a +third in my kitchen. Each tablet has a view of the current state of the +application and updates in soft real-time. + +I track the rates at which I complete each habit and compile all of the metrics +into a dashboard. When I move a habit from Saturday to Sunday or from Wednesday +to Monday, it doesn't break the tracking. + +When I complete a habit, it quickly renders some consistency information like +"completing rate since Monday" and "length of current streak". + +I don't consider this application that sensitive, but for security purposes I +would like this application to be accessible within a private network. This is +something I don't know too much about setting up, but I don't want anyone to be +able to visit www.BillAndHisHabits.com and change the states of my habits and +affect the tracking data. Nor do I want anyone to be able to make HTTP requests +to my server to alter the state of the application without my permission. + +## Client + +Language: Elm + +### Updates across devices + +Instead of setting up sockets on my server and subscribing to them from the +client, I think each device should poll the server once every second (or fewer) +to maintain UI consistency. + +## Server + +Language: Haskell +Database: SQLite diff --git a/website/habit-screens/elm-srcs.nix b/website/habit-screens/elm-srcs.nix new file mode 100644 index 000000000000..167708e072b0 --- /dev/null +++ b/website/habit-screens/elm-srcs.nix @@ -0,0 +1,77 @@ +{ + + "elm-community/maybe-extra" = { + sha256 = "0qslmgswa625d218djd3p62pnqcrz38f5p558mbjl6kc1ss0kzv3"; + version = "5.2.0"; + }; + + "elm/html" = { + sha256 = "1n3gpzmpqqdsldys4ipgyl1zacn0kbpc3g4v3hdpiyfjlgh8bf3k"; + version = "1.0.0"; + }; + + "elm-community/random-extra" = { + sha256 = "1dg2nz77w2cvp16xazbdsxkkw0xc9ycqpkd032faqdyky6gmz9g6"; + version = "3.1.0"; + }; + + "elm/svg" = { + sha256 = "1cwcj73p61q45wqwgqvrvz3aypjyy3fw732xyxdyj6s256hwkn0k"; + version = "1.0.1"; + }; + + "justinmimbs/date" = { + sha256 = "1f0wcl8yhlvp3x4rj53rdy4r4ga7lkl6n8fdfh6b96scz2rnxmd4"; + version = "3.2.1"; + }; + + "elm/browser" = { + sha256 = "0nagb9ajacxbbg985r4k9h0jadqpp0gp84nm94kcgbr5sf8i9x13"; + version = "1.0.2"; + }; + + "elm/core" = { + sha256 = "19w0iisdd66ywjayyga4kv2p1v9rxzqjaxhckp8ni6n8i0fb2dvf"; + version = "1.0.5"; + }; + + "elm-community/list-extra" = { + sha256 = "1ayv3148drynqnxdfwpjxal8vwzgsjqanjg7yxp6lhdcbkxgd3vd"; + version = "8.2.3"; + }; + + "elm/random" = { + sha256 = "138n2455wdjwa657w6sjq18wx2r0k60ibpc4frhbqr50sncxrfdl"; + version = "1.0.0"; + }; + + "elm/time" = { + sha256 = "0vch7i86vn0x8b850w1p69vplll1bnbkp8s383z7pinyg94cm2z1"; + version = "1.0.0"; + }; + + "elm/json" = { + sha256 = "0kjwrz195z84kwywaxhhlnpl3p251qlbm5iz6byd6jky2crmyqyh"; + version = "1.1.3"; + }; + + "elm/parser" = { + sha256 = "0a3cxrvbm7mwg9ykynhp7vjid58zsw03r63qxipxp3z09qks7512"; + version = "1.1.0"; + }; + + "owanturist/elm-union-find" = { + sha256 = "13gm7msnp0gr1lqia5m7m4lhy3m6kvjg37d304whb3psn88wqhj5"; + version = "1.0.0"; + }; + + "elm/url" = { + sha256 = "0av8x5syid40sgpl5vd7pry2rq0q4pga28b4yykn9gd9v12rs3l4"; + version = "1.0.0"; + }; + + "elm/virtual-dom" = { + sha256 = "0q1v5gi4g336bzz1lgwpn5b1639lrn63d8y6k6pimcyismp2i1yg"; + version = "1.0.2"; + }; +} diff --git a/website/habit-screens/elm.json b/website/habit-screens/elm.json new file mode 100644 index 000000000000..6839ac4fabdc --- /dev/null +++ b/website/habit-screens/elm.json @@ -0,0 +1,32 @@ +{ + "type": "application", + "source-directories": [ + "src" + ], + "elm-version": "0.19.1", + "dependencies": { + "direct": { + "elm/browser": "1.0.2", + "elm/core": "1.0.5", + "elm/html": "1.0.0", + "elm/random": "1.0.0", + "elm/svg": "1.0.1", + "elm/time": "1.0.0", + "elm-community/list-extra": "8.2.3", + "elm-community/maybe-extra": "5.2.0", + "elm-community/random-extra": "3.1.0", + "justinmimbs/date": "3.2.1" + }, + "indirect": { + "elm/json": "1.1.3", + "elm/parser": "1.1.0", + "elm/url": "1.0.0", + "elm/virtual-dom": "1.0.2", + "owanturist/elm-union-find": "1.0.0" + } + }, + "test-dependencies": { + "direct": {}, + "indirect": {} + } +} diff --git a/website/habit-screens/index.css b/website/habit-screens/index.css new file mode 100644 index 000000000000..b5c61c956711 --- /dev/null +++ b/website/habit-screens/index.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/website/habit-screens/index.html b/website/habit-screens/index.html new file mode 100644 index 000000000000..b587e0901284 --- /dev/null +++ b/website/habit-screens/index.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="UTF-8" /> + <title>Elm SPA</title> + <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Chilanka"> + <link rel="stylesheet" href="./output.css"> + <style> + body { + font-family: 'Chilanka'; + } + </style> + <script src="./Main.min.js"></script> + </head> + <body> + <div id="mount"></div> + <script> + Elm.Main.init({node: document.getElementById("mount")}); + </script> + </body> +</html> diff --git a/website/habit-screens/registry.dat b/website/habit-screens/registry.dat new file mode 100644 index 000000000000..d2671b2cf17a --- /dev/null +++ b/website/habit-screens/registry.dat Binary files differdiff --git a/website/habit-screens/shell.nix b/website/habit-screens/shell.nix new file mode 100644 index 000000000000..00bb4b0b3edc --- /dev/null +++ b/website/habit-screens/shell.nix @@ -0,0 +1,10 @@ +let + briefcase = import <briefcase> {}; + pkgs = briefcase.third_party.pkgs; +in pkgs.mkShell { + buildInputs = with pkgs.elmPackages; [ + elm + elm-format + elm-live + ]; +} diff --git a/website/habit-screens/src/Habits.elm b/website/habit-screens/src/Habits.elm new file mode 100644 index 000000000000..bbd5887f8bd5 --- /dev/null +++ b/website/habit-screens/src/Habits.elm @@ -0,0 +1,465 @@ +module Habits exposing (render) + +import Browser +import Date exposing (Date) +import Html exposing (..) +import Html.Attributes exposing (..) +import Html.Events exposing (..) +import Set exposing (Set) +import State exposing (HabitType(..)) +import Time exposing (Weekday(..)) +import UI +import Utils exposing (Strategy(..)) + + +morning : List State.Habit +morning = + List.map + (\( duration, x ) -> + { label = x + , habitType = State.Morning + , minutesDuration = duration + } + ) + [ ( 1, "Make bed" ) + , ( 2, "Brush teeth" ) + , ( 10, "Shower" ) + , ( 1, "Do push-ups" ) + , ( 10, "Meditate" ) + ] + + +evening : List State.Habit +evening = + List.map + (\( duration, x ) -> + { label = x + , habitType = State.Evening + , minutesDuration = duration + } + ) + [ ( 30, "Read" ) + , ( 1, "Record in habit Journal" ) + ] + + +monday : List ( Int, String ) +monday = + [ ( 90, "Bikram Yoga @ 17:00" ) + ] + + +tuesday : List ( Int, String ) +tuesday = + [ ( 90, "Bikram Yoga @ 18:00" ) + ] + + +wednesday : List ( Int, String ) +wednesday = + [ ( 5, "Shave" ) + , ( 90, "Bikram Yoga @ 17:00" ) + ] + + +thursday : List ( Int, String ) +thursday = + [] + + +friday : List ( Int, String ) +friday = + [ ( 60, "Bikram Yoga @ 17:00" ) + , ( 3, "Take-out trash" ) + , ( 60, "Shop for groceries" ) + ] + + +saturday : List ( Int, String ) +saturday = + [ ( 60, "Warm Yin Yoga @ 15:00" ) + ] + + +sunday : List ( Int, String ) +sunday = + [ ( 1, "Shampoo" ) + , ( 5, "Shave" ) + , ( 1, "Trim nails" ) + , ( 1, "Combine trash cans" ) + , ( 10, "Mop tile and wood floors" ) + , ( 10, "Laundry" ) + , ( 5, "Vacuum bedroom" ) + , ( 5, "Dust surfaces" ) + , ( 5, "Clean mirrors" ) + , ( 5, "Clean desk" ) + ] + + +payday : List State.Habit +payday = + List.map + (\( duration, x ) -> + { label = x + , habitType = State.Payday + , minutesDuration = duration + } + ) + [ ( 1, "Ensure \"Emergency\" fund has a balance of 1000 GBP" ) + , ( 1, "Open \"finances_2020\" Google Sheet" ) + , ( 1, "Settle up with Mimi on TransferWise" ) + , ( 1, "Adjust GBP:USD exchange rate" ) + , ( 1, "Adjust \"Stocks (after tax)\" to reflect amount Google sent" ) + , ( 1, "Add remaining cash to \"Carryover (cash)\"" ) + , ( 1, "Adjust \"Paycheck\" to reflect amount Google sent" ) + , ( 5, "In the \"International Xfer\" table, send \"Xfer amount\" from Monzo to USAA" ) + , ( 10, "Go to an ATM and extract the amount in \"ATM withdrawal\"" ) + , ( 0, "Await the TransferWise transaction to complete and pay MyFedLoan in USD" ) + ] + + +firstOfTheMonth : List State.Habit +firstOfTheMonth = + List.map + (\( duration, x ) -> + { label = x + , habitType = State.FirstOfTheMonth + , minutesDuration = duration + } + ) + [ ( 10, "Create habit template in journal" ) + , ( 30, "Assess previous month's performance" ) + , ( 5, "Register for Bikram Yoga classes" ) + ] + + +firstOfTheYear : List State.Habit +firstOfTheYear = + List.map + (\( duration, x ) -> + { label = x + , habitType = State.FirstOfTheYear + , minutesDuration = duration + } + ) + [ ( 60, "Write a post mortem for the previous year" ) + ] + + +habitTypes : + { 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 + Mon -> + "Monday" + + Tue -> + "Tuesday" + + Wed -> + "Wednesday" + + Thu -> + "Thursday" + + Fri -> + "Friday" + + Sat -> + "Saturday" + + Sun -> + "Sunday" + + +timeRemaining : State.WeekdayLabel -> State.CompletedHabits -> List State.Habit -> Int +timeRemaining weekdayLabel completed habits = + habits + |> List.indexedMap + (\i { label, minutesDuration } -> + if Set.member ( weekdayLabel, label ) completed then + 0 + + else + minutesDuration + ) + |> List.sum + + +render : State.Model -> Html State.Msg +render { today, visibleDayOfWeek, completed, includeMorning, includeEvening } = + case ( today, visibleDayOfWeek ) of + ( Just todaysDate, Just visibleWeekday ) -> + let + todaysWeekday : Weekday + todaysWeekday = + Date.weekday todaysDate + + habits : List State.Habit + habits = + habitTypes + { includeMorning = includeMorning + , includeEvening = includeEvening + , date = todaysDate + } + |> List.map (\habitType -> habitsFor habitType todaysWeekday) + |> List.concat + in + div + [ Utils.class + [ Always "max-w-xl mx-auto py-6 px-6" + , When (todaysWeekday /= visibleWeekday) "pt-20" + ] + ] + [ header [] + [ if todaysWeekday /= visibleWeekday then + 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" ] + [ text "As you are not viewing today's habits, the UI is in read-only mode" ] + , UI.button + [ class "bg-blue-200 px-4 py-2 rounded text-blue-600 text-xs font-bold" + , onClick State.ViewToday + ] + [ text "View Today's Habits" ] + ] + + else + text "" + , div [ class "flex center" ] + [ UI.button + [ class "w-1/4 text-gray-500" + , onClick State.ViewPrevious + ] + [ text "‹ previous" ] + , h1 [ class "font-bold text-blue-500 text-3xl text-center w-full" ] + [ text (weekdayLabelFor visibleWeekday) ] + , UI.button + [ class "w-1/4 text-gray-500" + , onClick State.ViewNext + ] + [ text "next ›" ] + ] + ] + , if todaysWeekday == visibleWeekday then + p [ class "text-center pt-1 pb-4" ] + [ let + t : Int + t = + timeRemaining (weekdayLabelFor todaysWeekday) completed habits + in + if t == 0 then + text "Nothing to do!" + + else + text + ((habits + |> timeRemaining (weekdayLabelFor todaysWeekday) completed + |> String.fromInt + ) + ++ " minutes remaining" + ) + ] + + else + text "" + , if todaysWeekday == visibleWeekday then + div [] + [ UI.button + [ onClick + (if Set.size completed == 0 then + State.DoNothing + + else + State.ClearAll + ) + , Utils.class + [ Always "ml-10 px-3" + , If (Set.size completed == 0) + "text-gray-500 cursor-not-allowed" + "text-red-500 underline cursor-pointer" + ] + ] + [ let + numCompleted : Int + numCompleted = + habits + |> List.indexedMap (\i { label } -> ( i, label )) + |> List.filter + (\( i, label ) -> + Set.member + ( weekdayLabelFor todaysWeekday, label ) + completed + ) + |> List.length + in + if numCompleted == 0 then + text "Clear" + + else + 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 + text "" + , ul [ class "pb-10" ] + (habits + |> List.indexedMap + (\i { label, minutesDuration } -> + let + isCompleted : Bool + isCompleted = + Set.member ( weekdayLabelFor todaysWeekday, label ) completed + in + li [ class "text-xl list-disc ml-6" ] + [ if todaysWeekday == visibleWeekday then + UI.button + [ class "py-5 px-3" + , onClick + (State.ToggleHabit + (weekdayLabelFor todaysWeekday) + label + ) + ] + [ span + [ Utils.class + [ Always "text-white pt-1 px-2 rounded" + , If isCompleted "bg-gray-400" "bg-blue-500" + ] + ] + [ text (String.fromInt minutesDuration ++ " mins") ] + , p + [ Utils.class + [ Always "inline pl-3" + , When isCompleted "line-through text-gray-400" + ] + ] + [ text label ] + ] + + else + UI.button + [ class "py-5 px-3 cursor-not-allowed" + , onClick State.DoNothing + ] + [ text label ] + ] + ) + ) + , footer [ class "bg-white text-sm text-center text-gray-500 fixed bottom-0 left-0 w-full py-4" ] + [ p [] [ text "This app is brought to you by William Carroll." ] + , 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." ] diff --git a/website/habit-screens/src/Main.elm b/website/habit-screens/src/Main.elm new file mode 100644 index 000000000000..2ddedb913357 --- /dev/null +++ b/website/habit-screens/src/Main.elm @@ -0,0 +1,29 @@ +module Main exposing (main) + +import Browser +import Habits +import Html exposing (..) +import State +import Time + + +subscriptions : State.Model -> Sub State.Msg +subscriptions model = + -- once per minute + Time.every (1000 * 60) (\_ -> State.MaybeAdjustWeekday) + + +view : State.Model -> Html State.Msg +view model = + case model.view of + State.Habits -> + Habits.render model + + +main = + Browser.element + { init = \() -> State.init + , subscriptions = subscriptions + , update = State.update + , view = view + } diff --git a/website/habit-screens/src/State.elm b/website/habit-screens/src/State.elm new file mode 100644 index 000000000000..c75c99322249 --- /dev/null +++ b/website/habit-screens/src/State.elm @@ -0,0 +1,195 @@ +module State exposing (..) + +import Date exposing (Date) +import Set exposing (Set) +import Task +import Time exposing (Weekday(..)) + + +type alias WeekdayLabel = + String + + +type alias HabitLabel = + String + + +type Msg + = DoNothing + | SetView View + | ReceiveDate Date + | ToggleHabit WeekdayLabel HabitLabel + | MaybeAdjustWeekday + | ViewToday + | ViewPrevious + | ViewNext + | ClearAll + | ToggleMorning + | ToggleEvening + + +type View + = Habits + + +type HabitType + = Morning + | Evening + | DayOfWeek + | Payday + | FirstOfTheMonth + | FirstOfTheYear + + +type alias Habit = + { label : HabitLabel + , habitType : HabitType + , minutesDuration : Int + } + + +type alias CompletedHabits = + Set ( WeekdayLabel, HabitLabel ) + + +type alias Model = + { isLoading : Bool + , view : View + , today : Maybe Date + , completed : CompletedHabits + , visibleDayOfWeek : Maybe Weekday + , includeMorning : Bool + , includeEvening : Bool + } + + +previousDay : Weekday -> Weekday +previousDay weekday = + case weekday of + Mon -> + Sun + + Tue -> + Mon + + Wed -> + Tue + + Thu -> + Wed + + Fri -> + Thu + + Sat -> + Fri + + Sun -> + Sat + + +nextDay : Weekday -> Weekday +nextDay weekday = + case weekday of + Mon -> + Tue + + Tue -> + Wed + + Wed -> + Thu + + Thu -> + Fri + + Fri -> + Sat + + Sat -> + Sun + + Sun -> + Mon + + +{-| The initial state for the application. +-} +init : ( Model, Cmd Msg ) +init = + ( { isLoading = False + , view = Habits + , today = Nothing + , completed = Set.empty + , visibleDayOfWeek = Nothing + , includeMorning = False + , includeEvening = False + } + , Date.today |> Task.perform ReceiveDate + ) + + +{-| Now that we have state, we need a function to change the state. +-} +update : Msg -> Model -> ( Model, Cmd Msg ) +update msg ({ today, visibleDayOfWeek, completed } as model) = + case msg of + DoNothing -> + ( model, Cmd.none ) + + SetView x -> + ( { model + | view = x + , isLoading = True + } + , Cmd.none + ) + + ReceiveDate x -> + ( { model + | today = Just x + , visibleDayOfWeek = Just (Date.weekday x) + } + , Cmd.none + ) + + ToggleHabit weekdayLabel habitLabel -> + ( { model + | completed = + if Set.member ( weekdayLabel, habitLabel ) completed then + Set.remove ( weekdayLabel, habitLabel ) completed + + else + Set.insert ( weekdayLabel, habitLabel ) completed + } + , Cmd.none + ) + + MaybeAdjustWeekday -> + ( model, Date.today |> Task.perform ReceiveDate ) + + ViewToday -> + ( { model | visibleDayOfWeek = today |> Maybe.map Date.weekday }, Cmd.none ) + + ViewPrevious -> + ( { model + | visibleDayOfWeek = visibleDayOfWeek |> Maybe.map previousDay + } + , Cmd.none + ) + + ViewNext -> + ( { model + | visibleDayOfWeek = visibleDayOfWeek |> Maybe.map nextDay + } + , Cmd.none + ) + + ClearAll -> + ( { model | completed = Set.empty }, Cmd.none ) + + ToggleMorning -> + ( { model | includeMorning = not model.includeMorning }, Cmd.none ) + + ToggleEvening -> + ( { model | includeEvening = not model.includeEvening }, Cmd.none ) diff --git a/website/habit-screens/src/UI.elm b/website/habit-screens/src/UI.elm new file mode 100644 index 000000000000..5b5426913570 --- /dev/null +++ b/website/habit-screens/src/UI.elm @@ -0,0 +1,9 @@ +module UI exposing (..) + +import Html exposing (..) +import Html.Attributes exposing (..) + + +button : List (Attribute msg) -> List (Html msg) -> Html msg +button attrs children = + Html.button ([ class "focus:outline-none" ] ++ attrs) children diff --git a/website/habit-screens/src/Utils.elm b/website/habit-screens/src/Utils.elm new file mode 100644 index 000000000000..23b13c224c68 --- /dev/null +++ b/website/habit-screens/src/Utils.elm @@ -0,0 +1,37 @@ +module Utils exposing (..) + +import Html exposing (..) +import Html.Attributes exposing (..) +import Maybe.Extra + + +type Strategy + = Always String + | When Bool String + | If Bool String String + + +class : List Strategy -> Attribute msg +class classes = + classes + |> List.map + (\strategy -> + case strategy of + Always x -> + Just x + + When True x -> + Just x + + When False _ -> + Nothing + + If True x _ -> + Just x + + If False _ x -> + Just x + ) + |> Maybe.Extra.values + |> String.join " " + |> Html.Attributes.class |