all repos

onasty @ 58c535a

a one-time notes service
16 files changed, 268 insertions(+), 72 deletions(-)
web: add some styles (#136)

* web: setup postcss

* web: delete debug things from home page

* web: refactor how user is stored

* web: add shared header

* web: set theme to light

* web(header): change the list of links that shown to user based on status

* web: fix lining errors

* web: change the look of auth form

* chore(ci): check the formatting of elm code

* fixup! web: fix lining errors
Author: Smirnov Oleksandr ss2316544@gmail.com
Committed by: GitHub noreply@github.com
Committed at: 2025-06-19 19:46:27 +0300
Parent: 8ffca5c
M .github/workflows/elm.yml

@@ -36,6 +36,10 @@

- name: elm-review run: bunx elm-review --ignore-dirs .elm-land + - name: elm-format + run: bunx elm-format --validate src/ + + - name: Tests run: bunx elm-test-rs
A web/.postcssrc.json

@@ -0,0 +1,6 @@

+{ + "plugins": { + "postcss-import": {}, + "autoprefixer": {} + } +}
M web/bun.lock

@@ -4,9 +4,13 @@ "workspaces": {

"": { "name": "web", "devDependencies": { + "autoprefixer": "^10.4.21", "elm-land": "^0.20.1", "elm-review": "^2.13.2", "elm-tooling": "^1.15.1", + "missing.css": "^1.1.3", + "postcss": "^8.5.6", + "postcss-import": "^16.1.1", }, }, },

@@ -157,6 +161,8 @@ "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],

"anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], + "autoprefixer": ["autoprefixer@10.4.21", "", { "dependencies": { "browserslist": "^4.24.4", "caniuse-lite": "^1.0.30001702", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ=="], + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],

@@ -169,6 +175,8 @@ "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],

"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + "browserslist": ["browserslist@4.25.0", "", { "dependencies": { "caniuse-lite": "^1.0.30001718", "electron-to-chromium": "^1.5.160", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA=="], + "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],

@@ -177,6 +185,8 @@ "cacheable-lookup": ["cacheable-lookup@5.0.4", "", {}, "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA=="],

"cacheable-request": ["cacheable-request@7.0.4", "", { "dependencies": { "clone-response": "^1.0.2", "get-stream": "^5.1.0", "http-cache-semantics": "^4.0.0", "keyv": "^4.0.0", "lowercase-keys": "^2.0.0", "normalize-url": "^6.0.1", "responselike": "^2.0.0" } }, "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg=="], + "caniuse-lite": ["caniuse-lite@1.0.30001723", "", {}, "sha512-1R/elMjtehrFejxwmexeXAtae5UO9iSyFn6G/I806CYC/BLyyBk1EPhrKBkWhy6wM6Xnm47dSJQec+tLJ39WHw=="], + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "chokidar": ["chokidar@3.5.3", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw=="],

@@ -207,6 +217,8 @@ "defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="],

"defer-to-connect": ["defer-to-connect@2.0.1", "", {}, "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg=="], + "electron-to-chromium": ["electron-to-chromium@1.5.170", "", {}, "sha512-GP+M7aeluQo9uAyiTCxgIj/j+PrWhMlY7LFVj8prlsPljd0Fdg9AprlfUi+OCSFWy9Y5/2D/Jrj9HS8Z4rpKWA=="], + "elm": ["elm@0.19.1-6", "", { "optionalDependencies": { "@elm_binaries/darwin_arm64": "0.19.1-0", "@elm_binaries/darwin_x64": "0.19.1-0", "@elm_binaries/linux_x64": "0.19.1-0", "@elm_binaries/win32_x64": "0.19.1-0" }, "bin": { "elm": "bin/elm" } }, "sha512-mKYyierHICPdMx/vhiIacdPmTPnh889gjHOZ75ZAoCxo3lZmSWbGP8HMw78wyctJH0HwvTmeKhlYSWboQNYPeQ=="], "elm-land": ["elm-land@0.20.1", "", { "dependencies": { "@lydell/elm": "0.19.1-14", "chokidar": "3.5.3", "terser": "5.15.1", "typescript": "4.9.3", "vite": "5.2.8", "vite-plugin-elm-watch": "1.3.3" }, "bin": { "elm-land": "src/index.js" } }, "sha512-AY8BxYNT7mblaIO9SS2YQPdskZdMsLL6fqjAA5bORdkGIRDkMeaw+rXgiVSHUM2+TK0k/ld0TdQEAd24Moi5nw=="],

