Begin working on Habit Screens project
Created a small MVP for digitizing my weekly habits. Much more to come. Lots of things happening: - Copied the boilerplate to get started - Added a brief project-level README - Outlined my ambitions in design.md See README and design.md for more context on this project.
This commit is contained in:
parent
02ce74eada
commit
9d331f3077
12 changed files with 414 additions and 0 deletions
12
scratch/habit-screens/README.md
Normal file
12
scratch/habit-screens/README.md
Normal file
|
@ -0,0 +1,12 @@
|
|||
# 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").
|
2
scratch/habit-screens/client/.envrc
Normal file
2
scratch/habit-screens/client/.envrc
Normal file
|
@ -0,0 +1,2 @@
|
|||
source_up
|
||||
use_nix
|
3
scratch/habit-screens/client/.gitignore
vendored
Normal file
3
scratch/habit-screens/client/.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
/elm-stuff
|
||||
/Main.min.js
|
||||
/output.css
|
18
scratch/habit-screens/client/README.md
Normal file
18
scratch/habit-screens/client/README.md
Normal file
|
@ -0,0 +1,18 @@
|
|||
# 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`!
|
32
scratch/habit-screens/client/elm.json
Normal file
32
scratch/habit-screens/client/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
scratch/habit-screens/client/index.css
Normal file
3
scratch/habit-screens/client/index.css
Normal file
|
@ -0,0 +1,3 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
15
scratch/habit-screens/client/index.html
Normal file
15
scratch/habit-screens/client/index.html
Normal file
|
@ -0,0 +1,15 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Elm SPA</title>
|
||||
<link rel="stylesheet" href="./output.css" />
|
||||
<script src="./Main.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="mount"></div>
|
||||
<script>
|
||||
Elm.Main.init({node: document.getElementById("mount")});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
10
scratch/habit-screens/client/shell.nix
Normal file
10
scratch/habit-screens/client/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
|
||||
];
|
||||
}
|
169
scratch/habit-screens/client/src/Habits.elm
Normal file
169
scratch/habit-screens/client/src/Habits.elm
Normal file
|
@ -0,0 +1,169 @@
|
|||
module Habits exposing (render)
|
||||
|
||||
import Browser
|
||||
import Html exposing (..)
|
||||
import Html.Attributes exposing (..)
|
||||
import Html.Events exposing (..)
|
||||
import Set
|
||||
import State
|
||||
import Time exposing (Weekday(..))
|
||||
|
||||
|
||||
morning : List State.Habit
|
||||
morning =
|
||||
[ "Make bed"
|
||||
, "Brush teeth"
|
||||
, "Shower"
|
||||
, "Do push-ups"
|
||||
, "Meditate"
|
||||
]
|
||||
|
||||
|
||||
evening : List State.Habit
|
||||
evening =
|
||||
[ "Read (30 minutes)"
|
||||
, "Record in State.Habit Journal"
|
||||
]
|
||||
|
||||
|
||||
monday : List State.Habit
|
||||
monday =
|
||||
[ "Bikram Yoga @ 17:00 (90 min)"
|
||||
]
|
||||
|
||||
|
||||
tuesday : List State.Habit
|
||||
tuesday =
|
||||
[ "Bikram Yoga @ 18:00 (90 min)"
|
||||
]
|
||||
|
||||
|
||||
wednesday : List State.Habit
|
||||
wednesday =
|
||||
[ "Shave"
|
||||
, "Bikram Yoga @ 17:00 (90 min)"
|
||||
]
|
||||
|
||||
|
||||
thursday : List State.Habit
|
||||
thursday =
|
||||
[]
|
||||
|
||||
|
||||
friday : List State.Habit
|
||||
friday =
|
||||
[ "Bikram Yoga @ 17:00 (60 min)"
|
||||
, "Take-out trash"
|
||||
, "Shop for groceries"
|
||||
]
|
||||
|
||||
|
||||
saturday : List State.Habit
|
||||
saturday =
|
||||
[ "Nap"
|
||||
]
|
||||
|
||||
|
||||
sunday : List State.Habit
|
||||
sunday =
|
||||
[ "Shampoo"
|
||||
, "Shave"
|
||||
, "Trim nails"
|
||||
, "Combine trash cans"
|
||||
, "Mop tile and wood floors"
|
||||
, "Laundry"
|
||||
, "Vacuum bedroom"
|
||||
, "Dust surfaces"
|
||||
, "Clean mirrors"
|
||||
, "Clean desk"
|
||||
]
|
||||
|
||||
|
||||
weekdayName : Weekday -> String
|
||||
weekdayName weekday =
|
||||
case weekday of
|
||||
Mon ->
|
||||
"Monday"
|
||||
|
||||
Tue ->
|
||||
"Tuesday"
|
||||
|
||||
Wed ->
|
||||
"Wednesday"
|
||||
|
||||
Thu ->
|
||||
"Thursday"
|
||||
|
||||
Fri ->
|
||||
"Friday"
|
||||
|
||||
Sat ->
|
||||
"Saturday"
|
||||
|
||||
Sun ->
|
||||
"Sunday"
|
||||
|
||||
|
||||
habitsFor : Weekday -> List State.Habit
|
||||
habitsFor weekday =
|
||||
case weekday of
|
||||
Mon ->
|
||||
monday
|
||||
|
||||
Tue ->
|
||||
tuesday
|
||||
|
||||
Wed ->
|
||||
wednesday
|
||||
|
||||
Thu ->
|
||||
thursday
|
||||
|
||||
Fri ->
|
||||
friday
|
||||
|
||||
Sat ->
|
||||
saturday
|
||||
|
||||
Sun ->
|
||||
sunday
|
||||
|
||||
|
||||
tailwind : List ( String, Bool ) -> Attribute msg
|
||||
tailwind classes =
|
||||
classes
|
||||
|> List.filter (\( k, v ) -> v)
|
||||
|> List.map (\( k, v ) -> k)
|
||||
|> String.join " "
|
||||
|> class
|
||||
|
||||
|
||||
render : State.Model -> Html State.Msg
|
||||
render { dayOfWeek, completed } =
|
||||
case dayOfWeek of
|
||||
Nothing ->
|
||||
p [] [ text "Unable to display habits because we do not know what day of the week it is." ]
|
||||
|
||||
Just weekday ->
|
||||
div [ class "font-mono py-6 px-6" ]
|
||||
[ h1 [ class "text-2xl text-center" ] [ text (weekdayName weekday) ]
|
||||
, ul []
|
||||
(weekday
|
||||
|> habitsFor
|
||||
|> List.indexedMap
|
||||
(\i x ->
|
||||
li [ class "text-xl" ]
|
||||
[ button
|
||||
[ class "py-5 px-6"
|
||||
, tailwind
|
||||
[ ( "line-through"
|
||||
, Set.member i completed
|
||||
)
|
||||
]
|
||||
, onClick (State.ToggleHabit i)
|
||||
]
|
||||
[ text x ]
|
||||
]
|
||||
)
|
||||
)
|
||||
]
|
27
scratch/habit-screens/client/src/Main.elm
Normal file
27
scratch/habit-screens/client/src/Main.elm
Normal file
|
@ -0,0 +1,27 @@
|
|||
module Main exposing (main)
|
||||
|
||||
import Browser
|
||||
import Habits
|
||||
import Html exposing (..)
|
||||
import State
|
||||
|
||||
|
||||
subscriptions : State.Model -> Sub State.Msg
|
||||
subscriptions model =
|
||||
Sub.none
|
||||
|
||||
|
||||
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
|
||||
}
|
80
scratch/habit-screens/client/src/State.elm
Normal file
80
scratch/habit-screens/client/src/State.elm
Normal file
|
@ -0,0 +1,80 @@
|
|||
module State exposing (..)
|
||||
|
||||
import Date
|
||||
import Set exposing (Set)
|
||||
import Task
|
||||
import Time exposing (Weekday(..))
|
||||
|
||||
|
||||
type Msg
|
||||
= DoNothing
|
||||
| SetView View
|
||||
| ReceiveDate Date.Date
|
||||
| ToggleHabit Int
|
||||
|
||||
|
||||
type View
|
||||
= Habits
|
||||
|
||||
|
||||
type HabitType
|
||||
= Daily
|
||||
| Weekly
|
||||
| Yearly
|
||||
|
||||
|
||||
type alias Habit =
|
||||
String
|
||||
|
||||
|
||||
type alias Model =
|
||||
{ isLoading : Bool
|
||||
, view : View
|
||||
, dayOfWeek : Maybe Weekday
|
||||
, completed : Set Int
|
||||
}
|
||||
|
||||
|
||||
{-| The initial state for the application.
|
||||
-}
|
||||
init : ( Model, Cmd Msg )
|
||||
init =
|
||||
( { isLoading = False
|
||||
, view = Habits
|
||||
, dayOfWeek = Nothing
|
||||
, completed = Set.empty
|
||||
}
|
||||
, 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 ({ completed } as model) =
|
||||
case msg of
|
||||
DoNothing ->
|
||||
( model, Cmd.none )
|
||||
|
||||
SetView x ->
|
||||
( { model
|
||||
| view = x
|
||||
, isLoading = True
|
||||
}
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
ReceiveDate x ->
|
||||
( { model | dayOfWeek = Just Sun }, Cmd.none )
|
||||
|
||||
ToggleHabit i ->
|
||||
( { model
|
||||
| completed =
|
||||
if Set.member i completed then
|
||||
Set.remove i completed
|
||||
|
||||
else
|
||||
Set.insert i completed
|
||||
}
|
||||
, Cmd.none
|
||||
)
|
43
scratch/habit-screens/design.md
Normal file
43
scratch/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
|
Loading…
Reference in a new issue