implement tag cloud: resolves #9

TODO: Related Tags, All Tags
This commit is contained in:
Jon Schoning 2020-01-24 19:29:21 -06:00
parent 0546de274d
commit 4e182c5afe
16 changed files with 1032 additions and 68 deletions

View file

@ -31,7 +31,11 @@ api/accountSettings EditAccountSettingsR POST
-- add
/add AddViewR GET
api/add AddR POST
-- api
api/lookuptitle LookupTitleR POST
api/tagcloud UserTagCloudR POST
api/tagcloudmode UserTagCloudModeR POST
-- edit
/bm/#Int64 DeleteR DELETE

View file

@ -4,7 +4,7 @@ cabal-version: 1.12
--
-- see: https://github.com/sol/hpack
--
-- hash: 9ee2c0431ff44c217101ae145895fa4c4df6141ac59c65d5d35f586938ce6657
-- hash: 853ba5e7d0435fcec73b493932da0f2748f4674bbfc62ebbd54df707166f999b
name: espial
version: 0.0.8
@ -79,6 +79,7 @@ extra-source-files:
purs/src/Component/NNote.purs
purs/src/Component/RawHtml.js
purs/src/Component/RawHtml.purs
purs/src/Component/TagCloud.purs
purs/test/Main.purs
source-repository head

579
purs/package-lock.json generated
View file