@@ -223,6 +235,8 @@ "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],

"esbuild": ["esbuild@0.20.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.20.2", "@esbuild/android-arm": "0.20.2", "@esbuild/android-arm64": "0.20.2", "@esbuild/android-x64": "0.20.2", "@esbuild/darwin-arm64": "0.20.2", "@esbuild/darwin-x64": "0.20.2", "@esbuild/freebsd-arm64": "0.20.2", "@esbuild/freebsd-x64": "0.20.2", "@esbuild/linux-arm": "0.20.2", "@esbuild/linux-arm64": "0.20.2", "@esbuild/linux-ia32": "0.20.2", "@esbuild/linux-loong64": "0.20.2", "@esbuild/linux-mips64el": "0.20.2", "@esbuild/linux-ppc64": "0.20.2", "@esbuild/linux-riscv64": "0.20.2", "@esbuild/linux-s390x": "0.20.2", "@esbuild/linux-x64": "0.20.2", "@esbuild/netbsd-x64": "0.20.2", "@esbuild/openbsd-x64": "0.20.2", "@esbuild/sunos-x64": "0.20.2", "@esbuild/win32-arm64": "0.20.2", "@esbuild/win32-ia32": "0.20.2", "@esbuild/win32-x64": "0.20.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g=="], + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + "fastest-levenshtein": ["fastest-levenshtein@1.0.16", "", {}, "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg=="], "fdir": ["fdir@6.4.6", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w=="],

@@ -233,7 +247,11 @@ "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],

"folder-hash": ["folder-hash@3.3.3", "", { "dependencies": { "debug": "^4.1.1", "graceful-fs": "~4.2.0", "minimatch": "~3.0.4" }, "bin": { "folder-hash": "bin/folder-hash" } }, "sha512-SDgHBgV+RCjrYs8aUwCb9rTgbTVuSdzvFmLaChsLre1yf+D64khCW++VYciaByZ8Rm0uKF8R/XEpXuTRSGUM1A=="], + "fraction.js": ["fraction.js@4.3.7", "", {}, "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew=="], + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], "get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="],

@@ -245,6 +263,8 @@ "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],

"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + "http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="], "http2-wrapper": ["http2-wrapper@1.0.3", "", { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.0.0" } }, "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg=="],

@@ -254,6 +274,8 @@

"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="], + + "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],

@@ -291,11 +313,17 @@ "minimatch": ["minimatch@3.0.8", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q=="],

"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + "missing.css": ["missing.css@1.1.3", "", {}, "sha512-dkGzliE9Zcv9tGPvuIHy1lpMc4Y5QAc+svF7Nqi+EMl4taRC5HfjARg55YC2jC9cbqOwX0qOUltUxqn57YIQiw=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + "node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="], + "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], + + "normalize-range": ["normalize-range@0.1.2", "", {}, "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA=="], "normalize-url": ["normalize-url@6.1.0", "", {}, "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A=="],

@@ -315,21 +343,33 @@ "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],

"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], + "pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="], + "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + "postcss-import": ["postcss-import@16.1.1", "", { "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", "resolve": "^1.1.7" }, "peerDependencies": { "postcss": "^8.0.0" } }, "sha512-2xVS1NCZAfjtVdvXiyegxzJ447GyqCeEI5V7ApgQVOWnros1p5lGNovJNapwPpMombyFBfqDwt7AD3n2l0KOfQ=="], + + "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], + "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], "pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="], "quick-lru": ["quick-lru@5.1.1", "", {}, "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="], + "read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="], + "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], + + "resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="], "resolve-alpn": ["resolve-alpn@1.2.1", "", {}, "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="],

