Move the habit-screens project into //website
I'd like to eventually deploy this to wpcarro.dev. Coming soon!
This commit is contained in:
parent
3feb8ceb9a
commit
9e2fbfde8e
17 changed files with 149 additions and 18 deletions
2
website/habit-screens/.envrc
Normal file
2
website/habit-screens/.envrc
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
source_up
|
||||
use_nix
|
||||
3
website/habit-screens/.gitignore
vendored
Normal file
3
website/habit-screens/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
/elm-stuff
|
||||
/Main.min.js
|
||||
/output.css
|
||||
31
website/habit-screens/README.md
Normal file
31
website/habit-screens/README.md
Normal file
|
|
@ -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`!
|
||||
53
website/habit-screens/default.nix
Normal file
53
website/habit-screens/default.nix
Normal file
|
|
@ -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;
|
||||
}
|
||||
|
||||
43
website/habit-screens/design.md
Normal file
43
website/habit-screens/design.md
Normal file
|
|
@ -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
|
||||
77
website/habit-screens/elm-srcs.nix
Normal file
77
website/habit-screens/elm-srcs.nix
Normal file
|
|
@ -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";
|
||||
};
|
||||
}
|
||||
32
website/habit-screens/elm.json
Normal file
32
website/habit-screens/elm.json
Normal file
|
|
@ -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": {}
|
||||
}
|
||||
}
|
||||
3
website/habit-screens/index.css
Normal file
3
website/habit-screens/index.css
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
21
website/habit-screens/index.html
Normal file
21
website/habit-screens/index.html
Normal file
|
|
@ -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>
|
||||
BIN
website/habit-screens/registry.dat
Normal file
BIN
website/habit-screens/registry.dat
Normal file
Binary file not shown.
10
website/habit-screens/shell.nix
Normal file
10
website/habit-screens/shell.nix
Normal file
|
|
@ -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
|
||||
];
|
||||
}
|
||||
465
website/habit-screens/src/Habits.elm
Normal file
465
website/habit-screens/src/Habits.elm
Normal file
|
|
@ -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." ]
|
||||
29
website/habit-screens/src/Main.elm
Normal file
29
website/habit-screens/src/Main.elm
Normal file
|
|
@ -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
|
||||
}
|
||||
195
website/habit-screens/src/State.elm
Normal file
195
website/habit-screens/src/State.elm
Normal file
|
|
@ -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 )
|
||||
9
website/habit-screens/src/UI.elm
Normal file
9
website/habit-screens/src/UI.elm
Normal file
|
|
@ -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
|
||||
37
website/habit-screens/src/Utils.elm
Normal file
37
website/habit-screens/src/Utils.elm
Normal file
|
|
@ -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
|
||||
Loading…
Add table
Add a link
Reference in a new issue