infrastructure

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

commit 7d89c3af10626c4493cb7ad94c0a7a1200e88732
Author: Silas Brack <silasbrack@gmail.com>
Date:   Mon, 30 Sep 2024 18:43:41 +0200

Initial commit

Diffstat:
A.gitignore | 1+
ALICENSE | 0
AREADME.md | 0
Aapps/my-app/README.md | 0
Aapps/my-app/client/.github/workflows/test.yml | 23+++++++++++++++++++++++
Aapps/my-app/client/.gitignore | 4++++
Aapps/my-app/client/README.md | 25+++++++++++++++++++++++++
Aapps/my-app/client/gleam.toml | 22++++++++++++++++++++++
Aapps/my-app/client/manifest.toml | 25+++++++++++++++++++++++++
Aapps/my-app/client/shell.nix | 15+++++++++++++++
Aapps/my-app/client/src/client.gleam | 860+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapps/my-app/client/src/client/state.gleam | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Aapps/my-app/client/test/client_test.gleam | 12++++++++++++
Aapps/my-app/module.nix | 141+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapps/my-app/server/.github/workflows/test.yml | 23+++++++++++++++++++++++
Aapps/my-app/server/.gitignore | 7+++++++
Aapps/my-app/server/README.md | 39+++++++++++++++++++++++++++++++++++++++
Aapps/my-app/server/gleam.toml | 48++++++++++++++++++++++++++++++++++++++++++++++++
Aapps/my-app/server/manifest.toml | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapps/my-app/server/priv/static/client.min.css | 2++
Aapps/my-app/server/priv/static/client.min.mjs | 6++++++
Aapps/my-app/server/shell.nix | 22++++++++++++++++++++++
Aapps/my-app/server/src/server.gleam | 30++++++++++++++++++++++++++++++
Aapps/my-app/server/src/server/db.gleam | 21+++++++++++++++++++++
Aapps/my-app/server/src/server/db/datetime_helper.gleam | 31+++++++++++++++++++++++++++++++
Aapps/my-app/server/src/server/db/login.gleam | 68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapps/my-app/server/src/server/db/post.gleam | 30++++++++++++++++++++++++++++++
Aapps/my-app/server/src/server/env.gleam | 42++++++++++++++++++++++++++++++++++++++++++
Aapps/my-app/server/src/server/response.gleam | 23+++++++++++++++++++++++
Aapps/my-app/server/src/server/router.gleam | 340+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapps/my-app/server/src/server/routes/posts.gleam | 70++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapps/my-app/server/src/server/scaffold.gleam | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapps/my-app/server/src/server/web.gleam | 43+++++++++++++++++++++++++++++++++++++++++++
Aapps/my-app/server/test/server_test.gleam | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Aapps/my-app/shared/.github/workflows/test.yml | 23+++++++++++++++++++++++
Aapps/my-app/shared/.gitignore | 4++++
Aapps/my-app/shared/README.md | 25+++++++++++++++++++++++++
Aapps/my-app/shared/gleam.toml | 21+++++++++++++++++++++
Aapps/my-app/shared/manifest.toml | 14++++++++++++++
Aapps/my-app/shared/shell.nix | 15+++++++++++++++
Aapps/my-app/shared/src/shared.gleam | 5+++++
Aapps/my-app/shared/test/shared_test.gleam | 12++++++++++++
Adb/db-postgresql-ams3-90424/README.md | 0
Adb/db-postgresql-ams3-90424/migrations/000.sql | 2++
Adb/db-postgresql-ams3-90424/migrations/001.sql | 21+++++++++++++++++++++
Adb/db-postgresql-ams3-90424/migrations/002.sql | 7+++++++
Adb/db-postgresql-ams3-90424/migrations/shell.nix | 7+++++++
Adocs/develop.md | 0
Adocs/philosophy.md | 19+++++++++++++++++++
Ainfrastructure/.gitignore | 3+++
Ainfrastructure/Makefile | 78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainfrastructure/README.md | 19+++++++++++++++++++
Ainfrastructure/_apps/default.nix | 6++++++
Ainfrastructure/_apps/hello.nix | 36++++++++++++++++++++++++++++++++++++
Ainfrastructure/_apps/test.nix | 148+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainfrastructure/hive.nix | 85+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainfrastructure/image.nix | 6++++++
Ainfrastructure/shell.nix | 5+++++
Ainfrastructure/vpn.nix | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
59 files changed, 2813 insertions(+), 0 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -0,0 +1 @@ + diff --git a/LICENSE b/LICENSE diff --git a/README.md b/README.md diff --git a/apps/my-app/README.md b/apps/my-app/README.md diff --git a/apps/my-app/client/.github/workflows/test.yml b/apps/my-app/client/.github/workflows/test.yml @@ -0,0 +1,23 @@ +name: test + +on: + push: + branches: + - master + - main + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: erlef/setup-beam@v1 + with: + otp-version: "26.0.2" + gleam-version: "1.1.0" + rebar3-version: "3" + # elixir-version: "1.15.4" + - run: gleam deps download + - run: gleam test + - run: gleam format --check src test diff --git a/apps/my-app/client/.gitignore b/apps/my-app/client/.gitignore @@ -0,0 +1,4 @@ +*.beam +*.ez +/build +erl_crash.dump diff --git a/apps/my-app/client/README.md b/apps/my-app/client/README.md @@ -0,0 +1,25 @@ +# client + +[![Package Version](https://img.shields.io/hexpm/v/client)](https://hex.pm/packages/client) +[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/client/) + +```sh +gleam add client +``` +```gleam +import client + +pub fn main() { + // TODO: An example of the project in use +} +``` + +Further documentation can be found at <https://hexdocs.pm/client>. + +## Development + +```sh +gleam run # Run the project +gleam test # Run the tests +gleam shell # Run an Erlang shell +``` diff --git a/apps/my-app/client/gleam.toml b/apps/my-app/client/gleam.toml @@ -0,0 +1,22 @@ +name = "client" +version = "1.0.0" +target = "javascript" + +# Fill out these fields if you intend to generate HTML documentation or publish +# your project to the Hex package manager. +# +# description = "" +# licences = ["Apache-2.0"] +# repository = { type = "github", user = "username", repo = "project" } +# links = [{ title = "Website", href = "https://gleam.run" }] +# +# For a full reference of all the available options, you can have a look at +# https://gleam.run/writing-gleam/gleam-toml/. + +[dependencies] +gleam_stdlib = ">= 0.34.0 and < 2.0.0" +lustre_http = ">= 0.5.2 and < 1.0.0" +shared = { path = "../shared/" } + +[dev-dependencies] +gleeunit = ">= 1.0.0 and < 2.0.0" diff --git a/apps/my-app/client/manifest.toml b/apps/my-app/client/manifest.toml @@ -0,0 +1,25 @@ +# This file was generated by Gleam +# You typically do not need to edit this file + +packages = [ + { name = "birl", version = "1.7.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "5C66647D62BCB11FE327E7A6024907C4A17954EF22865FE0940B54A852446D01" }, + { name = "gleam_erlang", version = "0.25.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "054D571A7092D2A9727B3E5D183B7507DAB0DA41556EC9133606F09C15497373" }, + { name = "gleam_fetch", version = "0.4.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_javascript", "gleam_stdlib"], otp_app = "gleam_fetch", source = "hex", outer_checksum = "7446410A44A1D1328F5BC1FF4FC9CBD1570479EA69349237B3F82E34521CCC10" }, + { name = "gleam_http", version = "3.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "8C07DF9DF8CC7F054C650839A51C30A7D3C26482AC241C899C1CEA86B22DBE51" }, + { name = "gleam_javascript", version = "0.12.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "6EB652538B31E852FE0A8307A8B6314DEB34930944B6DDC41CCC31CA344DA35D" }, + { name = "gleam_json", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "9063D14D25406326C0255BDA0021541E797D8A7A12573D849462CAFED459F6EB" }, + { name = "gleam_otp", version = "0.11.2", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "517FFB679E44AD71D059F3EF6A17BA6EFC8CB94FA174D52E22FB6768CF684D78" }, + { name = "gleam_stdlib", version = "0.40.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "86606B75A600BBD05E539EB59FABC6E307EEEA7B1E5865AFB6D980A93BCB2181" }, + { name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" }, + { name = "lustre", version = "4.4.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib"], otp_app = "lustre", source = "hex", outer_checksum = "B64EFA26B6DDA8391C85E33B06C2B5F4CD24B9772342824085DBC445B7E05C5A" }, + { name = "lustre_http", version = "0.5.2", build_tools = ["gleam"], requirements = ["gleam_fetch", "gleam_http", "gleam_javascript", "gleam_json", "gleam_stdlib", "lustre"], otp_app = "lustre_http", source = "hex", outer_checksum = "FB0478CBFA6B16DBE8ECA326DAE2EC15645E04900595EF2C4F039ABFA0512ABA" }, + { name = "ranger", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "1566C272B1D141B3BBA38B25CB761EF56E312E79EC0E2DFD4D3C19FB0CC1F98C" }, + { name = "shared", version = "1.0.0", build_tools = ["gleam"], requirements = ["birl", "gleam_stdlib"], source = "local", path = "../shared" }, + { name = "thoas", version = "1.2.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "E38697EDFFD6E91BD12CEA41B155115282630075C2A727E7A6B2947F5408B86A" }, +] + +[requirements] +gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" } +gleeunit = { version = ">= 1.0.0 and < 2.0.0" } +lustre_http = { version = ">= 0.5.2 and < 1.0.0" } +shared = { path = "../shared/" } diff --git a/apps/my-app/client/shell.nix b/apps/my-app/client/shell.nix @@ -0,0 +1,15 @@ +let + # pkgs = import <nixpkgs> {}; + pkgs = import (builtins.fetchTarball { + name = "nixos-24.05"; + url = "https://github.com/nixos/nixpkgs/archive/63dacb46bf939521bdc93981b4cbb7ecb58427a0.tar.gz"; + sha256 = "1lr1h35prqkd1mkmzriwlpvxcb34kmhc9dnr48gkm8hh089hifmx"; + }) {}; +in pkgs.mkShell { + buildInputs = [ + pkgs.gleam + pkgs.erlang_27 + pkgs.rebar3 + ]; + # imports = ["./test.nix"]; +} diff --git a/apps/my-app/client/src/client.gleam b/apps/my-app/client/src/client.gleam @@ -0,0 +1,860 @@ +import client/components/button.{button_class} +import client/components/like.{like_comment, like_post} +import client/routes/change_password.{change_password} +import client/routes/create_post.{create_post_view} +import client/routes/forgot_password.{forgot_password} +import client/routes/latest.{latest_view} +import client/routes/login.{login, login_view} +import client/routes/post.{show_post_view} +import client/routes/signup.{signup_view} +import client/routes/user.{user_view} +import client/state.{ + type Model, type Msg, type Route, Active, AuthUser, AuthUserRecieved, + ChangePassword, ChangePasswordResponded, ChangePasswordTargetRecieved, + CreateAuthCodeResponded, CreateCommentResponded, CreateCommentUpdateBody, + CreateCommentUpdateError, CreateCommentUpdateParentId, CreatePost, + CreatePostResponded, CreatePostUpdateBody, CreatePostUpdateError, + CreatePostUpdateHref, CreatePostUpdateOriginalCreator, CreatePostUpdateTags, + CreatePostUpdateTitle, CreatePostUpdateUseBody, ForgotPassword, + ForgotPasswordResponded, GetPostsResponse, GetTagsResponse, InviterRecieved, + LikeCommentResponded, LikePostResponded, Login, LoginResponded, + LoginUpdateEmailUsername, LoginUpdateError, LoginUpdatePassword, + LogoutResponded, Model, NotFound, OnRouteChange, PostsRecieved, + RequestChangePassword, RequestCreateAuthCode, RequestCreateComment, + RequestCreatePost, RequestForgotPassword, RequestLikeComment, RequestLikePost, + RequestLogin, RequestLogout, RequestSignUp, ShowPost, ShowPostRecieved, + SignUpResponded, SignUpUpdateEmail, SignUpUpdateError, SignUpUpdatePassword, + SignUpUpdateUsername, Signup, TagsRecieved, UserPage, UsernameResponse, + message_error_decoder, +} +import decode +import env +import gleam/dynamic +import gleam/int +import gleam/json +import gleam/list +import gleam/option.{None, Some} +import gleam/uri.{type Uri} +import lustre +import lustre/attribute.{class, href, id, src} +import lustre/effect.{type Effect} +import lustre/element.{type Element, text} +import lustre/element/html.{a, body, footer, img, nav, p, span} +import lustre_http +import modem +import shared.{type Post, type PostComment, type Tag, Post, PostComment, Tag} + +pub fn main() { + lustre.application(init, update, view) + |> lustre.start("#app", Nil) +} + +fn init(_) -> #(Model, Effect(Msg)) { + #( + Model( + route: get_route(), + inviter: "", + auth_user: None, + sign_up_username: "", + sign_up_email: "", + sign_up_password: "", + sign_up_error: None, + login_email_username: "", + login_password: "", + login_error: None, + create_post_title: "", + create_post_href: "", + create_post_body: "", + create_post_original_creator: False, + create_post_tags: [], + create_post_use_body: False, + create_post_error: None, + posts: [], + show_post: None, + create_comment_body: "", + create_comment_parent_id: None, + create_comment_error: None, + tags: [], + invite_link: None, + forgot_password_response: None, + change_password_target: "", + ), + effect.batch( + [modem.init(on_url_change), get_auth_user(), get_posts()] + |> list.append(case get_route() { + ShowPost(_) -> [get_show_post()] + Signup(_) -> [get_inviter(get_auth_code())] + ChangePassword(token) -> [get_change_password_target(token)] + _ -> [] + }), + ), + ) +} + +fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) { + case msg { + OnRouteChange(route) -> #( + Model( + ..model, + route: route, + show_post: case route { + ShowPost(_) -> None + _ -> model.show_post + }, + ), + case route { + ShowPost(_) -> get_show_post() + CreatePost -> + case model.tags { + [] -> get_tags() + _ -> effect.none() + } + _ -> effect.none() + }, + ) + InviterRecieved(auth_code_result) -> + case auth_code_result { + Ok(res) -> #(Model(..model, inviter: res.username), effect.none()) + Error(_) -> #(model, effect.none()) + } + AuthUserRecieved(auth_user_result) -> + case auth_user_result { + Ok(auth_user) -> #(Model(..model, auth_user: Some(auth_user)), case + get_route() + { + CreatePost -> + case uri.parse("/create-post") { + Ok(uri) -> + effect.from(fn(dispatch) { + on_url_change(uri) + |> dispatch + }) + Error(_) -> effect.none() + } + _ -> effect.none() + }) + Error(_) -> #(model, effect.none()) + } + PostsRecieved(get_posts_result) -> + case get_posts_result { + Ok(get_posts) -> #( + Model(..model, posts: get_posts.posts), + effect.none(), + ) + Error(_) -> #(model, effect.none()) + } + ShowPostRecieved(get_post_result) -> + case get_post_result { + Ok(get_post) -> #( + Model(..model, show_post: Some(get_post)), + effect.none(), + ) + Error(_) -> #(model, effect.none()) + } + TagsRecieved(get_tags_result) -> + case get_tags_result { + Ok(get_tags) -> #(Model(..model, tags: get_tags.tags), effect.none()) + Error(_) -> #(model, effect.none()) + } + + SignUpUpdateUsername(value) -> #( + Model(..model, sign_up_username: value), + effect.none(), + ) + SignUpUpdateEmail(value) -> #( + Model(..model, sign_up_email: value), + effect.none(), + ) + SignUpUpdatePassword(value) -> #( + Model(..model, sign_up_password: value), + effect.none(), + ) + SignUpUpdateError(value) -> #( + Model(..model, sign_up_error: value), + effect.none(), + ) + RequestSignUp -> #(model, signup(model)) + SignUpResponded(resp_result) -> + case resp_result { + Ok(resp) -> + case resp.error { + Some(err) -> #( + model, + effect.from(fn(dispatch) { + dispatch(SignUpUpdateError(Some(err))) + }), + ) + None -> #( + Model( + ..model, + sign_up_username: "", + sign_up_email: "", + sign_up_password: "", + sign_up_error: None, + ), + effect.batch([ + modem.push("/", None, None), + get_auth_user(), + get_show_post(), + get_posts(), + ]), + ) + } + Error(_) -> #( + model, + effect.from(fn(dispatch) { + dispatch(SignUpUpdateError(Some("HTTP Error"))) + }), + ) + } + + LoginUpdateEmailUsername(value) -> #( + Model(..model, login_email_username: value), + effect.none(), + ) + LoginUpdatePassword(value) -> #( + Model(..model, login_password: value), + effect.none(), + ) + LoginUpdateError(value) -> #( + Model(..model, login_error: value), + effect.none(), + ) + RequestLogin -> #(model, login(model)) + LoginResponded(resp_result) -> + case resp_result { + Ok(resp) -> + case resp.error { + Some(err) -> #( + model, + effect.from(fn(dispatch) { dispatch(LoginUpdateError(Some(err))) }), + ) + None -> #( + Model( + ..model, + login_email_username: "", + login_password: "", + login_error: None, + ), + effect.batch([ + modem.push("/", None, None), + get_auth_user(), + get_posts(), + get_show_post(), + ]), + ) + } + Error(_) -> #( + model, + effect.from(fn(dispatch) { + dispatch(LoginUpdateError(Some("HTTP Error"))) + }), + ) + } + + RequestLogout -> #(model, logout(model)) + LogoutResponded(_) -> #( + Model(..model, auth_user: None), + effect.batch([ + modem.push("/", None, None), + get_posts(), + get_show_post(), + get_auth_user(), + ]), + ) + + CreatePostUpdateTitle(value) -> #( + Model(..model, create_post_title: value), + effect.none(), + ) + CreatePostUpdateHref(value) -> #( + Model(..model, create_post_href: value), + effect.none(), + ) + CreatePostUpdateBody(value) -> #( + Model(..model, create_post_body: value), + effect.none(), + ) + CreatePostUpdateOriginalCreator(value) -> #( + Model(..model, create_post_original_creator: value), + effect.none(), + ) + CreatePostUpdateTags(tag_id) -> #( + Model( + ..model, + create_post_tags: case list.contains(model.create_post_tags, tag_id) { + True -> list.filter(model.create_post_tags, fn(tag) { tag != tag_id }) + False -> [tag_id, ..model.create_post_tags] + }, + ), + effect.none(), + ) + CreatePostUpdateUseBody(value) -> #( + Model(..model, create_post_use_body: value, create_post_href: ""), + effect.none(), + ) + CreatePostUpdateError(value) -> #( + Model(..model, create_post_error: value), + effect.none(), + ) + RequestCreatePost -> #(model, create_post(model)) + CreatePostResponded(resp_result) -> + case resp_result { + Ok(resp) -> + case resp.error { + Some(err) -> #( + model, + effect.from(fn(dispatch) { + dispatch(CreatePostUpdateError(Some(err))) + }), + ) + None -> #( + Model( + ..model, + create_post_title: "", + create_post_href: "", + create_post_tags: [], + create_post_error: None, + ), + effect.batch([modem.push("/", None, None), get_posts()]), + ) + } + Error(_) -> #( + model, + effect.from(fn(dispatch) { + dispatch(CreatePostUpdateError(Some("HTTP Error"))) + }), + ) + } + + RequestLikePost(post_id) -> #(model, like_post(post_id)) + LikePostResponded(_) -> #(model, case model.show_post { + Some(_) -> get_show_post() + None -> get_posts() + }) + + CreateCommentUpdateBody(body) -> #( + Model(..model, create_comment_body: body), + effect.none(), + ) + CreateCommentUpdateParentId(value) -> #( + Model(..model, create_comment_parent_id: value), + effect.none(), + ) + CreateCommentUpdateError(value) -> #( + Model(..model, create_comment_error: value), + effect.none(), + ) + RequestCreateComment -> #(model, create_comment(model)) + CreateCommentResponded(resp_result) -> + case resp_result { + Ok(resp) -> + case resp.error { + Some(err) -> #( + model, + effect.from(fn(dispatch) { + dispatch(CreateCommentUpdateError(Some(err))) + }), + ) + None -> #( + Model( + ..model, + create_comment_parent_id: None, + create_comment_body: "", + create_comment_error: None, + ), + get_show_post(), + ) + } + Error(_) -> #( + model, + effect.from(fn(dispatch) { + dispatch(CreatePostUpdateError(Some("HTTP Error"))) + }), + ) + } + + RequestLikeComment(post_comment_id) -> #( + model, + like_comment(post_comment_id), + ) + LikeCommentResponded(_) -> #(model, get_show_post()) + + RequestCreateAuthCode -> #(model, create_auth_code()) + CreateAuthCodeResponded(resp_result) -> + case resp_result { + Ok(resp) -> + case resp.message { + Some(code) -> { + set_clipboard(env.get_api_url() <> "/auth/signup/" <> code) + #( + Model( + ..model, + invite_link: Some( + env.get_api_url() <> "/auth/signup/" <> code, + ), + ), + effect.none(), + ) + } + None -> #(model, effect.none()) + } + Error(_) -> #(model, effect.none()) + } + + RequestForgotPassword -> #(model, request_forgot_password(model)) + ForgotPasswordResponded(resp_result) -> + case resp_result { + Ok(resp) -> #( + Model( + ..model, + login_email_username: "", + forgot_password_response: case resp.message { + Some(message) -> Some(Ok(message)) + None -> + case resp.error { + Some(error) -> Some(Error(error)) + None -> None + } + }, + ), + effect.none(), + ) + Error(_) -> #( + Model(..model, forgot_password_response: Some(Error("HTTP Error"))), + effect.none(), + ) + } + ChangePasswordTargetRecieved(target_response) -> + case target_response { + Ok(res) -> #( + Model(..model, change_password_target: res.username), + effect.none(), + ) + Error(_) -> #(model, effect.none()) + } + + RequestChangePassword -> #(model, send_password_change(model)) + ChangePasswordResponded(resp_result) -> + case resp_result { + Ok(_) -> #( + Model(..model, login_password: ""), + modem.push("/auth/login", None, None), + ) + Error(_) -> #( + Model(..model, forgot_password_response: Some(Error("HTTP Error"))), + effect.none(), + ) + } + } +} + +fn on_url_change(uri: Uri) -> Msg { + set_url(uri.path) + OnRouteChange(get_route()) +} + +@external(javascript, "./ffi.mjs", "set_clipboard") +fn set_clipboard(text: String) -> String + +@external(javascript, "./ffi.mjs", "get_route") +fn do_get_route() -> String + +@external(javascript, "./ffi.mjs", "set_url") +fn set_url(url: String) -> String + +fn get_route() -> Route { + let uri = case do_get_route() |> uri.parse { + Ok(uri) -> uri + _ -> panic as "Invalid uri" + } + + case uri.path |> uri.path_segments { + [] -> Active + ["auth", "login"] -> Login + ["auth", "signup", auth_code] -> Signup(auth_code: auth_code) + ["auth", "forgot-password"] -> ForgotPassword + ["auth", "forgot-password", token] -> ChangePassword(token) + ["create-post"] -> CreatePost + ["user", username] -> UserPage(username) + ["post", post_id] -> + case int.parse(post_id) { + Ok(id) -> ShowPost(id) + Error(_) -> NotFound + } + _ -> NotFound + } +} + +fn request_forgot_password(model: Model) { + lustre_http.post( + env.get_api_url() <> "/api/auth/forgot-password", + json.object([#("email", json.string(model.login_email_username))]), + lustre_http.expect_json(message_error_decoder(), ForgotPasswordResponded), + ) +} + +fn create_auth_code() { + lustre_http.post( + env.get_api_url() <> "/api/auth-code", + json.object([]), + lustre_http.expect_json(message_error_decoder(), CreateAuthCodeResponded), + ) +} + +fn get_auth_code() -> String { + let uri = case do_get_route() |> uri.parse { + Ok(uri) -> uri + _ -> panic as "Invalid uri" + } + + case uri.path |> uri.path_segments { + ["auth", "signup", auth_code] -> auth_code + _ -> "1" + } +} + +fn get_forgot_password_token() -> String { + let uri = case do_get_route() |> uri.parse { + Ok(uri) -> uri + _ -> panic as "Invalid uri" + } + + case uri.path |> uri.path_segments { + ["auth", "forgot-password", token] -> token + _ -> "1" + } +} + +fn get_post_id() -> String { + let uri = case do_get_route() |> uri.parse { + Ok(uri) -> uri + _ -> panic as "Invalid uri" + } + + case uri.path |> uri.path_segments { + ["post", post_id] -> post_id + _ -> "" + } +} + +pub fn get_inviter(auth_code: String) -> Effect(Msg) { + let url = env.get_api_url() <> "/api/auth-code/" <> auth_code + let decoder = + dynamic.decode1(UsernameResponse, dynamic.field("username", dynamic.string)) + + lustre_http.get(url, lustre_http.expect_json(decoder, InviterRecieved)) +} + +pub fn get_change_password_target(token: String) -> Effect(Msg) { + let url = env.get_api_url() <> "/api/auth/forgot-password/" <> token + let decoder = + dynamic.decode1(UsernameResponse, dynamic.field("username", dynamic.string)) + + lustre_http.get( + url, + lustre_http.expect_json(decoder, ChangePasswordTargetRecieved), + ) +} + +pub fn get_auth_user() -> Effect(Msg) { + let url = env.get_api_url() <> "/api/auth/validate" + + let decoder = + dynamic.decode3( + AuthUser, + dynamic.field("user_id", dynamic.int), + dynamic.field("username", dynamic.string), + dynamic.field("is_admin", dynamic.bool), + ) + + lustre_http.get(url, lustre_http.expect_json(decoder, AuthUserRecieved)) +} + +pub fn get_show_post() -> Effect(Msg) { + let url = env.get_api_url() <> "/api/posts/" <> get_post_id() + + lustre_http.get( + url, + lustre_http.expect_json( + fn(data) { decode.from(post_decoder(), data) }, + ShowPostRecieved, + ), + ) +} + +pub fn post_decoder() { + decode.into({ + use id <- decode.parameter + use title <- decode.parameter + use href <- decode.parameter + use body <- decode.parameter + use likes <- decode.parameter + use user_like_post <- decode.parameter + use comments_count <- decode.parameter + use comments <- decode.parameter + use tags <- decode.parameter + use username <- decode.parameter + use original_creator <- decode.parameter + use created_at <- decode.parameter + + Post( + id, + title, + href, + body, + likes, + user_like_post, + comments_count, + comments, + tags, + username, + original_creator, + created_at, + ) + }) + |> decode.field("id", decode.int) + |> decode.field("title", decode.string) + |> decode.field("href", decode.optional(decode.string)) + |> decode.field("body", decode.optional(decode.string)) + |> decode.field("likes", decode.int) + |> decode.field("user_like_post", decode.bool) + |> decode.field("comments_count", decode.int) + |> decode.field("comments", decode.list(comment_decoder())) + |> decode.field("tags", decode.list(decode.string)) + |> decode.field("username", decode.string) + |> decode.field("original_creator", decode.bool) + |> decode.field("created_at", decode.int) +} + +fn comment_decoder() { + decode.into({ + use id <- decode.parameter + use body <- decode.parameter + use username <- decode.parameter + use likes <- decode.parameter + use user_like_post_comment <- decode.parameter + use parent_id <- decode.parameter + use created_at <- decode.parameter + + PostComment( + id, + body, + username, + likes, + user_like_post_comment, + parent_id, + created_at, + ) + }) + |> decode.field("id", decode.int) + |> decode.field("body", decode.string) + |> decode.field("username", decode.string) + |> decode.field("likes", decode.int) + |> decode.field("user_like_post_comment", decode.bool) + |> decode.field("parent_id", decode.optional(decode.int)) + |> decode.field("created_at", decode.int) +} + +pub fn get_posts() -> Effect(Msg) { + let url = env.get_api_url() <> "/api/posts" + + let response_decoder = + decode.into({ + use posts <- decode.parameter + + GetPostsResponse(posts) + }) + |> decode.field("posts", decode.list(post_decoder())) + + lustre_http.get( + url, + lustre_http.expect_json( + fn(data) { response_decoder |> decode.from(data) }, + PostsRecieved, + ), + ) +} + +pub fn tag_decoder() { + decode.into({ + use id <- decode.parameter + use name <- decode.parameter + use category <- decode.parameter + use permission <- decode.parameter + + Tag( + id, + name, + category: shared.string_to_tag_category(category), + permission: shared.string_to_tag_permission(permission), + ) + }) + |> decode.field("id", decode.int) + |> decode.field("name", decode.string) + |> decode.field("category", decode.string) + |> decode.field("permission", decode.string) +} + +pub fn get_tags() -> Effect(Msg) { + let url = env.get_api_url() <> "/api/tags" + + let response_decoder = + decode.into({ + use tags <- decode.parameter + + GetTagsResponse(tags) + }) + |> decode.field("tags", decode.list(tag_decoder())) + + lustre_http.get( + url, + lustre_http.expect_json( + fn(data) { response_decoder |> decode.from(data) }, + TagsRecieved, + ), + ) +} + +fn signup(model: Model) { + lustre_http.post( + env.get_api_url() <> "/api/users", + json.object([ + #("username", json.string(model.sign_up_username)), + #("email", json.string(model.sign_up_email)), + #("password", json.string(model.sign_up_password)), + #("auth_code", json.string(get_auth_code())), + ]), + lustre_http.expect_json(message_error_decoder(), SignUpResponded), + ) +} + +fn logout(model _: Model) { + lustre_http.post( + env.get_api_url() <> "/api/auth/logout", + json.object([]), + lustre_http.expect_json(message_error_decoder(), LogoutResponded), + ) +} + +fn create_post(model: Model) { + lustre_http.post( + env.get_api_url() <> "/api/posts", + json.object([ + #("title", json.string(model.create_post_title)), + #("original_creator", json.bool(model.create_post_original_creator)), + case !model.create_post_use_body { + True -> #("href", json.string(model.create_post_href)) + False -> #("body", json.string(model.create_post_body)) + }, + #("tags", json.array(model.create_post_tags, fn(tag) { json.int(tag) })), + ]), + lustre_http.expect_json(message_error_decoder(), CreatePostResponded), + ) +} + +fn send_password_change(model: Model) { + lustre_http.post( + env.get_api_url() + <> "/api/auth/change-password/" + <> get_forgot_password_token(), + json.object([#("password", json.string(model.login_password))]), + lustre_http.expect_json(message_error_decoder(), ChangePasswordResponded), + ) +} + +fn create_comment(model: Model) { + let post_id = case model.show_post { + Some(post) -> post.id |> int.to_string + None -> panic as "Invalid state" + } + + lustre_http.post( + env.get_api_url() <> "/api/posts/" <> post_id <> "/comments", + json.object([ + #("body", json.string(model.create_comment_body)), + #("parent_id", case model.create_comment_parent_id { + Some(parent_id) -> json.int(parent_id) + None -> json.null() + }), + ]), + lustre_http.expect_json(message_error_decoder(), CreateCommentResponded), + ) +} + +pub fn view(model: Model) -> Element(Msg) { + body( + [ + class( + "bg-[#fefefc] text-[#151515] w-[100vw] min-h-[100vh] h-[100vh] px-4 max-w-[800px] py-4 mx-auto flex flex-col h-screen gap-4", + ), + id("app"), + ], + [ + nav( + [ + class( + "text-sm font-bold text-neutral-700 h-[28px] flex justify-between items-center", + ), + ], + [ + a([href("/"), class("flex group gap-2 items-center")], [ + img([ + src("https://gleam.run/images/lucy/lucy.svg"), + attribute.alt("Lucy"), + class("size-[18px] group-hover:animate-wiggle"), + ]), + p([class("group-hover:underline")], [text("Latest")]), + ]), + case model.auth_user { + None -> + a([href("/auth/login"), class("hover:underline")], [text("Login")]) + Some(auth_user) -> + span([class("flex gap-2 items-center")], [ + a( + [ + class("font-normal"), + button_class(), + class("hover:bg-[#584355]/80"), + href("/create-post"), + ], + [text("Post")], + ), + a( + [ + class("hover:underline"), + href("/user/" <> auth_user.username), + ], + [text(auth_user.username)], + ), + ]) + }, + ], + ), + html.main([class("mb-auto")], [ + case model.route, model.auth_user { + Active, _ -> latest_view(model) + Login, _ -> login_view(model) + Signup(auth_code), _ -> signup_view(model, auth_code) + ForgotPassword, _ -> forgot_password(model) + ChangePassword(_), _ -> change_password(model) + CreatePost, Some(_) -> create_post_view(model) + ShowPost(_), _ -> show_post_view(model) + UserPage(_), Some(_) -> user_view(model) + NotFound, _ -> text("404 Not found") + _, _ -> text("404 Not found") + }, + ]), + footer([class("text-center text-neutral-500 text-xs pb-2")], [ + text("Made with <3 by the community, contribute on "), + a( + [ + href("https://github.com/dinkelspiel/kirakira"), + class("hover:underline text-neutral-700 font-bold"), + ], + [text("GitHub")], + ), + ]), + ], + ) +} diff --git a/apps/my-app/client/src/client/state.gleam b/apps/my-app/client/src/client/state.gleam @@ -0,0 +1,51 @@ +import gleam/dynamic +import gleam/option.{type Option} +import lustre_http +import shared.{type Post} + +pub type Route { + Active + Login + Signup(auth_code: String) + ForgotPassword + ChangePassword(token: String) + CreatePost + UserPage(username: String) + ShowPost(post_id: Int) + NotFound +} + +pub type Model { + Model( + route: Route, + inviter: String, + auth_user: Option(AuthUser), + sign_up_username: String, + sign_up_email: String, + sign_up_password: String, + sign_up_error: Option(String), + login_email_username: String, + login_password: String, + login_error: Option(String), + create_post_title: String, + create_post_href: String, + create_post_body: String, + create_post_original_creator: Bool, + create_post_use_body: Bool, + create_post_tags: List(Int), + create_post_error: Option(String), + posts: List(Post), + show_post: Option(Post), + create_comment_body: String, + create_comment_error: Option(String), + create_comment_parent_id: Option(Int), + tags: List(Tag), + invite_link: Option(String), + forgot_password_response: Option(Result(String, String)), + change_password_target: String, + ) +} + +pub type Msg { + LoadPosts +} diff --git a/apps/my-app/client/test/client_test.gleam b/apps/my-app/client/test/client_test.gleam @@ -0,0 +1,12 @@ +import gleeunit +import gleeunit/should + +pub fn main() { + gleeunit.main() +} + +// gleeunit test functions end in `_test` +pub fn hello_world_test() { + 1 + |> should.equal(1) +} diff --git a/apps/my-app/module.nix b/apps/my-app/module.nix @@ -0,0 +1,141 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + pkg = import ./.; + migrations = pkg.migrations; + server = pkg.server; + cfg = config.services.myapp; +in { + options = { + services.myapp = { + enable = mkEnableOption "My App"; + + environment = mkOption { + type = types.enum [ "dev" "prod" ]; + default = "dev"; + example = "prod"; + description = "The environment (dev / prod) to deploy as."; + }; + + user = mkOption { + type = types.str; + default = "myapp"; + description = "User account under which my app runs."; + }; + + port = mkOption { + type = types.nullOr types.int; + default = 3003; + example = 8080; + description = '' + The port to bind my app server to. + ''; + }; + }; + }; + + config = mkIf cfg.enable { + networking.firewall.allowedTCPPorts = [ cfg.port ]; + users.users.${cfg.user} = { + name = cfg.user; + description = "My app service user"; + isSystemUser = true; + createHome = false; + packages = with pkgs; [ + gleam + erlang_27 + rebar3 + ]; + }; + + systemd.services = { + # postgres-migration = { + # description = "Postgres migrations"; + # wantedBy = [ "multi-user.target" ]; + # after = [ "postgresql.service" ]; + # requires = [ "postgresql.service" ]; + # }; + + myapp = { + wantedBy = [ "multi-user.target" ]; + description = "Start my app server."; + after = [ "network.target" ]; + # requires = [ "db-migration.service" ]; + + # environment = { + # DB_URI = ""; + # }; + + serviceConfig = { + Type = "simple"; + User = cfg.user; + ExecStart = "/root/apps/myapp/test_pgo_app"; + Restart = "always"; + KillMode = "process"; + }; + }; + }; + + services = mkIf cfg.environment == "dev" { + postgresql = { + enable = true; + enableTCPIP = true; + ensureDatabases = [ cfg.user ]; + ensureUsers = [ + { + name = cfg.user; + ensurePermissions = { + "DATABASE ${cfg.user}" = "ALL PRIVILEGES"; + }; + } + ]; + authentication = pkgs.lib.mkOverride 10 '' + local sameuser all peer + host sameuser all ::1/32 trust + ''; + }; + }; + + # systemd.services = { + # + # # db-migration = { + # # description = "DB migrations script"; + # # wantedBy = [ "multi-user.target" ]; + # # after = [ "postgresql.service" ]; + # # requires = [ "postgresql.service" ]; + # # + # # environment = { + # # DATABASE_URL = "postgres://${cfg.user}@localhost:5432/myapp?sslmode=disable"; + # # }; + # # + # # serviceConfig = { + # # User = cfg.user; + # # Type = "oneshot"; + # # ExecStart = "${pkgs.dbmate}/bin/dbmate -d ${migrations} --no-dump-schema up"; + # # }; + # # }; + # + # myapp = { + # wantedBy = [ "multi-user.target" ]; + # description = "Start my app server."; + # after = [ "network.target" ]; + # # requires = [ "db-migration.service" ]; + # + # # environment = { + # # APP_PORT = toString cfg.port; + # # DATABASE_URL = "postgres:///${cfg.user}"; + # # }; + # + # serviceConfig = { + # Type = "simple"; + # User = cfg.user; + # ExecStart = ''${server}/bin/server''; + # Restart = "always"; + # KillMode = "process"; + # }; + # }; + # }; + }; +} diff --git a/apps/my-app/server/.github/workflows/test.yml b/apps/my-app/server/.github/workflows/test.yml @@ -0,0 +1,23 @@ +name: test + +on: + push: + branches: + - master + - main + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: erlef/setup-beam@v1 + with: + otp-version: "26.0.2" + gleam-version: "1.1.0" + rebar3-version: "3" + # elixir-version: "1.15.4" + - run: gleam deps download + - run: gleam test + - run: gleam format --check src test diff --git a/apps/my-app/server/.gitignore b/apps/my-app/server/.gitignore @@ -0,0 +1,7 @@ +*.beam +*.ez +/build +erl_crash.dump + +node_modules/ + diff --git a/apps/my-app/server/README.md b/apps/my-app/server/README.md @@ -0,0 +1,39 @@ +# test_pgo + +[![Package Version](https://img.shields.io/hexpm/v/test_pgo)](https://hex.pm/packages/test_pgo) +[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/test_pgo/) + +```sh +gleam add test_pgo@1 +``` +```gleam +import test_pgo + +pub fn main() { + // TODO: An example of the project in use +} +``` + +Further documentation can be found at <https://hexdocs.pm/test_pgo>. + +## Development + +```sh +gleam run # Run the project +gleam test # Run the tests +``` + +## Migrations + +Set environment variable `PGPASSWORD` +``` +LATEST_VERSION = $(psql -U ${PGUSER} -h ${PGHOST} -p ${PGPORT} -d ${PGDATABASE} --set=sslmode=verify-full -AXqtc 'SELECT MAX(version) FROM migrations;') +LATEST_VERSION = ${LATEST_VERSION:-0} +MAX_VERSION = $( ls ./migrations/ | sort -n | tail -1 | xargs -I _ basename _ .sql | xargs -I _ echo $(( _ )) ) +for VERSION in $(seq -f "%04g" $LATEST_VERSION $MAX_VERSION) +do + psql -U ${PGUSER} -h ${PGHOST} -p ${PGPORT} -d ${PGDATABASE} --set=sslmode=verify-full -a -f migrations/$VERSION.sql + psql -U ${PGUSER} -h ${PGHOST} -p ${PGPORT} -d ${PGDATABASE} --set=sslmode=verify-full -a -c 'INSERT INTO migrations (version) VALUES ('$(( $VERSION ))');' +done +``` + diff --git a/apps/my-app/server/gleam.toml b/apps/my-app/server/gleam.toml @@ -0,0 +1,48 @@ +name = "server" +version = "1.0.0" +target = "erlang" + +# Fill out these fields if you intend to generate HTML documentation or publish +# your project to the Hex package manager. +# +# description = "" +# licences = ["Apache-2.0"] +# repository = { type = "github", user = "username", repo = "project" } +# links = [{ title = "Website", href = "https://gleam.run" }] +# +# For a full reference of all the available options, you can have a look at +# https://gleam.run/writing-gleam/gleam-toml/. + +[dependencies] +mist = ">= 2.0.0 and < 3.0.0" +lustre = ">= 4.3.6 and < 5.0.0" +gleam_pgo = ">= 0.14.0 and < 1.0.0" +wisp = ">= 1.1.0 and < 2.0.0" +gleam_stdlib = ">= 0.40.0 and < 1.0.0" +gleam_erlang = ">= 0.25.0 and < 1.0.0" +glenv = ">= 0.4.0 and < 1.0.0" +birl = ">= 1.7.1 and < 2.0.0" +gleam_json = ">= 1.0.1 and < 2.0.0" +gleam_http = ">= 3.6.0 and < 4.0.0" +decode = ">= 0.2.0 and < 1.0.0" +shared = { path = "../shared" } +# gleam_json = ">= 1.0.1 and < 2.0.0" +# cors_builder = ">= 1.0.0 and < 2.0.0" +# decode = ">= 0.2.0 and < 1.0.0" +# gleam_http = ">= 3.6.0 and < 4.0.0" +# # client = { path = "../client" } +# lustre = ">= 4.3.1 and < 5.0.0" +# birl = ">= 1.7.1 and < 2.0.0" +# gleam_pgo = ">= 0.14.0 and < 1.0.0" +# glisten = ">= 3.0.0 and < 4.0.0" + +[dev-dependencies] +gleeunit = ">= 1.2.0 and < 2.0.0" +gleescript = ">= 1.4.0 and < 2.0.0" +# lustre_dev_tools = ">= 1.5.3 and < 2.0.0" + +[erlang] +# Starting an SSL connection relies on ssl application to be started. +# Add pgo application to make sure it's started before querying postgres. +extra_applications = ["ssl"] + diff --git a/apps/my-app/server/manifest.toml b/apps/my-app/server/manifest.toml @@ -0,0 +1,59 @@ +# This file was generated by Gleam +# You typically do not need to edit this file + +packages = [ + { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" }, + { name = "backoff", version = "1.1.6", build_tools = ["rebar3"], requirements = [], otp_app = "backoff", source = "hex", outer_checksum = "CF0CFFF8995FB20562F822E5CC47D8CCF664C5ECDC26A684CBE85C225F9D7C39" }, + { name = "birl", version = "1.7.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "5C66647D62BCB11FE327E7A6024907C4A17954EF22865FE0940B54A852446D01" }, + { name = "decode", version = "0.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "decode", source = "hex", outer_checksum = "965F517F67B8C172CA27A5C8E34C73733139E8C9E64736181B8C3179281F9793" }, + { name = "directories", version = "1.1.0", build_tools = ["gleam"], requirements = ["envoy", "gleam_stdlib", "platform", "simplifile"], otp_app = "directories", source = "hex", outer_checksum = "BDA521A4EB9EE3A7894F0DC863797878E91FF5C7826F7084B2E731E208BDB076" }, + { name = "envoy", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "CFAACCCFC47654F7E8B75E614746ED924C65BD08B1DE21101548AC314A8B6A41" }, + { name = "exception", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "F5580D584F16A20B7FCDCABF9E9BE9A2C1F6AC4F9176FA6DD0B63E3B20D450AA" }, + { name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" }, + { name = "gleam_crypto", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "ADD058DEDE8F0341F1ADE3AAC492A224F15700829D9A3A3F9ADF370F875C51B7" }, + { name = "gleam_erlang", version = "0.25.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "054D571A7092D2A9727B3E5D183B7507DAB0DA41556EC9133606F09C15497373" }, + { name = "gleam_http", version = "3.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "8C07DF9DF8CC7F054C650839A51C30A7D3C26482AC241C899C1CEA86B22DBE51" }, + { name = "gleam_json", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "9063D14D25406326C0255BDA0021541E797D8A7A12573D849462CAFED459F6EB" }, + { name = "gleam_otp", version = "0.11.2", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "517FFB679E44AD71D059F3EF6A17BA6EFC8CB94FA174D52E22FB6768CF684D78" }, + { name = "gleam_pgo", version = "0.14.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "pgo"], otp_app = "gleam_pgo", source = "hex", outer_checksum = "12256A7F351E994A6E43AB67276DD8AACE752991C013F48A288C4A848F3A9758" }, + { name = "gleam_stdlib", version = "0.40.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "86606B75A600BBD05E539EB59FABC6E307EEEA7B1E5865AFB6D980A93BCB2181" }, + { name = "gleescript", version = "1.4.0", build_tools = ["gleam"], requirements = ["argv", "filepath", "gleam_erlang", "gleam_stdlib", "simplifile", "snag", "tom"], otp_app = "gleescript", source = "hex", outer_checksum = "8CDDD29F91064E69950A91A40061785F10275ADB70A0520075591F61A724C455" }, + { name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" }, + { name = "glenv", version = "0.4.0", build_tools = ["gleam"], requirements = ["decode", "envoy", "gleam_stdlib"], otp_app = "glenv", source = "hex", outer_checksum = "46164B9FFEB08927FD2CEBD96C3AFBE082AD5CC2C6F2FC4A78EFDA9EB61E3510" }, + { name = "glisten", version = "5.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib", "logging", "telemetry"], otp_app = "glisten", source = "hex", outer_checksum = "48EF7F6D1DCA877C2F49AF35CC33946C7129EEB05A114758A2CC569C708BFAF8" }, + { name = "gramps", version = "2.0.3", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "3CCAA6E081225180D95C79679D383BBF51C8D1FDC1B84DA1DA444F628C373793" }, + { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" }, + { name = "logging", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "1098FBF10B54B44C2C7FDF0B01C1253CAFACDACABEFB4B0D027803246753E06D" }, + { name = "lustre", version = "4.3.6", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib"], otp_app = "lustre", source = "hex", outer_checksum = "A122A11C761E2366EEA4649CBBE5C162BCA8EF5D78C0B1C7AACBA6E824AE9F5D" }, + { name = "marceau", version = "1.2.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "5188D643C181EE350D8A20A3BDBD63AF7B6C505DE333CFBE05EF642ADD88A59B" }, + { name = "mist", version = "2.0.0", build_tools = ["gleam"], requirements = ["birl", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "981F12FC8BA0656B40099EC876D6F2BEE7B95593610F342E9AB0DC4E663A932F" }, + { name = "opentelemetry_api", version = "1.3.0", build_tools = ["rebar3", "mix"], requirements = ["opentelemetry_semantic_conventions"], otp_app = "opentelemetry_api", source = "hex", outer_checksum = "B9E5FF775FD064FA098DBA3C398490B77649A352B40B0B730A6B7DC0BDD68858" }, + { name = "opentelemetry_semantic_conventions", version = "0.2.0", build_tools = ["rebar3", "mix"], requirements = [], otp_app = "opentelemetry_semantic_conventions", source = "hex", outer_checksum = "D61FA1F5639EE8668D74B527E6806E0503EFC55A42DB7B5F39939D84C07D6895" }, + { name = "pg_types", version = "0.4.0", build_tools = ["rebar3"], requirements = [], otp_app = "pg_types", source = "hex", outer_checksum = "B02EFA785CAECECF9702C681C80A9CA12A39F9161A846CE17B01FB20AEEED7EB" }, + { name = "pgo", version = "0.14.0", build_tools = ["rebar3"], requirements = ["backoff", "opentelemetry_api", "pg_types"], otp_app = "pgo", source = "hex", outer_checksum = "71016C22599936E042DC0012EE4589D24C71427D266292F775EBF201D97DF9C9" }, + { name = "platform", version = "1.0.0", build_tools = ["gleam"], requirements = [], otp_app = "platform", source = "hex", outer_checksum = "8339420A95AD89AAC0F82F4C3DB8DD401041742D6C3F46132A8739F6AEB75391" }, + { name = "ranger", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "1566C272B1D141B3BBA38B25CB761EF56E312E79EC0E2DFD4D3C19FB0CC1F98C" }, + { name = "shared", version = "1.0.0", build_tools = ["gleam"], requirements = ["birl", "gleam_stdlib"], source = "local", path = "../shared" }, + { name = "simplifile", version = "2.1.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "BDD04F5D31D6D34E2EDFAEF0B68A6297AEC939888C3BFCE61133DE13857F6DA2" }, + { name = "snag", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "54D32E16E33655346AA3E66CBA7E191DE0A8793D2C05284E3EFB90AD2CE92BCC" }, + { name = "telemetry", version = "1.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "telemetry", source = "hex", outer_checksum = "7015FC8919DBE63764F4B4B87A95B7C0996BD539E0D499BE6EC9D7F3875B79E6" }, + { name = "thoas", version = "1.2.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "E38697EDFFD6E91BD12CEA41B155115282630075C2A727E7A6B2947F5408B86A" }, + { name = "tom", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "tom", source = "hex", outer_checksum = "9EECB60150E834A07238BD5C7DF1FF07F7D4C5862BB8A773923D1981C7875FB0" }, + { name = "wisp", version = "1.1.0", build_tools = ["gleam"], requirements = ["directories", "exception", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_json", "gleam_stdlib", "logging", "marceau", "mist", "simplifile"], otp_app = "wisp", source = "hex", outer_checksum = "5B25E37E08A94039668D2ACF7B2D2A665BEAF4B48EFA8613010B4E3164137664" }, +] + +[requirements] +birl = { version = ">= 1.7.1 and < 2.0.0" } +decode = { version = ">= 0.2.0 and < 1.0.0" } +gleam_erlang = { version = ">= 0.25.0 and < 1.0.0" } +gleam_http = { version = ">= 3.6.0 and < 4.0.0" } +gleam_json = { version = ">= 1.0.1 and < 2.0.0" } +gleam_pgo = { version = ">= 0.14.0 and < 1.0.0" } +gleam_stdlib = { version = ">= 0.40.0 and < 1.0.0" } +gleescript = { version = ">= 1.4.0 and < 2.0.0" } +gleeunit = { version = ">= 1.2.0 and < 2.0.0" } +glenv = { version = ">= 0.4.0 and < 1.0.0" } +lustre = { version = ">= 4.3.6 and < 5.0.0" } +mist = { version = ">= 2.0.0 and < 3.0.0" } +shared = { path = "../shared" } +wisp = { version = ">= 1.1.0 and < 2.0.0" } diff --git a/apps/my-app/server/priv/static/client.min.css b/apps/my-app/server/priv/static/client.min.css @@ -0,0 +1 @@ +/*! tailwindcss v3.4.1 | MIT License | https://tailwindcss.com*/*,:after,:before{border:0 solid #e5e7eb;box-sizing:border-box}:after,:before{--tw-content:""}:host,html{-webkit-text-size-adjust:100%;font-feature-settings:normal;-webkit-tap-highlight-color:transparent;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-variation-settings:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4}body{line-height:inherit;margin:0}hr{border-top-width:1px;color:inherit;height:0}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-feature-settings:normal;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em;font-variation-settings:normal}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}button,input,optgroup,select,textarea{font-feature-settings:inherit;color:inherit;font-family:inherit;font-size:100%;font-variation-settings:inherit;font-weight:inherit;line-height:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{color:#9ca3af;opacity:1}input::placeholder,textarea::placeholder{color:#9ca3af;opacity:1}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}[hidden]{display:none}*,::backdrop,:after,:before{--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:#3b82f680;--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: }.absolute{position:absolute}.relative{position:relative}.-left-\[18px\]{left:-18px}.mx-auto{margin-left:auto;margin-right:auto}.mb-auto{margin-bottom:auto}.me-auto{margin-inline-end:auto}.ms-auto{margin-inline-start:auto}.mt-2{margin-top:.5rem}.flex{display:flex}.grid{display:grid}.hidden{display:none}.size-\[18px\]{height:18px;width:18px}.h-64{height:16rem}.h-\[100vh\]{height:100vh}.h-\[28px\]{height:28px}.h-screen{height:100vh}.min-h-\[100vh\]{min-height:100vh}.w-\[100vw\]{width:100vw}.w-fit{width:-moz-fit-content;width:fit-content}.w-full{width:100%}.max-w-\[25ch\]{max-width:25ch}.max-w-\[450px\]{max-width:450px}.max-w-\[600px\]{max-width:600px}.max-w-\[60ch\]{max-width:60ch}.max-w-\[800px\]{max-width:800px}.-translate-x-1\/2{--tw-translate-x:-50%}.-translate-x-1\/2,.translate-y-2{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-2{--tw-translate-y:0.5rem}.list-disc{list-style-type:disc}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-\[18px\2c 1fr\]{grid-template-columns:18px 1fr}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-4{gap:1rem}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rounded-2xl{border-radius:1rem}.rounded-full{border-radius:9999px}.rounded-md{border-radius:.375rem}.border{border-width:1px}.border-s{border-inline-start-width:1px}.border-\[\#ffaff3\]{--tw-border-opacity:1;border-color:rgb(255 175 243/var(--tw-border-opacity))}.border-s-neutral-200{--tw-border-opacity:1;border-inline-start-color:rgb(229 229 229/var(--tw-border-opacity))}.border-s-neutral-400{--tw-border-opacity:1;border-inline-start-color:rgb(163 163 163/var(--tw-border-opacity))}.bg-\[\#584355\]{--tw-bg-opacity:1;background-color:rgb(88 67 85/var(--tw-bg-opacity))}.bg-\[\#fefefc\]{--tw-bg-opacity:1;background-color:rgb(254 254 252/var(--tw-bg-opacity))}.bg-\[\#ffaff3\]\/50{background-color:#ffaff380}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.px-1{padding-left:.25rem;padding-right:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-4{padding-left:1rem;padding-right:1rem}.py-1{padding-bottom:.25rem;padding-top:.25rem}.py-4{padding-bottom:1rem;padding-top:1rem}.pb-1{padding-bottom:.25rem}.pb-2{padding-bottom:.5rem}.pb-4{padding-bottom:1rem}.ps-2{padding-inline-start:.5rem}.ps-\[18px\]{padding-inline-start:18px}.pt-3{padding-top:.75rem}.text-center{text-align:center}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-normal{font-weight:400}.font-semibold{font-weight:600}.italic{font-style:italic}.leading-tight{line-height:1.25}.text-\[\#151515\]{--tw-text-opacity:1;color:rgb(21 21 21/var(--tw-text-opacity))}.text-\[\#584355\]{--tw-text-opacity:1;color:rgb(88 67 85/var(--tw-text-opacity))}.text-cyan-700{--tw-text-opacity:1;color:rgb(14 116 144/var(--tw-text-opacity))}.text-green-700{--tw-text-opacity:1;color:rgb(21 128 61/var(--tw-text-opacity))}.text-neutral-500{--tw-text-opacity:1;color:rgb(115 115 115/var(--tw-text-opacity))}.text-neutral-700{--tw-text-opacity:1;color:rgb(64 64 64/var(--tw-text-opacity))}.text-red-500{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity))}.text-red-700{--tw-text-opacity:1;color:rgb(185 28 28/var(--tw-text-opacity))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.underline{text-decoration-line:underline}.accent-\[\#ffaff3\]\/50{accent-color:#ffaff380}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.hover\:bg-\[\#584355\]\/80:hover{background-color:#584355cc}.hover\:underline:hover{text-decoration-line:underline}.enabled\:hover\:bg-\[\#584355\]\/80:hover:enabled{background-color:#584355cc}.disabled\:bg-\[\#2f2f2f\]\/30:disabled{background-color:#2f2f2f4d}@keyframes wiggle{0%,to{transform:rotate(-8deg)}50%{transform:rotate(8deg)}}.group:hover .group-hover\:animate-wiggle{animation:wiggle .3s linear infinite}.group:hover .group-hover\:underline{text-decoration-line:underline}@media (min-width:640px){.sm\:block{display:block}.sm\:h-36{height:9rem}.sm\:max-w-\[35ch\]{max-width:35ch}.sm\:grid-cols-\[170px\2c 1fr\]{grid-template-columns:170px 1fr}.sm\:flex-row{flex-direction:row}.sm\:items-center{align-items:center}.sm\:justify-start{justify-content:flex-start}.sm\:gap-2{gap:.5rem}}@media (min-width:768px){.md\:h-24{height:6rem}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}@media (min-width:1024px){.lg\:max-w-\[45ch\]{max-width:45ch}} +\ No newline at end of file diff --git a/apps/my-app/server/priv/static/client.min.mjs b/apps/my-app/server/priv/static/client.min.mjs @@ -0,0 +1,6 @@ +var a=class{withFields(t){let r=Object.keys(this).map(n=>n in t?t[n]:this[n]);return new this.constructor(...r)}},W=class{static fromArray(t,r){let n=r||new Wn;for(let i=t.length-1;i>=0;--i)n=new Jr(t[i],n);return n}[Symbol.iterator](){return new us(this)}toArray(){return[...this]}atLeastLength(t){for(let r of this){if(t<=0)return!0;t--}return t<=0}hasLength(t){for(let r of this){if(t<=0)return!1;t--}return t===0}countLength(){let t=0;for(let r of this)t++;return t}};function $(e,t){return new Jr(e,t)}function s(e,t){return W.fromArray(e,t)}var us=class{#e;constructor(t){this.#e=t}next(){if(this.#e instanceof Wn)return{done:!0};{let{head:t,tail:r}=this.#e;return this.#e=r,{value:t,done:!1}}}},Wn=class extends W{},Jr=class extends W{constructor(t,r){super(),this.head=t,this.tail=r}},Kr=class e{constructor(t){if(!(t instanceof Uint8Array))throw"BitArray can only be constructed from a Uint8Array";this.buffer=t}get length(){return this.buffer.length}byteAt(t){return this.buffer[t]}floatFromSlice(t,r,n){return $a(this.buffer,t,r,n)}intFromSlice(t,r,n,i){return ma(this.buffer,t,r,n,i)}binaryFromSlice(t,r){return new e(this.buffer.slice(t,r))}sliceAfter(t){return new e(this.buffer.slice(t))}},Jn=class{constructor(t){this.value=t}};function ma(e,t,r,n,i){let o=0;if(n)for(let u=t;u<r;u++)o=o*256+e[u];else for(let u=r-1;u>=t;u--)o=o*256+e[u];if(i){let l=2**((r-t)*8-1);o>=l&&(o-=l*2)}return o}function $a(e,t,r,n){let i=new DataView(e.buffer),o=r-t;if(o===8)return i.getFloat64(t,!n);if(o===4)return i.getFloat32(t,!n);{let u=`Sized floats must be 32-bit or 64-bit on JavaScript, got size of ${o*8} bits`;throw new globalThis.Error(u)}}var Bt=class e extends a{static isResult(t){return t instanceof e}},m=class extends Bt{constructor(t){super(),this[0]=t}isOk(){return!0}},h=class extends Bt{constructor(t){super(),this[0]=t}isOk(){return!1}};function T(e,t){let r=[e,t];for(;r.length;){let n=r.pop(),i=r.pop();if(n===i)continue;if(!Ao(n)||!Ao(i)||!Aa(n,i)||wa(n,i)||xa(n,i)||ga(n,i)||ya(n,i)||ba(n,i)||ka(n,i))return!1;let u=Object.getPrototypeOf(n);if(u!==null&&typeof u.equals=="function")try{if(n.equals(i))continue;return!1}catch{}let[l,p]=ha(n);for(let c of l(n))r.push(p(n,c),p(i,c))}return!0}function ha(e){if(e instanceof Map)return[t=>t.keys(),(t,r)=>t.get(r)];{let t=e instanceof globalThis.Error?["message"]:[];return[r=>[...t,...Object.keys(r)],(r,n)=>r[n]]}}function wa(e,t){return e instanceof Date&&(e>t||e<t)}function xa(e,t){return e.buffer instanceof ArrayBuffer&&e.BYTES_PER_ELEMENT&&!(e.byteLength===t.byteLength&&e.every((r,n)=>r===t[n]))}function ga(e,t){return Array.isArray(e)&&e.length!==t.length}function ya(e,t){return e instanceof Map&&e.size!==t.size}function ba(e,t){return e instanceof Set&&(e.size!=t.size||[...e].some(r=>!t.has(r)))}function ka(e,t){return e instanceof RegExp&&(e.source!==t.source||e.flags!==t.flags)}function Ao(e){return typeof e=="object"&&e!==null}function Aa(e,t){return typeof e!="object"&&typeof t!="object"&&(!e||!t)||[Promise,WeakSet,WeakMap,Function].some(n=>e instanceof n)?!1:e.constructor===t.constructor}function yt(e,t){return t===0?0:e%t}function lt(e,t){return Math.trunc(vo(e,t))}function vo(e,t){return t===0?0:e/t}function J(e,t,r,n,i,o){let u=new globalThis.Error(i);u.gleam_error=e,u.module=t,u.line=r,u.fn=n;for(let l in o)u[l]=o[l];return u}var f=class extends a{constructor(t){super(),this[0]=t}},d=class extends a{};function bt(e,t){if(e instanceof f){let r=e[0];return new m(r)}else return new h(t)}function Kn(e){if(e.isOk()){let t=e[0];return new f(t)}else return new d}function Ut(e,t){return e instanceof f?e[0]:t}function Vr(e,t){if(e instanceof f){let r=e[0];return new f(t(r))}else return new d}function Lo(e){return e instanceof f?e[0]:new d}var Dt=class extends a{constructor(t,r){super(),this.content=t,this.submatches=r}},Vn=class extends a{constructor(t,r){super(),this.error=t,this.byte_index=r}},Yn=class extends a{constructor(t,r){super(),this.case_insensitive=t,this.multi_line=r}};function Oo(e,t){return jo(e,t)}function Zn(e,t){return Eo(e,t)}var G=class extends a{},Z=class extends a{},ee=class extends a{};function Ve(e){return e>=0?e:e*-1}function kt(e){return So(e)}function A(e){return Co(e)}function Yr(e,t){return e===t?new Z:e<t?new G:new ee}function qo(e){return e[1]}var I=class extends a{},ze=class extends a{};function ae(e,t){for(;;){let r=e,n=t;if(r.hasLength(0))return n;{let i=r.head;e=r.tail,t=$(i,n)}}}function X(e){return ae(e,s([]))}function Mo(e){return T(e,s([]))}function Gt(e,t){for(;;){let r=e,n=t;if(r.hasLength(0))return!1;if(r.atLeastLength(1)&&T(r.head,n)){let i=r.head;return!0}else e=r.tail,t=n}}function Po(e){if(e.hasLength(0))return new h(void 0);{let t=e.head;return new m(t)}}function va(e,t,r){for(;;){let n=e,i=t,o=r;if(n.hasLength(0))return X(o);{let u=n.head,l=n.tail,p=i(u)?$(u,o):o;e=l,t=i,r=p}}}function Ye(e,t){return va(e,t,s([]))}function La(e,t,r){for(;;){let n=e,i=t,o=r;if(n.hasLength(0))return X(o);{let u=n.head;e=n.tail,t=i,r=$(i(u),o)}}}function P(e,t){return La(e,t,s([]))}function Oa(e,t,r){for(;;){let n=e,i=t,o=r;if(n.hasLength(0))return new m(X(o));{let u=n.head,l=n.tail,p=i(u);if(p.isOk()){let c=p[0];e=l,t=i,r=$(c,o)}else{let c=p[0];return new h(c)}}}}function Zr(e,t){return Oa(e,t,s([]))}function ei(e,t){for(;;){let r=e,n=t;if(n<=0)return r;if(r.hasLength(0))return s([]);e=r.tail,t=n-1}}function ja(e,t,r){for(;;){let n=e,i=t,o=r;if(i<=0)return X(o);if(n.hasLength(0))return X(o);{let l=n.head;e=n.tail,t=i-1,r=$(l,o)}}}function as(e,t){return ja(e,t,s([]))}function Ea(e,t){for(;;){let r=e,n=t;if(r.hasLength(0))return n;{let i=r.head;e=r.tail,t=$(i,n)}}}function pe(e,t){return Ea(X(e),t)}function Ta(e,t){for(;;){let r=e,n=t;if(r.hasLength(0))return n;{let i=r.head;e=r.tail,t=$(i,n)}}}function Sa(e,t){for(;;){let r=e,n=t;if(r.hasLength(0))return X(n);{let i=r.head;e=r.tail,t=Ta(i,n)}}}function ps(e){return Sa(e,s([]))}function Ze(e,t,r){for(;;){let n=e,i=t,o=r;if(n.hasLength(0))return i;{let u=n.head;e=n.tail,t=o(i,u),r=o}}}function Xr(e,t,r){if(e.hasLength(0))return t;{let n=e.head,i=e.tail;return r(Xr(i,t,r),n)}}function Ca(e,t){for(;;){let r=e,n=t;if(r.hasLength(0))return new h(void 0);{let i=r.head,o=r.tail,u=n(i);if(u.isOk()){let l=u[0];return new m(l)}else e=o,t=n}}}function zo(e,t){for(;;){let r=e,n=t;if(r.hasLength(0))return!1;{let i=r.head,o=r.tail;if(n(i))return!0;e=o,t=n}}}function qa(e,t,r,n,i,o){for(;;){let u=e,l=t,p=r,c=n,w=i,x=o,g=$(w,p);if(u.hasLength(0))return c instanceof I?$(ae(g,s([])),x):$(g,x);{let v=u.head,C=u.tail,Q=l(w,v);if(Q instanceof ee&&c instanceof ze)e=C,t=l,r=g,n=c,i=v,o=x;else if(Q instanceof G&&c instanceof I)e=C,t=l,r=g,n=c,i=v,o=x;else if(Q instanceof Z&&c instanceof I)e=C,t=l,r=g,n=c,i=v,o=x;else if(Q instanceof ee&&c instanceof I){let R=c instanceof I?$(ae(g,s([])),x):$(g,x);if(C.hasLength(0))return $(s([v]),R);{let D=C.head,j=C.tail,y=(()=>{let S=l(v,D);return S instanceof G?new I:S instanceof Z?new I:new ze})();e=j,t=l,r=s([v]),n=y,i=D,o=R}}else if(Q instanceof G&&c instanceof ze){let R=c instanceof I?$(ae(g,s([])),x):$(g,x);if(C.hasLength(0))return $(s([v]),R);{let D=C.head,j=C.tail,y=(()=>{let S=l(v,D);return S instanceof G?new I:S instanceof Z?new I:new ze})();e=j,t=l,r=s([v]),n=y,i=D,o=R}}else{let R=c instanceof I?$(ae(g,s([])),x):$(g,x);if(C.hasLength(0))return $(s([v]),R);{let D=C.head,j=C.tail,y=(()=>{let S=l(v,D);return S instanceof G?new I:S instanceof Z?new I:new ze})();e=j,t=l,r=s([v]),n=y,i=D,o=R}}}}}function Na(e,t,r,n){for(;;){let i=e,o=t,u=r,l=n;if(i.hasLength(0))return ae(o,l);if(o.hasLength(0))return ae(i,l);{let p=i.head,c=i.tail,w=o.head,x=o.tail,g=u(p,w);g instanceof G?(e=c,t=o,r=u,n=$(p,l)):g instanceof ee?(e=i,t=x,r=u,n=$(w,l)):(e=i,t=x,r=u,n=$(w,l))}}}function Ma(e,t,r){for(;;){let n=e,i=t,o=r;if(n.hasLength(0))return ae(o,s([]));if(n.hasLength(1)){let u=n.head;return ae($(ae(u,s([])),o),s([]))}else{let u=n.head,l=n.tail.head,p=n.tail.tail,c=Na(u,l,i,s([]));e=p,t=i,r=$(c,o)}}}function Pa(e,t,r,n){for(;;){let i=e,o=t,u=r,l=n;if(i.hasLength(0))return ae(o,l);if(o.hasLength(0))return ae(i,l);{let p=i.head,c=i.tail,w=o.head,x=o.tail,g=u(p,w);g instanceof G?(e=i,t=x,r=u,n=$(w,l)):g instanceof ee?(e=c,t=o,r=u,n=$(p,l)):(e=c,t=o,r=u,n=$(p,l))}}}function za(e,t,r){for(;;){let n=e,i=t,o=r;if(n.hasLength(0))return ae(o,s([]));if(n.hasLength(1)){let u=n.head;return ae($(ae(u,s([])),o),s([]))}else{let u=n.head,l=n.tail.head,p=n.tail.tail,c=Pa(u,l,i,s([]));e=p,t=i,r=$(c,o)}}}function Fa(e,t,r){for(;;){let n=e,i=t,o=r;if(n.hasLength(0))return s([]);if(n.hasLength(1)&&i instanceof I)return n.head;if(n.hasLength(1)&&i instanceof ze){let u=n.head;return ae(u,s([]))}else i instanceof I?(e=Ma(n,o,s([])),t=new ze,r=o):(e=za(n,o,s([])),t=new I,r=o)}}function cs(e,t){if(e.hasLength(0))return s([]);if(e.hasLength(1)){let r=e.head;return s([r])}else{let r=e.head,n=e.tail.head,i=e.tail.tail,o=(()=>{let l=t(r,n);return l instanceof G?new I:l instanceof Z?new I:new ze})(),u=qa(i,t,s([r]),o,n,s([]));return Fa(u,new I,t)}}function Ia(e,t,r){for(;;){let n=e,i=t,o=r;if(i<=0)return o;e=n,t=i-1,r=$(n,o)}}function Fo(e,t){return Ia(e,t,s([]))}function At(e,t){return Ca(e,r=>{let n=r[0],i=r[1];return T(n,t)?new m(i):new h(void 0)})}function ti(e,t,r){if(e.hasLength(0))return s([[t,r]]);if(e.atLeastLength(1)&&T(e.head[0],t)){let n=e.head[0],i=e.tail;return $([t,r],i)}else{let n=e.head,i=e.tail;return $(n,ti(i,t,r))}}function we(e,t){if(e.isOk()){let r=e[0];return new m(t(r))}else{let r=e[0];return new h(r)}}function en(e,t){if(e.isOk()){let r=e[0];return new m(r)}else{let r=e[0];return new h(t(r))}}function qe(e,t){if(e.isOk()){let r=e[0];return t(r)}else{let r=e[0];return new h(r)}}function xe(e,t){return qe(e,t)}function Bo(e,t){return e.isOk()?e[0]:t}function Uo(e){return en(e,t=>{})}var vt=class extends a{},at=class extends a{constructor(t,r){super(),this[0]=t,this[1]=r}},Ht=class extends a{constructor(t){super(),this.continuation=t}},tn=class extends a{constructor(t,r){super(),this.element=t,this.accumulator=r}};function Ba(){return new vt}function Do(e,t){return()=>{let r=t(e);if(r instanceof tn){let n=r.element,i=r.accumulator;return new at(n,Do(i,t))}else return new vt}}function Go(e,t){let n=Do(e,t);return new Ht(n)}function Ua(e){return Go(void 0,t=>new tn(e(),void 0))}function Ro(e){return Ua(()=>e)}function Da(e,t,r){for(;;){let n=e,i=t,o=r,u=n();if(u instanceof at){let l=u[0];e=u[1],t=i,r=i(o,l)}else return o}}function Ga(e,t,r){let n=e.continuation;return Da(n,r,t)}function Ho(e){let r=Ga(e,s([]),(n,i)=>$(i,n));return X(r)}function Wo(e,t){return()=>{if(t>0){let n=e();if(n instanceof vt)return new vt;{let i=n[0],o=n[1];return new at(i,Wo(o,t-1))}}else return new vt}}function Jo(e,t){let r=e.continuation,n=Wo(r,t);return new Ht(n)}function Ko(e,t){let r=e();if(r instanceof at){let n=r[0],i=r[1];return new at(n,()=>Ko(i,t))}else return t()}function ds(e,t){let r=()=>Ko(e.continuation,t.continuation);return new Ht(r)}function Vo(e){let t=()=>new at(e(),Ba);return new Ht(t)}function _s(e){return Vo(()=>e)}function Wt(e){return $s(e)}function rn(e){return e}function Xe(e){return e}function Xo(e,t){return Qo(e,t)}function ii(e){return ru(e)}function Qe(e){return ms(e)}function si(e,t){return su(e,t)}function et(e){let r=Wt(e);return Xe(r)}function pt(e,t){return iu(e,t)}function Kt(e){return nu(e)}function eu(e,t,r){let i=ni(e),o=ei(i,t),u=as(o,r);return et(u)}function tu(e,t,r){if(r<0)return"";if(t<0){let o=ii(e)+t;return o<0?"":eu(e,o,r)}else return eu(e,t,r)}function nn(e,t){if(t==="")return ni(e);{let n=rn(e),i=Xo(n,t);return P(i,Xe)}}function Ja(e,t){let r=ii(t),n=lt(e,r),i=yt(e,r),o=Ro(t),u=Jo(o,n);return ds(u,_s(tu(t,0,i)))}function Oe(e,t,r){let n=ii(e),i=t-n,o=Ja(i,r),u=ds(o,_s(e)),l=Ho(u);return et(l)}function ws(e){let t=Fe(e);return Xe(t)}var tt=class extends a{constructor(t,r,n){super(),this.expected=t,this.found=r,this.path=n}};function rt(e){return e}function Lt(e){return ks(e)}function Yt(e){return _u(e)}function Zt(e){return mu(e)}function Xa(e){return $u(e)}function lu(e){return t=>As(t,e)}function oi(e){return t=>{if(e.hasLength(0))return new h(s([new tt("another type",Lt(t),s([]))]));{let r=e.head,n=e.tail,i=r(t);if(i.isOk()){let o=i[0];return new m(o)}else return oi(n)(t)}}}function Vt(e){return e.isOk()?s([]):e[0]}function xs(e,t){return r=>{let n=t(r);if(n.isOk()){let i=n[0];return new m(e(i))}else{let i=n;return new h(Vt(i))}}}function gs(e,t){let r=rt(t),n=oi(s([be,o=>we(Yt(o),A)])),i=(()=>{let o=n(r);if(o.isOk())return o[0];{let u=s(["<",Lt(r),">"]),l=Wt(u);return Xe(l)}})();return e.withFields({path:$(i,e.path)})}function au(e){return t=>qe(Xa(t),r=>{let i=Zr(r,e);return ys(i,o=>gs(o,"*"))})}function ys(e,t){return en(e,r=>P(r,t))}function be(e){return du(e)}function Ne(e,t){return r=>{let n=new tt("field","nothing",s([]));return qe(vs(r,e),i=>{let u=bt(i,s([n])),l=qe(u,t);return ys(l,p=>gs(p,e))})}}function bs(e,t){return r=>qe(vs(r,e),n=>{if(n instanceof d)return new m(new d);{let o=n[0],u=As(o,t);return ys(u,l=>gs(l,e))}})}function pu(e,t,r){return n=>{let i=t(n),o=r(n);if(i.isOk()&&o.isOk()){let u=i[0],l=o[0];return new m(e(u,l))}else{let u=i,l=o;return new h(ps(s([Vt(u),Vt(l)])))}}}function cu(e,t,r,n){return i=>{let o=t(i),u=r(i),l=n(i);if(o.isOk()&&u.isOk()&&l.isOk()){let p=o[0],c=u[0],w=l[0];return new m(e(p,c,w))}else{let p=o,c=u,w=l;return new h(ps(s([Vt(p),Vt(c),Vt(w)])))}}}var hu=new WeakMap,Ls=new DataView(new ArrayBuffer(8)),Os=0;function js(e){let t=hu.get(e);if(t!==void 0)return t;let r=Os++;return Os===2147483647&&(Os=0),hu.set(e,r),r}function Es(e,t){return e^t+2654435769+(e<<6)+(e>>2)|0}function Ss(e){let t=0,r=e.length;for(let n=0;n<r;n++)t=Math.imul(31,t)+e.charCodeAt(n)|0;return t}function wu(e){Ls.setFloat64(0,e);let t=Ls.getInt32(0),r=Ls.getInt32(4);return Math.imul(73244475,t>>16^t)^r}function Qa(e){return Ss(e.toString())}function ep(e){let t=Object.getPrototypeOf(e);if(t!==null&&typeof t.hashCode=="function")try{let n=e.hashCode(e);if(typeof n=="number")return n}catch{}if(e instanceof Promise||e instanceof WeakSet||e instanceof WeakMap)return js(e);if(e instanceof Date)return wu(e.getTime());let r=0;if(e instanceof ArrayBuffer&&(e=new Uint8Array(e)),Array.isArray(e)||e instanceof Uint8Array)for(let n=0;n<e.length;n++)r=Math.imul(31,r)+je(e[n])|0;else if(e instanceof Set)e.forEach(n=>{r=r+je(n)|0});else if(e instanceof Map)e.forEach((n,i)=>{r=r+Es(je(n),je(i))|0});else{let n=Object.keys(e);for(let i=0;i<n.length;i++){let o=n[i],u=e[o];r=r+Es(je(u),Ss(o))|0}}return r}function je(e){if(e===null)return 1108378658;if(e===void 0)return 1108378659;if(e===!0)return 1108378657;if(e===!1)return 1108378656;switch(typeof e){case"number":return wu(e);case"string":return Ss(e);case"bigint":return Qa(e);case"object":return ep(e);case"symbol":return js(e);case"function":return js(e);default:return 0}}var Be=5,Cs=Math.pow(2,Be),tp=Cs-1,rp=Cs/2,np=Cs/4,$e=0,Ie=1,ke=2,Ot=3,qs={type:ke,bitmap:0,array:[]};function sn(e,t){return e>>>t&tp}function li(e,t){return 1<<sn(e,t)}function ip(e){return e-=e>>1&1431655765,e=(e&858993459)+(e>>2&858993459),e=e+(e>>4)&252645135,e+=e>>8,e+=e>>16,e&127}function Ns(e,t){return ip(e&t-1)}function Ee(e,t,r){let n=e.length,i=new Array(n);for(let o=0;o<n;++o)i[o]=e[o];return i[t]=r,i}function sp(e,t,r){let n=e.length,i=new Array(n+1),o=0,u=0;for(;o<t;)i[u++]=e[o++];for(i[u++]=r;o<n;)i[u++]=e[o++];return i}function Ts(e,t){let r=e.length,n=new Array(r-1),i=0,o=0;for(;i<t;)n[o++]=e[i++];for(++i;i<r;)n[o++]=e[i++];return n}function xu(e,t,r,n,i,o){let u=je(t);if(u===n)return{type:Ot,hash:u,array:[{type:$e,k:t,v:r},{type:$e,k:i,v:o}]};let l={val:!1};return on(Ms(qs,e,u,t,r,l),e,n,i,o,l)}function on(e,t,r,n,i,o){switch(e.type){case Ie:return op(e,t,r,n,i,o);case ke:return Ms(e,t,r,n,i,o);case Ot:return up(e,t,r,n,i,o)}}function op(e,t,r,n,i,o){let u=sn(r,t),l=e.array[u];if(l===void 0)return o.val=!0,{type:Ie,size:e.size+1,array:Ee(e.array,u,{type:$e,k:n,v:i})};if(l.type===$e)return T(n,l.k)?i===l.v?e:{type:Ie,size:e.size,array:Ee(e.array,u,{type:$e,k:n,v:i})}:(o.val=!0,{type:Ie,size:e.size,array:Ee(e.array,u,xu(t+Be,l.k,l.v,r,n,i))});let p=on(l,t+Be,r,n,i,o);return p===l?e:{type:Ie,size:e.size,array:Ee(e.array,u,p)}}function Ms(e,t,r,n,i,o){let u=li(r,t),l=Ns(e.bitmap,u);if(e.bitmap&u){let p=e.array[l];if(p.type!==$e){let w=on(p,t+Be,r,n,i,o);return w===p?e:{type:ke,bitmap:e.bitmap,array:Ee(e.array,l,w)}}let c=p.k;return T(n,c)?i===p.v?e:{type:ke,bitmap:e.bitmap,array:Ee(e.array,l,{type:$e,k:n,v:i})}:(o.val=!0,{type:ke,bitmap:e.bitmap,array:Ee(e.array,l,xu(t+Be,c,p.v,r,n,i))})}else{let p=e.array.length;if(p>=rp){let c=new Array(32),w=sn(r,t);c[w]=Ms(qs,t+Be,r,n,i,o);let x=0,g=e.bitmap;for(let v=0;v<32;v++){if(g&1){let C=e.array[x++];c[v]=C}g=g>>>1}return{type:Ie,size:p+1,array:c}}else{let c=sp(e.array,l,{type:$e,k:n,v:i});return o.val=!0,{type:ke,bitmap:e.bitmap|u,array:c}}}}function up(e,t,r,n,i,o){if(r===e.hash){let u=Ps(e,n);if(u!==-1)return e.array[u].v===i?e:{type:Ot,hash:r,array:Ee(e.array,u,{type:$e,k:n,v:i})};let l=e.array.length;return o.val=!0,{type:Ot,hash:r,array:Ee(e.array,l,{type:$e,k:n,v:i})}}return on({type:ke,bitmap:li(e.hash,t),array:[e]},t,r,n,i,o)}function Ps(e,t){let r=e.array.length;for(let n=0;n<r;n++)if(T(t,e.array[n].k))return n;return-1}function ui(e,t,r,n){switch(e.type){case Ie:return lp(e,t,r,n);case ke:return ap(e,t,r,n);case Ot:return pp(e,n)}}function lp(e,t,r,n){let i=sn(r,t),o=e.array[i];if(o!==void 0){if(o.type!==$e)return ui(o,t+Be,r,n);if(T(n,o.k))return o}}function ap(e,t,r,n){let i=li(r,t);if(!(e.bitmap&i))return;let o=Ns(e.bitmap,i),u=e.array[o];if(u.type!==$e)return ui(u,t+Be,r,n);if(T(n,u.k))return u}function pp(e,t){let r=Ps(e,t);if(!(r<0))return e.array[r]}function zs(e,t,r,n){switch(e.type){case Ie:return cp(e,t,r,n);case ke:return fp(e,t,r,n);case Ot:return dp(e,n)}}function cp(e,t,r,n){let i=sn(r,t),o=e.array[i];if(o===void 0)return e;let u;if(o.type===$e){if(!T(o.k,n))return e}else if(u=zs(o,t+Be,r,n),u===o)return e;if(u===void 0){if(e.size<=np){let l=e.array,p=new Array(e.size-1),c=0,w=0,x=0;for(;c<i;){let g=l[c];g!==void 0&&(p[w]=g,x|=1<<c,++w),++c}for(++c;c<l.length;){let g=l[c];g!==void 0&&(p[w]=g,x|=1<<c,++w),++c}return{type:ke,bitmap:x,array:p}}return{type:Ie,size:e.size-1,array:Ee(e.array,i,u)}}return{type:Ie,size:e.size,array:Ee(e.array,i,u)}}function fp(e,t,r,n){let i=li(r,t);if(!(e.bitmap&i))return e;let o=Ns(e.bitmap,i),u=e.array[o];if(u.type!==$e){let l=zs(u,t+Be,r,n);return l===u?e:l!==void 0?{type:ke,bitmap:e.bitmap,array:Ee(e.array,o,l)}:e.bitmap===i?void 0:{type:ke,bitmap:e.bitmap^i,array:Ts(e.array,o)}}return T(n,u.k)?e.bitmap===i?void 0:{type:ke,bitmap:e.bitmap^i,array:Ts(e.array,o)}:e}function dp(e,t){let r=Ps(e,t);if(r<0)return e;if(e.array.length!==1)return{type:Ot,hash:e.hash,array:Ts(e.array,r)}}function gu(e,t){if(e===void 0)return;let r=e.array,n=r.length;for(let i=0;i<n;i++){let o=r[i];if(o!==void 0){if(o.type===$e){t(o.v,o.k);continue}gu(o,t)}}}var jt=class e{static fromObject(t){let r=Object.keys(t),n=e.new();for(let i=0;i<r.length;i++){let o=r[i];n=n.set(o,t[o])}return n}static fromMap(t){let r=e.new();return t.forEach((n,i)=>{r=r.set(i,n)}),r}static new(){return new e(void 0,0)}constructor(t,r){this.root=t,this.size=r}get(t,r){if(this.root===void 0)return r;let n=ui(this.root,0,je(t),t);return n===void 0?r:n.v}set(t,r){let n={val:!1},i=this.root===void 0?qs:this.root,o=on(i,0,je(t),t,r,n);return o===this.root?this:new e(o,n.val?this.size+1:this.size)}delete(t){if(this.root===void 0)return this;let r=zs(this.root,0,je(t),t);return r===this.root?this:r===void 0?e.new():new e(r,this.size-1)}has(t){return this.root===void 0?!1:ui(this.root,0,je(t),t)!==void 0}entries(){if(this.root===void 0)return[];let t=[];return this.forEach((r,n)=>t.push([n,r])),t}forEach(t){gu(this.root,t)}hashCode(){let t=0;return this.forEach((r,n)=>{t=t+Es(je(r),je(n))|0}),t}equals(t){if(!(t instanceof e)||this.size!==t.size)return!1;let r=!0;return this.forEach((n,i)=>{r=r&&T(t.get(i,!n),n)}),r}};var Fs=void 0,yu={};function So(e){return/^[-+]?(\d+)$/.test(e)?new m(parseInt(e)):new h(Fs)}function Co(e){return e.toString()}function ru(e){if(e==="")return 0;let t=Is(e);if(t){let r=0;for(let n of t)r++;return r}else return e.match(/./gsu).length}function ni(e){let t=Is(e);return t?W.fromArray(Array.from(t).map(r=>r.segment)):W.fromArray(e.match(/./gsu))}function Is(e){if(Intl&&Intl.Segmenter)return new Intl.Segmenter().segment(e)[Symbol.iterator]()}function nu(e){let t,r=Is(e);return r?t=r.next().value?.segment:t=e.match(/./su)?.[0],t?new m([t,e.slice(t.length)]):new h(Fs)}function ms(e){return e.toLowerCase()}function Qo(e,t){return W.fromArray(e.split(t))}function iu(e,t){let r=e[Symbol.iterator](),n=r.next().value||"",i=r.next();for(;!i.done;)n=n+t+i.value,i=r.next();return n}function $s(e){let t="";for(let r of e)t=t+r;return t}function su(e,t){return e.startsWith(t)}var Au=[" "," ",` +`,"\v","\f","\r","\x85","\u2028","\u2029"].join(),Bd=new RegExp(`^([${Au}]*)`,"g"),Ud=new RegExp(`([${Au}]*)$`,"g");function vu(e){typeof process=="object"&&process.stderr?.write?process.stderr.write(e+` +`):typeof Deno=="object"?Deno.stderr.writeSync(new TextEncoder().encode(e+` +`)):console.log(e)}function jo(e,t){try{let r="gu";return t.case_insensitive&&(r+="i"),t.multi_line&&(r+="m"),new m(new RegExp(e,r))}catch(r){let n=(r.columnNumber||0)|0;return new h(new Vn(r.message,n))}}function Eo(e,t){let r=Array.from(t.matchAll(e)).map(n=>{let i=n[0],o=[];for(let u=n.length-1;u>0;u--){if(n[u]){o[u-1]=new f(n[u]);continue}o.length>0&&(o[u-1]=new d)}return new Dt(i,W.fromArray(o))});return W.fromArray(r)}function Lu(e,t){let r=e.get(t,yu);return r===yu?new h(Fs):new m(r)}function ks(e){if(typeof e=="string")return"String";if(typeof e=="boolean")return"Bool";if(e instanceof Bt)return"Result";if(e instanceof W)return"List";if(e instanceof Kr)return"BitArray";if(e instanceof jt)return"Dict";if(Number.isInteger(e))return"Int";if(Array.isArray(e))return`Tuple of ${e.length} elements`;if(typeof e=="number")return"Float";if(e===null)return"Null";if(e===void 0)return"Nil";{let t=typeof e;return t.charAt(0).toUpperCase()+t.slice(1)}}function un(e,t){return _p(e,ks(t))}function _p(e,t){return new h(W.fromArray([new tt(e,t,W.fromArray([]))]))}function du(e){return typeof e=="string"?new m(e):un("String",e)}function _u(e){return Number.isInteger(e)?new m(e):un("Int",e)}function mu(e){return typeof e=="boolean"?new m(e):un("Bool",e)}function $u(e){return Array.isArray(e)?new m(W.fromArray(e)):e instanceof W?new m(e):un("List",e)}function As(e,t){if(e==null||e instanceof d)return new m(new d);e instanceof f&&(e=e[0]);let r=t(e);return r.isOk()?new m(new f(r[0])):r}function vs(e,t){let r=()=>un("Dict",e);if(e instanceof jt||e instanceof WeakMap||e instanceof Map){let n=Lu(e,t);return new m(n.isOk()?new f(n[0]):new d)}else return e===null?r():Object.getPrototypeOf(e)==Object.prototype?bu(e,t,()=>new m(new d)):bu(e,t,r)}function bu(e,t,r){try{return t in e?new m(new f(e[t])):r()}catch{return r()}}function Fe(e){let t=typeof e;if(e===!0)return"True";if(e===!1)return"False";if(e===null)return"//js(null)";if(e===void 0)return"Nil";if(t==="string")return mp(e);if(t==="bigint"||t==="number")return e.toString();if(Array.isArray(e))return`#(${e.map(Fe).join(", ")})`;if(e instanceof W)return xp(e);if(e instanceof Jn)return yp(e);if(e instanceof Kr)return gp(e);if(e instanceof a)return wp(e);if(e instanceof jt)return $p(e);if(e instanceof Set)return`//js(Set(${[...e].map(Fe).join(", ")}))`;if(e instanceof RegExp)return`//js(${e})`;if(e instanceof Date)return`//js(Date("${e.toISOString()}"))`;if(e instanceof Function){let r=[];for(let n of Array(e.length).keys())r.push(String.fromCharCode(n+97));return`//fn(${r.join(", ")}) { ... }`}return hp(e)}function mp(e){let t='"';for(let r=0;r<e.length;r++){let n=e[r];switch(n){case` +`:t+="\\n";break;case"\r":t+="\\r";break;case" ":t+="\\t";break;case"\f":t+="\\f";break;case"\\":t+="\\\\";break;case'"':t+='\\"';break;default:n<" "||n>"~"&&n<"\xA0"?t+="\\u{"+n.charCodeAt(0).toString(16).toUpperCase().padStart(4,"0")+"}":t+=n}}return t+='"',t}function $p(e){let t="dict.from_list([",r=!0;return e.forEach((n,i)=>{r||(t=t+", "),t=t+"#("+Fe(i)+", "+Fe(n)+")",r=!1}),t+"])"}function hp(e){let t=Object.getPrototypeOf(e)?.constructor?.name||"Object",r=[];for(let o of Object.keys(e))r.push(`${Fe(o)}: ${Fe(e[o])}`);let n=r.length?" "+r.join(", ")+" ":"";return`//js(${t==="Object"?"":t+" "}{${n}})`}function wp(e){let t=Object.keys(e).map(r=>{let n=Fe(e[r]);return isNaN(parseInt(r))?`${r}: ${n}`:n}).join(", ");return t?`${e.constructor.name}(${t})`:e.constructor.name}function xp(e){return`[${e.toArray().map(Fe).join(", ")}]`}function gp(e){return`<<${Array.from(e.buffer).join(", ")}>>`}function yp(e){return`//utfcodepoint(${String.fromCodePoint(e.value)})`}function Ou(e,t){let r=Number.isInteger(t);if(r&&Array.isArray(e)||e&&typeof e=="object"||Object.getPrototypeOf(e)===Object.prototype)return new m(e[t]);if(value instanceof Dict||value instanceof WeakMap||value instanceof Map){let n=map_get(value,name);return new m(n.isOk()?new Some(n[0]):new None)}return Object.getPrototypeOf(value)==Object.prototype?try_get_field(value,name,()=>new m(new None)):new h(r?"Indexable":"Dict")}var Ue=class extends a{constructor(t){super(),this.continuation=t}};function Xt(e){return new Ue(t=>new m(e))}function ln(e,t){return e.continuation(t)}var Me=new Ue(be),ai=new Ue(Zt),De=new Ue(Yt);function an(e){return new Ue(au(e.continuation))}function pi(e){return new Ue(lu(e.continuation))}function Ap(e,t){let r=rt(t),n=oi(s([be,o=>we(Yt(o),A)])),i=(()=>{let o=n(r);return o.isOk()?o[0]:"<"+Lt(r)+">"})();return P(e,o=>o.withFields({path:$(i,o.path)}))}function vp(e,t,r){let n=Ou(r,e);if(n.isOk()){let i=n[0],o=t(i);if(o.isOk()){let u=o[0];return new m(u)}else{let u=o[0];return new h(Ap(u,e))}}else{let i=n[0];return new h(s([new tt(i,Lt(r),s([]))]))}}function Lp(e,t){return new Ue(r=>Xr(e,t.continuation,(i,o)=>u=>vp(o,i,u))(r))}function Op(e,t,r){return new Ue(n=>{let i=e.continuation(n),o=ln(Lp(t,r),n);if(i.isOk()&&o.isOk()){let u=i[0],l=o[0];return new m(u(l))}else if(!i.isOk()&&!o.isOk()){let u=i[0],l=o[0];return new h(pe(u,l))}else if(o.isOk()){let u=i[0];return new h(u)}else{let u=o[0];return new h(u)}})}function N(e,t,r){return Op(e,s([t]),r)}function Bs(e){return JSON.stringify(e)}function ju(e){return Object.fromEntries(e)}function Eu(e){return e.toArray()}function Tu(){return null}function Su(e){try{let t=JSON.parse(e);return new m(t)}catch(t){return new h(Ep(t,e))}}function Ep(e,t){return Tp(e)?new fi:Sp(e,t)}function Tp(e){return/((unexpected (end|eof))|(end of data)|(unterminated string)|(json( parse error|\.parse)\: expected '(\:|\}|\])'))/i.test(e.message)}function Sp(e,t){let r=[Cp,qp,Mp,Np];for(let n of r){let i=n(e,t);if(i)return i}return new ct("",0)}function Cp(e){let r=/unexpected token '(.)', ".+" is not valid JSON/i.exec(e.message);if(!r)return null;let n=ci(r[1]);return new ct(n,-1)}function qp(e){let r=/unexpected token (.) in JSON at position (\d+)/i.exec(e.message);if(!r)return null;let n=ci(r[1]),i=Number(r[2]);return new ct(n,i)}function Np(e,t){let n=/(unexpected character|expected .*) at line (\d+) column (\d+)/i.exec(e.message);if(!n)return null;let i=Number(n[2]),o=Number(n[3]),u=Pp(i,o,t),l=ci(t[u]);return new ct(l,u)}function Mp(e){let r=/unexpected (identifier|token) "(.)"/i.exec(e.message);if(!r)return null;let n=ci(r[2]);return new ct(n,0)}function ci(e){return"0x"+e.charCodeAt(0).toString(16).toUpperCase()}function Pp(e,t,r){if(e===1)return t-1;let n=1,i=0;return r.split("").find((o,u)=>(o===` +`&&(n+=1),n===e?(i=u+t,!0):!1)),i}var fi=class extends a{},ct=class extends a{constructor(t,r){super(),this.byte=t,this.position=r}};var Us=class extends a{constructor(t){super(),this[0]=t}};function Fp(e,t){return xe(Su(e),r=>{let n=t(r);return en(n,i=>new Us(i))})}function Cu(e,t){return Fp(e,t)}function qu(e){return Bs(e)}function ce(e){return e}function Gs(e){return e}function pn(e){return e}function Nu(){return Tu()}function ye(e){return ju(e)}function Rs(e){return Eu(e)}function Hs(e,t){let n=P(e,t);return Rs(n)}var it=class extends a{constructor(t,r,n,i,o,u,l){super(),this.scheme=t,this.userinfo=r,this.host=n,this.port=i,this.path=o,this.query=u,this.fragment=l}};function Mu(e,t){let n=Oo(e,new Yn(!0,!1)),i=Uo(n),o=we(i,p=>Zn(p,t)),u=qe(o,Po),l=we(u,p=>p.submatches);return Bo(l,s([]))}function Bp(e){if(e instanceof d)return new d;{let t=e[0],r=Kt(t);if(r.isOk()&&r[0][0]==="?"){let n=r[0][1];return new f(n)}else return new d}}function di(e){return e instanceof f&&e[0]===""?new d:e instanceof d?new d:e}function Up(e,t){for(;;){let r=e,n=t;if(n===0)return 0;if(r.hasLength(0))return n;e=r.tail,t=n-1}}function Pu(e,t){return pe(e,Fo(new d,Up(e,t)))}function Dp(e){let t=Ut(e,"");if(t==="")return[new d,new d,new d];if(t==="//")return[new d,new f(""),new d];{let r=t,n=(()=>{let o=Mu("^(//)?((.*)@)?(\\[[a-zA-Z0-9:.]*\\]|[^:]*)(:(\\d*))?",r);return Pu(o,6)})();if(n.hasLength(6)){let i=n.tail.tail.head,o=n.tail.tail.tail.head,u=n.tail.tail.tail.tail.tail.head,l=di(i),p=di(o),c=(()=>{let x=Ut(u,""),g=kt(x);return Kn(g)})();return[l,p,c]}else return[new d,new d,new d]}}function Gp(e){let t="^(([a-z][a-z0-9\\+\\-\\.]*):)?(//([^/?#]*))?([^?#]*)(\\?([^#]*))?(#.*)?",r=(()=>{let y=Mu(t,e);return Pu(y,8)})(),n=(()=>{if(r.hasLength(8)){let j=r.tail.head,y=r.tail.tail.head,S=r.tail.tail.tail.tail.head,H=r.tail.tail.tail.tail.tail.head,Ce=r.tail.tail.tail.tail.tail.tail.tail.head;return[j,y,S,H,Ce]}else return[new d,new d,new d,new d,new d]})(),i=n[0],o=n[1],u=n[2],l=n[3],p=n[4],c=di(i),w=Ut(u,""),x=Bp(l),g=Dp(o),v=g[0],C=g[1],Q=g[2],R=(()=>{let y=bt(p,void 0),S=qe(y,Kt),H=we(S,qo);return Kn(H)})(),D=(()=>{let y=di(c);return Vr(y,Qe)})();return new m(new it(D,v,C,Q,w,x,R))}function ft(e){return Gp(e)}function Rp(e,t){for(;;){let r=e,n=t;if(r.hasLength(0))return X(n);{let i=r.head,o=r.tail,u=i===""||i==="."?n:i===".."&&n.hasLength(0)?s([]):i===".."&&n.atLeastLength(1)?n.tail:$(i,n);e=o,t=u}}}function Hp(e){return Rp(e,s([]))}function er(e){return Hp(nn(e,"/"))}function _i(e){let t=(()=>{let l=e.fragment;if(l instanceof f){let p=l[0];return s(["#",p])}else return s([])})(),r=(()=>{let l=e.query;if(l instanceof f){let p=l[0];return $("?",$(p,t))}else return t})(),n=$(e.path,r),i=(()=>{let l=e.host,p=si(e.path,"/");if(l instanceof f&&!p&&l[0]!==""){let c=l[0];return $("/",n)}else return n})(),o=(()=>{let l=e.host,p=e.port;if(l instanceof f&&p instanceof f){let c=p[0];return $(":",$(A(c),i))}else return i})(),u=(()=>{let l=e.scheme,p=e.userinfo,c=e.host;if(l instanceof f&&p instanceof f&&c instanceof f){let w=l[0],x=p[0],g=c[0];return $(w,$("://",$(x,$("@",$(g,o)))))}else if(l instanceof f&&p instanceof d&&c instanceof f){let w=l[0],x=c[0];return $(w,$("://",$(x,o)))}else if(l instanceof f&&p instanceof f&&c instanceof d){let w=l[0];return $(w,$(":",o))}else if(l instanceof f&&p instanceof d&&c instanceof d){let w=l[0];return $(w,$(":",o))}else if(l instanceof d&&p instanceof d&&c instanceof f){let w=c[0];return $("//",$(w,o))}else return o})();return et(u)}function Ge(e,t,r){return e?t:r()}var cn=class extends a{constructor(t){super(),this.all=t}};function fe(e){return new cn(s([(t,r)=>e(t)]))}function k(){return new cn(s([]))}function rr(e){return new cn(Ze(e,s([]),(t,r)=>{let n=r.all;return pe(t,n)}))}var nr=class extends a{constructor(t){super(),this.content=t}},K=class extends a{constructor(t,r,n,i,o,u,l){super(),this.key=t,this.namespace=r,this.tag=n,this.attrs=i,this.children=o,this.self_closing=u,this.void=l}};var ir=class extends a{constructor(t,r){super(),this.elements=t,this.key=r}},sr=class extends a{constructor(t,r,n){super(),this[0]=t,this[1]=r,this.as_property=n}},dn=class extends a{constructor(t,r){super(),this[0]=t,this[1]=r}};function L(e,t){return new sr(e,rt(t),!1)}function Uu(e,t){return new sr(e,rt(t),!0)}function Du(e,t){return new dn("on"+e,t)}function Gu(e){return L("style",Ze(e,"",(t,r)=>{let n=r[0],i=r[1];return t+n+":"+i+";"}))}function _(e){return L("class",e)}function Ru(){return _("")}function E(e){return L("id",e)}function F(e){return L("type",e)}function mi(e){return L("value",e)}function Hu(e){return Uu("checked",e)}function Wu(e){return L("placeholder",e)}function _n(e){return Uu("disabled",e)}function te(e){return L("for",e)}function de(e){return L("href",e)}function Ws(e){return L("target",e)}function Ju(e){return L("src",e)}function Ku(e){return L("alt",e)}function z(e,t,r){return e==="area"?new K("","",e,t,s([]),!1,!0):e==="base"?new K("","",e,t,s([]),!1,!0):e==="br"?new K("","",e,t,s([]),!1,!0):e==="col"?new K("","",e,t,s([]),!1,!0):e==="embed"?new K("","",e,t,s([]),!1,!0):e==="hr"?new K("","",e,t,s([]),!1,!0):e==="img"?new K("","",e,t,s([]),!1,!0):e==="input"?new K("","",e,t,s([]),!1,!0):e==="link"?new K("","",e,t,s([]),!1,!0):e==="meta"?new K("","",e,t,s([]),!1,!0):e==="param"?new K("","",e,t,s([]),!1,!0):e==="source"?new K("","",e,t,s([]),!1,!0):e==="track"?new K("","",e,t,s([]),!1,!0):e==="wbr"?new K("","",e,t,s([]),!1,!0):new K("","",e,t,r,!1,!1)}function O(e){return new nr(e)}function re(){return new nr("")}function Yp(e){return Xr(e,s([]),(t,r)=>{if(r instanceof ir){let n=r.elements;return pe(n,t)}else return $(r,t)})}function Js(e){let t=Yp(e);return new ir(t,"")}var $i=class extends a{constructor(t){super(),this[0]=t}},Re=class extends a{constructor(t){super(),this[0]=t}};var Et=class extends a{};var hi=class extends a{constructor(t){super(),this[0]=t}};function Ks(e,t,r,n=!1){let i,o=[{prev:e,next:t,parent:e.parentNode}];for(;o.length;){let{prev:u,next:l,parent:p}=o.pop();if(l.subtree!==void 0&&(l=l.subtree()),l.content!==void 0)if(u)if(u.nodeType===Node.TEXT_NODE)u.textContent!==l.content&&(u.textContent=l.content),i??=u;else{let c=document.createTextNode(l.content);p.replaceChild(c,u),i??=c}else{let c=document.createTextNode(l.content);p.appendChild(c),i??=c}else if(l.tag!==void 0){let c=rc({prev:u,next:l,dispatch:r,stack:o,isComponent:n});u?u!==c&&p.replaceChild(c,u):p.appendChild(c),i??=c}else l.elements!==void 0?wi(l,c=>{o.unshift({prev:u,next:c,parent:p}),u=u?.nextSibling}):l.subtree!==void 0&&o.push({prev:u,next:l,parent:p})}return i}function rc({prev:e,next:t,dispatch:r,stack:n}){let i=t.namespace||"http://www.w3.org/1999/xhtml",o=e&&e.nodeType===Node.ELEMENT_NODE&&e.localName===t.tag&&e.namespaceURI===(t.namespace||"http://www.w3.org/1999/xhtml"),u=o?e:i?document.createElementNS(i,t.tag):document.createElement(t.tag),l;if(mn.has(u))l=mn.get(u);else{let j=new Map;mn.set(u,j),l=j}let p=o?new Set(l.keys()):null,c=o?new Set(Array.from(e.attributes,j=>j.name)):null,w=null,x=null,g=null;for(let j of t.attrs){let y=j[0],S=j[1];if(j.as_property)u[y]!==S&&(u[y]=S),o&&c.delete(y);else if(y.startsWith("on")){let H=y.slice(2),Ce=r(S);l.has(H)||u.addEventListener(H,$n),l.set(H,Ce),o&&p.delete(H)}else if(y.startsWith("data-lustre-on-")){let H=y.slice(15),Ce=r(nc);l.has(H)||u.addEventListener(H,$n),l.set(H,Ce),u.setAttribute(y,S)}else y==="class"?w=w===null?S:w+" "+S:y==="style"?x=x===null?S:x+S:y==="dangerous-unescaped-html"?g=S:(u.getAttribute(y)!==S&&u.setAttribute(y,S),(y==="value"||y==="selected")&&(u[y]=S),o&&c.delete(y))}if(w!==null&&(u.setAttribute("class",w),o&&c.delete("class")),x!==null&&(u.setAttribute("style",x),o&&c.delete("style")),o){for(let j of c)u.removeAttribute(j);for(let j of p)l.delete(j),u.removeEventListener(j,$n)}if(t.key!==void 0&&t.key!=="")u.setAttribute("data-lustre-key",t.key);else if(g!==null)return u.innerHTML=g,u;let v=u.firstChild,C=null,Q=null,R=null,D=t.children[Symbol.iterator]().next().value;o&&D!==void 0&&D.key!==void 0&&D.key!==""&&(C=new Set,Q=Vu(e),R=Vu(t));for(let j of t.children)wi(j,y=>{y.key!==void 0&&C!==null?v=ic(v,y,u,n,R,Q,C):(n.unshift({prev:v,next:y,parent:u}),v=v?.nextSibling)});for(;v;){let j=v.nextSibling;u.removeChild(v),v=j}return u}var mn=new WeakMap;function $n(e){let t=e.currentTarget;if(!mn.has(t)){t.removeEventListener(e.type,$n);return}let r=mn.get(t);if(!r.has(e.type)){t.removeEventListener(e.type,$n);return}r.get(e.type)(e)}function nc(e){let t=e.currentTarget,r=t.getAttribute(`data-lustre-on-${e.type}`),n=JSON.parse(t.getAttribute("data-lustre-data")||"{}"),i=JSON.parse(t.getAttribute("data-lustre-include")||"[]");switch(e.type){case"input":case"change":i.push("target.value");break}return{tag:r,data:i.reduce((o,u)=>{let l=u.split(".");for(let p=0,c=o,w=e;p<l.length;p++)p===l.length-1?c[l[p]]=w[l[p]]:(c[l[p]]??={},w=w[l[p]],c=c[l[p]]);return o},{data:n})}}function Vu(e){let t=new Map;if(e)for(let r of e.children)wi(r,n=>{let i=n?.key||n?.getAttribute?.("data-lustre-key");i&&t.set(i,n)});return t}function ic(e,t,r,n,i,o,u){for(;e&&!i.has(e.getAttribute("data-lustre-key"));){let p=e.nextSibling;r.removeChild(e),e=p}if(o.size===0)return wi(t,p=>{n.unshift({prev:e,next:p,parent:r}),e=e?.nextSibling}),e;if(u.has(t.key))return console.warn(`Duplicate key found in Lustre vnode: ${t.key}`),n.unshift({prev:null,next:t,parent:r}),e;u.add(t.key);let l=o.get(t.key);if(!l&&!e)return n.unshift({prev:null,next:t,parent:r}),e;if(!l&&e!==null){let p=document.createTextNode("");return r.insertBefore(p,e),n.unshift({prev:p,next:t,parent:r}),e}return!l||l===e?(n.unshift({prev:e,next:t,parent:r}),e=e?.nextSibling,e):(r.insertBefore(l,e),n.unshift({prev:l,next:t,parent:r}),e)}function wi(e,t){if(e.elements!==void 0)for(let r of e.elements)t(r);else t(e)}var xi=class e{#e=null;#r=[];#t=[];#n=!1;#o=!1;#i=null;#u=null;#s=null;static start(t,r,n,i,o){if(!Tt())return new h(new or);let u=r instanceof HTMLElement?r:document.querySelector(r);if(!u)return new h(new gi(r));let l=new e(n(t),i,o,u);return new m(p=>l.send(p))}constructor([t,r],n,i,o=document.body,u=!1){this.#i=t,this.#u=n,this.#s=i,this.#e=o,this.#t=r.all.toArray(),this.#n=!0,this.#o=u,window.requestAnimationFrame(()=>this.#l())}send(t){switch(!0){case t instanceof Re:{this.#r.push(t[0]),this.#l();return}case t instanceof Et:{this.#c();return}case t instanceof $i:{this.#p(t[0]);return}default:return}}emit(t,r){this.#e.dispatchEvent(new CustomEvent(t,{bubbles:!0,detail:r,composed:!0}))}#l(){if(this.#a(),this.#n){let t=this.#s(this.#i),r=n=>i=>{let o=n(i);o instanceof m&&this.send(new Re(o[0]))};this.#n=!1,this.#e=Ks(this.#e,t,r,this.#o)}}#a(t=0){for(;this.#r.length;){let[r,n]=this.#u(this.#i,this.#r.shift());this.#n||=this.#i!==r,this.#i=r,this.#t=this.#t.concat(n.all.toArray())}for(;this.#t.length;)this.#t.shift()(r=>this.send(new Re(r)),(r,n)=>this.emit(r,n));this.#r.length&&(t<5?this.#a(++t):window.requestAnimationFrame(()=>this.#l()))}#p(t){switch(!0){case t instanceof hi:{let r=this.#s(t[0]),n=i=>o=>{let u=i(o);u instanceof m&&this.send(new Re(u[0]))};this.#r=[],this.#t=[],this.#n=!1,this.#e=Ks(this.#e,r,n,this.#o)}}}#c(){this.#e.remove(),this.#e=null,this.#i=null,this.#r=[],this.#t=[],this.#n=!1,this.#u=()=>{},this.#s=()=>{}}},Yu=(e,t,r)=>xi.start(r,t,e.init,e.update,e.view),Tt=()=>globalThis.window&&window.document;var Zu=e=>e.preventDefault();var Vs=class extends a{constructor(t,r,n,i){super(),this.init=t,this.update=r,this.view=n,this.on_attribute_change=i}};var gi=class extends a{constructor(t){super(),this.selector=t}},or=class extends a{};function Xu(e,t,r){return new Vs(e,t,r,new d)}function Qu(e,t,r){return Ge(!Tt(),new h(new or),()=>Yu(e,t,r))}function b(e){return O(e)}function rl(e,t){return z("body",e,t)}function nl(e,t){return z("footer",e,t)}function He(e,t){return z("h1",e,t)}function il(e,t){return z("h2",e,t)}function sl(e,t){return z("main",e,t)}function ol(e,t){return z("nav",e,t)}function _e(e,t){return z("section",e,t)}function M(e,t){return z("div",e,t)}function ur(e,t){return z("li",e,t)}function yi(e,t){return z("ol",e,t)}function he(e,t){return z("p",e,t)}function ul(e,t){return z("ul",e,t)}function ne(e,t){return z("a",e,t)}function Pe(e,t){return z("span",e,t)}function bi(e,t){return z("time",e,t)}function ll(e){return z("img",e,s([]))}function V(e,t){return z("button",e,t)}function Te(e,t){return z("form",e,t)}function ie(e){return z("input",e,s([]))}function B(e,t){return z("label",e,t)}function ki(e,t){return z("textarea",e,s([O(t)]))}var lr=class extends a{},ar=class extends a{},Ai=class extends a{},vi=class extends a{},Li=class extends a{},Oi=class extends a{},ji=class extends a{},Ei=class extends a{},Ti=class extends a{};var Si=class extends a{},hn=class extends a{};function pl(e){return e instanceof ji?"connect":e instanceof Li?"delete":e instanceof lr?"get":e instanceof Ai?"head":e instanceof Ei?"options":e instanceof Ti?"patch":e instanceof ar?"post":e instanceof vi?"put":e instanceof Oi?"trace":e[0]}function cl(e){return e instanceof Si?"http":"https"}function fl(e){let t=Qe(e);return t==="http"?new m(new Si):t==="https"?new m(new hn):new h(void 0)}var Ci=class extends a{constructor(t,r,n,i,o,u,l,p){super(),this.method=t,this.headers=r,this.body=n,this.scheme=i,this.host=o,this.port=u,this.path=l,this.query=p}};function ml(e){return new it(new f(cl(e.scheme)),new d,new f(e.host),e.port,e.path,e.query,new d)}function pc(e){return xe((()=>{let t=e.scheme,r=Ut(t,"");return fl(r)})(),t=>xe((()=>{let r=e.host;return bt(r,void 0)})(),r=>{let n=new Ci(new lr,s([]),"",t,r,e.port,e.path,e.query);return new m(n)}))}function $l(e,t,r){let n=ti(e.headers,Qe(t),r);return e.withFields({headers:n})}function hl(e,t){let r=e.method,n=e.headers,i=e.scheme,o=e.host,u=e.port,l=e.path,p=e.query;return new Ci(r,n,t,i,o,u,l,p)}function wl(e,t){return e.withFields({method:t})}function Zs(e){let r=ft(e);return xe(r,pc)}var st=class extends a{constructor(t,r,n){super(),this.status=t,this.headers=r,this.body=n}};var pr=class e{constructor(t){this.promise=t}static wrap(t){return t instanceof Promise?new e(t):t}static unwrap(t){return t instanceof e?t.promise:t}};function qi(e){return Promise.resolve(pr.wrap(e))}function xl(e,t){return e.then(r=>t(pr.unwrap(r)))}function Ni(e,t){return e.then(r=>pr.wrap(t(pr.unwrap(r))))}function Xs(e,t){return e.catch(r=>t(r))}function gl(e,t){return Ni(e,n=>(t(n),n))}function Mi(e,t){return xl(e,n=>{if(n.isOk()){let i=n[0];return t(i)}else{let i=n[0];return qi(new h(i))}})}async function bl(e){try{return new m(await fetch(e))}catch(t){return new h(new Pi(t.toString()))}}function kl(e){return new st(e.status,W.fromArray([...e.headers]),e)}function Al(e){let t=_i(ml(e)),r=pl(e.method).toUpperCase(),n={headers:dc(e.headers),method:r};return r!=="GET"&&r!=="HEAD"&&(n.body=e.body),new globalThis.Request(t,n)}function dc(e){let t=new globalThis.Headers;for(let[r,n]of e)t.append(r.toLowerCase(),n);return t}async function Qs(e){let t;try{t=await e.body.text()}catch{return new h(new zi)}return new m(e.withFields({body:t}))}var Pi=class extends a{constructor(t){super(),this[0]=t}},zi=class extends a{};function vl(e){let r=Al(e),n=bl(r);return Mi(n,i=>qi(new m(kl(i))))}var Fi=class extends a{constructor(t){super(),this[0]=t}},eo=class extends a{constructor(t){super(),this[0]=t}},to=class extends a{constructor(t){super(),this[0]=t}},Ii=class extends a{},ro=class extends a{},no=class extends a{constructor(t,r){super(),this[0]=t,this[1]=r}},io=class extends a{},so=class extends a{constructor(t){super(),this.run=t}};function Ll(e,t,r){let n=vl(e),i=Mi(n,Qs),o=Ni(i,l=>{if(l.isOk()){let p=l[0];return t.run(new m(p))}else return t.run(new h(new Ii))}),u=Xs(o,l=>t.run(new h(new Ii)));gl(u,r)}function St(e,t){return fe(r=>{let n=Zs(e);if(n.isOk()){let i=n[0];return Ll(i,t,r)}else return r(t.run(new h(new Fi(e))))})}function ve(e,t,r){return fe(n=>{let i=Zs(e);if(i.isOk()){let u=i[0],l=wl(u,new ar),p=$l(l,"Content-Type","application/json"),c=hl(p,qu(t));return Ll(c,r,n)}else return n(r.run(new h(new Fi(e))))})}function mc(e){if(e instanceof st&&200<=e.status&&e.status<=299){let t=e.status,r=e.body;return new m(r)}else{if(e instanceof st&&e.status===401)return new h(new io);if(e instanceof st&&e.status===404)return new h(new ro);if(e instanceof st&&e.status===500){let t=e.body;return new h(new eo(t))}else{let t=e.status,r=e.body;return new h(new no(t,r))}}}function Y(e,t){return new so(r=>{let i=xe(r,mc),o=xe(i,u=>{let l=Cu(u,e);if(l.isOk()){let p=l[0];return new m(p)}else{let p=l[0];return new h(new to(p))}});return t(o)})}var $c={handle_external_links:!1,handle_internal_links:!0},W0=window?.location?.href;var uo=(e,t=$c)=>{document.body.addEventListener("click",r=>{let n=El(r.target);if(n)try{let i=new URL(n.href),o=Ol(i),u=i.host!==window.location.host;return!t.handle_external_links&&u||!t.handle_internal_links&&!u?void 0:(r.preventDefault(),u||(window.history.pushState({},"",n.href),window.requestAnimationFrame(()=>{i.hash&&document.getElementById(i.hash.slice(1))?.scrollIntoView()})),e(o))}catch{return}}),window.addEventListener("popstate",r=>{r.preventDefault();let n=new URL(window.location.href),i=Ol(n);window.requestAnimationFrame(()=>{n.hash&&document.getElementById(n.hash.slice(1))?.scrollIntoView()}),e(i)}),window.addEventListener("modem-push",({detail:r})=>{e(r)}),window.addEventListener("modem-replace",({detail:r})=>{e(r)})},jl=e=>{window.history.pushState({},"",_i(e)),window.requestAnimationFrame(()=>{e.fragment[0]&&document.getElementById(e.fragment[0])?.scrollIntoView()}),window.dispatchEvent(new CustomEvent("modem-push",{detail:e}))};var El=e=>e.tagName==="BODY"?null:e.tagName==="A"?e:El(e.parentElement),Ol=e=>new it(e.protocol?new f(e.protocol):new d,new d,e.hostname?new f(e.hostname):new d,e.port?new f(Number(e.port)):new d,e.pathname,e.search?new f(e.search.slice(1)):new d,e.hash?new f(e.hash.slice(1)):new d);function Tl(e){return fe(t=>Ge(!Tt(),void 0,()=>uo(r=>{let i=e(r);return t(i)})))}var hc=new it(new d,new d,new d,new d,"",new d,new d);function dr(e,t,r){return fe(n=>Ge(!Tt(),void 0,()=>jl(hc.withFields({path:e,query:t,fragment:r}))))}function Sl(e){let r=ws(e);return vu(r),e}var Bi=class extends a{constructor(t,r,n,i,o,u,l,p,c,w,x,g){super(),this.id=t,this.title=r,this.href=n,this.body=i,this.likes=o,this.user_like_post=u,this.comments_count=l,this.comments=p,this.tags=c,this.username=w,this.original_creator=x,this.created_at=g}},Ui=class extends a{constructor(t,r,n,i,o,u,l){super(),this.id=t,this.body=r,this.username=n,this.likes=i,this.user_like_post_comment=o,this.parent_id=u,this.created_at=l}},wn=class extends a{},xn=class extends a{},gn=class extends a{},yn=class extends a{},bn=class extends a{},Di=class extends a{},lo=class extends a{},kn=class extends a{},Gi=class extends a{constructor(t,r,n,i){super(),this.id=t,this.name=r,this.category=n,this.permission=i}};function Cl(e){if(e==="format")return new wn;if(e==="genre")return new xn;if(e==="kirakira")return new gn;if(e==="platforms")return new yn;if(e==="practices")return new bn;if(e==="tools")return new Di;throw Sl(e),J("panic","shared",43,"string_to_tag_category","Invalid tag category",{})}function ql(e){return e instanceof wn?"format":e instanceof xn?"genre":e instanceof gn?"kirakira":e instanceof yn?"platforms":e instanceof bn?"practices":"tools"}function Nl(e){if(e==="member")return new lo;if(e==="admin")return new kn;throw J("panic","shared",63,"string_to_tag_permission","Invalid tag permission",{})}var Ml=s([new wn,new xn,new gn,new yn,new bn,new Di]);function se(){return _("rounded-full w-fit px-4 text-sm py-1 bg-[#584355] enabled:hover:bg-[#584355]/80 disabled:bg-[#2f2f2f]/30 text-white")}function Ri(e,t){return Du(e,t)}function We(e){return Ri("click",t=>new m(e))}function gc(e){let t=e;return Ne("target",Ne("value",be))(t)}function oe(e){return Ri("input",t=>{let r=gc(t);return we(r,e)})}function yc(e){let t=e;return Ne("target",Ne("checked",Zt))(t)}function ao(e){return Ri("change",t=>{let r=yc(t);return we(r,e)})}function Se(e){return Ri("submit",t=>{let r=Zu(t);return new m(e)})}var An=class extends a{},vn=class extends a{},_r=class extends a{constructor(t){super(),this.auth_code=t}},Ln=class extends a{},mr=class extends a{constructor(t){super(),this.token=t}},Ct=class extends a{},On=class extends a{constructor(t){super(),this.username=t}},mt=class extends a{constructor(t){super(),this.post_id=t}},$r=class extends a{},Hi=class extends a{constructor(t,r,n,i,o,u,l,p,c,w,x,g,v,C,Q,R,D,j,y,S,H,Ce,Hr,Hn,ss,os){super(),this.route=t,this.inviter=r,this.auth_user=n,this.sign_up_username=i,this.sign_up_email=o,this.sign_up_password=u,this.sign_up_error=l,this.login_email_username=p,this.login_password=c,this.login_error=w,this.create_post_title=x,this.create_post_href=g,this.create_post_body=v,this.create_post_original_creator=C,this.create_post_use_body=Q,this.create_post_tags=R,this.create_post_error=D,this.posts=j,this.show_post=y,this.create_comment_body=S,this.create_comment_error=H,this.create_comment_parent_id=Ce,this.tags=Hr,this.invite_link=Hn,this.forgot_password_response=ss,this.change_password_target=os}},jn=class extends a{constructor(t){super(),this[0]=t}},En=class extends a{constructor(t){super(),this[0]=t}},Tn=class extends a{constructor(t){super(),this[0]=t}},Sn=class extends a{constructor(t){super(),this[0]=t}},Cn=class extends a{constructor(t){super(),this[0]=t}},qn=class extends a{constructor(t){super(),this[0]=t}},hr=class extends a{constructor(t){super(),this.value=t}},wr=class extends a{constructor(t){super(),this.value=t}},xr=class extends a{constructor(t){super(),this.value=t}},gr=class extends a{constructor(t){super(),this.value=t}},yr=class extends a{},Nn=class extends a{constructor(t){super(),this.resp_result=t}},$t=class extends a{constructor(t){super(),this.value=t}},ht=class extends a{constructor(t){super(),this.value=t}},br=class extends a{constructor(t){super(),this.value=t}},kr=class extends a{},Ar=class extends a{constructor(t){super(),this.resp_result=t}},vr=class extends a{},Mn=class extends a{constructor(t){super(),this.resp_result=t}},Lr=class extends a{constructor(t){super(),this.value=t}},Or=class extends a{constructor(t){super(),this.value=t}},jr=class extends a{constructor(t){super(),this.value=t}},Er=class extends a{constructor(t){super(),this.value=t}},Tr=class extends a{constructor(t){super(),this.value=t}},Sr=class extends a{constructor(t){super(),this.tag_id=t}},qt=class extends a{constructor(t){super(),this.value=t}},Cr=class extends a{},Pn=class extends a{constructor(t){super(),this.resp_result=t}},qr=class extends a{constructor(t){super(),this.post_id=t}},Nr=class extends a{constructor(t){super(),this.resp_result=t}},Mr=class extends a{constructor(t){super(),this.value=t}},Nt=class extends a{constructor(t){super(),this.value=t}},zn=class extends a{constructor(t){super(),this.value=t}},Pr=class extends a{},Fn=class extends a{constructor(t){super(),this.resp_result=t}},zr=class extends a{constructor(t){super(),this.post_comment_id=t}},Fr=class extends a{constructor(t){super(),this.resp_result=t}},Ir=class extends a{},In=class extends a{constructor(t){super(),this.resp_result=t}},Br=class extends a{},Bn=class extends a{constructor(t){super(),this.resp_result=t}},Un=class extends a{constructor(t){super(),this[0]=t}},Ur=class extends a{},Wi=class extends a{constructor(t){super(),this.resp_result=t}},Dn=class extends a{constructor(t){super(),this.username=t}},po=class extends a{constructor(t,r){super(),this.message=t,this.error=r}},Ji=class extends a{constructor(t){super(),this.posts=t}},Ki=class extends a{constructor(t){super(),this.tags=t}},Vi=class extends a{constructor(t,r,n){super(),this.user_id=t,this.username=r,this.is_admin=n}};function Le(){return pu((e,t)=>new po(e,t),bs("message",be),bs("error",be))}function U(){return"http://localhost:8001"}function Yi(e,t,r,n){return V(s([_("flex flex-col justify-center items-center"),_(n),We(r),F("button")]),s([M(s([Gu(s([["width","0"],["height","0"],["border-left","5px solid transparent"],["border-right","5px solid transparent"],["border-bottom","10px solid "+(e?"#000000":"#939393")]]))]),s([])),B(s([_("text-xs text-neutral-500")]),s([O(A(t))]))]))}function zl(e){return ve(U()+"/api/posts/"+A(e)+"/likes",ye(s([])),Y(Le(),t=>new Nr(t)))}function Fl(e){return ve(U()+"/api/posts/comments/"+A(e)+"/likes",ye(s([])),Y(Le(),t=>new Fr(t)))}function ue(){return _("px-2 py-1 rounded-2xl border text-sm")}function Il(e){return _e(s([_("flex flex-col mx-auto max-w-[450px] w-full gap-4")]),s([He(s([_("text-[#584355] font-bold")]),s([b("Change Password for "+e.change_password_target)])),Te(s([_("grid gap-2 w-full"),Se(new Ur)]),s([B(s([te("change_password_form:password")]),s([b("New Password")])),ie(s([ue(),oe(t=>new ht(t)),E("change_password_form:password"),F("password"),L("autocomplete","current-password")])),(()=>{let t=e.forgot_password_response;if(t instanceof f){let r=t[0];if(r.isOk()){let n=r[0];return M(s([_("text-green-700")]),s([b(n)]))}else{let n=r[0];return M(s([_("text-red-700")]),s([b(n)]))}}else return re()})(),M(s([_("flex justify-between items-center mt-2")]),s([V(s([se(),L("type","submit")]),s([b("Change Password")]))]))]))]))}function Zi(e){return B(s([_("text-xs rounded-md text-neutral-700 bg-[#ffaff3]/50 border border-[#ffaff3] px-1 w-fit")]),s([b(e)]))}function Bl(e){return _e(s([_("flex flex-col mx-auto max-w-[600px]"),E("create_post_form")]),s([He(s([_("text-[#584355] font-bold")]),s([b("Create Post")])),Te(s([_("w-full gap-4 grid"),Se(new Cr)]),pe(s([M(s([_("grid sm:grid-cols-[170px,1fr] items-center sm:justify-start gap-2 w-full")]),s([B(s([te("post_form:title")]),s([b("Title")])),ie(s([ue(),oe(t=>new Lr(t)),E("post_form:title"),F("text")])),B(s([te("post_form:by_you")]),s([b("Is this by you?")])),ie(s([L("type","checkbox"),E("post_form:by_you"),ao(t=>new Er(t)),_("mx-auto accent-[#ffaff3]/50")])),B(s([te("post_form:replace_link")]),s([b("Replace link with body?")])),ie(s([L("type","checkbox"),E("post_form:replace_link"),ao(t=>new Tr(t)),_("mx-auto accent-[#ffaff3]/50")])),e.create_post_use_body?Js(s([B(s([te("post_form:body")]),s([b("Body")])),ki(s([ue(),_("h-64 sm:h-36 md:h-24"),E("post_form:body"),oe(r=>new jr(r))]),""),M(s([]),s([])),he(s([_("text-xs leading-tight")]),s([b("Kirakira is above all else a showcase or sharing forum. If you are going to ask for help with gleam refer to the "),ne(s([de("https://discord.gg/Fm8Pwmy"),_("hover:underline text-cyan-700")]),s([b("gleam discord.")]))]))])):Js(s([B(s([te("post_form:link")]),s([b("Link")])),ie(s([ue(),E("post_form:link"),oe(r=>new Or(r)),F("url")]))]))])),M(s([_("grid grid-cols-2 md:grid-cols-3 gap-4 px-4")]),P(Ml,t=>M(s([]),s([il(s([_("underline text-lg font-semibold")]),s([b(ql(t))])),ul(s([_("list-disc")]),(()=>{let r=Ye(e.tags,i=>T(i.category,t)),n=Ye(r,i=>{let o=e.auth_user;if(o instanceof f){let u=o[0];return!(T(i.permission,new kn)&&!u.is_admin)}else return!1});return P(n,i=>ur(s([]),s([Pe(s([_("flex items-center gap-1 list")]),s([ie(s([F("checkbox"),_("accent-[#ffaff3]/50"),Hu(Gt(e.create_post_tags,i.id)),We(new Sr(i.id))])),Zi(i.name)]))])))})())]))))]),s([V(s([se(),_n(e.create_post_title===""||e.create_post_href===""&&!e.create_post_use_body||e.create_post_body===""&&e.create_post_use_body||Mo(e.create_post_tags)),_("mx-auto"),F("submit")]),s([b("Create Post")])),(()=>{let t=e.create_post_error;if(t instanceof f){let r=t[0];return he(s([_("text-red-500 text-center")]),s([b("Error: "+r)]))}else return re()})()])))]))}function Ul(e){return _e(s([_("flex flex-col mx-auto max-w-[450px] w-full gap-4")]),s([He(s([_("text-[#584355] font-bold")]),s([b("Forgot Password")])),Te(s([_("grid gap-2 w-full"),Se(new Br)]),s([B(s([te("forgot_password_form:email")]),s([b("E-mail")])),ie(s([ue(),oe(t=>new $t(t)),E("forgot_password_form:email"),F("text"),L("autocomplete","email"),mi(e.login_email_username)])),(()=>{let t=e.forgot_password_response;if(t instanceof f){let r=t[0];if(r.isOk()){let n=r[0];return M(s([_("text-green-700")]),s([b(n)]))}else{let n=r[0];return M(s([_("text-red-700")]),s([b(n)]))}}else return re()})(),M(s([_("flex justify-between items-center mt-2")]),s([V(s([se(),L("type","submit")]),s([b("Request")])),ne(s([de("/auth/login"),_("ms-auto text-[#584355]")]),s([b("Back to Login")]))]))]))]))}var Gn=class extends a{constructor(t){super(),this[0]=t}},ot=class extends a{},Mt=class extends a{},Pt=class extends a{},xt=class extends a{},ut=class extends a{},zt=class extends a{},Ft=class extends a{},It=class extends a{},Dr=class extends a{};function wt(e,t){return[lt(e,t),yt(e,t)]}var fo=1e3;var _o=1e6;var mo=6e7;var $o=36e8;var ho=864e8;var wo=6048e8;var xo=2592e9;var go=31536e9;function Gl(e){let r=Ze(e,0,(n,i)=>{if(i[1]instanceof ot){let o=i[0];return n+o}else if(i[1]instanceof Mt){let o=i[0];return n+o*fo}else if(i[1]instanceof Pt){let o=i[0];return n+o*_o}else if(i[1]instanceof xt){let o=i[0];return n+o*mo}else if(i[1]instanceof ut){let o=i[0];return n+o*$o}else if(i[1]instanceof zt){let o=i[0];return n+o*ho}else if(i[1]instanceof Ft){let o=i[0];return n+o*wo}else if(i[1]instanceof It){let o=i[0];return n+o*xo}else{let o=i[0];return n+o*go}});return new Gn(r)}function yo(e){let t=e[0],r=Ve(t),n=wt(r,go),i=n[0],o=n[1],u=wt(o,xo),l=u[0],p=u[1],c=wt(p,wo),w=c[0],x=c[1],g=wt(x,ho),v=g[0],C=g[1],Q=wt(C,$o),R=Q[0],D=Q[1],j=wt(D,mo),y=j[0],S=j[1],H=wt(S,_o),Ce=H[0],Hr=H[1],Hn=wt(Hr,fo),ss=Hn[0],os=Hn[1],da=s([[i,new Dr],[l,new It],[w,new Ft],[v,new zt],[R,new ut],[y,new xt],[Ce,new Pt],[ss,new Mt],[os,new ot]]),_a=Ye(da,Wr=>Wr[0]>0);return P(_a,Wr=>t<0?[-1*Wr[0],Wr[1]]:Wr)}var Dl=s([[new Dr,go],[new It,xo],[new Ft,wo],[new zt,ho],[new ut,$o],[new xt,mo],[new Pt,_o],[new Mt,fo],[new ot,1]]);function vc(e){for(;;){let t=e;if(!t.atLeastLength(2))throw J("assignment_no_match","birl/duration",254,"inner_blur","Assignment pattern did not match",{value:t});let r=t.head,n=t.tail.head,i=At(Dl,n[1]);if(!i.isOk())throw J("assignment_no_match","birl/duration",255,"inner_blur","Assignment pattern did not match",{value:i});let o=i[0],u=At(Dl,r[1]);if(!u.isOk())throw J("assignment_no_match","birl/duration",256,"inner_blur","Assignment pattern did not match",{value:u});let l=u[0],p=r[0]*l<lt(o*2,3)?n:[n[0]+1,n[1]],c=ei(t,2);if(c.hasLength(0))return p;e=$(p,c)}}function Rl(e){let t=yo(e);if(t.hasLength(0))return[0,new ot];{let i=X(t);return vc(i)}}var Hl=s([["Africa/Abidjan",0],["Africa/Algiers",3600],["Africa/Bissau",0],["Africa/Cairo",7200],["Africa/Casablanca",3600],["Africa/Ceuta",3600],["Africa/El_Aaiun",3600],["Africa/Johannesburg",7200],["Africa/Juba",7200],["Africa/Khartoum",7200],["Africa/Lagos",3600],["Africa/Maputo",7200],["Africa/Monrovia",0],["Africa/Nairobi",10800],["Africa/Ndjamena",3600],["Africa/Sao_Tome",0],["Africa/Tripoli",7200],["Africa/Tunis",3600],["Africa/Windhoek",7200],["America/Adak",-36e3],["America/Anchorage",-32400],["America/Araguaina",-10800],["America/Argentina/Buenos_Aires",-10800],["America/Argentina/Catamarca",-10800],["America/Argentina/Cordoba",-10800],["America/Argentina/Jujuy",-10800],["America/Argentina/La_Rioja",-10800],["America/Argentina/Mendoza",-10800],["America/Argentina/Rio_Gallegos",-10800],["America/Argentina/Salta",-10800],["America/Argentina/San_Juan",-10800],["America/Argentina/San_Luis",-10800],["America/Argentina/Tucuman",-10800],["America/Argentina/Ushuaia",-10800],["America/Asuncion",-14400],["America/Bahia",-10800],["America/Bahia_Banderas",-21600],["America/Barbados",-14400],["America/Belem",-10800],["America/Belize",-21600],["America/Boa_Vista",-14400],["America/Bogota",-18e3],["America/Boise",-25200],["America/Cambridge_Bay",-25200],["America/Campo_Grande",-14400],["America/Cancun",-18e3],["America/Caracas",-14400],["America/Cayenne",-10800],["America/Chicago",-21600],["America/Chihuahua",-21600],["America/Ciudad_Juarez",-25200],["America/Costa_Rica",-21600],["America/Cuiaba",-14400],["America/Danmarkshavn",0],["America/Dawson",-25200],["America/Dawson_Creek",-25200],["America/Denver",-25200],["America/Detroit",-18e3],["America/Edmonton",-25200],["America/Eirunepe",-18e3],["America/El_Salvador",-21600],["America/Fort_Nelson",-25200],["America/Fortaleza",-10800],["America/Glace_Bay",-14400],["America/Goose_Bay",-14400],["America/Grand_Turk",-18e3],["America/Guatemala",-21600],["America/Guayaquil",-18e3],["America/Guyana",-14400],["America/Halifax",-14400],["America/Havana",-18e3],["America/Hermosillo",-25200],["America/Indiana/Indianapolis",-18e3],["America/Indiana/Knox",-21600],["America/Indiana/Marengo",-18e3],["America/Indiana/Petersburg",-18e3],["America/Indiana/Tell_City",-21600],["America/Indiana/Vevay",-18e3],["America/Indiana/Vincennes",-18e3],["America/Indiana/Winamac",-18e3],["America/Inuvik",-25200],["America/Iqaluit",-18e3],["America/Jamaica",-18e3],["America/Juneau",-32400],["America/Kentucky/Louisville",-18e3],["America/Kentucky/Monticello",-18e3],["America/La_Paz",-14400],["America/Lima",-18e3],["America/Los_Angeles",-28800],["America/Maceio",-10800],["America/Managua",-21600],["America/Manaus",-14400],["America/Martinique",-14400],["America/Matamoros",-21600],["America/Mazatlan",-25200],["America/Menominee",-21600],["America/Merida",-21600],["America/Metlakatla",-32400],["America/Mexico_City",-21600],["America/Miquelon",-10800],["America/Moncton",-14400],["America/Monterrey",-21600],["America/Montevideo",-10800],["America/New_York",-18e3],["America/Nome",-32400],["America/Noronha",-7200],["America/North_Dakota/Beulah",-21600],["America/North_Dakota/Center",-21600],["America/North_Dakota/New_Salem",-21600],["America/Nuuk",-7200],["America/Ojinaga",-21600],["America/Panama",-18e3],["America/Paramaribo",-10800],["America/Phoenix",-25200],["America/Port-au-Prince",-18e3],["America/Porto_Velho",-14400],["America/Puerto_Rico",-14400],["America/Punta_Arenas",-10800],["America/Rankin_Inlet",-21600],["America/Recife",-10800],["America/Regina",-21600],["America/Resolute",-21600],["America/Rio_Branco",-18e3],["America/Santarem",-10800],["America/Santiago",-14400],["America/Santo_Domingo",-14400],["America/Sao_Paulo",-10800],["America/Scoresbysund",-7200],["America/Sitka",-32400],["America/St_Johns",-12600],["America/Swift_Current",-21600],["America/Tegucigalpa",-21600],["America/Thule",-14400],["America/Tijuana",-28800],["America/Toronto",-18e3],["America/Vancouver",-28800],["America/Whitehorse",-25200],["America/Winnipeg",-21600],["America/Yakutat",-32400],["Antarctica/Casey",28800],["Antarctica/Davis",25200],["Antarctica/Macquarie",36e3],["Antarctica/Mawson",18e3],["Antarctica/Palmer",-10800],["Antarctica/Rothera",-10800],["Antarctica/Troll",0],["Antarctica/Vostok",18e3],["Asia/Almaty",18e3],["Asia/Amman",10800],["Asia/Anadyr",43200],["Asia/Aqtau",18e3],["Asia/Aqtobe",18e3],["Asia/Ashgabat",18e3],["Asia/Atyrau",18e3],["Asia/Baghdad",10800],["Asia/Baku",14400],["Asia/Bangkok",25200],["Asia/Barnaul",25200],["Asia/Beirut",7200],["Asia/Bishkek",21600],["Asia/Chita",32400],["Asia/Choibalsan",28800],["Asia/Colombo",19800],["Asia/Damascus",10800],["Asia/Dhaka",21600],["Asia/Dili",32400],["Asia/Dubai",14400],["Asia/Dushanbe",18e3],["Asia/Famagusta",7200],["Asia/Gaza",7200],["Asia/Hebron",7200],["Asia/Ho_Chi_Minh",25200],["Asia/Hong_Kong",28800],["Asia/Hovd",25200],["Asia/Irkutsk",28800],["Asia/Jakarta",25200],["Asia/Jayapura",32400],["Asia/Jerusalem",7200],["Asia/Kabul",16200],["Asia/Kamchatka",43200],["Asia/Karachi",18e3],["Asia/Kathmandu",20700],["Asia/Khandyga",32400],["Asia/Kolkata",19800],["Asia/Krasnoyarsk",25200],["Asia/Kuching",28800],["Asia/Macau",28800],["Asia/Magadan",39600],["Asia/Makassar",28800],["Asia/Manila",28800],["Asia/Nicosia",7200],["Asia/Novokuznetsk",25200],["Asia/Novosibirsk",25200],["Asia/Omsk",21600],["Asia/Oral",18e3],["Asia/Pontianak",25200],["Asia/Pyongyang",32400],["Asia/Qatar",10800],["Asia/Qostanay",18e3],["Asia/Qyzylorda",18e3],["Asia/Riyadh",10800],["Asia/Sakhalin",39600],["Asia/Samarkand",18e3],["Asia/Seoul",32400],["Asia/Shanghai",28800],["Asia/Singapore",28800],["Asia/Srednekolymsk",39600],["Asia/Taipei",28800],["Asia/Tashkent",18e3],["Asia/Tbilisi",14400],["Asia/Tehran",12600],["Asia/Thimphu",21600],["Asia/Tokyo",32400],["Asia/Tomsk",25200],["Asia/Ulaanbaatar",28800],["Asia/Urumqi",21600],["Asia/Ust-Nera",36e3],["Asia/Vladivostok",36e3],["Asia/Yakutsk",32400],["Asia/Yangon",23400],["Asia/Yekaterinburg",18e3],["Asia/Yerevan",14400],["Atlantic/Azores",-3600],["Atlantic/Bermuda",-14400],["Atlantic/Canary",0],["Atlantic/Cape_Verde",-3600],["Atlantic/Faroe",0],["Atlantic/Madeira",0],["Atlantic/South_Georgia",-7200],["Atlantic/Stanley",-10800],["Australia/Adelaide",34200],["Australia/Brisbane",36e3],["Australia/Broken_Hill",34200],["Australia/Darwin",34200],["Australia/Eucla",31500],["Australia/Hobart",36e3],["Australia/Lindeman",36e3],["Australia/Lord_Howe",37800],["Australia/Melbourne",36e3],["Australia/Perth",28800],["Australia/Sydney",36e3],["CET",3600],["CST6CDT",-21600],["EET",7200],["EST",-18e3],["EST5EDT",-18e3],["Etc/GMT",0],["Etc/GMT+1",-3600],["Etc/GMT+10",-36e3],["Etc/GMT+11",-39600],["Etc/GMT+12",-43200],["Etc/GMT+2",-7200],["Etc/GMT+3",-10800],["Etc/GMT+4",-14400],["Etc/GMT+5",-18e3],["Etc/GMT+6",-21600],["Etc/GMT+7",-25200],["Etc/GMT+8",-28800],["Etc/GMT+9",-32400],["Etc/GMT-1",3600],["Etc/GMT-10",36e3],["Etc/GMT-11",39600],["Etc/GMT-12",43200],["Etc/GMT-13",46800],["Etc/GMT-14",50400],["Etc/GMT-2",7200],["Etc/GMT-3",10800],["Etc/GMT-4",14400],["Etc/GMT-5",18e3],["Etc/GMT-6",21600],["Etc/GMT-7",25200],["Etc/GMT-8",28800],["Etc/GMT-9",32400],["Etc/UTC",0],["Europe/Andorra",3600],["Europe/Astrakhan",14400],["Europe/Athens",7200],["Europe/Belgrade",3600],["Europe/Berlin",3600],["Europe/Brussels",3600],["Europe/Bucharest",7200],["Europe/Budapest",3600],["Europe/Chisinau",7200],["Europe/Dublin",3600],["Europe/Gibraltar",3600],["Europe/Helsinki",7200],["Europe/Istanbul",10800],["Europe/Kaliningrad",7200],["Europe/Kirov",10800],["Europe/Kyiv",7200],["Europe/Lisbon",0],["Europe/London",0],["Europe/Madrid",3600],["Europe/Malta",3600],["Europe/Minsk",10800],["Europe/Moscow",10800],["Europe/Paris",3600],["Europe/Prague",3600],["Europe/Riga",7200],["Europe/Rome",3600],["Europe/Samara",14400],["Europe/Saratov",14400],["Europe/Simferopol",10800],["Europe/Sofia",7200],["Europe/Tallinn",7200],["Europe/Tirane",3600],["Europe/Ulyanovsk",14400],["Europe/Vienna",3600],["Europe/Vilnius",7200],["Europe/Volgograd",10800],["Europe/Warsaw",3600],["Europe/Zurich",3600],["HST",-36e3],["Indian/Chagos",21600],["Indian/Maldives",18e3],["Indian/Mauritius",14400],["MET",3600],["MST",-25200],["MST7MDT",-25200],["PST8PDT",-28800],["Pacific/Apia",46800],["Pacific/Auckland",43200],["Pacific/Bougainville",39600],["Pacific/Chatham",45900],["Pacific/Easter",-21600],["Pacific/Efate",39600],["Pacific/Fakaofo",46800],["Pacific/Fiji",43200],["Pacific/Galapagos",-21600],["Pacific/Gambier",-32400],["Pacific/Guadalcanal",39600],["Pacific/Guam",36e3],["Pacific/Honolulu",-36e3],["Pacific/Kanton",46800],["Pacific/Kiritimati",50400],["Pacific/Kosrae",39600],["Pacific/Kwajalein",43200],["Pacific/Marquesas",-34200],["Pacific/Nauru",43200],["Pacific/Niue",-39600],["Pacific/Norfolk",39600],["Pacific/Noumea",39600],["Pacific/Pago_Pago",-39600],["Pacific/Palau",32400],["Pacific/Pitcairn",-28800],["Pacific/Port_Moresby",36e3],["Pacific/Rarotonga",-36e3],["Pacific/Tahiti",-36e3],["Pacific/Tarawa",43200],["Pacific/Tongatapu",46800],["WET",0]]);function Wl(){return Date.now()*1e3}function Jl(){return-new Date().getTimezoneOffset()}function Kl(){return new f(Intl.DateTimeFormat().resolvedOptions().timeZone)}function Vl(){return Math.floor(globalThis.performance.now()*1e3)}function Yl(e,t){let r=new Date((e+t)/1e3);return[[r.getUTCFullYear(),r.getUTCMonth()+1,r.getUTCDate()],[r.getUTCHours(),r.getUTCMinutes(),r.getUTCSeconds(),r.getUTCMilliseconds()]]}var Xi=class extends a{constructor(t,r,n,i){super(),this.wall_time=t,this.offset=r,this.timezone=n,this.monotonic_time=i}};function bo(e){{let t=e.wall_time;return lt(t,1e6)}}function Gr(e){return new Xi(e*1e6,0,new d,new d)}function jc(e,t){let r=e.wall_time,n=e.monotonic_time,i=t.wall_time,o=t.monotonic_time,u=(()=>{if(n instanceof f&&o instanceof f){let c=n[0],w=o[0];return[c,w]}else return[r,i]})(),l=u[0],p=u[1];return new Gn(l-p)}function Ec(e){return Ge(e===0,new m("Z"),()=>{let t=(()=>{let r=s([[e,new ot]]),n=Gl(r);return yo(n)})();if(t.hasLength(2)&&t.head[1]instanceof ut&&t.tail.head[1]instanceof xt){let r=t.head[0],n=t.tail.head[0],i=s([r>0?et(s(["+",(()=>{let p=A(r);return Oe(p,2,"0")})()])):et(s(["-",(()=>{let p=Ve(r),c=A(p);return Oe(c,2,"0")})()])),(()=>{let l=Ve(n),p=A(l);return Oe(p,2,"0")})()]),o=pt(i,":");return new m(o)}else if(t.hasLength(1)&&t.head[1]instanceof ut){let r=t.head[0],n=s([r>0?et(s(["+",(()=>{let l=A(r);return Oe(l,2,"0")})()])):et(s(["-",(()=>{let l=Ve(r),p=A(l);return Oe(p,2,"0")})()])),"00"]),i=pt(n,":");return new m(i)}else return new h(void 0)})}function Tc(e){{let t=e.wall_time,r=e.offset,n=Yl(t,r),i=n[0],o=n[1],u=Ec(r);if(!u.isOk())throw J("assignment_no_match","birl",1318,"to_parts","Assignment pattern did not match",{value:u});let l=u[0];return[i,o,l]}}function Qi(e){let t=Tc(e),r=t[0][0],n=t[0][1],i=t[0][2],o=t[1][0],u=t[1][1],l=t[1][2],p=t[1][3],c=t[2];return A(r)+"-"+(()=>{let x=A(n);return Oe(x,2,"0")})()+"-"+(()=>{let x=A(i);return Oe(x,2,"0")})()+"T"+(()=>{let x=A(o);return Oe(x,2,"0")})()+":"+(()=>{let x=A(u);return Oe(x,2,"0")})()+":"+(()=>{let x=A(l);return Oe(x,2,"0")})()+"."+(()=>{let x=A(p);return Oe(x,3,"0")})()+c}function es(){let e=Wl(),t=Jl(),r=Vl(),n=Kl();return new Xi(e,t*6e7,(()=>{let i=Vr(n,o=>zo(Hl,l=>l[0]===o)?new f(o):new d);return Lo(i)})(),new f(r))}var Sc=s([[new Dr,"year"],[new It,"month"],[new Ft,"week"],[new zt,"day"],[new ut,"hour"],[new xt,"minute"],[new Pt,"second"]]);function Zl(e,t){let r=(()=>{let n=jc(e,t);return Rl(n)})();if(r[1]instanceof ot)return"just now";if(r[1]instanceof Mt)return"just now";{let n=r[0],i=r[1],o=At(Sc,i);if(!o.isOk())throw J("assignment_no_match","birl",963,"legible_difference","Assignment pattern did not match",{value:o});let u=o[0],l=n<0,p=Ve(n),c=p===1?u:u+"s";return l?"in "+A(p)+" "+c:(()=>{let x=Ve(p);return A(x)})()+" "+c+" ago"}}function ts(e,t){return bo(e)-bo(t)<60?"<1 minute ago":Zl(e,t)}function rs(e){return ur(s([_("grid grid-cols-[18px,1fr] gap-2"),E("post-"+A(e.id))]),s([Yi(e.user_like_post,e.likes,new qr(e.id),""),M(s([_("flex flex-col gap-1")]),s([Pe(s([_("flex flex-col sm:flex-row sm:gap-2 sm:items-center")]),s([ne(s([_("text-[#584355] font-bold"),de((()=>{let t=e.href;return t instanceof f?t[0]:"/post/"+(()=>{let r=e.id;return A(r)})()})()),Ws("_blank")]),s([O(e.title)])),(()=>{let t=e.body;if(t instanceof f){let r=t[0];return ne(s([L("title",r),_("text-xs text-neutral-500 hidden sm:block")]),s([O("\u2636")]))}else return re()})(),Pe(s([_("flex gap-2 items-center")]),pe((()=>{let t=e.tags;return P(t,r=>Zi(r))})(),s([(()=>{let t=e.href;if(t instanceof f){let r=t[0];return ne(s([_("text-xs text-neutral-500 italic truncate max-w-[25ch] sm:max-w-[35ch] lg:max-w-[45ch]"),de(r),Ws("_blank")]),s([O(r)]))}else return re()})()])))])),Pe(s([_("flex items-center gap-2")]),s([he(s([_("text-neutral-500 text-xs")]),s([O((e.original_creator?"authored by":"via")+" "+e.username+" "),bi(s([L("datetime",(()=>{let t=e.created_at,r=Gr(t);return Qi(r)})())]),s([O(ts(es(),Gr(e.created_at)))]))])),ne(s([_("text-neutral-500 text-xs border-s border-s-neutral-400 ps-2"),de("/post/"+(()=>{let t=e.id;return A(t)})())]),s([O((()=>{let t=e.comments_count;return A(t)})()+(e.comments_count!==1?" comments":" comment"))]))]))]))]))}function Ql(e){return _e(s([E("latest")]),s([yi(s([_("flex flex-col gap-2")]),(()=>{let t=e.posts;return P(t,r=>rs(r))})())]))}function ea(e){return ve(U()+"/api/auth/login",ye(s([["email_username",ce(e.login_email_username)],["password",ce(e.login_password)]])),Y(Le(),t=>new Ar(t)))}function ta(e){return _e(s([_("flex flex-col mx-auto max-w-[450px] w-full gap-4"),E("login_form")]),s([He(s([_("text-[#584355] font-bold")]),s([O("Login")])),Te(s([_("grid gap-2 w-full"),Se(new kr)]),s([B(s([te("login_form:email_username")]),s([O("E-mail or Username")])),ie(s([ue(),oe(t=>new $t(t)),E("login_form:email_username"),F("text"),L("autocomplete","username"),mi(e.login_email_username)])),B(s([te("login_form:password")]),s([O("Password")])),ie(s([ue(),oe(t=>new ht(t)),E("login_form:password"),L("type","password"),L("autocomplete","current-password")])),M(s([_("flex justify-between items-center mt-2")]),s([V(s([se(),L("type","submit")]),s([O("Login")])),ne(s([de("/auth/forgot-password"),_("ms-auto text-[#584355]")]),s([O("Forgot Password")]))])),(()=>{let t=e.login_error;if(t instanceof f){let r=t[0];return he(s([_("text-red-500 text-center")]),s([O("Error: "+r)]))}else return re()})()]))]))}function ra(e){return Te(s([_("grid gap-2"),Se(new Pr)]),s([ki($(ue(),$(oe(t=>new Mr(t)),$(_("max-w-[60ch]"),e.auth_user instanceof d?s([Wu("You must be logged in to leave a comment"),_n(!0)]):s([])))),""),V(s([se(),e.auth_user instanceof d?_n(!0):Ru(),F("submit")]),s([b("Post")])),(()=>{let t=e.create_comment_error;if(t instanceof f){let r=t[0];return he(s([_("text-red-500 text-center")]),s([b("Error: "+r)]))}else return re()})()]))}function na(e,t,r){let i=Ye(t,o=>T(o.parent_id,r));return P(i,o=>ur(s([_("ps-[18px] flex flex-col gap-2 border-s border-s-neutral-200"),E("comment-"+A(o.id))]),$(M(s([_("flex flex-col gap-1")]),s([Pe(s([_("flex items-center gap-2 text-neutral-500 text-xs relative")]),s([Yi(o.user_like_post_comment,o.likes,new zr(o.id),"absolute -left-[18px] -translate-x-1/2 bg-white pb-1 translate-y-2 pt-3"),Pe(s([]),s([b(o.username+" "),bi(s([L("datetime",(()=>{let u=o.created_at,l=Gr(u);return Qi(l)})())]),s([b(ts(es(),Gr(o.created_at)))]))])),V(s([_("border-s border-s-neutral-400 ps-2"),We(new Nt(new f(o.id)))]),s([b("reply")]))])),he(s([_("pb-2")]),s([b(o.body)])),(()=>{let u=e.create_comment_parent_id;if(u instanceof f&&u[0]===o.id){let l=u[0];return ra(e)}else return re()})()])),na(e,t,new f(o.id)))))}function ia(e){let t=e.show_post;if(t instanceof f){let r=t[0];return _e(s([_("grid gap-4"),E("thread")]),s([rs(r),M(s([_("ps-[18px] grid gap-4 pb-4")]),s([(()=>{let n=r.body;if(n instanceof f){let i=n[0];return M(s([_("text-sm")]),s([b(i)]))}else return re()})(),e.create_comment_parent_id instanceof d?ra(e):V(s([_("text-xs text-neutral-500 me-auto"),F("button"),We(new Nt(new d))]),s([b("Reset reply")]))])),yi(s([E("comments")]),na(e,(()=>{let n=r.comments;return cs(n,(i,o)=>Yr(o.likes,i.likes))})(),new d))]))}else return M(s([]),s([b("Loading...")]))}function sa(e,t){return _e(s([_("flex flex-col mx-auto max-w-[450px] w-full gap-4"),E("signup_form")]),s([M(s([_("flex flex-col gap-2")]),s([He(s([_("text-[#584355] font-bold")]),s([O("Create your account")])),he(s([_("text-sm text-neutral-500")]),s([O("You have been invited by "+e.inviter)]))])),Te(s([_("grid gap-2 w-full"),Se(new yr)]),s([B(s([te("signup_form:username")]),s([O("Username")])),ie(s([ue(),oe(r=>new hr(r)),E("signup_form:username"),F("text"),L("autocomplete","username")])),B(s([te("signup_form:email")]),s([O("E-mail")])),ie(s([ue(),oe(r=>new wr(r)),E("signup_form:email"),F("email"),L("autocomplete","email")])),B(s([te("signup_form:password")]),s([O("Password")])),ie(s([ue(),oe(r=>new xr(r)),E("signup_form:password"),L("type","password"),L("autocomplete","new-password")])),V(s([se(),_("mx-auto"),L("type","submit")]),s([O("Sign up")])),(()=>{let r=e.sign_up_error;if(r instanceof f){let n=r[0];return he(s([_("text-red-500 text-center")]),s([O("Error: "+n)]))}else return re()})()]))]))}function oa(e){return _e(s([_("flex gap-2"),E("user_settings")]),s([V(s([se(),We(new vr),F("button")]),s([b("Logout")])),(()=>{let t=e.invite_link;if(t instanceof f){let r=t[0];return b(r)}else return V(s([se(),We(new Ir),F("button")]),s([b("Create Invite Link")]))})()]))}function Rn(){return window.location.pathname}function ua(e){window.history.replaceState({},null,e)}function la(e){navigator.clipboard.writeText(e)}function is(){let e=(()=>{let r=(()=>{let n=Rn();return ft(n)})();if(r.isOk())return r[0];throw J("panic","client",469,"get_route","Invalid uri",{})})(),t=(()=>{let r=e.path;return er(r)})();if(t.hasLength(0))return new An;if(t.hasLength(2)&&t.head==="auth"&&t.tail.head==="login")return new vn;if(t.hasLength(3)&&t.head==="auth"&&t.tail.head==="signup"){let r=t.tail.tail.head;return new _r(r)}else{if(t.hasLength(2)&&t.head==="auth"&&t.tail.head==="forgot-password")return new Ln;if(t.hasLength(3)&&t.head==="auth"&&t.tail.head==="forgot-password"){let r=t.tail.tail.head;return new mr(r)}else{if(t.hasLength(1)&&t.head==="create-post")return new Ct;if(t.hasLength(2)&&t.head==="user"){let r=t.tail.head;return new On(r)}else if(t.hasLength(2)&&t.head==="post"){let r=t.tail.head,n=kt(r);if(n.isOk()){let i=n[0];return new mt(i)}else return new $r}else return new $r}}}function aa(e){return ua(e.path),new jn(is())}function Cc(e){return ve(U()+"/api/auth/forgot-password",ye(s([["email",ce(e.login_email_username)]])),Y(Le(),t=>new Bn(t)))}function qc(){return ve(U()+"/api/auth-code",ye(s([])),Y(Le(),e=>new In(e)))}function pa(){let e=(()=>{let r=(()=>{let n=Rn();return ft(n)})();if(r.isOk())return r[0];throw J("panic","client",508,"get_auth_code","Invalid uri",{})})(),t=(()=>{let r=e.path;return er(r)})();return t.hasLength(3)&&t.head==="auth"&&t.tail.head==="signup"?t.tail.tail.head:"1"}function Nc(){let e=(()=>{let r=(()=>{let n=Rn();return ft(n)})();if(r.isOk())return r[0];throw J("panic","client",520,"get_forgot_password_token","Invalid uri",{})})(),t=(()=>{let r=e.path;return er(r)})();return t.hasLength(3)&&t.head==="auth"&&t.tail.head==="forgot-password"?t.tail.tail.head:"1"}function Mc(){let e=(()=>{let r=(()=>{let n=Rn();return ft(n)})();if(r.isOk())return r[0];throw J("panic","client",532,"get_post_id","Invalid uri",{})})(),t=(()=>{let r=e.path;return er(r)})();return t.hasLength(2)&&t.head==="post"?t.tail.head:""}function Pc(e){let t=U()+"/api/auth-code/"+e,r=xs(n=>new Dn(n),Ne("username",be));return St(t,Y(r,n=>new En(n)))}function zc(e){let t=U()+"/api/auth/forgot-password/"+e,r=xs(n=>new Dn(n),Ne("username",be));return St(t,Y(r,n=>new Un(n)))}function ns(){let e=U()+"/api/auth/validate",t=cu((r,n,i)=>new Vi(r,n,i),Ne("user_id",Yt),Ne("username",be),Ne("is_admin",Zt));return St(e,Y(t,r=>new Tn(r)))}function Fc(){let e=Xt(l=>p=>c=>w=>x=>g=>v=>new Ui(l,p,c,w,x,g,v)),t=N(e,"id",De),r=N(t,"body",Me),n=N(r,"username",Me),i=N(n,"likes",De),o=N(i,"user_like_post_comment",ai),u=N(o,"parent_id",pi(De));return N(u,"created_at",De)}function ca(){let e=Xt(g=>v=>C=>Q=>R=>D=>j=>y=>S=>H=>Ce=>Hr=>new Bi(g,v,C,Q,R,D,j,y,S,H,Ce,Hr)),t=N(e,"id",De),r=N(t,"title",Me),n=N(r,"href",pi(Me)),i=N(n,"body",pi(Me)),o=N(i,"likes",De),u=N(o,"user_like_post",ai),l=N(u,"comments_count",De),p=N(l,"comments",an(Fc())),c=N(p,"tags",an(Me)),w=N(c,"username",Me),x=N(w,"original_creator",ai);return N(x,"created_at",De)}function gt(){let e=U()+"/api/posts/"+Mc();return St(e,Y(t=>ln(ca(),t),t=>new Cn(t)))}function Rr(){let e=U()+"/api/posts",t=(()=>{let r=Xt(n=>new Ji(n));return N(r,"posts",an(ca()))})();return St(e,Y(r=>ln(t,r),r=>new Sn(r)))}function Ic(e){return[new Hi(is(),"",new d,"","","",new d,"","",new d,"","","",!1,!1,s([]),new d,s([]),new d,"",new d,new d,s([]),new d,new d,""),rr((()=>{let t=s([Tl(aa),ns(),Rr()]);return pe(t,(()=>{let r=is();if(r instanceof mt)return s([gt()]);if(r instanceof _r)return s([Pc(pa())]);if(r instanceof mr){let n=r.token;return s([zc(n)])}else return s([])})())})())]}function Bc(){let e=Xt(i=>o=>u=>l=>new Gi(i,o,Cl(u),Nl(l))),t=N(e,"id",De),r=N(t,"name",Me),n=N(r,"category",Me);return N(n,"permission",Me)}function Uc(){let e=U()+"/api/tags",t=(()=>{let r=Xt(n=>new Ki(n));return N(r,"tags",an(Bc()))})();return St(e,Y(r=>ln(t,r),r=>new qn(r)))}function Dc(e){return ve(U()+"/api/users",ye(s([["username",ce(e.sign_up_username)],["email",ce(e.sign_up_email)],["password",ce(e.sign_up_password)],["auth_code",ce(pa())]])),Y(Le(),t=>new Nn(t)))}function Gc(e){return ve(U()+"/api/auth/logout",ye(s([])),Y(Le(),t=>new Mn(t)))}function Rc(e){return ve(U()+"/api/posts",ye(s([["title",ce(e.create_post_title)],["original_creator",Gs(e.create_post_original_creator)],e.create_post_use_body?["body",ce(e.create_post_body)]:["href",ce(e.create_post_href)],["tags",Hs(e.create_post_tags,t=>pn(t))]])),Y(Le(),t=>new Pn(t)))}function Hc(e){return ve(U()+"/api/auth/change-password/"+Nc(),ye(s([["password",ce(e.login_password)]])),Y(Le(),t=>new Wi(t)))}function Wc(e){let t=(()=>{let r=e.show_post;if(r instanceof f){let i=r[0].id;return A(i)}else throw J("panic","client",769,"create_comment","Invalid state",{})})();return ve(U()+"/api/posts/"+t+"/comments",ye(s([["body",ce(e.create_comment_body)],["parent_id",(()=>{let r=e.create_comment_parent_id;if(r instanceof f){let n=r[0];return pn(n)}else return Nu()})()]])),Y(Le(),r=>new Fn(r)))}function Jc(e,t){if(t instanceof jn){let r=t[0];return[e.withFields({route:r,show_post:r instanceof mt?new d:e.show_post}),r instanceof mt?gt():r instanceof Ct?e.tags.hasLength(0)?Uc():k():k()]}else if(t instanceof En){let r=t[0];if(r.isOk()){let n=r[0];return[e.withFields({inviter:n.username}),k()]}else return[e,k()]}else if(t instanceof Tn){let r=t[0];if(r.isOk()){let n=r[0];return[e.withFields({auth_user:new f(n)}),(()=>{if(is()instanceof Ct){let o=ft("/create-post");if(o.isOk()){let u=o[0];return fe(l=>{let p=aa(u);return l(p)})}else return k()}else return k()})()]}else return[e,k()]}else if(t instanceof Sn){let r=t[0];if(r.isOk()){let n=r[0];return[e.withFields({posts:n.posts}),k()]}else return[e,k()]}else if(t instanceof Cn){let r=t[0];if(r.isOk()){let n=r[0];return[e.withFields({show_post:new f(n)}),k()]}else return[e,k()]}else if(t instanceof qn){let r=t[0];if(r.isOk()){let n=r[0];return[e.withFields({tags:n.tags}),k()]}else return[e,k()]}else if(t instanceof hr){let r=t.value;return[e.withFields({sign_up_username:r}),k()]}else if(t instanceof wr){let r=t.value;return[e.withFields({sign_up_email:r}),k()]}else if(t instanceof xr){let r=t.value;return[e.withFields({sign_up_password:r}),k()]}else if(t instanceof gr){let r=t.value;return[e.withFields({sign_up_error:r}),k()]}else{if(t instanceof yr)return[e,Dc(e)];if(t instanceof Nn){let r=t.resp_result;if(r.isOk()){let i=r[0].error;if(i instanceof f){let o=i[0];return[e,fe(u=>u(new gr(new f(o))))]}else return[e.withFields({sign_up_username:"",sign_up_email:"",sign_up_password:"",sign_up_error:new d}),rr(s([dr("/",new d,new d),ns(),gt(),Rr()]))]}else return[e,fe(n=>n(new gr(new f("HTTP Error"))))]}else if(t instanceof $t){let r=t.value;return[e.withFields({login_email_username:r}),k()]}else if(t instanceof ht){let r=t.value;return[e.withFields({login_password:r}),k()]}else if(t instanceof br){let r=t.value;return[e.withFields({login_error:r}),k()]}else{if(t instanceof kr)return[e,ea(e)];if(t instanceof Ar){let r=t.resp_result;if(r.isOk()){let i=r[0].error;if(i instanceof f){let o=i[0];return[e,fe(u=>u(new br(new f(o))))]}else return[e.withFields({login_email_username:"",login_password:"",login_error:new d}),rr(s([dr("/",new d,new d),ns(),Rr(),gt()]))]}else return[e,fe(n=>n(new br(new f("HTTP Error"))))]}else{if(t instanceof vr)return[e,Gc(e)];if(t instanceof Mn)return[e.withFields({auth_user:new d}),rr(s([dr("/",new d,new d),Rr(),gt(),ns()]))];if(t instanceof Lr){let r=t.value;return[e.withFields({create_post_title:r}),k()]}else if(t instanceof Or){let r=t.value;return[e.withFields({create_post_href:r}),k()]}else if(t instanceof jr){let r=t.value;return[e.withFields({create_post_body:r}),k()]}else if(t instanceof Er){let r=t.value;return[e.withFields({create_post_original_creator:r}),k()]}else if(t instanceof Sr){let r=t.tag_id;return[e.withFields({create_post_tags:Gt(e.create_post_tags,r)?Ye(e.create_post_tags,i=>i!==r):$(r,e.create_post_tags)}),k()]}else if(t instanceof Tr){let r=t.value;return[e.withFields({create_post_use_body:r,create_post_href:""}),k()]}else if(t instanceof qt){let r=t.value;return[e.withFields({create_post_error:r}),k()]}else{if(t instanceof Cr)return[e,Rc(e)];if(t instanceof Pn){let r=t.resp_result;if(r.isOk()){let i=r[0].error;if(i instanceof f){let o=i[0];return[e,fe(u=>u(new qt(new f(o))))]}else return[e.withFields({create_post_title:"",create_post_href:"",create_post_tags:s([]),create_post_error:new d}),rr(s([dr("/",new d,new d),Rr()]))]}else return[e,fe(n=>n(new qt(new f("HTTP Error"))))]}else if(t instanceof qr){let r=t.post_id;return[e,zl(r)]}else{if(t instanceof Nr)return[e,e.show_post instanceof f?gt():Rr()];if(t instanceof Mr){let r=t.value;return[e.withFields({create_comment_body:r}),k()]}else if(t instanceof Nt){let r=t.value;return[e.withFields({create_comment_parent_id:r}),k()]}else if(t instanceof zn){let r=t.value;return[e.withFields({create_comment_error:r}),k()]}else{if(t instanceof Pr)return[e,Wc(e)];if(t instanceof Fn){let r=t.resp_result;if(r.isOk()){let i=r[0].error;if(i instanceof f){let o=i[0];return[e,fe(u=>u(new zn(new f(o))))]}else return[e.withFields({create_comment_parent_id:new d,create_comment_body:"",create_comment_error:new d}),gt()]}else return[e,fe(n=>n(new qt(new f("HTTP Error"))))]}else if(t instanceof zr){let r=t.post_comment_id;return[e,Fl(r)]}else{if(t instanceof Fr)return[e,gt()];if(t instanceof Ir)return[e,qc()];if(t instanceof In){let r=t.resp_result;if(r.isOk()){let i=r[0].message;if(i instanceof f){let o=i[0];return la(U()+"/auth/signup/"+o),[e.withFields({invite_link:new f(U()+"/auth/signup/"+o)}),k()]}else return[e,k()]}else return[e,k()]}else{if(t instanceof Br)return[e,Cc(e)];if(t instanceof Bn){let r=t.resp_result;if(r.isOk()){let n=r[0];return[e.withFields({login_email_username:"",forgot_password_response:(()=>{let i=n.message;if(i instanceof f){let o=i[0];return new f(new m(o))}else{let o=n.error;if(o instanceof f){let u=o[0];return new f(new h(u))}else return new d}})()}),k()]}else return[e.withFields({forgot_password_response:new f(new h("HTTP Error"))}),k()]}else if(t instanceof Un){let r=t[0];if(r.isOk()){let n=r[0];return[e.withFields({change_password_target:n.username}),k()]}else return[e,k()]}else return t instanceof Ur?[e,Hc(e)]:t.resp_result.isOk()?[e.withFields({login_password:""}),dr("/auth/login",new d,new d)]:[e.withFields({forgot_password_response:new f(new h("HTTP Error"))}),k()]}}}}}}}}}function Kc(e){return rl(s([_("bg-[#fefefc] text-[#151515] w-[100vw] min-h-[100vh] h-[100vh] px-4 max-w-[800px] py-4 mx-auto flex flex-col h-screen gap-4"),E("app")]),s([ol(s([_("text-sm font-bold text-neutral-700 h-[28px] flex justify-between items-center")]),s([ne(s([de("/"),_("flex group gap-2 items-center")]),s([ll(s([Ju("https://gleam.run/images/lucy/lucy.svg"),Ku("Lucy"),_("size-[18px] group-hover:animate-wiggle")])),he(s([_("group-hover:underline")]),s([O("Latest")]))])),(()=>{let t=e.auth_user;if(t instanceof d)return ne(s([de("/auth/login"),_("hover:underline")]),s([O("Login")]));{let r=t[0];return Pe(s([_("flex gap-2 items-center")]),s([ne(s([_("font-normal"),se(),_("hover:bg-[#584355]/80"),de("/create-post")]),s([O("Post")])),ne(s([_("hover:underline"),de("/user/"+r.username)]),s([O(r.username)]))]))}})()])),sl(s([_("mb-auto")]),s([(()=>{let t=e.route,r=e.auth_user;if(t instanceof An)return Ql(e);if(t instanceof vn)return ta(e);if(t instanceof _r){let n=t.auth_code;return sa(e,n)}else return t instanceof Ln?Ul(e):t instanceof mr?Il(e):t instanceof Ct&&r instanceof f?Bl(e):t instanceof mt?ia(e):t instanceof On&&r instanceof f?oa(e):t instanceof $r?O("404 Not found"):O("404 Not found")})()])),nl(s([_("text-center text-neutral-500 text-xs pb-2")]),s([O("Made with <3 by the community, contribute on "),ne(s([de("https://github.com/dinkelspiel/kirakira"),_("hover:underline text-neutral-700 font-bold")]),s([O("GitHub")]))]))]))}function fa(){let e=Xu(Ic,Jc,Kc);return Qu(e,"#app",void 0)}fa(); diff --git a/apps/my-app/server/shell.nix b/apps/my-app/server/shell.nix @@ -0,0 +1,22 @@ +let + # pkgs = import <nixpkgs> {}; + # pkgs = import (builtins.fetchTarball { + # name = "nixos-24.05"; + # url = "https://github.com/nixos/nixpkgs/archive/63dacb46bf939521bdc93981b4cbb7ecb58427a0.tar.gz"; + # sha256 = "1lr1h35prqkd1mkmzriwlpvxcb34kmhc9dnr48gkm8hh089hifmx"; + # }) {}; + pkgs = import (builtins.fetchTarball { + name = "nixos-unstable"; + url = "https://github.com/nixos/nixpkgs/archive/896d74a4f75c23bca045a48ea7d0c1f16f604943.tar.gz"; + sha256 = "1j9rhjql4qjm9xky90m0chdcjhkm3dsijqr0kc6n3xaw7f21wgj4"; + }) {}; +in pkgs.mkShell { + buildInputs = [ + pkgs.gleam + pkgs.erlang_27 + pkgs.rebar3 + pkgs.inotify-tools + pkgs.nodejs_22 + ]; + # imports = ["./test.nix"]; +} diff --git a/apps/my-app/server/src/server.gleam b/apps/my-app/server/src/server.gleam @@ -0,0 +1,30 @@ +import gleam/erlang/process +import mist +import server/db +import server/router +import server/web +import wisp + +pub fn main() { + // This sets the logger to print INFO level logs, and other sensible defaults + // for a web application. + wisp.configure_logger() + + // // Here we generate a secret key, but in a real application you would want to + // // load this from somewhere so that it is not regenerated on every restart. + // let secret_key_base = wisp.random_string(64) + + let ctx = web.Context(connection: db.get_connection()) + + // Start the Mist web server. + let assert Ok(_) = + router.handle_request(_, ctx) + // wisp.mist_handler(router.handle_request(_, ctx), secret_key_base) + |> mist.new + |> mist.port(8001) + |> mist.start_http + + // The web server runs in new Erlang process, so put this one to sleep while + // it works concurrently. + process.sleep_forever() +} diff --git a/apps/my-app/server/src/server/db.gleam b/apps/my-app/server/src/server/db.gleam @@ -0,0 +1,21 @@ +import gleam/option.{Some} +import gleam/pgo +import server/env.{get_env} + +pub fn get_connection() { + let env = get_env() + + let connection = + pgo.connect( + pgo.Config( + ..pgo.default_config(), + host: env.db_host, + port: env.db_port, + user: env.db_user, + password: Some(env.db_password), + database: env.db_name, + ssl: True, + ), + ) + connection +} diff --git a/apps/my-app/server/src/server/db/datetime_helper.gleam b/apps/my-app/server/src/server/db/datetime_helper.gleam @@ -0,0 +1,31 @@ +import birl.{type Time} +import gleam/dynamic.{type DecodeError, type Dynamic} +import gleam/float +import gleam/result + +// pub fn convert_time(time: Time) { +// time +// |> birl.to_erlang_universal_datetime() +// |> dynamic.from() +// // |> dynamic.unsafe_coerce() +// } + +pub fn decode_time(data: Dynamic) -> Result(Time, List(DecodeError)) { + data + |> dynamic.tuple2(decode_time_tuple, decode_time_tuple) + |> result.map(birl.from_erlang_universal_datetime) +} + +fn decode_time_tuple(data: Dynamic) { + dynamic.tuple3( + dynamic.int, + dynamic.int, + dynamic.any([dynamic.int, rounded_float]), + )(data) +} + +fn rounded_float(data: Dynamic) { + data + |> dynamic.float() + |> result.map(float.round) +} diff --git a/apps/my-app/server/src/server/db/login.gleam b/apps/my-app/server/src/server/db/login.gleam @@ -0,0 +1,68 @@ +import gleam/dynamic +import gleam/pgo + +pub fn create_user( + db: pgo.Connection, + username: String, + email: String, + password: String, +) -> Result(Nil, pgo.QueryError) { + // ) -> Result(String, pgo.QueryError) { + let query = + " + INSERT INTO twitter.users (username, email, password_hash) + VALUES ($1, $2, CRYPT($3, GEN_SALT('md5'))); + " + let response = + pgo.execute( + query, + db, + [pgo.text(username), pgo.text(email), pgo.text(password)], + dynamic.element(0, dynamic.string), + ) + case response { + Ok(pgo.Returned(1, [])) -> Ok(Nil) + Error(e) -> Error(e) + _ -> panic + } +} + +pub type LoginError { + IncorrectPassword + UserNotFound(user: String) + QueryError(pgo.QueryError) +} + +pub fn log_in( + db: pgo.Connection, + username: String, + password: String, +) -> Result(Nil, LoginError) { + let query = + " + SELECT (password_hash = CRYPT($2, password_hash)) AS password_matches + FROM twitter.users + WHERE username = $1; + " + let response = + pgo.execute( + query, + db, + [pgo.text(username), pgo.text(password)], + dynamic.element(0, dynamic.bool), + ) + + case response { + Ok(returned) -> + case returned { + pgo.Returned(1, [result]) -> + case result { + True -> Ok(Nil) + False -> Error(IncorrectPassword) + } + pgo.Returned(0, []) -> Error(UserNotFound(username)) + _ -> panic + } + Error(e) -> Error(QueryError(e)) + } +} diff --git a/apps/my-app/server/src/server/db/post.gleam b/apps/my-app/server/src/server/db/post.gleam @@ -0,0 +1,30 @@ +import gleam/dynamic.{type DecodeError, type Dynamic} +import gleam/pgo +import gleam/result +import server/db/datetime_helper +import shared + +fn decode_post(data: Dynamic) -> Result(shared.Post, List(DecodeError)) { + data + |> dynamic.tuple4( + dynamic.int, + dynamic.int, + dynamic.string, + datetime_helper.decode_time, + ) + |> result.map(fn(x) { + shared.Post(id: x.0, user_id: x.1, content: x.2, created_at: x.3) + }) +} + +pub fn posts( + db: pgo.Connection, +) -> Result(pgo.Returned(shared.Post), pgo.QueryError) { + let query = + " + SELECT id, user_id, content, created_at + FROM twitter.tweets + ORDER BY created_at DESC; + " + pgo.execute(query, db, [], decode_post) +} diff --git a/apps/my-app/server/src/server/env.gleam b/apps/my-app/server/src/server/env.gleam @@ -0,0 +1,42 @@ +import decode +import glenv + +pub type Env { + Env( + db_host: String, + db_password: String, + db_port: Int, + db_user: String, + db_name: String, + ) +} + +pub fn get_env() { + let definitions = [ + #("PGHOST", glenv.String), + #("PGPASSWORD", glenv.String), + #("PGPORT", glenv.Int), + #("PGUSER", glenv.String), + #("PGDB", glenv.String), + ] + + let decoder = + decode.into({ + use db_host <- decode.parameter + use db_password <- decode.parameter + use db_port <- decode.parameter + use db_user <- decode.parameter + use db_name <- decode.parameter + + Env(db_host, db_password, db_port, db_user, db_name) + }) + |> decode.field("PGHOST", decode.string) + |> decode.field("PGPASSWORD", decode.string) + |> decode.field("PGPORT", decode.int) + |> decode.field("PGUSER", decode.string) + |> decode.field("PGDB", decode.string) + + let assert Ok(env) = glenv.load(decoder, definitions) + + env +} diff --git a/apps/my-app/server/src/server/response.gleam b/apps/my-app/server/src/server/response.gleam @@ -0,0 +1,23 @@ +import gleam/json +import gleam/string_builder.{type StringBuilder} +import wisp + +pub fn generate_wisp_response(result: Result(StringBuilder, String)) { + case result { + Ok(json) -> wisp.json_response(json, 200) + Error(error) -> + wisp.json_response( + json.object([#("error", json.string(error))]) + |> json.to_string_builder, + 200, + ) + } +} + +pub fn error(error: String) { + wisp.json_response( + json.object([#("error", json.string(error))]) + |> json.to_string_builder, + 400, + ) +} diff --git a/apps/my-app/server/src/server/router.gleam b/apps/my-app/server/src/server/router.gleam @@ -0,0 +1,340 @@ +import gleam/list +import gleam/pgo +import server/routes/posts +import server/web + +import gleam/bytes_builder +import gleam/http/request.{type Request} +import gleam/http/response.{type Response} +import gleam/int +import gleam/string_builder +import lustre/attribute.{attribute, class, href, id, name, rel, src} +import lustre/element.{type Element, text} +import lustre/element/html.{ + a, body, div, footer, head, html, img, li, link, meta, nav, ol, p, script, + section, span, title, +} +import mist.{type Connection, type ResponseData} +import server/db/post +import shared + +pub type Model { + Model(posts: List(shared.Post)) +} + +pub type Message { + LoadPosts +} + +pub fn handle_request( + req: Request(Connection), + ctx: web.Context, +) -> Response(ResponseData) { + // use req <- web.middleware(req) + + // note assets under /static are caught by web.middleware before this + let segments = request.path_segments(req) + case segments { + [] | ["posts"] -> { + let posts = case post.posts(ctx.connection) { + Ok(returned) -> + case returned { + pgo.Returned(_count, rows) -> rows + } + Error(e) -> + case e { + pgo.ConstraintViolated(message, _constraint, _detail) -> + panic as message + pgo.PostgresqlError(_code, _name, message) -> panic as message + // UnexpectedArgumentCount(expected, got) -> panic as + // UnexpectedArgumentType(expected, got) + pgo.UnexpectedResultType(_err) -> panic as "unexpected result" + pgo.ConnectionUnavailable -> panic as "connection unavailable" + _ -> panic + } + } + let model = Model(posts:) + // let html = + // html([], [ + // html.head([], [ + // html.script([attribute.type_("module"), attribute.src("...")], ""), + // html.script( + // [attribute.type_("application/json"), attribute.id("model")], + // json.int(model) + // |> json.to_string, + // ), + // ]), + // html.body([], [html.div([attribute.id("app")], [counter.view(model)])]), + // ]) + response.new(200) + |> response.set_header("Content-Type", "text/html") + |> response.set_body( + view(model) + |> page_scaffold + |> element.to_document_string_builder() + |> string_builder.to_string + |> bytes_builder.from_string + |> mist.Bytes, + ) + } + ["api", "posts"] -> posts.posts(req, ctx) + // ["api", ..] -> wisp.not_found() + // _ -> page_routes(req, wisp.path_segments(req), ctx) + _ -> { + let empty_body = mist.Bytes(bytes_builder.new()) + let not_found = response.new(404) |> response.set_body(empty_body) + not_found + } + } +} + +pub fn page_scaffold(content: element.Element(a)) { + html([], [ + head([], [ + meta([attribute("charset", "UTF-8")]), + meta([ + attribute("content", "width=device-width, initial-scale=1.0"), + name("viewport"), + ]), + title([], "📋 Kirakira"), + meta([ + attribute("content", "A forum made in Gleam for the Gleam community"), + name("description"), + ]), + meta([attribute("content", "max-image-preview:large"), name("robots")]), + meta([attribute("content", "en_US"), attribute("property", "og:locale")]), + meta([ + attribute("content", "📋 Kirakira"), + attribute("property", "og:site_name"), + ]), + meta([attribute("content", "website"), attribute("property", "og:type")]), + meta([ + attribute("content", "📋 Kirakira"), + attribute("property", "og:title"), + ]), + meta([ + attribute("content", "A forum made in Gleam for the Gleam community"), + attribute("property", "og:description"), + ]), + meta([ + attribute("content", "https://kirakira.keii.dev/"), + attribute("property", "og:url"), + ]), + meta([attribute("content", "summary"), name("twitter:card")]), + meta([attribute("content", "📋 Kirakira"), name("twitter:title")]), + meta([ + attribute("content", "A forum made in Gleam for the Gleam community"), + name("twitter:description"), + ]), + link([ + href("/priv/static/favicon.ico"), + attribute.type_("image/x-icon"), + rel("icon"), + ]), + link([href("/static/client.min.css"), rel("stylesheet")]), + script([src("/static/client.min.mjs"), attribute.type_("module")], ""), + script( + [ + src("https://plausible.keii.dev/js/script.js"), + attribute("data-domain", "kirakira.keii.dev"), + attribute("defer", ""), + ], + "", + ), + ]), + content, + ]) +} + +pub fn view(model: Model) -> Element(Message) { + body( + [ + class( + "bg-[#fefefc] text-[#151515] w-[100vw] min-h-[100vh] h-[100vh] px-4 max-w-[800px] py-4 mx-auto flex flex-col h-screen gap-4", + ), + id("app"), + ], + [ + // title([], "Hello World"), + nav( + [ + class( + "text-sm font-bold text-neutral-700 h-[28px] flex justify-between items-center", + ), + ], + [ + a([href("/"), class("flex group gap-2 items-center")], [ + // img([ + // src("https://gleam.run/images/lucy/lucy.svg"), + // attribute.alt("Lucy"), + // class("size-[18px] group-hover:animate-wiggle"), + // ]), + p([class("group-hover:underline")], [text("Latest")]), + ]), + ], + ), + html.main([class("mb-auto")], [ + view_posts(model), + // case model.route, model.auth_user { + // Active, _ -> latest_view(model) + // Login, _ -> login_view(model) + // Signup(auth_code), _ -> signup_view(model, auth_code) + // ForgotPassword, _ -> forgot_password(model) + // ChangePassword(_), _ -> change_password(model) + // CreatePost, Some(_) -> create_post_view(model) + // ShowPost(_), _ -> show_post_view(model) + // UserPage(_), Some(_) -> user_view(model) + // NotFound, _ -> text("404 Not found") + // _, _ -> text("404 Not found") + // }, + ]), + footer([class("text-center text-neutral-500 text-xs pb-2")], [ + text("Made with <3 by the community, contribute on "), + a( + [ + href("https://github.com/dinkelspiel/kirakira"), + class("hover:underline text-neutral-700 font-bold"), + ], + [text("GitHub")], + ), + ]), + ], + ) +} + +fn view_post(post: shared.Post) { + li( + [ + class("grid grid-cols-[18px,1fr] gap-2"), + attribute.id("post-" <> int.to_string(post.id)), + ], + [ + div([class("flex flex-col gap-1")], [ + span([class("flex flex-col sm:flex-row sm:gap-2 sm:items-center")], [ + // a( + // [ + // class("text-[#584355] font-bold"), + // // href(case post.href { + // // Some(post_href) -> post_href + // // None -> "/post/" <> { post.id |> int.to_string } + // // }), + // // target("_blank"), + // ], + // [text(post.)], + // ), + a( + [ + attribute.attribute("title", post.content), + class("text-xs text-neutral-500 hidden sm:block"), + ], + [text("☶"), text(post.content)], + ), + ]), + ]), + ], + ) + // div([class("ps-[18px] grid gap-4 pb-4")], [ + // div([class("text-sm")], [text(body)]), + // create_comment_view(model), + // button( + // [ + // class("text-xs text-neutral-500 me-auto"), + // attribute.type_("button"), + // event.on_click(CreateCommentUpdateParentId(None)), + // ], + // [text("Reset reply")], + // ) + // ]), + // ol( + // [attribute.id("comments")], + // comments_view( + // model, + // post.comments + // |> list.sort(fn(a, b) { int.compare(b.likes, a.likes) }), + // None, + // ), + // ), +} + +pub fn view_posts(model: Model) { + section( + [class("grid gap-4"), attribute.id("thread")], + model.posts |> list.map(view_post), + ) +} +// pub fn view(model: Model) -> Element(Msg) { +// body( +// [ +// class( +// "bg-[#fefefc] text-[#151515] w-[100vw] min-h-[100vh] h-[100vh] px-4 max-w-[800px] py-4 mx-auto flex flex-col h-screen gap-4", +// ), +// id("app"), +// ], +// [ +// nav( +// [ +// class( +// "text-sm font-bold text-neutral-700 h-[28px] flex justify-between items-center", +// ), +// ], +// [ +// a([href("/"), class("flex group gap-2 items-center")], [ +// img([ +// src("https://gleam.run/images/lucy/lucy.svg"), +// attribute.alt("Lucy"), +// class("size-[18px] group-hover:animate-wiggle"), +// ]), +// p([class("group-hover:underline")], [text("Latest")]), +// ]), +// case model.auth_user { +// None -> +// a([href("/auth/login"), class("hover:underline")], [text("Login")]) +// Some(auth_user) -> +// span([class("flex gap-2 items-center")], [ +// a( +// [ +// class("font-normal"), +// button_class(), +// class("hover:bg-[#584355]/80"), +// href("/create-post"), +// ], +// [text("Post")], +// ), +// a( +// [ +// class("hover:underline"), +// href("/user/" <> auth_user.username), +// ], +// [text(auth_user.username)], +// ), +// ]) +// }, +// ], +// ), +// html.main([class("mb-auto")], [ +// case model.route, model.auth_user { +// Active, _ -> latest_view(model) +// Login, _ -> login_view(model) +// Signup(auth_code), _ -> signup_view(model, auth_code) +// ForgotPassword, _ -> forgot_password(model) +// ChangePassword(_), _ -> change_password(model) +// CreatePost, Some(_) -> create_post_view(model) +// ShowPost(_), _ -> show_post_view(model) +// UserPage(_), Some(_) -> user_view(model) +// NotFound, _ -> text("404 Not found") +// _, _ -> text("404 Not found") +// }, +// ]), +// footer([class("text-center text-neutral-500 text-xs pb-2")], [ +// text("Made with <3 by the community, contribute on "), +// a( +// [ +// href("https://github.com/dinkelspiel/kirakira"), +// class("hover:underline text-neutral-700 font-bold"), +// ], +// [text("GitHub")], +// ), +// ]), +// ], +// ) +// } diff --git a/apps/my-app/server/src/server/routes/posts.gleam b/apps/my-app/server/src/server/routes/posts.gleam @@ -0,0 +1,70 @@ +import birl +import gleam/bytes_builder +import gleam/http.{Get} +import gleam/json.{type Json} +import gleam/pgo +import gleam/result +import gleam/string_builder +import server/db/post +import server/web +import shared + +// import wisp.{type Request, type Response} +import gleam/http/request.{type Request} + +import gleam/http/response.{type Response} +import mist.{type Connection, type ResponseData} + +pub fn posts( + req: Request(Connection), + ctx: web.Context, +) -> Response(ResponseData) { + case req.method { + Get -> list_posts(req, ctx) + _ -> panic + // _ -> wisp.method_not_allowed([Get]) + // Post -> create_post(req, ctx) + // _ -> wisp.method_not_allowed([Get, Post]) + } +} + +pub fn list_posts( + _req: Request(Connection), + ctx: web.Context, +) -> Response(ResponseData) { + let posts = post.posts(ctx.connection) + + let result = case posts { + Ok(rows) -> + case rows { + pgo.Returned(_count, rows) -> + Ok( + json.object([ + #( + "posts", + rows + |> json.array(fn(post) { post_to_json(post) }), + ), + ]) + |> json.to_string_builder + |> string_builder.to_string, + ) + // _ -> Error("Unknown error occurred.") + } + Error(_error) -> Error("Query error!") + } + response.new(200) + |> response.set_body( + result |> result.unwrap_both() |> bytes_builder.from_string |> mist.Bytes, + ) + // response.generate_wisp_response(result) +} + +fn post_to_json(post: shared.Post) -> Json { + json.object([ + #("id", json.int(post.id)), + #("title", json.int(post.user_id)), + #("content", json.string(post.content)), + #("created_at", json.string(birl.to_time_string(post.created_at))), + ]) +} diff --git a/apps/my-app/server/src/server/scaffold.gleam b/apps/my-app/server/src/server/scaffold.gleam @@ -0,0 +1,61 @@ +import lustre/attribute.{attribute, href, name, rel, src} +import lustre/element +import lustre/element/html.{head, html, link, meta, script, title} + +pub fn page_scaffold(content: element.Element(a)) { + html([], [ + head([], [ + meta([attribute("charset", "UTF-8")]), + meta([ + attribute("content", "width=device-width, initial-scale=1.0"), + name("viewport"), + ]), + title([], "📋 Kirakira"), + meta([ + attribute("content", "A forum made in Gleam for the Gleam community"), + name("description"), + ]), + meta([attribute("content", "max-image-preview:large"), name("robots")]), + meta([attribute("content", "en_US"), attribute("property", "og:locale")]), + meta([ + attribute("content", "📋 Kirakira"), + attribute("property", "og:site_name"), + ]), + meta([attribute("content", "website"), attribute("property", "og:type")]), + meta([ + attribute("content", "📋 Kirakira"), + attribute("property", "og:title"), + ]), + meta([ + attribute("content", "A forum made in Gleam for the Gleam community"), + attribute("property", "og:description"), + ]), + meta([ + attribute("content", "https://kirakira.keii.dev/"), + attribute("property", "og:url"), + ]), + meta([attribute("content", "summary"), name("twitter:card")]), + meta([attribute("content", "📋 Kirakira"), name("twitter:title")]), + meta([ + attribute("content", "A forum made in Gleam for the Gleam community"), + name("twitter:description"), + ]), + link([ + href("/priv/static/favicon.ico"), + attribute.type_("image/x-icon"), + rel("icon"), + ]), + link([href("/static/client.min.css"), rel("stylesheet")]), + script([src("/static/client.min.mjs"), attribute.type_("module")], ""), + script( + [ + src("https://plausible.keii.dev/js/script.js"), + attribute("data-domain", "kirakira.keii.dev"), + attribute("defer", ""), + ], + "", + ), + ]), + content, + ]) +} diff --git a/apps/my-app/server/src/server/web.gleam b/apps/my-app/server/src/server/web.gleam @@ -0,0 +1,43 @@ +import gleam/pgo + +pub type Context { + Context(connection: pgo.Connection) +} +// /// The middleware stack that the request handler uses. The stack is itself a +// /// middleware function! +// /// +// /// Middleware wrap each other, so the request travels through the stack from +// /// top to bottom until it reaches the request handler, at which point the +// /// response travels back up through the stack. +// /// +// /// The middleware used here are the ones that are suitable for use in your +// /// typical web application. +// /// +// pub fn middleware( +// req: wisp.Request, +// handle_request: fn(wisp.Request) -> wisp.Response, +// ) -> wisp.Response { +// // Permit browsers to simulate methods other than GET and POST using the +// // `_method` query parameter. +// let req = wisp.method_override(req) +// +// // Log information about the request and response. +// use <- wisp.log_request(req) +// +// // Return a default 500 response if the request handler crashes. +// use <- wisp.rescue_crashes +// +// // Rewrite HEAD requests to GET requests and return an empty body. +// use req <- wisp.handle_head(req) +// +// // Serve build output site files +// let assert Ok(priv_directory) = wisp.priv_directory("server") +// use <- wisp.serve_static( +// req, +// under: "/static", +// from: priv_directory <> "/static", +// ) +// +// // Handle the request! +// handle_request(req) +// } diff --git a/apps/my-app/server/test/server_test.gleam b/apps/my-app/server/test/server_test.gleam @@ -0,0 +1,49 @@ +import gleam/option +import gleam/pgo +import gleeunit +import gleeunit/should + +import server/db/login + +pub fn main() { + gleeunit.main() +} + +fn get_test_connection() -> pgo.Connection { + let cfg = + pgo.Config( + ..pgo.default_config(), + host: "db-postgresql-ams3-90424-do-user-17154797-0.j.db.ondigitalocean.com", + database: "defaultdb", + user: "doadmin", + password: option.Some("AVNS_1pjlrBSPkd4w0iFMGiE"), + port: 25_060, + ssl: True, + ) + let db = pgo.connect(cfg) + db +} + +pub fn successful_log_in_test() { + let db = get_test_connection() + let successful = db |> login.log_in("silas", "mypassword") + should.be_ok(successful) +} + +pub fn wrong_password_log_in_test() { + let db = get_test_connection() + let wrong_password = db |> login.log_in("silas", "mypasswodr") + case wrong_password { + Error(login.IncorrectPassword) -> Nil + _ -> panic as "password should be wrong" + } +} + +pub fn missing_user_log_in_test() { + let db = get_test_connection() + let missing_user = db |> login.log_in("silos", "mypasswodr") + case missing_user { + Error(login.UserNotFound(_)) -> Nil + _ -> panic as "user should be missing" + } +} diff --git a/apps/my-app/shared/.github/workflows/test.yml b/apps/my-app/shared/.github/workflows/test.yml @@ -0,0 +1,23 @@ +name: test + +on: + push: + branches: + - master + - main + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: erlef/setup-beam@v1 + with: + otp-version: "26.0.2" + gleam-version: "1.1.0" + rebar3-version: "3" + # elixir-version: "1.15.4" + - run: gleam deps download + - run: gleam test + - run: gleam format --check src test diff --git a/apps/my-app/shared/.gitignore b/apps/my-app/shared/.gitignore @@ -0,0 +1,4 @@ +*.beam +*.ez +/build +erl_crash.dump diff --git a/apps/my-app/shared/README.md b/apps/my-app/shared/README.md @@ -0,0 +1,25 @@ +# shared + +[![Package Version](https://img.shields.io/hexpm/v/shared)](https://hex.pm/packages/shared) +[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/shared/) + +```sh +gleam add shared +``` +```gleam +import shared + +pub fn main() { + // TODO: An example of the project in use +} +``` + +Further documentation can be found at <https://hexdocs.pm/shared>. + +## Development + +```sh +gleam run # Run the project +gleam test # Run the tests +gleam shell # Run an Erlang shell +``` diff --git a/apps/my-app/shared/gleam.toml b/apps/my-app/shared/gleam.toml @@ -0,0 +1,21 @@ +name = "shared" +version = "1.0.0" +target = "javascript" + +# Fill out these fields if you intend to generate HTML documentation or publish +# your project to the Hex package manager. +# +# description = "" +# licences = ["Apache-2.0"] +# repository = { type = "github", user = "username", repo = "project" } +# links = [{ title = "Website", href = "https://gleam.run" }] +# +# For a full reference of all the available options, you can have a look at +# https://gleam.run/writing-gleam/gleam-toml/. + +[dependencies] +gleam_stdlib = ">= 0.34.0 and < 2.0.0" +birl = ">= 1.7.1 and < 2.0.0" + +[dev-dependencies] +gleeunit = ">= 1.0.0 and < 2.0.0" diff --git a/apps/my-app/shared/manifest.toml b/apps/my-app/shared/manifest.toml @@ -0,0 +1,14 @@ +# This file was generated by Gleam +# You typically do not need to edit this file + +packages = [ + { name = "birl", version = "1.7.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "5C66647D62BCB11FE327E7A6024907C4A17954EF22865FE0940B54A852446D01" }, + { name = "gleam_stdlib", version = "0.40.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "86606B75A600BBD05E539EB59FABC6E307EEEA7B1E5865AFB6D980A93BCB2181" }, + { name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" }, + { name = "ranger", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "1566C272B1D141B3BBA38B25CB761EF56E312E79EC0E2DFD4D3C19FB0CC1F98C" }, +] + +[requirements] +birl = { version = ">= 1.7.1 and < 2.0.0" } +gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" } +gleeunit = { version = ">= 1.0.0 and < 2.0.0" } diff --git a/apps/my-app/shared/shell.nix b/apps/my-app/shared/shell.nix @@ -0,0 +1,15 @@ +let + # pkgs = import <nixpkgs> {}; + pkgs = import (builtins.fetchTarball { + name = "nixos-24.05"; + url = "https://github.com/nixos/nixpkgs/archive/63dacb46bf939521bdc93981b4cbb7ecb58427a0.tar.g z"; + sha256 = "1lr1h35prqkd1mkmzriwlpvxcb34kmhc9dnr48gkm8hh089hifmx"; + }) {}; +in pkgs.mkShell { + buildInputs = [ + pkgs.gleam + pkgs.erlang_27 + pkgs.rebar3 + ]; + # imports = ["./test.nix"]; +} diff --git a/apps/my-app/shared/src/shared.gleam b/apps/my-app/shared/src/shared.gleam @@ -0,0 +1,5 @@ +import birl + +pub type Post { + Post(id: Int, user_id: Int, content: String, created_at: birl.Time) +} diff --git a/apps/my-app/shared/test/shared_test.gleam b/apps/my-app/shared/test/shared_test.gleam @@ -0,0 +1,12 @@ +import gleeunit +import gleeunit/should + +pub fn main() { + gleeunit.main() +} + +// gleeunit test functions end in `_test` +pub fn hello_world_test() { + 1 + |> should.equal(1) +} diff --git a/db/db-postgresql-ams3-90424/README.md b/db/db-postgresql-ams3-90424/README.md diff --git a/db/db-postgresql-ams3-90424/migrations/000.sql b/db/db-postgresql-ams3-90424/migrations/000.sql @@ -0,0 +1,2 @@ +CREATE TABLE migrations ( version BIGINT, applied_at TIMESTAMP WITH time zone default now() ); + diff --git a/db/db-postgresql-ams3-90424/migrations/001.sql b/db/db-postgresql-ams3-90424/migrations/001.sql @@ -0,0 +1,21 @@ +create table if not exists twitter.users ( + id bigint primary key generated always as identity, + username text unique not null, + email text unique not null, + password_hash text not null, + created_at timestamp with time zone default now() +); + +create table if not exists twitter.tweets ( + id bigint primary key generated always as identity, + user_id bigint not null references twitter.users (id), + content text not null, + created_at timestamp with time zone default now() +); + +create table if not exists twitter.followers ( + follower_id bigint not null references twitter.users (id), + followee_id bigint not null references twitter.users (id), + followed_at timestamp with time zone default now(), + primary key (follower_id, followee_id) +); diff --git a/db/db-postgresql-ams3-90424/migrations/002.sql b/db/db-postgresql-ams3-90424/migrations/002.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS twitter.user_sessions ( + id bigint primary key generated always as identity, + user_id bigint not null references twitter.users (id), + token VARCHAR(64) NOT NULL, + created_at timestamp with time zone default now() +); + diff --git a/db/db-postgresql-ams3-90424/migrations/shell.nix b/db/db-postgresql-ams3-90424/migrations/shell.nix @@ -0,0 +1,7 @@ +let + pkgs = import <nixpkgs> {}; +in pkgs.mkShell { + buildInputs = [ + pkgs.squawk + ]; +} diff --git a/docs/develop.md b/docs/develop.md diff --git a/docs/philosophy.md b/docs/philosophy.md @@ -0,0 +1,19 @@ +# My Project + +## NixOS Deployment +- Read Justinas' guide ([part 1](https://justinas.org/nixos-in-the-cloud-step-by-step-part-1) and [part 2](https://justinas.org/nixos-in-the-cloud-step-by-step-part-2)) + +## Tools +- Go +- Postgres (RDS) +- S3 +- EC2 +- HTMX +- Docker + +## Philosophy +- Radical simplicity +- Local first +- Minimise compile time +- Minimise dependencies + diff --git a/infrastructure/.gitignore b/infrastructure/.gitignore @@ -0,0 +1,3 @@ +result +/secrets/ + diff --git a/infrastructure/Makefile b/infrastructure/Makefile @@ -0,0 +1,78 @@ +SHELL := /bin/sh + +## Create public & private wireguard keys to add a new device to the VPN. +add-to-vpn: + @read -p "Enter device name: " DEVICE && \ + umask 077 && \ + wg genkey > ./secrets/wireguard-keys/$$DEVICE.private && \ + wg pubkey < ./secrets/wireguard-keys/$$DEVICE.private > ./secrets/wireguard-keys/$$DEVICE.public && \ + echo "Successfully saved keys for device $$DEVICE to ./secrets/wireguard-keys/" + +## Deploy changes to hive.nix to DigitalOcean droplets. +deploy: + colmena apply + +## Build a NixOS image to upload to DigitalOcean. +image: + nix-build image.nix + +################################################################################# +# Self Documenting Commands # +################################################################################# + +.DEFAULT_GOAL := help + +# Inspired by <http://marmelab.com/blog/2016/02/29/auto-documented-makefile.html> +# sed script explained: +# /^##/: +# * save line in hold space +# * purge line +# * Loop: +# * append newline + line to hold space +# * go to next line +# * if line starts with doc comment, strip comment character off and loop +# * remove target prerequisites +# * append hold space (+ newline) to line +# * replace newline plus comments by `---` +# * print line +# Separate expressions are necessary because labels cannot be delimited by +# semicolon; see <http://stackoverflow.com/a/11799865/1968> +.PHONY: help +help: + @echo "$$(tput bold)Available rules:$$(tput sgr0)" + @echo + @sed -n -e "/^## / { \ + h; \ + s/.*//; \ + :doc" \ + -e "H; \ + n; \ + s/^## //; \ + t doc" \ + -e "s/:.*//; \ + G; \ + s/\\n## /---/; \ + s/\\n/ /g; \ + p; \ + }" ${MAKEFILE_LIST} \ + | LC_ALL='C' sort --ignore-case \ + | awk -F '---' \ + -v ncol=$$(tput cols) \ + -v indent=19 \ + -v col_on="$$(tput setaf 6)" \ + -v col_off="$$(tput sgr0)" \ + '{ \ + printf "%s%*s%s ", col_on, -indent, $$1, col_off; \ + n = split($$2, words, " "); \ + line_length = ncol - indent; \ + for (i = 1; i <= n; i++) { \ + line_length -= length(words[i]) + 1; \ + if (line_length <= 0) { \ + line_length = ncol - indent - length(words[i]) - 1; \ + printf "\n%*s ", -indent, " "; \ + } \ + printf "%s ", words[i]; \ + } \ + printf "\n"; \ + }' \ + | more $(shell test $(shell uname) = Darwin && echo '--no-init --raw-control-chars') diff --git a/infrastructure/README.md b/infrastructure/README.md @@ -0,0 +1,19 @@ +# My Project + +## NixOS Deployment +- Read Justinas' guide ([part 1](https://justinas.org/nixos-in-the-cloud-step-by-step-part-1) and [part 2](https://justinas.org/nixos-in-the-cloud-step-by-step-part-2)) + +## Tools +- Go +- Postgres (RDS) +- S3 +- EC2 +- HTMX +- Docker + +## Philosophy +- Radical simplicity +- Local first +- Minimise compile time +- Minimise dependencies + diff --git a/infrastructure/_apps/default.nix b/infrastructure/_apps/default.nix @@ -0,0 +1,6 @@ +{ + imports = [ + # ./test.nix + ./hello.nix + ]; +} diff --git a/infrastructure/_apps/hello.nix b/infrastructure/_apps/hello.nix @@ -0,0 +1,36 @@ +{ lib, pkgs, config, ... }: +with lib; +let + # Shorter name to access final settings a + # user of hello.nix module HAS ACTUALLY SET. + # cfg is a typical convention. + cfg = config.services.hello; +in { + # Declare what settings a user of this "hello.nix" module CAN SET. + options.services.hello = { + enable = mkEnableOption "hello service"; + greeter = mkOption { + type = types.str; + default = "world"; + }; + }; + + # Define what other settings, services and resources should be active IF + # a user of this "hello.nix" module ENABLED this module + # by setting "services.hello.enable = true;". + # config = mkIf cfg.enable { + # systemd.services.hello = { + # wantedBy = [ "multi-user.target" ]; + # serviceConfig.ExecStart = "${pkgs.hello}/bin/hello -g'Hello, ${escapeShellArg cfg.greeter}!'"; + # }; + # }; + config.services.postgres = { + enable = true; + ensureDatabases = [ "test" ]; + authentication = pkgs.lib.mkOverride 10 '' + #type database DBuser auth-method + local all all trust + ''; + }; +} + diff --git a/infrastructure/_apps/test.nix b/infrastructure/_apps/test.nix @@ -0,0 +1,148 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + pkg = import ./.; + + migrations = pkg.migrations; + server = pkg.server; + + cfg = config.services.myapp; + +in { + + options = { + services.myapp = { + enable = mkEnableOption "My App"; + + environment = mkOption { + type = types.enum [ "dev" "prod" ]; + default = "dev"; + example = "prod"; + description = "The environment (dev / prod) to deploy as."; + }; + + user = mkOption { + type = types.str; + default = "myapp"; + description = "User account under which my app runs."; + }; + + port = mkOption { + type = types.nullOr types.int; + default = 3003; + example = 8080; + description = '' + The port to bind my app server to. + ''; + }; + }; + }; + + config = mkIf cfg.enable { + + networking.firewall.allowedTCPPorts = [ cfg.port ]; + + users.users.${cfg.user} = { + name = cfg.user; + description = "My app service user"; + isSystemUser = true; + createHome = false; + packages = with pkgs; [ + gleam + erlang_27 + rebar3 + ]; + }; + + systemd.services = { + # postgres-migration = { + # description = "Postgres migrations"; + # wantedBy = [ "multi-user.target" ]; + # after = [ "postgresql.service" ]; + # requires = [ "postgresql.service" ]; + # }; + + myapp = { + wantedBy = [ "multi-user.target" ]; + description = "Start my app server."; + after = [ "network.target" ]; + # requires = [ "db-migration.service" ]; + + # environment = { + # DB_URI = ""; + # }; + + serviceConfig = { + Type = "simple"; + User = cfg.user; + ExecStart = "/root/apps/myapp/test_pgo_app"; + Restart = "always"; + KillMode = "process"; + }; + }; + }; + + services = mkIf cfg.environment == "dev" { + postgresql = { + enable = true; + enableTCPIP = true; + ensureDatabases = [ cfg.user ]; + ensureUsers = [ + { + name = cfg.user; + ensurePermissions = { + "DATABASE ${cfg.user}" = "ALL PRIVILEGES"; + }; + } + ]; + authentication = pkgs.lib.mkOverride 10 '' + local sameuser all peer + host sameuser all ::1/32 trust + ''; + }; + }; + + # systemd.services = { + # + # # db-migration = { + # # description = "DB migrations script"; + # # wantedBy = [ "multi-user.target" ]; + # # after = [ "postgresql.service" ]; + # # requires = [ "postgresql.service" ]; + # # + # # environment = { + # # DATABASE_URL = "postgres://${cfg.user}@localhost:5432/myapp?sslmode=disable"; + # # }; + # # + # # serviceConfig = { + # # User = cfg.user; + # # Type = "oneshot"; + # # ExecStart = "${pkgs.dbmate}/bin/dbmate -d ${migrations} --no-dump-schema up"; + # # }; + # # }; + # + # myapp = { + # wantedBy = [ "multi-user.target" ]; + # description = "Start my app server."; + # after = [ "network.target" ]; + # # requires = [ "db-migration.service" ]; + # + # # environment = { + # # APP_PORT = toString cfg.port; + # # DATABASE_URL = "postgres:///${cfg.user}"; + # # }; + # + # serviceConfig = { + # Type = "simple"; + # User = cfg.user; + # ExecStart = ''${server}/bin/server''; + # Restart = "always"; + # KillMode = "process"; + # }; + # }; + # }; + }; +} diff --git a/infrastructure/hive.nix b/infrastructure/hive.nix @@ -0,0 +1,85 @@ +let + nixos_24_05 = builtins.fetchTarball { + name = "nixos-24.05"; + url = "https://github.com/nixos/nixpkgs/archive/63dacb46bf939521bdc93981b4cbb7ecb58427a0.tar.gz"; + sha256 = "1lr1h35prqkd1mkmzriwlpvxcb34kmhc9dnr48gkm8hh089hifmx"; + }; +in { + meta = { + nixpkgs = (import nixos_24_05) {}; + }; + + defaults = { pkgs, ... }: { + system.stateVersion = "24.05"; # Do not change lightly! + environment.systemPackages = with pkgs; [ + vim wget curl + ]; + services.openssh.enable = true; + networking.firewall.allowedTCPPorts = [ 80 ]; + + boot.loader.grub = { + enable = false; + }; + fileSystems."/" = { + device = "/dev/sda1"; + fsType = "ext4"; + }; + }; + + nixos-s-1vcpu-512mb-10gb-ams3-01 = { pkgs, name, ... }: { + deployment = { + targetHost = "188.166.127.72"; + targetUser = "root"; + keys."wireguard_server_key.private" = { + keyFile = ./secrets/wireguard-keys/server.private; + }; + }; + networking.hostName = name; + time.timeZone = "Europe/Amsterdam"; + + imports = [ + ./vpn.nix + # ./_apps/hello.nix + # ../apps/my-app/module.nix + ]; + # networking.firewall.allowedTCPPorts = [ 8000 ]; + security.pki.certificateFiles = [ ./secrets/ca-certificate.crt ]; + + + users.users.silas = { + isNormalUser = true; + description = "Silas Brack"; + home = "/home/silas"; + extraGroups = [ "networkmanager" "wheel" ]; # "keys" + openssh.authorizedKeys.keys = [ + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFPYO29H4+yDvJbaoUqTg5V6IpD3CMFlB2F47MCzHNpY silasbrack@gmail.com" # Thinkpad + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGqHJMc2SCzL50SO9Flfnhn012p1dmp4Iph8lPQ2aoe/ silasbrack@gmail.com" # Desktop + ]; + packages = with pkgs; [ + neovim + ]; + }; + + # services.hello = { + # enable = true; + # greeter = "Bob"; + # }; + + # services.postgres = { + # + # }; + + services.nginx = { + enable = true; + virtualHosts."localhost" = { + locations."/" = { + return = "200 'Hello, NixOS!'"; + extraConfig = '' + default_type text/plain; + ''; + }; + }; + }; + + }; +} diff --git a/infrastructure/image.nix b/infrastructure/image.nix @@ -0,0 +1,6 @@ +{ pkgs ? import <nixpkgs> { } }: +let config = { + imports = [ <nixpkgs/nixos/modules/virtualisation/digital-ocean-image.nix> ]; +}; +in +(pkgs.nixos config).digitalOceanImage diff --git a/infrastructure/shell.nix b/infrastructure/shell.nix @@ -0,0 +1,5 @@ +{ pkgs ? import <nixpkgs> { } }: +pkgs.mkShell { + buildInputs = with pkgs; [ curl morph colmena wireguard-tools ]; +} + diff --git a/infrastructure/vpn.nix b/infrastructure/vpn.nix @@ -0,0 +1,59 @@ +{ pkgs, ... }: + +{ + networking.nat = { + enable = true; + externalInterface = "eth0"; + internalInterfaces = [ "wg0" ]; + }; + # Port 53 for DNS, port 51820 for WireGuard + networking.firewall = { + allowedTCPPorts = [ 53 ]; + allowedUDPPorts = [ 53 51820 ]; + }; + # boot.kernel.sysctl = { + # # "net.ipv4.ip_forward" = lib.mkOverride 98 true; + # "net.ipv4.conf.all.forwarding" = lib.mkOverride 98 true; + # "net.ipv4.conf.default.forwarding" = lib.mkOverride 98 true; + # }; + services.dnsmasq = { + enable = true; + extraConfig = '' + interface=wg0 + ''; + }; + networking.wg-quick.interfaces = { + wg0 = { + address = [ "10.100.0.1/24" ]; + listenPort = 51820; + privateKeyFile = "/run/keys/wireguard_server_key.private"; + + postUp = '' + ${pkgs.iptables}/bin/iptables -A FORWARD -i wg0 -o eth0 -j ACCEPT + ${pkgs.iptables}/bin/iptables -A FORWARD -o wg0 -i eth0 -j ACCEPT + ${pkgs.iptables}/bin/iptables -t nat -A POSTROUTING -s 10.100.0.1/24 -j MASQUERADE + ''; + preDown = '' + ${pkgs.iptables}/bin/iptables -D FORWARD -i wg0 -o eth0 -j ACCEPT + ${pkgs.iptables}/bin/iptables -D FORWARD -o wg0 -i eth0 -j ACCEPT + ${pkgs.iptables}/bin/iptables -t nat -D POSTROUTING -s 10.100.0.1/24 -j MASQUERADE + ''; + + peers = [ + { # Thinkpad + publicKey = "3owbBawNk7IEI8sHmJjr+Wna7QH5WxYuRGjU+OP4i2M="; + allowedIPs = [ "10.100.0.2/32" ]; + } + { # Pixel + publicKey = "pTuUtRphmXYKhwInaCIQY7nXRgftNzgKJxleACutrFc="; + allowedIPs = [ "10.100.0.3/32" ]; + } + { # Desktop + publicKey = "F/UODKJ2SQaRUOb7my2LtvRaN4p/khfW8bstO+09/zo="; + allowedIPs = [ "10.100.0.4/32" ]; + } + ]; + }; + }; +} +