@@ -367,6 +407,8 @@ "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],

"supports-hyperlinks": ["supports-hyperlinks@2.3.0", "", { "dependencies": { "has-flag": "^4.0.0", "supports-color": "^7.0.0" } }, "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA=="], + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + "terminal-link": ["terminal-link@2.1.1", "", { "dependencies": { "ansi-escapes": "^4.2.1", "supports-hyperlinks": "^2.0.0" } }, "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ=="], "terser": ["terser@5.15.1", "", { "dependencies": { "@jridgewell/source-map": "^0.3.2", "acorn": "^8.5.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-K1faMUvpm/FBxjBXud0LWVAGxmvoPbZbfTCYbSgaaYQaIXI3/TdI7a7ZGA73Zrou6Q8Zmz3oeUTsp/dj+ag2Xw=="],

@@ -382,6 +424,8 @@

"typescript": ["typescript@4.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA=="], "undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], + + "update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="], "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
M web/elm-land.json

@@ -12,7 +12,8 @@ "env": [],

"html": { "attributes": { "html": { - "lang": "en" + "lang": "en", + "class": "-no-dark-theme" }, "head": {} },
M web/package.json

@@ -4,8 +4,12 @@ "scripts": {

"postinstall": "elm-tooling install" }, "devDependencies": { + "autoprefixer": "^10.4.21", "elm-land": "^0.20.1", "elm-review": "^2.13.2", - "elm-tooling": "^1.15.1" + "elm-tooling": "^1.15.1", + "missing.css": "^1.1.3", + "postcss": "^8.5.6", + "postcss-import": "^16.1.1" } }
M web/src/Auth.elm

@@ -1,6 +1,7 @@

module Auth exposing (User, onPageLoad, viewCustomPage) import Auth.Action +import Auth.User import Dict import Route exposing (Route) import Route.Path

@@ -9,28 +10,23 @@ import View exposing (View)

type alias User = - { accessToken : String - , refreshToken : String - } + Auth.User.User {-| Called before an auth-only page is loaded. -} onPageLoad : Shared.Model -> Route () -> Auth.Action.Action User onPageLoad shared _ = - case shared.credentials of - Just credentials -> - Auth.Action.loadPageWithUser - { accessToken = credentials.accessToken - , refreshToken = credentials.refreshToken - } - - _ -> + case shared.user of + Auth.User.NotSignedIn -> Auth.Action.pushRoute { path = Route.Path.Auth , query = Dict.empty , hash = Nothing } + + Auth.User.SignedIn credentials -> + Auth.Action.loadPageWithUser credentials {-| Renders whenever `Auth.Action.loadCustomPage` is returned from `onPageLoad`.
A web/src/Auth/User.elm

@@ -0,0 +1,12 @@

+module Auth.User exposing (SignInStatus(..), User) + + +type alias User = + { accessToken : String + , refreshToken : String + } + + +type SignInStatus + = SignedIn User + | NotSignedIn
M web/src/Effect.elm

@@ -5,9 +5,9 @@ , sendCmd, sendMsg

, pushRoute, replaceRoute , pushRoutePath, replaceRoutePath , loadExternalUrl, back - , sendApiRequest + , sendApiRequest, refreshTokens , signin, logout, saveUser, clearUser - , map, toCmd, refreshTokens + , map, toCmd ) {-|

@@ -29,6 +29,7 @@

-} import Api exposing (HttpRequestDetails) +import Auth.User import Browser.Navigation import Data.Credentials exposing (Credentials) import Dict exposing (Dict)

@@ -308,15 +309,15 @@ SendApiRequest opts ->

let headers : List Http.Header headers = - case options.shared.credentials of - Just tok -> + case options.shared.user of + Auth.User.SignedIn cred -> if not (String.contains opts.endpoint "refresh-tokens") then - [ Http.header "Authorization" ("Bearer " ++ tok.accessToken) ] + [ Http.header "Authorization" ("Bearer " ++ cred.accessToken) ] else [] - Nothing -> + Auth.User.NotSignedIn -> [] in Http.request
A web/src/Layouts/Header.elm

@@ -0,0 +1,101 @@

+module Layouts.Header exposing (Model, Msg, Props, layout) + +import Auth.User +import Effect exposing (Effect) +import Html exposing (Html) +import Html.Attributes as Attr +import Html.Events +import Layout exposing (Layout) +import Route exposing (Route) +import Route.Path +import Shared +import View exposing (View) + + +type alias Props = + {} + + +layout : Props -> Shared.Model -> Route () -> Layout () Model Msg contentMsg +layout _ shared _ = + Layout.new + { init = init + , update = update + , view = view shared + , subscriptions = subscriptions + } + + + +-- MODEL + + +type alias Model = + {} + + +init : () -> ( Model, Effect Msg ) +init _ = + ( {}, Effect.none ) + + + +-- UPDATE + + +type Msg + = UserClickedLogout + + +update : Msg -> Model -> ( Model, Effect Msg ) +update msg model = + case msg of + UserClickedLogout -> + ( model, Effect.logout ) + + +subscriptions : Model -> Sub Msg +subscriptions _ = + Sub.none + + + +-- VIEW + + +view : Shared.Model -> { toContentMsg : Msg -> contentMsg, content : View contentMsg, model : Model } -> View contentMsg +view shared { toContentMsg, content } = + { title = content.title + , body = + [ viewNavbar shared |> Html.map toContentMsg + , Html.main_ [] content.body + ] + } + + +viewNavbar : Shared.Model -> Html Msg +viewNavbar shared = + Html.header [ Attr.class "navbar" ] + [ Html.nav [ Attr.class "f-row justify-content:space-between" ] + [ Html.ul [ Attr.attribute "role" "list" ] + [ Html.li [] [ viewNavLink ( "home", Route.Path.Home_ ) ] ] + , Html.ul [ Attr.attribute "role" "list" ] + (case shared.user of + Auth.User.SignedIn _ -> + [ Html.li [] [ viewNavLink ( "profile", Route.Path.Profile_Me ) ] + , Html.li [] [ Html.a [ Html.Events.onClick UserClickedLogout ] [ Html.text "logout" ] ] + ] + + Auth.User.NotSignedIn -> + [ Html.li [] [ viewNavLink ( "sign in", Route.Path.Auth ) ] + ] + ) + ] + ] + + +viewNavLink : ( String, Route.Path.Path ) -> Html msg +viewNavLink ( label, path ) = + Html.a + [ Route.Path.href path ] + [ Html.text label ]
M web/src/Pages/Auth.elm

@@ -2,12 +2,14 @@ module Pages.Auth exposing (Model, Msg, Variant, page)

import Api import Api.Auth +import Auth.User import Data.Credentials exposing (Credentials) import Effect exposing (Effect) import Html exposing (Html) import Html.Attributes as Attr import Html.Events import Http +import Layouts import Page exposing (Page) import Route exposing (Route) import Route.Path

@@ -23,6 +25,7 @@ , update = update

, subscriptions = subscriptions , view = view } + |> Page.withLayout (\_ -> Layouts.Header {})

@@ -48,11 +51,11 @@ , passwordAgain = ""

, formVariant = SignIn , error = Nothing } - , case shared.credentials of - Just _ -> + , case shared.user of + Auth.User.SignedIn _ -> Effect.pushRoutePath Route.Path.Home_ - Nothing -> + Auth.User.NotSignedIn -> Effect.none )

