From 31aceb49c7ac1b614bca7f9b8faaa6142a2e629e Mon Sep 17 00:00:00 2001 From: "@s.roertgen" Date: Thu, 21 Nov 2024 16:27:08 +0100 Subject: [PATCH] some things are working --- karma.conf.js | 27 ++ package-lock.json | 208 ++++++++- package.json | 3 +- resources/public/css/output.css | 747 +++++++++++++++++++++++++++---- shadow-cljs.edn | 7 +- src/ied/components/icons.cljs | 63 +++ src/ied/components/resource.cljs | 108 +++++ src/ied/config.cljs | 2 + src/ied/core.cljs | 6 +- src/ied/db.cljs | 76 +++- src/ied/events.cljs | 495 ++++++++++++++++---- src/ied/nostr.cljs | 96 +++- src/ied/opencard/subs.cljs | 13 + src/ied/opencard/views.cljs | 331 ++++++++++++++ src/ied/routes.cljs | 13 +- src/ied/subs.cljs | 138 +++++- src/ied/utils.cljs | 113 +++++ src/ied/views.cljs | 246 +++++----- src/ied/views/add.cljs | 280 ++++++++++++ src/ied/views/resource.cljs | 43 ++ src/ied/views/search.cljs | 123 +++++ 21 files changed, 2767 insertions(+), 371 deletions(-) create mode 100644 karma.conf.js create mode 100644 src/ied/components/icons.cljs create mode 100644 src/ied/components/resource.cljs create mode 100644 src/ied/opencard/subs.cljs create mode 100644 src/ied/opencard/views.cljs create mode 100644 src/ied/utils.cljs create mode 100644 src/ied/views/add.cljs create mode 100644 src/ied/views/resource.cljs create mode 100644 src/ied/views/search.cljs diff --git a/karma.conf.js b/karma.conf.js new file mode 100644 index 0000000..5cf54d2 --- /dev/null +++ b/karma.conf.js @@ -0,0 +1,27 @@ +module.exports = function (config) { + var junitOutputDir = process.env.CIRCLE_TEST_REPORTS || "target/junit" + + config.set({ + browsers: ['ChromeHeadless'], + basePath: 'target', + files: ['karma-test.js'], + frameworks: ['cljs-test'], + plugins: [ + 'karma-cljs-test', + 'karma-chrome-launcher', + 'karma-junit-reporter' + ], + colors: true, + logLevel: config.LOG_INFO, + client: { + args: ['shadow.test.karma.init'] + }, + + // the default configuration + junitReporter: { + outputDir: junitOutputDir + '/karma', // results will be saved as outputDir/browserName.xml + outputFile: undefined, // if included, results will be saved as outputDir/browserName/outputFile + suite: '' // suite will become the package name attribute in xml testsuite element + } + }) +} diff --git a/package-lock.json b/package-lock.json index 0777e9f..2b3c9be 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,17 @@ { - "name": "ied", + "name": "edufeed", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "ied", + "name": "edufeed", "dependencies": { "@noble/secp256k1": "^2.1.0", "bech32": "^2.0.0", "js-confetti": "^0.12.0", "nostr-tools": "^2.7.2", "react": "17.0.2", + "react-beautiful-dnd": "^13.1.1", "react-dom": "17.0.2" }, "devDependencies": { @@ -31,6 +32,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@babel/runtime": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.6.tgz", + "integrity": "sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==", + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -237,6 +250,44 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz", + "integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==", + "license": "MIT", + "dependencies": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.13", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", + "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.7.tgz", + "integrity": "sha512-KUnDCJF5+AiZd8owLIeVHqmW9yM4sqmDVf2JRJiBMFkGvkoZ4/WyV2lL4zVsoinmRS/W3FeEdZLEWFRofnT2FQ==", + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-redux": { + "version": "7.1.33", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.33.tgz", + "integrity": "sha512-NF8m5AjWCkert+fosDsN3hAlHzpjSiXlVy9EgQEmLoBhaNXbmyeGs/aj5dQzKuF+/q+S7JQagorGDW8pJ28Hmg==", + "license": "MIT", + "dependencies": { + "@types/hoist-non-react-statics": "^3.3.0", + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0", + "redux": "^4.0.0" + } + }, "node_modules/ansi-regex": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", @@ -717,6 +768,15 @@ "node": "*" } }, + "node_modules/css-box-model": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", + "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", + "license": "MIT", + "dependencies": { + "tiny-invariant": "^1.0.6" + } + }, "node_modules/css-selector-tokenizer": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.8.0.tgz", @@ -739,6 +799,12 @@ "node": ">=4" } }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" + }, "node_modules/culori": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/culori/-/culori-3.3.0.tgz", @@ -857,10 +923,11 @@ "dev": true }, "node_modules/elliptic": { - "version": "6.5.5", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.5.tgz", - "integrity": "sha512-7EjbcmUm17NQFu4Pmgmq2olYMj8nwMnpcddByChSUjArp8F5DQWcIcpriwO4ZToLNAJig0yiyjswfyGNje/ixw==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.0.tgz", + "integrity": "sha512-dpwoQcLc/2WLQvJvLRHKZ+f9FgOdjnq11rurqwekGQygGPsYSK29OMMD2WalatiqQ+XGFDglTNixpPfI+lpaAA==", "dev": true, + "license": "MIT", "dependencies": { "bn.js": "^4.11.9", "brorand": "^1.1.0", @@ -1162,6 +1229,21 @@ "minimalistic-crypto-utils": "^1.0.1" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/https-browserify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", @@ -1349,6 +1431,12 @@ "safe-buffer": "^5.1.2" } }, + "node_modules/memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", + "license": "MIT" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -1359,10 +1447,11 @@ } }, "node_modules/micromatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", - "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, + "license": "MIT", "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -1872,6 +1961,23 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "dev": true }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/public-encrypt": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", @@ -1942,6 +2048,12 @@ } ] }, + "node_modules/raf-schd": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz", + "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==", + "license": "MIT" + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -1973,6 +2085,25 @@ "node": ">=0.10.0" } }, + "node_modules/react-beautiful-dnd": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz", + "integrity": "sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "^7.9.2", + "css-box-model": "^1.2.0", + "memoize-one": "^5.1.1", + "raf-schd": "^4.0.2", + "react-redux": "^7.2.0", + "redux": "^4.0.4", + "use-memo-one": "^1.1.1" + }, + "peerDependencies": { + "react": "^16.8.5 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.5 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-dom": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", @@ -1986,6 +2117,37 @@ "react": "17.0.2" } }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "license": "MIT" + }, + "node_modules/react-redux": { + "version": "7.2.9", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz", + "integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.15.4", + "@types/react-redux": "^7.1.20", + "hoist-non-react-statics": "^3.3.2", + "loose-envify": "^1.4.0", + "prop-types": "^15.7.2", + "react-is": "^17.0.2" + }, + "peerDependencies": { + "react": "^16.8.3 || ^17 || ^18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -2046,6 +2208,21 @@ "node": ">= 0.8.0" } }, + "node_modules/redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -2507,6 +2684,12 @@ "node": ">=0.6.0" } }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, "node_modules/to-arraybuffer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", @@ -2547,6 +2730,15 @@ "qs": "^6.11.2" } }, + "node_modules/use-memo-one": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz", + "integrity": "sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/util": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", diff --git a/package.json b/package.json index 2d11fc7..97455ac 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "ied", + "name": "edufeed", "scripts": { "ancient": "clojure -Sdeps '{:deps {com.github.liquidz/antq {:mvn/version \"RELEASE\"}}}' -m antq.core", "watch": "npx shadow-cljs watch app browser-test karma-test", @@ -12,6 +12,7 @@ "js-confetti": "^0.12.0", "nostr-tools": "^2.7.2", "react": "17.0.2", + "react-beautiful-dnd": "^13.1.1", "react-dom": "17.0.2" }, "devDependencies": { diff --git a/resources/public/css/output.css b/resources/public/css/output.css index 9134b02..3490aab 100644 --- a/resources/public/css/output.css +++ b/resources/public/css/output.css @@ -828,6 +828,18 @@ html { --tw-text-opacity: 1; color: var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity))); } + + .table tr.hover:hover, + .table tr.hover:nth-child(even):hover { + --tw-bg-opacity: 1; + background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity))); + } + + .table-zebra tr.hover:hover, + .table-zebra tr.hover:nth-child(even):hover { + --tw-bg-opacity: 1; + background-color: var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity))); + } } .btn { @@ -874,6 +886,12 @@ html { pointer-events: none; } +.btn-square { + height: 3rem; + width: 3rem; + padding: 0px; +} + .btn-circle { height: 3rem; width: 3rem; @@ -1329,23 +1347,6 @@ html { border-end-end-radius: inherit; } -.kbd { - display: inline-flex; - align-items: center; - justify-content: center; - border-radius: var(--rounded-btn, 0.5rem); - border-width: 1px; - border-color: var(--fallback-bc,oklch(var(--bc)/var(--tw-border-opacity))); - --tw-border-opacity: 0.2; - --tw-bg-opacity: 1; - background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity))); - padding-left: 0.5rem; - padding-right: 0.5rem; - border-bottom-width: 2px; - min-height: 2.2em; - min-width: 2.2em; -} - .menu { display: flex; flex-direction: column; @@ -1462,6 +1463,12 @@ html { opacity: 1; } +.modal-action { + display: flex; + margin-top: 1.5rem; + justify-content: flex-end; +} + :root:has(:is(.modal-open, .modal:target, .modal-toggle:checked + .modal, .modal[open])) { overflow: hidden; scrollbar-gutter: stable; @@ -1480,6 +1487,57 @@ html { align-items: center; } +.range { + height: 1.5rem; + width: 100%; + cursor: pointer; + -moz-appearance: none; + appearance: none; + -webkit-appearance: none; + --range-shdw: var(--fallback-bc,oklch(var(--bc)/1)); + overflow: hidden; + border-radius: var(--rounded-box, 1rem); + background-color: transparent; +} + +.range:focus { + outline: none; +} + +.select { + display: inline-flex; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + height: 3rem; + min-height: 3rem; + padding-inline-start: 1rem; + padding-inline-end: 2.5rem; + font-size: 0.875rem; + line-height: 1.25rem; + line-height: 2; + border-radius: var(--rounded-btn, 0.5rem); + border-width: 1px; + border-color: transparent; + --tw-bg-opacity: 1; + background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity))); + background-image: linear-gradient(45deg, transparent 50%, currentColor 50%), + linear-gradient(135deg, currentColor 50%, transparent 50%); + background-position: calc(100% - 20px) calc(1px + 50%), + calc(100% - 16.1px) calc(1px + 50%); + background-size: 4px 4px, + 4px 4px; + background-repeat: no-repeat; +} + +.select[multiple] { + height: auto; +} + .swap { position: relative; display: inline-grid; @@ -1539,6 +1597,11 @@ html { background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity))); } +.avatar-group { + display: flex; + overflow: hidden; +} + .avatar-group :where(.avatar) { overflow: hidden; border-radius: 9999px; @@ -1556,12 +1619,13 @@ html { color: var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity))); } -.badge-info { - border-color: transparent; +.badge-secondary { + --tw-border-opacity: 1; + border-color: var(--fallback-s,oklch(var(--s)/var(--tw-border-opacity))); --tw-bg-opacity: 1; - background-color: var(--fallback-in,oklch(var(--in)/var(--tw-bg-opacity))); + background-color: var(--fallback-s,oklch(var(--s)/var(--tw-bg-opacity))); --tw-text-opacity: 1; - color: var(--fallback-inc,oklch(var(--inc)/var(--tw-text-opacity))); + color: var(--fallback-sc,oklch(var(--sc)/var(--tw-text-opacity))); } .badge-outline.badge-primary { @@ -1569,9 +1633,9 @@ html { color: var(--fallback-p,oklch(var(--p)/var(--tw-text-opacity))); } -.badge-outline.badge-info { +.badge-outline.badge-secondary { --tw-text-opacity: 1; - color: var(--fallback-in,oklch(var(--in)/var(--tw-text-opacity))); + color: var(--fallback-s,oklch(var(--s)/var(--tw-text-opacity))); } .btm-nav > *.disabled, @@ -1611,6 +1675,10 @@ html { --btn-color: var(--fallback-p); } + .btn-neutral { + --btn-color: var(--fallback-n); + } + .btn-warning { --btn-color: var(--fallback-wa); } @@ -1654,6 +1722,10 @@ html { --btn-color: var(--p); } + .btn-neutral { + --btn-color: var(--n); + } + .btn-warning { --btn-color: var(--wa); } @@ -1663,6 +1735,12 @@ html { } } +.btn-neutral { + --tw-text-opacity: 1; + color: var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity))); + outline-color: var(--fallback-n,oklch(var(--n)/1)); +} + .btn-warning { --tw-text-opacity: 1; color: var(--fallback-wac,oklch(var(--wac)/var(--tw-text-opacity))); @@ -2204,6 +2282,12 @@ details.collapse summary::-webkit-details-marker { margin-top: 0; } +.mockup-phone .display { + overflow: hidden; + border-radius: 40px; + margin-top: -25px; +} + .mockup-browser .mockup-browser-toolbar .input { position: relative; margin-left: auto; @@ -2276,6 +2360,12 @@ details.collapse summary::-webkit-details-marker { 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)); } +.modal-action > :not([hidden]) ~ :not([hidden]) { + --tw-space-x-reverse: 0; + margin-right: calc(0.5rem * var(--tw-space-x-reverse)); + margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse))); +} + @keyframes modal-pop { 0% { opacity: 0; @@ -2305,6 +2395,65 @@ details.collapse summary::-webkit-details-marker { } } +.range:focus-visible::-webkit-slider-thumb { + --focus-shadow: 0 0 0 6px var(--fallback-b1,oklch(var(--b1)/1)) inset, 0 0 0 2rem var(--range-shdw) inset; +} + +.range:focus-visible::-moz-range-thumb { + --focus-shadow: 0 0 0 6px var(--fallback-b1,oklch(var(--b1)/1)) inset, 0 0 0 2rem var(--range-shdw) inset; +} + +.range::-webkit-slider-runnable-track { + height: 0.5rem; + width: 100%; + border-radius: var(--rounded-box, 1rem); + background-color: var(--fallback-bc,oklch(var(--bc)/0.1)); +} + +.range::-moz-range-track { + height: 0.5rem; + width: 100%; + border-radius: var(--rounded-box, 1rem); + background-color: var(--fallback-bc,oklch(var(--bc)/0.1)); +} + +.range::-webkit-slider-thumb { + position: relative; + height: 1.5rem; + width: 1.5rem; + border-radius: var(--rounded-box, 1rem); + border-style: none; + --tw-bg-opacity: 1; + background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity))); + appearance: none; + -webkit-appearance: none; + top: 50%; + color: var(--range-shdw); + transform: translateY(-50%); + --filler-size: 100rem; + --filler-offset: 0.6rem; + box-shadow: 0 0 0 3px var(--range-shdw) inset, + var(--focus-shadow, 0 0), + calc(var(--filler-size) * -1 - var(--filler-offset)) 0 0 var(--filler-size); +} + +.range::-moz-range-thumb { + position: relative; + height: 1.5rem; + width: 1.5rem; + border-radius: var(--rounded-box, 1rem); + border-style: none; + --tw-bg-opacity: 1; + background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity))); + top: 50%; + color: var(--range-shdw); + --filler-size: 100rem; + --filler-offset: 0.5rem; + box-shadow: 0 0 0 3px var(--range-shdw) inset, + var(--focus-shadow, 0 0), + calc(var(--filler-size) * -1 - var(--filler-offset)) 0 0 var(--filler-size); +} + @keyframes rating-pop { 0% { transform: translateY(-0.125em); @@ -2319,6 +2468,54 @@ details.collapse summary::-webkit-details-marker { } } +.select-bordered { + border-color: var(--fallback-bc,oklch(var(--bc)/0.2)); +} + +.select:focus { + box-shadow: none; + border-color: var(--fallback-bc,oklch(var(--bc)/0.2)); + outline-style: solid; + outline-width: 2px; + outline-offset: 2px; + outline-color: var(--fallback-bc,oklch(var(--bc)/0.2)); +} + +.select-disabled, + .select:disabled, + .select[disabled] { + cursor: not-allowed; + --tw-border-opacity: 1; + border-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity))); + --tw-bg-opacity: 1; + background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity))); + color: var(--fallback-bc,oklch(var(--bc)/0.4)); +} + +.select-disabled::-moz-placeholder, .select:disabled::-moz-placeholder, .select[disabled]::-moz-placeholder { + color: var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity))); + --tw-placeholder-opacity: 0.2; +} + +.select-disabled::placeholder, + .select:disabled::placeholder, + .select[disabled]::placeholder { + color: var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity))); + --tw-placeholder-opacity: 0.2; +} + +.select-multiple, + .select[multiple], + .select[size].select:not([size="1"]) { + background-image: none; + padding-right: 1rem; +} + +[dir="rtl"] .select { + background-position: calc(0% + 12px) calc(1px + 50%), + calc(0% + 16px) calc(1px + 50%); +} + @keyframes skeleton { from { background-position: 150%; @@ -2329,6 +2526,10 @@ details.collapse summary::-webkit-details-marker { } } +.textarea-bordered { + border-color: var(--fallback-bc,oklch(var(--bc)/0.2)); +} + .textarea:focus { box-shadow: none; border-color: var(--fallback-bc,oklch(var(--bc)/0.2)); @@ -2381,6 +2582,30 @@ details.collapse summary::-webkit-details-marker { padding-right: 0.438rem; } +.btn-square:where(.btn-xs) { + height: 1.5rem; + width: 1.5rem; + padding: 0px; +} + +.btn-square:where(.btn-sm) { + height: 2rem; + width: 2rem; + padding: 0px; +} + +.btn-square:where(.btn-md) { + height: 3rem; + width: 3rem; + padding: 0px; +} + +.btn-square:where(.btn-lg) { + height: 4rem; + width: 4rem; + padding: 0px; +} + .btn-circle:where(.btn-xs) { height: 1.5rem; width: 1.5rem; @@ -2481,13 +2706,26 @@ details.collapse summary::-webkit-details-marker { 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)); } -.kbd-sm { - padding-left: 0.25rem; - padding-right: 0.25rem; - font-size: 0.875rem; - line-height: 1.25rem; - min-height: 1.6em; - min-width: 1.6em; +.tooltip { + position: relative; + display: inline-block; + --tooltip-offset: calc(100% + 1px + var(--tooltip-tail, 0px)); +} + +.tooltip:before { + position: absolute; + pointer-events: none; + z-index: 1; + content: var(--tw-content); + --tw-content: attr(data-tip); +} + +.tooltip:before, .tooltip-top:before { + transform: translateX(-50%); + top: auto; + left: 50%; + right: auto; + bottom: var(--tooltip-offset); } .avatar.online:before { @@ -2608,6 +2846,94 @@ details.collapse summary::-webkit-details-marker { border-bottom-left-radius: 0px; } +.tooltip { + position: relative; + display: inline-block; + text-align: center; + --tooltip-tail: 0.1875rem; + --tooltip-color: var(--fallback-n,oklch(var(--n)/1)); + --tooltip-text-color: var(--fallback-nc,oklch(var(--nc)/1)); + --tooltip-tail-offset: calc(100% + 0.0625rem - var(--tooltip-tail)); +} + +.tooltip:before, +.tooltip:after { + opacity: 0; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter; + transition-delay: 100ms; + transition-duration: 200ms; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); +} + +.tooltip:after { + position: absolute; + content: ""; + border-style: solid; + border-width: var(--tooltip-tail, 0); + width: 0; + height: 0; + display: block; +} + +.tooltip:before { + max-width: 20rem; + white-space: normal; + border-radius: 0.25rem; + padding-left: 0.5rem; + padding-right: 0.5rem; + padding-top: 0.25rem; + padding-bottom: 0.25rem; + font-size: 0.875rem; + line-height: 1.25rem; + background-color: var(--tooltip-color); + color: var(--tooltip-text-color); + width: -moz-max-content; + width: max-content; +} + +.tooltip.tooltip-open:before { + opacity: 1; + transition-delay: 75ms; +} + +.tooltip.tooltip-open:after { + opacity: 1; + transition-delay: 75ms; +} + +.tooltip:hover:before { + opacity: 1; + transition-delay: 75ms; +} + +.tooltip:hover:after { + opacity: 1; + transition-delay: 75ms; +} + +.tooltip:has(:focus-visible):after, +.tooltip:has(:focus-visible):before { + opacity: 1; + transition-delay: 75ms; +} + +.tooltip:not([data-tip]):hover:before, +.tooltip:not([data-tip]):hover:after { + visibility: hidden; + opacity: 0; +} + +.tooltip:after, .tooltip-top:after { + transform: translateX(-50%); + border-color: var(--tooltip-color) transparent transparent transparent; + top: auto; + left: 50%; + right: auto; + bottom: var(--tooltip-tail-offset); +} + .visible { visibility: visible; } @@ -2624,10 +2950,6 @@ details.collapse summary::-webkit-details-marker { position: fixed; } -.absolute { - position: absolute; -} - .relative { position: relative; } @@ -2636,46 +2958,30 @@ details.collapse summary::-webkit-details-marker { position: sticky; } -.right-0 { - right: 0px; -} - -.bottom-0 { - bottom: 0px; -} - -.right-5 { - right: 1.25rem; -} - -.right-12 { - right: 3rem; -} - -.-bottom-2 { - bottom: -0.5rem; -} - -.-bottom-4 { - bottom: -1rem; -} - -.top-0 { - top: 0px; +.bottom-4 { + bottom: 1rem; } .left-0 { left: 0px; } -.z-\[1\] { - z-index: 1; +.right-4 { + right: 1rem; +} + +.top-0 { + top: 0px; } .z-50 { z-index: 50; } +.z-\[1\] { + z-index: 1; +} + .float-right { float: right; } @@ -2688,8 +2994,9 @@ details.collapse summary::-webkit-details-marker { margin: 0.5rem; } -.m-auto { - margin: auto; +.mx-1 { + margin-left: 0.25rem; + margin-right: 0.25rem; } .mx-auto { @@ -2697,11 +3004,29 @@ details.collapse summary::-webkit-details-marker { margin-right: auto; } +.my-1 { + margin-top: 0.25rem; + margin-bottom: 0.25rem; +} + .my-2 { margin-top: 0.5rem; margin-bottom: 0.5rem; } +.my-4 { + margin-top: 1rem; + margin-bottom: 1rem; +} + +.mb-0 { + margin-bottom: 0px; +} + +.ml-0 { + margin-left: 0px; +} + .ml-auto { margin-left: auto; } @@ -2714,18 +3039,49 @@ details.collapse summary::-webkit-details-marker { margin-right: 0.5rem; } +.mr-auto { + margin-right: auto; +} + +.mt-1 { + margin-top: 0.25rem; +} + +.mt-2 { + margin-top: 0.5rem; +} + .mt-3 { margin-top: 0.75rem; } -.mt-6 { - margin-top: 1.5rem; +.mt-auto { + margin-top: auto; +} + +.line-clamp-3 { + overflow: hidden; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 3; } .flex { display: flex; } +.grid { + display: grid; +} + +.hidden { + display: none; +} + +.h-4 { + height: 1rem; +} + .h-48 { height: 12rem; } @@ -2734,38 +3090,82 @@ details.collapse summary::-webkit-details-marker { height: 1.25rem; } +.h-6 { + height: 1.5rem; +} + .h-64 { height: 16rem; } -.h-12 { - height: 3rem; +.max-h-\[40vh\] { + max-height: 40vh; } -.min-h-\[620px\] { - min-height: 620px; +.max-h-screen { + max-height: 100vh; +} + +.min-h-16 { + min-height: 4rem; +} + +.min-h-32 { + min-height: 8rem; +} + +.min-h-full { + min-height: 100%; +} + +.w-1\/2 { + width: 50%; +} + +.w-1\/4 { + width: 25%; +} + +.w-1\/6 { + width: 16.666667%; } .w-10 { width: 2.5rem; } -.w-24 { - width: 6rem; +.w-3\/4 { + width: 75%; +} + +.w-4 { + width: 1rem; } .w-5 { width: 1.25rem; } +.w-5\/6 { + width: 83.333333%; +} + .w-52 { width: 13rem; } +.w-6 { + width: 1.5rem; +} + .w-64 { width: 16rem; } +.w-8 { + width: 2rem; +} + .w-96 { width: 24rem; } @@ -2774,14 +3174,6 @@ details.collapse summary::-webkit-details-marker { width: 100%; } -.w-1\/2 { - width: 50%; -} - -.w-12 { - width: 3rem; -} - .max-w-xs { max-width: 20rem; } @@ -2798,6 +3190,10 @@ details.collapse summary::-webkit-details-marker { flex-grow: 1; } +.basis-5\/6 { + flex-basis: 83.333333%; +} + @keyframes flyIn { 0% { transform: translateX(-100%); @@ -2814,10 +3210,18 @@ details.collapse summary::-webkit-details-marker { animation: flyIn 0.5s ease-out forwards; } +.cursor-grab { + cursor: grab; +} + .cursor-pointer { cursor: pointer; } +.grid-cols-4 { + grid-template-columns: repeat(4, minmax(0, 1fr)); +} + .flex-row { flex-direction: row; } @@ -2830,32 +3234,72 @@ details.collapse summary::-webkit-details-marker { flex-wrap: wrap; } +.flex-nowrap { + flex-wrap: nowrap; +} + .items-center { align-items: center; } -.justify-end { - justify-content: flex-end; +.justify-between { + justify-content: space-between; } -.justify-center { - justify-content: center; +.gap-1 { + gap: 0.25rem; } .gap-2 { gap: 0.5rem; } +.gap-4 { + gap: 1rem; +} + +.-space-x-6 > :not([hidden]) ~ :not([hidden]) { + --tw-space-x-reverse: 0; + margin-right: calc(-1.5rem * var(--tw-space-x-reverse)); + margin-left: calc(-1.5rem * calc(1 - var(--tw-space-x-reverse))); +} + .space-y-4 > :not([hidden]) ~ :not([hidden]) { --tw-space-y-reverse: 0; margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse))); margin-bottom: calc(1rem * var(--tw-space-y-reverse)); } -.space-y-2 > :not([hidden]) ~ :not([hidden]) { - --tw-space-y-reverse: 0; - margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse))); - margin-bottom: calc(0.5rem * var(--tw-space-y-reverse)); +.divide-y > :not([hidden]) ~ :not([hidden]) { + --tw-divide-y-reverse: 0; + border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse))); + border-bottom-width: calc(1px * var(--tw-divide-y-reverse)); +} + +.divide-slate-500 > :not([hidden]) ~ :not([hidden]) { + --tw-divide-opacity: 1; + border-color: rgb(100 116 139 / var(--tw-divide-opacity)); +} + +.divide-slate-700 > :not([hidden]) ~ :not([hidden]) { + --tw-divide-opacity: 1; + border-color: rgb(51 65 85 / var(--tw-divide-opacity)); +} + +.self-end { + align-self: flex-end; +} + +.overflow-auto { + overflow: auto; +} + +.overflow-hidden { + overflow: hidden; +} + +.overflow-y-auto { + overflow-y: auto; } .truncate { @@ -2864,8 +3308,12 @@ details.collapse summary::-webkit-details-marker { white-space: nowrap; } -.break-all { - word-break: break-all; +.text-ellipsis { + text-overflow: ellipsis; +} + +.text-wrap { + text-wrap: wrap; } .rounded { @@ -2880,8 +3328,8 @@ details.collapse summary::-webkit-details-marker { border-radius: 9999px; } -.rounded-xl { - border-radius: 0.75rem; +.rounded-md { + border-radius: 0.375rem; } .border { @@ -2896,9 +3344,8 @@ details.collapse summary::-webkit-details-marker { border-style: solid; } -.border-white { - --tw-border-opacity: 1; - border-color: rgb(255 255 255 / var(--tw-border-opacity)); +.border-dashed { + border-style: dashed; } .border-base-300 { @@ -2906,6 +3353,26 @@ details.collapse summary::-webkit-details-marker { border-color: var(--fallback-b3,oklch(var(--b3)/var(--tw-border-opacity))); } +.border-green-500 { + --tw-border-opacity: 1; + border-color: rgb(34 197 94 / var(--tw-border-opacity)); +} + +.border-red-200 { + --tw-border-opacity: 1; + border-color: rgb(254 202 202 / var(--tw-border-opacity)); +} + +.border-slate-100 { + --tw-border-opacity: 1; + border-color: rgb(241 245 249 / var(--tw-border-opacity)); +} + +.border-white { + --tw-border-opacity: 1; + border-color: rgb(255 255 255 / var(--tw-border-opacity)); +} + .bg-base-100 { --tw-bg-opacity: 1; background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity))); @@ -2916,11 +3383,50 @@ details.collapse summary::-webkit-details-marker { background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity))); } +.bg-blue-500 { + --tw-bg-opacity: 1; + background-color: rgb(59 130 246 / var(--tw-bg-opacity)); +} + +.bg-green-200 { + --tw-bg-opacity: 1; + background-color: rgb(187 247 208 / var(--tw-bg-opacity)); +} + .bg-green-500 { --tw-bg-opacity: 1; background-color: rgb(34 197 94 / var(--tw-bg-opacity)); } +.bg-neutral { + --tw-bg-opacity: 1; + background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity))); +} + +.bg-primary { + --tw-bg-opacity: 1; + background-color: var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity))); +} + +.bg-red-200 { + --tw-bg-opacity: 1; + background-color: rgb(254 202 202 / var(--tw-bg-opacity)); +} + +.bg-slate-100 { + --tw-bg-opacity: 1; + background-color: rgb(241 245 249 / var(--tw-bg-opacity)); +} + +.bg-slate-200 { + --tw-bg-opacity: 1; + background-color: rgb(226 232 240 / var(--tw-bg-opacity)); +} + +.bg-transparent { + background-color: transparent; +} + .bg-white { --tw-bg-opacity: 1; background-color: rgb(255 255 255 / var(--tw-bg-opacity)); @@ -2940,14 +3446,21 @@ details.collapse summary::-webkit-details-marker { padding: 0.5rem; } +.p-3 { + padding: 0.75rem; +} + +.p-4 { + padding: 1rem; +} + .py-4 { padding-top: 1rem; padding-bottom: 1rem; } -.py-2 { - padding-top: 0.5rem; - padding-bottom: 0.5rem; +.pl-2 { + padding-left: 0.5rem; } .text-2xl { @@ -2978,12 +3491,31 @@ details.collapse summary::-webkit-details-marker { color: rgb(0 0 0 / var(--tw-text-opacity)); } +.text-white { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity)); +} + +.opacity-50 { + opacity: 0.5; +} + +.opacity-70 { + opacity: 0.7; +} + .shadow { --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color); box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); } +.shadow-lg { + --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + .shadow-xl { --tw-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); --tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color); @@ -2994,11 +3526,30 @@ details.collapse summary::-webkit-details-marker { 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\:cursor-grab:hover { + cursor: grab; +} + +.hover\:bg-base-200:hover { + --tw-bg-opacity: 1; + background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity))); +} + +.hover\:bg-blue-600:hover { + --tw-bg-opacity: 1; + background-color: rgb(37 99 235 / var(--tw-bg-opacity)); +} + .hover\:bg-orange-500:hover { --tw-bg-opacity: 1; background-color: rgb(249 115 22 / var(--tw-bg-opacity)); } +.hover\:bg-yellow-200:hover { + --tw-bg-opacity: 1; + background-color: rgb(254 240 138 / var(--tw-bg-opacity)); +} + .hover\:text-black:hover { --tw-text-opacity: 1; color: rgb(0 0 0 / var(--tw-text-opacity)); diff --git a/shadow-cljs.edn b/shadow-cljs.edn index 50a7259..13e0be1 100644 --- a/shadow-cljs.edn +++ b/shadow-cljs.edn @@ -10,10 +10,11 @@ [clj-commons/pushy "0.3.10"] [binaryage/devtools "1.0.6"] [day8.re-frame/re-frame-10x "1.9.3"] - + [day8.re-frame/http-fx "0.2.4"] [funcool/promesa "11.0.678"] - [nilenso/wscljs "0.2.0"] - ] + [markdown-to-hiccup "0.6.2"] + [superstructor/re-frame-fetch-fx "0.4.0"] + [nilenso/wscljs "0.2.0"]] :dev-http {8280 "resources/public" diff --git a/src/ied/components/icons.cljs b/src/ied/components/icons.cljs new file mode 100644 index 0000000..6a2fe27 --- /dev/null +++ b/src/ied/components/icons.cljs @@ -0,0 +1,63 @@ +(ns ied.components.icons) + +(defn close-icon [] + [:svg + {:class "h-6 w-6" + :fill "" + :xmlns "http://www.w3.org/2000/svg" + :viewBox "0 0 512 512"} + (comment + "!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.") + [:path + {:d + "M256 48a208 208 0 1 1 0 416 208 208 0 1 1 0-416zm0 464A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM175 175c-9.4 9.4-9.4 24.6 0 33.9l47 47-47 47c-9.4 9.4-9.4 24.6 0 33.9s24.6 9.4 33.9 0l47-47 47 47c9.4 9.4 24.6 9.4 33.9 0s9.4-24.6 0-33.9l-47-47 47-47c9.4-9.4 9.4-24.6 0-33.9s-24.6-9.4-33.9 0l-47 47-47-47c-9.4-9.4-24.6-9.4-33.9 0z"}]]) + +(defn add + ([] + (add "")) + ([fill-color] + [:svg + + {:class "h-6 w-6" + :fill fill-color + :xmlns "http://www.w3.org/2000/svg", :viewBox "0 0 448 512"} + (comment + "!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.") + [:path + {:d + "M256 80c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 144L48 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l144 0 0 144c0 17.7 14.3 32 32 32s32-14.3 32-32l0-144 144 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-144 0 0-144z"}]])) + +(defn move [] + [:svg + {:class "h-6 w-6" + :fill "" + :xmlns "http://www.w3.org/2000/svg", :viewBox "0 0 512 512"} + (comment + "!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.") + [:path + {:d + "M278.6 9.4c-12.5-12.5-32.8-12.5-45.3 0l-64 64c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l9.4-9.4L224 224l-114.7 0 9.4-9.4c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-64 64c-12.5 12.5-12.5 32.8 0 45.3l64 64c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3l-9.4-9.4L224 288l0 114.7-9.4-9.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l64 64c12.5 12.5 32.8 12.5 45.3 0l64-64c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-9.4 9.4L288 288l114.7 0-9.4 9.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l64-64c12.5-12.5 12.5-32.8 0-45.3l-64-64c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l9.4 9.4L288 224l0-114.7 9.4 9.4c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3l-64-64z"}]]) + +(defn grid [] + [:svg + {:xmlns "http://www.w3.org/2000/svg", + :width "16", + :height "16", + :fill "currentColor", + :class "bi bi-grid", + :viewBox "0 0 16 16"} + [:path + {:d + "M1 2.5A1.5 1.5 0 0 1 2.5 1h3A1.5 1.5 0 0 1 7 2.5v3A1.5 1.5 0 0 1 5.5 7h-3A1.5 1.5 0 0 1 1 5.5zM2.5 2a.5.5 0 0 0-.5.5v3a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 0 0-.5-.5zm6.5.5A1.5 1.5 0 0 1 10.5 1h3A1.5 1.5 0 0 1 15 2.5v3A1.5 1.5 0 0 1 13.5 7h-3A1.5 1.5 0 0 1 9 5.5zm1.5-.5a.5.5 0 0 0-.5.5v3a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 0 0-.5-.5zM1 10.5A1.5 1.5 0 0 1 2.5 9h3A1.5 1.5 0 0 1 7 10.5v3A1.5 1.5 0 0 1 5.5 15h-3A1.5 1.5 0 0 1 1 13.5zm1.5-.5a.5.5 0 0 0-.5.5v3a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 0 0-.5-.5zm6.5.5A1.5 1.5 0 0 1 10.5 9h3a1.5 1.5 0 0 1 1.5 1.5v3a1.5 1.5 0 0 1-1.5 1.5h-3A1.5 1.5 0 0 1 9 13.5zm1.5-.5a.5.5 0 0 0-.5.5v3a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 0 0-.5-.5z"}]]) + +(defn pencil [] + [:svg + {:xmlns "http://www.w3.org/2000/svg", + :width "16", + :height "16", + :fill "currentColor", + :class "bi bi-pencil", + :viewBox "0 0 16 16"} + [:path + {:d + "M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325"}]]) diff --git a/src/ied/components/resource.cljs b/src/ied/components/resource.cljs new file mode 100644 index 0000000..9b5a8f5 --- /dev/null +++ b/src/ied/components/resource.cljs @@ -0,0 +1,108 @@ +(ns ied.components.resource + (:require [re-frame.core :as re-frame] + [clojure.string :as str] + [ied.nostr :as nostr] + [ied.subs :as subs] + [ied.events :as events])) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Components used to display info about resources +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn about-tags [[event]] + (doall + (for [about (nostr/get-about-names-from-metadata-event event)] + [:div {:class "badge badge-primary m-1 truncate " + :key about} about]))) + +(defn grouped-about-tags [[group-key values]] + ;; group key is the identifier of the concept + ;; values is an array of events that referenced the concept + (let [_ (println "values " values) + pubkeys (map :pubkey values) + profiles (re-frame/subscribe [::subs/profiles pubkeys]) + user-language (re-frame/subscribe [::subs/user-language]) + _ (js/console.log "group key and values" (clj->js values)) + _ (js/console.log (clj->js @profiles))] + [:div {:on-click #(re-frame/dispatch [::events/handle-filter-search ["about.id" group-key]]) + :data-tip (str + (str/join + ", " + (remove str/blank? (map (fn [[_ p]] (get p :display_name "unbekannt")) @profiles))) + (if (> (count @profiles) 1) + " haben" + " hat") + " das zugeordnet.") + :class " tooltip "} + [:div {:class "badge badge-primary m-1 truncate cursor-pointer"} + (vals (:about-label (first (filter #(= @user-language (:label-language %)) values))))]])) + +;; Helper function to extract the information and associate it with `about` +(defn extract-about-info [tags event] + (let [abouts (map (fn [e] {:about-id (second e) + :label-language (nth e 3 "") + :about-label {(keyword (nth e 3 "")) (nth e 2)} + :pubkey (:pubkey event) + :id (:id event)}) + tags) + _ (.log js/console "abouts " (clj->js abouts))] + abouts)) + +;; Flatten and group by `about-id` +(defn group-by-for-skos-tags + ;; TODO update this docstring + "Expects an array of kind 30142 skos tag arrays, like + ``` + [ + [ + ['about' 'id' 'val' 'lang'] + ['about' 'id' 'val' 'lang'] + ] + [ + ['about' 'id' 'val' 'lang'] + ] + ] + + Returns: + - a map of grouped by the ids of the skos tags + ``` + " + [events name] + (println "events array" events) + (let [tags-array (map + (fn [e] + (filter #(= name (first %)) e)) + (map :tags events)) + extracted-about (into [] (apply concat (map extract-about-info tags-array events))) + grouped-about (group-by :about-id extracted-about)] + grouped-about)) + +(defn skos-tags [[events name]] + (let [grouped (group-by-for-skos-tags events name)] + (doall + (for [[k v] grouped] + ^{:key k} [grouped-about-tags [k v]])))) + +(defn keywords-component [kw] + ^{:key kw}[:div {:on-click #(do + (re-frame/dispatch [::events/navigate [:search-view]]) + (re-frame/dispatch [::events/handle-filter-search ["keywords" kw]])) + :class "badge badge-secondary m-1 cursor-pointer"} + (str "#" kw)]) + +; (defn keywords-component [kw] +; [:div {:on-click #(re-frame/dispatch +; [::events/navigate [:search-view]] +; [::events/handle-filter-search ["keywords" kw]]) +; :class "badge badge-secondary m-1 cursor-pointer"} +; (str "#" kw)]) + +(defn authors-component [event] + (let [creators (nostr/get-creators-from-metadata-event event)] + [:div {:class "flex flex-row"} + (doall + (for [creator creators] + ^{:key (:name creator)} [:p {:class "mx-1"} (:name creator)]))])) + +(comment + (hash "https://w3id.org/kim/hcrt/scheme")) diff --git a/src/ied/config.cljs b/src/ied/config.cljs index 5811284..38fae4c 100644 --- a/src/ied/config.cljs +++ b/src/ied/config.cljs @@ -3,3 +3,5 @@ (def debug? ^boolean goog.DEBUG) +(def typesense-uri "http://localhost:8108/collections/amb/documents/") + diff --git a/src/ied/core.cljs b/src/ied/core.cljs index 77ea58d..5970af8 100644 --- a/src/ied/core.cljs +++ b/src/ied/core.cljs @@ -5,6 +5,7 @@ [ied.events :as events] [ied.routes :as routes] [ied.views :as views] + [ied.subs :as subs] [ied.config :as config])) (defn dev-setup [] @@ -18,9 +19,10 @@ (rdom/render [views/main-panel] root-el))) (defn init [] + (let [default-relays (re-frame/subscribe [::subs/default-relays])] (routes/start!) (re-frame/dispatch-sync [::events/initialize-db]) - (re-frame/dispatch [::events/connect-to-default-relays]) + (re-frame/dispatch [::events/connect-to-default-relays @default-relays]) (re-frame/dispatch [::events/set-visit-timestamp]) (dev-setup) - (mount-root)) + (mount-root))) diff --git a/src/ied/db.cljs b/src/ied/db.cljs index cd4e6ee..b25ef33 100644 --- a/src/ied/db.cljs +++ b/src/ied/db.cljs @@ -1,35 +1,69 @@ -(ns ied.db) +(ns ied.db + (:require + [ied.config :as config])) + +(def default-opencard-board + {:id 1 + :title "Default board" + :lists [{:id 1 :items [{:id 1 :content "*Item 1*"} {:id 2 :content "Item 2"}]} + {:id 2 :items [{:id 3 :content "Item 3"} {:id 4 :content "Item 4"}]} + {:id 3 :items [{:id 5 :content "Item 5"} {:id 6 :content "Item 6"}]} + {:id 4 :items [{:id 7 :content "Item 7"} {:id 8 :content "Item 8"}]}]}) (def default-db {:name "re-frame" :current-path nil + :concept-schemes {} + :confetti false :show-add-event false :events #{} + :md-form-resource nil + :selected-md-scheme nil :pk nil :sk nil :list-kinds [30001 30004] - :default-relays [{:name "strfry-1" - :uri "http://localhost:7777" - :id (random-uuid) - :status "disconnected"} - {:name "strfry-2" - :uri "http://localhost:7778" - :id (random-uuid) - :status "disconnected"} - {:name "rust-relay" - :uri "http://localhost:4445" - :id (random-uuid) - :status "disconnected"} - ; {:name "damus" - ; :uri "wss://relay.damus.io" - ; :status "disconnected"} - {:name "SC24" - :uri "wss://relay.sc24.steffen-roertgen.de" - :status "disconnected"} - ] + :opencard-kinds [30043 30044 30045] + :opencard-boards [default-opencard-board] + :follow-sets [30000] + :resource-to-add nil + :default-relays (concat + (if config/debug? + [{:name "strfry-1" + :uri "http://localhost:7777" + :id (random-uuid) + :status "disconnected" + :type ["outbox" "inbox"]} + {:name "strfry-2" + :uri "http://localhost:7778" + :id (random-uuid) + :status "disconnected" + :type ["outbox" "inbox"]} + {:name "rust-relay" + :uri "http://localhost:4445" + :id (random-uuid) + :status "disconnected" + :type ["outbox" "inbox"]}] + [{:name "SC24" + :uri "wss://relay.sc24.steffen-roertgen.de" + :status "disconnected" + :type ["outbox" "inbox"]}]) + [{:name "Purplepages" + :uri "wss://purplepag.es" + :status "disconnected" + :type ["search"]}]) :selected-events #{} :selected-list-ids #{} :show-lists-modal false :show-create-list-modal false :show-event-data-modal false - :sockets []}) + :sockets [] + :search-results nil + :user-language "de"}) + +(comment + (filter + (fn [s] + (some + #(= "search" %) + (:type s))) + (-> default-db :default-relays))) diff --git a/src/ied/events.cljs b/src/ied/events.cljs index 21da542..7590c46 100644 --- a/src/ied/events.cljs +++ b/src/ied/events.cljs @@ -1,22 +1,22 @@ (ns ied.events (:require [re-frame.core :as re-frame] + [ied.config :as config] [ied.db :as db] [day8.re-frame.tracing :refer-macros [fn-traced]] - [ied.subs :as subs] [ied.nostr :as nostr] - + [day8.re-frame.http-fx] [promesa.core :as p] [wscljs.client :as ws] [wscljs.format :as fmt] [clojure.string :as str] [clojure.set :as set] + [superstructor.re-frame.fetch-fx] + [ajax.core :as ajax] ["js-confetti" :as jsConfetti] [js-confetti :as jsConfetti])) -(def list-kinds [30001 30004]) - (re-frame/reg-event-db ::initialize-db (fn-traced [_ _] @@ -38,15 +38,37 @@ (fn-traced [{:keys [db]} [_ route]] {:db (assoc db :route route)})) +(def confetti-instance + (new jsConfetti)) + +(re-frame/reg-fx + ::add-confetti + (fn [cofx _] + (let [visited-at (-> cofx :db :visited-at) + now (quot (.now js/Date) 1000) + diff (- now visited-at)] + (when (-> cofx :db :confetti) + (when (>= diff 5) + (.addConfetti confetti-instance (clj->js {:emojis ["😺" "🐈‍⬛" "🦄"]})))) + {}))) + +(re-frame/reg-fx + ::relay-list + (fn [event] + (when (= 30002 (:kind event)) + (.log js/console "got a relay list!")))) + ;; Database Event? (re-frame/reg-event-fx ::save-event ;; TODO if EOSE retrieved end connection identified by uri (fn-traced [{:keys [db]} [_ [uri raw-event]]] (let [event (nth raw-event 2 raw-event)] + (when (and (= (first raw-event) "EVENT")) - {:dispatch [::add-confetti] + {:fx [[::add-confetti] + [::relay-list event]] :db (update db :events conj event)})))) (defn handlers @@ -72,15 +94,14 @@ (re-frame/reg-event-fx ::load-events (fn-traced [cofx [_ ws-uri]] - {::load-events-fx ws-uri})) + {::load-events-fx [ws-uri (-> cofx :db :sockets)]})) (re-frame/reg-fx ::load-events-fx - (fn [ws-uri] + (fn [[ws-uri sockets]] (println "loading events") - (let [sockets (re-frame/subscribe [::subs/sockets]) - target-ws (first (filter #(= ws-uri (:uri %)) @sockets))] + (let [target-ws (first (filter #(= ws-uri (:uri %)) sockets))] (ws/send (:socket target-ws) ["REQ" "424242" {:kinds [30004 30142] :limit 100}] fmt/json) ; (ws/close (:socket (first @sockets))) ;; should be handled otherwise (?) @@ -115,13 +136,12 @@ (re-frame/reg-event-fx ::connect-to-websocket (fn-traced [{:keys [db]} [_ ws-uri]] - {::connect-to-websocket-fx ws-uri})) + {::connect-to-websocket-fx [ws-uri (:sockets db)]})) (re-frame/reg-fx ::connect-to-websocket-fx - (fn [ws-uri] - (let [sockets (re-frame/subscribe [::subs/sockets]) - target-ws (first (filter #(= ws-uri (:uri %)) @sockets))] + (fn [[ws-uri sockets]] + (let [target-ws (first (filter #(= ws-uri (:uri %)) sockets))] (re-frame/dispatch [::create-websocket target-ws])))) ;; TODO use id to close socket @@ -130,20 +150,18 @@ (re-frame/reg-event-fx ::close-connection-to-websocket (fn-traced [{:keys [db]} [_ ws-uri]] - {::close-connection-to-websocket-fx ws-uri})) + {::close-connection-to-websocket-fx [ws-uri (:sockets db)]})) (re-frame/reg-fx ::close-connection-to-websocket-fx - (fn [ws-uri] - (let [sockets (re-frame/subscribe [::subs/sockets])] - (ws/close (:socket (first (filter #(= ws-uri (:uri %)) @sockets)))) - (re-frame/dispatch [::update-ws-connection-status ws-uri "disconnected"])))) + (fn [[ws-uri sockets]] + (ws/close (:socket (first (filter #(= ws-uri (:uri %)) sockets)))) + (re-frame/dispatch [::update-ws-connection-status ws-uri "disconnected"]))) (re-frame/reg-event-fx ::connect-to-default-relays - (fn-traced [cofx [_]] - (let [default-relays (re-frame/subscribe [::subs/default-relays])] - {::connect-to-default-relays-fx @default-relays}))) + (fn-traced [cofx [_ default-relays]] + {::connect-to-default-relays-fx default-relays})) (re-frame/reg-fx ::connect-to-default-relays-fx @@ -154,9 +172,14 @@ ;; TODO (re-frame/reg-event-fx - ::send-to-relays + ::publish-signed-event (fn-traced [cofx [_ signedEvent]] - (let [sockets (-> cofx :db :sockets)] + (let [sockets (filter + (fn [s] + (some #(= "outbox" %) + (:type s))) + (-> cofx :db :sockets)) + _ (.log js/console "available sockets " (clj->js sockets))] {::send-to-relays-fx [sockets signedEvent]}))) (re-frame/reg-fx @@ -174,13 +197,12 @@ (re-frame/reg-event-fx ::remove-websocket (fn-traced [{:keys [db]} [_ socket]] - {::remove-websocket-fx (:id socket)})) + {::remove-websocket-fx [(:id socket) (:sockets db)]})) (re-frame/reg-fx ::remove-websocket-fx - (fn [id] - (let [sockets (re-frame/subscribe [::subs/sockets]) - filtered (filter #(not= id (:id %)) @sockets)] ;; TODO maybe this can also be done using the URI + (fn [[id sockets]] + (let [filtered (filter #(not= id (:id %)) sockets)] ;; TODO maybe this can also be done using the URI (re-frame/dispatch [::update-websockets filtered])))) (re-frame/reg-event-db @@ -192,34 +214,48 @@ (re-frame/reg-event-fx ::publish-resource [(re-frame/inject-cofx :now)] - (fn-traced [cofx [_ resource]] - (let [event {:kind 30142 + (fn-traced [cofx _] + ;; TODO hier muss ich den ansatz ändern, es ist vmtl sinnvoller durch alle keys in md-form-resource zu iterieren und abhängig davon die tags zu bauen + ;; -> vllt kann ich einfach eine Funktion bauen, die md-form resource amb-konform verwandet und dann die funktion zur publikation von amb daten recyclen + (let [form-data (-> cofx :db :md-form-resource) + about (map (fn [e] ["about" + (:id e) + (second (first (filter (fn [l] + (= :de (first l))) + (:prefLabel e)))) + "de"]) + (:about form-data)) ;; TODO this should be abstracted in a nostr-make-event-kind-function or sth + tags [["d" (:uri form-data)] + ["id" (:uri form-data)] + ["author" "" (:author form-data)] + ["name" (:name form-data)]] + event {:kind 30142 :created_at (:now cofx) :content "" - :tags [["d" (:id resource)] - ["id" (:id resource)] - #_["author" "" (:author resource)] - ["name" (:name resource)]]}] + :tags (into tags about)} + _ (.log js/console "Event to publish " (clj->js event))] {:navigate [:home] ::sign-and-publish-event [event (-> cofx :db :sk)] - } + :db (assoc (:db cofx) :md-form-resource nil)}))) - #_{::sign-and-publish-event [event (-> cofx :db :sk)]}))) +(defn sign-event [unsignedEvent sk] + (if (nostr/valid-unsigned-nostr-event? unsignedEvent) + (p/let [_ (js/console.log (clj->js unsignedEvent)) + signedEvent (if (nil? sk) + (.nostr.signEvent js/window (clj->js unsignedEvent)) + (nostr/finalize-event unsignedEvent sk)) + _ (js/console.log "Signed event: " (clj->js signedEvent))] + signedEvent) + (.error js/console "Event is not a valid nostr event: " (clj->js unsignedEvent)))) ;; TODO maybe we need some validation before publishing (re-frame/reg-fx ::sign-and-publish-event (fn [[unsignedEvent sk]] - (if (nostr/valid-unsigned-nostr-event? unsignedEvent) - (p/let [_ (js/console.log (clj->js unsignedEvent)) - _ (js/console.log (nostr/sk-as-hex sk)) - signedEvent (if (nil? sk) - (.nostr.signEvent js/window (clj->js unsignedEvent)) - (nostr/finalize-event unsignedEvent sk)) - _ (js/console.log "Signed event: " (clj->js signedEvent))] - (re-frame/dispatch [::send-to-relays signedEvent])) - (.error js/console "Event is not a valid nostr event: " (clj->js unsignedEvent))))) + (p/let [signedEvent (sign-event unsignedEvent sk)] + (re-frame/dispatch [::publish-signed-event signedEvent])))) +;; TODO make login a multimethod and call it with either extension or anononymslouy keyword? (re-frame/reg-event-fx ::login-with-extension (fn-traced [cofx [_ _]] @@ -229,7 +265,8 @@ ::login-with-extension-fx (fn [db _] (p/let [pk (.nostr.getPublicKey js/window)] - (re-frame/dispatch [::save-pk pk])))) + (re-frame/dispatch [::save-pk pk]) + (re-frame/dispatch [::relay-list-for-pk pk])))) (re-frame/reg-event-fx ::save-pk @@ -258,27 +295,41 @@ (assoc cofx :pk (:pk cofx)))) (defn convert-amb-to-nostr-event - [json-string created_at] - (let [parsed-json (js->clj (js/JSON.parse json-string) :keywordize-keys true) - tags (into [["d" (:id parsed-json)] + [parsed-json created_at] + (let [tags (into [["d" (:id parsed-json)] ["r" (:id parsed-json)] ["id" (:id parsed-json)] ["name" (:name parsed-json)] ["description" (:description parsed-json "")] + (into ["keywords"] (:keywords parsed-json)) ["image" (:image parsed-json "")]] - cat [(map (fn [e] ["about" (:id e) (-> e :prefLabel :de)]) (:about parsed-json)) + cat [(map (fn [e] ["about" (:id e) (-> e :prefLabel :de) "de"]) (:about parsed-json)) ;; TODO fix the language parsing and make it generic + (map (fn [e] ["creator" + (if-let [id (get e :id)] + id + "") + (:name e)]) + (:creator parsed-json)) (map (fn [e] ["inLanguage" e]) (:inLanguage parsed-json))]) event {:kind 30142 :created_at created_at - :content "Added AMB Resource with d-tag" + :content "" :tags tags}] event)) (re-frame/reg-event-fx - ::convert-amb-and-publish-as-nostr-event + ::convert-amb-string-and-publish-as-nostr-event [(re-frame/inject-cofx :now)] (fn-traced [cofx [_ json-string]] - (let [event (convert-amb-to-nostr-event json-string (:now cofx))] + (let [parsed-json (js->clj (js/JSON.parse json-string) :keywordize-keys true) + event (convert-amb-to-nostr-event parsed-json (:now cofx))] + {::sign-and-publish-event [event (-> cofx :db :sk)]}))) + +(re-frame/reg-event-fx + ::convert-amb-json-and-publish-as-nostr-event + [(re-frame/inject-cofx :now)] + (fn-traced [cofx [_ json]] + (let [event (convert-amb-to-nostr-event json (:now cofx))] {::sign-and-publish-event [event (-> cofx :db :sk)]}))) (re-frame/reg-event-fx @@ -308,7 +359,7 @@ tags-to-add (filter #(not (contains? existing-tags-set %)) (map (fn [e] (cond (= 1 (:kind e)) ["e" (:id e)] - (= 30142 (:kind e)) (nostr/build-kind-30142-tag e))) + (= 30142 (:kind e)) (nostr/build-tag-for-adressable-event e))) resources-to-add)) new-tags (vec (concat existing-tags tags-to-add)) event {:kind 30004 @@ -337,11 +388,32 @@ (let [query-for-lists ["REQ" (make-sub-id "lists-" npub) ;; TODO maybe make this more explicit later {:authors [(nostr/get-pk-from-npub npub)] - :kinds list-kinds}] + :kinds (concat (:follow-sets db) (:list-kinds db))}] sockets (:sockets db)] {::request-from-relay [sockets query-for-lists] :dispatch [::get-deleted-lists-for-npub [sockets npub]]}))) +(re-frame/reg-event-fx + ::relay-list-for-pk + (fn [{:keys [db]} [_ pk]] + (.log js/console "get relay list for pk" pk) + (let [query-for-relay-list ["REQ" + (make-sub-id "relay-lists-" pk) ;; TODO maybe make this more explicit later + {:authors [pk] + :kinds [30002]}] + sockets (:sockets db)] + {::request-from-relay [sockets query-for-relay-list]}))) + +(re-frame/reg-event-fx + ::get-follow-set-for-npub + (fn [{:keys [db]} [_ npub]] + (let [query-for-lists ["REQ" + (make-sub-id "follow-set-" npub) ;; TODO maybe make this more explicit later + {:authors [(nostr/get-pk-from-npub npub)] + :kinds (:follow-sets db)}] + sockets (:sockets db)] + {::request-from-relay [sockets query-for-lists]}))) + (re-frame/reg-event-fx ::get-deleted-lists-for-npub (fn [cofx [_ [sockets npub]]] @@ -351,7 +423,16 @@ :kinds [5]}]] {::request-from-relay [sockets query-for-deleted-lists]}))) -(comment) +(re-frame/reg-event-fx + ::events-from-pks-actor-follows + (fn [{:keys [db]} [_ pks]] + (.log js/console "requesting events from pks actor follows..") + (let [query-for-events ["REQ" + (make-sub-id "events-from-pks-actor-follows" (:pk db)) + {:authors pks + :kinds [1] + :limit 10}]] ;; TODO remember timestamps and use them in query + {::request-from-relay [(:sockets db) query-for-events]}))) (re-frame/reg-fx ::request-from-relay @@ -374,32 +455,97 @@ (str/replace #"\s" "-") (str/replace #"[^a-zA-Z0-9]" "-"))) -(comment - (cleanup-list-name "this is gönna be an awesüm+ l]st")) - (re-frame/reg-event-fx ::create-new-list [(re-frame/inject-cofx :now)] (fn [cofx [_ name]] (let [tags [["d" (cleanup-list-name name)] - ["name" name]] + ["title" name]] create-list-event {:kind 30004 :created_at (:now cofx) :content "" :tags tags}] {::sign-and-publish-event [create-list-event (-> cofx :db :sk)]}))) +;;;;;;;;;;;;;;;;;;;;;;;; +;; Opencard stuff +;;;;;;;;;;;;;;;;;;;;;;;; + +(re-frame/reg-event-fx + ::create-new-opencard-index + [(re-frame/inject-cofx :now)] + (fn [cofx [_ name]] + (let [tags [["d" (cleanup-list-name name)] + ["title" name]] + create-opencard-index-event {:kind 30043 + :created_at (:now cofx) + :content "" + :tags tags}] + {::sign-and-publish-event [create-opencard-index-event (-> cofx :db :sk)]}))) + +;; create, sign, publish list event +;; add signed list event to opencard-index +(re-frame/reg-event-fx + ::add-opencard-list-to-index + (fn [cofx [_ [name opencard-index-old]]] + (let [opencard-list {:kind 30044 + :created_at (:now cofx) + :content "" + :tags [["d" (cleanup-list-name name)] + ["title" name]]} + opencard-index (update opencard-index-old :created_at (:now cofx))] + {::add-opencard-list-to-index-fx [opencard-list opencard-index (-> cofx :db :sk)]}))) + +(re-frame/reg-fx + ::add-opencard-list-to-index-fx + (fn [opencard-list opencard-index sk] + (p/let [opencard-list-signed (sign-event opencard-list sk) + opencard-index (update opencard-index :tags (fn [tags] + (conj tags ["a" (nostr/build-tag-for-adressable-event opencard-list-signed)]))) + opencard-index-signed (sign-event opencard-index sk)] + (re-frame/dispatch [::publish-signed-event opencard-list-signed]) + (re-frame/dispatch [::publish-signed-event opencard-index-signed])))) + +(re-frame/reg-event-fx + ::remove-opencard-list-from-index + [(re-frame/inject-cofx :now)] + (fn [cofx [_ opencard-list-to-delete opencard-index]] + (let [opencard-index-new {:kind 30043 + :created_at (:now cofx) + :content "" + :tags (filter #(not= (:id opencard-list-to-delete) (:id %)) (:tags opencard-index))}] + {::sign-and-publish-event [opencard-index-new (-> cofx :db :sk)] + ::delete-list [opencard-list-to-delete]}))) + +;; TODO add 30045 opencard note to opencard-list +(re-frame/reg-event-fx + ::add-opencard-note-to-list + [(re-frame/inject-cofx :now)] + (fn [cofx [_ [name content] opencard-list]] + (let [tags [["d" (cleanup-list-name name)] + ["title" name]] ;; TODO depending on the note content we might need to add more tags like references to other events and so on + opencard-note-event {:kind 30045 + :created_at (:now cofx) + :content content + :tags tags}] + {::sign-and-publish-event [opencard-note-event (-> cofx :db :sk)]}))) + +;; TODO delete-opencard-index +;; should we just remove the index or anything associated? maybe ask the user first + (re-frame/reg-event-fx ::delete-list [(re-frame/inject-cofx :now)] (fn [cofx [_ l]] - (let [deletion-event {:kind 5 + (let [{:keys [list-kinds opencard-kinds]} (:db cofx) + all-list-kinds (concat list-kinds opencard-kinds) + deletion-event {:kind 5 :created_at (:now cofx) :content "" :tags [(cond (= 1 (:kind l)) ["e" (:id l)] - (some #{(:kind l)} (-> cofx :db :list-kinds)) + (some #{(:kind l)} all-list-kinds) ["a" (str (:kind l) ":" (:pubkey l) ":" (second (first (filter #(= "d" (first %)) (:tags l)))))])]}] @@ -448,27 +594,15 @@ (p/let [raw-html (js/fetch uri {:headers {"Access-Control-Allow-Origin" "*"}})] (println raw-html)))) -(comment - (p/->> (js/fetch "https://oersi.org/resources/aHR0cHM6Ly9lZ292LWNhbXB1cy5vcmcvY291cnNlcy9hcmJlaXRlbnVuZGZ1ZWhyZW5fdXBfMjAyMi0x") - (println))) - (re-frame/reg-event-fx ::publish-amb-uri-as-nostr-event (fn [db [_ uri]] - {::get-amb-json-from-uri uri})) - -(def confetti-instance - (new jsConfetti)) - -(re-frame/reg-event-fx - ::add-confetti - (fn [cofx _] - (let [visited-at (-> cofx :db :visited-at) - now (quot (.now js/Date) 1000) - diff (- now visited-at)] - (when (>= diff 5) - (.addConfetti confetti-instance (clj->js {:emojis ["😺" "🐈‍⬛" "🦄"]}))) - {}))) + {:http-xhrio {:method :get + :uri uri + :timeout 8000 + :response-format (ajax/json-response-format {:keywords? true}) + :on-success [::convert-amb-json-and-publish-as-nostr-event] + :on-failure (.log js/console "publishing amb uri as nostr did not work")}})) (re-frame/reg-event-fx ::set-visit-timestamp @@ -476,4 +610,207 @@ (fn [cofx [_]] {:db (assoc (:db cofx) :visited-at (:now cofx))})) +;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Get metadata from uri ;; +;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn valid-uri? [s] + (try + (js/URL. s) + true + (catch :default e false))) + +(re-frame/reg-event-fx + ::try-get-metadata-from-uri + (fn [cofx [_ uri]] + (when (valid-uri? uri) + (println "valid uri" (valid-uri? uri)) + {:dispatch [::get-metadata-from-json uri]}))) + +(re-frame/reg-fx + ::try-get-metadata-from-uri-fx + (fn [uri] + (try + (println "trying to get metadata from json") + (re-frame/dispatch [::get-metadata-from-json uri]) + (catch :default e + (println "... did not work..looking for script tag") + (try + (re-frame/dispatch [::get-metadata-from-script-tag uri]) + (catch :default e + (js/console.error "all attempts to fetch something sensible failed."))))))) + +(re-frame/reg-event-db + ::prefill-metadata-form + (fn [db [_ uri data]] + (if (and + (:id data) + (:about data)) + (assoc db :resource-to-add data) + (doall (js/console.error "Not the right kind of data") + (re-frame/dispatch [::get-metadata-from-script-tag uri]))))) + +(re-frame/reg-event-fx + ::failure + (fn [cofx _] + (println "failure in xhrio request"))) + +(re-frame/reg-event-db + ::parse-text-for-script-tag + (fn [db [_ uri data]] + (let [parser (js/DOMParser.) + doc (.parseFromString parser data "text/html") + script-tag (.querySelector doc "script[type='application/ld+json']")] + (if script-tag + (let [ld-json (js/JSON.parse (.-textContent script-tag))] + (assoc db :resource-to-add (js->clj ld-json))) + (println "did not get script tag"))))) + +(re-frame/reg-event-fx + ::get-metadata-from-script-tag + (fn [cofx [_ uri]] + (println "now trying to get script tag..") + {:http-xhrio {:method :get + :uri uri + :timeout 3000 + :response-format (ajax/text-response-format) + :on-success [::parse-text-for-script-tag uri] + :on-failure [::failure]}})) + +(re-frame/reg-event-fx + ::get-metadata-from-json + (fn [cofx [_ uri]] + {:http-xhrio {:method :get + :uri (if (str/ends-with? uri "json") ;; TODO elaborate this a bit more + uri + (str/replace uri #".html" ".json")) + :timeout 3000 + :response-format (ajax/json-response-format {:keywords? true}) + :on-success [::prefill-metadata-form uri] + :on-failure [::get-metadata-from-script-tag uri]}})) + +(re-frame/reg-event-db + ::save-concept-scheme + (fn [db [_ cs]] + (assoc-in db [:concept-schemes (:id cs)] cs))) + +(comment + (keyword "https://ww.googl-e.com/")) + +(defn jsonize-uri + [uri] + (cond + (str/ends-with? uri ".json") + uri + (str/ends-with? uri ".html") + (str/replace uri #"\.\w{4}" ".json") + :else + (str uri ".json"))) + +(comment + (jsonize-uri "https:/goo")) + +(re-frame/reg-event-fx + ::skos-concept-scheme-from-uri + (fn [cofx [_ uri]] + {:http-xhrio {:method :get + :uri (jsonize-uri uri) + :timeout 5000 + :response-format (ajax/json-response-format {:keywords? true}) + :on-success [::save-concept-scheme] + :on-failure [::failure]}})) + +(re-frame/reg-event-db + ::toggle-concept + (fn [db [_ [concept field]]] + (update-in db [:md-form-resource field] (fn [coll] + (if (some #(= (:id concept) (:id %)) coll) + (filter (fn [e] (not= (:id e) (:id concept))) coll) + (conj coll (select-keys concept [:id :notation :prefLabel] ))))))) + +(re-frame/reg-event-db + ::handle-md-form-input + (fn [db [_ [field-name field-value]]] + (assoc-in db [:md-form-resource field-name] field-value))) + +(re-frame/reg-event-db + ::handle-md-form-array-input + (fn [db [_ [field-name field-id field-value]]] + (assoc-in db [:md-form-resource field-name field-id] field-value))) + +(re-frame/reg-event-db + ::handle-md-form-rm-input + (fn [db [_ [field-name field-id]]] + (update-in + db + [:md-form-resource field-name] + dissoc + field-id))) + +(re-frame/reg-event-db + ::handle-md-form-add-input + (fn [db [_ [field-name]]] + (assoc-in db [:md-form-resource field-name (random-uuid)] nil))) + +(re-frame/reg-event-fx + ::handle-search + (fn [cofx [_ search-term]] + (let [uri (str config/typesense-uri + "search?q=" + search-term + "&query_by=" + "name,about,description,creator")] + {:http-xhrio {:method :get + :uri uri + :headers {"x-typesense-api-key" "xyz"} + :timeout 5000 + :response-format (ajax/json-response-format {:keywords? true}) + :on-success [::save-search-results] + :on-failure [::failure]}}))) + +(defn sanitize-filter-term [term] + (str/replace term #"[ ()]" " ")) + +(re-frame/reg-event-fx + ::handle-filter-search + (fn [cofx [_ [filter-attribute filter-term]]] + (let [uri (str config/typesense-uri + "search?q=*" + "&query_by=" + "name" + "&filter_by=" + filter-attribute + ":=" + (sanitize-filter-term filter-term ))] ;; parantetheses seem to cause error when filtering + {:http-xhrio {:method :get + :uri uri + :headers {"x-typesense-api-key" "xyz"} + :timeout 5000 + :response-format (ajax/json-response-format {:keywords? true}) + :on-success [::save-search-results] + :on-failure [::failure]}}))) + +(re-frame/reg-event-db + ::save-search-results + (fn [db [_ results]] + (let [raw-result-events (map #(-> % :document :event_raw) (:hits results))] + (-> db + (assoc :search-results (:hits results)) + (update :events into raw-result-events))))) + +(re-frame/reg-event-fx + ::load-profile + (fn [cofx [_ pubkey]] + (let [sockets (filter #(= "connected" (:status %)) (-> cofx :db :sockets)) + profile-request ["REQ" + (make-sub-id "profile" pubkey) + {:kinds [0] + :authors [pubkey] + :limit 10}]] + {::request-from-relay [sockets profile-request]}))) + +(re-frame/reg-event-db + ::set-md-scheme + (fn [db [_ md-scheme]] + (assoc db :selected-md-scheme md-scheme))) diff --git a/src/ied/nostr.cljs b/src/ied/nostr.cljs index 558f25b..bf6016e 100644 --- a/src/ied/nostr.cljs +++ b/src/ied/nostr.cljs @@ -7,6 +7,15 @@ ["nostr-tools/pure" :as nostr] [cljs.core :as c])) +;;;;;;;;;;;;;;;;;;;; +;; Profile +;;;;;;;;;;;;;;;;;;;; + +(defn profile-picture [profile pubkey] + (if (:picture profile) + (:picture profile) + (str "https://robohash.org/" pubkey))) + (defn event-to-serialized-json [m] (let [event [0 (:pubkey m) @@ -47,10 +56,14 @@ (defn sk-as-hex [sk] (byte-array-to-hex sk)) +(defn string-to-byte-array [s] + (let [encoder (js/TextEncoder.)] + (.encode encoder s))) + (defn sk-as-nsec [sk] ;; let nsec = nip19.nsecEncode(sk) ;; sk should be byte-array - (.nsecEncode nip19 sk)) + (.nsecEncode nip19 (string-to-byte-array sk))) (defn nsec-as-sk [nsec] ;; byte array is returned @@ -84,11 +97,76 @@ (.encode encoder s))) (defn get-npub-from-pk [pk] - (.npubEncode nip19 pk)) + (.npubEncode nip19 pk)) + +(comment + (get-npub-from-pk "1c5ff3caacd842c01dca8f378231b16617516d214da75c7aeabbe9e1efe9c0f6")) (defn get-pk-from-npub [npub] (.-data (.decode nip19 npub))) +(comment + (get-pk-from-npub "npub1r30l8j4vmppvq8w23umcyvd3vct4zmfpfkn4c7h2h057rmlfcrmq9xt9ma")) + +(defn get-d-id-from-event [event] + (second (first (filter #(= "d" (first %)) (:tags event))))) + +(defn naddr-from-event [event] + (let [pk (:pubkey event) + relays #_(get-relays-from-event event) ["ws://localhost:7777" "wss://relay.sc24.steffen-roertgen.de"] ;; TODO how to know what relays to use here? do i need to remember where i fetched an event from? or maybe just skip? + kind (:kind event) + identifier (get-d-id-from-event event) + naddr (.naddrEncode nip19 (clj->js {:pubkey pk + :relays relays + :kind kind + :identifier identifier}))] + naddr)) + +(defn decode-naddr + "Takes a nip-19 naddr and returns its data + Returns: + A map with `:identifier` `:pubkey` `:relays` `:kind`. + " + [naddr] + + (:data (js->clj (.decode nip19 naddr) :keywordize-keys true))) + +(comment + (def addressable-event {:content "Added AMB Resource with d-tag", + :created_at 1729616701, + :id + "dec183af8f1ac91dac3bb7716b8564ab515c050480721c7ee2ea308b51caf982", + :kind 30142, + :pubkey + "1c5ff3caacd842c01dca8f378231b16617516d214da75c7aeabbe9e1efe9c0f6", + :sig + "17d54fc1a418100a41d8b9dd0eac5db0cad544bff84c5d13befa1c1791979fbf319234c9749a690d1cb166116741642deafb8b9c8f91a16590931f7eb80f3bb4", + :tags + [["d" "https://av.tib.eu/media/40427"] + ["r" "https://av.tib.eu/media/40427"] + ["id" "https://av.tib.eu/media/40427"] + ["name" "Digitale Identitäten: Sicher, dezentral und Europäisch?"] + ["description" + "(de)#INFORMATIK2018 Panelgespräch: Digitale Identitäten: Sicher, dezentral und Europäisch? Moderation: Dr. Jan Sürmeli, TU Berlin • Prof. Dr. Reinhard Riedl, Präsident Schweizer InformatikGesellschaft (FH Bern) • Prof. Dr. Hannes Federrath, Präsident der Gesellschaft für Informatik (Universität Hamburg) • Prof. Dr. Kai Rannenberg, Präsidium der Gesellschaft für Informatik (Uni Frankfurt) • Prof. Dr. Stefan Jähnichen, TU Berlin / FZI Forschungszentrum Informatik • Benjamin Helfritz, DIN Deutsches Institut für Normung e.V. • Arno Fiedler, Vorstand Sichere Identität Berlin Brandenburg"] + ["keywords" "Computer Science"] + ["image" "https://av.tib.eu/thumbnail/40427"] + ["about" + "https://w3id.org/kim/hochschulfaechersystematik/n71" + "Studienbereich Informatik" + "de"] + ["about" + "https://w3id.org/kim/hochschulfaechersystematik/n8" + "Ingenieurwissenschaften" + "de"] + ["creator" "test-uri" "maxi muster"] + ["creator" "" "maxa muster"] + ["inLanguage" "de"]]}) + (naddr-from-event addressable-event) + (get-creators-from-metadata-event addressable-event) + (.log js/console (clj->js addressable-event)) + + (decode-naddr "naddr1qvzqqqr4hcpzq8zl7092ekzzcqwu4rehsgcmzesh29kjznd8t3aw4wlfu8h7ns8kqqwksar5wpen5te0v9mzuarfvghx2af0d4jkg6tp9u6rqdpjxunlx7u8")) + ;; TODO make multimethod? (defn get-list-name [list] (or (second (first (filter #(= "title" (first %)) (:tags list)))) @@ -96,14 +174,20 @@ (second (first (filter #(= "d" (first %)) (:tags list)))) (str "No name found for List-ID: " (:id list)))) -(defn get-d-id-from-event [event] - (second (first (filter #(= "d" (first %)) (:tags event))))) - (defn get-name-from-metadata-event [event] (or (second (first (filter #(= "name" (first %)) (:tags event)))) + (second (first (filter #(= "title" (first %)) (:tags event)))) (second (first (filter #(= "id" (first %)) (:tags event)))) (str "No name found for Metadata-Event: " (:id event)))) +(defn get-creators-from-metadata-event [event] + (or (map (fn [e] {:id (second e) + :name (nth e 2 "n/a")}) (filter #(= "creator" (first %)) (:tags event))) + (str "No creators found for Metadata-Event: " (:id event)))) + +(defn get-keywords-from-metadata-event [event] + (rest (first (filter (fn [e] (= "keywords" (first e))) (:tags event))))) + (defn get-image-from-metadata-event [event] (or (let [img-url (second (first (filter #(= "image" (first %)) (:tags event))))] (if (= "" img-url) false img-url)) @@ -182,7 +266,7 @@ (seq metadata-event-ids-in-list) (contains? metadata-event-ids-in-list event-id)))) -(defn build-kind-30142-tag [event] +(defn build-tag-for-adressable-event [event] ["a" (str "30142:" (:id event) ":" (second (first (filter #(= "d" (first %)) (:tags event)))))]) (defn get-event-ids-from-list [list] diff --git a/src/ied/opencard/subs.cljs b/src/ied/opencard/subs.cljs new file mode 100644 index 0000000..090b986 --- /dev/null +++ b/src/ied/opencard/subs.cljs @@ -0,0 +1,13 @@ +(ns ied.opencard.subs + (:require + [re-frame.core :as re-frame])) + + +(re-frame/reg-sub + ::opencard-boards + (fn [db] + (:opencard-boards db))) + +(comment +(re-frame/subscribe [::opencard-boards]) + ) diff --git a/src/ied/opencard/views.cljs b/src/ied/opencard/views.cljs new file mode 100644 index 0000000..a790b3f --- /dev/null +++ b/src/ied/opencard/views.cljs @@ -0,0 +1,331 @@ +(ns ied.opencard.views + (:require + [re-frame.core :as re-frame] + [ied.opencard.subs :as subs] + [clojure.string :as str] + [ied.routes :as routes] + [ied.components.icons :as icons] + [markdown-to-hiccup.core :as m] + [reagent.core :as reagent])) + +;; Sample data +(defonce state (reagent/atom {:lists [{:id 1 :items [{:id 1 :content "*Item 1*"} {:id 2 :content "Item 2"}]} + {:id 2 :items [{:id 3 :content "Item 3"} {:id 4 :content "Item 4"}]} + {:id 3 :items [{:id 5 :content "Item 5"} {:id 6 :content "Item 6"}]} + {:id 4 :items [{:id 7 :content "Item 7"} {:id 8 :content "Item 8"}]}] + :show-edit-item-modal false + :selected-item {:id nil + :content ""} + :dragging nil + :dragging-over nil + :hover-index nil + :hover-item nil + :dragged-column nil + :hover-column-id nil + :dragging-item nil + :dragging-over-list nil})) ;; New state to track the current column being dragged over + +;; Helper functions +(defn find-item [lists item-id] + (some (fn [list] + (some (fn [item] + (when (= (:id item) item-id) [list item])) + (:items list))) + lists)) + +(defn reorder-items [items from-index to-index] + (let [item (nth items from-index) + items (vec (concat (subvec items 0 from-index) (subvec items (inc from-index)))) + head (subvec items 0 to-index) + tail (subvec items to-index)] + (vec (concat head [item] tail)))) + +(defn reorder-columns [columns from-index to-index] + (let [column (nth columns from-index) + columns-filtered (vec (concat (subvec (vec columns) 0 from-index) (subvec (vec columns) (inc from-index)))) + head (subvec columns-filtered 0 to-index) + tail (subvec columns-filtered to-index) + reordered (vec (concat head [column] tail))] + reordered)) + +(defn handle-drag-start [item-id] + (swap! state assoc :dragging item-id :hover-index nil :hover-item nil :dragged-column nil :dragging-item item-id)) + +(defn set-dragged-column + "Sets `:dragged-column`. It's the id of the column" + [column-id] + (swap! state assoc :dragged-column column-id :hover-column-id nil :dragging nil)) + +(defn handle-drag-over [e list-id & [index item-id]] + (.preventDefault e) + (swap! state assoc :dragging-over list-id :dragging-over-list list-id) ;; Track the list being dragged over + (when (and index item-id) ;; Only update hover-index and hover-item if item details are provided + (swap! state assoc :hover-index index :hover-item item-id))) + +(defn handle-column-drag-over [e column-id] + (.preventDefault e) + (swap! state assoc :hover-column-id column-id)) + +(defn handle-column-drop [] + (let [is-dragged (:dragged-column @state) + columns (:lists @state) + hover-column-id (:hover-column-id @state) + hover-column-index (.indexOf (map :id columns) hover-column-id) + from-index (.indexOf (map :id columns) is-dragged)] + (when (and is-dragged (not= from-index hover-column-index)) ;; Only reorder if we are dragging a column + (swap! state update :lists #(reorder-columns % from-index hover-column-index))) + ;; Clear column drag state + (swap! state assoc :dragged-column nil :hover-column-id nil))) + +(defn handle-drop [list-id] + (when-not (:dragged-column @state) ;; Only handle item drop if not dragging a column + (let [item-id (:dragging @state) + [source-list item] (find-item (:lists @state) item-id) + hover-index (:hover-index @state) + is-same-list (= (:id source-list) list-id)] + (when item + ;; Handle reorder if the item is dropped in the same list + (if is-same-list + (when (not= (.indexOf (:items source-list) item) hover-index) ; Only reorder if the position changed + (swap! state update :lists + (fn [lists] + (map (fn [list] + (if (= (:id list) list-id) + (update list :items #(reorder-items % (.indexOf (:items list) item) + (if hover-index hover-index (count (:items list))))) + list)) + lists)))) + ;; Move the item to another list + (swap! state update :lists + (fn [lists] + (map (fn [list] + (cond + ;; Remove item from the source list + (= (:id list) (:id source-list)) (update list :items #(remove (fn [i] (= (:id i) item-id)) %)) + ;; Insert item into the destination list at the hover index or at the end if not hovering over any item + (= (:id list) list-id) (update list :items + (fn [items] + (if hover-index + (let [split-items (split-at hover-index items)] + (vec (concat (first split-items) [item] (second split-items)))) + ;; Insert at the end if no hover index is provided + (conj items item)))) + :else list)) + lists))))) + + ;; Clear dragging state + (swap! state assoc :dragging nil :dragging-over nil :hover-index nil :hover-item nil :dragging-item nil :dragging-over-list nil)))) + +(defn handle-drag-end [] + (swap! state assoc :dragging nil :dragging-over nil :hover-index nil :hover-item nil :dragging-item nil :dragging-over-list nil)) + +(defn handle-add-item [column] + (swap! state update :lists + (fn [lists] + (map (fn [list] + (if (= (:id column) (:id list)) + (update list :items + (fn [items] (conj items {:id (rand-int 1000) :content "huhu"}))) + list)) + lists)))) + +(defn handle-add-column [] + (let [item {:id (rand-int 1000) :content "huhu"} + column {:id (rand-int 1000) :items [item]}] + (swap! state update :lists + (fn [lists] + (conj lists column))))) + +(defn update-item-content [data-atom target-id new-content] + (println "update item content with : " new-content) + (swap! data-atom + (fn [data] + (update data :lists + (fn [lists] + (mapv (fn [list] + (update list :items + (fn [items] + (mapv (fn [item] + (if (= (:id item) target-id) + (assoc item :content new-content) + item)) + items)))) + lists)))))) + +(defn find-item-by-id [target-id] + (some #(when (= (:id %) target-id) %) + (mapcat :items (:lists @state)))) + +(defn open-edit-item-modal [] + (.showModal (.getElementById js/document "my_modal_1"))) + +;; Components +(defn draggable-item [{:keys [id content]} list-id index] + (fn [] + (let [item (find-item-by-id id) + is-hovered (= id (:hover-item @state)) + is-dragging (= id (:dragging-item @state))] ;; Determine if this item is being dragged + [:div {:draggable true + :on-drag-start #(handle-drag-start id) + :on-drag-over (fn [e] (handle-drag-over e list-id index id)) + :on-double-click #((swap! state assoc :selected-item {:id id + :content content}) + (open-edit-item-modal)) + :class (str/join " " ["hover:cursor-grab" + "hover:bg-yellow-200" + "bg-slate-100" + "min-h-16" + "p-2" + "text-black" + (when is-dragging "hidden") + ])} ;; Bring to front if dragging + (->> (:content item) + (m/md->hiccup) + (m/component))]))) + +(defn droppable-list [id] + (fn [] + (let [items (:items (some #(when (= id (:id %)) %) (:lists @state))) + dragging-over? (= id (:dragging-over @state)) + dragging-item (:dragging-item @state) + dragging-over-list (:dragging-over-list @state)] ;; Get the list currently being dragged over + [:div {:on-drag-over (fn [e] (handle-drag-over e id)) ; Only list id is passed here to mark the column as hovered + :on-drop (fn [_] (handle-drop id)) + :class (str/join " " ["relative" + "flex flex-col gap-1" + "rounded-md" + "min-h-32" + (if dragging-over? "bg-green-200" "") + (if dragging-over? "border-dashed border-2 border-green-500" "")])} + ;; Ensure the dragged item is positioned correctly + (doall + (for [index (range (count items))] + (let [item (nth items index)] + ;; Render the placeholder only in the correct column + ^{:key (:id item)} [:<> + (when (and dragging-item (= id dragging-over-list) (= index (:hover-index @state))) + [:div {:class (str/join " " ["min-h-16" + "m-2" + "bg-slate-100" + "opacity-50"])}]) + [draggable-item item id index]])))]))) + +;; TODO update title +(defn title-field [] + (let [edit (reagent/atom false)] + (fn [] + [:div {:class "flex justify-between items-center"} + (if @edit + [:input {:class "input"}] + [:p {} "Title of column"]) + [:label {:on-click #(reset! edit (not @edit)) + :class "btn"} [icons/pencil]]]))) + +(defn draggable-column [column index] + (fn [] + (let [is-hovered (= (:id column) (:hover-column-id @state)) + is-dragged (:dragged-column @state)] ;; Get the currently dragged column + [:div {:on-drag-over (fn [e] (when is-dragged (handle-column-drag-over e (:id column)))) ;; Only handle if a column is being dragged + :on-drag-end #(handle-drag-end) + :on-drop (fn [_] (handle-column-drop)) + :class (str/join " " ["" + "overflow-y-auto" + "max-h-screen" + "m-2" + "p-4" + "rounded-md" + "w-96" + (if (and is-hovered is-dragged) + "border-dashed border-2 border-red-200 bg-red-200" + "border-solid border-1 border-slate-100 bg-slate-200")])} + +;; Add the handle for column dragging + [:div {:class "flex flex-col gap-1"} + [:span {:draggable true + :on-drag-start (fn [_] (set-dragged-column (:id column))) + ; :on-drag-start (fn [_] (handle-column-drag-start index)) + :class "cursor-grab p-2 mr-0 ml-auto bg-slate-100 border-solid rounded-full"} + [icons/move]] + [:div {:class "rounded-md bg-primary p-2 text-xl text-white font-medium"} + [title-field]] + [:label + {:class "btn btn-neutral my-1" + :on-click #(handle-add-item column)} + [icons/add "white"]] + + [droppable-list (:id column)]]]))) + +(defn add-column [] + [:label + {:class "btn btn-rounded fixed bottom-4 right-4 bg-blue-500 text-white p-3 shadow-lg hover:bg-blue-600" + :on-click #(handle-add-column)} + [icons/add]]) + +(defn drag-and-drop [] + [:div {:class "flex overflow-auto"} + [:div {:class "flex flex-nowrap "} + (doall + (for [index (range (count (:lists @state)))] + (let [hash (hash (:items (nth (:lists @state) index)))] + ^{:key hash} [draggable-column (nth (:lists @state) index) index])))] + [add-column]]) + +(defn edit-item-modal [] + (let [content (reagent/atom "") + selected-item-content (reagent/reaction + (let [item (:selected-item @state)] + (:content item)))] + (reagent/track! (fn [] + (reset! content @selected-item-content))) + (fn [] + (let [item (:selected-item @state) + id (:id item)] + [:dialog + {:id "my_modal_1", :class "modal"} + [:div + {:class "modal-box"} + [:h3 {:class "text-lg font-bold"} "Hello!"] + [:p @content] + [:textarea {:class "textarea textarea-bordered" + :value @content + :on-change (fn [e] (reset! content (-> e .-target .-value)))}] + [:p + {:class "py-4"} + "Press ESC key or click the button below to close"] + [:div + {:class "modal-action"} + [:form + {:method "dialog"} + [:button {:class "btn" + :on-click (fn [e] + (update-item-content state id @content))} "Save"]]]]])))) + +(defn board-component [board] + [:div {:class ""} + [:button {:class "btn"} (:title board)]]) + +(defn opencard-boards [] + (let [boards (re-frame/subscribe [::subs/opencard-boards])] + [:div {:class "grid grid-cols-4 gap-2 mt-2"} + (doall + (for [board @boards] + ^{:key (:id board)} [board-component board]))])) + +(defn opencard-component [] + (let [show-board-overview (reagent/atom true)] + (fn [] + [:div {:class "flex flex-col"} + [:button {:class "btn btn-square ml-0 mr-auto" + :on-click #(reset! show-board-overview (not @show-board-overview))} [icons/grid]] + [:div {:class ""} + (if @show-board-overview + [opencard-boards] + [drag-and-drop])]]))) + +(defn opencard-view-panel [] + [:div {:class ""} + [opencard-component] + [edit-item-modal]]) + +(defmethod routes/panels :opencard-view-panel [] [opencard-view-panel]) + diff --git a/src/ied/routes.cljs b/src/ied/routes.cljs index 4dee571..b4b7ae0 100644 --- a/src/ied/routes.cljs +++ b/src/ied/routes.cljs @@ -12,18 +12,25 @@ (atom ["/" {"" :home + "search" :search-view "add-resource" :add-resource "keys" :keys "feed" :event-feed "about" :about "settings" :settings ["" [#"npub1[ac-hj-np-z02-9]{58}" :npub]] {"" :npub-view - "/" :npub-view}}])) + "/" :npub-view} + ["" [#"naddr1[ac-hj-np-z02-9]+" :naddr]] {"" :naddr-view + "/" :naddr-view} + "opencard" :opencard-view}])) (comment - (parse "/npub1r30l8j4vmppvq8w23umcyvd3vct4zmfpfkn4c7h2h057rmlfcrmq9xt9ma/") + (navigate! (parse "/npub1r30l8j4vmppvq8w23umcyvd3vct4zmfpfkn4c7h2h057rmlfcrmq9xt9ma/")) + (navigate! (parse "/naddr1r30l8j4vmppvq8w23umcyvd3vct4zmfpfkn4c7h2h057rmlfcrmq9xt9ma/")) (apply url-for [:npub-view :npub "npub1r30l8j4vmppvq8w23umcyvd3vct4zmfpfkn4c7h2h057rmlfcrmq9xt9ma"]) - (apply url-for [:home])) + (apply url-for [:naddr-view :naddr "naddr1r30l8j4vmppvq8w23umcyvd3vct4zmfpfkn4c7h2h057rmlfcrmq9xt9ma"]) + (apply url-for [:home]) + (navigate! (parse "/opencard"))) (defn parse [url] diff --git a/src/ied/subs.cljs b/src/ied/subs.cljs index 13e2538..a5fb099 100644 --- a/src/ied/subs.cljs +++ b/src/ied/subs.cljs @@ -1,6 +1,7 @@ (ns ied.subs (:require [re-frame.core :as re-frame] + [ied.events :as events] [clojure.string :as str] [clojure.set :as set] [ied.nostr :as nostr])) @@ -25,6 +26,15 @@ (fn [db _] (:events db))) +(re-frame/reg-sub + ::event-by-id + (fn [db [_ id]] + (first (filter #(= (:id %) id) (:events db))))) + +(re-frame/reg-sub + ::events-by-d-tag + (fn [db [_ d]] + (sort-by :created_at #(> %1 %2) (filter #(= d (nostr/get-d-id-from-event %)) (:events db))))) (re-frame/reg-sub ::metadata-events @@ -44,7 +54,9 @@ (re-frame/reg-sub ::npub (fn [db _] - (nostr/get-npub-from-pk (:pk db)))) + (if (:pk db) + (nostr/get-npub-from-pk (:pk db)) + nil))) (re-frame/reg-sub ::nsec @@ -93,7 +105,7 @@ deleted-list-ids) (not (contains? deleted-list-ids d-id))) -(defn most-recent-by-d-tag +(defn most-recent-event-by-d-tag [events] (->> events (group-by (fn [event] @@ -112,9 +124,9 @@ :<- [::deleted-list-ids] (fn [[list-kinds events deleted-lists]] (let [all-lists (filter #(and (some #{(:kind %)} list-kinds) - #_(d-id-not-in-deleted-list-ids (get-d-id-from-tags (:tags %)) deleted-lists)) + (d-id-not-in-deleted-list-ids (get-d-id-from-tags (:tags %)) deleted-lists)) events) - most-recent-lists (most-recent-by-d-tag all-lists)] + most-recent-lists (most-recent-event-by-d-tag all-lists)] #_(.log js/console "all lists: " (clj->js all-lists)) #_(.log js/console "most recent lists: " (clj->js most-recent-lists)) most-recent-lists))) @@ -210,6 +222,120 @@ (filter (fn [e] (contains? event-ids (:id e))) (:events db)))) (re-frame/reg-sub - ::visited-at + ::visited-at + (fn [db _] + (:visited-at db))) + +(re-frame/reg-sub + ::follow-sets + (fn [db _] + (filter #(= 30000 (:kind %)) (:events db)))) + +(re-frame/reg-sub + ;; returns a vector of pks of people the actor follows + ::following + :<- [::follow-sets] + :<- [::pk] + (fn [[follow-sets pk]] + (let [f (->> follow-sets + (filter #(= pk (:pubkey %))) + (map :tags) + (apply concat) + (filter #(= "p" (first %))) + (map second))] + f))) + +(re-frame/reg-sub + ::posts-from-pks-actor-follows + :<- [::following] + :<- [::events] + (fn [[following-pks events]] + (->> events + (filter (fn [e] (some #(= (:pubkey e) %) following-pks))) + (filter #(= 1 (:kind %)))))) + +(re-frame/reg-sub + ::concept-schemes + (fn [db [_ schemes]] + (into {} (map (fn [cs] + [cs (get-in db [:concept-schemes cs]) nil]) + schemes)))) + +(comment + (get-in {:concept-schemes {"1" :yes "2" :no}} [:concept-schemes "3"] nil)) + +(re-frame/reg-sub + ::toggled-concepts + (fn [db] + (apply concat (vals (:md-form-resource db))))) + +(re-frame/reg-sub + ::toggled + :<- [::toggled-concepts] + (fn [[toggled-concepts] id] + (some #(= (:id %) id) toggled-concepts))) + +(re-frame/reg-sub + ::md-form-array-input-fields + (fn [db [_ field]] + (get-in db [:md-form-resource field] {(random-uuid) nil}))) + +(re-frame/reg-sub + ::search-results + (fn [db] + (get db :search-results []))) + +(re-frame/reg-sub + ::grouped-search-results + :<- [::search-results] + (fn [search-results] + (group-by #(get-in % [:document :resource_id]) search-results))) + +(defn profile-from-db [db pubkey] ;; TODO think about if this will always fetch the latest profile + (js->clj (js/JSON.parse (:content (first (filter #(and (= 0 (:kind %)) + (= pubkey (:pubkey %))) (:events db))))) + :keywordize-keys true)) + +(re-frame/reg-sub + ::profile + (fn [db [_ pubkey]] + (let [profile (profile-from-db db pubkey)] + (when (nil? profile) + (re-frame/dispatch [::events/load-profile pubkey])) + profile))) + +(re-frame/reg-sub + ::profiles + (fn [db [_ pubkeys]] + (let [profiles (map (fn [p] [p (profile-from-db db p)]) pubkeys)] + (doseq [[pubkey profile] profiles] + (when (nil? profile) + (re-frame/dispatch [::events/load-profile pubkey]))) + profiles))) ;; unique profiles + +(re-frame/reg-sub + ::md-form-image (fn [db _] - (:visited-at db))) + (-> db :md-form-resource :image))) + +(re-frame/reg-sub + ::selected-md-scheme + (fn [db] + (:selected-md-scheme db))) + +(re-frame/reg-sub + ::md-form-resource + (fn [db] + (:md-form-resource db))) + +(re-frame/reg-sub + ::user-language + (fn [db] + (:user-language db))) + +(comment + @(re-frame/subscribe [::profile "1c5ff3caacd842c01dca8f378231b16617516d214da75c7aeabbe9e1efe9c0f6"]) + + @(re-frame/subscribe [::md-form-resource]) + @(re-frame/subscribe [::md-form-image]) + ) diff --git a/src/ied/utils.cljs b/src/ied/utils.cljs new file mode 100644 index 0000000..dc690f3 --- /dev/null +++ b/src/ied/utils.cljs @@ -0,0 +1,113 @@ +(ns ied.utils + (:require [re-frame.core :as re-frame])) + +(comment + (def test-form-data + {:description "eine beschreibung", + :creator + {#uuid "36620422-1c79-4610-96e7-dc161fcc823e" "autor 1", + #uuid "270fc20f-74ca-40a1-89ea-12e35253d714" "autor 2"}, + :publisher + {#uuid "4b47e9cf-21d5-4e35-8afd-c7989a3c464b" "v1", + #uuid "340af7ad-e94a-45ff-8221-94bc89d1f976" "v2"}, + :funder + {#uuid "cd65e75f-e0e6-4b6f-994d-14ab89b5ce7c" "stil", + #uuid "2f6cf069-8ddb-4e17-b479-d0c388931b83" "daad"}, + :learningResourceType + ({:id "https://w3id.org/kim/hcrt/application", + :prefLabel + {:de "Softwareanwendung", + :en "Software Application", + :nl "Computerprogramma", + :uk "Програмне забезпечення", + :cs "Počítačový program", + :fr "Application logicielle"}} + {:id "https://w3id.org/kim/hcrt/assessment", + :prefLabel + {:de "Lernkontrolle", + :en "Assessment", + :nl "Evaluatie", + :uk "Оцінювання", + :cs "Hodnocení", + :fr "Contrôle d’apprentissage"}}), + :name "ein name", + :datePublished "2018-01-10", + :keywords + {#uuid "516abd15-67dd-49fb-b1dd-699c84e7e76d" "s1", + #uuid "27d95b48-8cbd-4ae8-90fc-2f88d88ee96e" "s2"}, + :image "ein bild", + :uri "eine uri", + :about + [{:id "https://w3id.org/kim/hochschulfaechersystematik/n136", + :notation ["136"], + :prefLabel + {:de "Religionswissenschaft", + :en "Religious Studies", + :uk "Релігієзнавство"}} + {:id "https://w3id.org/kim/hochschulfaechersystematik/n1", + :notation ["1"], + :prefLabel + {:de "Geisteswissenschaften", + :en "Humanities", + :uk "Гуманітарні науки"}} + {:id "https://w3id.org/kim/hochschulfaechersystematik/n02", + :notation ["02"], + :prefLabel + {:de "Evang. Theologie, -Religionslehre", + :en "Protestant Theology, Protestant Religious Education", + :uk "Протестантська теологія, протестантська релігійна освіта"}} + {:id "https://w3id.org/kim/hochschulfaechersystematik/n01", + :notation ["01"], + :prefLabel + {:de "Geisteswissenschaften allgemein", + :en "Humanities (general)", + :uk "Гуманітарні науки загалом"}}]}) + + (println test-form-data) + + (form-data-to-tags test-form-data :description) + (form-data-to-tags test-form-data :creator-form) + + (def creator-uuid + {:creator + {#uuid "36620422-1c79-4610-96e7-dc161fcc823e" {:id 1 + :name "autor 1"} + #uuid "270fc20f-74ca-40a1-89ea-12e35253d714" {:id 2 + :name "autor 2"}}}) + + (form-data-to-tags creator-uuid :creator-form) + + (def creator-array + {:creator + [{:id 1 + :name "autor 1"} + {:id 2 + :name "autor 2"}]}) + + (form-data-to-tags creator-array :creator)) + +(defmulti form-data-to-tags (fn [form-data k] k)) + +(defmethod form-data-to-tags :default [form-data k] + nil) + +(defmethod form-data-to-tags :description [form-data _] + ["description" (:description form-data)]) + +(defmethod form-data-to-tags :creator-form [form-data _] + (map (fn [e] + ["creator" (get (val e) :id "") (get (val e) :name "")]) + (:creator form-data))) + +(defmethod form-data-to-tags :creator [form-data _] + (map (fn [e] + ["creator" (get e :id "") (get e :name "")]) + (:creator form-data))) + +(defmethod form-data-to-tags :publisher [form-data _] + (map (fn [e] ["creator" (val e)]) (:creator form-data))) + +(defmethod form-data-to-tags :funder [form-data _] + (map (fn [e] ["creator" (val e)]) (:creator form-data))) + +(defmethod form-data-to-tags :learningResourceType [form-data _]) diff --git a/src/ied/views.cljs b/src/ied/views.cljs index 802a99a..8617803 100644 --- a/src/ied/views.cljs +++ b/src/ied/views.cljs @@ -6,6 +6,11 @@ [ied.routes :as routes] [ied.subs :as subs] [ied.nostr :as nostr] + [ied.opencard.views :as opencard] + [ied.views.search :as search] + [ied.views.resource :as resource] + [ied.views.add :as add] + [ied.components.resource :as resource-component] [reagent.core :as reagent])) (defn layer-icon [] @@ -30,68 +35,6 @@ {:d "M6 2c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm7 7V3.5L18.5 9z"}]]) -;; add resource form -(defn add-resource-form - [name uri author] - (let [s (reagent/atom {:name name - :uri uri - :author author})] - (fn [] - [:form {:class "flex flex-col space-y-4" - :on-submit (fn [e] - (.preventDefault e) - ;; do something with the state @s - )} - [:label - {:class "input input-bordered flex items-center gap-2"} - "Name" - [:input {:on-change (fn [e] - (swap! s assoc :name (-> e .-target .-value))) - :type "text", :class "grow", :placeholder "Best Resource Ever"}]] - [:label - {:class "input input-bordered flex items-center gap-2"} - "URL" - [:input {:on-change (fn [e] - (swap! s assoc :uri (-> e .-target .-value))) - :type "text", :class "grow", :placeholder "https://wirlernenonline.de/r31"}]] - #_[:label - {:class "input input-bordered flex items-center gap-2"} - "Author" - [:input {:on-change (fn [e] - (swap! s assoc :author (-> e .-target .-value))) - :type "text", :class "grow", :placeholder "Robbi"}]] - - [:button {:class "btn btn-warning w-1/2 mx-auto" - :on-click #(re-frame/dispatch [::events/publish-resource {:name (:name @s) ;; TODO this should be sth like build event - :id (:uri @s) - :author (:author @s)}])} - "Publish Resource"]]))) - -(defn add-resource-by-json [] - (let [s (reagent/atom {:json-string ""})] - (fn [] - [:div - [:p "Paste an AMB object"] - [:form {:on-submit (fn [e] (.preventDefault e))} - [:textarea {:on-change (fn [e] - (swap! s assoc :json-string (-> e .-target .-value)))}] - [:button {:class "btn btn-warning" - :on-click #(re-frame/dispatch [::events/convert-amb-and-publish-as-nostr-event (:json-string @s)])} - "Publish as Nostr Event"]]]))) - -;; TODO try again using xhrio -(defn add-resosurce-by-uri [] - (let [uri (reagent/atom {:uri ""})] - (fn [] - [:form {:on-submit (fn [e] (.preventDefault e))} - [:label {:for "uri"} "URI: "] - [:input {:id "uri" - :on-change (fn [e] - (swap! uri assoc :uri (-> e .-target .-value)))}] - [:button {:class "btn btn-warning" - :on-click #(re-frame/dispatch [::events/publish-amb-uri-as-nostr-event (:uri @uri)])} - "Publish as Nostr Event"]]))) - ;; event data modal (defn event-data-modal [] (let [visible? @(re-frame/subscribe [::subs/show-event-data-modal]) @@ -108,68 +51,83 @@ [:button "close"]]]))) ;; metadata event component +;; TODO add author profile name and image (defn metadata-event-component [event] (let [selected-events @(re-frame/subscribe [::subs/selected-events])] - [:div - {:class "animate-flyIn card bg-base-100 w-96 shadow-xl min-h-[620px]"} - [:div {:class "relative"} - [:div {:class "absolute w-12 h-12 right-12 -bottom-4 rounded-full bg-white"} - (cond - (= 30004 (:kind event)) [layer-icon] - (= 30142 (:kind event)) [file-icon])] + [:div {:class "flex flex-row gap-2"} + [:div {:class "w-3/4 p-2 min-h-full flex flex-col mt-2 justify-between"} + [:div {:class ""} + [:div {:class "flex flex-row gap-2 "} + [:div {:class "w-6 h-6 rounded-full bg-white"} + (cond + (= 30004 (:kind event)) [layer-icon] + (= 30142 (:kind event)) [file-icon])] + [:a {:href (nostr/get-d-id-from-event event) + :class "font-bold hover:underline text-ellipsis overflow-hidden"} + (nostr/get-name-from-metadata-event event)]] + [:div {:class "flex flex-wrap"} + (resource-component/about-tags [event])] + [:p {:class "text-wrap"} + (nostr/get-description-from-metadata-event event)]] + + [:div {:class "flex flex-row justify-between items-center"} + [:button {:class "btn" + :on-click #(re-frame/dispatch [::events/toggle-show-event-data-modal event])} "Show Event Data"] + [:div + [:div {:class "form-control"} + [:label {:class "cursor-pointer label "} + [:span {:class "label-text m-2"} "Add to List "] + [:input + {:type "checkbox" + :checked (contains? (set (map #(:id %) selected-events)) (:id event)) + :class "checkbox checkbox-success" + :on-change #(re-frame/dispatch [::events/toggle-selected-events event])}]]]]]] + [:div {:class "w-1/4"} [:figure [:img - {:class "h-48 object-cover" + {:class "h-48 object-cover mx-auto m-2" :loading "lazy" :src (nostr/get-image-from-metadata-event event) - :alt ""}]]] - [:div - {:class "card-body"} - [:a {:href (nostr/get-d-id-from-event event) - :class "card-title hover:underline truncate"} - (nostr/get-name-from-metadata-event event)] - (doall - (for [about (nostr/get-about-names-from-metadata-event event)] - [:div {:class "badge badge-primary m-1 truncate " - :key about} about])) - [:p {:class "break-all"} - (nostr/get-description-from-metadata-event event)] - [:button {:on-click #(re-frame/dispatch [::events/toggle-show-event-data-modal event])} "Show Event Data"] - [:div - {:class "card-actions justify-end"} - [:div - {:class "form-control"} - [:label - {:class "cursor-pointer label"} - [:span {:class "label-text"} ""] - [:input - {:type "checkbox" - :checked (contains? (set (map #(:id %) selected-events)) (:id event)) - :class "checkbox checkbox-success" - :on-change #(re-frame/dispatch [::events/toggle-selected-events event])}]]]]]])) + :alt ""}]]]])) + +(defn activity-feed [] + (let [following @(re-frame/subscribe [::subs/following]) + npub @(re-frame/subscribe [::subs/npub]) + _ (when (nil? following) + (when npub (re-frame/dispatch [::events/get-follow-set-for-npub npub]))) + following-events @(re-frame/subscribe [::subs/posts-from-pks-actor-follows]) + _ (.log js/console (count following-events) (< 5 (count following-events))) + _ (when (> 5 (count following-events)) + (re-frame/dispatch [::events/events-from-pks-actor-follows following]))] + + [:div + (doall + (for [event following-events] + [:p {:key (:id event)} (:content event)]))])) ;; events (defn events-panel [] - (let [events @(re-frame/subscribe [::subs/feed-events]) - selected-events @(re-frame/subscribe [::subs/selected-events])] - [:div {:class ""} - [:p (str "Num of events: " (count events))] - (if (> (count events) 0) - [:div {:class "flex flex-wrap justify-center gap-2"} - (doall + (fn [] + (let [events @(re-frame/subscribe [::subs/feed-events]) + selected-events @(re-frame/subscribe [::subs/selected-events])] + [:div {:class "flex flex-col mx-auto gap-4 "} + [activity-feed] + (if (> (count events) 0) + [:div {:class "divide-y divide-slate-500"} + (doall + (for [event events] + [:div {:key (:id event) + :class ""} + [metadata-event-component event]]))] - (for [event events] - [:div {:key (:id event)} - [metadata-event-component event]]))] - - [:p "no events there"]) - [:button {:class "btn" - :disabled (not (boolean (seq selected-events))) - :on-click #(re-frame/dispatch [::events/add-metadata-event-to-list [{:d "unique-id-1" - :name "Test List SC"} - selected-events]])} - "Add To Lists"]])) + [:p "no events there"]) + [:button {:class "btn" + :disabled (not (boolean (seq selected-events))) + :on-click #(re-frame/dispatch [::events/add-metadata-event-to-list [{:d "unique-id-1" + :name "Test List SC"} + selected-events]])} + "Add To Lists"]]))) ;; event feed component (defn event-feed-component [event] @@ -195,7 +153,7 @@ [:h1 "Event Feed"] [:p (str "Num of events: " (count events))] (if (> (count events) 0) - [:div {:class "flex flex-row gap-2"} + [:div {:class "flex flex-col"} (doall (for [event events] [:div {:key (:id event)} @@ -375,7 +333,8 @@ "Add To Lists"]]]]])) (defn user-menu [] - (let [pk @(re-frame/subscribe [::subs/pk])] + (let [pk @(re-frame/subscribe [::subs/pk]) + user-profile (re-frame/subscribe [::subs/profile pk])] (if pk [:div {:class "dropdown dropdown-end"} @@ -388,7 +347,7 @@ [:img {:alt "user image", :src - (str "https://robohash.org/" pk)}]]] + (nostr/profile-picture @user-profile pk)}]]] [:ul {:tabIndex "0", :class @@ -413,23 +372,21 @@ "... with Extension"]]]]))) ;; Header -(defn new-header [] - (let [selected-events @(re-frame/subscribe [::subs/selected-events]) - pk @(re-frame/subscribe [::subs/pk])] +(defn header [] + (let [pk @(re-frame/subscribe [::subs/pk])] [:div {:class "navbar sticky z-50 top-0 left-0 bg-base-100"} [:div {:class "flex-1"} - [:a {:class "btn btn-ghost text-xl" + [:a {:class "cursor-pointer text-xl font-bold" :on-click #(re-frame/dispatch [::events/navigate [:home]])} "edufeed"] #_[:a {:class "btn btn-circle" :on-click #(re-frame/dispatch [::events/navigate [:event-feed]])} "Event-Feed"]] [:div {:class "flex-none"} - [:a {:disabled (nil? pk) - :class "btn btn-circle btn-primary" + :class "btn btn-circle btn-primary text-xl" :on-click #(re-frame/dispatch [::events/navigate [:add-resource]])} "+"] (when (not (nil? pk)) [shopping-cart]) @@ -445,24 +402,6 @@ (defmethod routes/panels :keys-panel [] [keys-panel]) -;; Add Resource Panel -(defn add-resource-panel [] - [:div - {:class "sm:w-1/2 p-2 mx-auto flex flex-col space-y-4"} - [add-resource-form] - [:div - {:tabIndex "0", - :class "collapse collapse-arrow border-base-300 bg-base-200 border"} - [:div - {:class "collapse-title text-xl font-medium"} - "Advanced Options"] - [:div - {:class "collapse-content"} - [add-resource-by-json] - #_[add-resosurce-by-uri]]]]) - -(defmethod routes/panels :add-resource-panel [] [add-resource-panel]) - ;; Settings (defn settings-panel [] [:div @@ -471,10 +410,25 @@ (defmethod routes/panels :settings-panel [] [settings-panel]) +(defn sidepanel [] + (let [pk (re-frame/subscribe [::subs/pk])] + (fn [] + [:div {:class "static flex flex-col m-2 "} + [:ul {:class "menu rounded-box"} + [:li + [:a {:on-click #(re-frame/dispatch [::events/navigate [:home]])} + "Feed"]] + [:li [:a {:on-click #(re-frame/dispatch [::events/navigate [:search-view]])} + "Search"]] + [:li [:a "My Network"]] + [:li [:a {:on-click #(re-frame/dispatch [::events/navigate [:npub-view :npub (nostr/get-npub-from-pk @pk)]])} + "Bookmarks"]] + [:li [:a {:on-click #(re-frame/dispatch [::events/navigate [:opencard-view]])} "Opencard"]]]]))) + ;; Home (defn home-panel [] (let [events (re-frame/subscribe [::subs/events])] - [:div + [:div {:class "basis-5/6"} [events-panel] [:p (count @events)]])) @@ -542,9 +496,13 @@ (defn main-panel [] (let [active-panel (re-frame/subscribe [::subs/active-panel])] [:div {:class "relative"} - [new-header] - [:div {:class ""} - (routes/panels @active-panel) + [header] + [:div {:class "flex flex-row"} + [:div {:class "w-1/6"} + [sidepanel]] + [:div {:class "w-5/6 mt-1"} + [:div {:class ""} + (routes/panels @active-panel)]] [add-to-lists-modal] [create-list-modal] [event-data-modal]]])) diff --git a/src/ied/views/add.cljs b/src/ied/views/add.cljs new file mode 100644 index 0000000..8288a62 --- /dev/null +++ b/src/ied/views/add.cljs @@ -0,0 +1,280 @@ +(ns ied.views.add + (:require [re-frame.core :as re-frame] + [reagent.core :as reagent] + [ied.subs :as subs] + [ied.events :as events] + [ied.components.icons :as icons] + [ied.routes :as routes])) + +(def md-scheme-map + {:amblight {:name {:title "Name" + :type :string} + :uri {:title "URL" + :type :string} + :author {:title "Author" + :type :string} + :keywords {:title "Schlagworte" + :type :array + :items {:type :string}} + :about {:title "Worum geht es??" + :type :skos + :schemes ["https://w3id.org/kim/hochschulfaechersystematik/scheme"]} + :learningResourceType {:type :skos + :schemes ["https://w3id.org/kim/hcrt/scheme"]}} + :genmint (array-map :name {:title "Name" + :type :string} + :uri {:title "URL" + :type :string} + :image {:title "Thumbnail" + :type :string} + :description {:title "Beschreibung" + :type :string + :x-string-type :textarea} + :creator {:title "Autor:innen" + :type :array + :items {:type :string}} + :publisher {:title "Veröffentlicher:innen" + :type :array + :items {:type :string}} + :funder {:title "Gefördert durch" + :type :array + :items {:type :string}} + :datePublished {:title "Publikationsdatum" + :type :string + :format :date} + :learningResourceType {:type :skos + :schemes ["https://w3id.org/kim/hcrt/scheme"]} + :teaches {:title "Welche Kompetenzen werden adressiert?" + :type :skos + :schemes [""]} + :about {:title "Fächersystematik" + :type :skos + :schemes ["https://w3id.org/kim/hochschulfaechersystematik/scheme"]} + :keywords {:title "Schlagworte" + :type :array + :items {:type :string}} + :inLanguage {:title "Sprache" + :type :string + :enum ["de" "en"]} + :isAccessibleForFree {:title "Frei zugänglich" + :type :string + :enum ["ja" "nein"]} + ;; LICENSE + :license {:title "Lizenz" + :type :string + :enum ["CC-0" "CC-BY"]} + ;; ConditionsOfAccess + ;; audience + ;; didaktische Methoden und Planungshilfen + ;; educationalLevel + ) + :amb {:name {:title "Name" + :type :string} + :uri {:title "URL" + :type :string} + :author {:title "Author" + :type :string} + :learningResourceType {:title "Learning Resource Typ" + :type :skos + :schemes ["https://w3id.org/kim/hcrt/scheme"]} + :about {:type :skos + :schemes ["https://w3id.org/kim/hochschulfaechersystematik/scheme"]}}}) + +(defn concept-label-component + [[concept field]] + (fn [] + (let [toggled-concepts @(re-frame/subscribe [::subs/toggled-concepts]) + toggled (some #(= (:id %) (:id concept)) toggled-concepts)] + [:div {:class ""} + [:p + {:class (str "hover:bg-base-200 cursor-pointer" (when toggled " bg-white")) + :on-click (fn [] + (re-frame/dispatch [::events/toggle-concept [concept field]]))} + (-> concept :prefLabel :de)] + (when-let [narrower (:narrower concept)] + [:div {:class "pl-2 "} + (for [child narrower] + ^{:key (:id child)} [concept-label-component [child field]])])]))) + +(defn skos-concept-scheme-multiselect-component + [[cs field field-title]] + [:div + {:class "dropdown w-full" + :key (:id cs)} + [:div + {:tabIndex "0" + :role "button" + :class "btn m-1 grow w-full"} + field-title] + [:div + {:tabIndex "0", + :class + "dropdown-content card card-compact bg-base-100 z-[1] w-64 p-2 shadow"} + [:div + {:class "card-body"} + [:h3 {:class "card-title"} + field-title] + (doall + (for [concept (:hasTopConcept cs)] + ^{:key (:id concept)} [concept-label-component [concept field]])) + [:p "you can use any element as a dropdown."]]]]) + +(defn array-fields-component [selected-md-scheme field field-title] + (let [array-items-type (get-in selected-md-scheme [field :items :type]) + fields (re-frame/subscribe [::subs/md-form-array-input-fields field])] + (fn [] + [:div {:class "flex flex-col gap-1"} + [:p field-title] + (case array-items-type + :string (doall (for [[k v] @fields] + [:div {:class "flex flex-row gap-1" + :key k} + [:input {:type "text" + :class "input input-bordered w-full" + :default-value "" + :on-blur (fn [e] + (re-frame/dispatch + [::events/handle-md-form-array-input + [field + k + (-> e .-target .-value)]]))}] + [:button {:class "btn btn-square" + :on-click + #(re-frame/dispatch + [::events/handle-md-form-rm-input + [field k]])} [icons/close-icon]]]))) + [:button {:class "btn" + :on-click #(re-frame/dispatch + [::events/handle-md-form-add-input [field]])} + "Add field"]]))) + +;; add resource form +(defn add-resource-form + [] + (let [md-scheme (re-frame/subscribe [::subs/selected-md-scheme]) + md-form-image (re-frame/subscribe [::subs/md-form-image])] + (fn [] + [:form {:class "flex flex-col space-y-4" + :on-submit (fn [e] + (.preventDefault e))} + [:select + {:default-value @md-scheme + :on-change (fn [e] + (let [value (-> e .-target .-value keyword)] + (re-frame/dispatch [::events/set-md-scheme value]))) + :class "select select-bordered w-full max-w-xs"} + [:option {:disabled true} "Select a Metadata Scheme"] + [:option {:value "amblight"} "AMB Light"] + [:option {:value "genmint"} "GenMint"]] + + (let [selected-md-scheme (get md-scheme-map @md-scheme (:amblight md-scheme-map))] + [:div {:class "flex flex-col gap-2"} + (when @md-form-image + [:img {:src @md-form-image}]) + (doall + (for [field (keys selected-md-scheme)] + (let [field-type (-> selected-md-scheme field :type) + field-title (get-in selected-md-scheme [field :title] (name field))] + (cond ;; TODO the conditions need to get more logic to evaluate form fields in order + (and (= :string field-type) + (= :textarea (-> selected-md-scheme field :x-string-type))) + [:textarea {:key field + :type "" + :class "textarea textarea-bordered grow" + :placeholder field-title + :on-blur #(re-frame/dispatch [::events/handle-md-form-input [field (-> % .-target .-value)]])}] + (and (= :string field-type) + (-> selected-md-scheme field :enum)) + (let [enum-vals (-> selected-md-scheme field :enum)] + [:select + {:key field + :class "select select-bordered w-full"} + [:option {:disabled "" :selected ""} field-title] + (doall + (for [val enum-vals] + [:option {:key val} val]))]) + (and (= :string field-type) + (= :date (-> selected-md-scheme field :format))) + [:div {:class "my-4"} + [:label {:for field} field-title] + [:input + {:type "date" + :on-blur #(re-frame/dispatch [::events/handle-md-form-input [field (-> % .-target .-value)]]) + :id field + :name field-title + ; :value "2018-07-22" + :min "2018-01-01" + :max "2018-12-31"}]] + (= :string field-type) + [:label {:key field + :class "input input-bordered flex items-center gap-2"} + (-> selected-md-scheme field :title) + [:input {:type "text" + :class "grow" + :placeholder (name field) + :on-blur #(re-frame/dispatch [::events/handle-md-form-input [field (-> % .-target .-value)]])}]] ;; TODO maybe better handle this with an atom in the component + (= :skos field-type) + (let [concept-schemes @(re-frame/subscribe + [::subs/concept-schemes (-> selected-md-scheme field :schemes)]) + _ (doall (for [[cs-uri _] (filter (fn [[_ v]] (nil? v)) concept-schemes)] + (re-frame/dispatch [::events/skos-concept-scheme-from-uri cs-uri])))] + + (doall + (for [cs (keys concept-schemes)] + ^{:key cs} [skos-concept-scheme-multiselect-component [(get concept-schemes cs) + field + field-title]]))) + (= :array field-type) + ^{:key field} [array-fields-component selected-md-scheme field field-title] + :else + [:p {:key field} "field type not found"]))))]) + + [:button {:class "btn btn-warning w-1/2 mx-auto" + :on-click #(re-frame/dispatch [::events/publish-resource])} + "Publish Resource"]]))) + +(defn add-resource-by-json [] + (let [s (reagent/atom {:json-string ""})] + (fn [] + [:div {:class "flex flex-col"} + [:p "Paste an AMB object"] + [:form {:class "flex flex-col" + :on-submit (fn [e] (.preventDefault e))} + [:textarea {:rows 10 + :on-change (fn [e] + (swap! s assoc :json-string (-> e .-target .-value)))}] + [:button {:class "btn btn-warning" + :on-click #(re-frame/dispatch [::events/convert-amb-string-and-publish-as-nostr-event (:json-string @s)])} + "Publish as Nostr Event"]]]))) + +;; TODO try again using xhrio +(defn add-resosurce-by-uri [] + (let [uri (reagent/atom {:uri ""})] + (fn [] + [:form {:class "flex flex-col" + :on-submit (fn [e] (.preventDefault e))} + [:label {:for "uri"} "URI: "] + [:input {:id "uri" + :on-change (fn [e] + (swap! uri assoc :uri (-> e .-target .-value)))}] + [:button {:class "btn btn-warning" + :on-click #(re-frame/dispatch [::events/publish-amb-uri-as-nostr-event (:uri @uri)])} + "Publish as Nostr Event"]]))) + +;; Add Resource Panel +(defn add-resource-panel [] + [:div + {:class "sm:w-1/2 p-2 mx-auto flex flex-col space-y-4"} + [add-resource-form] + [:details + {:tabIndex "0", + :class "collapse collapse-arrow border-base-300 bg-base-200 border"} + [:summary + {:class "collapse-title text-xl font-medium"} + "Advanced Options"] + [:div + {:class "collapse-content"} + [add-resource-by-json] + [add-resosurce-by-uri]]]]) + +(defmethod routes/panels :add-resource-panel [] [add-resource-panel]) diff --git a/src/ied/views/resource.cljs b/src/ied/views/resource.cljs new file mode 100644 index 0000000..c9ef140 --- /dev/null +++ b/src/ied/views/resource.cljs @@ -0,0 +1,43 @@ +(ns ied.views.resource + (:require + [re-frame.core :as re-frame] + [ied.subs :as subs] + [ied.nostr :as nostr] + [ied.routes :as routes] + [ied.components.resource :as resource-components] + [ied.components.resource :as resource-component])) + +(defn naddr-view-panel [] + (let [route-params @(re-frame/subscribe [::subs/route-params]) + naddr (:naddr route-params) + data (nostr/decode-naddr naddr) + events-with-same-d (re-frame/subscribe [::subs/events-by-d-tag (:identifier data)]) + latest-event (first @events-with-same-d) + _ (.log js/console "events with same d id" (clj->js @events-with-same-d)) + ; _ (.log js/) + ] + + [:div + (case (:kind data) + 30142 [:div {:class "flex flex-col"} + [:img {:class "w-full object-contain bg-transparent max-h-[40vh] " + :src (nostr/get-image-from-metadata-event latest-event)}] + [:h1 {:class "text-2xl font-bold"} (nostr/get-name-from-metadata-event latest-event)] + [:div + (resource-component/skos-tags [@events-with-same-d "about"])] + ;; Keywords + [:div + (doall + (for [kw (nostr/get-keywords-from-metadata-event latest-event)] + (resource-components/keywords-component kw)))] + ;; Author + [:div {:class "ml-auto mr-0 my-2"} + (resource-component/authors-component latest-event)] + [:p (nostr/get-description-from-metadata-event latest-event)]] + + [:p "Dunno how to render that, sorry 🤷"])])) + +(comment + (re-frame/subscribe [::subs/events-by-d-tag "https://langsci-press.org/catalog/book/406"])) + +(defmethod routes/panels :naddr-view-panel [] [naddr-view-panel]) diff --git a/src/ied/views/search.cljs b/src/ied/views/search.cljs new file mode 100644 index 0000000..3e936c8 --- /dev/null +++ b/src/ied/views/search.cljs @@ -0,0 +1,123 @@ +(ns ied.views.search + (:require [re-frame.core :as re-frame] + [ied.routes :as routes] + [ied.subs :as subs] + [ied.events :as events] + [reagent.core :as reagent] + [clojure.string :as str] + [ied.components.resource :as resource-components] + [ied.nostr :as nostr] + [ied.components.resource :as resource-component] + [cljs.core :as c])) + +;; TODO refactor this to also work with raw event data +(defn extract-string [result] + (let [document (:document result) + event-info (select-keys document [:id :event_created_at :event_kind :event_pubkey]) + extracted-string (map (fn [k] + {:keyword k + :event-info event-info}) + (get document :keywords))] + extracted-string)) + +(defn group-by-string [data] + (let [extracted-string (into [] (apply concat (map extract-string data))) + grouped-keyword (group-by :keyword extracted-string)] + grouped-keyword)) + +(defn result [r] + ; r is an array of matching event results + (let [id (nostr/get-d-id-from-event (-> (first r) :document :event_raw)) + pubkeys (map #(get-in % [:document :event_pubkey]) r) + profiles (re-frame/subscribe [::subs/profiles pubkeys]) + name (first (map #(get-in % [:document :name]) r)) + description (first (map #(get-in % [:document :description]) r)) + events (map #(-> % :document :event_raw) r) + keywords (group-by-string r) + latest-event (-> (last (sort-by #(-> r :document :event_created_at) r)) + :document + :event_raw) + naddr (nostr/naddr-from-event latest-event)] + (fn [] + [:div {:class "flex flex-row"} + [:div {:class "flex flex-col w-3/4"} + [:div {:class "flex flex-row items-center mt-2"} + [:div + {:class "avatar-group -space-x-6 "} + (doall + (for [[pubkey profile] @profiles] + ^{:key pubkey} [:div + {:class "avatar bg-neutral"} + [:div + {:class "w-8"} + [:img + {:src + (nostr/profile-picture profile pubkey)}]]]))] + [:p (str + (str/join + ", " + (remove str/blank? (map (fn [[_ p]] (get p :display_name "unbekannt")) @profiles))) + (if (> (count @profiles) 1) + " haben" + " hat") + " das geteilt.")]] + + [:a {:href id + :target "_blank" + :class "text-xl hover:underline"} name] + [:div + (resource-components/skos-tags [events "about"])] + ;; keywords + [:div {:class "flex flex-wrap"} + (doall + (for [[k v] keywords] + ^{:key k} (resource-component/keywords-component k)))] + + [:p {:class "line-clamp-3"} description] + + [:label {:class "btn self-end mb-0 mt-auto " + :on-click #(re-frame/dispatch [::events/navigate [:naddr-view :naddr naddr]])} + "Details"]] + + [:div {:class "w-1/4"} + [:figure + [:img + {:class "h-48 object-cover mx-auto m-2" + :loading "lazy" + :src + (nostr/get-image-from-metadata-event latest-event) + :alt ""}]]]]))) + +(defn search-view-panel [] + (let [search-term (reagent/atom nil) + grouped-results (re-frame/subscribe [::subs/grouped-search-results]) + _ (.log js/console (clj->js @grouped-results))] + [:div {:class ""} + [:form {:on-submit (fn [e] + (.preventDefault e) + (.log js/console @search-term) + (re-frame/dispatch [::events/handle-search @search-term]))} + [:label + {:class "input input-bordered flex items-center gap-2 w-1/2 mx-auto"} + [:input {:type "search" + :class "grow" + :placeholder "Search" + :on-change (fn [e] (reset! search-term (-> e .-target .-value)))}] + [:svg + {:xmlns "http://www.w3.org/2000/svg", + :viewBox "0 0 16 16", + :fill "currentColor", + :class "h-4 w-4 opacity-70"} + [:path + {:fill-rule "evenodd", + :d + "M9.965 11.026a5 5 0 1 1 1.06-1.06l2.755 2.754a.75.75 0 1 1-1.06 1.06l-2.755-2.754ZM10.5 7a3.5 3.5 0 1 1-7 0 3.5 3.5 0 0 1 7 0Z", + :clip-rule "evenodd"}]]]] + (when (not-empty @grouped-results) + [:div {:class "divide-y divide-slate-700 flex flex-col gap-2"} + (doall + (for [[k v] @grouped-results] + ^{:key k} [result v]))])])) + +(defmethod routes/panels :search-view-panel [] [search-view-panel]) +