@ -1080,6 +1080,12 @@
"integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=",
"dev": true
},
"ansi-escapes": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz",
"integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==",
"dev": true
},
"ansi-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
@ -1125,6 +1131,18 @@
}
}
},
"aproba": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
"integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
"dev": true
},
"arch": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/arch/-/arch-2.1.1.tgz",
"integrity": "sha512-BLM56aPo9vLLFVa8+/+pJLnrZ7QGGTVHWsCwieAWT9o9K8UeGaQbzZbGoabWLOo2ksBCztoXdqBZBplqLDDCSg==",
"dev": true
},
"argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
@ -1408,6 +1426,12 @@
"file-uri-to-path": "1.0.0"
}
},
"bluebird": {
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
"dev": true
},
"bn.js": {
"version": "4.11.8",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
@ -1608,6 +1632,34 @@
"integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=",
"dev": true
},
"byline": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz",
"integrity": "sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE=",
"dev": true
},
"cacache": {
"version": "11.3.3",
"resolved": "https://registry.npmjs.org/cacache/-/cacache-11.3.3.tgz",
"integrity": "sha512-p8WcneCytvzPxhDvYp31PD039vi77I12W+/KfR9S8AZbaiARFBCpsPJS+9uhWfeBfeAtW7o/4vt3MUqLkbY6nA==",
"dev": true,
"requires": {
"bluebird": "^3.5.5",
"chownr": "^1.1.1",
"figgy-pudding": "^3.5.1",
"glob": "^7.1.4",
"graceful-fs": "^4.1.15",
"lru-cache": "^5.1.1",
"mississippi": "^3.0.0",
"mkdirp": "^0.5.1",
"move-concurrently": "^1.0.1",
"promise-inflight": "^1.0.1",
"rimraf": "^2.6.3",
"ssri": "^6.0.1",
"unique-filename": "^1.1.1",
"y18n": "^4.0.0"
}
},
"cache-base": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz",
@ -1716,6 +1768,12 @@
"upath": "^1.1.1"
}
},
"chownr": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.3.tgz",
"integrity": "sha512-i70fVHhmV3DtTl6nqvZOnIjbY0Pe4kAUjwHj8z0zAdgBtYrJyYwLKCCuRBQ5ppkyL0AkN7HKRnETdmdp1zqNXw==",
"dev": true
},
"cipher-base": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz",
@ -1920,6 +1978,20 @@
"safe-buffer": "~5.1.1"
}
},
"copy-concurrently": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz",
"integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==",
"dev": true,
"requires": {
"aproba": "^1.1.1",
"fs-write-stream-atomic": "^1.0.8",
"iferr": "^0.1.5",
"mkdirp": "^0.5.1",
"rimraf": "^2.5.4",
"run-queue": "^1.0.0"
}
},
"copy-descriptor": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz",
@ -2335,6 +2407,12 @@
"cssom": "0.3.x"
}
},
"cyclist": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz",
"integrity": "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=",
"dev": true
},
"dashdash": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
@ -2592,6 +2670,18 @@
"readable-stream": "^2.0.2"
}
},
"duplexify": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz",
"integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==",
"dev": true,
"requires": {
"end-of-stream": "^1.0.0",
"inherits": "^2.0.1",
"readable-stream": "^2.0.0",
"stream-shift": "^1.0.0"
}
},
"ecc-jsbn": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
@ -2641,12 +2731,27 @@
"integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=",
"dev": true
},
"end-of-stream": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
"dev": true,
"requires": {
"once": "^1.4.0"
}
},
"entities": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz",
"integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==",
"dev": true
},
"env-paths": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.0.tgz",
"integrity": "sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA==",
"dev": true
},
"envinfo": {
"version": "7.5.0",
"resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.5.0.tgz",
@ -2765,6 +2870,81 @@
"safe-buffer": "^5.1.1"
}
},
"execa": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/execa/-/execa-2.1.0.tgz",
"integrity": "sha512-Y/URAVapfbYy2Xp/gb6A0E7iR8xeqOCXsuuaoMn7A5PzrXUK84E1gyiEfq0wQd/GHA6GsoHWwhNq8anb0mleIw==",
"dev": true,
"requires": {
"cross-spawn": "^7.0.0",
"get-stream": "^5.0.0",
"is-stream": "^2.0.0",
"merge-stream": "^2.0.0",
"npm-run-path": "^3.0.0",
"onetime": "^5.1.0",
"p-finally": "^2.0.0",
"signal-exit": "^3.0.2",
"strip-final-newline": "^2.0.0"
},
"dependencies": {
"cross-spawn": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.1.tgz",
"integrity": "sha512-u7v4o84SwFpD32Z8IIcPZ6z1/ie24O6RU3RbtL5Y316l3KuHVPx9ItBgWQ6VlfAFnRnTtMUrsQ9MUUTuEZjogg==",
"dev": true,
"requires": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
}
},
"mimic-fn": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
"dev": true
},
"onetime": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz",
"integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==",
"dev": true,
"requires": {
"mimic-fn": "^2.1.0"
}
},
"path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true
},
"shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
"requires": {
"shebang-regex": "^3.0.0"
}
},
"shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true
},
"which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"requires": {
"isexe": "^2.0.0"
}
}
}
},
"expand-brackets": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz",
@ -2971,6 +3151,12 @@
"integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==",
"dev": true
},
"figgy-pudding": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz",
"integrity": "sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w==",
"dev": true
},
"file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
@ -3015,6 +3201,36 @@
"locate-path": "^3.0.0"
}
},
"flush-write-stream": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz",
"integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==",
"dev": true,
"requires": {
"inherits": "^2.0.3",
"readable-stream": "^2.3.6"
}
},
"follow-redirects": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.10.0.tgz",
"integrity": "sha512-4eyLK6s6lH32nOvLLwlIOnr9zrL8Sm+OvW4pVTJNoXeGzYIkHVf+pADQi+OJ0E67hiuSLezPVPyBcIZO50TmmQ==",
"dev": true,
"requires": {
"debug": "^3.0.0"
},
"dependencies": {
"debug": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
"dev": true,
"requires": {
"ms": "^2.1.1"
}
}
}
},
"for-in": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
@ -3059,6 +3275,37 @@
"integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=",
"dev": true
},
"from2": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz",
"integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=",
"dev": true,
"requires": {
"inherits": "^2.0.1",
"readable-stream": "^2.0.0"
}
},
"fs-minipass": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz",
"integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==",
"dev": true,
"requires": {
"minipass": "^2.6.0"
}
},
"fs-write-stream-atomic": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz",
"integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=",
"dev": true,
"requires": {
"graceful-fs": "^4.1.2",
"iferr": "^0.1.5",
"imurmurhash": "^0.1.4",
"readable-stream": "1 || 2"
}
},
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@ -3647,6 +3894,15 @@
"integrity": "sha1-3Xzn3hh8Bsi/NTeWrHHgmfCYDrw=",
"dev": true
},
"get-stream": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz",
"integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==",
"dev": true,
"requires": {
"pump": "^3.0.0"
}
},
"get-value": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz",
@ -3987,6 +4243,12 @@
"integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==",
"dev": true
},
"iferr": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz",
"integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=",
"dev": true
},
"import-fresh": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz",
@ -3997,6 +4259,12 @@
"resolve-from": "^3.0.0"
}
},
"imurmurhash": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
"integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=",
"dev": true
},
"indexes-of": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz",
@ -4208,6 +4476,12 @@
"integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=",
"dev": true
},
"is-plain-obj": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz",
"integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==",
"dev": true
},
"is-plain-object": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
@ -4232,6 +4506,12 @@
"integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==",
"dev": true
},
"is-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz",
"integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==",
"dev": true
},
"is-svg": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-svg/-/is-svg-3.0.0.tgz",
@ -4529,6 +4809,17 @@
"chalk": "^2.0.1"
}
},
"log-update": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/log-update/-/log-update-3.3.0.tgz",
"integrity": "sha512-YSKm5n+YjZoGZT5lfmOqasVH1fIH9xQA9A81Y48nZ99PxAP62vdCCtua+Gcu6oTn0nqtZd/LwRV+Vflo53ZDWA==",
"dev": true,
"requires": {
"ansi-escapes": "^3.2.0",
"cli-cursor": "^2.1.0",
"wrap-ansi": "^5.0.0"
}
},
"loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@ -4538,6 +4829,15 @@
"js-tokens": "^3.0.0 || ^4.0.0"
}
},
"lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
"dev": true,
"requires": {
"yallist": "^3.0.2"
}
},
"magic-string": {
"version": "0.22.5",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.22.5.tgz",
@ -4602,6 +4902,12 @@
}
}
},
"merge-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
"dev": true
},
"merge2": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.3.0.tgz",
@ -4693,6 +4999,43 @@
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
"dev": true
},
"minipass": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz",
"integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==",
"dev": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
}
},
"minizlib": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz",
"integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==",
"dev": true,
"requires": {
"minipass": "^2.9.0"
}
},
"mississippi": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz",
"integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==",
"dev": true,
"requires": {
"concat-stream": "^1.5.0",
"duplexify": "^3.4.2",
"end-of-stream": "^1.1.0",
"flush-write-stream": "^1.0.0",
"from2": "^2.1.0",
"parallel-transform": "^1.1.0",
"pump": "^3.0.0",
"pumpify": "^1.3.3",
"stream-each": "^1.1.0",
"through2": "^2.0.0"
}
},
"mixin-deep": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz",
@ -4737,6 +5080,20 @@
"integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==",
"dev": true
},
"move-concurrently": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
"integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=",
"dev": true,
"requires": {
"aproba": "^1.1.1",
"copy-concurrently": "^1.0.0",
"fs-write-stream-atomic": "^1.0.8",
"mkdirp": "^0.5.1",
"rimraf": "^2.5.4",
"run-queue": "^1.0.3"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@ -4861,6 +5218,23 @@
"integrity": "sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg==",
"dev": true
},
"npm-run-path": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-3.1.0.tgz",
"integrity": "sha512-Dbl4A/VfiVGLgQv29URL9xshU8XDY1GeLy+fsaZ1AA8JDSfjvr5P5+pzRbWqRSBxk6/DW7MIh8lTM/PaGnP2kg==",
"dev": true,
"requires": {
"path-key": "^3.0.0"
},
"dependencies": {
"path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true
}
}
},
"nth-check": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz",
@ -5053,6 +5427,12 @@
"integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=",
"dev": true
},
"p-finally": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-2.0.1.tgz",
"integrity": "sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw==",
"dev": true
},
"p-limit": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.2.tgz",
@ -5083,6 +5463,17 @@
"integrity": "sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU=",
"dev": true
},
"parallel-transform": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.2.0.tgz",
"integrity": "sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==",
"dev": true,
"requires": {
"cyclist": "^1.0.1",
"inherits": "^2.0.3",
"readable-stream": "^2.1.5"
}
},
"parcel-bundler": {
"version": "1.12.4",
"resolved": "https://registry.npmjs.org/parcel-bundler/-/parcel-bundler-1.12.4.tgz",
@ -5793,6 +6184,12 @@
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"dev": true
},
"promise-inflight": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz",
"integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=",
"dev": true
},
"psl": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.7.0.tgz",
@ -5813,12 +6210,99 @@
"safe-buffer": "^5.1.2"
}
},
"pump": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
"integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
"dev": true,
"requires": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
},
"pumpify": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz",
"integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==",
"dev": true,
"requires": {
"duplexify": "^3.6.0",
"inherits": "^2.0.3",
"pump": "^2.0.0"
},
"dependencies": {
"pump": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz",
"integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==",
"dev": true,
"requires": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
}
}
},
"punycode": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
"dev": true
},
"purescript": {
"version": "0.13.6",
"resolved": "https://registry.npmjs.org/purescript/-/purescript-0.13.6.tgz",
"integrity": "sha512-PC93xqr0zDs5l5xnfTlptKzv5jBWbML+dwtpDCZkOOH7h9wgLusQfU4PNfHvdwrSmsBntalGm+Cbd6VrokN7Sg==",
"dev": true,
"requires": {
"purescript-installer": "^0.2.0"
}
},
"purescript-installer": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/purescript-installer/-/purescript-installer-0.2.5.tgz",
"integrity": "sha512-fQAWWP5a7scuchXecjpU4r4KEgSPuS6bBnaP01k9f71qqD28HaJ2m4PXHFkhkR4oATAxTPIGCtmTwtVoiBOHog==",
"dev": true,
"requires": {
"arch": "^2.1.1",
"byline": "^5.0.0",
"cacache": "^11.3.2",
"chalk": "^2.4.2",
"env-paths": "^2.2.0",
"execa": "^2.0.3",
"filesize": "^4.1.2",
"is-plain-obj": "^2.0.0",
"log-symbols": "^3.0.0",
"log-update": "^3.2.0",
"minimist": "^1.2.0",
"mkdirp": "^0.5.1",
"ms": "^2.1.2",
"once": "^1.4.0",
"pump": "^3.0.0",
"request": "^2.88.0",
"rimraf": "^2.6.3",
"tar": "^4.4.6",
"which": "^1.3.1",
"zen-observable": "^0.8.14"
},
"dependencies": {
"filesize": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/filesize/-/filesize-4.2.1.tgz",
"integrity": "sha512-bP82Hi8VRZX/TUBKfE24iiUGsB/sfm2WUrwTQyAzQrhO3V9IhcBBNBXMyzLY5orACxRyYJ3d2HeRVX+eFv4lmA==",
"dev": true
},
"log-symbols": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz",
"integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==",
"dev": true,
"requires": {
"chalk": "^2.4.2"
}
}
}
},
"purgecss": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/purgecss/-/purgecss-1.4.2.tgz",
@ -6177,6 +6661,15 @@
"inherits": "^2.0.1"
}
},
"run-queue": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz",
"integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=",
"dev": true,
"requires": {
"aproba": "^1.1.1"
}
},
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
@ -6541,6 +7034,16 @@
"integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=",
"dev": true
},
"spago": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/spago/-/spago-0.10.0.tgz",
"integrity": "sha512-hcyAVObO+ZgXZaqy0EMCZgw8+Yr+PAGpYfV77C1EuL5gcSaly4snCY6dFBIdcUq0/vYhkCuUpBUGi6Dx+rnxiw==",
"dev": true,
"requires": {
"follow-redirects": "^1.7.0",
"tar": "^4.4.8"
}
},
"split-string": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz",
@ -6573,6 +7076,15 @@
"tweetnacl": "~0.14.0"
}
},
"ssri": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz",
"integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==",
"dev": true,
"requires": {
"figgy-pudding": "^3.5.1"
}
},
"stable": {
"version": "0.1.8",
"resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz",
@ -6674,6 +7186,16 @@
"readable-stream": "^2.0.2"
}
},
"stream-each": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz",
"integrity": "sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==",
"dev": true,
"requires": {
"end-of-stream": "^1.1.0",
"stream-shift": "^1.0.0"
}
},
"stream-http": {
"version": "2.8.3",
"resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz",
@ -6687,6 +7209,12 @@
"xtend": "^4.0.0"
}
},
"stream-shift": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz",
"integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==",
"dev": true
},
"string-width": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
@ -6753,6 +7281,12 @@
"ansi-regex": "^3.0.0"
}
},
"strip-final-newline": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
"integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
"dev": true
},
"stylehacks": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-4.0.3.tgz",
@ -6813,6 +7347,21 @@
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
"dev": true
},
"tar": {
"version": "4.4.13",
"resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz",
"integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==",
"dev": true,
"requires": {
"chownr": "^1.1.1",
"fs-minipass": "^1.2.5",
"minipass": "^2.8.6",
"minizlib": "^1.2.1",
"mkdirp": "^0.5.0",
"safe-buffer": "^5.1.2",
"yallist": "^3.0.3"
}
},
"terser": {
"version": "4.6.3",
"resolved": "https://registry.npmjs.org/terser/-/terser-4.6.3.tgz",
@ -7074,6 +7623,24 @@
"integrity": "sha1-/+3ks2slKQaW5uFl1KWe25mOawI=",
"dev": true
},
"unique-filename": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz",
"integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==",
"dev": true,
"requires": {
"unique-slug": "^2.0.0"
}
},
"unique-slug": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz",
"integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==",
"dev": true,
"requires": {
"imurmurhash": "^0.1.4"
}
},
"unquote": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz",
@ -7390,6 +7957,12 @@
"integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==",
"dev": true
},
"yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
"dev": true
},
"yargs": {
"version": "14.2.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-14.2.2.tgz",
@ -7418,6 +7991,12 @@
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
}
},
"zen-observable": {
"version": "0.8.15",
"resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz",
"integrity": "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==",
"dev": true
}
}
}

