some things are working

This commit is contained in:
@s.roertgen 2024-11-21 16:27:08 +01:00
parent 83ff1e86ac
commit 31aceb49c7
21 changed files with 2767 additions and 371 deletions

27
karma.conf.js Normal file
View file

@ -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
}
})
}

208
package-lock.json generated
View file

@ -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",

View file

@ -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": {

File diff suppressed because it is too large Load diff

View file

@ -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"

View file

@ -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"}]])

View file

@ -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"))

View file

@ -3,3 +3,5 @@
(def debug?
^boolean goog.DEBUG)
(def typesense-uri "http://localhost:8108/collections/amb/documents/")

View file

@ -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)))

View file

@ -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)))

View file

@ -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)))

View file

@ -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]

View file

@ -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])
)

331
src/ied/opencard/views.cljs Normal file
View file

@ -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])

View file

@ -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]

View file

@ -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])
)

113
src/ied/utils.cljs Normal file
View file

@ -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 dapprentissage"}}),
: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 _])

View file

@ -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]]]))

280
src/ied/views/add.cljs Normal file
View file

@ -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])

View file

@ -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])

123
src/ied/views/search.cljs Normal file
View file

@ -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])