From 4f87830f3d357141fdea72d8f45307842b6ae8d6 Mon Sep 17 00:00:00 2001 From: Sambit Sahoo Date: Sun, 3 Nov 2024 02:40:53 +0530 Subject: [PATCH] feat: added initial setup for UI --- .gitignore | 4 + bun.lockb | Bin 0 -> 44490 bytes gleam.toml | 16 +- index.ts | 1 + manifest.toml | 4 + package.json | 14 + priv/ui/components/login.html | 9 + priv/ui/css/index.css | 852 ++++++++++++++++++++++ priv/ui/js/boot.js | 18 + src/app/controllers/home.gleam | 22 - src/app/controllers/sessions.gleam | 6 +- src/app/css/app.css | 3 + src/app/css/tailwind.gleam | 52 ++ src/app/hooks/auth.gleam | 29 +- src/app/hooks/hook.gleam | 5 + src/app/hooks/ui.gleam | 102 +++ src/app/lib/auth_cookie.gleam | 22 + src/app/lib/logger.gleam | 2 +- src/app/router.gleam | 3 - src/app/serializers/user_serializer.gleam | 14 +- src/dev.gleam | 28 + src/okane.gleam | 25 +- tailwind.config.js | 8 + tsconfig.json | 27 + 24 files changed, 1187 insertions(+), 79 deletions(-) create mode 100755 bun.lockb create mode 100644 index.ts create mode 100644 package.json create mode 100644 priv/ui/components/login.html create mode 100644 priv/ui/css/index.css create mode 100644 priv/ui/js/boot.js delete mode 100644 src/app/controllers/home.gleam create mode 100644 src/app/css/app.css create mode 100644 src/app/css/tailwind.gleam create mode 100644 src/app/hooks/ui.gleam create mode 100644 src/app/lib/auth_cookie.gleam create mode 100644 src/dev.gleam create mode 100644 tailwind.config.js create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore index 68c6351..58e13d3 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,7 @@ erl_crash.dump .lasso-marks-tracker + + +# blak hole +node_modules diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..e882a753af62752f0d1a8f6b04788e3b4b6b75bf GIT binary patch literal 44490 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 G_x}L7gMlmn literal 0 HcmV?d00001 diff --git a/gleam.toml b/gleam.toml index 48103e1..73c39be 100644 --- a/gleam.toml +++ b/gleam.toml @@ -1,14 +1,18 @@ name = "okane" version = "1.0.0" -# Fill out these fields if you intend to generate HTML documentation or publish -# your project to the Hex package manager. -# -# description = "" +description = "A bill splitting app" licences = ["MIT"] repository = { type = "github", user = "soulsam480", repo = "okane" } # links = [{ title = "Website", href = "https://gleam.run" }] -# + +[tailwind] +args = [ + "--config=tailwind.config.js", + "--input=./src/app/css/app.css", + "--output=./priv/ui/css/app.css" +] +path = "./node_modules/.bin/tailwind" migrations_dir = "./src/app/db/migrations" schemafile = "./src/app/db/schema.sql" @@ -29,6 +33,8 @@ feather = ">= 1.2.0 and < 2.0.0" decode = ">= 0.3.0 and < 1.0.0" birl = ">= 1.7.1 and < 2.0.0" gleam_json = ">= 2.0.0 and < 3.0.0" +filespy = ">= 0.5.0 and < 1.0.0" [dev-dependencies] gleeunit = "~> 1.0.0" +glailglind = ">= 1.1.3 and < 2.0.0" diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..f67b2c6 --- /dev/null +++ b/index.ts @@ -0,0 +1 @@ +console.log("Hello via Bun!"); \ No newline at end of file diff --git a/manifest.toml b/manifest.toml index 57a1a3e..da76753 100644 --- a/manifest.toml +++ b/manifest.toml @@ -15,9 +15,11 @@ packages = [ { name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" }, { name = "filespy", version = "0.5.0", build_tools = ["gleam"], requirements = ["fs", "gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "filespy", source = "hex", outer_checksum = "F8E7A9C9CA86D68CCC25491125BFF36BEF7483892D7BEC24AA30D6B540504F06" }, { name = "fs", version = "8.6.1", build_tools = ["rebar3"], requirements = [], otp_app = "fs", source = "hex", outer_checksum = "61EA2BDAEDAE4E2024D0D25C63E44DCCF65622D4402DB4A2DF12868D1546503F" }, + { name = "glailglind", version = "1.1.3", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_http", "gleam_httpc", "gleam_stdlib", "shellout", "simplifile", "tom"], otp_app = "glailglind", source = "hex", outer_checksum = "4617C93C84172CF99EC05B4706720098A0BF299539DD58DD3D90DB2BF2B472F8" }, { name = "gleam_crypto", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "8AE56026B3E05EBB1F076778478A762E9EB62B31AEEB4285755452F397029D22" }, { name = "gleam_erlang", version = "0.27.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "DE468F676D71B313C6C8C5334425CFCF827837333F8AB47B64D8A6D7AA40185D" }, { name = "gleam_http", version = "3.7.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "EA66440C2269F7CED0F6845E5BD0DB68095775D627FA709A841CA78A398D6D56" }, + { name = "gleam_httpc", version = "2.3.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "CF6CDD88830CC9853F7638ECC0BE7D7CD9522640DA5FAB4C08CFAC8DEBD08028" }, { name = "gleam_json", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "CB10B0E7BF44282FB25162F1A24C1A025F6B93E777CCF238C4017E4EEF2CDE97" }, { name = "gleam_otp", version = "0.12.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "BFACC1513410DF5A1617169A9CD7EA334973AC71D860A17574BA7B2EADD89A6F" }, { name = "gleam_stdlib", version = "0.40.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "86606B75A600BBD05E539EB59FABC6E307EEEA7B1E5865AFB6D980A93BCB2181" }, @@ -47,6 +49,8 @@ cake = { version = "~> 2.0.1" } decode = { version = ">= 0.3.0 and < 1.0.0" } dot_env = { version = "~> 1.2.0" } feather = { version = ">= 1.2.0 and < 2.0.0" } +filespy = { version = ">= 0.5.0 and < 1.0.0" } +glailglind = { version = ">= 1.1.3 and < 2.0.0" } gleam_erlang = { version = "~> 0.27.0" } gleam_http = { version = "~> 3.7.0" } gleam_json = { version = ">= 2.0.0 and < 3.0.0" } diff --git a/package.json b/package.json new file mode 100644 index 0000000..e4465d8 --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "name": "okane", + "type": "module", + "devDependencies": { + "@types/bun": "latest", + "tailwindcss": "^3.4.14" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "dependencies": { + "daisyui": "^4.12.14" + } +} diff --git a/priv/ui/components/login.html b/priv/ui/components/login.html new file mode 100644 index 0000000..eeeb3db --- /dev/null +++ b/priv/ui/components/login.html @@ -0,0 +1,9 @@ + diff --git a/priv/ui/css/index.css b/priv/ui/css/index.css new file mode 100644 index 0000000..906acd0 --- /dev/null +++ b/priv/ui/css/index.css @@ -0,0 +1,852 @@ +*, ::before, ::after { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; + --tw-contain-size: ; + --tw-contain-layout: ; + --tw-contain-paint: ; + --tw-contain-style: ; +} + +::backdrop { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; + --tw-contain-size: ; + --tw-contain-layout: ; + --tw-contain-paint: ; + --tw-contain-style: ; +} + +/* +! tailwindcss v3.4.14 | MIT License | https://tailwindcss.com +*/ + +/* +1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) +2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) +*/ + +*, +::before, +::after { + box-sizing: border-box; + /* 1 */ + border-width: 0; + /* 2 */ + border-style: solid; + /* 2 */ + border-color: #e5e7eb; + /* 2 */ +} + +::before, +::after { + --tw-content: ''; +} + +/* +1. Use a consistent sensible line-height in all browsers. +2. Prevent adjustments of font size after orientation changes in iOS. +3. Use a more readable tab size. +4. Use the user's configured `sans` font-family by default. +5. Use the user's configured `sans` font-feature-settings by default. +6. Use the user's configured `sans` font-variation-settings by default. +7. Disable tap highlights on iOS +*/ + +html, +:host { + line-height: 1.5; + /* 1 */ + -webkit-text-size-adjust: 100%; + /* 2 */ + -moz-tab-size: 4; + /* 3 */ + -o-tab-size: 4; + tab-size: 4; + /* 3 */ + font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + /* 4 */ + font-feature-settings: normal; + /* 5 */ + font-variation-settings: normal; + /* 6 */ + -webkit-tap-highlight-color: transparent; + /* 7 */ +} + +/* +1. Remove the margin in all browsers. +2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. +*/ + +body { + margin: 0; + /* 1 */ + line-height: inherit; + /* 2 */ +} + +/* +1. Add the correct height in Firefox. +2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) +3. Ensure horizontal rules are visible by default. +*/ + +hr { + height: 0; + /* 1 */ + color: inherit; + /* 2 */ + border-top-width: 1px; + /* 3 */ +} + +/* +Add the correct text decoration in Chrome, Edge, and Safari. +*/ + +abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; +} + +/* +Remove the default font size and weight for headings. +*/ + +h1, +h2, +h3, +h4, +h5, +h6 { + font-size: inherit; + font-weight: inherit; +} + +/* +Reset links to optimize for opt-in styling instead of opt-out. +*/ + +a { + color: inherit; + text-decoration: inherit; +} + +/* +Add the correct font weight in Edge and Safari. +*/ + +b, +strong { + font-weight: bolder; +} + +/* +1. Use the user's configured `mono` font-family by default. +2. Use the user's configured `mono` font-feature-settings by default. +3. Use the user's configured `mono` font-variation-settings by default. +4. Correct the odd `em` font sizing in all browsers. +*/ + +code, +kbd, +samp, +pre { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + /* 1 */ + font-feature-settings: normal; + /* 2 */ + font-variation-settings: normal; + /* 3 */ + font-size: 1em; + /* 4 */ +} + +/* +Add the correct font size in all browsers. +*/ + +small { + font-size: 80%; +} + +/* +Prevent `sub` and `sup` elements from affecting the line height in all browsers. +*/ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* +1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) +2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) +3. Remove gaps between table borders by default. +*/ + +table { + text-indent: 0; + /* 1 */ + border-color: inherit; + /* 2 */ + border-collapse: collapse; + /* 3 */ +} + +/* +1. Change the font styles in all browsers. +2. Remove the margin in Firefox and Safari. +3. Remove default padding in all browsers. +*/ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; + /* 1 */ + font-feature-settings: inherit; + /* 1 */ + font-variation-settings: inherit; + /* 1 */ + font-size: 100%; + /* 1 */ + font-weight: inherit; + /* 1 */ + line-height: inherit; + /* 1 */ + letter-spacing: inherit; + /* 1 */ + color: inherit; + /* 1 */ + margin: 0; + /* 2 */ + padding: 0; + /* 3 */ +} + +/* +Remove the inheritance of text transform in Edge and Firefox. +*/ + +button, +select { + text-transform: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Remove default button styles. +*/ + +button, +input:where([type='button']), +input:where([type='reset']), +input:where([type='submit']) { + -webkit-appearance: button; + /* 1 */ + background-color: transparent; + /* 2 */ + background-image: none; + /* 2 */ +} + +/* +Use the modern Firefox focus style for all focusable elements. +*/ + +:-moz-focusring { + outline: auto; +} + +/* +Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) +*/ + +:-moz-ui-invalid { + box-shadow: none; +} + +/* +Add the correct vertical alignment in Chrome and Firefox. +*/ + +progress { + vertical-align: baseline; +} + +/* +Correct the cursor style of increment and decrement buttons in Safari. +*/ + +::-webkit-inner-spin-button, +::-webkit-outer-spin-button { + height: auto; +} + +/* +1. Correct the odd appearance in Chrome and Safari. +2. Correct the outline style in Safari. +*/ + +[type='search'] { + -webkit-appearance: textfield; + /* 1 */ + outline-offset: -2px; + /* 2 */ +} + +/* +Remove the inner padding in Chrome and Safari on macOS. +*/ + +::-webkit-search-decoration { + -webkit-appearance: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Change font properties to `inherit` in Safari. +*/ + +::-webkit-file-upload-button { + -webkit-appearance: button; + /* 1 */ + font: inherit; + /* 2 */ +} + +/* +Add the correct display in Chrome and Safari. +*/ + +summary { + display: list-item; +} + +/* +Removes the default spacing and border for appropriate elements. +*/ + +blockquote, +dl, +dd, +h1, +h2, +h3, +h4, +h5, +h6, +hr, +figure, +p, +pre { + margin: 0; +} + +fieldset { + margin: 0; + padding: 0; +} + +legend { + padding: 0; +} + +ol, +ul, +menu { + list-style: none; + margin: 0; + padding: 0; +} + +/* +Reset default styling for dialogs. +*/ + +dialog { + padding: 0; +} + +/* +Prevent resizing textareas horizontally by default. +*/ + +textarea { + resize: vertical; +} + +/* +1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) +2. Set the default placeholder color to the user's configured gray 400 color. +*/ + +input::-moz-placeholder, textarea::-moz-placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +input::placeholder, +textarea::placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +/* +Set the default cursor for buttons. +*/ + +button, +[role="button"] { + cursor: pointer; +} + +/* +Make sure disabled buttons don't get the pointer cursor. +*/ + +:disabled { + cursor: default; +} + +/* +1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) +2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) + This can trigger a poorly considered lint error in some tools but is included by design. +*/ + +img, +svg, +video, +canvas, +audio, +iframe, +embed, +object { + display: block; + /* 1 */ + vertical-align: middle; + /* 2 */ +} + +/* +Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) +*/ + +img, +video { + max-width: 100%; + height: auto; +} + +/* Make elements with the HTML hidden attribute stay hidden by default */ + +[hidden]:where(:not([hidden="until-found"])) { + display: none; +} + +:root, +[data-theme] { + background-color: var(--fallback-b1,oklch(var(--b1)/1)); + color: var(--fallback-bc,oklch(var(--bc)/1)); +} + +@supports not (color: oklch(0% 0 0)) { + :root { + color-scheme: light; + --fallback-p: #491eff; + --fallback-pc: #d4dbff; + --fallback-s: #ff41c7; + --fallback-sc: #fff9fc; + --fallback-a: #00cfbd; + --fallback-ac: #00100d; + --fallback-n: #2b3440; + --fallback-nc: #d7dde4; + --fallback-b1: #ffffff; + --fallback-b2: #e5e6e6; + --fallback-b3: #e5e6e6; + --fallback-bc: #1f2937; + --fallback-in: #00b3f0; + --fallback-inc: #000000; + --fallback-su: #00ca92; + --fallback-suc: #000000; + --fallback-wa: #ffc22d; + --fallback-wac: #000000; + --fallback-er: #ff6f70; + --fallback-erc: #000000; + } + + @media (prefers-color-scheme: dark) { + :root { + color-scheme: dark; + --fallback-p: #7582ff; + --fallback-pc: #050617; + --fallback-s: #ff71cf; + --fallback-sc: #190211; + --fallback-a: #00c7b5; + --fallback-ac: #000e0c; + --fallback-n: #2a323c; + --fallback-nc: #a6adbb; + --fallback-b1: #1d232a; + --fallback-b2: #191e24; + --fallback-b3: #15191e; + --fallback-bc: #a6adbb; + --fallback-in: #00b3f0; + --fallback-inc: #000000; + --fallback-su: #00ca92; + --fallback-suc: #000000; + --fallback-wa: #ffc22d; + --fallback-wac: #000000; + --fallback-er: #ff6f70; + --fallback-erc: #000000; + } + } +} + +html { + -webkit-tap-highlight-color: transparent; +} + +* { + scrollbar-color: color-mix(in oklch, currentColor 35%, transparent) transparent; +} + +*:hover { + scrollbar-color: color-mix(in oklch, currentColor 60%, transparent) transparent; +} + +:root { + color-scheme: light; + --in: 72.06% 0.191 231.6; + --su: 64.8% 0.150 160; + --wa: 84.71% 0.199 83.87; + --er: 71.76% 0.221 22.18; + --pc: 89.824% 0.06192 275.75; + --ac: 15.352% 0.0368 183.61; + --inc: 0% 0 0; + --suc: 0% 0 0; + --wac: 0% 0 0; + --erc: 0% 0 0; + --rounded-box: 1rem; + --rounded-btn: 0.5rem; + --rounded-badge: 1.9rem; + --animation-btn: 0.25s; + --animation-input: .2s; + --btn-focus-scale: 0.95; + --border-btn: 1px; + --tab-border: 1px; + --tab-radius: 0.5rem; + --p: 49.12% 0.3096 275.75; + --s: 69.71% 0.329 342.55; + --sc: 98.71% 0.0106 342.55; + --a: 76.76% 0.184 183.61; + --n: 32.1785% 0.02476 255.701624; + --nc: 89.4994% 0.011585 252.096176; + --b1: 100% 0 0; + --b2: 96.1151% 0 0; + --b3: 92.4169% 0.00108 197.137559; + --bc: 27.8078% 0.029596 256.847952; +} + +@media (prefers-color-scheme: dark) { + :root { + color-scheme: dark; + --in: 72.06% 0.191 231.6; + --su: 64.8% 0.150 160; + --wa: 84.71% 0.199 83.87; + --er: 71.76% 0.221 22.18; + --pc: 13.138% 0.0392 275.75; + --sc: 14.96% 0.052 342.55; + --ac: 14.902% 0.0334 183.61; + --inc: 0% 0 0; + --suc: 0% 0 0; + --wac: 0% 0 0; + --erc: 0% 0 0; + --rounded-box: 1rem; + --rounded-btn: 0.5rem; + --rounded-badge: 1.9rem; + --animation-btn: 0.25s; + --animation-input: .2s; + --btn-focus-scale: 0.95; + --border-btn: 1px; + --tab-border: 1px; + --tab-radius: 0.5rem; + --p: 65.69% 0.196 275.75; + --s: 74.8% 0.26 342.55; + --a: 74.51% 0.167 183.61; + --n: 31.3815% 0.021108 254.139175; + --nc: 74.6477% 0.0216 264.435964; + --b1: 25.3267% 0.015896 252.417568; + --b2: 23.2607% 0.013807 253.100675; + --b3: 21.1484% 0.01165 254.087939; + --bc: 74.6477% 0.0216 264.435964; + } +} + +[data-theme=light] { + color-scheme: light; + --in: 72.06% 0.191 231.6; + --su: 64.8% 0.150 160; + --wa: 84.71% 0.199 83.87; + --er: 71.76% 0.221 22.18; + --pc: 89.824% 0.06192 275.75; + --ac: 15.352% 0.0368 183.61; + --inc: 0% 0 0; + --suc: 0% 0 0; + --wac: 0% 0 0; + --erc: 0% 0 0; + --rounded-box: 1rem; + --rounded-btn: 0.5rem; + --rounded-badge: 1.9rem; + --animation-btn: 0.25s; + --animation-input: .2s; + --btn-focus-scale: 0.95; + --border-btn: 1px; + --tab-border: 1px; + --tab-radius: 0.5rem; + --p: 49.12% 0.3096 275.75; + --s: 69.71% 0.329 342.55; + --sc: 98.71% 0.0106 342.55; + --a: 76.76% 0.184 183.61; + --n: 32.1785% 0.02476 255.701624; + --nc: 89.4994% 0.011585 252.096176; + --b1: 100% 0 0; + --b2: 96.1151% 0 0; + --b3: 92.4169% 0.00108 197.137559; + --bc: 27.8078% 0.029596 256.847952; +} + +[data-theme=dark] { + color-scheme: dark; + --in: 72.06% 0.191 231.6; + --su: 64.8% 0.150 160; + --wa: 84.71% 0.199 83.87; + --er: 71.76% 0.221 22.18; + --pc: 13.138% 0.0392 275.75; + --sc: 14.96% 0.052 342.55; + --ac: 14.902% 0.0334 183.61; + --inc: 0% 0 0; + --suc: 0% 0 0; + --wac: 0% 0 0; + --erc: 0% 0 0; + --rounded-box: 1rem; + --rounded-btn: 0.5rem; + --rounded-badge: 1.9rem; + --animation-btn: 0.25s; + --animation-input: .2s; + --btn-focus-scale: 0.95; + --border-btn: 1px; + --tab-border: 1px; + --tab-radius: 0.5rem; + --p: 65.69% 0.196 275.75; + --s: 74.8% 0.26 342.55; + --a: 74.51% 0.167 183.61; + --n: 31.3815% 0.021108 254.139175; + --nc: 74.6477% 0.0216 264.435964; + --b1: 25.3267% 0.015896 252.417568; + --b2: 23.2607% 0.013807 253.100675; + --b3: 21.1484% 0.01165 254.087939; + --bc: 74.6477% 0.0216 264.435964; +} + +@keyframes button-pop { + 0% { + transform: scale(var(--btn-focus-scale, 0.98)); + } + + 40% { + transform: scale(1.02); + } + + 100% { + transform: scale(1); + } +} + +@keyframes checkmark { + 0% { + background-position-y: 5px; + } + + 50% { + background-position-y: -2px; + } + + 100% { + background-position-y: 0; + } +} + +@keyframes modal-pop { + 0% { + opacity: 0; + } +} + +@keyframes progress-loading { + 50% { + background-position-x: -115%; + } +} + +@keyframes radiomark { + 0% { + box-shadow: 0 0 0 12px var(--fallback-b1,oklch(var(--b1)/1)) inset, + 0 0 0 12px var(--fallback-b1,oklch(var(--b1)/1)) inset; + } + + 50% { + box-shadow: 0 0 0 3px var(--fallback-b1,oklch(var(--b1)/1)) inset, + 0 0 0 3px var(--fallback-b1,oklch(var(--b1)/1)) inset; + } + + 100% { + box-shadow: 0 0 0 4px var(--fallback-b1,oklch(var(--b1)/1)) inset, + 0 0 0 4px var(--fallback-b1,oklch(var(--b1)/1)) inset; + } +} + +@keyframes rating-pop { + 0% { + transform: translateY(-0.125em); + } + + 40% { + transform: translateY(-0.125em); + } + + 100% { + transform: translateY(0); + } +} + +@keyframes skeleton { + from { + background-position: 150%; + } + + to { + background-position: -50%; + } +} + +@keyframes toast-pop { + 0% { + transform: scale(0.9); + opacity: 0; + } + + 100% { + transform: scale(1); + opacity: 1; + } +} diff --git a/priv/ui/js/boot.js b/priv/ui/js/boot.js new file mode 100644 index 0000000..882b211 --- /dev/null +++ b/priv/ui/js/boot.js @@ -0,0 +1,18 @@ +/** + * @param {string} path + */ +async function load_component(path) { + await import(path).then((comp) => { + console.log("LOG", comp); + }); +} + +// console.log(okane); + +// window.sprae.effect(() => { +// console.log("SOME"); + +// if (window.okane.user === null) { +// console.log("JE::P"); +// } +// }); diff --git a/src/app/controllers/home.gleam b/src/app/controllers/home.gleam deleted file mode 100644 index 737fc67..0000000 --- a/src/app/controllers/home.gleam +++ /dev/null @@ -1,22 +0,0 @@ -import gleam/http.{Get} -import gleam/string_builder -import wisp.{type Request, type Response} - -fn show(_req: Request) -> Response { - // The home page can only be accessed via GET requests, so this middleware is - // used to return a 405: Method Not Allowed response for all other methods. - // use <- wisp.require_method(req, Get) - - let html = string_builder.from_string("Welcome to Okane") - - wisp.ok() - |> wisp.html_body(html) -} - -pub fn controller(req: Request) -> Response { - case req.method { - Get -> show(req) - - _ -> wisp.method_not_allowed([Get]) - } -} diff --git a/src/app/controllers/sessions.gleam b/src/app/controllers/sessions.gleam index f3f5978..5b559d7 100644 --- a/src/app/controllers/sessions.gleam +++ b/src/app/controllers/sessions.gleam @@ -1,6 +1,6 @@ import app/config import app/db/models/user -import app/hooks/auth +import app/lib/auth_cookie import app/serializers/base_serializer import app/serializers/user_serializer import gleam/http @@ -50,7 +50,7 @@ fn handle_login(req: Request, ctx: config.Context) -> Response { // read it when looking for user in auth hook wisp.ok() |> wisp.json_body(user_serializer.run(user)) - |> auth.set_cookie(req, user) + |> auth_cookie.set_cookie(req, user) } False -> { @@ -99,7 +99,7 @@ fn handle_register(req: Request, ctx: config.Context) { Ok(new_user) -> { wisp.ok() |> wisp.json_body(user_serializer.run(new_user)) - |> auth.set_cookie(req, new_user) + |> auth_cookie.set_cookie(req, new_user) } } } diff --git a/src/app/css/app.css b/src/app/css/app.css new file mode 100644 index 0000000..b5c61c9 --- /dev/null +++ b/src/app/css/app.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/src/app/css/tailwind.gleam b/src/app/css/tailwind.gleam new file mode 100644 index 0000000..bbf022e --- /dev/null +++ b/src/app/css/tailwind.gleam @@ -0,0 +1,52 @@ +import filespy +import gleam/list +import gleam/string +import tailwind +import wisp + +pub fn build() { + wisp.log_info("[HOT CSS]: starting tailwind build....") + + let _ = + [ + "--config=tailwind.config.js", "--input=./src/app/css/app.css", + "--output=./priv/ui/css/index.css", + ] + |> tailwind.run() + + wisp.log_info("[HOT CSS]: done building css with tailwind.") +} + +pub fn start() { + // 1. build tailwind on start + build() + + // 2. watch and rebuild on further changes + let _ = + filespy.new() + |> filespy.add_dir("./src/app/css") + |> filespy.add_dir("./priv/ui") + |> filespy.set_handler(fn(path, event) { + let is_path = + !string.ends_with(path, "index.css") + && { + [".gleam", ".css", ".html"] + |> list.any(fn(el) { string.ends_with(path, el) }) + } + + case event { + filespy.Closed | filespy.Closed -> { + case is_path { + True -> build() + _ -> Nil + } + } + _ -> Nil + } + + Nil + }) + |> filespy.start() + + Nil +} diff --git a/src/app/hooks/auth.gleam b/src/app/hooks/auth.gleam index d9a03c2..87359a2 100644 --- a/src/app/hooks/auth.gleam +++ b/src/app/hooks/auth.gleam @@ -1,23 +1,19 @@ import app/config import app/db/models/user +import app/lib/auth_cookie import app/lib/response_helpers import app/serializers/base_serializer +import gleam/option import gleam/result import wisp -pub const cookie_max_age = 604_800 - -pub const cookie_name = "__session" - -pub fn get_cookie( +fn get_cookie( req: wisp.Request, with: fn(String) -> wisp.Response, ) -> wisp.Response { - let cookie_res = wisp.get_cookie(req, cookie_name, wisp.Signed) - - case cookie_res { - Ok(c) -> with(c) - Error(_) -> { + case auth_cookie.get_cookie(req) { + option.Some(c) -> with(c) + option.None -> { response_helpers.unauthorized() |> wisp.json_body(base_serializer.serialize_error( "Invalid token or token not found", @@ -26,17 +22,6 @@ pub fn get_cookie( } } -pub fn set_cookie(res: wisp.Response, req: wisp.Request, user: user.User) { - wisp.set_cookie( - res, - req, - cookie_name, - user.email, - wisp.Signed, - cookie_max_age, - ) -} - /// session/auth hook /// 1. check if cookie is present /// 2. find user if there and put it inside context @@ -46,6 +31,8 @@ pub fn hook( ctx: config.Context, handle: fn(config.Context) -> wisp.Response, ) -> wisp.Response { + // TODO: re-use user inside ctx + use user_email <- get_cookie(req) user.find_by_email(user_email, ctx.db) diff --git a/src/app/hooks/hook.gleam b/src/app/hooks/hook.gleam index 221ed32..941a3b9 100644 --- a/src/app/hooks/hook.gleam +++ b/src/app/hooks/hook.gleam @@ -1,5 +1,6 @@ import app/config.{type Context} import app/hooks/auth +import app/hooks/ui import wisp pub fn hook_on( @@ -19,6 +20,10 @@ pub fn hook_on( // Rewrite HEAD requests to GET requests and return an empty body. use req <- wisp.handle_head(req) + // serve UI + // NOTE: this will add user to context if present + use ctx <- ui.hook(req, ctx) + case wisp.path_segments(req) { ["auth"] -> { use auth_ctx <- auth.hook(req, ctx) diff --git a/src/app/hooks/ui.gleam b/src/app/hooks/ui.gleam new file mode 100644 index 0000000..b9f9645 --- /dev/null +++ b/src/app/hooks/ui.gleam @@ -0,0 +1,102 @@ +import app/config +import app/db/models/user +import app/lib/auth_cookie +import app/serializers/user_serializer +import gleam/json +import gleam/option +import gleam/result +import gleam/string_builder +import wisp + +fn make_ssr_data(user: option.Option(user.User)) { + json.object([ + #("user", case user { + option.Some(u) -> user_serializer.to_json(u) + _ -> json.null() + }), + ]) + |> json.to_string_builder +} + +fn app_shell(user: option.Option(user.User)) { + string_builder.new() + |> string_builder.append( + " + + + + + + Okane | A Bill splitting app + + + +
+
+ +
+
+ + +
", + ) + |> string_builder.append_builder(make_ssr_data(user)) + |> string_builder.append( + "
+ + + +", + ) +} + +/// this hook handles UI and related redirects +/// 1. render app shell with SSR'ed user session +/// 2. serve ui from priv/ui +pub fn hook( + req: wisp.Request, + ctx: config.Context, + handle: fn(config.Context) -> wisp.Response, +) -> wisp.Response { + let assert Ok(priv) = wisp.priv_directory("okane") + + use <- wisp.serve_static(req, "/", priv <> "/ui") + + let user_email = auth_cookie.get_cookie(req) + + // TODO: find ways to improve this + let user = + user_email + |> option.to_result(Nil) + |> result.map(fn(email) { + user.find_by_email(email, ctx.db) |> result.replace_error(Nil) + }) + |> result.flatten + |> option.from_result + + case wisp.path_segments(req) { + [] -> { + wisp.ok() |> wisp.html_body(app_shell(user)) + } + _ -> + handle(case user { + option.Some(u) -> config.set_user(ctx, u) + _ -> ctx + }) + } +} diff --git a/src/app/lib/auth_cookie.gleam b/src/app/lib/auth_cookie.gleam new file mode 100644 index 0000000..0b350e2 --- /dev/null +++ b/src/app/lib/auth_cookie.gleam @@ -0,0 +1,22 @@ +import app/db/models/user +import gleam/option +import wisp + +pub const cookie_max_age = 604_800 + +pub const cookie_name = "__session" + +pub fn get_cookie(req: wisp.Request) -> option.Option(String) { + wisp.get_cookie(req, cookie_name, wisp.Signed) |> option.from_result +} + +pub fn set_cookie(res: wisp.Response, req: wisp.Request, user: user.User) { + wisp.set_cookie( + res, + req, + cookie_name, + user.email, + wisp.Signed, + cookie_max_age, + ) +} diff --git a/src/app/lib/logger.gleam b/src/app/lib/logger.gleam index e59ff2e..3276d95 100644 --- a/src/app/lib/logger.gleam +++ b/src/app/lib/logger.gleam @@ -1,5 +1,5 @@ import wisp pub fn info(message: String) { - wisp.log_notice("[Okane]: " <> message) + wisp.log_info("[Okane]: " <> message) } diff --git a/src/app/router.gleam b/src/app/router.gleam index b2853d1..f0a778d 100644 --- a/src/app/router.gleam +++ b/src/app/router.gleam @@ -1,5 +1,4 @@ import app/config -import app/controllers/home import app/controllers/sessions import app/controllers/users import app/hooks/hook @@ -9,8 +8,6 @@ pub fn handle_request(req: Request, ctx: config.Context) -> Response { use req, ctx <- hook.hook_on(req, ctx) case wisp.path_segments(req) { - [] -> home.controller(req) - ["sessions", ..session_segments] -> sessions.controller(req, ctx |> config.scope_to(session_segments)) diff --git a/src/app/serializers/user_serializer.gleam b/src/app/serializers/user_serializer.gleam index cdec199..841262f 100644 --- a/src/app/serializers/user_serializer.gleam +++ b/src/app/serializers/user_serializer.gleam @@ -2,16 +2,20 @@ import app/db/models/user import app/serializers/base_serializer import gleam/json -/// here we can do many things. taking inspirations from rails serializers -/// 1. control visibility of attributes -/// 2. computed attributes that are only needed during response -/// 3. calling and serializing sub queries -pub fn run(user: user.User) { +pub fn to_json(user: user.User) { json.object([ #("id", json.int(user.id)), #("email", json.string(user.email)), #("name", json.string(user.name)), #("created_at", json.string(user.created_at)), ]) +} + +/// here we can do many things. taking inspirations from rails serializers +/// 1. control visibility of attributes +/// 2. computed attributes that are only needed during response +/// 3. calling and serializing sub queries +pub fn run(user: user.User) { + to_json(user) |> base_serializer.wrap } diff --git a/src/dev.gleam b/src/dev.gleam new file mode 100644 index 0000000..c3072ad --- /dev/null +++ b/src/dev.gleam @@ -0,0 +1,28 @@ +import app/css/tailwind +import gleam/list +import gleam/result +import gleam/string +import radiate +import wisp + +pub fn run() -> Nil { + tailwind.start() + + let _ = + radiate.new() + |> radiate.add_dir("src") + //TODO: handle mac + // |> radiate.add_dir(".") + |> radiate.on_reload(fn(_, path) { + wisp.log_info( + "[HOT RELOAD]: " + <> path + |> string.split("/") + |> list.last() + |> result.unwrap("unknown"), + ) + }) + |> radiate.start() + + Nil +} diff --git a/src/okane.gleam b/src/okane.gleam index 8c5d42a..e98ed5e 100644 --- a/src/okane.gleam +++ b/src/okane.gleam @@ -2,12 +2,11 @@ import app/config import app/db/connection import app/db/migrator import app/router +import dev import dot_env import dot_env/env import gleam/erlang/process -import gleam/io import mist -import radiate import wisp import wisp/wisp_mist @@ -15,28 +14,16 @@ pub fn main() { // load env vars dot_env.load_default() + // This sets the logger to print INFO level logs, and other sensible defaults + // for a web application. + wisp.configure_logger() + // only enable hot-reload in dev case env.get_string_or("MODE", "dev") { - "dev" -> { - let _ = - radiate.new() - |> radiate.add_dir("src") - |> radiate.add_dir(".") - |> radiate.on_reload(fn(_state, path) { - io.println("Change in " <> path <> ", reloading!") - }) - |> radiate.start() - - Nil - } - + "dev" -> dev.run() _ -> Nil } - // This sets the logger to print INFO level logs, and other sensible defaults - // for a web application. - wisp.configure_logger() - let assert Ok(_) = migrator.migrate_to_latest() use db <- connection.with_connection() diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..d09b78f --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,8 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: [], + theme: { + extend: {}, + }, + plugins: [require("daisyui")], +}; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..238655f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + // Enable latest features + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +}