From cef1c0aca8c744e548998a83185c5c119390c95d Mon Sep 17 00:00:00 2001 From: arne Date: Sat, 8 Nov 2025 10:03:10 +0100 Subject: [PATCH] Initial commit --- .envrc | 4 + .gitignore | 21 ++ flake.lock | 26 ++ flake.nix | 19 ++ package-lock.json | 238 ++++++++++++++++++ package.json | 12 + public/index.html | 11 + shadow-cljs.edn | 21 ++ src/main/computersandblues/lodestone/app.cljs | 87 +++++++ 9 files changed, 439 insertions(+) create mode 100644 .envrc create mode 100644 .gitignore create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 public/index.html create mode 100644 shadow-cljs.edn create mode 100644 src/main/computersandblues/lodestone/app.cljs diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..c7786e3 --- /dev/null +++ b/.envrc @@ -0,0 +1,4 @@ +use flake + +export PGDATA="$(pwd)/postgres" +export PGHOST=$PGDATA diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4f82e06 --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +node_modules/ +public/js + +/target +/checkouts +/src/gen + +pom.xml +pom.xml.asc +*.iml +*.jar +*.log +.shadow-cljs +.idea +.lein-* +.nrepl-* +.DS_Store +.cpcache + +.hgignore +.hg/ diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..a429a6a --- /dev/null +++ b/flake.lock @@ -0,0 +1,26 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1761373498, + "narHash": "sha256-Q/uhWNvd7V7k1H1ZPMy/vkx3F8C13ZcdrKjO7Jv7v0c=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "6a08e6bb4e46ff7fcbb53d409b253f6bad8a28ce", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..d193d80 --- /dev/null +++ b/flake.nix @@ -0,0 +1,19 @@ +{ + description = "A very basic flake"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs"; + }; + + outputs = { nixpkgs, ... }: let + system = "x86_64-linux"; + pkgs = import nixpkgs { inherit system; }; + in { + devShells.${system}.default = pkgs.mkShell { + buildInputs = [ + pkgs.nodejs + pkgs.openjdk21 + ]; + }; + }; +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..e93e603 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,238 @@ +{ + "name": "computersandblues.lodestone", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "computersandblues.lodestone", + "version": "0.0.1", + "dependencies": { + "react": "^19.2.0", + "react-dom": "^19.2.0" + }, + "devDependencies": { + "shadow-cljs": "3.2.1" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/react": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", + "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", + "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.0" + } + }, + "node_modules/readline-sync": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/readline-sync/-/readline-sync-1.4.10.tgz", + "integrity": "sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/shadow-cljs": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/shadow-cljs/-/shadow-cljs-3.2.1.tgz", + "integrity": "sha512-xsTSHGUBGZqotbjdKTbKUuPaYoj41ozMPbylr0aQNHvpG+TEner7YTALPdthNGUsIseE+U7kNHV9HNTfRXc/Zw==", + "dev": true, + "license": "ISC", + "dependencies": { + "buffer": "^6.0.3", + "process": "^0.11.10", + "readline-sync": "^1.4.10", + "shadow-cljs-jar": "1.3.4", + "source-map-support": "^0.5.21", + "which": "^5.0.0", + "ws": "^8.18.1" + }, + "bin": { + "shadow-cljs": "cli/runner.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/shadow-cljs-jar": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/shadow-cljs-jar/-/shadow-cljs-jar-1.3.4.tgz", + "integrity": "sha512-cZB2pzVXBnhpJ6PQdsjO+j/MksR28mv4QD/hP/2y1fsIa9Z9RutYgh3N34FZ8Ktl4puAXaIGlct+gMCJ5BmwmA==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..590728e --- /dev/null +++ b/package.json @@ -0,0 +1,12 @@ +{ + "name": "computersandblues.lodestone", + "version": "0.0.1", + "private": true, + "devDependencies": { + "shadow-cljs": "3.2.1" + }, + "dependencies": { + "react": "^19.2.0", + "react-dom": "^19.2.0" + } +} diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..03b21bb --- /dev/null +++ b/public/index.html @@ -0,0 +1,11 @@ + + + + + Lodestone + + +
+ + + diff --git a/shadow-cljs.edn b/shadow-cljs.edn new file mode 100644 index 0000000..ca3369a --- /dev/null +++ b/shadow-cljs.edn @@ -0,0 +1,21 @@ +;; shadow-cljs configuration +{:source-paths + ["src/main" + "src/dev" + "src/test"] + + :dependencies + [[datascript/datascript "1.7.8"] ; unused + + [reagent/reagent "2.0.1"] + [io.github.tonsky/fast-edn "1.1.3"] + + [binaryage/devtools "1.0.7"] ; loaded automatically, see https://shadow-cljs.github.io/docs/UsersGuide.html#_preloads + ] + + :dev-http {8080 "public"} + + :builds + {:frontend + {:target :browser + :modules {:main {:init-fn computersandblues.lodestone.app/init}}}}} diff --git a/src/main/computersandblues/lodestone/app.cljs b/src/main/computersandblues/lodestone/app.cljs new file mode 100644 index 0000000..ffa7f52 --- /dev/null +++ b/src/main/computersandblues/lodestone/app.cljs @@ -0,0 +1,87 @@ +(ns computersandblues.lodestone.app + (:require [reagent.core :as r] + [reagent.dom.client :as rd] + [clojure.string :as str])) + +(defonce state (r/atom {:root nil + :query nil + ; TODO: Handle other lists + :favorites []})) + +; TODO: Login / Landing Page / Store bearer token in localstorage + +(defn fetch-favs [{:keys [server-url bearer-token]}] + ; TODO: Pagination + (let [url (str server-url "/api/v1/favourites") + auth-header (str "Bearer " bearer-token)] + (.. (js/fetch url + #js {:method "GET" + :headers #js {"Authorization" auth-header}}) + (then (fn [res] + (if (.-ok res) + (.then (.json res) + (fn [body] + {:raw res + :body (js->clj body {:keywordize-keys true})})) + (do + (println res) + (throw (ex-info "Could not fetch favorites" {:response res}))))))))) + +(defn search [] + [:input {:placeholder "Start typing to search…" + :on-change (fn [e] + (let [query (.. e -target -value)] + (swap! state assoc :query (if (str/blank? query) nil query)))) + :value (:query @state)}]) + +(defn user [{:keys [user]}] + (:username user)) + +(defn post [{:keys [post]}] + ; TODO: handle (:sensitive post) + ; TODO: handle attachments + [:article + [:div.users + [user {:user (:account post)}] + (when (seq (:mentions post)) + [:span.mentions + {:style #js {:color "#777"}} + " (mentioining " (->> (map-indexed (fn [idx account] + ^{:key idx} [user {:user account}]) + (:mentions post)) + (interleave (repeat ", ")) + (drop 1)) ")"])] + [:div.url [:a {:href (:url post)} (:url post)]] + [:pre (prn-str [:div.content {:dangerouslySetInnerHTML {:__html (:content post)}}])]]) + +(defn app [] + (let [favorites (:favorites @state) + query (:query @state) + matches? (if query + (partial re-find (js/RegExp query "i")) + (constantly true)) + matches (filter (fn [post] + (if query + (or (matches? (:content post)) + (matches? (-> post :account :acct)) ; search for url + username of poster + (some #(matches? (:username %)) (:mentions post))) ; search only for username of mentions + true)) favorites)] + [:div#app + [:h1 "Lodestone"] + [:h2 "Favorites"] + [:span (str "Loaded " (count favorites) " favorites" + (when query + (str ", displaying " (count matches) " matches")))] + [:div [search]] + [:ul (map-indexed (fn [idx favorite] + ^{:key idx} [:li [post {:post favorite}]]) matches)]])) + +(defn ^:dev/after-load render [] + (rd/render (:root @state) [app])) + +(defn init [] + (-> (fetch-favs {:server-url "https://post.lurk.org" + :bearer-token "CHANGEME"}) + (.then #(swap! state assoc :favorites (:body %)))) + (swap! state assoc :root (rd/create-root (.-body js/document))) + (render))