subtree(users/wpcarro): docking briefcase at '24f5a642'

git-subtree-dir: users/wpcarro
git-subtree-mainline: 464bbcb15c
git-subtree-split: 24f5a642af
Change-Id: I6105b3762b79126b3488359c95978cadb3efa789
This commit is contained in:
Vincent Ambo 2021-12-14 01:51:19 +03:00
commit 019f8fd211
766 changed files with 175420 additions and 0 deletions

View file

@ -0,0 +1,11 @@
# wpcarro.dev
https://wpcarro.dev is my personal website. I expose a few subdomains, one of
which you are probably visiting right now, git.wpcarro.dev. Here are some of
the others:
- `blog.wpcarro.dev`: My personal blog
- `learn.wpcarro.dev`: Teaching others to code
- `sandbox.wpcarro.dev`: Where I deploy some pet projects and code sketches
Visit https://wpcarro.dev for a sitemap.

View file

@ -0,0 +1,2 @@
source_up
use_nix

View file

@ -0,0 +1,6 @@
---
title: "{{ replace .Name "-" " " | title }}"
date: {{ .Date }}
draft: true
---

View file

@ -0,0 +1,25 @@
baseURL = "https://blog.wpcarro.dev"
disqusShortname = "blog-wpcarro-dev"
languageCode = "en-us"
title = "blog.wpcarro.dev"
theme = "tailwind"
pygmentsCodeFences = true
pygmentsUseClasses = true
[taxonomies]
tag = "tags"
[permalinks]
posts = "/posts/:year/:month/:title/"
[params]
author = "William Carroll"
description = "Loosely structured streams of consciousness"
tagline = "Loosely structured streams of consciousness"
[languages]
[languages.en]
contentDir = "content/english"
languageCode = "en-us"
languageName = "English"
weight = 1

View file

@ -0,0 +1,5 @@
---
title: "Caffeine"
date: 2020-03-11T22:50:40Z
draft: true
---

View file