View file

@ -9,7 +9,7 @@ import Affjax.RequestBody as AXReq
import Affjax.RequestHeader (RequestHeader(..))
import Affjax.ResponseFormat as AXRes
import Affjax.StatusCode (StatusCode(..))
import Data.Argonaut (Json)
import Data.Argonaut (Json, decodeJson)
import Data.Array ((:))
import Data.Either (Either(..), hush)
import Data.FormURLEncoded (FormURLEncoded)
@ -19,7 +19,7 @@ import Data.MediaType.Common (applicationFormURLEncoded, applicationJSON)
import Effect.Aff (Aff)
import Effect.Class (liftEffect)
import Globals (app')
import Model (Bookmark, Bookmark'(..), Note, Note'(..), AccountSettings, AccountSettings'(..))
import Model (Bookmark, Bookmark'(..), Note, Note'(..), AccountSettings, AccountSettings'(..), TagCloudMode, TagCloudMode'(..), TagCloud)
import Simple.JSON as J
import Web.HTML (window)
import Web.HTML.Location (reload)
@ -60,6 +60,16 @@ lookupTitle bm = do
then Just res.body
else Nothing
getTagCloud :: TagCloudMode -> Aff (Maybe TagCloud)
getTagCloud mode = do
eres <- fetchJson POST "api/tagcloud" (Just (TagCloudMode' mode)) AXRes.json
pure $ hush eres >>= \res ->
hush (decodeJson res.body)
updateTagCloudMode :: TagCloudMode -> Aff (Either Error (Response Unit))
updateTagCloudMode mode = do
fetchJson POST "api/tagcloudmode" (Just (TagCloudMode' mode)) AXRes.ignore
destroyNote :: Int -> Aff (Either Error (Response Unit))
destroyNote nid = do
fetchUrlEnc DELETE ("api/note/" <> show nid) Nothing AXRes.ignore

View file

@ -0,0 +1,154 @@
module Component.TagCloud where
import Prelude hiding (div)
import App (getTagCloud, updateTagCloudMode)
import Data.Array (sortBy, drop, foldMap, fromFoldable)
import Data.Foldable (for_, maximum, minimum)
import Data.Int (toNumber)
import Data.Lens (Lens', lens, use, (%=), (.=))
import Data.Maybe (Maybe(..), fromMaybe, maybe)
import Data.Monoid (guard)
import Data.Ord (comparing)
import Data.String (toLower, null, split) as S
import Data.String.Pattern (Pattern(..))
import Data.Symbol (SProxy(..))
import Data.Tuple (fst, snd, uncurry)
import Effect.Aff (Aff)
import Effect.Class (liftEffect)
import Foreign.Object (toUnfoldable, toArrayWithKey, empty, values) as F
import Globals (app')
import Halogen (AttrName(..))
import Halogen as H
import Halogen.HTML (HTML, a, attr, br_, button, div, form, input, label, p, span, text, textarea)
import Halogen.HTML as HH
import Halogen.HTML.Events (onChecked, onClick, onSubmit, onValueChange)
import Halogen.HTML.Properties (ButtonType(..), InputType(..), checked, for, href, id_, name, rows, title, type_, value)
import Math (log, pow, sqrt)
import Model (TagCloud, TagCloudMode, TagCloudModeF(..), tagCloudModeFromF, isExpanded, setExpanded, isSameMode, showMode)
import Util (_loc, class_, fromNullableStr, ifElseH, whenH)
import Web.Event.Event (Event, preventDefault)
data TAction
= TInitialize
| TExpanded Boolean
| TChangeMode TagCloudModeF
type TState =
{ mode :: TagCloudModeF
, tagcloud :: TagCloud
}
_mode :: Lens' TState TagCloudModeF
_mode = lens _.mode (_ { mode = _ })
tagcloudcomponent :: forall q i o. TagCloudModeF -> H.Component HTML q i o Aff
tagcloudcomponent m' =
H.mkComponent
{ initialState: const (mkState m')
, render
, eval: H.mkEval $ H.defaultEval { handleAction = handleAction
, initialize = Just TInitialize
}
}
where
app = app' unit
mkState m =
{ mode: m
, tagcloud: F.empty
}
render :: TState -> H.ComponentHTML TAction () Aff
render s@{ mode:TagCloudModeNone } =
div [class_ "tag_cloud" ] []
render s@{ mode, tagcloud } =
div [class_ "tag_cloud mv3" ]
[ div [class_ "tag_cloud_header mb2"]
[ button [ type_ ButtonButton, class_ ("pa1 f7 link hover-blue mr1" <> guard (mode == modetop) " b")
, title "show a cloud of your most-used tags"
, onClick \_ -> Just (TChangeMode modetop)
] [text "Top Tags"]
, button [ type_ ButtonButton, class_ ("pa1 f7 link hover-blue ml2 " <> guard (mode == modelb2) " b")
, title "show tags with at least 2 bookmarks"
, onClick \_ -> Just (TChangeMode modelb2)
] [text "2"]
, text "‧"
, button [ type_ ButtonButton, class_ ("pa1 f7 link hover-blue" <> guard (mode == modelb5) " b")
, title "show tags with at least 5 bookmarks"
, onClick \_ -> Just (TChangeMode modelb5)
] [text "5"]
, text "‧"
, button [ type_ ButtonButton, class_ ("pa1 f7 link hover-blue" <> guard (mode == modelb10) " b")
, title "show tags with at least 10 bookmarks"
, onClick \_ -> Just (TChangeMode modelb10)
] [text "10"]
, text "‧"
, button [ type_ ButtonButton, class_ ("pa1 f7 link hover-blue" <> guard (mode == modelb20) " b")
, title "show tags with at least 20 bookmarks"
, onClick \_ -> Just (TChangeMode modelb20)
] [text "20"]
, button [ type_ ButtonButton, class_ "pa1 ml2 f7 link silver hover-blue "
, onClick \_ -> Just (TExpanded (not (isExpanded mode)))]
[ text (if isExpanded mode then "hide" else "show") ]
]
, whenH (isExpanded mode) $ \_ -> do
let n = fromMaybe 1 (minimum (F.values tagcloud))
m = fromMaybe 1 (maximum (F.values tagcloud))
div [class_ "tag_cloud_body"] $ case mode of
TagCloudModeNone -> []
_ -> toArray n m tagcloud
]
where
modetop = TagCloudModeTop (isExpanded mode) 200
modelb2 = TagCloudModeLowerBound (isExpanded mode) 2
modelb5 = TagCloudModeLowerBound (isExpanded mode) 5
modelb10 = TagCloudModeLowerBound (isExpanded mode) 10
modelb20 = TagCloudModeLowerBound (isExpanded mode) 20
toArray :: Int -> Int -> _
toArray n m =
map (uncurry (toSizedTag n m))
<<< sortBy (comparing (S.toLower <<< fst))
<<< F.toUnfoldable
linkToFilterTag tag = fromNullableStr app.userR <> "/t:" <> tag
toSizedTag :: Int -> Int -> String -> Int -> _
toSizedTag n m k v =
a [ href (linkToFilterTag k)
, class_ "link tag mr1"
, attr (AttrName "style") ("font-size:" <> show fontsize <> "%" <>
";opacity:" <> show opacity)
] [text k]
where
fontsize = rescale identity (toNumber v) (toNumber n) (toNumber m) 100.0 150.0
opacity = rescale (log <<< (1.0 + _)) (toNumber v) (toNumber n) (toNumber m) 0.6 1.0
rescale :: (Number -> Number) -> Number -> Number -> Number -> Number -> Number -> Number
rescale f v n m l h = (f (v - n) / f (m - n)) * (h - l) + l
fetchTagCloud :: TagCloudModeF -> H.HalogenM TState TAction () o Aff Unit
fetchTagCloud mode' = do
case mode' of
TagCloudModeNone -> pure unit
_ -> do
tagcloud <- H.liftAff $ getTagCloud (tagCloudModeFromF mode')
H.modify_ (\s -> s {
mode = mode',
tagcloud = fromMaybe F.empty tagcloud
})
handleAction :: TAction -> H.HalogenM TState TAction () o Aff Unit
handleAction TInitialize = do
mode <- H.gets _.mode
fetchTagCloud mode
handleAction (TExpanded expanded) = do
H.modify_ (\s -> s { mode = setExpanded s.mode expanded })
mode <- H.gets _.mode
void $ H.liftAff $ updateTagCloudMode (tagCloudModeFromF mode)
handleAction (TChangeMode mode') = do
mode <- H.gets _.mode
if mode == mode'
then handleAction (TExpanded (not (isExpanded mode)))
else fetchTagCloud (setExpanded mode' true)

View file

@ -3,18 +3,19 @@ module Main where
import Prelude
import App (logout)
import Component.AccountSettings (usetting)
import Component.Add (addbmark)
import Component.BList (blist)
import Component.NList (nlist)
import Component.NNote (nnote)
import Component.AccountSettings (usetting)
import Component.TagCloud (tagcloudcomponent)
import Data.Foldable (traverse_)
import Effect (Effect)
import Effect.Aff (Aff, launchAff)
import Effect.Class (liftEffect)
import Halogen.Aff as HA
import Halogen.VDom.Driver (runUI)
import Model (Bookmark, Note, AccountSettings)
import Model (AccountSettings, Bookmark, Note, TagCloudMode, tagCloudModeToF)
import Web.DOM.Element (setAttribute)
import Web.DOM.ParentNode (QuerySelector(..))
import Web.Event.Event (Event, preventDefault)
@ -35,6 +36,12 @@ renderBookmarks renderElSelector bmarks = do
void $ runUI (blist bmarks) unit el
viewRendered
renderTagCloud :: String -> TagCloudMode -> Effect Unit
renderTagCloud renderElSelector tagCloudMode = do
HA.runHalogenAff do
HA.selectElement (QuerySelector renderElSelector) >>= traverse_ \el -> do
void $ runUI (tagcloudcomponent (tagCloudModeToF tagCloudMode)) unit el
renderAddForm :: String -> Bookmark -> Effect Unit
renderAddForm renderElSelector bmark = do
HA.runHalogenAff do

View file

@ -1,54 +1,126 @@
module Model where
import Data.Nullable (Nullable)
import Simple.JSON as J
type BookmarkId = Int
type TagId = Int
type Bookmark =
{ url :: String
, title :: String
, description :: String
, tags :: String
, private :: Boolean
, toread :: Boolean
, bid :: BookmarkId
, slug :: String
, selected :: Boolean
, time :: String
, archiveUrl :: Nullable String
}
newtype Bookmark' = Bookmark' Bookmark
derive newtype instance bookmark_rfI :: J.ReadForeign Bookmark'
derive newtype instance bookmark_wfI :: J.WriteForeign Bookmark'
type NoteId = Int
type NoteSlug = String
type Note =
{ id :: NoteId
, slug :: NoteSlug
, title :: String
, text :: String
, length :: Int
, isMarkdown :: Boolean
, shared :: Boolean
, created :: String
, updated :: String
}
newtype Note' = Note' Note
derive newtype instance note_rfI :: J.ReadForeign Note'
derive newtype instance note_wfI :: J.WriteForeign Note'
type AccountSettings =
{ archiveDefault :: Boolean
, privateDefault :: Boolean
, privacyLock :: Boolean
}
newtype AccountSettings' = AccountSettings' AccountSettings
derive newtype instance usersettings_rfI :: J.ReadForeign AccountSettings'
derive newtype instance usersettings_wfI :: J.WriteForeign AccountSettings'
module Model where
import Control.Monad
import Foreign
import Prelude
import Control.Monad.Except (runExcept)
import Data.Array (intercalate, singleton)
import Data.Either (Either, hush)
import Data.Maybe (Maybe, fromMaybe)
import Data.Nullable (Nullable)
import Foreign.Object (Object)
import Simple.JSON as J
type BookmarkId = Int
type TagId = Int
type Bookmark =
{ url :: String
, title :: String
, description :: String
, tags :: String
, private :: Boolean
, toread :: Boolean
, bid :: BookmarkId
, slug :: String
, selected :: Boolean
, time :: String
, archiveUrl :: Nullable String
}
newtype Bookmark' = Bookmark' Bookmark
derive newtype instance bookmark_rfI :: J.ReadForeign Bookmark'
derive newtype instance bookmark_wfI :: J.WriteForeign Bookmark'
type NoteId = Int
type NoteSlug = String
type Note =
{ id :: NoteId
, slug :: NoteSlug
, title :: String
, text :: String
, length :: Int
, isMarkdown :: Boolean
, shared :: Boolean
, created :: String
, updated :: String
}
newtype Note' = Note' Note
derive newtype instance note_rfI :: J.ReadForeign Note'
derive newtype instance note_wfI :: J.WriteForeign Note'
type AccountSettings =
{ archiveDefault :: Boolean
, privateDefault :: Boolean
, privacyLock :: Boolean
}
newtype AccountSettings' = AccountSettings' AccountSettings
derive newtype instance usersettings_rfI :: J.ReadForeign AccountSettings'
derive newtype instance usersettings_wfI :: J.WriteForeign AccountSettings'
type TagCloudMode =
{ mode :: String
, value :: Foreign
, expanded :: Boolean
}
newtype TagCloudMode' = TagCloudMode' TagCloudMode
derive newtype instance tagcloudmode_rfi :: J.ReadForeign TagCloudMode'
derive newtype instance tagcloudmode_wfI :: J.WriteForeign TagCloudMode'
type TagCloud = Object Int
data TagCloudModeF
= TagCloudModeTop Boolean Int
| TagCloudModeLowerBound Boolean Int
| TagCloudModeRelated Boolean (Array String)
| TagCloudModeNone
derive instance eqTagCloudModeF :: Eq TagCloudModeF
tagCloudModeToF :: TagCloudMode -> TagCloudModeF
tagCloudModeToF tagCloudMode =
fromMaybe TagCloudModeNone $ hush $ runExcept $
case tagCloudMode.mode of
"top" -> TagCloudModeTop tagCloudMode.expanded <$> readInt tagCloudMode.value
"lowerBound" -> TagCloudModeLowerBound tagCloudMode.expanded <$> readInt tagCloudMode.value
"related" -> (\s -> TagCloudModeRelated tagCloudMode.expanded (singleton s)) <$> readString tagCloudMode.value
_ -> pure TagCloudModeNone
tagCloudModeFromF :: TagCloudModeF -> TagCloudMode
tagCloudModeFromF (TagCloudModeTop e i) =
{ mode: "top" , value: unsafeToForeign i, expanded: e }
tagCloudModeFromF (TagCloudModeLowerBound e i) =
{ mode: "lowerBound" , value: unsafeToForeign i, expanded: e }
tagCloudModeFromF (TagCloudModeRelated e tags) =
{ mode: "related" , value: unsafeToForeign (intercalate " " tags), expanded: e }
tagCloudModeFromF TagCloudModeNone =
{ mode: "related" , value: unsafeToForeign "", expanded: false }
isExpanded :: TagCloudModeF -> Boolean
isExpanded (TagCloudModeTop e _) = e
isExpanded (TagCloudModeLowerBound e _) = e
isExpanded (TagCloudModeRelated e _) = e
isExpanded TagCloudModeNone = false
setExpanded :: TagCloudModeF -> Boolean -> TagCloudModeF
setExpanded (TagCloudModeTop e i) e' = TagCloudModeTop e' i
setExpanded (TagCloudModeLowerBound e i) e' = TagCloudModeLowerBound e' i
setExpanded (TagCloudModeRelated e i) e' = TagCloudModeRelated e' i
setExpanded TagCloudModeNone _ = TagCloudModeNone
isSameMode :: TagCloudModeF -> TagCloudModeF -> Boolean
isSameMode (TagCloudModeTop _ _) (TagCloudModeTop _ _) = true
isSameMode (TagCloudModeLowerBound _ _) (TagCloudModeLowerBound _ _) = true
isSameMode (TagCloudModeRelated _ _) (TagCloudModeRelated _ _) = true
isSameMode TagCloudModeNone TagCloudModeNone = true
isSameMode _ _ = false
showMode :: TagCloudModeF -> String
showMode (TagCloudModeTop _ _) = "top"
showMode (TagCloudModeLowerBound _ _) = "lowerBound"
showMode (TagCloudModeRelated _ _) = "related"
showMode TagCloudModeNone = ""

View file

@ -5,6 +5,7 @@ import Import
import Data.FileEmbed (embedFile)
import Text.Read
import Data.Aeson as A
-- These handlers embed files in the executable at compile time to avoid a
-- runtime dependency, and for efficiency.
@ -21,11 +22,43 @@ getRobotsR = return $ TypedContent typePlain
lookupPagingParams :: Handler (Maybe Int64, Maybe Int64)
lookupPagingParams = do
cq <- fmap parseMaybe (lookupGetParam "count")
cs <- fmap parseMaybe (lookupSession "count")
for_ cq (setSession "count" . (pack . show))
pq <- fmap parseMaybe (lookupGetParam "page")
pure (cq <|> cs, pq)
lookupPagingParams =
(,)
<$> getUrlSessionParam "count"
<*> getUrlParam "page"
getUrlParam :: (Read a) => Text -> Handler (Maybe a)
getUrlParam name = do
p <- fmap parseMaybe (lookupGetParam name)
pure p
where
parseMaybe x = readMaybe . unpack =<< x
getUrlSessionParam :: forall a.
(Show a, Read a)
=> Text
-> Handler (Maybe a)
getUrlSessionParam name = do
p <- fmap parseMaybe (lookupGetParam name)
s <- fmap parseMaybe (lookupSession name)
for_ p (setSession name . (pack . show))
pure (p <|> s)
where
parseMaybe :: Maybe Text -> Maybe a
parseMaybe x = readMaybe . unpack =<< x
lookupTagCloudMode :: MonadHandler m => m (Maybe TagCloudMode)
lookupTagCloudMode = do
(A.decode . fromStrict =<<) <$> lookupSessionBS "tagCloudMode"
setTagCloudMode :: MonadHandler m => TagCloudMode -> m ()
setTagCloudMode = setSessionBS "tagCloudMode" . toStrict . A.encode
getTagCloudMode :: MonadHandler m => Bool -> [Tag] -> m TagCloudMode
getTagCloudMode isowner tags = do
ms <- lookupTagCloudMode
pure $
if not isowner then TagCloudModeNone else
if not (null tags)
then fromMaybe (TagCloudModeTop False 200) ms --TagCloudModeRelated False tags
else fromMaybe (TagCloudModeTop False 200) ms

View file

@ -2,12 +2,13 @@
module Handler.User where
import qualified Data.Text as T
import Handler.Common (lookupPagingParams)
import Handler.Common
import Import
import Text.Blaze.Html (toHtml)
import qualified Text.Blaze.Html5 as H
import Yesod.RssFeed
import qualified Database.Esqueleto as E
import qualified Data.Map as Map
getUserR :: UserNameP -> Handler Html
getUserR uname@(UserNameP name) = do
@ -50,22 +51,54 @@ _getUser unamep@(UserNameP uname) sharedp' filterp' (TagsP pathtags) = do
pure (cnt, bm, tg)
when (bcount == 0) (case filterp of FilterSingle _ -> notFound; _ -> pure ())
mroute <- getCurrentRoute
tagCloudMode <- getTagCloudMode isowner pathtags
req <- getRequest
defaultLayout $ do
let pager = $(widgetFile "pager")
search = $(widgetFile "search")
renderEl = "bookmarks" :: Text
tagCloudRenderEl = "tagCloud" :: Text
rssLink (UserFeedR unamep) "feed"
$(widgetFile "user")
toWidgetBody [julius|
app.dat.bmarks = #{ toJSON $ toBookmarkFormList bmarks alltags } || [];
app.dat.isowner = #{ isowner };
app.userR = "@{UserR unamep}";
app.tagCloudMode = #{ toJSON $ tagCloudMode } || {};
|]
toWidget [julius|
PS['Main'].renderTagCloud('##{rawJS tagCloudRenderEl}')(app.tagCloudMode)();
PS['Main'].renderBookmarks('##{rawJS renderEl}')(app.dat.bmarks)();
|]
-- Form
postUserTagCloudR :: Handler ()
postUserTagCloudR = do
userId <- requireAuthId
mode <- requireCheckJsonBody
_updateTagCloudMode mode
tc <- runDB $ case mode of
TagCloudModeTop _ n -> tagCountTop userId n
TagCloudModeLowerBound _ n -> tagCountLowerBound userId n
TagCloudModeRelated _ _ -> notFound
TagCloudModeNone -> notFound
sendStatusJSON ok200 (Map.fromList tc :: Map.Map Text Int)
postUserTagCloudModeR :: Handler ()
postUserTagCloudModeR = do
userId <- requireAuthId
mode <- requireCheckJsonBody
_updateTagCloudMode mode
_updateTagCloudMode :: TagCloudMode -> Handler ()
_updateTagCloudMode mode =
case mode of
TagCloudModeTop _ _ -> setTagCloudMode mode
TagCloudModeLowerBound _ _ -> setTagCloudMode mode
TagCloudModeRelated _ _ -> notFound
TagCloudModeNone -> notFound
bookmarkToRssEntry :: (Entity Bookmark,[Text]) -> FeedEntry Text
bookmarkToRssEntry ((Entity entryId entry), tags) =
FeedEntry { feedEntryLink = (bookmarkHref entry)

View file

@ -405,8 +405,76 @@ allUserBookmarks user = do
let tags = sqlite_group_concat (t ^. BookmarkTagTag) (E.val " ")
pure (t ^. BookmarkTagBookmarkId, tags))
data TagCloudMode
= TagCloudModeTop Bool Int -- { mode: "top", value: 200 }
| TagCloudModeLowerBound Bool Int -- { mode: "lowerBound", value: 20 }
| TagCloudModeRelated Bool [Tag]
| TagCloudModeNone
deriving (Show, Eq, Read, Generic)
instance FromJSON TagCloudMode where
parseJSON (Object o) =
case lookup "mode" o of
Just (String "top") -> TagCloudModeTop <$> o .: "expanded" <*> o .: "value"
Just (String "lowerBound") -> TagCloudModeLowerBound <$> o .: "expanded" <*> o .: "value"
Just (String "related") -> TagCloudModeRelated <$> o .: "expanded" <*> (fmap words (o .: "value"))
Just (String "none") -> pure TagCloudModeNone
_ -> fail "bad parse"
parseJSON _ = fail "bad parse"
instance ToJSON TagCloudMode where
toJSON (TagCloudModeTop e i) =
object [ "mode" .= String "top"
, "value" .= toJSON i
, "expanded" .= Bool e
]
toJSON (TagCloudModeLowerBound e i) =
object [ "mode" .= String "lowerBound"
, "value" .= toJSON i
, "expanded" .= Bool e
]
toJSON (TagCloudModeRelated e tags) =
object [ "mode" .= String "related"
, "value" .= String (unwords tags)
, "expanded" .= Bool e
]
toJSON TagCloudModeNone =
object [ "mode" .= String "none"
, "value" .= Null
, "expanded" .= Bool False
]
type Tag = Text
tagCountTop :: Key User -> Int -> DB [(Text, Int)]
tagCountTop user top =
sortOn (toLower . fst) .
fmap (\(tname, tcount) -> (E.unValue tname, E.unValue tcount)) <$>
( select $
from $ \t -> do
where_ (t ^. BookmarkTagUserId E.==. val user)
E.groupBy (E.lower_ $ t ^. BookmarkTagTag)
let countRows' = E.countRows
E.orderBy [E.desc countRows']
E.limit ((fromIntegral . toInteger) top)
pure $ (t ^. BookmarkTagTag, countRows')
)
tagCountLowerBound :: Key User -> Int -> DB [(Text, Int)]
tagCountLowerBound user lowerBound =
fmap (\(tname, tcount) -> (E.unValue tname, E.unValue tcount)) <$>
( select $
from $ \t -> do
where_ (t ^. BookmarkTagUserId E.==. val user)
E.groupBy (E.lower_ $ t ^. BookmarkTagTag)
let countRows' = E.countRows
E.orderBy [E.asc (t ^. BookmarkTagTag)]
E.having (countRows' E.>=. E.val lowerBound)
pure $ (t ^. BookmarkTagTag, countRows')
)
-- Notes
fileNoteToNote :: UserId -> FileNote -> IO Note

View file

@ -101,6 +101,7 @@ label {
.tag {
color:#a51;
line-height:190%;
display: inline-block;
}
.private { background:#ddd;border:1px solid #d1d1d1; }

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

View file

@ -41,6 +41,8 @@ $maybe route <- mroute
<div .cf>
<div ##{tagCloudRenderEl}>
<div ##{renderEl} .mt3>
<div .cf>