From 4d823497a68512830258dd895783ea8e3b14ecab Mon Sep 17 00:00:00 2001 From: Daoud Clarke Date: Wed, 25 Oct 2023 19:17:02 +0100 Subject: [PATCH] Add original curation front-end --- front-end/assets/css/global.css | 94 ++++++++++ front-end/assets/opensearch.xml | 10 +- front-end/config.js | 1 + front-end/package-lock.json | 82 +++------ front-end/package.json | 3 - front-end/src/components/app.js | 14 +- front-end/src/components/login.js | 69 ++++++++ .../src/components/molecules/add-button.js | 24 +++ .../src/components/molecules/add-result.js | 69 ++++++++ .../src/components/molecules/delete-button.js | 35 ++++ front-end/src/components/molecules/result.js | 22 ++- .../components/molecules/validate-button.js | 53 ++++++ front-end/src/components/organisms/footer.js | 10 +- front-end/src/components/organisms/results.js | 166 +++++++++++++++++- front-end/src/components/organisms/save.js | 122 +++++++++++++ .../src/components/organisms/search-bar.js | 6 +- front-end/src/components/register.js | 84 +++++++++ front-end/src/index.html | 13 +- front-end/src/index.js | 3 + front-end/src/stats/index.html | 12 +- front-end/vite.config.js | 9 +- 21 files changed, 795 insertions(+), 106 deletions(-) create mode 100644 front-end/src/components/login.js create mode 100644 front-end/src/components/molecules/add-button.js create mode 100644 front-end/src/components/molecules/add-result.js create mode 100644 front-end/src/components/molecules/delete-button.js create mode 100644 front-end/src/components/molecules/validate-button.js create mode 100644 front-end/src/components/organisms/save.js create mode 100644 front-end/src/components/register.js diff --git a/front-end/assets/css/global.css b/front-end/assets/css/global.css index d21eff3..b6f04dd 100644 --- a/front-end/assets/css/global.css +++ b/front-end/assets/css/global.css @@ -117,6 +117,10 @@ mwmbl-results, footer { padding: 10px; } +.result { + min-height: 120px; +} + .result a { display: block; text-decoration: none; @@ -229,4 +233,94 @@ a { font-weight: var(--bold-font-weight); color: var(--primary-color); text-decoration: underline; +} + +.result-container { + display: flex; +} + +.curation-buttons { + padding: 20px; +} + +.curation-button { + opacity: 0; + color: inherit; + border: none; + padding: 0; + font: inherit; + outline: inherit; + cursor: pointer; + + background: darkgrey; + box-shadow: 3px 3px 3px lightgrey; + width: 20px; + height: 20px; + border-radius: 50%; + display: flex; /* or inline-flex */ + align-items: center; + justify-content: center; + margin: 10px 0 10px 0; +} + +.result:hover .curation-button { + opacity: 70%; + transition: + opacity 200ms ease-in-out; +} + +.result:hover .curation-button:hover { + opacity: 100%; +} + +.curate-delete { + margin-top: 0; +} + +.validated { + background: lightgreen; + opacity: 100%; +} + +.curate-add { + margin-bottom: 0; +} + + +.modal { + /*display: none; !* Hidden by default *!*/ + position: fixed; /* Stay in place */ + z-index: 100; /* Sit on top */ + left: 0; + top: 0; + width: 100%; /* Full width */ + height: 100%; /* Full height */ + overflow: auto; /* Enable scroll if needed */ + background-color: rgb(0,0,0); /* Fallback color */ + background-color: rgba(0,0,0,0.4); /* Black w/ opacity */ +} + +/* Modal Content/Box */ +.modal-content { + background-color: #fefefe; + margin: 15% auto; /* 15% from the top and centered */ + padding: 20px; + border: 1px solid #888; + max-width: 800px; + width: 80%; /* Could be more or less, depending on screen size */ +} + +/* The Close Button */ +.close { + color: #aaa; + float: right; + font-size: 28px; + font-weight: bold; +} + +.close:hover, +.close:focus { + color: black; + text-decoration: none; + cursor: pointer; } \ No newline at end of file diff --git a/front-end/assets/opensearch.xml b/front-end/assets/opensearch.xml index bf0fe92..a9bdbcf 100644 --- a/front-end/assets/opensearch.xml +++ b/front-end/assets/opensearch.xml @@ -1,10 +1,10 @@ - MWMBL - Search MWMBL - - - MWMBL Search + MWMBL Local + Search MWMBL Local + + + MWMBL Search Local data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+Cjxzdmcgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgdmlld0JveD0iMCAwIDkzNzUgOTM3NSIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWw6c3BhY2U9InByZXNlcnZlIiB4bWxuczpzZXJpZj0iaHR0cDovL3d3dy5zZXJpZi5jb20vIiBzdHlsZT0iZmlsbC1ydWxlOmV2ZW5vZGQ7Y2xpcC1ydWxlOmV2ZW5vZGQ7c3Ryb2tlLWxpbmVjYXA6cm91bmQ7c3Ryb2tlLWxpbmVqb2luOnJvdW5kO3N0cm9rZS1taXRlcmxpbWl0OjEuNTsiPgo8c3R5bGU+CgkJcGF0aCB7CgkJCWZpbGw6ICMwMDA7CgkJfQoJCUBtZWRpYSAoIHByZWZlcnMtY29sb3Itc2NoZW1lOiBkYXJrICkgewoJCQlwYXRoIHsKCQkJCWZpbGw6ICNmZmYgIWltcG9ydGFudDsKCQkJfQoJCX0KCTwvc3R5bGU+CjxwYXRoIGQ9Ik02MTI4LjcyLDgyNTEuNTZjNDk1LjY1LDAgOTE5LjY5NywtMTc2LjIyMiAxMjcyLjEzLC01MjguNjU5YzM1Mi40MzcsLTM1Mi40MzggNTI4LjY1OSwtNzc2LjQ4NCA1MjguNjU5LC0xMjcyLjEzbC0wLC0zMzU4Ljc1Yy0wLC05NC42NDQgLTM1LjQ5MiwtMTc2Ljg0MSAtMTA2LjQ4MiwtMjQ2LjU4MWMtNzAuOTg1LC02OS43MzkgLTE1My44MDEsLTEwNC42MTIgLTI0OC40NDUsLTEwNC42MTJjLTk5LjYzNCwtMCAtMTg0LjMxNCwzNC44NzMgLTI1NC4wNTQsMTA0LjYxMmMtNjkuNzQ2LDY5Ljc0IC0xMDQuNjEyLDE1MS45MzcgLTEwNC42MTIsMjQ2LjU4MWwtMCwzMzU4Ljc1Yy0wLDMwMS4zNzMgLTEwNS44NTcsNTU3LjkyMyAtMzE3LjU3MSw3NjkuNjNjLTIxMS43MDgsMjExLjcxNCAtNDY4LjI1MSwzMTcuNTcxIC03NjkuNjMsMzE3LjU3MWMtMjk4Ljg5LDAgLTU1NC44MDgsLTEwNS44NTcgLTc2Ny43NjYsLTMxNy41NzFjLTIxMi45NTgsLTIxMS43MDcgLTMxOS40MzQsLTQ2OC4yNTcgLTMxOS40MzQsLTc2OS42M2wtMCwtMzM1OC43NWMtMCwtOTQuNjQ0IC0zNC44NzMsLTE3Ni44NDEgLTEwNC42MTMsLTI0Ni41ODFjLTY5LjczOSwtNjkuNzM5IC0xNTQuNDI2LC0xMDQuNjEyIC0yNTQuMDU0LC0xMDQuNjEyYy05NC42NDksLTAgLTE3Ni44NDEsMzQuODczIC0yNDYuNTgsMTA0LjYxMmMtNjkuNzQsNjkuNzQgLTEwNC42MTMsMTUxLjkzNyAtMTA0LjYxMywyNDYuNTgxbDAsMzM1OC43NWMwLDMwMS4zNzMgLTEwNi40NzYsNTU3LjkyMyAtMzE5LjQzNCw3NjkuNjNjLTIxMi45NTksMjExLjcxNCAtNDY4Ljg4MywzMTcuNTcxIC03NjcuNzY2LDMxNy41NzFjLTMwMS4zNzksMCAtNTU3LjkyMywtMTA1Ljg1NyAtNzY5LjYzNiwtMzE3LjU3MWMtMjExLjcwOCwtMjExLjcwNyAtMzE3LjU2NSwtNDY4LjI1NyAtMzE3LjU2NSwtNzY5LjYzbDAsLTMzNTguNzVjMCwtOTQuNjQ0IC0zNC44NzMsLTE3Ni44NDEgLTEwNC42MTIsLTI0Ni41ODFjLTY5Ljc0LC02OS43MzkgLTE1NC40MjcsLTEwNC42MTIgLTI1NC4wNTQsLTEwNC42MTJjLTk0LjY1LC0wIC0xNzYuODQxLDM0Ljg3MyAtMjQ2LjU4MSwxMDQuNjEyYy02OS43MzksNjkuNzQgLTEwNC42MTIsMTUxLjkzNyAtMTA0LjYxMiwyNDYuNTgxbC0wLDMzNTguNzVjLTAsMzI2LjI4MyA4MC4zMjcsNjI3LjY2MiAyNDAuOTc2LDkwNC4xMzFjMTYwLjY1NiwyNzYuNDY5IDM3OC41OTMsNDk1LjAzMSA2NTMuODE3LDY1NS42ODZjMjc1LjIyNCwxNjAuNjQ5IDU3NS45ODQsMjQwLjk3NyA5MDIuMjY3LDI0MC45NzdjMjkxLjQxNiwwIDU2My41MjUsLTY0Ljc2MSA4MTYuMzM1LC0xOTQuMjc3YzI1Mi44MSwtMTI5LjUxNyA0NjAuMTU4LC0zMDcuNjA4IDYyMi4wNTgsLTUzNC4yNjNjMTY2Ljg3OCwyMjYuNjU1IDM3Ni43MjIsNDA0Ljc0NiA2MjkuNTMyLDUzNC4yNjNjMjUyLjgwOSwxMjkuNTE2IDUyNC45MTksMTk0LjI3NyA4MTYuMzM1LDE5NC4yNzdabS0wLjk2LC0xNjE3LjM5bC0wLjU4MiwtMGMtOTkuNjI3LC0wIC0xODQuMzE0LC0zNC44NzMgLTI1NC4wNTQsLTEwNC42MTJjLTY5LjczOSwtNjkuNzQgLTEwNC42MTIsLTE1MS45MzggLTEwNC42MTIsLTI0Ni41ODFsLTAsLTMzNTguNzRjLTAsLTMwMS4zNzMgLTEwNS44NTcsLTU1Ny45MjMgLTMxNy41NjUsLTc2OS42M2MtMjEwLjY5OCwtMjEwLjY5OSAtNDY1Ljc5OSwtMzE2LjU0OSAtNzY1LjMyLC0zMTcuNTU5Yy0yOTkuNTIxLDEuMDEgLTU1NC42MjIsMTA2Ljg2IC03NjUuMzE0LDMxNy41NTljLTIxMS43MTQsMjExLjcwNyAtMzE3LjU3MSw0NjguMjU3IC0zMTcuNTcxLDc2OS42M2wwLDMzNTguNzVjMCw5NC42NDQgLTM0Ljg2NiwxNzYuODQxIC0xMDQuNjA2LDI0Ni41ODFjLTY5LjczOSw2OS43MzkgLTE1NC40MjYsMTA0LjYxMiAtMjU0LjA1NCwxMDQuNjEybC04LjYzOCwwYy05NC42NDMsMCAtMTc2Ljg0MSwtMzQuODczIC0yNDYuNTgsLTEwNC42MTJjLTY5Ljc0LC02OS43NCAtMTA0LjYxMywtMTUxLjkzNyAtMTA0LjYxMywtMjQ2LjU4MWwwLC0zMzU4Ljc1YzAsLTMwMS4zNzMgLTEwNi40NzYsLTU1Ny45MjMgLTMxOS40MzQsLTc2OS42M2MtMjEyLjk1OSwtMjExLjcxNCAtNDY4Ljg3NiwtMzE3LjU3MSAtNzY3Ljc2NiwtMzE3LjU3MWMtMzAxLjM3OSwtMCAtNTU3LjkyMiwxMDUuODU3IC03NjkuNjMsMzE3LjU3MWMtMjExLjcxNCwyMTEuNzA3IC0zMTcuNTcxLDQ2OC4yNTcgLTMxNy41NzEsNzY5LjYzbDAsMzM1OC43NWMwLDk0LjY0NCAtMzQuODY3LDE3Ni44NDEgLTEwNC42MTIsMjQ2LjU4MWMtNjkuNzQsNjkuNzM5IC0xNTQuNDIsMTA0LjYxMiAtMjU0LjA1NCwxMDQuNjEyYy05NC42NDQsMCAtMTc2Ljg0MSwtMzQuODczIC0yNDYuNTgxLC0xMDQuNjEyYy02OS43MzksLTY5Ljc0IC0xMDQuNjA2LC0xNTEuOTM3IC0xMDQuNjA2LC0yNDYuNTgxbDAsLTMzNTguNzVjMCwtMzI2LjI4MyA4MC4zMjEsLTYyNy42NjIgMjQwLjk3NywtOTA0LjEzMWMxNjAuNjQ5LC0yNzYuNDY5IDM3OC41ODYsLTQ5NS4wMzEgNjUzLjgxNiwtNjU1LjY4NmMyNzUuMjI0LC0xNjAuNjQ5IDU3NS45NzgsLTI0MC45NzcgOTAyLjI2MSwtMjQwLjk3N2MyOTEuNDE2LC0wIDU2My41MjYsNjQuNzYxIDgxNi4zMzUsMTk0LjI3N2MyNTIuODEsMTI5LjUxNyA0NjAuMTY0LDMwNy42MDggNjIyLjA1OCw1MzQuMjYzYzE2Ni44NzgsLTIyNi42NTUgMzc2LjcyMiwtNDA0Ljc0NiA2MjkuNTMyLC01MzQuMjYzYzI1Mi44MDksLTEyOS41MTYgNTI0LjkxOSwtMTk0LjI3NyA4MTYuMzM1LC0xOTQuMjc3bDguNjM4LC0wYzE2NC44MjIsLTAgMzIzLjQ3MiwyMC43MTggNDc1Ljk0MSw2Mi4xNTRsNS4yMzksMS40MzFjNDEuMTE0LDExLjI2MyA4MS42MDksMjQuMDI0IDEyMS40OTcsMzguMjg0YzcyLjY4NywyNS44NyAxNDMuOTA3LDU2LjY3NSAyMTMuNjUyLDkyLjQwOGMyNTAuNjM2LDEyOC40MDggNDU2LjU5MiwzMDQuNTQ5IDYxNy44NjYsNTI4LjQxMmw0LjMyOCw1LjY2NWMxNjYuODcyLC0yMjYuNTggMzc2LjY2NywtNDA0LjU5OCA2MjkuMzk2LC01MzQuMDc3YzI1Mi44MDksLTEyOS41MTYgNTI0LjkyNSwtMTk0LjI3NyA4MTYuMzM1LC0xOTQuMjc3YzQ5NS42NTcsLTAgOTE5LjcwNCwxNzYuMjIyIDEyNzIuMTQsNTI4LjY1OWMzNTIuNDM3LDM1Mi40MzggNTI4LjY1Myw3NzYuNDg0IDUyOC42NTMsMTI3Mi4xM2wwLDMzNTguNzVjMCw5NC42NDQgLTM1LjQ5MiwxNzYuODQxIC0xMDYuNDc2LDI0Ni41ODFjLTcwLjk4NCw2OS43MzkgLTE1My44MDEsMTA0LjYxMiAtMjQ4LjQ1MSwxMDQuNjEyYy05OS42MjcsMCAtMTg0LjMxNCwtMzQuODczIC0yNTQuMDU0LC0xMDQuNjEyYy02OS43MzksLTY5Ljc0IC0xMDQuNjEyLC0xNTEuOTM3IC0xMDQuNjEyLC0yNDYuNTgxbC0wLC0zMzU4Ljc1Yy0wLC0zMDEuMzczIC0xMDUuODUxLC01NTcuOTIzIC0zMTcuNTY1LC03NjkuNjNjLTIxMS43MTMsLTIxMS43MTQgLTQ2OC4yNTcsLTMxNy41NzEgLTc2OS42MzYsLTMxNy41NzFjLTI5OC44ODMsLTAgLTU1NC44MDcsMTA1Ljg1NyAtNzY3Ljc2NiwzMTcuNTcxYy0yMTIuOTUyLDIxMS43MDcgLTMxOS40MzQsNDY4LjI1NyAtMzE5LjQzNCw3NjkuNjNsLTAsMzM1OC43NWMtMCw5NC42NDQgLTM0Ljg2NywxNzYuODQxIC0xMDQuNjA2LDI0Ni41ODFjLTY5Ljc0Niw2OS43MzkgLTE1NC40MjcsMTA0LjYxMiAtMjU0LjA1NSwxMDQuNjEybC0wLjU4MiwtMC4wMDZaIiBzdHlsZT0ic3Ryb2tlOiMxODVBREI7c3Ryb2tlLXdpZHRoOjQuMTdweDsiLz48L3N2Zz4K open diff --git a/front-end/config.js b/front-end/config.js index c42b4a8..1c7c23c 100644 --- a/front-end/config.js +++ b/front-end/config.js @@ -9,6 +9,7 @@ export default { componentPrefix: 'mwmbl', publicApiURL: 'https://api.mwmbl.org/', + // publicApiURL: 'http://localhost:5000/', searchQueryParam: 'q', footerLinks: [ { diff --git a/front-end/package-lock.json b/front-end/package-lock.json index 285df52..277dfb4 100644 --- a/front-end/package-lock.json +++ b/front-end/package-lock.json @@ -5,9 +5,6 @@ "packages": { "": { "name": "front-end", - "dependencies": { - "chart.js": "^4.4.0" - }, "devDependencies": { "@vitejs/plugin-legacy": "^2.3.1", "terser": "^5.16.1", @@ -113,11 +110,6 @@ "@jridgewell/sourcemap-codec": "1.4.14" } }, - "node_modules/@kurkle/color": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz", - "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==" - }, "node_modules/@vitejs/plugin-legacy": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/@vitejs/plugin-legacy/-/plugin-legacy-2.3.1.tgz", @@ -156,17 +148,6 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, - "node_modules/chart.js": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.0.tgz", - "integrity": "sha512-vQEj6d+z0dcsKLlQvbKIMYFHd3t8W/7L2vfJIbYcfyPcRx92CsHqECpueN8qVGNlKyDcr5wBrYAYKnfu/9Q1hQ==", - "dependencies": { - "@kurkle/color": "^0.3.0" - }, - "engines": { - "pnpm": ">=7" - } - }, "node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", @@ -598,16 +579,10 @@ } }, "node_modules/nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", + "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -628,9 +603,9 @@ "dev": true }, "node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "version": "8.4.19", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.19.tgz", + "integrity": "sha512-h+pbPsyhlYj6N2ozBmHhHrs9DzGmbaarbLvWipMRO7RLS+v4onj26MPFXA5OBYFxyqYhUJK456SwDcY9H2/zsA==", "dev": true, "funding": [ { @@ -640,14 +615,10 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" } ], "dependencies": { - "nanoid": "^3.3.6", + "nanoid": "^3.3.4", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" }, @@ -765,9 +736,9 @@ } }, "node_modules/vite": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.7.tgz", - "integrity": "sha512-29pdXjk49xAP0QBr0xXqu2s5jiQIXNvE/xwd0vUizYT2Hzqe4BksNNoWllFVXJf4eLZ+UlVQmXfB4lWrc+t18g==", + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.5.tgz", + "integrity": "sha512-4mVEpXpSOgrssFZAOmGIr85wPHKvaDAcXqxVxVRZhljkJOMZi1ibLibzjLHzJvcok8BMguLc7g1W6W/GqZbLdQ==", "dev": true, "dependencies": { "esbuild": "^0.15.9", @@ -884,11 +855,6 @@ "@jridgewell/sourcemap-codec": "1.4.14" } }, - "@kurkle/color": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz", - "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==" - }, "@vitejs/plugin-legacy": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/@vitejs/plugin-legacy/-/plugin-legacy-2.3.1.tgz", @@ -914,14 +880,6 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, - "chart.js": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.0.tgz", - "integrity": "sha512-vQEj6d+z0dcsKLlQvbKIMYFHd3t8W/7L2vfJIbYcfyPcRx92CsHqECpueN8qVGNlKyDcr5wBrYAYKnfu/9Q1hQ==", - "requires": { - "@kurkle/color": "^0.3.0" - } - }, "commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", @@ -1145,9 +1103,9 @@ } }, "nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", + "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", "dev": true }, "path-parse": { @@ -1163,12 +1121,12 @@ "dev": true }, "postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "version": "8.4.19", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.19.tgz", + "integrity": "sha512-h+pbPsyhlYj6N2ozBmHhHrs9DzGmbaarbLvWipMRO7RLS+v4onj26MPFXA5OBYFxyqYhUJK456SwDcY9H2/zsA==", "dev": true, "requires": { - "nanoid": "^3.3.6", + "nanoid": "^3.3.4", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } @@ -1252,9 +1210,9 @@ } }, "vite": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.7.tgz", - "integrity": "sha512-29pdXjk49xAP0QBr0xXqu2s5jiQIXNvE/xwd0vUizYT2Hzqe4BksNNoWllFVXJf4eLZ+UlVQmXfB4lWrc+t18g==", + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.5.tgz", + "integrity": "sha512-4mVEpXpSOgrssFZAOmGIr85wPHKvaDAcXqxVxVRZhljkJOMZi1ibLibzjLHzJvcok8BMguLc7g1W6W/GqZbLdQ==", "dev": true, "requires": { "esbuild": "^0.15.9", diff --git a/front-end/package.json b/front-end/package.json index 53a57e0..76e8d53 100644 --- a/front-end/package.json +++ b/front-end/package.json @@ -11,8 +11,5 @@ "@vitejs/plugin-legacy": "^2.3.1", "terser": "^5.16.1", "vite": "^3.2.3" - }, - "dependencies": { - "chart.js": "^4.4.0" } } diff --git a/front-end/src/components/app.js b/front-end/src/components/app.js index 9644a6b..d53cb3b 100644 --- a/front-end/src/components/app.js +++ b/front-end/src/components/app.js @@ -1,16 +1,22 @@ import define from '../utils/define.js'; +import addResult from "./molecules/add-result.js"; +import save from "./organisms/save.js"; const template = () => /*html*/`
+
    +
  • +
- mwmbl logo + mwmbl logo MWMBL
-
- -
+
+ +
+
`; diff --git a/front-end/src/components/login.js b/front-end/src/components/login.js new file mode 100644 index 0000000..9546695 --- /dev/null +++ b/front-end/src/components/login.js @@ -0,0 +1,69 @@ +import define from '../utils/define.js'; +import config from "../../config.js"; + +const template = () => /*html*/` +
+
Login
+
+ +
+ +
+
+
+ +
+ + +
+
+
+ +
+
+`; + +export default define('login', class extends HTMLElement { + constructor() { + super(); + this.loginForm = null; + this.emailOrUsernameInput = null; + this.passwordInput = null; + this.__setup(); + this.__events(); + } + + __setup() { + this.innerHTML = template(); + this.loginForm = this.querySelector('form'); + this.emailOrUsernameInput = this.querySelector('#login-email-or-username'); + this.passwordInput = this.querySelector('#login-password'); + } + + __events() { + this.loginForm.addEventListener('submit', (e) => { + e.preventDefault(); + this.__handleLogin(e); + }); + } + + __handleLogin = async () => { + const response = await fetch(`${config.publicApiURL}user/login`, { + method: 'POST', + cache: 'no-cache', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ + "username_or_email": this.emailOrUsernameInput.value, + "password": this.passwordInput.value, + }) + }); + if (response.status === 200) { + const loginData = await response.json(); + console.log("Login data", loginData); + document.cookie = `jwt=${loginData["jwt"]}; SameSite=Strict`; + console.log("Login success"); + } else { + console.log("Login error", response); + } + } +}); \ No newline at end of file diff --git a/front-end/src/components/molecules/add-button.js b/front-end/src/components/molecules/add-button.js new file mode 100644 index 0000000..d59884a --- /dev/null +++ b/front-end/src/components/molecules/add-button.js @@ -0,0 +1,24 @@ +import define from "../../utils/define.js"; +import {globalBus} from "../../utils/events.js"; +import addResult from "./add-result.js"; +import emptyResult from "./empty-result.js"; + + +export default define('add-button', class extends HTMLButtonElement { + constructor() { + super(); + this.__setup(); + } + + __setup() { + this.__events(); + } + + __events() { + this.addEventListener('click', (e) => { + console.log("Add button"); + document.querySelector('.modal').style.display = 'block'; + document.querySelector('.modal input').focus(); + }) + } +}, { extends: 'button' }); diff --git a/front-end/src/components/molecules/add-result.js b/front-end/src/components/molecules/add-result.js new file mode 100644 index 0000000..6e9ce17 --- /dev/null +++ b/front-end/src/components/molecules/add-result.js @@ -0,0 +1,69 @@ +import define from '../../utils/define.js'; +import config from "../../../config.js"; +import {globalBus} from "../../utils/events.js"; + + +const FETCH_URL = `${config['publicApiURL']}crawler/fetch?` + + +const template = () => /*html*/` + +`; + +export default define('add-result', class extends HTMLDivElement { + constructor() { + super(); + this.classList.add('modal'); + this.__setup(); + } + + __setup() { + this.innerHTML = template(); + this.__events(); + this.style.display = 'none'; + } + + __events() { + this.querySelector('.close').addEventListener('click', e => { + if (e.target === this) { + this.style.display = 'none'; + } + }); + + this.addEventListener('click', e => { + this.style.display = 'none'; + }); + + this.querySelector('form').addEventListener('click', e => { + // Clicking on the form shouldn't close it + e.stopPropagation(); + }); + + this.addEventListener('submit', this.__urlSubmitted.bind(this)); + } + + async __urlSubmitted(e) { + e.preventDefault(); + const value = this.querySelector('input').value; + console.log("Input value", value); + + const query = document.querySelector('.search-bar input').value; + + const url = `${FETCH_URL}url=${encodeURIComponent(value)}&query=${encodeURIComponent(query)}`; + const response = await fetch(url); + if (response.status === 200) { + const data = await response.json(); + console.log("Data", data); + + const addResultEvent = new CustomEvent('curate-add-result', {detail: data}); + globalBus.dispatch(addResultEvent); + } else { + console.log("Bad response", response); + // TODO + } + } +}, { extends: 'div' }); diff --git a/front-end/src/components/molecules/delete-button.js b/front-end/src/components/molecules/delete-button.js new file mode 100644 index 0000000..1914684 --- /dev/null +++ b/front-end/src/components/molecules/delete-button.js @@ -0,0 +1,35 @@ +import define from "../../utils/define.js"; +import {globalBus} from "../../utils/events.js"; + + +export default define('delete-button', class extends HTMLButtonElement { + constructor() { + super(); + this.__setup(); + } + + __setup() { + this.__events(); + } + + __events() { + this.addEventListener('click', (e) => { + console.log("Delete button"); + + const result = this.closest('.result'); + const parent = result.parentNode; + + const index = Array.prototype.indexOf.call(parent.children, result); + console.log("Delete index", index); + + const beginCuratingEvent = new CustomEvent('curate-delete-result', { + detail: { + data: { + delete_index: index + } + } + }); + globalBus.dispatch(beginCuratingEvent); + }) + } +}, { extends: 'button' }); diff --git a/front-end/src/components/molecules/result.js b/front-end/src/components/molecules/result.js index 696a7a1..2a2859e 100644 --- a/front-end/src/components/molecules/result.js +++ b/front-end/src/components/molecules/result.js @@ -1,13 +1,25 @@ import define from '../../utils/define.js'; import escapeString from '../../utils/escapeString.js'; import { globalBus } from '../../utils/events.js'; +import deleteButton from "./delete-button.js"; +import validateButton from "./validate-button.js"; +import addButton from "./add-button.js"; const template = ({ data }) => /*html*/` - - -

${data.title}

-

${data.extract}

-
+ `; export default define('result', class extends HTMLLIElement { diff --git a/front-end/src/components/molecules/validate-button.js b/front-end/src/components/molecules/validate-button.js new file mode 100644 index 0000000..65e3b10 --- /dev/null +++ b/front-end/src/components/molecules/validate-button.js @@ -0,0 +1,53 @@ +import define from "../../utils/define.js"; +import {globalBus} from "../../utils/events.js"; + + +const VALIDATED_CLASS = "validated"; + +export default define('validate-button', class extends HTMLButtonElement { + constructor() { + super(); + this.__setup(); + } + + __setup() { + this.__events(); + } + + __events() { + this.addEventListener('click', (e) => { + console.log("Validate button"); + + const result = this.closest('.result'); + const parent = result.parentNode; + + const index = Array.prototype.indexOf.call(parent.children, result); + console.log("Validate index", index); + + const curationValidateEvent = new CustomEvent('curate-validate-result', { + detail: { + data: { + validate_index: index + } + } + }); + globalBus.dispatch(curationValidateEvent); + }) + } + + isValidated() { + return this.classList.contains(VALIDATED_CLASS); + } + + validate() { + this.classList.add(VALIDATED_CLASS); + } + + unvalidate() { + this.classList.remove(VALIDATED_CLASS); + } + + toggleValidate() { + this.classList.toggle(VALIDATED_CLASS); + } +}, { extends: 'button' }); diff --git a/front-end/src/components/organisms/footer.js b/front-end/src/components/organisms/footer.js index 73344cb..4b8d62f 100644 --- a/front-end/src/components/organisms/footer.js +++ b/front-end/src/components/organisms/footer.js @@ -4,14 +4,14 @@ import config from '../../../config.js'; const template = ({ data }) => /*html*/` `; @@ -22,7 +22,7 @@ export default define('footer', class extends HTMLElement { } __setup() { - this.innerHTML = template({ + this.innerHTML = template({ data: { links: config.footerLinks } @@ -31,6 +31,6 @@ export default define('footer', class extends HTMLElement { } __events() { - + } }, { extends: 'footer' }); \ No newline at end of file diff --git a/front-end/src/components/organisms/results.js b/front-end/src/components/organisms/results.js index 40f558c..37fefe5 100644 --- a/front-end/src/components/organisms/results.js +++ b/front-end/src/components/organisms/results.js @@ -1,5 +1,5 @@ import define from '../../utils/define.js'; -import { globalBus } from '../../utils/events.js'; +import {globalBus} from '../../utils/events.js'; // Components import result from '../molecules/result.js'; @@ -17,6 +17,8 @@ export default define('results', class extends HTMLElement { constructor() { super(); this.results = null; + this.oldIndex = null; + this.curating = false; this.__setup(); } @@ -31,7 +33,7 @@ export default define('results', class extends HTMLElement { this.results.innerHTML = ''; let resultsHTML = ''; if (!e.detail.error) { - // If there is no details the input is empty + // If there is no details the input is empty if (!e.detail.results) { resultsHTML = /*html*/`
  • @@ -42,7 +44,7 @@ export default define('results', class extends HTMLElement { for(const resultData of e.detail.results) { resultsHTML += /*html*/`
  • { this.results.firstElementChild.firstElementChild.focus(); - }) + }); + + globalBus.on('curate-delete-result', (e) => { + console.log("Curate delete result event", e); + this.__beginCurating.bind(this)(); + + const children = this.results.getElementsByClassName('result'); + let deleteIndex = e.detail.data.delete_index; + const child = children[deleteIndex]; + this.results.removeChild(child); + const newResults = this.__getResults(); + + const curationSaveEvent = new CustomEvent('save-curation', { + detail: { + type: 'delete', + data: { + url: document.location.href, + results: newResults, + curation: { + delete_index: deleteIndex + } + } + } + }); + globalBus.dispatch(curationSaveEvent); + }); + + globalBus.on('curate-validate-result', (e) => { + console.log("Curate validate result event", e); + this.__beginCurating.bind(this)(); + + const children = this.results.getElementsByClassName('result'); + const validateChild = children[e.detail.data.validate_index]; + validateChild.querySelector('.curate-approve').toggleValidate(); + + const newResults = this.__getResults(); + + const curationStartEvent = new CustomEvent('save-curation', { + detail: { + type: 'validate', + data: { + url: document.location.href, + results: newResults, + curation: e.detail.data + } + } + }); + globalBus.dispatch(curationStartEvent); + }); + + globalBus.on('begin-curating-results', (e) => { + // We might not be online, or logged in, so save the curation in local storage in case: + console.log("Begin curation event", e); + this.__beginCurating.bind(this)(); + }); + + globalBus.on('curate-add-result', (e) => { + console.log("Add result", e); + this.__beginCurating(); + const resultData = e.detail; + const resultHTML = /*html*/` +
  • + `; + this.results.insertAdjacentHTML('afterbegin', resultHTML); + + const newResults = this.__getResults(); + + const curationSaveEvent = new CustomEvent('save-curation', { + detail: { + type: 'add', + data: { + url: document.location.href, + results: newResults, + curation: { + insert_index: 0, + url: e.detail.url + } + } + } + }); + globalBus.dispatch(curationSaveEvent); + + }); + } + + __sortableActivate(event, ui) { + console.log("Sortable activate", ui); + this.__beginCurating(); + this.oldIndex = ui.item.index(); + } + + __beginCurating() { + if (!this.curating) { + const results = this.__getResults(); + const curationStartEvent = new CustomEvent('save-curation', { + detail: { + type: 'begin', + data: { + url: document.location.href, + results: results + } + } + }); + globalBus.dispatch(curationStartEvent); + this.curating = true; + } + } + + __getResults() { + const resultsElements = document.querySelectorAll('.results .result:not(.ui-sortable-placeholder)'); + const results = []; + for (let resultElement of resultsElements) { + const result = { + url: resultElement.querySelector('a').href, + title: resultElement.querySelector('.title').innerText, + extract: resultElement.querySelector('.extract').innerText, + curated: resultElement.querySelector('.curate-approve').isValidated() + } + results.push(result); + } + console.log("Results", results); + return results; + } + + __sortableDeactivate(event, ui) { + const newIndex = ui.item.index(); + console.log('Sortable deactivate', ui, this.oldIndex, newIndex); + + const newResults = this.__getResults(); + + const curationMoveEvent = new CustomEvent('save-curation', { + detail: { + type: 'move', + data: { + url: document.location.href, + results: newResults, + curation: { + old_index: this.oldIndex, + new_index: newIndex, + } + } + } + }); + globalBus.dispatch(curationMoveEvent); } }); \ No newline at end of file diff --git a/front-end/src/components/organisms/save.js b/front-end/src/components/organisms/save.js new file mode 100644 index 0000000..dc2f883 --- /dev/null +++ b/front-end/src/components/organisms/save.js @@ -0,0 +1,122 @@ +import define from '../../utils/define.js'; +import {globalBus} from "../../utils/events.js"; +import config from "../../../config.js"; + + +const CURATION_KEY_PREFIX = "curation-"; +const CURATION_URL = config.publicApiURL + "user/curation/"; + + +const template = () => /*html*/` + 🖫 +`; + + +export default define('save', class extends HTMLLIElement { + constructor() { + super(); + this.currentCurationId = null; + this.classList.add('save'); + this.sendId = 0; + this.sending = false; + this.__setup(); + } + + __setup() { + this.innerHTML = template(); + this.__events(); + // TODO: figure out when to call __sendToApi() + // setInterval(this.__sendToApi.bind(this), 1000); + } + + __events() { + globalBus.on('save-curation', (e) => { + // We might not be online, or logged in, so save the curation in local storage in case: + console.log("Curation event", e); + this.__setCuration(e.detail); + this.__sendToApi(); + }); + } + + __setCuration(curation) { + this.sendId += 1; + const key = CURATION_KEY_PREFIX + this.sendId; + localStorage.setItem(key, JSON.stringify(curation)); + } + + __getOldestCurationKey() { + let oldestId = Number.MAX_SAFE_INTEGER; + let oldestKey = null; + for (let i=0; i row.startsWith('jwt=')) + ?.split('=')[1]; + + if (!auth) { + console.log("No auth"); + return; + } + + if (localStorage.length > 0) { + const key = this.__getOldestCurationKey(); + const value = JSON.parse(localStorage.getItem(key)); + console.log("Value", value); + const url = CURATION_URL + value['type']; + + let data = value['data']; + if (value.type !== 'begin') { + if (this.currentCurationId === null) { + throw ReferenceError("No current curation found"); + } + data['curation_id'] = this.currentCurationId; + } + data['auth'] = auth; + + console.log("Data", data); + const response = await fetch(url, { + method: 'POST', + cache: 'no-cache', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify(data), + }); + + console.log("Save curation API response", response); + + if (response.status === 200) { + localStorage.removeItem(key); + } else { + console.log("Bad response, skipping"); + return; + } + + const responseData = await response.json(); + console.log("Response data", responseData); + if (responseData["curation_id"]) { + this.currentCurationId = responseData["curation_id"]; + } + + // There may be more to send, wait a second and see + setTimeout(this.__sendToApi.bind(this), 1000); + } + this.sending = false; + } +}, { extends: 'li' }); + diff --git a/front-end/src/components/organisms/search-bar.js b/front-end/src/components/organisms/search-bar.js index f700fda..5adf52b 100644 --- a/front-end/src/components/organisms/search-bar.js +++ b/front-end/src/components/organisms/search-bar.js @@ -144,8 +144,12 @@ export default define('search-bar', class extends HTMLElement { // Focus search bar when pressing `ctrl + k` or `/` document.addEventListener('keydown', (e) => { - if ((e.key === 'k' && e.ctrlKey) || e.key === '/' || e.key === 'Escape') { + if ((e.key === 'k' && e.ctrlKey) || e.key === 'Escape') { e.preventDefault(); + + // Remove the modal if it's visible + document.querySelector('.modal').style.display = 'none'; + this.searchInput.focus(); } }); diff --git a/front-end/src/components/register.js b/front-end/src/components/register.js new file mode 100644 index 0000000..ff5bfc0 --- /dev/null +++ b/front-end/src/components/register.js @@ -0,0 +1,84 @@ +import define from '../utils/define.js'; +import config from "../../config.js"; + +const template = () => /*html*/` +
    +
    Register
    +
    + +
    + +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    +
    +`; + +export default define('register', class extends HTMLElement { + constructor() { + super(); + this.registerForm = null; + this.emailInput = null; + this.usernameInput = null; + this.passwordInput = null; + this.passwordVerifyInput = null; + this.__setup(); + this.__events(); + } + + __setup() { + this.innerHTML = template(); + this.registerForm = this.querySelector('form'); + this.emailInput = this.querySelector('#register-email'); + this.usernameInput = this.querySelector('#register-username'); + this.passwordInput = this.querySelector('#register-password'); + this.passwordVerifyInput = this.querySelector('#register-password-verify'); + } + + __events() { + this.registerForm.addEventListener('submit', (e) => { + e.preventDefault(); + this.__handleRegister(e); + }); + } + + __handleRegister = async () => { + const response = await fetch(`${config.publicApiURL}user/register`, { + method: 'POST', + cache: 'no-cache', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ + "username": this.usernameInput.value, + "email": this.emailInput.value, + "password": this.passwordInput.value, + "password_verify": this.passwordVerifyInput.value, + }) + }); + if (response.status === 200) { + const registerData = await response.json(); + console.log("Register data", registerData); + document.cookie = `jwt=${registerData["jwt"]}; SameSite=Strict`; + console.log("Register success"); + } else { + console.log("Register error", response); + } + } +}); \ No newline at end of file diff --git a/front-end/src/index.html b/front-end/src/index.html index 23c0b52..843bfc8 100644 --- a/front-end/src/index.html +++ b/front-end/src/index.html @@ -35,21 +35,28 @@ - + + + + + + + +