@ -0,0 +1,280 @@
---
title: "Cell Phone Experiment"
date: 2020-04-02T02:02:07Z
draft: false
---
### TL;DR
I will not use my cell phone during March to learn more about how much I depend
on it.
### Explore/Exploit
Ever since I read Charles Duhigg's book, [The Power of Habit](poh), I try to
habituate as many aspects of my life as I can.
Making my bed every morning is an example of a habit -- so too is flossing at
night before bed.
The *exploit* axis of the [explore/exploit tradeoff](exp-exp) endows habits with
their power. Brian Christian and Tom Griffiths explain this concept more clearly
than I can in Chapter 2 of their exceptional book, [Algorithms to Live
By](algos).
Habits are powerful, but if I overly exploit an activity, I may settle on a
local optimum in lieu of settling on a global optimum; these are the opportunity
costs of exploiting (i.e. habits) versus exploring (i.e. spontaneity).
But what if it was possible to habituate exploration?
### Monthly challenges
Every month since October 2018, I challenge myself to try something new. In the
past, monthly challenges have been things like:
- sign up and take Brazilian Jiu Jitsu classes
- buy a guitar and learn [Freight Train](https://www.youtube.com/watch?v=IUK8emiWabU)
- study Italian
- learn a handstand
Typically for an activity to qualify as a challenge, I must spend *at least
fifteen minutes* working on it *at least five days* each week.
This month (i.e. March) I'm challenging myself to avoid using my cell phone.
My parents gave me a cell phone when when I was a freshman in High School; I was
14 years old. I am now 28, so I have been using a cell phone semi-daily for over
a decade.
While I enjoy the convenience that my cell phone provides me, I am curious to
suspend my usage to more clearly understand how much I depend on it...
### April
Now it is early April, and I completed March's challenge. So how was it?
Below I outline the parts of using a cell phone that I missed and the parts that
I surprisingly did not miss. I will also mention the two times that I used my
cell phone and why.
The first three things that I missed all relate to time.
#### Timekeeping
On the first day I realized that unless I was near a computer, I did not know
what time it was.
I exclusively use my cell phone as my watch; I do not wear a watch. To adapt, I
started looking for clocks around my office and while I was taking public
transportation. Thankfully London posts the current time on the digital train
schedules. This oriented me while I was traveling, which was also when I needed
to know the time the most.
Most of the month, however, I never precisely knew what time it was.
#### Alarm clocks
While I anticipated living without an alarm clock prior to the experiment, I
decided against buying a substitute. Prior to this month, I theorized that
morning alarms probably disrupt the quality of my sleep. If I'm tired, shouldn't
I keep sleeping?
As the month progressed and my 24 hour day morphed into a 25 hour day, I learned
that I would prefer waking up at a set time every day and synchronize my
schedule with the rest of my timezone.
I am still unsure if alarm clocks are helpful in the long-term. I would have
slept with the curtains drawn to allow the morning sun to wake me
up. Unfortunately, I live on the ground floor nearby a brightly shining street
lamp that spills into my bedroom.
If I lived somewhere more remote (perhaps even a suburb would do) I would like
to repeat an experiment where I live for a month without an alarm clock.
For now, I must return to the Temple of Chronology and supplicate until Father
Time restores my sanity.
#### Timers
Using timers motivates me to do a bunch of short tasks like cleaning my flat for
fifteen minutes, stretching, or reading before bed. Thankfully, I already owned
a physical timer that I keep in my kitchen. This replaced the timer on my phone
without disrupting my routine.
#### Maps
Speaking of being disoriented, what about living without maps software? On the
few occasions where I traveled somewhere that was unfamiliar to me, I had to
memorize the directions from my computer before I departed.
At least I didn't need to visit gas stations or museums to buy trifold tourist
maps...
I once left my office mistakenly assuming that I would download the directions
to my destination while commuting. As I awaited the office elevator, I realized
that I had no clue where I was heading.
Thankfully I wasn't far from the safety, comfort, and familiarity of my desktop
computer -- with its fatty WiFi connection. In no time I was studying Google
Maps in my web browser and memorizing the directions.
Overall this was hardly an inconvenience, and I think I even enjoyed
stress-testing my memory: a job that I so often outsource to hardware.
#### Rendezvouses
A couple of times I met friends in various parts of the city. Organizing these
particular rendezvouses was a novel (read: anachronistic) experience. For all
you young whippersnappers reading, take out your stone tablets and chisels. I'm
going to explain how this works:
First I would tell my friends where and when to meet me. I emphasized that I
would be quite helpless to any changes they might make to the plans once I began
commuting, which made the commitments unusually more binding.
On one occasion my friend -- who is characteristically prompt, and even chides
me for when I'm late -- was twenty minutes late for our engagement. My friend is
German, so I figured I should do my civic duty of alerting the German embassy
that my friend had broken German code, is obscenely late, and should therefore
hand-in his passport and renounce his citizenship. After awhile my conscience
advised me to reconsider.
It was fortunate for both of us that I did not fully understand how late he was.
Remember: I didn't know what time it was.
I decided this would be a useful opportunity to test my patience, so I loitered
for twenty minutes outside of our meeting point. He couldn't text me to tell me
that he was late. I couldn't listen to music, call family or friends, or partake
in any of the other rituals that modern-day loiterers observe to pass the
time. In the end he showed up, and it was scarcely a big deal.
This experience made me wonder what the policy for abandoning plans is when
someone is running late. Before smart phones, how long did people wait? Maybe
the proper etiquette is to wait long enough for you to absolve yourself of the
guilt of flaking in the unlikely event that your friend arrives shortly after
you leave.
So... thirty minutes? I'll call my grandma tomorrow and ask her.
#### Boredom
My phone couldn't entertain me while I queued at the grocery store. Same too
when I commuted.
I also found myself listening to less music than I usually do. I decided to read
to occupy the void when I could; this helped me progress towards completing this
year's [GoodReads challenge][gr-annual].
### Cheating
I used my phone twice during March.
1. Once to use my bank's mobile app to internationally transfer money from my
U.K. account to my U.S. account. I could have used [TransferWise's][tw]
website, but I didn't.
2. Another time I used my phone to take pictures of an item that I wanted to
sell on [CraigsList][cl]. I could have and perhaps should have used my laptop's
webcam, but at the time, I didn't want to. I am accustomed to using my phone
to take pictures, and I wanted to sell something.
In both of these cases, prior habits eroded my resolve to stay the course. These
are useful reminders that habits don't distinguish between helpful and hurtful;
they just exist.
In total I would estimate that I spent somewhere around fifteen minutes using
my phone in March. While not perfect:
> Better a diamond with a flaw than a pebble without (Confucius)
### Substitution = Dilution
While the explicit goal of this challenge was to avoid using my cell phone for a
month, the implicit goal was to disengage from many of the
[nonessential][essentialism] activities that compete for my attention.
There were some activities that I didn't miss while living without a cell
phone. This wasn't because I don't value these activities, but rather because I
can adequately replace them with alternatives.
For texting and making phone calls, I used [Telegram][wtf-telegram]. Telegram
helped me sustain a healthy relationship with my girlfriend while still honoring
the constraints of the challenge.
While I appreciated the convenience Telegram provided, I felt that I remained
about as [available][wtf-availability] during March as I was in February. If I
ever experiment with drastically reducing my availability, I will be more
explicit about my objectives.
### Distraction displacement (whack-a-mole)
Because cell phones and other electronics have conditioned my behavior, I
habitually avoid boredom and seek entertainment. On its face this may not sound
like a harmful practice. My generation drills the aphorism "you only live once",
suggesting that we may want to embrace a Hedonistic lifestyle.
Hedonism may or may not be a wise way to play the game of Life. All I know is
that living a life in which I am often stimulated but proportionately distracted
appeals increasingly less to me as time progresses.
During March I noticed that once I freed my attention from sending/receiving
texts, my brain quickly reassigned my attention to maintaining a vigil over the
other social media outposts that I maintain.
I should also admit that I habitually checked Telegram now that it served as my
new cell phone. Didn't see that coming...
In another case, once I discovered that I could use Instagram in a web browser
instead of on my phone, I filled my newfound time and attention on
[Instagram.com][ig] (don't click!): displacing the time that I spent on an app
on my phone to time that I spent on a website in a web browser.
Holy whack-a-mole!
Halfway through the month, I wrote a [program to block websites][url-blocker] on
my computer. Surprisingly this worked and forced me to more deliberately fill
this hard-fought, foreign time with other activities.
### Easy come, easy go?
As the saying for making friends goes, "easy come, easy go", implying that
friendships that you easily form can just as easily be destroyed.
Habits invert this creation/destruction relationship. In my experience "easy
come" implies "difficult to go".
For example, I could easily form the habit of eating chocolate around 15:00 at
work; curbing this habit would require more effort. When I compare this to the
difficulty I experienced habituating a meditation practice, and how easily I
can dislodge my meditation practice, it seems to me that the laws of habits
dictate "easy come, difficult go; difficult come, easy go".
I suspect that while my cravings for using a cell phone have temporarily ceased,
they will return shortly after I start using my cell phone. And as if nothing
happened, I return to where I was at the end of February just before I decided
to curb my cell phone usage.
Because of this, I'm planning on keeping my cell phone in my closet where I
stored it during the month of March. As noted, enough substitutes exist for me
to live a mostly normal life: one where I am not unnecessarily straining the
relationships of my friends and my family. After all these are the people who
matter most to me and those who drive me to explore new ways to improve.
I recognize that the "self" in self-experimentation is a misnomer. Can you truly
conduct an [N of 1 trial][nof1]? My decisions impact the people in my life, and
I want to thank everyone who tolerates my eccentric and oftentimes annoying
experimentation.
Thank you for reading.
[pod]: https://www.goodreads.com/book/show/12609433-the-power-of-habit
[exp-exp]: https://en.wikipedia.org/wiki/Multi-armed_bandit
[algos]: https://www.goodreads.com/book/show/25666050-algorithms-to-live-by
[gr-annual]: https://www.goodreads.com/user_challenges/19737920
[cl]: http://craigslist.com
[tw]: https://transferwise.com
[url-blocker]: https://github.com/wpcarro/url-blocker
[wtf-telegram]: https://telegram.org
[wtf-availability]: https://landing.google.com/sre/sre-book/chapters/availability-table
[essentialism]: https://www.goodreads.com/book/show/18077875-essentialism
[ig]: https://instagram.com
[nof1]: https://en.wikipedia.org/wiki/N_of_1_trial

View file

@ -0,0 +1,49 @@
---
title: "Lets Learn Nix Caching"
date: 2020-03-17T18:05:38Z
draft: true
---
## TL;DR
1. I use `NixOS/nixpkgs-channels` instead of `NixOS/nixpkgs` and avoid
`nix-channel`.
## More information
- By default the Nix package manager uses cache.nixos.org as a binary cache.
- Visit status.nixos.org
- `git clone git@github.com:NixOS/nixpkgs-channels` instead of
`NixOS/nixpkgs`. The former mirrors the latter and uses Git branches to track
the published channels.
## What is a Nix channel
If you run...
```shell
$ git clone git@github.com:NixOS/nixpkgs ~/nixpkgs
$ export NIX_PATH="nixpkgs=$(realpath ~/nixpkgs)"
```
One benefit to cloning nixpkgs is that you can browse the source code on your
machine using tools like `git` and `emacs`. You can also experimentally patch
and test Nix code this way.
If any of the above appeals to you, clone `nixpkgs-channels` instead.
The Nix maintainers build and test the commits from `nixpkgs` using Hydra. Tests
include reproducibility tests, etc.
Various channels have different verification phases.
The cache at cache.nixos.org is populate the cache at cache.nixos.org.
You want to increase the likelihood that you are hitting this cache. For
example, `google-chrome` takes hours to build.
## What is a binary cache?
## What is Hydra (Nix CI)?
## What is Cachix?

View file

@ -0,0 +1,121 @@
---
title: "Lets Learn Nix: Reproducibility"
date: 2020-03-17T12:06:47Z
draft: true
---
I am dedicating this page to defining and disambiguating some terminology. I
think it is important to use these terms precisely, so it may be worthwhile to
memorize these definitions and ensure that you are clarifying the discourse
rather than muddying it.
## Terms
- repeatable build:
- reproducible build:
- deterministic build:
- pure function:
- impure function:
- idempotent function:
TODO(wpcarro): Consistently and deliberately use reproducible and
deterministic.
## Repeatable vs. Reproducible
Is NixOS reproducible? Visit [@grhmc][who-grhmc]'s website,
[r13y.com](https://r13y.com), to find out.
At the time of this writing, 1519 of 1568 (i.e. 96.9%) of the paths in the
`nixos.iso_minimal.x86_64-linux` installation image are reproducible.
## What hinders reproducibility?
Timestamps.
If package A encodes a timestamp into its build artifact, then we can
demonstrate that package A is *not reproducible* simply by building it at two
different times and doing a byte-for-byte comparison of the build artifacts.
## Does Nix protect developers against non-determinism
Yes. But not entirely. How?
## Deterministic Nix derivation
```nix
{ pkgs ? import <nixpkgs> {}, ... }:
with pkgs;
stdenv.mkDerivation {
name = "reproducible";
phases = [ "buildPhase" ];
buildPhase = "echo reproducible >$out";
}
```
## Non-deterministic Nix derivation
We can introduce some non-determinism into our build using the `date` function.
```nix
# file: /tmp/test.nix
{ pkgs ? import <nixpkgs> {}, ... }:
with pkgs;
stdenv.mkDerivation {
name = "non-reproducible";
phases = [ "buildPhase" ];
buildPhase = "date >$out";
}
```
Then run...
```shell
$ nix-build /tmp/test.nix
$ nix-build /tmp/test.nix --check --keep-failed
```
## How do you test reproducibility?
We can use `cmp` to compare files byte-for-byte. The following comparison should
fail:
```shell
$ echo foo >/tmp/a
$ echo bar >/tmp/b
$ cmp --silent /tmp/{a,b}
$ echo $?
```
And the following comparison should succeed:
```shell
$ echo hello >/tmp/a
$ echo hello >/tmp/b
$ cmp --silent /tmp/{a,b}
$ echo $?
```
## Reproducible vs. deterministic
Reproducible builds *are* deterministic builds and deterministic build
## Deterministic, Reproducible, Pure, Idempotent, oh my
- A pure function has no side-effects.
- An idempotent function can be executed more than once with the same arguments
without altering the side-effects.
- A deterministic function ensures that
## Deterministic vs. Reproducible
I can check if a build is reproducible using [these tools][wtf-repro-tools].
[wtf-repro-tools]: https://reproducible-builds.org/tools/
[who-grhmc]: https://twitter.com/grhmc

View file

@ -0,0 +1,401 @@
---
title: "Let's Learn Nix: Dotfiles"
date: 2020-03-13T22:23:02Z
draft: true
---
## Let's Learn Nix: Dotfiles
### Dependencies
Speaking of dependencies, here's what you should know before reading this tutorial.
- Basic Nix syntax: Nix 1p
What version of Nix are we using? What version of `<nixpkgs>` are we using? What
operating system are we using? So many variables...
Cartesian product of all possibilities...
TODO(wpcarro): Create a graphic of the options.
### The problems of dotfiles
How do you manage your dependencies?
You can use `stow` to install the dotfiles.
### home-manager
What we are going to write is most likely less preferable to the following
alternatives:
- using Nix home-manager
- committing your `.gitconfig` into your
In the next tutorial, we will use [home-manager][wtf-home-mgr] to replace the
functionality that we wrote.
So why bother completing this?
### Let's begin
Welcome to the first tutorial in the [Let's Learn Nix][wtf-lln] series. Today we
are going to create a Nix derivation for one of your dotfiles.
"Dotfiles" refers to a user's collection of configuration files. Typically these
files look like:
- `.vimrc`
- `.xsessionrc`
- `.bashrc`
The leading "dot" at the beginning gives dotfiles their name.
You probably have amassed a collection of dotfiles whether or not you are
aware. For example, if you use [git][wtf-git], the file `~/.gitconfig` should
exist on your machine. You can verify this with:
```shell
$ stat ~/.gitconfig
```
When I was first learning `git`, I learned to configure it using commands I
found in books and tutorials that often looked like:
```shell
$ git config user.email
```
The `~/.gitconfig` file on your machine may look something like this:
```.gitconfig
[user]
name = John Cleese
email = john@flying-circus.com
username = jcleese
[core]
editor = emacs
[web]
browser = google-chrome
[rerere]
enabled = 1
autoupdate = 1
[push]
default = matching
[color]
ui = auto
[alias]
a = add --all
ai = add -i
b = branch
cl = clone
cp = cherry-pick
d = diff
fo = fetch origin
lg = log --oneline --graph --decorate
ps = push
pb = pull --rebase
s = status
```
As I ran increasingly more `git config` commands to configure my `git`
preferences, the size of my `.gitconfig` increased, and the less likely I was to
remember which options I set to which values.
Thankfully a coworker at the time, Ryan ([@rschmukler][who-ryan]), told me that
he version-controlled his `.gitconfig` file along with his other configuration
files (e.g. `.vimrc`) in a repository he called "dotfiles".
Version-controlling your dotfiles improves upon a workflow where you have a
variety of configuration files scattered around your machine.
If you look at the above `.gitconfig`, can you spot the dependencies?
We explicitly depend `emacs` and `google-chrome`. We also *implicitly* depend on
`git`: there is not much value of having a `.gitconfig` file if you also do not
have `git` installed on your machine.
Dependencies:
- `emacs`
- `google-chrome`
Let's use Nix to generate this `.gitconfig` file. Here is what I would like our
API to be:
Let's create a file `gitconfig.nix` and build our function section-by-section:
TODO(wpcarro): Link to sections here
- options.user
- options.core
- options.web
- options.rerere
- options.push
- options.color
- options.alias
```shell
$ touch gitconfig.nix
```
### options.user
```haskell
AttrSet -> String
```
```nix
user = {
name = "John Cleese";
email = "john@flying-circus.com";
username = "jcleese";
};
```
```.gitconfig
[user]
name = John Cleese
email = john@flying-circus.com
username = jcleese
```
### options.core
```nix
core = {
editor = "${pkgs.emacs}/bin/emacs";
};
```
```.gitconfig
[core]
editor = /nix/store/<hash>-emacs-<version>/bin/emacs
```
### options.web
```nix
web.browser = "${pkgs.google-chrome}/bin/google-chrome";
```
```.gitconfig
[web]
browser = /nix/store/<hash>-google-chrome-<version>/bin/google-chrome
```
### options.rerere
```nix
rerere = {
enabled = true;
autoupdate = true;
};
```
```.gitconfig
[rerere]
enabled = 1
autoupdate = 1
```
### options.push
```nix
push.default = "matching";
```
```.gitconfig
[push]
default = matching
```
### options.color
```nix
color.ui = "auto";
```
```.gitconfig
[color]
ui = auto
```
We need to define a function named `gitconfig` that creates a Nix [derivation][wtf-derivation]:
```nix
# file: gitconfig.nix
let
# Import the <nixpkgs> package repository.
pkgs = import <nixpkgs> {};
# Stringify the attribute set, `xs`, as a multilined string formatted as "<key> = <value>".
# See attrsets.nix for more functions that work with attribute sets.
encodeAttrSet = xs: lib.concatStringsSep "\n" (lib.mapAttrsToList (k: v: "${k} = ${v}") xs);
# Define out function name `gitconfig` that accepts an `options` argument.
gitconfig = options: pkgs.stdenv.mkDerivation {
# The gitconfig file that Nix builds will be located /nix/store/some-hash-gitconfig.
name = "gitconfig";
src = pkgs.writeTextFile ".gitconfig" ''
[user]
name = ${options.user.name}
email = ${options.user.email}
username = ${options.user.username}
[core]
editor = ${options.core.editor}
[web]
editor = ${options.web.browser}
[rerere]
enabled = ${if options.rerere.enabled "1" else "0"}
autoupdate = ${if options.rerere.autoupdate "1" else "0"}
[push]
default = ${options.push.default}
[color]
ui = ${options.color.ui}
[alias]
${encodeAttrSet options.aliases}
'';
buildPhase = ''
${pkgs.coreutils}/bin/cp $src $out
'';
installPhase = ''
${pkgs.coreutils}/bin/ln -s $out ~/.gitconfig
'';
};
} in gitconfig {
user = {
name = "John Cleese";
email = "john@flying-circus.com";
username = "jcleese";
};
core = {
editor = "${pkgs.emacs}/bin/emacs";
};
web.browser = "${pkgs.google-chrome}/bin/google-chrome";
rerere = {
enabled = true;
autoupdate = true;
};
push.default = "matching";
color.ui = "auto";
aliases = {
a = "add --all";
ai = "add -i";
b = "branch";
cl = "clone";
cp = "cherry-pick";
d = "diff";
fo = "fetch origin";
lg = "log --oneline --graph --decorate";
ps = "push";
pb = "pull --rebase";
s = "status";
};
}
```
### options.alias
We want to write a function that accepts an attribute set and returns a
string. While Nix is a dynamically typed programming language, thinking in types
helps me clarify what I'm trying to write.
```haskell
encodeAttrSet :: AttrSet -> String
```
I prefer using a Haskell-inspired syntax for describing type signatures. Even if
you haven't written Haskell before, you may find the syntax intuitive.
Here is a non comprehensive, but demonstrative list of example type signatures:
- `[String]`: A list of strings (i.e. `[ "cogito" "ergo" "sum" ]`)
- `AttrSet`: A nix attribute set (i.e. `{ name = "John Cleese"; age = 80; }`).
- `add :: Integer -> Integer -> Integer`: A function named `add` that accepts
two integers and returns an integer.
Specifically, we want to make sure that when we call:
```nix
encodeAttrSet {
a = "add --all";
b = "branch";
}
```
...it returns a string that looks like this:
```.gitconfig
a = "add --all"
b = "branch"
```
TODO(wpcarro): @tazjin's nix-1p mentions this. Link to it.
Nix has useful functions scattered all over the place:
- `lib.nix`
- `list.nix`
- `lib.attrSet`
But I cannot recall exactly which functions we will need to write
`encodeAttrSet`. In these cases, I do the following:
1. Run `nix repl`.
2. Browse the Nix source code.
Google "nix attribute sets" and find the Github link to `attrsets.nix`.
You should consider repeating this search but instead of searching for
"attribute sets" search for "lists" and "strings". That is how I found the
functions needed to write `encodeAttrSet`. Let's return to our `nix repl`.
Load the nixpkgs set:
```nix
nix-repl> :l <nixpkgs>
Added 11484 variables.
```
Define a test input called `attrs`:
```nix
nix-repl> attrs = { fname = "John"; lname = "Cleese"; }
```
Map the attribute set into `[String]` using `lib.mapAttrsToList`:
```nix
nix-repl> lib.mapAttrsToList (k: v: "${k} = ${toString v}") attrs
[ "fname = John" "lname = Cleese" ]
```
Now join the `[String]` together using `lib.concatStringsSep`:
```nix
nix-repl> lib.concatStringsSep "\n" (lib.mapAttrsToList (k: v: "${k} = ${v}") attrs)
"fname = John\nlname = Cleese"
```
Now let's use this to define our function `encodeAttrSet`:
```nix
# file: gitconfig.nix
encodeAttrSet = xs: lib.concatStringsSep "\n" (lib.mapAttrsToList (k: v: "${k} = ${v}") xs);
```
### Using nixpkgs search
[Nixpkgs search][wtf-nixpkgs-search].
### Conclusion
We learned how to help ourselves.
- Where does `emacs` exist? What about `google-chrome`? [nixpkgs search][wtf-nixpkgs-search]
- Verify that I have it? [nix REPL][using-nix-repl]
We used Nix to create our first derivation.
[wtf-lln]: /lets-learn-nix
[wtf-git]: https://git-scm.com/
[wtf-derivation]: https://nixos.org/nixos/nix-pills/our-first-derivation.html
[wtf-nixpkgs-search]: https://nixos.org/nixos/packages.html?channel=nixos-19.09
[using-nix-repl]: /using-the-nix-repl
[wtf-home-mgr]: https://github.com/rycee/home-manager
[who-ryan]: https://twitter.com/rschmukler

View file

@ -0,0 +1,41 @@
---
title: "Lets Learn Nix: Tutorial Reproducibility"
date: 2020-03-17T18:34:58Z
draft: true
---
## Install Nix
Link to nixos page.
## The rest
Start with this...
```shell
$ mkdir ~/lets-learn-nix
$ cd ~/lets-learn-nix
```
...done. Copy the following and paste it into a file name `shell.nix`.
```nix
# file: shell.nix
let
pkgs = import (builtins.fetchGit {
url = "https://github.com/NixOS/nixpkgs-channels";
ref = "refs/heads/nixos-19.09";
}) {}
in pkgs.mkShell {
buildInputs = with pkgs; [
git
];
NIX_PATH = "nixpkgs=${pkgs}";
};
```
...then...
```shell
$ nix-shell
```

View file

@ -0,0 +1,58 @@
---
title: "Lets Learn Nix"
date: 2020-03-13T21:50:47Z
draft: false
---
## Background
[Nix][wtf-nix] may be the most useful tool that I use. I consider it as valuable
as [Git][wtf-git] or [Emacs][wtf-emacs]. My friend, David ([@dmjio][who-dmjio]),
first introduced me to Nix when we worked together at a Haskell startup in
NYC. Before this, I had been managing my system configuration using software
that I wrote -- first in Bash, then in Python, then in Golang.
It took me awhile to understand Nix. I left the NYC startup, joined Google, and
relocated to London. Here I met another Nix-enlightened monk, Vincent
([@tazjin][who-tazjin]), who patiently taught me enough Nix to become
self-reliant and productive.
Many resources exist to learn Nix; the Nix community on IRC continues to help me
and others effectively use Nix. I'm creating this series to write the tutorials
that I would have found useful when I started learning Nix. If you are just
beginning your Nix journey, I hope these tutorials help you.
## Goals
I aim to make each tutorial in the "Let's Learn Nix" series:
- Actionable: Readers will be writing code.
- Digestible: Readers should be able to finish each tutorial in fifteen minutes.
- Reproducible: Readers should expect the output of their code to match what
these tutorials claim they should see.
## About the author
My name is William ([@wpcarro][who-wpcarro]). My three favorite tools are Git,
Emacs, and Nix. I am an American expat currently working at Google in
London. While during the day I primarily write Java, Python, and TypeScript, I
prefer functional programming. I use Nix to deploy software and manage the
multiple machines across which I work.
## Let's Begin
Before we get started, Nix is a programming language. To familiarize yourself
with the syntax, semantics, and idioms, consider reading this brief [Nix One
Pager][nix-1p]. I recommend keeping it around as a reference.
When I was first learning Nix, I wanted to use it to manage my dotfiles. Our
first tutorial will help you get started: [Let's Learn Nix:
Dotfiles][lln-dotfiles]
[wtf-nix]: https://nixos.org
[wtf-git]: https://git-scm.com
[wtf-emacs]: https://www.gnu.org/software/emacs
[who-dmjio]: https://twitter.com/dmjio
[who-tazjin]: https://twitter.com/tazjin
[who-wpcarro]: https://twitter.com/wpcarro
[lln-dotfiles]: /lets-learn-nix-dotfiles
[nix-1p]: https://github.com/tazjin/nix-1p

View file

@ -0,0 +1,5 @@
---
title: "Deploy Hugo blog with Nix"
date: 2020-03-11T18:42:32Z
draft: true
---

View file

@ -0,0 +1,6 @@
---
title: "Self Hosting"
date: 2020-03-11T22:53:56Z
draft: true
---

View file

@ -0,0 +1,12 @@
{ pkgs, ... }:
pkgs.stdenv.mkDerivation {
name = "blog.wpcarro.dev";
buildInputs = with pkgs; [ hugo ];
src = builtins.path { path = ./.; name = "blog"; };
buildPhase = ''
mkdir -p $out
${pkgs.hugo}/bin/hugo --minify --destination $out
'';
dontInstall = true;
}

View file

@ -0,0 +1,8 @@
let
briefcase = import <briefcase> {};
pkgs = briefcase.third_party.pkgs;
in pkgs.mkShell {
buildInputs = with pkgs; [
hugo
];
}

View file

@ -0,0 +1,5 @@
.vscode/
node_modules
public/build
yarn.lock

View file

@ -0,0 +1,7 @@
---
title: "{{ replace .Name "-" " " | title }}"
description: ""
date: {{ .Date }}
draft: true
tags: []
---

View file

@ -0,0 +1,5 @@
- id: back_home
translation: "Back Home"
- id: not_found_page_title
translation: "Whoops! The page you're looking for doesn't exist :("

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

View file

@ -0,0 +1,12 @@
{{ define "heading"}}
<div>
<a class="text-lg mb-8 inline-block" href="{{ .Site.BaseURL | relLangURL }}">&larr; {{ i18n "back_home" }}</a>
<h1 class="text-4xl font-bold">{{ i18n "not_found_page_title" }}</h1>
</div>
{{ end }}
{{ define "content" }}
<section class="mb-24">
<img src="{{ "images/404-background.png" | relURL }}" alt="Page Not Found">
</section>
{{ end }}

View file

@ -0,0 +1,87 @@
<!doctype html>
<html lang="{{ .Site.Params.LanguageCode }}">
<head>
<meta charset="utf-8">
{{ hugo.Generator }}
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Twitter Card -->
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="{{ if .IsHome }}{{ .Site.Title }}{{ else }}{{ .Title }} - {{ .Site.Title }}{{ end }}">
<meta name="twitter:description" content="{{ if .IsHome }}{{ .Site.Params.description }}{{ else }}{{ .Summary | plainify }}{{ end }}">
<meta name="twitter:site" content="{{ .Site.BaseURL }}">
<meta name="twitter:creator" content="{{ .Params.Author }}">
<meta name="twitter:image" content="{{ .Site.Params.Avatar | absURL }}">
<!-- Open-Graph Data -->
<meta property="og:locale" content="{{ .Site.Params.LanguageCode }}">
<meta property="og:type" content="{{ if .IsHome }}website{{ else }}article{{ end }}">
<meta property="og:title" content="{{ if .IsHome }}{{ .Site.Title }}{{ else }}{{ .Title }} - {{ .Site.Title }}{{ end }}">
<meta property="og:description" content="{{ if .IsHome }}{{ .Site.Params.description }}{{ else }}{{ .Summary | plainify }}{{ end }}">
<meta property="og:url" content="{{ .Permalink }}">
<meta property="og:site_name" content="{{ .Site.Title }}">
<meta property="og:image" content="{{ .Site.Params.Avatar | absURL }}">
<title>{{ if .IsHome }}{{ .Site.Title }}{{ else }}{{ .Title }} - {{ .Site.Title }}{{ end }}</title>
<meta name="author" content="{{ .Site.Params.Author }}">
<meta name="description" content="{{ if .IsHome }}{{ .Site.Params.description }}{{ else }}{{ .Summary | plainify }}{{ end }}">
<!-- RSS -->
{{ with .OutputFormats.Get "RSS" }}
<link rel="alternate" href="{{ .RelPermalink | absURL }}" type="application/rss+xml" title="{{ $.Site.Title }}">
{{ end }}
<!-- Translations -->
{{ if .IsTranslated }}
{{ range .Translations }}
<link rel="alternate" hreflang="{{ .Language.Lang }}" href="{{ .Permalink }}" title="{{ .Site.Title }}">
{{ end }}
{{ end }}
<!-- Stylesheets -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Code+Pro|Arvo:400,700">
<link rel="stylesheet" href="{{ "css/theme.css" | absURL }}">
<link rel="stylesheet" href="{{ "css/chroma.dracula.css" | absURL }}">
</head>
<body class="font-serif border-t-4 border-blue-500 antialiased">
<div class="w-full p-6 md:w-2/3 md:px-0 md:mx-auto xl:w-2/5">
<header class="mb-6">
<!-- All the pages must have a heading block, defaults to a link for the home page and a title. -->
<div class="mb-6 md:flex md:items-center">
{{ block "heading" . }}
<div>
{{ partial "back-home.html" . }}
<h1 class="text-4xl font-bold">{{ .Title }}</h1>
</div>
{{ end }}
</div>
<!-- If the blog has translation, they shoul be displayed here. -->
{{ if .IsTranslated }}
<nav>
{{ range $i, $lang := .Translations }}
{{ if $i }}/{{ end }}
<a href="{{ .Permalink }}">{{ $lang.Language.LanguageName }}</a>
{{ end}}
</nav>
{{ end }}
</header>
<!-- The content block. -->
{{ block "content" . }}{{ end }}
<footer>
<p>
&copy; {{ now.Format "2006"}}. Thank you for reading.
</p>
</footer>
</div>
{{ template "_internal/google_analytics.html" . }}
<script data-ad-client="ca-pub-6018268443649487" async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
</body>
</html>

View file

@ -0,0 +1,7 @@
{{ define "content" }}
<section class="mb-24">
{{ range site.RegularPages.GroupByDate "2006" -}}
{{ partial "posts.html" . }}
{{ end }}
</section>
{{ end }}

View file

@ -0,0 +1,28 @@
{{ define "heading" }}
<div>
{{ partial "back-home.html" . }}
<!-- Title and Publication Date -->
<h1 class="text-4xl font-bold">{{ .Title }}</h1>
<time datetime="{{ .Date.Format "2006-01-02 15:04:05 MST" }}">{{ .Date.Format "02 Jan 2006" }}</time>
<!-- Tags -->
{{ with .Params.tags }}
<ol class="mt-4">
{{ range . }}
<li class="inline-block">
<a class="border-none text-gray-800 text-xs bg-gray-400 hover:bg-gray-600 hover:text-white rounded-sm px-3 py-1" href="{{ "tags" | absURL }}/{{ . | urlize }}">{{ . }}</a>
</li>
{{ end }}
</ol>
{{ end }}
</div>
{{ end }}
{{ define "content" }}
<article class="mb-12">
{{ .Content }}
{{ template "_internal/disqus.html" . }}
</article>
{{ end }}

View file

@ -0,0 +1,18 @@
{{ define "heading" }}
{{ if .Site.Params.Avatar }}
<img class="hidden md:block w-20 rounded-full mr-6" src="{{ .Site.Params.Avatar | absURL }}" alt="{{ .Site.Params.Author }}">
{{ end }}
<div>
<h1 class="text-4xl font-bold">{{ .Site.Title }}</h1>
<p>{{ .Site.Params.tagline }}</p>
</div>
{{ end }}
{{ define "content" }}
<section class="mb-24">
{{ range site.RegularPages.GroupByDate "2006" -}}
{{ partial "posts.html" . }}
{{ end }}
</section>
{{ end }}

View file

@ -0,0 +1 @@
<a class="text-lg mb-8 inline-block" href="{{ .Site.BaseURL | relLangURL }}">&larr; {{ i18n "back_home" }}</a>

View file

@ -0,0 +1,12 @@
<div>
<h2 class="text-3xl font-bold mb-2">{{ .Key }}</h2>
<ol>
{{ range .Pages -}}
<li class="mb-6 md:flex md:flex-row">
<time class=" block md:flex-l-24" datetime="{{ .Date.Format "2006-01-02 15:04:05 MST" }}">{{ .Date.Format "Jan 02"}}</time>
<a class="text-lg md:ml-12" href="{{ .RelPermalink }}">{{ .Title }}</a>
</li>
{{- end }}
</ol>
</div>

View file

@ -0,0 +1,13 @@
{{ define "content" }}
<section class="mb-24">
<ol class="-mx-2">
{{ range .Pages -}}
<li class="inline-block mx-2 my-2">
<a class="border-none text-gray-800 bg-gray-400 hover:bg-gray-600 hover:text-white rounded-sm px-3 py-1" href="{{ .RelPermalink }}">
{{ .Title }}
</a>
</li>
{{- end }}
</ol>
</section>
{{ end }}

View file

@ -0,0 +1,7 @@
Copyright 2019 Ian Rodrigues.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -0,0 +1,17 @@
{
"scripts": {
"watch": "cross-env NODE_ENV=development postcss scss/theme.scss -o static/css/theme.css --watch",
"build": "cross-env NODE_ENV=production postcss scss/theme.scss -o static/css/theme.css"
},
"dependencies": {
"autoprefixer": "^9.5.1",
"tailwindcss": "^1.0"
},
"devDependencies": {
"@fullhuman/postcss-purgecss": "^1.2.0",
"concurrently": "^4.1.0",
"cross-env": "^5.2.0",
"cssnano": "^4.1.10",
"postcss-cli": "^6.1.2"
}
}

View file

@ -0,0 +1,14 @@
const purgecss = require('@fullhuman/postcss-purgecss')({
content: ['./layouts/**/*.html'],
defaultExtractor: content => content.match(/[A-Za-z0-9-_:/]+/g) || []
})
module.exports = {
plugins: [
require('tailwindcss'),
require('autoprefixer'),
...process.env.NODE_ENV === 'production'
? [purgecss, require('cssnano')]
: []
]
}

View file

@ -0,0 +1,62 @@
# TailwindCSS Journal
_TailwindCSS Journal_ is a minimalist theme for [Hugo](https://gohugo.io) using [TailwindCSS](https://tailwindcss.com).
Based on [Journal](https://dashdashzako.github.io/hugo-journal-demo/), it also focuses on improving reading experience with no fancy effect.
It uses [Chroma](https://gohugo.io/content-management/syntax-highlighting/) for the syntaxic coloration of code snippets.
Demo is available [here](https://ianrodrigues.github.io/hugo-tailwind-journal-demo/).
## Installation
Please refer to the [Hugo documentation](https://gohugo.io/themes/installing/).
## Configuration
A few parameters should be adjusted in the site config:
```toml
baseURL = "https://username.github.io/"
disqusShortname = "username"
googleAnalytics = "UA-XXXXXXXXX-X"
title = "Tailwind Journal"
theme = "hugo-tailwind-journal"
pygmentsCodeFences = true
pygmentsUseClasses = true
[taxonomies]
tag = "tags"
[permalinks]
posts = "/posts/:year/:month/:title/"
[params]
author = "John Doe"
avatar = "images/avatar.jpg"
description = "A minimalist journal template for Hugo using TailwindCSS."
tagline = "A minimalist journal template for Hugo using TailwindCSS."
[languages]
[languages.en]
contentDir = "content/english"
languageCode = "en-us"
languageName = "🇺🇸 English"
weight = 1
[languages.pt-br]
contentDir = "content/portuguese"
description = "Um template minimalista para Hugo usando TailwindCSS."
languageCode = "pt-br"
languageName = "🇧🇷 Português"
tagline = "Um template minimalista para Hugo usando TailwindCSS."
weight = 2
[languages.de]
contentDir = "content/german"
description = "Eine minimalistische Journalvorlage für Hugo mit TailwindCSS."
languageCode = "de"
languageName = "🇩🇪 Deutsch"
tagline = "Eine minimalistische Journalvorlage für Hugo mit TailwindCSS."
weight = 3
```

View file

@ -0,0 +1,51 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
@apply text-gray-800;
}
h1, h2, h3, h4, h5, h6, strong {
@apply text-gray-900;
}
article > p, ul, ol {
@apply text-lg tracking-wide;
}
article > div, p, ul, ol, pre:not(:last-child) {
@apply mb-6;
}
article > ol, ul {
@apply list-disc ml-8;
}
article > li:not(:last-of-type) {
@apply mb-2;
}
article p > code {
@apply p-1/2 bg-gray-400;
}
article > h2 {
@apply text-2xl my-8 font-bold text-black;
}
a {
@apply border-b border-black text-black;
}
/* purgecss ignore */
pre.chroma {
@apply p-4 overflow-x-auto font-mono text-lg;
}
@screen md {
/* purgecss ignore */
div.highlight {
@apply -mx-12;
}
}

View file

@ -0,0 +1 @@
.chroma{color:#f8f8f2;background-color:#282a36}.chroma .lntd{vertical-align:top;padding:0;margin:0;border:0}.chroma .lntable{border-spacing:0;padding:0;margin:0;border:0;width:auto;overflow:auto;display:block}.chroma .hl{display:block;width:100%;background-color:#ffc}.chroma .lnt{margin-right:.4em;padding:0 .4em 0 .4em;color:#7f7f7f}.chroma .ln{margin-right:.4em;padding:0 .4em 0 .4em;color:#7f7f7f}.chroma .k{color:#ff79c6}.chroma .kc{color:#ff79c6}.chroma .kd{color:#8be9fd;font-style:italic}.chroma .kn{color:#ff79c6}.chroma .kp{color:#ff79c6}.chroma .kr{color:#ff79c6}.chroma .kt{color:#8be9fd}.chroma .na{color:#50fa7b}.chroma .nb{color:#8be9fd;font-style:italic}.chroma .nc{color:#50fa7b}.chroma .nf{color:#50fa7b}.chroma .nl{color:#8be9fd;font-style:italic}.chroma .nt{color:#ff79c6}.chroma .nv{color:#8be9fd;font-style:italic}.chroma .vc{color:#8be9fd;font-style:italic}.chroma .vg{color:#8be9fd;font-style:italic}.chroma .vi{color:#8be9fd;font-style:italic}.chroma .s{color:#f1fa8c}.chroma .sa{color:#f1fa8c}.chroma .sb{color:#f1fa8c}.chroma .sc{color:#f1fa8c}.chroma .dl{color:#f1fa8c}.chroma .sd{color:#f1fa8c}.chroma .s2{color:#f1fa8c}.chroma .se{color:#f1fa8c}.chroma .sh{color:#f1fa8c}.chroma .si{color:#f1fa8c}.chroma .sx{color:#f1fa8c}.chroma .sr{color:#f1fa8c}.chroma .s1{color:#f1fa8c}.chroma .ss{color:#f1fa8c}.chroma .m{color:#bd93f9}.chroma .mb{color:#bd93f9}.chroma .mf{color:#bd93f9}.chroma .mh{color:#bd93f9}.chroma .mi{color:#bd93f9}.chroma .il{color:#bd93f9}.chroma .mo{color:#bd93f9}.chroma .o{color:#ff79c6}.chroma .ow{color:#ff79c6}.chroma .c{color:#6272a4}.chroma .ch{color:#6272a4}.chroma .cm{color:#6272a4}.chroma .c1{color:#6272a4}.chroma .cs{color:#6272a4}.chroma .cp{color:#ff79c6}.chroma .cpf{color:#ff79c6}.chroma .gd{color:#8b080b}.chroma .ge{text-decoration:underline}.chroma .gh{font-weight:700}.chroma .gi{font-weight:700}.chroma .go{color:#44475a}.chroma .gu{font-weight:700}.chroma .gl{text-decoration:underline}

View file

@ -0,0 +1 @@
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}a{background-color:transparent}strong{font-weight:bolder}img{border-style:none}button,input{font-family:inherit;font-size:100%;line-height:1.15;margin:0;overflow:visible}button{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}legend{color:inherit;display:table;max-width:100%;white-space:normal}[type=checkbox],[type=radio],legend{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}[hidden],template{display:none}html{box-sizing:border-box;font-family:sans-serif}*,:after,:before{box-sizing:inherit}h1,h2,p{margin:0}button{background:transparent;padding:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}ol{list-style:none;margin:0;padding:0}html{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5}*,:after,:before{border:0 solid #e2e8f0}img{border-style:solid}input::-webkit-input-placeholder{color:#a0aec0}input::-moz-placeholder{color:#a0aec0}input:-ms-input-placeholder{color:#a0aec0}input::-ms-input-placeholder{color:#a0aec0}input::placeholder{color:#a0aec0}[role=button],button{cursor:pointer}h1,h2{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}button,input{padding:0;line-height:inherit;color:inherit}canvas,img{display:block;vertical-align:middle}img{max-width:100%;height:auto}.bg-gray-200{background-color:#edf2f7}.bg-gray-400{background-color:#cbd5e0}.hover\:bg-gray-600:hover{background-color:#718096}.border-blue-500{border-color:#4299e1}.rounded-sm{border-radius:.125rem}.rounded-full{border-radius:9999px}.border-none{border-style:none}.border-t-4{border-top-width:4px}.block{display:block}.inline-block{display:inline-block}.hidden{display:none}.font-serif{font-family:Arvo}.font-bold{font-weight:700}.my-2{margin-top:.5rem;margin-bottom:.5rem}.mx-2{margin-left:.5rem;margin-right:.5rem}.-mx-2{margin-left:-.5rem;margin-right:-.5rem}.mb-2{margin-bottom:.5rem}.mt-4{margin-top:1rem}.mr-6{margin-right:1.5rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.mb-12{margin-bottom:3rem}.mb-24{margin-bottom:6rem}.p-6{padding:1.5rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.px-3{padding-left:.75rem;padding-right:.75rem}.text-gray-800{color:#2d3748}.hover\:text-white:hover{color:#fff}.text-xs{font-size:.75rem}.text-lg{font-size:1.125rem}.text-3xl{font-size:1.875rem}.text-4xl{font-size:2.25rem}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.w-20{width:5rem}.w-full{width:100%}body{color:#2d3748}h1,h2,strong{color:#1a202c}article>p,ol{font-size:1.125rem;letter-spacing:.025em}article>div,ol,p{margin-bottom:1.5rem}article>ol{list-style-type:disc;margin-left:2rem}article>li:not(:last-of-type){margin-bottom:.5rem}article>h2{font-size:1.5rem;margin-top:2rem;margin-bottom:2rem;font-weight:700;color:#000}a{border-bottom-width:1px;border-color:#000;color:#000}pre.chroma{padding:1rem;overflow-x:auto;font-family:Source Code Pro;font-size:1.125rem}@media (min-width:768px){div.highlight{margin-left:-3rem;margin-right:-3rem}}@media (min-width:768px){.md\:block{display:block}.md\:flex{display:-webkit-box;display:flex}.md\:flex-row{-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-direction:row}.md\:items-center{-webkit-box-align:center;align-items:center}.md\:flex-l-24{-webkit-box-flex:0;flex:0 0 6rem}.md\:mx-auto{margin-left:auto;margin-right:auto}.md\:ml-12{margin-left:3rem}.md\:px-0{padding-left:0;padding-right:0}.md\:w-2\/3{width:66.666667%}}@media (min-width:1280px){.xl\:w-2\/5{width:40%}}

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

View file

@ -0,0 +1,18 @@
module.exports = {
theme: {
extend: {
flex: {
'l-24': '0 0 6rem'
},
fontFamily: {
serif: ['Arvo'],
mono: ['Source Code Pro']
},
padding: {
'1/2': '0.125rem'
}
}
},
variants: {},
plugins: [],
}

View file

@ -0,0 +1,12 @@
name = "Tailwind Journal"
license = "MIT"
licenselink = "https://github.com/ianrodrigues/hugo-tailwind-journal/blob/master/license.md"
description = "A minimalist journal template for Hugo using TailwindCSS."
homepage = "https://github.com/ianrodrigues/hugo-tailwind-journal"
tags = ["minimalist", "reading", "blog", "tailwindcss"]
features = ["blog"]
min_version = "0.54.0"
[author]
name = "Ian Rodrigues"
homepage = "https://ianrodrigues.dev"

View file

@ -0,0 +1,13 @@
{ pkgs, briefcase, ... }:
pkgs.stdenv.mkDerivation {
name = "wpcarro.dev";
src = builtins.path { path = ./.; name = "website"; };
installPhase = ''
mkdir -p $out
cp $src/index.html $out
mkdir -p $out/habits
cp -r ${briefcase.website.habit-screens} $out/habits/index.html
'';
}

View file

@ -0,0 +1,2 @@
source_up
use_nix

View file

@ -0,0 +1,3 @@
/.cache
/dist/**/*
/node_modules

View file

@ -0,0 +1,5 @@
# Goals
Kent C. Dodds taught me that I can create a React website without any bundling
software. To practice this, I created a simple React app to track some of my
goals. Notice how I wrote JSX inside of a `<script type="text/babel">` tag.

View file

@ -0,0 +1,19 @@
{ pkgs, ... }:
pkgs.stdenv.mkDerivation {
name = "goals-webpage";
src = builtins.path { path = ./.; name = "goals"; };
buildInputs = with pkgs; [
nodejs
# Exposes lscpu for parcel.js
utillinux
];
# parcel.js needs number of CPUs
PARCEL_WORKERS = "1";
buildPhase = ''
npx parcel build src/index.html --public-url ./
'';
installPhase = ''
mv dist $out
'';
}

View file

@ -0,0 +1,28 @@
{
"name": "tailwindcss",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"dev": "parcel src/index.html & npx tsc --watch --noEmit",
"prettier": "prettier --ignore-path .gitignore --write \"**/*.{js,ts,jsx,tsx,html,css.json}\""
},
"devDependencies": {
"@types/node": "^13.9.3",
"parcel-bundler": "^1.12.4",
"prettier": "^2.0.2",
"tailwindcss": "^1.2.0",
"typescript": "^3.8.3"
},
"dependencies": {
"@reduxjs/toolkit": "^1.2.5",
"@types/react": "^16.9.25",
"@types/react-dom": "^16.9.5",
"@types/react-redux": "^7.1.7",
"@types/react-router-dom": "^5.1.3",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-redux": "^7.2.0",
"react-router-dom": "^5.1.2"
}
}

View file

@ -0,0 +1,5 @@
const tailwindcss = require("tailwindcss");
module.exports = {
plugins: [tailwindcss("./tailwind.config.js")],
};

View file

@ -0,0 +1,9 @@
let
briefcase = import <briefcase> {};
pkgs = briefcase.third_party.pkgs;
in pkgs.mkShell {
buildInputs = with pkgs; [
nodejs
yarn
];
}

View file

@ -0,0 +1,132 @@
import React from "react";
function ProgressBar(props: {
done: number;
total: number;
units: string;
color: string;
}) {
const { done, total, units, color } = props;
const width = Math.floor((done / total) * 100);
const rest = 100 - width;
let [fg, bg] = [`bg-${color}-600`, `bg-${color}-100`];
if (color === "white") {
[fg, bg] = ["bg-gray-600", "bg-gray-100"];
}
return (
<div className={`relative ${bg} h-5`}>
<div
className={`${fg} h-5 absolute top-0 left-0`}
style={{ width: `${width}%` }}
></div>
<p className="absolute text-xs pl-1 pt-1">
{done} of {total} {units}
</p>
</div>
);
}
function Goal(props: {
subject: string;
goal: string;
done: number;
total: number;
units: string;
color: string;
}) {
const { subject, goal, done, total, units, color } = props;
const width = "6em";
const Tr = (props: {
label: string;
value: string;
valueComponent?: React.ReactElement;
}) => (
<tr className="flex py-2">
<td className="text-gray-600" style={{ width: width }}>
{props.label}
</td>
<td className="flex-1">
{props.valueComponent ? props.valueComponent : props.value}
</td>
</tr>
);
return (
<table className="w-full mb-10">
<tbody>
<Tr label="Subject" value={subject} />
<Tr label="Goal" value={goal} />
<Tr
label="Progress"
value={goal}
valueComponent={
<ProgressBar
done={done}
total={total}
units={units}
color={color}
/>
}
/>
</tbody>
</table>
);
}
function Copy(props: { children: React.ReactNode }) {
return <p className="pb-4 leading-loose">{props.children}</p>;
}
function App() {
return (
<div className="container mx-auto font-mono">
<section>
<h1 className="text-center pt-12 pb-6">Goals</h1>
<Copy>
For me, a goal is something that is difficult for me to complete but
easy for me to measure. I tend to add new goals as time progresses,
mistakenly assuming that I can support additional goals for free. To
counterbalance my tendancy to casually accumulate goals, I aim to only
have three goals; I will not add a new goal until I complete an
existing goal. I created and published this page to clarify that idea.
</Copy>
<Copy>
Here are my current goals and the progress I have made towards
achieving them.
</Copy>
</section>
<section className="pt-4">
<Goal
subject="Meditation"
goal="Meditate for 10,000 hours"
done={100}
total={10000}
units="hrs"
color="purple"
/>
<Goal
subject="Debt"
goal="Pay my student debt balance"
done={30000}
total={70000}
units="USD"
color="green"
/>
<Goal
subject="Brazilian Jiu Jitsu"
goal="Train until an instructor gives me a black belt"
done={1}
total={5}
units="belts"
color="white"
/>
</section>
</div>
);
}
export default App;

View file

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View file

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="stylesheet" href="./index.css" />
</head>
<body>
<div id="mount"></div>
<script src="./index.tsx"></script>
</body>
</html>

View file

@ -0,0 +1,5 @@
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
ReactDOM.render(<App />, document.getElementById("mount"));

View file

@ -0,0 +1,7 @@
module.exports = {
theme: {
extend: {},
},
variants: {},
plugins: [],
};

View file

@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react"
},
"include": ["src/**/*"]
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,2 @@
source_up
use_nix

View file

@ -0,0 +1,2 @@
/elm-stuff
/Main.min.js

View 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`!

View file

@ -0,0 +1,61 @@
{ pkgs ? <nixpkgs> , ... }:
with pkgs;
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)}
'';
};
mainDotElm = mkDerivation {
name = "elm-app-0.1.0";
srcs = ./elm-srcs.nix;
src = ./.;
targets = ["Main"];
srcdir = "./src";
outputJavaScript = true;
};
in stdenv.mkDerivation {
name = "habit-screens";
buildInputs = [];
src = builtins.path { path = ./.; name = "habit-screens"; };
buildPhase = ''
mkdir -p $out
cp index.html output.css ${mainDotElm}/Main.min.js $out
'';
dontInstall = true;
}

View 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

View 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";
};
}

View 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": {}
}
}

View file

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View 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>

File diff suppressed because it is too large Load diff

Binary file not shown.

View 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
];
}

View file

@ -0,0 +1,463 @@
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" )
, ( 30, "Run (15 minutes)" )
, ( 10, "Shower" )
, ( 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, "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." ]

View 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
}

View 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 = True
, includeEvening = True
}
, 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 )

View 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

View 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

View file

@ -0,0 +1,44 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>wpcarro.dev</title>
</head>
<body>
<h1>Sitemap</h1>
<ul>
<li>Documents</li>
<ul>
<li>
<a href="/habits">Habits</a>
</li>
</ul>
<li>Other</li>
<ul>
<li>
<a href="https://blog.wpcarro.dev">blog.wpcarro.dev</a>
</li>
<li>
<a href="https://learn.wpcarro.dev">learn.wpcarro.dev</a>
</li>
<li>
<a href="https://sandbox.wpcarro.dev">sandbox.wpcarro.dev</a>
</li>
</ul>
<li>Social</li>
<ul>
<li>
<a href="https://twitter.com/wpcarro">Twitter</a>
</li>
<li>
<a href="https://github.com/wpcarro">Github</a>
</li>
<li>
<a href="https://linkedin.com/in/williampatrickcarroll">LinkedIn</a>
</li>
</ul>
</ul>
</body>
</html>

View file

@ -0,0 +1,4 @@
# Learn
Hosting the content for my website `learn.wpcarro.dev`, where I advertise
teaching others how to program.

View file

@ -0,0 +1,10 @@
{ pkgs, ... }:
pkgs.stdenv.mkDerivation {
name = "learn.wpcarro.dev";
src = ./static;
buildPhase = ''
cp -R $src $out
'';
dontInstall = true;
}

View file

@ -0,0 +1,102 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="Learn to code" />
<link
href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css"
rel="stylesheet"
/>
<title>Learn to code</title>
</head>
<body class="font-serif container max-w-2xl mx-auto px-8">
<h1 class="text-5xl text-center mt-12 mb-8">I can teach you to code.</h1>
<div class="my-4">
<p class="leading-relaxed mb-4">
My name is William. I have programmed as a professional for five years
and as a hobbyist for ten years. I am currently an engineer at Google,
but I have worked at small start-ups and medium-sized corporations.
</p>
<p class="leading-relaxed mb-4">
Whether you have never written a single line of code, or you know how to
code and you are interested in going deeper, I can teach you a variety
of skills that professional software engineers use to succeed.
</p>
</div>
<div class="my-4">
<h2 class="text-3xl">Why coding?</h2>
<p class="leading-relaxed mb-4">
Are you interested in creating your own website? Perhaps you would like
to make your own video game. Maybe you like the notion of automating the
boring things in your life with code, but you do not know where to
start.
</p>
<p class="leading-relaxed mb-4">
Coding is the most creative outlet in my life, and I say that as a
musician and a former architecture student. I know many people who want
to learn how to code, who know they can learn online, but they still
cannot code. Together we will overcome this barrier to entry. I will
teach you how to code, and then I will teach you how to take yourself
the rest of the way. <b>You will be coding from day one.</b>
</p>
</div>
<div class="my-4">
<h2 class="text-3xl">Pricing</h2>
<p class="leading-relaxed mb-4">
I charge <bold class="font-bold">£50</bold> per hour for video lessons
and <bold class="font-bold">£100</bold> per hour for in-person sessions.
I am currently based in South Kensington, London.
</p>
</div>
<div class="my-4">
<h2 class="text-3xl">Contact</h2>
<p class="leading-relaxed mb-4">
Whether you want to sign-up or simply want to learn more, send me an
email at
<a
href="mailto:wpcarro@gmail.com"
class="font-bold text-blue-600 hover:underline"
>wpcarro@gmail.com</a
>.
</p>
<p class="text-center my-8">Why delay? <em>Start today.</em></p>
</div>
<footer class="mb-8 lg:flex">
<a
class="block py-2 lg:w-1/4 text-center hover:underline"
href="https://blog.wpcarro.dev"
>Blog</a
>
<a
class="block py-2 lg:w-1/4 text-center hover:underline"
href="https://linkedin.com/in/williampatrickcarroll"
>LinkedIn</a
>
<a
class="block py-2 lg:w-1/4 text-center hover:underline"
href="https://twitter.com/wpcarro"
>Twitter</a
>
<a
class="block py-2 lg:w-1/4 text-center hover:underline"
href="https://github.com/wpcarro"
>Github</a
>
</footer>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script
async
src="https://www.googletagmanager.com/gtag/js?id=UA-160226702-1"
></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag("js", new Date());
gtag("config", "UA-160226702-1");
</script>
</body>
</html>

View file

@ -0,0 +1,4 @@
source_up
use_nix
export CONTENTFUL_SPACE_ID="$(jq -j '.contentful | .spaceId' < ~/briefcase/secrets.json)"
export CONTENTFUL_ACCESS_TOKEN="$(jq -j '.contentful | .accessToken' < ~/briefcase/secrets.json)"

View file

@ -0,0 +1,2 @@
.cache
dist

View file

@ -0,0 +1,18 @@
# Contentful
I have not used a CMS in a few years. I learned about Contentful from a
Gatsby.js tutorial, and I wanted to learn more; I created a Contentful account,
and I'm experimenting with the data here.
## Developing
```shell
$ nix-shell
$ yarn run dev
```
## Building
```shell
$ nix-build
```

View file

@ -0,0 +1,19 @@
{ pkgs, ... }:
pkgs.stdenv.mkDerivation {
name = "ideal-website";
src = builtins.path { path = ./.; name = "contentful"; };
buildInputs = with pkgs; [
nodejs
# Exposes lscpu for parcel.js
utillinux
];
# parcel.js needs number of CPUs
PARCEL_WORKERS = "1";
buildPhase = ''
npx parcel build index.html
'';
installPhase = ''
mv dist $out
'';
}

View file

@ -0,0 +1,26 @@
{
"name": "tailwindcss",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"dev": "npx parcel src/index.html & npx tsc --watch --noEmit"
},
"devDependencies": {
"@types/node": "^13.9.3",
"parcel-bundler": "^1.12.4",
"tailwindcss": "^1.2.0",
"typescript": "^3.8.3"
},
"dependencies": {
"@reduxjs/toolkit": "^1.2.5",
"@types/react-dom": "^16.9.5",
"@types/react-redux": "^7.1.7",
"@types/react-router-dom": "^5.1.3",
"contentful": "^7.14.0",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-redux": "^7.2.0",
"react-router-dom": "^5.1.2"
}
}

View file

@ -0,0 +1,5 @@
const tailwindcss = require("tailwindcss");
module.exports = {
plugins: [tailwindcss("./tailwind.config.js")],
};

View file

@ -0,0 +1,9 @@
let
briefcase = import <briefcase> {};
pkgs = briefcase.third_party.pkgs;
in pkgs.mkShell {
buildInputs = with pkgs; [
nodejs
yarn
];
}

View file

@ -0,0 +1,49 @@
import React, { useEffect } from "react";
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
import { useDispatch } from "react-redux";
import { actions, useTypedSelector } from "./store";
import { Link } from "react-router-dom";
import { getClient } from "./contentful";
import type { Book } from "./store";
const App: React.FC = () => {
const dispatch = useDispatch();
const { isLoading, books } = useTypedSelector((state) => ({
isLoading: state.isLoading,
books: state.books,
}));
useEffect(() => {
async function fetchData() {
const entries = await getClient().getEntries();
const books = entries.items.map((x) => x.fields) as Book[];
dispatch(actions.setBooks(books));
}
fetchData();
}, []);
return (
<Router>
<Switch>
<Route exact path="/">
<div className="container mx-auto">
<h1 className="py-6 text-2xl">Books</h1>
<ul>
{books.map((book) => (
<li key={book.title} className="py-3">
<p>
<span className="font-bold pr-3">{book.title}</span>
<span className="text-gray-600">{book.author}</span>
</p>
</li>
))}
</ul>
</div>
</Route>
</Switch>
</Router>
);
};
export default App;

View file

@ -0,0 +1,27 @@
import { createClient } from "contentful";
import type { ContentfulClientApi } from "contentful";
const space = process.env.CONTENTFUL_SPACE_ID;
const accessToken = process.env.CONTENTFUL_ACCESS_TOKEN;
let client: ContentfulClientApi;
// Idempotent way to get a reference to the Contentful client.
export const getClient = (): ContentfulClientApi => {
if (typeof client !== "undefined") {
return client;
} else {
if (typeof space === "string" && typeof accessToken === "string") {
let client = createClient({
space,
accessToken,
});
return client;
} else {
throw new Error(
"Please set CONTENTFUL_SPACE_ID and CONTENTFUL_ACCESS_TOKEN"
);
}
}
};

View file

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View file

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="stylesheet" href="./index.css" />
</head>
<body>
<div id="mount"></div>
<script src="./index.tsx"></script>
</body>
</html>

View file

@ -0,0 +1,12 @@
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import { Provider } from "react-redux";
import store from "./store";
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("mount")
);

View file

@ -0,0 +1,36 @@
import { createSlice, configureStore, PayloadAction } from "@reduxjs/toolkit";
import { useSelector, TypedUseSelectorHook } from "react-redux";
export interface Book {
title: string;
author: string;
// TODO(wpcarro): Prefer datetime type here.
publicationDate: string;
}
export interface State {
isLoading: boolean;
books: Book[];
}
const initialState: State = {
isLoading: true,
books: [],
};
export const { actions, reducer } = createSlice({
name: "application",
initialState,
reducers: {
toggleIsLoading: (state) => ({ ...state, isLoading: !state.isLoading }),
setBooks: (state, action) => ({ ...state, books: action.payload }),
},
});
/**
* Defining and consuming this allows us to avoid annotating State in all of our
* selectors.
*/
export const useTypedSelector: TypedUseSelectorHook<State> = useSelector;
export default configureStore({ reducer });

View file

@ -0,0 +1,7 @@
module.exports = {
theme: {
extend: {},
},
variants: {},
plugins: [],
};

View file

@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react"
},
"include": ["src/**/*"]
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,16 @@
{ pkgs, ... }:
pkgs.stdenv.mkDerivation {
name = "covid-uk";
buildInputs = [];
src = builtins.path { path = ./.; name = "covid-uk"; };
# TODO(wpcarro): Need to run `yarn install` somehow.
# TODO(wpcarro): Need to run `npx tailwindcss build styles.css -o output.css`.
buildPhase = ''
mkdir -p $out
mkdir -p $out/node_modules/chart.js/dist
cp $src/node_modules/chart.js/dist/Chart.bundle.min.js $out/node_modules/chart.js/dist
cp $src/index.html $src/output.css $out
'';
dontInstall = true;
}

View file

@ -0,0 +1,99 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>COVID-19 UK</title>
<link rel="stylesheet" href="output.css">
</head>
<body class="container mx-auto py-10">
<div>
<h1 class="text-center">COVID-19 in the UK</h1>
<p>
Up until recently, I used a couple of resources (i.e.
<a href="https://multimedia.scmp.com/infographics/news/china/article/3047038/wuhan-virus/index.html">one</a>,
<a href="https://www.worldometers.info/coronavirus/">two</a>) for tracking
an updated number of confirmed covid-19 cases.
</p>
<p>
Given the high speed at which the virus is spreading, I was having a
difficult time intuiting the shape of this growth. For example if today
the total number of confirmed cases for covid-19 in the UK was 500, I
could not remember if yesterday it was 450, 400, or 200.
</p>
<p>
Thankfully someone is <a
href="https://github.com/pomber/covid19">publishing this data</a> as a
timeseries database. I am currently living in London, so I decided to
chart the <u>daily number of confirmed covid-19 cases in the UK</u> to
better understand what is happening.
</p>
</div>
<canvas id="myChart" class="py-12"></canvas>
<script src="./node_modules/chart.js/dist/Chart.bundle.min.js"></script>
<script>
var timeseries =
fetch('https://pomber.github.io/covid19/timeseries.json')
.then(res => res.json())
.then(createChart);
function createChart(data) {
var uk = data["United Kingdom"];
var data = uk.map(x => x["confirmed"]);
var labels = uk.map(x => x["date"]);
var ctx = document.getElementById('myChart').getContext('2d');
var myChart = new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [{
label: 'Number of confirmed COVID-19 cases in the U.K.',
data: data,
backgroundColor: 'rgba(255, 0, 100, 0.2)',
borderWidth: 3
}]
},
options: {
scales: {
yAxes: [{
ticks: {
beginAtZero: true
}
}]
}
}
});
}
</script>
<div>
<h2 class="text-center">Back of the envelope predictions</h2>
<p>
From what I have read, a population where 60% of its constituents have
been infected with covid-19 and have recovered is said to have "herd
immunity". Once a population has herd immunity, the rate at which the
virus spreads decreases.
</p>
<p>
Roughly 60M people live in the UK; 60% of 60M is around 40M. Before a
population reaches "herd immunity", the total number of <em>true
covid-19 cases</em> <u>doubles every five days</u>. Therefore in <u>fifty
days</u> you might expect the number of true cases to be <u>1000x
larger</u> than what it is today.
</p>
<p>
So if you think the total number of <em>true covid-19 cases</em>
<u>today</u> is 40,000 then you might expect the rate of growth to slow
down in a little less than two months.
</p>
<p>
Thank you for reading.
</p>
</div>
<footer class="pt-5 mb-8 lg:flex">
<a class="block py-2 lg:w-1/4 text-center hover:underline" href="https://learn.wpcarro.dev">Learn</a>
<a class="block py-2 lg:w-1/4 text-center hover:underline" href="https://blog.wpcarro.dev">Blog</a>
<a class="block py-2 lg:w-1/4 text-center hover:underline" href="https://twitter.com/wpcarro">Twitter</a>
<a class="block py-2 lg:w-1/4 text-center hover:underline" href="https://github.com/wpcarro">Github</a>
</footer>
</body>
</html>

View file

@ -0,0 +1,16 @@
{
"name": "covid-uk",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"chart.js": "^2.9.3",
"tailwindcss": "^1.2.0"
}
}

View file

@ -0,0 +1,9 @@
let
briefcase = import <briefcase> {};
pkgs = briefcase.third_party.pkgs;
in pkgs.mkShell {
buildInputs = with pkgs; [
yarn
nodejs
];
}

View file

@ -0,0 +1,28 @@
@tailwind base;
body {
@apply font-mono;
}
h1 {
@apply text-3xl mb-5 font-bold;
}
h2 {
@apply text-2xl mb-5 font-bold;
}
h3 {
@apply text-xl mb-5 font-bold;
}
a {
@apply text-blue-600 underline;
}
p {
@apply mt-2 mb-5;
}
@tailwind components;
@tailwind utilities;

View file

@ -0,0 +1,7 @@
module.exports = {
theme: {
extend: {},
},
variants: {},
plugins: [],
}

View file

@ -0,0 +1,542 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@types/color-name@^1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0"
integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==
acorn-node@^1.6.1:
version "1.8.2"
resolved "https://registry.yarnpkg.com/acorn-node/-/acorn-node-1.8.2.tgz#114c95d64539e53dede23de8b9d96df7c7ae2af8"
integrity sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==
dependencies:
acorn "^7.0.0"
acorn-walk "^7.0.0"
xtend "^4.0.2"
acorn-walk@^7.0.0:
version "7.1.1"
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.1.1.tgz#345f0dffad5c735e7373d2fec9a1023e6a44b83e"
integrity sha512-wdlPY2tm/9XBr7QkKlq0WQVgiuGTX6YWPyRyBviSoScBuLfTVQhvwg6wJ369GJ/1nPfTLMfnrFIfjqVg6d+jQQ==
acorn@^7.0.0:
version "7.1.1"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.1.1.tgz#e35668de0b402f359de515c5482a1ab9f89a69bf"
integrity sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==
ansi-styles@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
dependencies:
color-convert "^1.9.0"
ansi-styles@^4.1.0:
version "4.2.1"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359"
integrity sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==
dependencies:
"@types/color-name" "^1.1.1"
color-convert "^2.0.1"
autoprefixer@^9.4.5:
version "9.7.4"
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.7.4.tgz#f8bf3e06707d047f0641d87aee8cfb174b2a5378"
integrity sha512-g0Ya30YrMBAEZk60lp+qfX5YQllG+S5W3GYCFvyHTvhOki0AEQJLPEcIuGRsqVwLi8FvXPVtwTGhfr38hVpm0g==
dependencies:
browserslist "^4.8.3"
caniuse-lite "^1.0.30001020"
chalk "^2.4.2"
normalize-range "^0.1.2"
num2fraction "^1.2.2"
postcss "^7.0.26"
postcss-value-parser "^4.0.2"
balanced-match@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
brace-expansion@^1.1.7:
version "1.1.11"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
dependencies:
balanced-match "^1.0.0"
concat-map "0.0.1"
browserslist@^4.8.3:
version "4.10.0"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.10.0.tgz#f179737913eaf0d2b98e4926ac1ca6a15cbcc6a9"
integrity sha512-TpfK0TDgv71dzuTsEAlQiHeWQ/tiPqgNZVdv046fvNtBZrjbv2O3TsWCDU0AWGJJKCF/KsjNdLzR9hXOsh/CfA==
dependencies:
caniuse-lite "^1.0.30001035"
electron-to-chromium "^1.3.378"
node-releases "^1.1.52"
pkg-up "^3.1.0"
bytes@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==
camelcase-css@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5"
integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==
caniuse-lite@^1.0.30001020, caniuse-lite@^1.0.30001035:
version "1.0.30001035"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001035.tgz#2bb53b8aa4716b2ed08e088d4dc816a5fe089a1e"
integrity sha512-C1ZxgkuA4/bUEdMbU5WrGY4+UhMFFiXrgNAfxiMIqWgFTWfv/xsZCS2xEHT2LMq7xAZfuAnu6mcqyDl0ZR6wLQ==
chalk@^2.4.1, chalk@^2.4.2:
version "2.4.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
dependencies:
ansi-styles "^3.2.1"
escape-string-regexp "^1.0.5"
supports-color "^5.3.0"
chalk@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4"
integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==
dependencies:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
chart.js@^2.9.3:
version "2.9.3"
resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-2.9.3.tgz#ae3884114dafd381bc600f5b35a189138aac1ef7"
integrity sha512-+2jlOobSk52c1VU6fzkh3UwqHMdSlgH1xFv9FKMqHiNCpXsGPQa/+81AFa+i3jZ253Mq9aAycPwDjnn1XbRNNw==
dependencies:
chartjs-color "^2.1.0"
moment "^2.10.2"
chartjs-color-string@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/chartjs-color-string/-/chartjs-color-string-0.6.0.tgz#1df096621c0e70720a64f4135ea171d051402f71"
integrity sha512-TIB5OKn1hPJvO7JcteW4WY/63v6KwEdt6udfnDE9iCAZgy+V4SrbSxoIbTw/xkUIapjEI4ExGtD0+6D3KyFd7A==
dependencies:
color-name "^1.0.0"
chartjs-color@^2.1.0:
version "2.4.1"
resolved "https://registry.yarnpkg.com/chartjs-color/-/chartjs-color-2.4.1.tgz#6118bba202fe1ea79dd7f7c0f9da93467296c3b0"
integrity sha512-haqOg1+Yebys/Ts/9bLo/BqUcONQOdr/hoEr2LLTRl6C5LXctUdHxsCYfvQVg5JIxITrfCNUDr4ntqmQk9+/0w==
dependencies:
chartjs-color-string "^0.6.0"
color-convert "^1.9.3"
color-convert@^1.9.0, color-convert@^1.9.3:
version "1.9.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
dependencies:
color-name "1.1.3"
color-convert@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
dependencies:
color-name "~1.1.4"
color-name@1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
color-name@^1.0.0, color-name@~1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
css-unit-converter@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/css-unit-converter/-/css-unit-converter-1.1.1.tgz#d9b9281adcfd8ced935bdbaba83786897f64e996"
integrity sha1-2bkoGtz9jO2TW9urqDeGiX9k6ZY=
cssesc@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
defined@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693"
integrity sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=
detective@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/detective/-/detective-5.2.0.tgz#feb2a77e85b904ecdea459ad897cc90a99bd2a7b"
integrity sha512-6SsIx+nUUbuK0EthKjv0zrdnajCCXVYGmbYYiYjFVpzcjwEs/JMDZ8tPRG29J/HhN56t3GJp2cGSWDRjjot8Pg==
dependencies:
acorn-node "^1.6.1"
defined "^1.0.0"
minimist "^1.1.1"
electron-to-chromium@^1.3.378:
version "1.3.379"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.379.tgz#81dc5e82a3e72bbb830d93e15bc35eda2bbc910e"
integrity sha512-NK9DBBYEBb5f9D7zXI0hiE941gq3wkBeQmXs1ingigA/jnTg5mhwY2Z5egwA+ZI8OLGKCx0h1Cl8/xeuIBuLlg==
escape-string-regexp@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
find-up@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73"
integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==
dependencies:
locate-path "^3.0.0"
fs-extra@^8.0.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0"
integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==
dependencies:
graceful-fs "^4.2.0"
jsonfile "^4.0.0"
universalify "^0.1.0"
fs.realpath@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
glob@^7.1.2:
version "7.1.6"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
inherits "2"
minimatch "^3.0.4"
once "^1.3.0"
path-is-absolute "^1.0.0"
graceful-fs@^4.1.6, graceful-fs@^4.2.0:
version "4.2.3"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423"
integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==
has-flag@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0=
has-flag@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
indexes-of@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607"
integrity sha1-8w9xbI4r00bHtn0985FVZqfAVgc=
inflight@^1.0.4:
version "1.0.6"
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=
dependencies:
once "^1.3.0"
wrappy "1"
inherits@2:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
jsonfile@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb"
integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=
optionalDependencies:
graceful-fs "^4.1.6"
locate-path@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e"
integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==
dependencies:
p-locate "^3.0.0"
path-exists "^3.0.0"
lodash.toarray@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz#24c4bfcd6b2fba38bfd0594db1179d8e9b656561"
integrity sha1-JMS/zWsvuji/0FlNsRedjptlZWE=
lodash@^4.17.15:
version "4.17.15"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
minimatch@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
dependencies:
brace-expansion "^1.1.7"
minimist@^1.1.1:
version "1.2.5"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
moment@^2.10.2:
version "2.24.0"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==
node-emoji@^1.8.1:
version "1.10.0"
resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.10.0.tgz#8886abd25d9c7bb61802a658523d1f8d2a89b2da"
integrity sha512-Yt3384If5H6BYGVHiHwTL+99OzJKHhgp82S8/dktEK73T26BazdgZ4JZh92xSVtGNJvz9UbXdNAc5hcrXV42vw==
dependencies:
lodash.toarray "^4.4.0"
node-releases@^1.1.52:
version "1.1.52"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.52.tgz#bcffee3e0a758e92e44ecfaecd0a47554b0bcba9"
integrity sha512-snSiT1UypkgGt2wxPqS6ImEUICbNCMb31yaxWrOLXjhlt2z2/IBpaOxzONExqSm4y5oLnAqjjRWu+wsDzK5yNQ==
dependencies:
semver "^6.3.0"
normalize-range@^0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942"
integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=
normalize.css@^8.0.1:
version "8.0.1"
resolved "https://registry.yarnpkg.com/normalize.css/-/normalize.css-8.0.1.tgz#9b98a208738b9cc2634caacbc42d131c97487bf3"
integrity sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg==
num2fraction@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede"
integrity sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=
object-assign@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
once@^1.3.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
dependencies:
wrappy "1"
p-limit@^2.0.0:
version "2.2.2"
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.2.tgz#61279b67721f5287aa1c13a9a7fbbc48c9291b1e"
integrity sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==
dependencies:
p-try "^2.0.0"
p-locate@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4"
integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==
dependencies:
p-limit "^2.0.0"
p-try@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
path-exists@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=
path-is-absolute@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
path-parse@^1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c"
integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==
pkg-up@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-3.1.0.tgz#100ec235cc150e4fd42519412596a28512a0def5"
integrity sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==
dependencies:
find-up "^3.0.0"
postcss-functions@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/postcss-functions/-/postcss-functions-3.0.0.tgz#0e94d01444700a481de20de4d55fb2640564250e"
integrity sha1-DpTQFERwCkgd4g3k1V+yZAVkJQ4=
dependencies:
glob "^7.1.2"
object-assign "^4.1.1"
postcss "^6.0.9"
postcss-value-parser "^3.3.0"
postcss-js@^2.0.0:
version "2.0.3"
resolved "https://registry.yarnpkg.com/postcss-js/-/postcss-js-2.0.3.tgz#a96f0f23ff3d08cec7dc5b11bf11c5f8077cdab9"
integrity sha512-zS59pAk3deu6dVHyrGqmC3oDXBdNdajk4k1RyxeVXCrcEDBUBHoIhE4QTsmhxgzXxsaqFDAkUZfmMa5f/N/79w==
dependencies:
camelcase-css "^2.0.1"
postcss "^7.0.18"
postcss-nested@^4.1.1:
version "4.2.1"
resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-4.2.1.tgz#4bc2e5b35e3b1e481ff81e23b700da7f82a8b248"
integrity sha512-AMayXX8tS0HCp4O4lolp4ygj9wBn32DJWXvG6gCv+ZvJrEa00GUxJcJEEzMh87BIe6FrWdYkpR2cuyqHKrxmXw==
dependencies:
postcss "^7.0.21"
postcss-selector-parser "^6.0.2"
postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.2:
version "6.0.2"
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz#934cf799d016c83411859e09dcecade01286ec5c"
integrity sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg==
dependencies:
cssesc "^3.0.0"
indexes-of "^1.0.1"
uniq "^1.0.1"
postcss-value-parser@^3.3.0:
version "3.3.1"
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281"
integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==
postcss-value-parser@^4.0.2:
version "4.0.3"
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.0.3.tgz#651ff4593aa9eda8d5d0d66593a2417aeaeb325d"
integrity sha512-N7h4pG+Nnu5BEIzyeaaIYWs0LI5XC40OrRh5L60z0QjFsqGWcHcbkBvpe1WYpcIS9yQ8sOi/vIPt1ejQCrMVrg==
postcss@^6.0.9:
version "6.0.23"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.23.tgz#61c82cc328ac60e677645f979054eb98bc0e3324"
integrity sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==
dependencies:
chalk "^2.4.1"
source-map "^0.6.1"
supports-color "^5.4.0"
postcss@^7.0.11, postcss@^7.0.18, postcss@^7.0.21, postcss@^7.0.26:
version "7.0.27"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.27.tgz#cc67cdc6b0daa375105b7c424a85567345fc54d9"
integrity sha512-WuQETPMcW9Uf1/22HWUWP9lgsIC+KEHg2kozMflKjbeUtw9ujvFX6QmIfozaErDkmLWS9WEnEdEe6Uo9/BNTdQ==
dependencies:
chalk "^2.4.2"
source-map "^0.6.1"
supports-color "^6.1.0"
pretty-hrtime@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1"
integrity sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=
reduce-css-calc@^2.1.6:
version "2.1.7"
resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-2.1.7.tgz#1ace2e02c286d78abcd01fd92bfe8097ab0602c2"
integrity sha512-fDnlZ+AybAS3C7Q9xDq5y8A2z+lT63zLbynew/lur/IR24OQF5x98tfNwf79mzEdfywZ0a2wpM860FhFfMxZlA==
dependencies:
css-unit-converter "^1.1.1"
postcss-value-parser "^3.3.0"
resolve@^1.14.2:
version "1.15.1"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.15.1.tgz#27bdcdeffeaf2d6244b95bb0f9f4b4653451f3e8"
integrity sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w==
dependencies:
path-parse "^1.0.6"
semver@^6.3.0:
version "6.3.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
source-map@^0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
supports-color@^5.3.0, supports-color@^5.4.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
dependencies:
has-flag "^3.0.0"
supports-color@^6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3"
integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==
dependencies:
has-flag "^3.0.0"
supports-color@^7.1.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1"
integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==
dependencies:
has-flag "^4.0.0"
tailwindcss@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-1.2.0.tgz#5df317cebac4f3131f275d258a39da1ba3a0f291"
integrity sha512-CKvY0ytB3ze5qvynG7qv4XSpQtFNGPbu9pUn8qFdkqgD8Yo/vGss8mhzbqls44YCXTl4G62p3qVZBj45qrd6FQ==
dependencies:
autoprefixer "^9.4.5"
bytes "^3.0.0"
chalk "^3.0.0"
detective "^5.2.0"
fs-extra "^8.0.0"
lodash "^4.17.15"
node-emoji "^1.8.1"
normalize.css "^8.0.1"
postcss "^7.0.11"
postcss-functions "^3.0.0"
postcss-js "^2.0.0"
postcss-nested "^4.1.1"
postcss-selector-parser "^6.0.0"
pretty-hrtime "^1.0.3"
reduce-css-calc "^2.1.6"
resolve "^1.14.2"
uniq@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff"
integrity sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=
universalify@^0.1.0:
version "0.1.2"
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
wrappy@1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
xtend@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==

View file

@ -0,0 +1,13 @@
{ pkgs, briefcase, ... }:
pkgs.stdenv.mkDerivation {
name = "covid-uk";
buildInputs = [];
src = builtins.path { path = ./.; name = "sandbox"; };
buildPhase = ''
mkdir -p $out
cp $src/index.html $out
cp -r ${briefcase.website.sandbox.covid-uk} $out/covid-uk
'';
dontInstall = true;
}

View file

@ -0,0 +1,28 @@
# Github Issues Service (GIS)
> 'Cause I got issues. But you got 'em too...
> - [Issues by Julia Michaels][issues]
You have a website and your users want to request features or report bugs. How
do they do this?
Our robot, GIS, can help you. GIS adds a widget to your website that allows
users to easily request features and report bugs.
## Getting Started
If Github is hosting your website's source code, you're ready to start using
GIS. GIS works with public and private repositories.
Let's adopt Github's notion of "issues" to group feature requests and bug
reports together. When users click the GIS widget to create an issue, GIS
displays a modal form that the user completes. When the user submits the form,
GIS creates an issue on your Github repository. Now your team can use all of
Github's rich issue-tracking tools to manage your issues.
## Installation
To add GIS to your website, register your Github repository with us and we'll
give you a snippet to add to your website's HTML. It's that simple.
[issues]: https://www.youtube.com/watch?v=9Ke4480MicU

View file

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>sandbox.wpcarro.dev</title>
</head>
<body>
<h1>Projects</h1>
<ul>
<li>
<a href="/covid-uk">COVID-19 in the UK</a>
</li>
</ul>
</body>
</html>

Some files were not shown because too many files have changed in this diff Show more