@@ -146,11 +149,12 @@ view : Model -> View Msg

view model = { title = "Authentication" , body = - [ Html.div [] + [ Html.div [ Attr.class "center" ] -- TODO: add oauth buttons - [ viewChangeVariant model.formVariant - , viewError model.error + [ viewError model.error + , viewChangeVariant model.formVariant , viewForm model + , viewForgotPassword ] ] }

@@ -158,7 +162,7 @@

viewChangeVariant : Variant -> Html Msg viewChangeVariant variant = - Html.div [] + Html.div [ Attr.class "mb1" ] [ Html.button [ Attr.disabled (variant == SignIn) , Html.Events.onClick (UserChangedFormVariant SignIn)

@@ -179,14 +183,14 @@ (case model.formVariant of

SignIn -> [ viewFormInput { field = Email, value = model.email } , viewFormInput { field = Password, value = model.password } - , viewFormControls model + , viewSubmitButton model ] SignUp -> [ viewFormInput { field = Email, value = model.email } , viewFormInput { field = Password, value = model.password } , viewFormInput { field = PasswordAgain, value = model.passwordAgain } - , viewFormControls model + , viewSubmitButton model ] )

@@ -195,8 +199,10 @@ viewError : Maybe Http.Error -> Html Msg

viewError maybeError = case maybeError of Just error -> - Html.div [ Attr.style "color" "red" ] - [ Html.text (Api.errorToFriendlyMessage error) ] + Html.div [ Attr.class "box bad" ] + [ Html.strong [ Attr.class "block titlebar" ] [ Html.text "Error" ] + , Html.text (Api.errorToFriendlyMessage error) + ] Nothing -> Html.text ""

@@ -204,7 +210,7 @@

viewFormInput : { field : Field, value : String } -> Html Msg viewFormInput opts = - Html.div [] + Html.div [ Attr.class "mb1" ] [ Html.label [] [ Html.text (fromFieldToLabel opts.field) ] , Html.div [] [ Html.input

@@ -217,18 +223,23 @@ ]

] -viewFormControls : Model -> Html Msg -viewFormControls model = +viewForgotPassword : Html Msg +viewForgotPassword = Html.div [] + [ Html.a + [ Attr.href "/forgot-password" + , Attr.class "gray" + ] + [ Html.text "Forgot password?" ] + ] + + +viewSubmitButton : Model -> Html Msg +viewSubmitButton model = + Html.div [ Attr.class "mb1" ] [ Html.button [ Attr.disabled (isFormDisabled model) ] - (case model.formVariant of - SignIn -> - [ Html.text "Sign In" ] - - SignUp -> - [ Html.text "Sign Up" ] - ) + [ Html.text (fromVariantToLabel model.formVariant) ] ]

@@ -246,6 +257,16 @@ || String.isEmpty model.email

|| String.isEmpty model.password || String.isEmpty model.passwordAgain || (model.password /= model.passwordAgain) + + +fromVariantToLabel : Variant -> String +fromVariantToLabel variant = + case variant of + SignIn -> + "Sign In" + + SignUp -> + "Sign Up" fromFieldToLabel : Field -> String
M web/src/Pages/Home_.elm

@@ -2,8 +2,8 @@ module Pages.Home_ exposing (Model, Msg, page)

import Effect exposing (Effect) import Html -import Html.Attributes as Attributes import Html.Events +import Layouts import Page exposing (Page) import Route exposing (Route) import Shared

@@ -18,6 +18,7 @@ , update = update

, subscriptions = subscriptions , view = view shared } + |> Page.withLayout Layouts.Header

@@ -38,14 +39,14 @@ -- UPDATE

type Msg - = LogOut + = NoOp update : Msg -> Model -> ( Model, Effect Msg ) update msg model = case msg of - LogOut -> - ( model, Effect.logout ) + NoOp -> + ( model, Effect.none )

@@ -64,19 +65,5 @@

view : Shared.Model -> Model -> View Msg view _ _ = { title = "Homepage" - , body = - [ Html.div [] - [ Html.p [] [ Html.text "Hello, world!" ] - , Html.p [] - [ Html.a - [ Attributes.href "/profile/me" ] - [ Html.text "/profile/me - fetches authorized data" ] - ] - , Html.p [] - [ Html.button - [ Html.Events.onClick LogOut ] - [ Html.text "Logout" ] - ] - ] - ] + , body = [ Html.p [ Html.Events.onClick NoOp ] [ Html.text "Hello, world!" ] ] }
M web/src/Pages/Profile/Me.elm

@@ -7,6 +7,7 @@ import Data.Me exposing (Me)

import Effect exposing (Effect) import Html exposing (Html) import Http +import Layouts import Page exposing (Page) import Route exposing (Route) import Shared

@@ -21,6 +22,7 @@ , update = update

, subscriptions = subscriptions , view = view shared } + |> Page.withLayout (\_ -> Layouts.Header {})
M web/src/Shared.elm

@@ -13,6 +13,7 @@

-} import Api.Auth +import Auth.User import Data.Credentials exposing (Credentials) import Dict import Effect exposing (Effect)

@@ -65,9 +66,18 @@ (\access refresh -> { accessToken = access, refreshToken = refresh })

flags.accessToken flags.refreshToken + user : Auth.User.SignInStatus + user = + case maybeCredentials of + Just credentials -> + Auth.User.SignedIn credentials + + Nothing -> + Auth.User.NotSignedIn + initModel : Model initModel = - { credentials = maybeCredentials + { user = user , timeZone = Time.utc , isRefreshingTokens = False }

@@ -95,10 +105,10 @@ Shared.Msg.GotZone timeZone ->

( { model | timeZone = timeZone }, Effect.none ) Shared.Msg.Logout -> - ( { model | credentials = Nothing }, Effect.clearUser ) + ( { model | user = Auth.User.NotSignedIn }, Effect.clearUser ) Shared.Msg.SignedIn credentials -> - ( { model | credentials = Just credentials } + ( { model | user = Auth.User.SignedIn credentials } , Effect.batch [ Effect.pushRoute { path = Route.Path.Home_

@@ -110,20 +120,20 @@ ]

) Shared.Msg.CheckTokenExpiration now -> - case model.credentials of - Just credentials -> + case model.user of + Auth.User.SignedIn credentials -> if JwtUtil.isExpired now credentials.accessToken then ( model, Effect.refreshTokens ) else ( model, Effect.none ) - Nothing -> + Auth.User.NotSignedIn -> ( model, Effect.none ) Shared.Msg.TriggerTokenRefresh -> - case model.credentials of - Just credentials -> + case model.user of + Auth.User.SignedIn credentials -> ( { model | isRefreshingTokens = True } , Api.Auth.refreshToken { onResponse = Shared.Msg.ApiRefreshTokensResponded

@@ -131,11 +141,11 @@ , refreshToken = credentials.refreshToken

} ) - Nothing -> + Auth.User.NotSignedIn -> ( model, Effect.none ) Shared.Msg.ApiRefreshTokensResponded (Ok credentials) -> - ( { model | isRefreshingTokens = False, credentials = Just credentials } + ( { model | isRefreshingTokens = False, user = Auth.User.SignedIn credentials } , Effect.saveUser credentials.accessToken credentials.refreshToken )
M web/src/Shared/Model.elm

@@ -1,11 +1,11 @@

module Shared.Model exposing (Model) -import Data.Credentials exposing (Credentials) +import Auth.User import Time type alias Model = - { credentials : Maybe Credentials + { user : Auth.User.SignInStatus , timeZone : Time.Zone , isRefreshingTokens : Bool }
M web/src/interop.js

@@ -1,3 +1,5 @@

+import "./styles.css"; + export const flags = (_) => { return { access_token: JSON.parse(window.localStorage.access_token || 'null'),

@@ -8,7 +10,7 @@

export const onReady = ({ app }) => { if (app.ports?.sendToLocalStorage) { app.ports.sendToLocalStorage.subscribe(({ key, value }) => { - window.localStorage[key] = JSON.stringify(value) + window.localStorage[key] = JSON.stringify(value); }) } }
A web/src/styles.css

@@ -0,0 +1,5 @@

+@import "missing.css"; + +.mb1 { + margin-bottom: 1rem; +}