feat(webserver): Vendor and reimplement main pages in leptos
This commit is contained in:
parent
aeba7301b0
commit
59fdb37222
25 changed files with 4862 additions and 176 deletions
7
.dir-locals.el
Normal file
7
.dir-locals.el
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
((rust-mode
|
||||||
|
. ((eglot-workspace-configuration
|
||||||
|
. (:rust-analyzer
|
||||||
|
(:cargo (:features "all")
|
||||||
|
:check (:command "clippy")
|
||||||
|
:rustfmt (:overrideCommand ["leptosfmt" "--stdin" "--rustfmt"])
|
||||||
|
:linkedProjects ["./pkgs/packages/webserver/Cargo.toml"]))))))
|
||||||
|
|
@ -8,7 +8,6 @@
|
||||||
imports = [
|
imports = [
|
||||||
flake-inputs.disko.nixosModules.disko
|
flake-inputs.disko.nixosModules.disko
|
||||||
flake-inputs.sops-nix.nixosModules.sops
|
flake-inputs.sops-nix.nixosModules.sops
|
||||||
flake-inputs.tlaternet-webserver.nixosModules.default
|
|
||||||
"${modulesPath}/profiles/minimal.nix"
|
"${modulesPath}/profiles/minimal.nix"
|
||||||
|
|
||||||
../modules
|
../modules
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,10 @@
|
||||||
{ config, ... }:
|
{
|
||||||
|
pkgs,
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
flake-inputs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
let
|
let
|
||||||
inherit (config.services.nginx) domain;
|
inherit (config.services.nginx) domain;
|
||||||
in
|
in
|
||||||
|
|
@ -8,11 +14,50 @@ in
|
||||||
443
|
443
|
||||||
];
|
];
|
||||||
|
|
||||||
services.tlaternet-webserver = {
|
systemd.services.tlaternet-webserver = {
|
||||||
enable = true;
|
description = "tlater.net webserver";
|
||||||
listen = {
|
wantedBy = [ "multi-user.target" ];
|
||||||
addr = "127.0.0.1";
|
after = [ "network.target" ];
|
||||||
port = 8000;
|
|
||||||
|
script = ''
|
||||||
|
${lib.getExe flake-inputs.self.packages.${pkgs.system}.webserver}
|
||||||
|
'';
|
||||||
|
|
||||||
|
environment = {
|
||||||
|
TLATERNET_NTFY_INSTANCE = "https://tlater.net";
|
||||||
|
LEPTOS_SITE_ADDR = "127.0.0.1:8000";
|
||||||
|
};
|
||||||
|
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "exec";
|
||||||
|
LoadCredential = "ntfy-topic:/run/secrets/tlaternet/ntfy-topic";
|
||||||
|
|
||||||
|
DynamicUser = true;
|
||||||
|
ProtectHome = true; # Override the default (read-only)
|
||||||
|
PrivateDevices = true;
|
||||||
|
PrivateIPC = true;
|
||||||
|
PrivateUsers = true;
|
||||||
|
ProtectHostname = true;
|
||||||
|
ProtectClock = true;
|
||||||
|
ProtectKernelTunables = true;
|
||||||
|
ProtectKernelModules = true;
|
||||||
|
ProtectKernelLogs = true;
|
||||||
|
ProtectControlGroups = true;
|
||||||
|
RestrictAddressFamilies = [
|
||||||
|
"AF_UNIX"
|
||||||
|
"AF_INET"
|
||||||
|
"AF_INET6"
|
||||||
|
];
|
||||||
|
RestrictNamespaces = true;
|
||||||
|
LockPersonality = true;
|
||||||
|
MemoryDenyWriteExecute = true;
|
||||||
|
RestrictRealtime = true;
|
||||||
|
RestrictSUIDSGID = true;
|
||||||
|
SystemCallArchitectures = "native";
|
||||||
|
SystemCallFilter = [
|
||||||
|
"@system-service"
|
||||||
|
"~@privileged @resources @setuid @keyring"
|
||||||
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -28,6 +73,11 @@ in
|
||||||
useACMEHost = "tlater.net";
|
useACMEHost = "tlater.net";
|
||||||
enableHSTS = true;
|
enableHSTS = true;
|
||||||
|
|
||||||
locations."/".proxyPass = "http://${addr}:${toString port}";
|
locations."/".proxyPass =
|
||||||
|
"http://${config.systemd.services.tlaternet-webserver.environment.LEPTOS_SITE_ADDR}";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
sops.secrets = {
|
||||||
|
"tlaternet/ntfy-topic" = { };
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
167
flake.lock
generated
167
flake.lock
generated
|
|
@ -136,50 +136,6 @@
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dream2nix": {
|
|
||||||
"inputs": {
|
|
||||||
"nixpkgs": [
|
|
||||||
"nixpkgs"
|
|
||||||
],
|
|
||||||
"purescript-overlay": "purescript-overlay",
|
|
||||||
"pyproject-nix": "pyproject-nix"
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1735160684,
|
|
||||||
"narHash": "sha256-n5CwhmqKxifuD4Sq4WuRP/h5LO6f23cGnSAuJemnd/4=",
|
|
||||||
"owner": "nix-community",
|
|
||||||
"repo": "dream2nix",
|
|
||||||
"rev": "8ce6284ff58208ed8961681276f82c2f8f978ef4",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nix-community",
|
|
||||||
"repo": "dream2nix",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"fenix": {
|
|
||||||
"inputs": {
|
|
||||||
"nixpkgs": [
|
|
||||||
"tlaternet-webserver",
|
|
||||||
"nixpkgs"
|
|
||||||
],
|
|
||||||
"rust-analyzer-src": "rust-analyzer-src"
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1737181903,
|
|
||||||
"narHash": "sha256-lvp77MhGzSN+ICd0MugppCjQR6cmlM2iAC5cjy2ZsaA=",
|
|
||||||
"owner": "nix-community",
|
|
||||||
"repo": "fenix",
|
|
||||||
"rev": "ac79bb490b8c1af4bbc587b84c76f9527d6b14f7",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nix-community",
|
|
||||||
"repo": "fenix",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"flake-compat": {
|
"flake-compat": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
|
|
@ -310,6 +266,19 @@
|
||||||
"url": "https://channels.nixos.org/nixos-25.05-small/nixexprs.tar.xz"
|
"url": "https://channels.nixos.org/nixos-25.05-small/nixexprs.tar.xz"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"nixpkgs-unstable": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1763835633,
|
||||||
|
"narHash": "sha256-nzRnw0UkYQpDm0o20AKvG/5oHCXy5qEGOsFAVhB5NmA=",
|
||||||
|
"rev": "050e09e091117c3d7328c7b2b7b577492c43c134",
|
||||||
|
"type": "tarball",
|
||||||
|
"url": "https://releases.nixos.org/nixos/unstable/nixos-25.11pre900642.050e09e09111/nixexprs.tar.xz?lastModified=1763835633&rev=050e09e091117c3d7328c7b2b7b577492c43c134"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"type": "tarball",
|
||||||
|
"url": "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz"
|
||||||
|
}
|
||||||
|
},
|
||||||
"pre-commit-hooks": {
|
"pre-commit-hooks": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"flake-compat": [
|
"flake-compat": [
|
||||||
|
|
@ -349,50 +318,6 @@
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"purescript-overlay": {
|
|
||||||
"inputs": {
|
|
||||||
"flake-compat": [
|
|
||||||
"deploy-rs",
|
|
||||||
"flake-compat"
|
|
||||||
],
|
|
||||||
"nixpkgs": [
|
|
||||||
"tlaternet-webserver",
|
|
||||||
"dream2nix",
|
|
||||||
"nixpkgs"
|
|
||||||
],
|
|
||||||
"slimlock": "slimlock"
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1728546539,
|
|
||||||
"narHash": "sha256-Sws7w0tlnjD+Bjck1nv29NjC5DbL6nH5auL9Ex9Iz2A=",
|
|
||||||
"owner": "thomashoneyman",
|
|
||||||
"repo": "purescript-overlay",
|
|
||||||
"rev": "4ad4c15d07bd899d7346b331f377606631eb0ee4",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "thomashoneyman",
|
|
||||||
"repo": "purescript-overlay",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"pyproject-nix": {
|
|
||||||
"flake": false,
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1702448246,
|
|
||||||
"narHash": "sha256-hFg5s/hoJFv7tDpiGvEvXP0UfFvFEDgTdyHIjDVHu1I=",
|
|
||||||
"owner": "davhau",
|
|
||||||
"repo": "pyproject.nix",
|
|
||||||
"rev": "5a06a2697b228c04dd2f35659b4b659ca74f7aeb",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "davhau",
|
|
||||||
"ref": "dream2nix",
|
|
||||||
"repo": "pyproject.nix",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"deploy-rs": "deploy-rs",
|
"deploy-rs": "deploy-rs",
|
||||||
|
|
@ -400,49 +325,9 @@
|
||||||
"flint": "flint",
|
"flint": "flint",
|
||||||
"foundryvtt": "foundryvtt",
|
"foundryvtt": "foundryvtt",
|
||||||
"nixpkgs": "nixpkgs",
|
"nixpkgs": "nixpkgs",
|
||||||
|
"nixpkgs-unstable": "nixpkgs-unstable",
|
||||||
"sonnenshift": "sonnenshift",
|
"sonnenshift": "sonnenshift",
|
||||||
"sops-nix": "sops-nix",
|
"sops-nix": "sops-nix"
|
||||||
"tlaternet-webserver": "tlaternet-webserver"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"rust-analyzer-src": {
|
|
||||||
"flake": false,
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1737140097,
|
|
||||||
"narHash": "sha256-m4SN8DeKzsP10EQFS7+2zgGfCrMhTfTt1H0QRNesD08=",
|
|
||||||
"owner": "rust-lang",
|
|
||||||
"repo": "rust-analyzer",
|
|
||||||
"rev": "f61bfa4d7feb84d07538d361fe77d34a29e3b375",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "rust-lang",
|
|
||||||
"ref": "nightly",
|
|
||||||
"repo": "rust-analyzer",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"slimlock": {
|
|
||||||
"inputs": {
|
|
||||||
"nixpkgs": [
|
|
||||||
"tlaternet-webserver",
|
|
||||||
"dream2nix",
|
|
||||||
"purescript-overlay",
|
|
||||||
"nixpkgs"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1688756706,
|
|
||||||
"narHash": "sha256-xzkkMv3neJJJ89zo3o2ojp7nFeaZc2G0fYwNXNJRFlo=",
|
|
||||||
"owner": "thomashoneyman",
|
|
||||||
"repo": "slimlock",
|
|
||||||
"rev": "cf72723f59e2340d24881fd7bf61cb113b4c407c",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "thomashoneyman",
|
|
||||||
"repo": "slimlock",
|
|
||||||
"type": "github"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sonnenshift": {
|
"sonnenshift": {
|
||||||
|
|
@ -501,28 +386,6 @@
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tlaternet-webserver": {
|
|
||||||
"inputs": {
|
|
||||||
"dream2nix": "dream2nix",
|
|
||||||
"fenix": "fenix",
|
|
||||||
"nixpkgs": [
|
|
||||||
"nixpkgs"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1737271785,
|
|
||||||
"narHash": "sha256-yVdaaawYK1/q9V5btfGpxVCQBdyQx1WcFHYO0yX5bP8=",
|
|
||||||
"ref": "refs/heads/master",
|
|
||||||
"rev": "5d3d84836101ec9b9867a5f754c9ee1b9d4dc538",
|
|
||||||
"revCount": 76,
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://gitea.tlater.net/tlaternet/tlaternet.git"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://gitea.tlater.net/tlaternet/tlaternet.git"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"utils": {
|
"utils": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"systems": "systems"
|
"systems": "systems"
|
||||||
|
|
|
||||||
19
flake.nix
19
flake.nix
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "https://channels.nixos.org/nixos-25.05-small/nixexprs.tar.xz";
|
nixpkgs.url = "https://channels.nixos.org/nixos-25.05-small/nixexprs.tar.xz";
|
||||||
|
nixpkgs-unstable.url = "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz";
|
||||||
|
|
||||||
## Nix/OS utilities
|
## Nix/OS utilities
|
||||||
|
|
||||||
|
|
@ -30,17 +31,6 @@
|
||||||
|
|
||||||
## Services
|
## Services
|
||||||
|
|
||||||
tlaternet-webserver = {
|
|
||||||
url = "git+https://gitea.tlater.net/tlaternet/tlaternet.git";
|
|
||||||
inputs = {
|
|
||||||
nixpkgs.follows = "nixpkgs";
|
|
||||||
dream2nix.inputs = {
|
|
||||||
nixpkgs.follows = "nixpkgs";
|
|
||||||
purescript-overlay.inputs.flake-compat.follows = "deploy-rs/flake-compat";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
foundryvtt = {
|
foundryvtt = {
|
||||||
url = "github:reckenrode/nix-foundryvtt";
|
url = "github:reckenrode/nix-foundryvtt";
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
|
@ -148,7 +138,10 @@
|
||||||
packages.${system} = {
|
packages.${system} = {
|
||||||
default = vm.config.system.build.vm;
|
default = vm.config.system.build.vm;
|
||||||
}
|
}
|
||||||
// import ./pkgs { pkgs = nixpkgs.legacyPackages.${system}; };
|
// import ./pkgs {
|
||||||
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
|
flake-inputs = inputs;
|
||||||
|
};
|
||||||
|
|
||||||
###################
|
###################
|
||||||
# Utility scripts #
|
# Utility scripts #
|
||||||
|
|
@ -184,6 +177,8 @@
|
||||||
minecraft = nixpkgs.legacyPackages.${system}.mkShell {
|
minecraft = nixpkgs.legacyPackages.${system}.mkShell {
|
||||||
packages = nixpkgs.lib.attrValues { inherit (nixpkgs.legacyPackages.${system}) packwiz; };
|
packages = nixpkgs.lib.attrValues { inherit (nixpkgs.legacyPackages.${system}) packwiz; };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
webserver = self.packages.${system}.webserver.devShell;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
tlaternet:
|
||||||
|
ntfy-topic: ENC[AES256_GCM,data:T0wEsXso0uJD4KDe8qv8d24+vaouDrpDFxpSROn5kiYnqa1w6BSy3VrFdqFy17s7mzlAsZtN7gfDZDFjIo1S3Q==,iv:evWlOBgdnVdprzrTy5m+rcRo0OkFEIX+lCgfwLyfw4I=,tag:iq4B6VLWy0nm80ezqJ+rgw==,type:str]
|
||||||
porkbun:
|
porkbun:
|
||||||
api-key: ENC[AES256_GCM,data:A5J1sqwq6hs=,iv:77Mar3IX7mq7z7x6s9sSeGNVYc1Wv78HptJElEC7z3Q=,tag:eM/EF9TxKu+zcbJ1SYXiuA==,type:str]
|
api-key: ENC[AES256_GCM,data:A5J1sqwq6hs=,iv:77Mar3IX7mq7z7x6s9sSeGNVYc1Wv78HptJElEC7z3Q=,tag:eM/EF9TxKu+zcbJ1SYXiuA==,type:str]
|
||||||
secret-api-key: ENC[AES256_GCM,data:8Xv+jWYaWMI=,iv:li4tdY0pch5lksftMmfMVS729caAwfaacoztaQ49az0=,tag:KhfElBGzVH4ByFPfuQsdhw==,type:str]
|
secret-api-key: ENC[AES256_GCM,data:8Xv+jWYaWMI=,iv:li4tdY0pch5lksftMmfMVS729caAwfaacoztaQ49az0=,tag:KhfElBGzVH4ByFPfuQsdhw==,type:str]
|
||||||
|
|
@ -28,8 +30,8 @@ turn:
|
||||||
env: ENC[AES256_GCM,data:xjIz/AY109lyiL5N01p5T3HcYco/rM5CJSRTtg==,iv:16bW6OpyOK/QL0QPGQp/Baa9xyT8E3ZsYkwqmjuofk0=,tag:J5re3uKxIykw3YunvQWBgg==,type:str]
|
env: ENC[AES256_GCM,data:xjIz/AY109lyiL5N01p5T3HcYco/rM5CJSRTtg==,iv:16bW6OpyOK/QL0QPGQp/Baa9xyT8E3ZsYkwqmjuofk0=,tag:J5re3uKxIykw3YunvQWBgg==,type:str]
|
||||||
secret: ENC[AES256_GCM,data:eQ7dAocoZtg=,iv:fgzjTPv30WqTKlLy+yMn5MsKQgjhPnwlGFFwYEg3gWs=,tag:1ze33U1NBkgMX/9SiaBNQg==,type:str]
|
secret: ENC[AES256_GCM,data:eQ7dAocoZtg=,iv:fgzjTPv30WqTKlLy+yMn5MsKQgjhPnwlGFFwYEg3gWs=,tag:1ze33U1NBkgMX/9SiaBNQg==,type:str]
|
||||||
sops:
|
sops:
|
||||||
lastmodified: "2025-11-19T16:42:43Z"
|
lastmodified: "2025-11-27T19:00:56Z"
|
||||||
mac: ENC[AES256_GCM,data:4YivckDS+jBX3Bkon0bTAm3SXya4v2ieZyqeBXjBUYZeCmelIng7bn2dP7791O6RK6RvSXAGhiykWgGRW/boG3QM8VLxDMSRTKovJo5k6oxtFJC8OLDJoh1EC5BQLznJDKl4So6FgYPEtdQ6rx+Q6Ah7JSMtQilxRoe/hYapT90=,iv:9BGtS585gVbvH6l96/YYZiY1DrwB565vPaNNtFC9vbk=,tag:HsZuDMqPFHTMPxQsD36LNQ==,type:str]
|
mac: ENC[AES256_GCM,data:Q8ATaYVuToQaToVgT5JNP6NGipLnu8JifBivD5jG0h/Xb82ObkMHYejy7tcir600+XgC2UJgfRBgEtYbhjK+SQcyvyBpmPWyv1cKfcidwnfDqRMbc8/1LLl/3mK1losv7/51aZDqvu7GuIvOwXh6IyDQldSuyyPf+wcmCCENiLY=,iv:zDGmnEL2659W7JPRG9dlLjtvDtCgz+iXjJt4EPx/OCM=,tag:3X69zX1sZqrZujB7En8jqA==,type:str]
|
||||||
pgp:
|
pgp:
|
||||||
- created_at: "2025-10-03T21:38:26Z"
|
- created_at: "2025-10-03T21:38:26Z"
|
||||||
enc: |-
|
enc: |-
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
{ pkgs }:
|
{ pkgs, flake-inputs }:
|
||||||
|
let
|
||||||
|
inherit (flake-inputs.nixpkgs-unstable.legacyPackages.${pkgs.system}) ast-grep;
|
||||||
|
in
|
||||||
pkgs.lib.packagesFromDirectoryRecursive {
|
pkgs.lib.packagesFromDirectoryRecursive {
|
||||||
inherit (pkgs) callPackage;
|
callPackage = pkgs.lib.callPackageWith (pkgs // { inherit ast-grep; });
|
||||||
directory = ./packages;
|
directory = ./packages;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1
pkgs/packages/webserver/.gitignore
vendored
Normal file
1
pkgs/packages/webserver/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
target/
|
||||||
3453
pkgs/packages/webserver/Cargo.lock
generated
Normal file
3453
pkgs/packages/webserver/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
68
pkgs/packages/webserver/Cargo.toml
Normal file
68
pkgs/packages/webserver/Cargo.toml
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
[package]
|
||||||
|
name = "tlaternet-webserver"
|
||||||
|
version = "0.2.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
axum = { version = "0.8.7", features = ["macros"], optional = true }
|
||||||
|
console_error_panic_hook = { version = "0.1.7", optional = true }
|
||||||
|
figment = { version = "0.10.19", features = ["toml", "env"] }
|
||||||
|
leptos = "0.8.3"
|
||||||
|
leptos_axum = { version = "0.8.3", optional = true }
|
||||||
|
leptos_meta = "0.8.3"
|
||||||
|
leptos_router = "0.8.3"
|
||||||
|
markdown_view_leptos = "0.1.3"
|
||||||
|
reqwest = "0.12.24"
|
||||||
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
|
thiserror = "2.0.17"
|
||||||
|
tokio = { version = "1.48.0", features = ["rt-multi-thread"], optional = true }
|
||||||
|
url = "2.5.7"
|
||||||
|
wasm-bindgen = { version = "=0.2.100", optional = true }
|
||||||
|
web-sys = "^0.3.77"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
hydrate = [
|
||||||
|
"leptos/hydrate",
|
||||||
|
"dep:console_error_panic_hook",
|
||||||
|
"dep:wasm-bindgen",
|
||||||
|
]
|
||||||
|
ssr = [
|
||||||
|
"dep:axum",
|
||||||
|
"dep:tokio",
|
||||||
|
"dep:leptos_axum",
|
||||||
|
"leptos/ssr",
|
||||||
|
"leptos_meta/ssr",
|
||||||
|
"leptos_router/ssr",
|
||||||
|
]
|
||||||
|
|
||||||
|
[profile.wasm-release]
|
||||||
|
inherits = "release"
|
||||||
|
opt-level = 'z'
|
||||||
|
lto = true
|
||||||
|
codegen-units = 1
|
||||||
|
panic = "abort"
|
||||||
|
|
||||||
|
[package.metadata.leptos]
|
||||||
|
output-name = "tlaternet-webserver"
|
||||||
|
site-root = "target/site"
|
||||||
|
site-pkg-dir = "pkg"
|
||||||
|
style-file = "style/main.scss"
|
||||||
|
site-addr = "127.0.0.1:3000"
|
||||||
|
reload-port = 3001
|
||||||
|
browserquery = "defaults"
|
||||||
|
env = "DEV"
|
||||||
|
bin-features = ["ssr"]
|
||||||
|
bin-default-features = false
|
||||||
|
lib-features = ["hydrate"]
|
||||||
|
lib-default-features = false
|
||||||
|
lib-profile-release = "wasm-release"
|
||||||
|
watch-additional-files = ["config.toml"]
|
||||||
|
|
||||||
|
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
|
||||||
|
# [Windows] for non-WSL use "npx.cmd playwright test"
|
||||||
|
# This binary name can be checked in Powershell with Get-Command npx
|
||||||
|
end2end-cmd = "npx playwright test"
|
||||||
|
end2end-dir = "end2end"
|
||||||
3
pkgs/packages/webserver/config.toml
Normal file
3
pkgs/packages/webserver/config.toml
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Can be specified with systemd creds instead
|
||||||
|
credentials_directory = "dev-creds"
|
||||||
|
ntfy_instance = "https://ntfy.sh"
|
||||||
5
pkgs/packages/webserver/leptosfmt.toml
Normal file
5
pkgs/packages/webserver/leptosfmt.toml
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
max_width = 100
|
||||||
|
tab_spaces = 2
|
||||||
|
attr_value_brace_style = "WhenRequired"
|
||||||
|
macro_names = [ "leptos::view", "view" ]
|
||||||
|
closing_tag_style = "SelfClosing"
|
||||||
295
pkgs/packages/webserver/package.nix
Normal file
295
pkgs/packages/webserver/package.nix
Normal file
|
|
@ -0,0 +1,295 @@
|
||||||
|
{
|
||||||
|
lib,
|
||||||
|
stdenvNoCC,
|
||||||
|
fetchFromGitHub,
|
||||||
|
fetchurl,
|
||||||
|
symlinkJoin,
|
||||||
|
makeBinaryWrapper,
|
||||||
|
|
||||||
|
pkg-config,
|
||||||
|
openssl,
|
||||||
|
cargo-leptos,
|
||||||
|
dart-sass,
|
||||||
|
rustPlatform,
|
||||||
|
wasm-bindgen-cli_0_2_100,
|
||||||
|
binaryen,
|
||||||
|
inkscape,
|
||||||
|
|
||||||
|
mkShell,
|
||||||
|
clangStdenv,
|
||||||
|
rust-analyzer,
|
||||||
|
rustc,
|
||||||
|
rustfmt,
|
||||||
|
leptosfmt,
|
||||||
|
cargo,
|
||||||
|
clippy,
|
||||||
|
|
||||||
|
writers,
|
||||||
|
ast-grep,
|
||||||
|
nix-prefetch-github,
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
cargoMetadata = lib.pipe ./Cargo.toml [
|
||||||
|
builtins.readFile
|
||||||
|
builtins.fromTOML
|
||||||
|
];
|
||||||
|
|
||||||
|
sass-dependencies = {
|
||||||
|
bulma = stdenvNoCC.mkDerivation (drv: {
|
||||||
|
pname = "bulma";
|
||||||
|
version = "1.0.4";
|
||||||
|
|
||||||
|
src = fetchFromGitHub {
|
||||||
|
owner = "jgthms";
|
||||||
|
repo = "bulma";
|
||||||
|
rev = drv.version;
|
||||||
|
hash = "sha256-hlejqBI6ayzhm15IymrzhTevkl3xffMfdTasZ2CmAas=";
|
||||||
|
};
|
||||||
|
|
||||||
|
installPhase = ''
|
||||||
|
mkdir -p $out/node_modules/bulma/
|
||||||
|
cp -r ./sass $out/node_modules/bulma/
|
||||||
|
'';
|
||||||
|
});
|
||||||
|
|
||||||
|
fontsource-scss = stdenvNoCC.mkDerivation {
|
||||||
|
pname = "fontsource-scss";
|
||||||
|
version = "0.2.2";
|
||||||
|
|
||||||
|
src = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/@fontsource-utils/scss/-/scss-0.2.2.tgz";
|
||||||
|
hash = "sha256-2BkCBhh01kZfMHhjHMMLDtUeesi7Uy7eMoeM1BAqX38=";
|
||||||
|
};
|
||||||
|
|
||||||
|
installPhase = ''
|
||||||
|
mkdir -p $out/node_modules/@fontsource-utils/scss/
|
||||||
|
cp -r . $out/node_modules/@fontsource-utils/scss/
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
fontsource-nunito = stdenvNoCC.mkDerivation {
|
||||||
|
pname = "fontsource-nunito";
|
||||||
|
version = "5.2.7";
|
||||||
|
|
||||||
|
src = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/@fontsource-variable/nunito/-/nunito-5.2.7.tgz";
|
||||||
|
hash = "sha256-xSt1sDpVL/hVYzffKTgN/t7uLI3JadDWtTfWow2jiPM=";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = [
|
||||||
|
"out"
|
||||||
|
"assets"
|
||||||
|
];
|
||||||
|
|
||||||
|
installPhase = ''
|
||||||
|
mkdir -p $out/node_modules/@fontsource-variable/nunito/
|
||||||
|
cp -r . $out/node_modules/@fontsource-variable/nunito/
|
||||||
|
|
||||||
|
mkdir -p $assets/@fontsource-variable/
|
||||||
|
cp -r files/ $assets/@fontsource-variable/nunito
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
fontsource-arimo = stdenvNoCC.mkDerivation {
|
||||||
|
pname = "fontsource-nunito";
|
||||||
|
version = "5.2.8";
|
||||||
|
|
||||||
|
src = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/@fontsource-variable/arimo/-/arimo-5.2.8.tgz";
|
||||||
|
hash = "sha256-jD1IGqy02j4bqMRAwbCgiIz/h97WPrTSd3eZ09nptHA=";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = [
|
||||||
|
"out"
|
||||||
|
"assets"
|
||||||
|
];
|
||||||
|
|
||||||
|
installPhase = ''
|
||||||
|
mkdir -p $out/node_modules/@fontsource-variable/arimo/
|
||||||
|
cp -r . $out/node_modules/@fontsource-variable/arimo/
|
||||||
|
|
||||||
|
mkdir -p $assets/@fontsource-variable/
|
||||||
|
cp -r files/ $assets/@fontsource-variable/arimo
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
other-dependencies = {
|
||||||
|
hack-font = stdenvNoCC.mkDerivation (drv: {
|
||||||
|
pname = "hack-font";
|
||||||
|
version = "3.003";
|
||||||
|
|
||||||
|
src = fetchFromGitHub {
|
||||||
|
owner = "source-foundry";
|
||||||
|
repo = "Hack";
|
||||||
|
rev = "v${drv.version}";
|
||||||
|
hash = "sha256-qGDtBvKecdfsleUBfXFezllz9Op679a030Qcj/oBs1o=";
|
||||||
|
};
|
||||||
|
|
||||||
|
dontBuild = true;
|
||||||
|
|
||||||
|
installPhase = ''
|
||||||
|
mkdir -p $out
|
||||||
|
cp -r build/web/fonts $out/hack-font/
|
||||||
|
'';
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
icons = stdenvNoCC.mkDerivation {
|
||||||
|
pname = "tlaternet-icons";
|
||||||
|
version = "1.0";
|
||||||
|
|
||||||
|
src = ./public/icon.svg;
|
||||||
|
|
||||||
|
dontUnpack = true;
|
||||||
|
|
||||||
|
nativeBuildInputs = [ inkscape ];
|
||||||
|
|
||||||
|
buildPhase = ''
|
||||||
|
inkscape -w 48 -h 48 $src --export-filename=favicon-48.png
|
||||||
|
'';
|
||||||
|
|
||||||
|
installPhase = ''
|
||||||
|
mkdir -p $out
|
||||||
|
cp $src $out/icon.svg
|
||||||
|
cp favicon-48.png $out/
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
assets = symlinkJoin {
|
||||||
|
name = "assets-${cargoMetadata.package.name}";
|
||||||
|
paths = [
|
||||||
|
sass-dependencies.fontsource-arimo.assets
|
||||||
|
sass-dependencies.fontsource-nunito.assets
|
||||||
|
other-dependencies.hack-font
|
||||||
|
icons
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Hack to allow importing sass *without* resorting to using `npm`
|
||||||
|
# and dealing with the insanity that is `package.json` files.
|
||||||
|
#
|
||||||
|
# dart-sass *in theory* supports completely arbitrary logic for
|
||||||
|
# package importing via the `pkg:` url prefix, but unfortunately
|
||||||
|
# currently the only implementation of it that is available in the
|
||||||
|
# upstream binary *requires* using a `node_modules` file in the
|
||||||
|
# repository root. See here:
|
||||||
|
# https://sass-lang.com/documentation/at-rules/use/#node-js-package-importer
|
||||||
|
#
|
||||||
|
# This wouldn't be so bad if it supported an environment variable or
|
||||||
|
# command line arg to specify what the repo root should be, but it
|
||||||
|
# doesn't; so instead we use the load-path to specify a package
|
||||||
|
# import root.
|
||||||
|
#
|
||||||
|
# As a consequence, we cannot use the `pkg:` prefix, so package
|
||||||
|
# imports are indistinguishable from relative path imports. This
|
||||||
|
# isn't the end of the world, but:
|
||||||
|
#
|
||||||
|
# TODO(tlater): See if we can talk to upstream about an
|
||||||
|
# implementation better suited for use with nix, perhaps in
|
||||||
|
# dart-sass (add an env variable?) or in cargo-leptos (add a config
|
||||||
|
# option that can also be set with an env variable, and use the sass
|
||||||
|
# protocol instead of the raw exe?).
|
||||||
|
dart-sass-with-packages =
|
||||||
|
let
|
||||||
|
packages = symlinkJoin {
|
||||||
|
name = "sass-packages";
|
||||||
|
paths = lib.attrValues sass-dependencies;
|
||||||
|
stripPrefix = "/node_modules";
|
||||||
|
};
|
||||||
|
in
|
||||||
|
symlinkJoin {
|
||||||
|
inherit (dart-sass) version;
|
||||||
|
pname = "dart-sass-with-packages";
|
||||||
|
|
||||||
|
paths = [ dart-sass ];
|
||||||
|
nativeBuildInputs = [ makeBinaryWrapper ];
|
||||||
|
|
||||||
|
postBuild = ''
|
||||||
|
wrapProgram $out/bin/sass \
|
||||||
|
--add-flag --load-path=${packages}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
in
|
||||||
|
rustPlatform.buildRustPackage (drv: {
|
||||||
|
inherit (cargoMetadata.package) version;
|
||||||
|
pname = cargoMetadata.package.name;
|
||||||
|
cargoLock.lockFile = drv.src + /Cargo.lock;
|
||||||
|
|
||||||
|
src = lib.fileset.toSource {
|
||||||
|
root = ./.;
|
||||||
|
fileset = lib.fileset.fromSource (lib.sources.cleanSource ./.);
|
||||||
|
};
|
||||||
|
|
||||||
|
nativeBuildInputs = [
|
||||||
|
cargo-leptos
|
||||||
|
rustc.llvmPackages.lld
|
||||||
|
dart-sass-with-packages
|
||||||
|
makeBinaryWrapper
|
||||||
|
pkg-config
|
||||||
|
wasm-bindgen-cli_0_2_100
|
||||||
|
binaryen
|
||||||
|
];
|
||||||
|
|
||||||
|
buildInputs = [ openssl ];
|
||||||
|
|
||||||
|
LEPTOS_ASSETS_DIR = assets.outPath;
|
||||||
|
|
||||||
|
buildPhase = ''
|
||||||
|
runHook preBuild
|
||||||
|
cargo leptos build --release
|
||||||
|
runHook postBuild
|
||||||
|
'';
|
||||||
|
|
||||||
|
installPhase = ''
|
||||||
|
runHook preInstall
|
||||||
|
|
||||||
|
mkdir -p $out/bin $out/share
|
||||||
|
cp target/release/tlaternet-webserver $out/bin
|
||||||
|
cp -r target/site $out/share
|
||||||
|
wrapProgram $out/bin/tlaternet-webserver \
|
||||||
|
--set LEPTOS_SITE_ROOT $out/share/site \
|
||||||
|
--set LEPTOS_ASSETS_DIR ${assets.outPath}
|
||||||
|
|
||||||
|
runHook postInstall
|
||||||
|
'';
|
||||||
|
|
||||||
|
meta.mainProgram = "tlaternet-webserver";
|
||||||
|
|
||||||
|
passthru = {
|
||||||
|
dependencies = sass-dependencies;
|
||||||
|
|
||||||
|
devShell = mkShell.override { stdenv = clangStdenv; } {
|
||||||
|
packages = [
|
||||||
|
pkg-config
|
||||||
|
openssl
|
||||||
|
cargo-leptos
|
||||||
|
dart-sass-with-packages
|
||||||
|
# lld is exposed as ld by the clangStdenv, adding it
|
||||||
|
# explicitly with bintools makes it work
|
||||||
|
rustc.llvmPackages.lld
|
||||||
|
|
||||||
|
rust-analyzer
|
||||||
|
rustc
|
||||||
|
rustfmt
|
||||||
|
leptosfmt
|
||||||
|
cargo
|
||||||
|
clippy
|
||||||
|
];
|
||||||
|
|
||||||
|
LEPTOS_ASSETS_DIR = assets.outPath;
|
||||||
|
};
|
||||||
|
|
||||||
|
updateScript = writers.writeNuBin "update-${cargoMetadata.package.name}" {
|
||||||
|
makeWrapperArgs = [
|
||||||
|
"--prefix"
|
||||||
|
"PATH"
|
||||||
|
":"
|
||||||
|
(lib.makeBinPath [
|
||||||
|
ast-grep
|
||||||
|
nix-prefetch-github
|
||||||
|
])
|
||||||
|
];
|
||||||
|
} ./update.nu;
|
||||||
|
};
|
||||||
|
})
|
||||||
78
pkgs/packages/webserver/public/icon.svg
Normal file
78
pkgs/packages/webserver/public/icon.svg
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
version="1.0"
|
||||||
|
width="260.000000pt"
|
||||||
|
height="260.000000pt"
|
||||||
|
viewBox="0 0 260.000000 260.000000"
|
||||||
|
preserveAspectRatio="xMidYMid meet"
|
||||||
|
id="svg70"
|
||||||
|
sodipodi:docname="icon.svg"
|
||||||
|
inkscape:version="1.0 (4035a4fb49, 2020-05-01)">
|
||||||
|
<metadata
|
||||||
|
id="metadata76">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title />
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<defs
|
||||||
|
id="defs74" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
inkscape:pagecheckerboard="true"
|
||||||
|
inkscape:document-rotation="0"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1"
|
||||||
|
objecttolerance="10"
|
||||||
|
gridtolerance="10"
|
||||||
|
guidetolerance="10"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:window-width="1916"
|
||||||
|
inkscape:window-height="1059"
|
||||||
|
id="namedview72"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="2.2269231"
|
||||||
|
inkscape:cx="173.33333"
|
||||||
|
inkscape:cy="173.33333"
|
||||||
|
inkscape:window-x="-2"
|
||||||
|
inkscape:window-y="13"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:current-layer="layer1" />
|
||||||
|
<g
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"
|
||||||
|
inkscape:label="Background">
|
||||||
|
<rect
|
||||||
|
style="fill:#99d1ce;stroke-width:0.75;fill-opacity:1"
|
||||||
|
id="rect843"
|
||||||
|
width="260"
|
||||||
|
height="260"
|
||||||
|
x="2.0117849e-08"
|
||||||
|
y="2.0117849e-08" />
|
||||||
|
</g>
|
||||||
|
<path
|
||||||
|
style="fill:#0f0f0f;fill-opacity:1;stroke:#991dce;stroke-width:0.1"
|
||||||
|
id="path62"
|
||||||
|
d="M 2.0117848e-8,130 V 260 H 130 260 V 130 2.0117848e-8 H 130 2.0117848e-8 Z M 132.5,19.099999 c 1.4,1.300001 2,3.6 2.3,8.300001 l 0.4,6.4 8.2,0.699999 C 160.7,35.9 177.9,40.300001 182.89999,44.600002 188.9,49.6 189.1,58.399998 183.3,62.999999 180,65.6 176.6,65.5 161.7,62.499998 155,61.199999 145.99999,59.8 141.79999,59.399999 l -7.8,-0.700001 V 85.7 v 27 L 147,116.9 c 34.8,11.2 49.4,24.6 50.69999,46.39999 C 198.7,178 194.8,188.9 184.79999,199.5 174.6,210.4 162.6,216.5 144.89999,219.6 l -9.69999,1.7 -0.4,7.5 c -0.3,6.29999 -0.7,7.69999 -2.7,9.29999 -3.5,2.8 -8.5,2.5 -11,-0.69999 -1.7,-2.10001 -2.1,-4.1 -2.1,-9.4 v -6.7 l -7.1,-0.70001 c -8.8,-0.79999 -23.900001,-4 -34.400001,-7.3 -10.300001,-3.3 -15.5,-7.99999 -15.5,-14 0,-5.59999 3.5,-10.49999 8.2,-11.7 2.999999,-0.8 5.4,-0.39999 12.500002,1.7 C 93.099998,192.5 103,194.59999 112.3,195.5 l 6.7,0.7 v -29.1 c 0,-22.50001 -0.3,-29.20001 -1.20001,-29.5 C 93.099998,130.3 81.199997,124.9 72.900001,117.3 58.5,103.9 54.800001,82.499998 63.699999,64.300001 70.999998,49.399999 91.399998,37.199999 113.4,34.499999 L 119,33.8 v -5.7 c 0,-10.400001 7.1,-15.1 13.5,-9.000001 z" />
|
||||||
|
<path
|
||||||
|
style="fill:#0f0f0f;stroke:#991dce;stroke-width:0.1;fill-opacity:1"
|
||||||
|
id="path64"
|
||||||
|
d="m 113.2,60.6 c -18.4,4.7 -25.8,18.1 -17.7,32 1.3,2.2 4,5.3 6,6.8 3.7,2.9 16.4,9 17.1,8.3 0.3,-0.2 0.3,-11.2 0.2,-24.4 l -0.3,-24 z" />
|
||||||
|
<path
|
||||||
|
style="fill:#0f0f0f;stroke:#991dce;stroke-width:0.1;fill-opacity:1"
|
||||||
|
id="path66"
|
||||||
|
d="m 134,169.1 v 26.2 l 4.4,-0.5 c 9.6,-1.1 21.1,-8.9 24.2,-16.4 1.9,-4.6 1.7,-12.1 -0.5,-16.9 -2.6,-5.7 -10,-11.8 -18.6,-15.4 -4,-1.7 -7.8,-3.1 -8.4,-3.1 -0.8,0 -1.1,8.2 -1.1,26.1 z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.6 KiB |
56
pkgs/packages/webserver/src/app.rs
Normal file
56
pkgs/packages/webserver/src/app.rs
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
use leptos::prelude::*;
|
||||||
|
use leptos_meta::{provide_meta_context, MetaTags, Stylesheet};
|
||||||
|
use leptos_router::{
|
||||||
|
components::{Route, Router, Routes},
|
||||||
|
StaticSegment,
|
||||||
|
};
|
||||||
|
|
||||||
|
mod homepage;
|
||||||
|
mod mail;
|
||||||
|
|
||||||
|
use crate::components::Navbar;
|
||||||
|
use homepage::HomePage;
|
||||||
|
use mail::Mail;
|
||||||
|
|
||||||
|
pub fn shell(options: LeptosOptions) -> impl IntoView {
|
||||||
|
view! {
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" data-theme="dark">
|
||||||
|
<head>
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
|
||||||
|
<link rel="icon" type="image/png" href="/favicon-48.png" sizes="48x48" />
|
||||||
|
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="author" content="Tristan Daniël Maat" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<AutoReload options=options.clone() />
|
||||||
|
<HydrationScripts options />
|
||||||
|
<MetaTags />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<App />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn App() -> impl IntoView {
|
||||||
|
provide_meta_context();
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<Stylesheet href="/pkg/tlaternet-webserver.css" />
|
||||||
|
|
||||||
|
<Navbar />
|
||||||
|
|
||||||
|
// content for this welcome page
|
||||||
|
<Router>
|
||||||
|
<main>
|
||||||
|
<Routes fallback=|| "Page not found.".into_view()>
|
||||||
|
<Route path=StaticSegment("") view=HomePage />
|
||||||
|
<Route path=StaticSegment("mail") view=Mail />
|
||||||
|
</Routes>
|
||||||
|
</main>
|
||||||
|
</Router>
|
||||||
|
}
|
||||||
|
}
|
||||||
70
pkgs/packages/webserver/src/app/homepage.rs
Normal file
70
pkgs/packages/webserver/src/app/homepage.rs
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
use leptos::prelude::*;
|
||||||
|
use leptos_meta::{Meta, Title};
|
||||||
|
use markdown_view_leptos::markdown_view;
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn HomePage() -> impl IntoView {
|
||||||
|
view! {
|
||||||
|
<Meta name="description" content="tlater.net homepage" />
|
||||||
|
<Title text="Welcome to tlater.net!" />
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="title has-text-weight-normal is-family-monospace">
|
||||||
|
"$ "<span id="typed-welcome" />
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column content">
|
||||||
|
{markdown_view!(
|
||||||
|
r#"
|
||||||
|
### About Me
|
||||||
|
|
||||||
|
Looks like you found my website. I suppose introductions are in order.
|
||||||
|
|
||||||
|
My name's Tristan, I'm an avid Dutch-South African software
|
||||||
|
engineer. You probably either met me at an open source conference, a
|
||||||
|
hackathon, a badminton session or at a roleplaying table.
|
||||||
|
"#
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="column content">
|
||||||
|
{markdown_view!(
|
||||||
|
r#"### My Work
|
||||||
|
|
||||||
|
I'm interested in a variety of things in the open source
|
||||||
|
world. Perhaps thanks to my pursuit of the perfect Linux desktop, this
|
||||||
|
has revolved a lot around reproducible build and deployment systems
|
||||||
|
for the last few years, initially starting with
|
||||||
|
[BuildStream](https://buildstream.build/) back in ~2017. I gave a
|
||||||
|
couple of talks on it at build meetups in the UK in subsequent years,
|
||||||
|
though sadly most evidence of that appears to have disappeared.
|
||||||
|
|
||||||
|
Since then this has culminated in a strong fondness for
|
||||||
|
[NixOS](https://nixos.org/) and Nix, as its active community makes
|
||||||
|
private use cases much more feasible. As such, I have a vested
|
||||||
|
interest in making this community as large as possible - I post a lot
|
||||||
|
on the NixOS [discourse](https://discourse.nixos.org/) trying to help
|
||||||
|
newcomers out where I can.
|
||||||
|
|
||||||
|
I also just enjoy Programming, my core languages for personal work are
|
||||||
|
currently probably Rust and Python, although I have a very varied
|
||||||
|
background. This is in part due to my former work as a consultant,
|
||||||
|
which required new languages every few months. I have experience from
|
||||||
|
JavaScript over Elm to Kotlin, but eventually I hope I might only need
|
||||||
|
to write Rust ;)
|
||||||
|
|
||||||
|
If you're interested in seeing these things for yourself, visit my
|
||||||
|
[Gitlab](https://gitlab.com/tlater) and
|
||||||
|
[GitHub](https://github.com/tlater) pages.
|
||||||
|
"#
|
||||||
|
)}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
}
|
||||||
|
}
|
||||||
217
pkgs/packages/webserver/src/app/mail.rs
Normal file
217
pkgs/packages/webserver/src/app/mail.rs
Normal file
|
|
@ -0,0 +1,217 @@
|
||||||
|
use leptos::{html::Form, logging};
|
||||||
|
use leptos_meta::{Meta, Title};
|
||||||
|
use leptos::{prelude::*, server_fn::codec::JsonEncoding};
|
||||||
|
use markdown_view_leptos::markdown_view;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[server]
|
||||||
|
async fn submit_mail(mail: String, subject: String, message: String) -> Result<String, MailError> {
|
||||||
|
use crate::AppState;
|
||||||
|
const NTFY_TOPIC_CREDENTIAL_NAME: &str = "ntfy-topic";
|
||||||
|
|
||||||
|
let mail = format!("From: {}\nSubject: {}\n{}", mail, subject, message);
|
||||||
|
logging::log!("{}", mail);
|
||||||
|
|
||||||
|
let state = use_context::<AppState>().unwrap();
|
||||||
|
let ntfy_topic = std::fs::read_to_string(
|
||||||
|
state
|
||||||
|
.config
|
||||||
|
.credentials_directory
|
||||||
|
.join(NTFY_TOPIC_CREDENTIAL_NAME),
|
||||||
|
)?;
|
||||||
|
let ntfy_url = state.config.ntfy_instance.join(ntfy_topic.trim())?;
|
||||||
|
|
||||||
|
state
|
||||||
|
.http_client
|
||||||
|
.post(ntfy_url)
|
||||||
|
.body(mail)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|err| err.without_url())?
|
||||||
|
.error_for_status()
|
||||||
|
.map_err(|err| err.without_url())?;
|
||||||
|
|
||||||
|
leptos_axum::redirect("/mail");
|
||||||
|
Ok("Mail successfully sent!".to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn MailForm() -> impl IntoView {
|
||||||
|
let form: NodeRef<Form> = NodeRef::new();
|
||||||
|
let submit_mail = ServerAction::<SubmitMail>::new();
|
||||||
|
let pending = submit_mail.pending();
|
||||||
|
let flash_message = submit_mail.value();
|
||||||
|
|
||||||
|
Effect::new(move ||
|
||||||
|
if flash_message.get().is_some_and(|m| m.is_ok()) {
|
||||||
|
if let Some(form) = form.get() {
|
||||||
|
form.reset();
|
||||||
|
} else {
|
||||||
|
logging::warn!("Failed to reset form");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<Show when=move || { flash_message.get().is_some() }>
|
||||||
|
<div
|
||||||
|
class="notification is-light"
|
||||||
|
class=("is-success", move || flash_message.get().is_some_and(|m| m.is_ok()))
|
||||||
|
class=("is-danger", move || flash_message.get().is_some_and(|m| m.is_err()))
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="delete"
|
||||||
|
aria-label="Close"
|
||||||
|
on:click=move |_| {
|
||||||
|
*flash_message.write() = None;
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<span role="alert">
|
||||||
|
{move || match flash_message.get() {
|
||||||
|
None => "".to_owned(),
|
||||||
|
Some(Ok(message)) => message,
|
||||||
|
Some(Err(error)) => format!("{}", error),
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<ActionForm node_ref=form action=submit_mail>
|
||||||
|
<fieldset disabled=move || pending.get()>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" for="mail">
|
||||||
|
Email address
|
||||||
|
</label>
|
||||||
|
<div class="control">
|
||||||
|
<input
|
||||||
|
id="mail"
|
||||||
|
class="input"
|
||||||
|
type="email"
|
||||||
|
placeholder="Your address"
|
||||||
|
name="mail"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" for="subject">
|
||||||
|
Subject
|
||||||
|
</label>
|
||||||
|
<div class="control">
|
||||||
|
<input
|
||||||
|
id="subject"
|
||||||
|
class="input"
|
||||||
|
type="text"
|
||||||
|
placeholder="E.g. There's a typo on your home page!"
|
||||||
|
name="subject"
|
||||||
|
autocomplete="off"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" for="message">
|
||||||
|
Message
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="message"
|
||||||
|
class="textarea"
|
||||||
|
type="text"
|
||||||
|
rows="6"
|
||||||
|
name="message"
|
||||||
|
autocomplete="off"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control">
|
||||||
|
<div class="field">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="button is-link"
|
||||||
|
class=("is-loading", move || pending.get())
|
||||||
|
>
|
||||||
|
Send
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</ActionForm>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn Mail() -> impl IntoView {
|
||||||
|
view! {
|
||||||
|
<Meta name="description" content="tlater.net mail submission" />
|
||||||
|
<Title text="Mail submission" />
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="title has-text-weight-normal">Contact Me</h1>
|
||||||
|
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column">
|
||||||
|
<MailForm />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column content">
|
||||||
|
{markdown_view!(
|
||||||
|
r#"
|
||||||
|
Any messages you enter here are directly forwarded to me. I aim to
|
||||||
|
respond within a day.
|
||||||
|
|
||||||
|
Don't be upset about the form, I want to avoid the spam publishing
|
||||||
|
your email address brings with it... And minimize the amount of mail
|
||||||
|
that doesn't reach me, this form is an exception in all my spam
|
||||||
|
filters, you see ;)
|
||||||
|
"#
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Error, Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub enum MailError {
|
||||||
|
#[error("This form appears to currently be broken :(")]
|
||||||
|
Permanent,
|
||||||
|
#[error("Mail service appears to be down; please try again later")]
|
||||||
|
Temporary,
|
||||||
|
#[error("Server error: {0}")]
|
||||||
|
ServerFnError(ServerFnErrorErr),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<url::ParseError> for MailError {
|
||||||
|
fn from(error: url::ParseError) -> Self {
|
||||||
|
logging::error!("Invalid ntfy URL: {error}");
|
||||||
|
Self::Permanent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<std::io::Error> for MailError {
|
||||||
|
fn from(error: std::io::Error) -> Self {
|
||||||
|
logging::error!("Couldn't read ntfy topic secret: {error}");
|
||||||
|
Self::Permanent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<reqwest::Error> for MailError {
|
||||||
|
fn from(error: reqwest::Error) -> Self {
|
||||||
|
logging::error!("Failed to connect to ntfy: {error}");
|
||||||
|
Self::Temporary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromServerFnError for MailError {
|
||||||
|
type Encoder = JsonEncoding;
|
||||||
|
|
||||||
|
fn from_server_fn_error(value: ServerFnErrorErr) -> Self {
|
||||||
|
Self::ServerFnError(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
45
pkgs/packages/webserver/src/components.rs
Normal file
45
pkgs/packages/webserver/src/components.rs
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
use leptos::prelude::*;
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn Navbar() -> impl IntoView {
|
||||||
|
let (active, set_active) = signal(false);
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<nav class="navbar" role="navigation" aria-label="main navigation">
|
||||||
|
<div class="navbar-brand">
|
||||||
|
<a class="navbar-item has-text-primary is-uppercase" href="/">
|
||||||
|
tlater
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
role="button"
|
||||||
|
on:click=move |_| { set_active.update(|active: &mut bool| *active = !*active) }
|
||||||
|
class="navbar-burger"
|
||||||
|
class=("is-active", move || active.get())
|
||||||
|
aria-label="menu"
|
||||||
|
aria-controls="main-navigation"
|
||||||
|
aria-expanded=move || if active.get() { "true" } else { "false" }
|
||||||
|
>
|
||||||
|
<span aria-hidden="true" />
|
||||||
|
<span aria-hidden="true" />
|
||||||
|
<span aria-hidden="true" />
|
||||||
|
<span aria-hidden="true" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="main-navigation" class="navbar-menu" class=("is-active", move || active.get())>
|
||||||
|
<div class="navbar-start">
|
||||||
|
<a class="navbar-item" href="/mail">
|
||||||
|
"E-Mail"
|
||||||
|
</a>
|
||||||
|
<a class="navbar-item" href="https://www.gitlab.com/tlater">
|
||||||
|
GitLab
|
||||||
|
</a>
|
||||||
|
<a class="navbar-item" href="https://www.github.com/TLATER">
|
||||||
|
GitHub
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
}
|
||||||
|
}
|
||||||
73
pkgs/packages/webserver/src/lib.rs
Normal file
73
pkgs/packages/webserver/src/lib.rs
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
pub mod app;
|
||||||
|
mod components;
|
||||||
|
|
||||||
|
#[cfg(feature = "hydrate")]
|
||||||
|
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||||
|
pub fn hydrate() {
|
||||||
|
use crate::app::*;
|
||||||
|
console_error_panic_hook::set_once();
|
||||||
|
leptos::mount::hydrate_body(App);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
pub use appstate::{AppState, Config};
|
||||||
|
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
mod appstate {
|
||||||
|
use axum::extract::FromRef;
|
||||||
|
use figment::{providers::Format, Figment};
|
||||||
|
use leptos::config::LeptosOptions;
|
||||||
|
use reqwest::Client;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
|
pub struct Config {
|
||||||
|
pub credentials_directory: PathBuf,
|
||||||
|
pub ntfy_instance: Url,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
pub fn parse() -> Self {
|
||||||
|
let config_path = std::env::var_os("TLATERNET_CONFIG")
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.or_else(|| {
|
||||||
|
std::env::current_dir()
|
||||||
|
.map(|dir| dir.join("config.toml"))
|
||||||
|
.ok()
|
||||||
|
});
|
||||||
|
|
||||||
|
let config: Result<Config, figment::Error> = if let Some(config_path) = config_path {
|
||||||
|
Figment::new().merge(figment::providers::Toml::file(config_path))
|
||||||
|
} else {
|
||||||
|
Figment::new()
|
||||||
|
}
|
||||||
|
.merge(figment::providers::Env::raw().only(&["CREDENTIALS_DIRECTORY"]))
|
||||||
|
.merge(figment::providers::Env::prefixed("TLATERNET_"))
|
||||||
|
.extract();
|
||||||
|
|
||||||
|
match config {
|
||||||
|
Ok(config) => {
|
||||||
|
if !config.credentials_directory.join("ntfy-topic").exists() {
|
||||||
|
leptos::logging::error!("Failed to find ntfy-topic credential");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
config
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
leptos::logging::error!("Failed to parse configuration: {:?}", error);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(FromRef, Debug, Clone)]
|
||||||
|
pub struct AppState {
|
||||||
|
pub config: Config,
|
||||||
|
pub http_client: Client,
|
||||||
|
pub leptos_options: LeptosOptions,
|
||||||
|
}
|
||||||
|
}
|
||||||
54
pkgs/packages/webserver/src/main.rs
Normal file
54
pkgs/packages/webserver/src/main.rs
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
use axum::Router;
|
||||||
|
use leptos::logging::log;
|
||||||
|
use leptos::prelude::*;
|
||||||
|
use leptos_axum::{LeptosRoutes, generate_route_list};
|
||||||
|
use tlaternet_webserver::app::*;
|
||||||
|
use tlaternet_webserver::{AppState, Config};
|
||||||
|
|
||||||
|
let config = Config::parse();
|
||||||
|
|
||||||
|
let (addr, leptos_options) = {
|
||||||
|
let conf = get_configuration(None).unwrap();
|
||||||
|
let addr = conf.leptos_options.site_addr;
|
||||||
|
let leptos_options = conf.leptos_options;
|
||||||
|
(addr, leptos_options)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generate the list of routes in your Leptos App
|
||||||
|
let routes = generate_route_list(App);
|
||||||
|
let http_client = reqwest::Client::new();
|
||||||
|
|
||||||
|
let state = AppState {
|
||||||
|
config,
|
||||||
|
http_client,
|
||||||
|
leptos_options,
|
||||||
|
};
|
||||||
|
|
||||||
|
let app = Router::new()
|
||||||
|
.leptos_routes(&state, routes, {
|
||||||
|
let leptos_options = state.leptos_options.clone();
|
||||||
|
move || shell(leptos_options.clone())
|
||||||
|
})
|
||||||
|
.fallback::<_, (_, _, axum::extract::State<AppState>, _)>(
|
||||||
|
leptos_axum::file_and_error_handler(shell),
|
||||||
|
)
|
||||||
|
.with_state(state);
|
||||||
|
|
||||||
|
// run our app with hyper
|
||||||
|
// `axum::Server` is a re-export of `hyper::Server`
|
||||||
|
log!("listening on http://{}", &addr);
|
||||||
|
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
|
||||||
|
axum::serve(listener, app.into_make_service())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "ssr"))]
|
||||||
|
pub fn main() {
|
||||||
|
// no client-side main function
|
||||||
|
// unless we want this to work with e.g., Trunk for pure client-side testing
|
||||||
|
// see lib.rs for hydration function instead
|
||||||
|
}
|
||||||
57
pkgs/packages/webserver/style/custom-bulma.scss
Normal file
57
pkgs/packages/webserver/style/custom-bulma.scss
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
@use "bulma/sass/utilities/initial-variables" as iv with (
|
||||||
|
$black: #0f0f0f,
|
||||||
|
$grey-darker: #11151c,
|
||||||
|
$grey-light: #dddddd,
|
||||||
|
$white: #ffffff,
|
||||||
|
|
||||||
|
$orange: #d26937,
|
||||||
|
$yellow: #b58900,
|
||||||
|
$green: #2aa889,
|
||||||
|
$cyan: #99d1ce,
|
||||||
|
$blue: #195466,
|
||||||
|
$red: #dc322f,
|
||||||
|
);
|
||||||
|
|
||||||
|
iv.$family-sans-serif: "Nunito", iv.$family-sans-serif;
|
||||||
|
iv.$family-monospace: "Hack", iv.$family-monospace;
|
||||||
|
|
||||||
|
@forward "bulma/sass/utilities/functions";
|
||||||
|
@use "bulma/sass/utilities/derived-variables" with (
|
||||||
|
$link: iv.$cyan,
|
||||||
|
$primary: iv.$cyan,
|
||||||
|
);
|
||||||
|
@forward "bulma/sass/utilities/controls";
|
||||||
|
|
||||||
|
@forward "bulma/sass/form";
|
||||||
|
|
||||||
|
@forward "bulma/sass/base" with (
|
||||||
|
$body-background-color: iv.$black,
|
||||||
|
$body-color: iv.$grey-light,
|
||||||
|
|
||||||
|
$hr-background-color: iv.$grey-light,
|
||||||
|
$hr-height: 1px,
|
||||||
|
);
|
||||||
|
@forward "bulma/sass/themes";
|
||||||
|
|
||||||
|
@forward "bulma/sass/elements/button";
|
||||||
|
@use "bulma/sass/elements/content" with (
|
||||||
|
$content-heading-weight: iv.$weight-semibold,
|
||||||
|
);
|
||||||
|
@use "bulma/sass/elements/notification";
|
||||||
|
@use "bulma/sass/elements/delete";
|
||||||
|
|
||||||
|
@use "bulma/sass/elements/title" with (
|
||||||
|
$title-color: iv.$cyan,
|
||||||
|
);
|
||||||
|
|
||||||
|
@forward "bulma/sass/grid/columns";
|
||||||
|
|
||||||
|
@forward "bulma/sass/helpers/typography";
|
||||||
|
@forward "bulma/sass/helpers/color";
|
||||||
|
|
||||||
|
@forward "bulma/sass/layout/container";
|
||||||
|
@forward "bulma/sass/layout/section";
|
||||||
|
|
||||||
|
@forward "bulma/sass/components/navbar" with (
|
||||||
|
$navbar-burger-color: iv.$grey-light,
|
||||||
|
);
|
||||||
48
pkgs/packages/webserver/style/fonts.scss
Normal file
48
pkgs/packages/webserver/style/fonts.scss
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
@use "@fontsource-utils/scss/src/mixins" as fontsource with (
|
||||||
|
$display: auto
|
||||||
|
);
|
||||||
|
@use "@fontsource-variable/arimo/scss/metadata.scss" as arimo;
|
||||||
|
@use "@fontsource-variable/nunito/scss/metadata.scss" as nunito;
|
||||||
|
|
||||||
|
@include fontsource.faces(
|
||||||
|
$metadata: nunito.$metadata,
|
||||||
|
$weights: (
|
||||||
|
300,
|
||||||
|
400,
|
||||||
|
500,
|
||||||
|
600,
|
||||||
|
700,
|
||||||
|
),
|
||||||
|
$subsets: latin,
|
||||||
|
$styles: (
|
||||||
|
normal,
|
||||||
|
italic,
|
||||||
|
),
|
||||||
|
$family: "Nunito",
|
||||||
|
$directory: "/@fontsource-variable/nunito"
|
||||||
|
);
|
||||||
|
|
||||||
|
@include fontsource.faces(
|
||||||
|
$metadata: arimo.$metadata,
|
||||||
|
$weights: 400,
|
||||||
|
$subsets: latin,
|
||||||
|
$styles: normal,
|
||||||
|
$family: "Arimo",
|
||||||
|
$directory: "/@fontsource-variable/arimo"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Hack *does* come with its own CSS, but it's broken and hasn't seen
|
||||||
|
// a release since https://github.com/source-foundry/Hack/issues/467
|
||||||
|
// was resolved.
|
||||||
|
|
||||||
|
$variants: regular normal 400, bold normal 700, italic italic 400, bolditalic italic 700;
|
||||||
|
|
||||||
|
@each $name, $style, $weights in $variants {
|
||||||
|
@font-face {
|
||||||
|
font-family: "Hack";
|
||||||
|
font-style: $style;
|
||||||
|
font-display: auto;
|
||||||
|
font-weight: $weights;
|
||||||
|
src: url("/hack-font/hack-#{$name}-subset.woff2") format("woff2-variations");
|
||||||
|
}
|
||||||
|
}
|
||||||
7
pkgs/packages/webserver/style/main.scss
Normal file
7
pkgs/packages/webserver/style/main.scss
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
@use "fonts";
|
||||||
|
@use "custom-bulma";
|
||||||
|
@use "typed";
|
||||||
|
|
||||||
|
#typed-welcome {
|
||||||
|
@include typed.typed-text(typed-welcome, "Welcome to tlater.net!", 1.2s);
|
||||||
|
}
|
||||||
149
pkgs/packages/webserver/style/typed.scss
Normal file
149
pkgs/packages/webserver/style/typed.scss
Normal file
|
|
@ -0,0 +1,149 @@
|
||||||
|
@use "sass:math";
|
||||||
|
@use "sass:list";
|
||||||
|
|
||||||
|
@mixin test {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin typed-text($id, $text, $duration) {
|
||||||
|
&::before {
|
||||||
|
@include typed($id, $text, $duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
@include cursor($id, 6s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Animate a blinking cursor.
|
||||||
|
@mixin cursor($id, $duration) {
|
||||||
|
$name: cursor-#{$id};
|
||||||
|
// The number of times we need to blink is = the number of full
|
||||||
|
// seconds (500ms * 2) that fit in the total duration, rounded up,
|
||||||
|
// and doubled.
|
||||||
|
$iterations: math.ceil(math.div($duration, 1s)) * 2;
|
||||||
|
|
||||||
|
animation: $name ease-in-out 500ms $iterations alternate;
|
||||||
|
content: " ";
|
||||||
|
|
||||||
|
@keyframes #{$name} {
|
||||||
|
from {
|
||||||
|
content: " ";
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
content: "█";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Animate a piece of text as if it was being typed by a human.
|
||||||
|
@mixin typed($id, $text, $duration) {
|
||||||
|
word-break: break-all;
|
||||||
|
|
||||||
|
// We don't want a linearly typed set of text, which makes this
|
||||||
|
// significantly more complex.
|
||||||
|
//
|
||||||
|
// CSS animations normally do not permit per-frame changes in
|
||||||
|
// duration (since the total animation time is fixed). This means we
|
||||||
|
// need to create multiple animations, and delay them so that they
|
||||||
|
// happen in the time sequence we want.
|
||||||
|
//
|
||||||
|
// We generate the raw values with _generate-animations, and then
|
||||||
|
// split up the result into the animation API.
|
||||||
|
$frames: str-length($text);
|
||||||
|
$animations: _generate-animations($id, $frames, 1.2s);
|
||||||
|
|
||||||
|
animation-name: _unzip($animations, 1);
|
||||||
|
animation-delay: _unzip($animations, 3);
|
||||||
|
animation-fill-mode: forwards;
|
||||||
|
content: "";
|
||||||
|
|
||||||
|
// We need to type each character in separate animations, see above
|
||||||
|
// comment.
|
||||||
|
@each $name, $character in $animations {
|
||||||
|
@keyframes #{$name} {
|
||||||
|
from {
|
||||||
|
content: str-slice($text, 0, $character);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
content: str-slice($text, 0, $character + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unzip a nested set of lists, taking the nth value of each sublist.
|
||||||
|
@function _unzip($lists, $i) {
|
||||||
|
$out: ();
|
||||||
|
$sep: comma;
|
||||||
|
@each $sublist in $lists {
|
||||||
|
$out: list.append($out, list.nth($sublist, $i), $sep);
|
||||||
|
}
|
||||||
|
@return $out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute the sum of all numbers in a list.
|
||||||
|
@function _sum($list) {
|
||||||
|
$out: 0;
|
||||||
|
@each $val in $list {
|
||||||
|
$out: $out + $val;
|
||||||
|
}
|
||||||
|
@return $out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Produce a list from a shorter list by repeating it up until size
|
||||||
|
/// $length.
|
||||||
|
@function _round-robin($base, $length) {
|
||||||
|
$out: ();
|
||||||
|
$sep: list.separator($out);
|
||||||
|
@for $i from 0 through $length {
|
||||||
|
$out: list.append($out, list.nth($base, $i % list.length($base) + 1));
|
||||||
|
}
|
||||||
|
@return $out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate the actual animation values.
|
||||||
|
///
|
||||||
|
/// This generates a nested list as:
|
||||||
|
///
|
||||||
|
/// (keyframe-name, index, start time)
|
||||||
|
///
|
||||||
|
/// The duration of each frame is taken from the internal $delays in a
|
||||||
|
/// round robin fashion, to give some amount of human-like variance to
|
||||||
|
/// the duration of each frame.
|
||||||
|
///
|
||||||
|
/// Start time is set to the time at which the frame should start to
|
||||||
|
/// achieve the desired frame-by-frame duration.
|
||||||
|
@function _generate-animations($id, $number, $total_duration) {
|
||||||
|
$out: ();
|
||||||
|
$sep: list.separator($out);
|
||||||
|
|
||||||
|
// A set of "human-like" delays for each typed character. In
|
||||||
|
// practice, my typing seems to be about 20-70ms, but it looks a bit
|
||||||
|
// nicer to increase all typing by 20ms to make the effect more
|
||||||
|
// noticeable.
|
||||||
|
//
|
||||||
|
// Numbers generated once with a random number generator, rather
|
||||||
|
// than using `math.random()`, since they end up in CSS verbatim,
|
||||||
|
// and the build would be non-reproducible if we didn't do it this
|
||||||
|
// way. Using `math.random() wouldn't change this dynamically each
|
||||||
|
// time the page loads anyway, so we don't really lose anything by
|
||||||
|
// pre-generating these numbers.
|
||||||
|
$delays: 69ms, 83ms, 49ms, 48ms, 52ms, 59ms, 40ms, 71ms, 80ms, 67ms;
|
||||||
|
|
||||||
|
@for $animation from 0 through $number {
|
||||||
|
$out: list.append(
|
||||||
|
$out,
|
||||||
|
(
|
||||||
|
type-#{$id}-#{$animation},
|
||||||
|
$animation,
|
||||||
|
_sum(_round_robin($delays, $animation))
|
||||||
|
),
|
||||||
|
$sep
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@return $out;
|
||||||
|
}
|
||||||
88
pkgs/packages/webserver/update.nu
Normal file
88
pkgs/packages/webserver/update.nu
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
const self = "pkgs/packages/webserver/package.nix"
|
||||||
|
let tmpdir = mktemp -d webserver-update.XXXXXXXXXX
|
||||||
|
|
||||||
|
let dependencies = {
|
||||||
|
fontsource-scss: (prefetch-npm @fontsource-utils/scss)
|
||||||
|
fontsource-arimo: (prefetch-npm @fontsource-variable/arimo)
|
||||||
|
fontsource-nunito: (prefetch-npm @fontsource-variable/nunito)
|
||||||
|
|
||||||
|
bulma: (prefetch-github jgthms bulma)
|
||||||
|
hack-font: (prefetch-github source-foundry Hack)
|
||||||
|
}
|
||||||
|
|
||||||
|
cd (git rev-parse --show-toplevel)
|
||||||
|
|
||||||
|
$dependencies | items {|name, metadata| $metadata | update-dependency $name }
|
||||||
|
|
||||||
|
rm -r $tmpdir
|
||||||
|
|
||||||
|
cd (dirname $self)
|
||||||
|
cargo update
|
||||||
|
|
||||||
|
def prefetch-npm [package: string] {
|
||||||
|
let metadata = http get $'https://registry.npmjs.org/($package)'
|
||||||
|
let version = $metadata.dist-tags.latest
|
||||||
|
let url = ($metadata.versions | get $version).dist.tarball
|
||||||
|
let tarball = ($tmpdir | path join "package.tgz")
|
||||||
|
|
||||||
|
http get $url | save -f $tarball
|
||||||
|
|
||||||
|
let hash = nix hash file $tarball
|
||||||
|
|
||||||
|
{
|
||||||
|
url: $url
|
||||||
|
version: $version
|
||||||
|
hash: $hash
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def prefetch-github [owner: string, repo: string] {
|
||||||
|
let metadata = http get $'https://api.github.com/repos/($owner)/($repo)/releases/latest'
|
||||||
|
let prefetch = nix-prefetch-github --rev $metadata.tag_name --json $owner $repo | from json
|
||||||
|
$prefetch | select hash | insert version ($metadata.name | str trim --left --char v)
|
||||||
|
}
|
||||||
|
|
||||||
|
def update-dependency [dependency_name: string] {
|
||||||
|
const replace_attribute_template = "
|
||||||
|
id: update-attribute
|
||||||
|
language: nix
|
||||||
|
utils:
|
||||||
|
is-attribute:
|
||||||
|
kind: string_fragment
|
||||||
|
inside:
|
||||||
|
kind: binding
|
||||||
|
stopBy: end
|
||||||
|
has:
|
||||||
|
field: attrpath
|
||||||
|
regex: '{attribute}'
|
||||||
|
|
||||||
|
rule:
|
||||||
|
matches: is-attribute
|
||||||
|
not:
|
||||||
|
regex: '{replacement}'
|
||||||
|
inside:
|
||||||
|
kind: binding
|
||||||
|
stopBy: end
|
||||||
|
has:
|
||||||
|
field: attrpath
|
||||||
|
regex: '{dependency_name}'
|
||||||
|
|
||||||
|
fix: '{replacement}'"
|
||||||
|
|
||||||
|
let template_data = (
|
||||||
|
$in | if ($in has url) {
|
||||||
|
[{attribute: url replacement: $in.url}]
|
||||||
|
} else { [] }
|
||||||
|
) ++ [
|
||||||
|
{attribute: version replacement: $in.version}
|
||||||
|
{attribute: hash replacement: $in.hash}
|
||||||
|
];
|
||||||
|
|
||||||
|
let ast_grep_rule = (
|
||||||
|
$template_data
|
||||||
|
| each { $in | insert dependency_name $dependency_name | format pattern $replace_attribute_template }
|
||||||
|
| str join "\n---\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
ast-grep scan --update-all --inline-rules $ast_grep_rule $self
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue