diff --git a/js/Makefile b/js/Makefile index 315d4ce25..c55170452 100644 --- a/js/Makefile +++ b/js/Makefile @@ -37,7 +37,7 @@ translations: ./$(OUTPUT_DIR)/translations.json mkdir -p $(dir $@) which gettext-extract # Extract gettext strings from templates files and create a POT dictionary template. - gettext-extract --attribute v-translate --quiet --output $@ $(GETTEXT_HTML_SOURCES) + gettext-extract --attribute v-translate --quiet --parseScript false --output $@ $(GETTEXT_HTML_SOURCES) # Extract gettext strings from JavaScript files. xgettext --language=JavaScript --keyword=npgettext:1c,2,3 \ --from-code=utf-8 --join-existing --no-wrap \ diff --git a/js/package-lock.json b/js/package-lock.json index 1e05c10c6..0a5d103d1 100644 --- a/js/package-lock.json +++ b/js/package-lock.json @@ -890,12 +890,6 @@ "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==", "dev": true }, - "@types/async": { - "version": "2.0.50", - "resolved": "https://registry.npmjs.org/@types/async/-/async-2.0.50.tgz", - "integrity": "sha512-VMhZMMQgV1zsR+lX/0IBfAk+8Eb7dPVMWiQGFAt3qjo5x7Ml6b77jUo0e1C3ToD+XRDXqtrfw+6AB0uUsPEr3Q==", - "optional": true - }, "@types/babel-types": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/@types/babel-types/-/babel-types-7.0.4.tgz", @@ -1144,7 +1138,7 @@ }, "globby": { "version": "8.0.1", - "resolved": "http://registry.npmjs.org/globby/-/globby-8.0.1.tgz", + "resolved": "https://registry.npmjs.org/globby/-/globby-8.0.1.tgz", "integrity": "sha512-oMrYrJERnKBLXNLVTqhm3vPEdJ/b2ZE28xN4YARiix1NOIOBPEpOUnm844K1iu/BkphCaf2WNFwMszv8Soi1pw==", "dev": true, "requires": { @@ -1657,6 +1651,12 @@ "integrity": "sha1-7PAh+hCP0X37Xms4Py3SM+Mf/Fk=", "dev": true }, + "ajv-keywords": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.2.0.tgz", + "integrity": "sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo=", + "dev": true + }, "align-text": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", @@ -1679,10 +1679,19 @@ "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", "dev": true }, + "ansi-align": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-2.0.0.tgz", + "integrity": "sha1-w2rsy6VjuJzrVW82kPCx2eNUf38=", + "dev": true, + "requires": { + "string-width": "^2.0.0" + } + }, "ansi-colors": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.1.tgz", - "integrity": "sha512-Xt+zb6nqgvV9SWAVp0EG3lRsHcbq5DDgqjPPz6pwgtj6RKz65zGXMNa82oJfOSBA/to6GmRP7Dr+6o+kbApTzQ==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz", + "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==", "dev": true }, "ansi-html": { @@ -1739,72 +1748,69 @@ } }, "apollo-cache": { - "version": "1.1.23", - "resolved": "https://registry.npmjs.org/apollo-cache/-/apollo-cache-1.1.23.tgz", - "integrity": "sha512-9FcavymJQdjaYX6of1xgjFmWp8UUCtlW/S9k5bfMMnMNAAQMDnuOFVEklawdSbyAhM4hh1kSymyt/r23K1tVSA==", + "version": "1.1.25", + "resolved": "https://registry.npmjs.org/apollo-cache/-/apollo-cache-1.1.25.tgz", + "integrity": "sha512-9HhI/tVEHAeGaJJvi1Vpf6PzXUCA0PqNbigi2G3uOc180JjxbcaBvEbKXMEDb/UyTXkFWzI4PiPDuDQFqmIMSA==", "requires": { - "apollo-utilities": "^1.1.0" + "apollo-utilities": "^1.1.2", + "tslib": "^1.9.3" }, "dependencies": { "apollo-utilities": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/apollo-utilities/-/apollo-utilities-1.1.0.tgz", - "integrity": "sha512-D3scmF6vJkCBECMasMEc0J9dNNHVULl6h2d7/oGJxWid1fgcAPHZJN5XLCZisuOfn0Dvvu3Unf/zK00Z3e49Qg==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/apollo-utilities/-/apollo-utilities-1.1.2.tgz", + "integrity": "sha512-EjDx8vToK+zkWIxc76ZQY/irRX52puNg04xf/w8R0kVTDAgHuVfnFVC01O5vE25kFnIaa5em0pFI0p9b6YMkhQ==", "requires": { - "fast-json-stable-stringify": "^2.0.0" + "fast-json-stable-stringify": "^2.0.0", + "tslib": "^1.9.3" } } } }, "apollo-cache-inmemory": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/apollo-cache-inmemory/-/apollo-cache-inmemory-1.4.0.tgz", - "integrity": "sha512-gqwiZCApG+hRXEbtDrNIUtNcKKeO3RTih4mydJZRJpFp+PgBf8AZ2z4/uTJFFijmR9krmUWdCRfW1Aesb5+Ktg==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/apollo-cache-inmemory/-/apollo-cache-inmemory-1.4.2.tgz", + "integrity": "sha512-fDVmj5j1e3W+inyuSwjIcMgbQ4edcFgmiKTBMFAEKAq0jg33X7FrbDX8JT2t5Vuf75Mva50JDlt5wXdu7C6WuA==", "requires": { - "apollo-cache": "^1.1.23", - "apollo-utilities": "^1.1.0", - "optimism": "^0.6.8" + "apollo-cache": "^1.1.25", + "apollo-utilities": "^1.1.2", + "optimism": "^0.6.9", + "tslib": "^1.9.3" }, "dependencies": { "apollo-utilities": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/apollo-utilities/-/apollo-utilities-1.1.0.tgz", - "integrity": "sha512-D3scmF6vJkCBECMasMEc0J9dNNHVULl6h2d7/oGJxWid1fgcAPHZJN5XLCZisuOfn0Dvvu3Unf/zK00Z3e49Qg==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/apollo-utilities/-/apollo-utilities-1.1.2.tgz", + "integrity": "sha512-EjDx8vToK+zkWIxc76ZQY/irRX52puNg04xf/w8R0kVTDAgHuVfnFVC01O5vE25kFnIaa5em0pFI0p9b6YMkhQ==", "requires": { - "fast-json-stable-stringify": "^2.0.0" + "fast-json-stable-stringify": "^2.0.0", + "tslib": "^1.9.3" } } } }, "apollo-client": { - "version": "2.4.9", - "resolved": "https://registry.npmjs.org/apollo-client/-/apollo-client-2.4.9.tgz", - "integrity": "sha512-7IoIz+3hAk3166663ioyrsPdlTSAQa24HsJPVWw2QM/UE1wZTommu+uhwj9KugL6vpE/wyhyYgEs/9R7PlCsQA==", + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/apollo-client/-/apollo-client-2.4.12.tgz", + "integrity": "sha512-E5ClFSB9btJLYibLKwLDSCg+w9tI+25eZgXOM+DClawu7of4d/xhuV/xvpuZpsMP3qwrp0QPacBnfG4tUJs3/w==", "requires": { - "@types/async": "2.0.50", "@types/zen-observable": "^0.8.0", - "apollo-cache": "1.1.23", + "apollo-cache": "1.1.25", "apollo-link": "^1.0.0", "apollo-link-dedup": "^1.0.0", - "apollo-utilities": "1.1.0", + "apollo-utilities": "1.1.2", "symbol-observable": "^1.0.2", + "tslib": "^1.9.3", "zen-observable": "^0.8.0" }, "dependencies": { - "apollo-cache": { - "version": "1.1.23", - "resolved": "https://registry.npmjs.org/apollo-cache/-/apollo-cache-1.1.23.tgz", - "integrity": "sha512-9FcavymJQdjaYX6of1xgjFmWp8UUCtlW/S9k5bfMMnMNAAQMDnuOFVEklawdSbyAhM4hh1kSymyt/r23K1tVSA==", - "requires": { - "apollo-utilities": "^1.1.0" - } - }, "apollo-utilities": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/apollo-utilities/-/apollo-utilities-1.1.0.tgz", - "integrity": "sha512-D3scmF6vJkCBECMasMEc0J9dNNHVULl6h2d7/oGJxWid1fgcAPHZJN5XLCZisuOfn0Dvvu3Unf/zK00Z3e49Qg==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/apollo-utilities/-/apollo-utilities-1.1.2.tgz", + "integrity": "sha512-EjDx8vToK+zkWIxc76ZQY/irRX52puNg04xf/w8R0kVTDAgHuVfnFVC01O5vE25kFnIaa5em0pFI0p9b6YMkhQ==", "requires": { - "fast-json-stable-stringify": "^2.0.0" + "fast-json-stable-stringify": "^2.0.0", + "tslib": "^1.9.3" } } } @@ -1824,25 +1830,6 @@ "integrity": "sha512-i4NuqT3DSFczFcC7NMUzmnYjKX7NggLY+rqYVf+kE9JjqKOQhT6wqhaWsVIABfIUGE/N0DTgYJBCMu/18aXmYA==", "requires": { "apollo-link": "^1.2.6" - }, - "dependencies": { - "apollo-link": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/apollo-link/-/apollo-link-1.2.6.tgz", - "integrity": "sha512-sUNlA20nqIF3gG3F8eyMD+mO80fmf3dPZX+GUOs3MI9oZR8ug09H3F0UsWJMcpEg6h55Yy5wZ+BMmAjrbenF/Q==", - "requires": { - "apollo-utilities": "^1.0.0", - "zen-observable-ts": "^0.8.13" - } - }, - "zen-observable-ts": { - "version": "0.8.13", - "resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-0.8.13.tgz", - "integrity": "sha512-WDb8SM0tHCb6c0l1k60qXWlm1ok3zN9U4VkLdnBKQwIYwUoB9psH7LIFgR+JVCCMmBxUgOjskIid8/N02k/2Bg==", - "requires": { - "zen-observable": "^0.8.0" - } - } } }, "apollo-link-http": { @@ -1939,7 +1926,7 @@ }, "array-equal": { "version": "1.0.0", - "resolved": "http://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=", "dev": true }, @@ -2077,7 +2064,7 @@ }, "async": { "version": "1.5.2", - "resolved": "http://registry.npmjs.org/async/-/async-1.5.2.tgz", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", "dev": true }, @@ -2191,7 +2178,7 @@ }, "chalk": { "version": "1.1.3", - "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "dev": true, "requires": { @@ -2210,7 +2197,7 @@ }, "strip-ansi": { "version": "3.0.1", - "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, "requires": { @@ -2501,9 +2488,9 @@ }, "dependencies": { "array-flatten": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.1.tgz", - "integrity": "sha1-Qmu52oQJDBg42BLIFQryCoMx4pY=", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", + "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", "dev": true } } @@ -2513,6 +2500,21 @@ "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" }, + "boxen": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz", + "integrity": "sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw==", + "dev": true, + "requires": { + "ansi-align": "^2.0.0", + "camelcase": "^4.0.0", + "chalk": "^2.0.1", + "cli-boxes": "^1.0.0", + "string-width": "^2.0.0", + "term-size": "^1.2.0", + "widest-line": "^2.0.0" + } + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -2652,6 +2654,14 @@ "node-releases": "^1.0.1" } }, + "buefy": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/buefy/-/buefy-0.7.1.tgz", + "integrity": "sha512-Pq7T5ASuSjpBMFPQfnkDLCKtQxz8ciftTwYkzix7DMWvZHjtcZh1dF34ve8StjXqknEKYxFruCyioE3LdPmqqQ==", + "requires": { + "bulma": "0.7.2" + } + }, "buffer": { "version": "4.9.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", @@ -2693,6 +2703,11 @@ "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", "dev": true }, + "bulma": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/bulma/-/bulma-0.7.2.tgz", + "integrity": "sha512-6JHEu8U/1xsyOst/El5ImLcZIiE2JFXgvrz8GGWbnDLwTNRPJzdAM0aoUM1Ns0avALcVb6KZz9NhzmU53dGDcQ==" + }, "bytes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", @@ -2701,7 +2716,7 @@ }, "cacache": { "version": "10.0.4", - "resolved": "http://registry.npmjs.org/cacache/-/cacache-10.0.4.tgz", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-10.0.4.tgz", "integrity": "sha512-Dph0MzuH+rTQzGPNT9fAnrPmMmjKfST6trxJeK7NQuHRaVw24VzPRWTmg9MpcwOVQZO0E1FBICUlFeNaKPIfHA==", "dev": true, "requires": { @@ -2807,7 +2822,7 @@ }, "camelcase-keys": { "version": "2.1.0", - "resolved": "http://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", "dev": true, "requires": { @@ -2841,6 +2856,12 @@ "integrity": "sha512-x/E/SNwD80I0bT+fF9Y3Kbwo7Xd1xSafCAmFlpJmaVg3SQoJJOH4Ivb9fi9S0WjfqewQ6Ydt1zEVZpmMVYNeDA==", "dev": true }, + "capture-stack-trace": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.1.tgz", + "integrity": "sha512-mYQLZnx5Qt1JgB1WEiMCf2647plpGeQ2NMR/5L0HNZzGQo4fuSPnK+wjfPnKZV0aiJDgzmWqqkV/g7JD+DW0qw==", + "dev": true + }, "case-sensitive-paths-webpack-plugin": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.1.2.tgz", @@ -3121,6 +3142,12 @@ } } }, + "cli-boxes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz", + "integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM=", + "dev": true + }, "cli-cursor": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", @@ -3292,7 +3319,7 @@ }, "colors": { "version": "1.1.2", - "resolved": "http://registry.npmjs.org/colors/-/colors-1.1.2.tgz", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=", "dev": true }, @@ -3391,10 +3418,24 @@ "typedarray": "^0.0.6" } }, + "configstore": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-3.1.2.tgz", + "integrity": "sha512-vtv5HtGjcYUgFrXc6Kx747B83MRRVS5R1VTEQoXvuP+kMI+if6uywV0nDGoiydJRy4yk7h9od5Og0kxx4zUXmw==", + "dev": true, + "requires": { + "dot-prop": "^4.1.0", + "graceful-fs": "^4.1.2", + "make-dir": "^1.0.0", + "unique-string": "^1.0.0", + "write-file-atomic": "^2.0.0", + "xdg-basedir": "^3.0.0" + } + }, "connect-history-api-fallback": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.5.0.tgz", - "integrity": "sha1-sGhzk0vF40T+9hGhlqb6rgruAVo=", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz", + "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==", "dev": true }, "console-browserify": { @@ -3574,6 +3615,15 @@ "elliptic": "^6.0.0" } }, + "create-error-class": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz", + "integrity": "sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=", + "dev": true, + "requires": { + "capture-stack-trace": "^1.0.0" + } + }, "create-hash": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", @@ -3638,9 +3688,15 @@ "randomfill": "^1.0.3" } }, + "crypto-random-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", + "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=", + "dev": true + }, "css-color-names": { "version": "0.0.4", - "resolved": "http://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", + "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", "integrity": "sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=", "dev": true }, @@ -3753,7 +3809,7 @@ }, "jsesc": { "version": "0.5.0", - "resolved": "http://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", "dev": true }, @@ -3770,13 +3826,13 @@ }, "regjsgen": { "version": "0.2.0", - "resolved": "http://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=", "dev": true }, "regjsparser": { "version": "0.1.5", - "resolved": "http://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=", "dev": true, "requires": { @@ -4005,7 +4061,7 @@ }, "deep-eql": { "version": "0.1.3", - "resolved": "http://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz", "integrity": "sha1-71WKyrjeJSBs1xOQbXTlaTDrafI=", "dev": true, "requires": { @@ -4018,6 +4074,12 @@ "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=", "dev": true }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true + }, "deep-is": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", @@ -4353,10 +4415,16 @@ }, "duplexer": { "version": "0.1.1", - "resolved": "http://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=", "dev": true }, + "duplexer3": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", + "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", + "dev": true + }, "duplexify": { "version": "3.6.1", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.6.1.tgz", @@ -5074,7 +5142,7 @@ }, "finalhandler": { "version": "1.1.1", - "resolved": "http://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", "dev": true, "requires": { @@ -5141,9 +5209,9 @@ } }, "follow-redirects": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", - "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.6.1.tgz", + "integrity": "sha512-t2JCjbzxQpWvbhts3l6SH1DKzSrx8a+SsaVf4h6bG4kOXUuPYS/kg2Lr4gQSb7eemaHqJkOThF1BGyjlUkO1GQ==", "dev": true, "requires": { "debug": "=3.1.0" @@ -5249,7 +5317,7 @@ }, "chalk": { "version": "1.1.3", - "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "dev": true, "requires": { @@ -5262,7 +5330,7 @@ }, "strip-ansi": { "version": "3.0.1", - "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, "requires": { @@ -5317,9 +5385,9 @@ "dev": true }, "fsevents": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz", - "integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.7.tgz", + "integrity": "sha512-Pxm6sI2MeBD7RdD12RYsqaP0nMiwx8eZBXCa6z2L+mRHm2DYrOYwihmhjpkdjUHwQhslWQjRpEgNq4XvBmaAuw==", "dev": true, "optional": true, "requires": { @@ -5345,7 +5413,7 @@ "optional": true }, "are-we-there-yet": { - "version": "1.1.4", + "version": "1.1.5", "bundled": true, "dev": true, "optional": true, @@ -5369,7 +5437,7 @@ } }, "chownr": { - "version": "1.0.1", + "version": "1.1.1", "bundled": true, "dev": true, "optional": true @@ -5405,7 +5473,7 @@ } }, "deep-extend": { - "version": "0.5.1", + "version": "0.6.0", "bundled": true, "dev": true, "optional": true @@ -5454,7 +5522,7 @@ } }, "glob": { - "version": "7.1.2", + "version": "7.1.3", "bundled": true, "dev": true, "optional": true, @@ -5474,12 +5542,12 @@ "optional": true }, "iconv-lite": { - "version": "0.4.21", + "version": "0.4.24", "bundled": true, "dev": true, "optional": true, "requires": { - "safer-buffer": "^2.1.0" + "safer-buffer": ">= 2.1.2 < 3" } }, "ignore-walk": { @@ -5540,16 +5608,16 @@ "dev": true }, "minipass": { - "version": "2.2.4", + "version": "2.3.5", "bundled": true, "dev": true, "requires": { - "safe-buffer": "^5.1.1", + "safe-buffer": "^5.1.2", "yallist": "^3.0.0" } }, "minizlib": { - "version": "1.1.0", + "version": "1.2.1", "bundled": true, "dev": true, "optional": true, @@ -5572,7 +5640,7 @@ "optional": true }, "needle": { - "version": "2.2.0", + "version": "2.2.4", "bundled": true, "dev": true, "optional": true, @@ -5583,18 +5651,18 @@ } }, "node-pre-gyp": { - "version": "0.10.0", + "version": "0.10.3", "bundled": true, "dev": true, "optional": true, "requires": { "detect-libc": "^1.0.2", "mkdirp": "^0.5.1", - "needle": "^2.2.0", + "needle": "^2.2.1", "nopt": "^4.0.1", "npm-packlist": "^1.1.6", "npmlog": "^4.0.2", - "rc": "^1.1.7", + "rc": "^1.2.7", "rimraf": "^2.6.1", "semver": "^5.3.0", "tar": "^4" @@ -5611,13 +5679,13 @@ } }, "npm-bundled": { - "version": "1.0.3", + "version": "1.0.5", "bundled": true, "dev": true, "optional": true }, "npm-packlist": { - "version": "1.1.10", + "version": "1.2.0", "bundled": true, "dev": true, "optional": true, @@ -5692,12 +5760,12 @@ "optional": true }, "rc": { - "version": "1.2.7", + "version": "1.2.8", "bundled": true, "dev": true, "optional": true, "requires": { - "deep-extend": "^0.5.1", + "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" @@ -5727,16 +5795,16 @@ } }, "rimraf": { - "version": "2.6.2", + "version": "2.6.3", "bundled": true, "dev": true, "optional": true, "requires": { - "glob": "^7.0.5" + "glob": "^7.1.3" } }, "safe-buffer": { - "version": "5.1.1", + "version": "5.1.2", "bundled": true, "dev": true }, @@ -5753,7 +5821,7 @@ "optional": true }, "semver": { - "version": "5.5.0", + "version": "5.6.0", "bundled": true, "dev": true, "optional": true @@ -5804,17 +5872,17 @@ "optional": true }, "tar": { - "version": "4.4.1", + "version": "4.4.8", "bundled": true, "dev": true, "optional": true, "requires": { - "chownr": "^1.0.1", + "chownr": "^1.1.1", "fs-minipass": "^1.2.5", - "minipass": "^2.2.4", - "minizlib": "^1.1.0", + "minipass": "^2.3.4", + "minizlib": "^1.1.1", "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.1", + "safe-buffer": "^5.1.2", "yallist": "^3.0.2" } }, @@ -5825,12 +5893,12 @@ "optional": true }, "wide-align": { - "version": "1.1.2", + "version": "1.1.3", "bundled": true, "dev": true, "optional": true, "requires": { - "string-width": "^1.0.2" + "string-width": "^1.0.2 || 2" } }, "wrappy": { @@ -5839,7 +5907,7 @@ "dev": true }, "yallist": { - "version": "3.0.2", + "version": "3.0.3", "bundled": true, "dev": true } @@ -5875,7 +5943,7 @@ }, "readable-stream": { "version": "1.1.14", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", "dev": true, "requires": { @@ -5942,7 +6010,7 @@ }, "strip-ansi": { "version": "3.0.1", - "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, "requires": { @@ -6080,6 +6148,15 @@ "integrity": "sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=", "dev": true }, + "global-dirs": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", + "integrity": "sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=", + "dev": true, + "requires": { + "ini": "^1.3.4" + } + }, "globals": { "version": "11.9.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.9.0.tgz", @@ -6088,7 +6165,7 @@ }, "globby": { "version": "6.1.0", - "resolved": "http://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", "dev": true, "requires": { @@ -6101,7 +6178,7 @@ "dependencies": { "pify": { "version": "2.3.0", - "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", "dev": true } @@ -6118,6 +6195,33 @@ "minimatch": "~3.0.2" } }, + "got": { + "version": "6.7.1", + "resolved": "http://registry.npmjs.org/got/-/got-6.7.1.tgz", + "integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=", + "dev": true, + "requires": { + "create-error-class": "^3.0.0", + "duplexer3": "^0.1.4", + "get-stream": "^3.0.0", + "is-redirect": "^1.0.0", + "is-retry-allowed": "^1.0.0", + "is-stream": "^1.0.0", + "lowercase-keys": "^1.0.0", + "safe-buffer": "^5.0.1", + "timed-out": "^4.0.0", + "unzip-response": "^2.0.1", + "url-parse-lax": "^1.0.0" + }, + "dependencies": { + "get-stream": { + "version": "3.0.0", + "resolved": "http://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", + "dev": true + } + } + }, "graceful-fs": { "version": "4.1.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", @@ -6139,21 +6243,11 @@ } }, "graphql-anywhere": { - "version": "4.1.25", - "resolved": "https://registry.npmjs.org/graphql-anywhere/-/graphql-anywhere-4.1.25.tgz", - "integrity": "sha512-6XgTH+3n2R+xhmfFMM37tIUyZPy7Q7/TA4H37m0x6dN/UtsDiGs4jfH1upCIiHc8eKtJ0leyadhFIiXqNB4KzA==", + "version": "4.1.23", + "resolved": "https://registry.npmjs.org/graphql-anywhere/-/graphql-anywhere-4.1.23.tgz", + "integrity": "sha512-8wtmwxWmLzAy52Z4WAw9UiYZ4ViiNXM+2DMOSlg2F7WsVstD0v75uOjCLJQUvbld5kHrgzTxter/THFVkGL+Yw==", "requires": { - "apollo-utilities": "^1.1.0" - }, - "dependencies": { - "apollo-utilities": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/apollo-utilities/-/apollo-utilities-1.1.0.tgz", - "integrity": "sha512-D3scmF6vJkCBECMasMEc0J9dNNHVULl6h2d7/oGJxWid1fgcAPHZJN5XLCZisuOfn0Dvvu3Unf/zK00Z3e49Qg==", - "requires": { - "fast-json-stable-stringify": "^2.0.0" - } - } + "apollo-utilities": "^1.0.26" } }, "graphql-tag": { @@ -6178,9 +6272,9 @@ } }, "handle-thing": { - "version": "1.2.5", - "resolved": "http://registry.npmjs.org/handle-thing/-/handle-thing-1.2.5.tgz", - "integrity": "sha1-/Xqtcmvxpf0W38KbL3pmAdJxOcQ=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.0.tgz", + "integrity": "sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ==", "dev": true }, "har-schema": { @@ -6419,7 +6513,7 @@ }, "html-webpack-plugin": { "version": "3.2.0", - "resolved": "http://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz", "integrity": "sha1-sBq71yOsqqeze2r0SS69oD2d03s=", "dev": true, "requires": { @@ -6448,7 +6542,7 @@ }, "htmlparser2": { "version": "3.3.0", - "resolved": "http://registry.npmjs.org/htmlparser2/-/htmlparser2-3.3.0.tgz", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.3.0.tgz", "integrity": "sha1-zHDQWln2VC5D8OaFyYLhTJJKnv4=", "dev": true, "requires": { @@ -6475,7 +6569,7 @@ }, "readable-stream": { "version": "1.0.34", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", "dev": true, "requires": { @@ -6501,7 +6595,7 @@ }, "http-errors": { "version": "1.6.3", - "resolved": "http://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", "dev": true, "requires": { @@ -6558,7 +6652,7 @@ }, "http-proxy-middleware": { "version": "0.18.0", - "resolved": "http://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.18.0.tgz", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.18.0.tgz", "integrity": "sha512-Fs25KVMPAIIcgjMZkVHJoKg9VcXcC1C8yb9JUgeDvVXY0S/zgVIhMb+qVswDIgtJe2DfckMSY2d6TuTEutlk6Q==", "dev": true, "requires": { @@ -6675,9 +6769,9 @@ "dev": true }, "immutable-tuple": { - "version": "0.4.9", - "resolved": "https://registry.npmjs.org/immutable-tuple/-/immutable-tuple-0.4.9.tgz", - "integrity": "sha512-LWbJPZnidF8eczu7XmcnLBsumuyRBkpwIRPCZxlojouhBo5jEBO4toj6n7hMy6IxHU/c+MqDSWkvaTpPlMQcyA==" + "version": "0.4.10", + "resolved": "https://registry.npmjs.org/immutable-tuple/-/immutable-tuple-0.4.10.tgz", + "integrity": "sha512-45jheDbc3Kr5Cw8EtDD+4woGRUV0utIrJBZT8XH0TPZRfm8tzT0/sLGGzyyCCFqFMG5Pv5Igf3WY/arn6+8V9Q==" }, "import-cwd": { "version": "2.1.0", @@ -6732,6 +6826,12 @@ } } }, + "import-lazy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", + "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=", + "dev": true + }, "import-local": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", @@ -6762,9 +6862,9 @@ } }, "p-limit": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.0.0.tgz", - "integrity": "sha512-fl5s52lI5ahKCernzzIyAP0QAZbGIovtVHGwpcu1Jr/EpzLVDI2myISHwGqK7m8uQFugVWSrbxH7XnhGtvEc+A==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.1.0.tgz", + "integrity": "sha512-NhURkNcrVB+8hNfLuysU8enY5xn2KXphsHBaC2YmRNTZRc7RWusw6apSpdEj3jo4CMb6W9nrF6tTnsJsJeyu6g==", "dev": true, "requires": { "p-try": "^2.0.0" @@ -6843,6 +6943,12 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", + "dev": true + }, "internal-ip": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-3.0.1.tgz", @@ -7063,6 +7169,22 @@ "is-extglob": "^2.1.1" } }, + "is-installed-globally": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.1.0.tgz", + "integrity": "sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA=", + "dev": true, + "requires": { + "global-dirs": "^0.1.0", + "is-path-inside": "^1.0.0" + } + }, + "is-npm": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz", + "integrity": "sha1-8vtjpl5JBbQGyGBydloaTceTufQ=", + "dev": true + }, "is-number": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", @@ -7123,6 +7245,12 @@ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" }, + "is-redirect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", + "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=", + "dev": true + }, "is-regex": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", @@ -7143,6 +7271,12 @@ "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==", "dev": true }, + "is-retry-allowed": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz", + "integrity": "sha1-EaBgVotnM5REAz0BJaYaINVk+zQ=", + "dev": true + }, "is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", @@ -7490,6 +7624,15 @@ "is-buffer": "^1.1.5" } }, + "latest-version": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-3.1.0.tgz", + "integrity": "sha1-ogU4P+oyKzO1rjsYq+4NwvNW7hU=", + "dev": true, + "requires": { + "package-json": "^4.0.0" + } + }, "launch-editor": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.2.1.tgz", @@ -7872,6 +8015,12 @@ "integrity": "sha1-miyr0bno4K6ZOkv31YdcOcQujqw=", "dev": true }, + "lowercase-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", + "dev": true + }, "lru-cache": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.3.tgz", @@ -7986,9 +8135,9 @@ "integrity": "sha512-TxFAc76Jnhb2OUu+n3yz9RMu4CwGfaT788br6HhEDlvWfdeJcLUsxk1Hgw2yJio0OXsxv7pyIPmvECY7bMbluA==" }, "markdown-it-toc-and-anchor": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/markdown-it-toc-and-anchor/-/markdown-it-toc-and-anchor-4.1.2.tgz", - "integrity": "sha1-snH2lKcL9xnmtygFbXvZMdNkIU0=", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/markdown-it-toc-and-anchor/-/markdown-it-toc-and-anchor-4.2.0.tgz", + "integrity": "sha512-DusSbKtg8CwZ92ztN7bOojDpP4h0+w7BVOPuA3PHDIaabMsERYpwsazLYSP/UlKedoQjOz21mwlai36TQ04EpA==", "requires": { "clone": "^2.1.0", "uslug": "^1.0.4" @@ -8045,7 +8194,7 @@ }, "media-typer": { "version": "0.3.0", - "resolved": "http://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", "dev": true }, @@ -8070,7 +8219,7 @@ }, "meow": { "version": "3.7.0", - "resolved": "http://registry.npmjs.org/meow/-/meow-3.7.0.tgz", + "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", "dev": true, "requires": { @@ -8313,7 +8462,7 @@ }, "mkdirp": { "version": "0.5.1", - "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "dev": true, "requires": { @@ -8361,7 +8510,7 @@ }, "commander": { "version": "2.15.1", - "resolved": "http://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", "dev": true }, @@ -8687,7 +8836,7 @@ "dependencies": { "semver": { "version": "5.3.0", - "resolved": "http://registry.npmjs.org/semver/-/semver-5.3.0.tgz", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", "dev": true } @@ -8785,7 +8934,7 @@ }, "chalk": { "version": "1.1.3", - "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "dev": true, "requires": { @@ -8808,7 +8957,7 @@ }, "strip-ansi": { "version": "3.0.1", - "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, "requires": { @@ -9064,9 +9213,9 @@ } }, "optimism": { - "version": "0.6.8", - "resolved": "https://registry.npmjs.org/optimism/-/optimism-0.6.8.tgz", - "integrity": "sha512-bN5n1KCxSqwBDnmgDnzMtQTHdL+uea2HYFx1smvtE+w2AMl0Uy31g0aXnP/Nt85OINnMJPRpJyfRQLTCqn5Weg==", + "version": "0.6.9", + "resolved": "https://registry.npmjs.org/optimism/-/optimism-0.6.9.tgz", + "integrity": "sha512-xoQm2lvXbCA9Kd7SCx6y713Y7sZ6fUc5R6VYpoL5M6svKJbTuvtNopexK8sO8K4s0EOUYHuPN2+yAEsNyRggkQ==", "requires": { "immutable-tuple": "^0.4.9" } @@ -9142,7 +9291,7 @@ }, "os-homedir": { "version": "1.0.2", - "resolved": "http://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", "dev": true }, @@ -9193,7 +9342,7 @@ }, "os-tmpdir": { "version": "1.0.2", - "resolved": "http://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "dev": true }, @@ -9302,6 +9451,18 @@ "thunkify": "~2.1.1" } }, + "package-json": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-4.0.1.tgz", + "integrity": "sha1-iGmgQBJTZhxMTKPabCEh7VVfXu0=", + "dev": true, + "requires": { + "got": "^6.7.1", + "registry-auth-token": "^3.0.1", + "registry-url": "^3.0.3", + "semver": "^5.1.0" + } + }, "pako": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.6.tgz", @@ -9368,6 +9529,75 @@ "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", "dev": true }, + "patch-package": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-5.1.1.tgz", + "integrity": "sha512-bO+vfFGgTVTtv89kXWGEMIPnrYhhhMtbOnJKStfIhNWUxperVjlI++1ixksi0YPCMPGuFy9W3zsKHxIITV2r2A==", + "dev": true, + "requires": { + "chalk": "^1.1.3", + "cross-spawn": "^5.1.0", + "fs-extra": "^4.0.1", + "minimist": "^1.2.0", + "rimraf": "^2.6.1", + "slash": "^1.0.0", + "tmp": "^0.0.31", + "update-notifier": "^2.2.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "dev": true, + "requires": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, "path-browserify": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.0.tgz", @@ -9388,7 +9618,7 @@ }, "path-is-absolute": { "version": "1.0.1", - "resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true }, @@ -10080,6 +10310,12 @@ "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", "dev": true }, + "prepend-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", + "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", + "dev": true + }, "prettier": { "version": "1.13.7", "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.13.7.tgz", @@ -10087,7 +10323,7 @@ }, "pretty-bytes": { "version": "4.0.2", - "resolved": "http://registry.npmjs.org/pretty-bytes/-/pretty-bytes-4.0.2.tgz", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-4.0.2.tgz", "integrity": "sha1-sr+C5zUNZcbDOqlaqlpPYyf2HNk=", "dev": true }, @@ -10482,6 +10718,18 @@ "unpipe": "1.0.0" } }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + } + }, "read-pkg": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-4.0.1.tgz", @@ -10527,7 +10775,7 @@ }, "load-json-file": { "version": "1.1.0", - "resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", "dev": true, "requires": { @@ -10560,7 +10808,7 @@ }, "pify": { "version": "2.3.0", - "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", "dev": true }, @@ -10697,6 +10945,25 @@ "resolved": "https://registry.npmjs.org/register-service-worker/-/register-service-worker-1.5.2.tgz", "integrity": "sha512-XNqSZHJsFGnvEGkg/2IrCp6G8Ya3qLj4mq0bSHil/dfdO82LOxGnMnJjAD9MYCvf/8cDCO8pL+1i65yzmP7rPQ==" }, + "registry-auth-token": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.3.2.tgz", + "integrity": "sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ==", + "dev": true, + "requires": { + "rc": "^1.1.6", + "safe-buffer": "^5.0.1" + } + }, + "registry-url": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", + "integrity": "sha1-PU74cPc93h138M+aOBQyRE4XSUI=", + "dev": true, + "requires": { + "rc": "^1.0.1" + } + }, "regjsgen": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.4.0.tgz", @@ -10714,7 +10981,7 @@ "dependencies": { "jsesc": { "version": "0.5.0", - "resolved": "http://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", "dev": true } @@ -10753,7 +11020,7 @@ }, "css-select": { "version": "1.2.0", - "resolved": "http://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", "dev": true, "requires": { @@ -10775,7 +11042,7 @@ }, "strip-ansi": { "version": "3.0.1", - "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, "requires": { @@ -10897,16 +11164,14 @@ "dev": true, "requires": { "resolve-from": "^3.0.0" - }, - "dependencies": { - "resolve-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", - "dev": true - } } }, + "resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", + "dev": true + }, "resolve-url": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", @@ -10992,7 +11257,7 @@ }, "safe-regex": { "version": "1.1.0", - "resolved": "http://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", "dev": true, "requires": { @@ -11051,7 +11316,7 @@ }, "os-locale": { "version": "1.4.0", - "resolved": "http://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", "dev": true, "requires": { @@ -11071,7 +11336,7 @@ }, "strip-ansi": { "version": "3.0.1", - "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, "requires": { @@ -11175,7 +11440,7 @@ "dependencies": { "source-map": { "version": "0.4.4", - "resolved": "http://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", "dev": true, "requires": { @@ -11211,6 +11476,15 @@ "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", "dev": true }, + "semver-diff": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz", + "integrity": "sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY=", + "dev": true, + "requires": { + "semver": "^5.0.3" + } + }, "send": { "version": "0.16.2", "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", @@ -11714,65 +11988,42 @@ "dev": true }, "spdy": { - "version": "3.4.7", - "resolved": "https://registry.npmjs.org/spdy/-/spdy-3.4.7.tgz", - "integrity": "sha1-Qv9B7OXMD5mjpsKKq7c/XDsDrLw=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.0.tgz", + "integrity": "sha512-ot0oEGT/PGUpzf/6uk4AWLqkq+irlqHXkrdbk51oWONh3bxQmBuljxPNl66zlRRcIJStWq0QkLUCPOPjgjvU0Q==", "dev": true, "requires": { - "debug": "^2.6.8", - "handle-thing": "^1.2.5", + "debug": "^4.1.0", + "handle-thing": "^2.0.0", "http-deceiver": "^1.2.7", - "safe-buffer": "^5.0.1", "select-hose": "^2.0.0", - "spdy-transport": "^2.0.18" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } + "spdy-transport": "^3.0.0" } }, "spdy-transport": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-2.1.1.tgz", - "integrity": "sha512-q7D8c148escoB3Z7ySCASadkegMmUZW8Wb/Q1u0/XBgDKMO880rLQDj8Twiew/tYi7ghemKUi/whSYOwE17f5Q==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", "dev": true, "requires": { - "debug": "^2.6.8", - "detect-node": "^2.0.3", + "debug": "^4.1.0", + "detect-node": "^2.0.4", "hpack.js": "^2.1.6", - "obuf": "^1.1.1", - "readable-stream": "^2.2.9", - "safe-buffer": "^5.0.1", - "wbuf": "^1.7.2" + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" }, "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "readable-stream": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.1.1.tgz", + "integrity": "sha512-DkN66hPyqDhnIQ6Jcsvx9bFjhw214O4poMBcIMgPVpQvNy9a0e0Uhg5SqySyDKAmUlwt8LonTBz1ezOnM8pUdA==", "dev": true, "requires": { - "ms": "2.0.0" + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true } } }, @@ -11981,7 +12232,7 @@ }, "strip-eof": { "version": "1.0.0", - "resolved": "http://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", "dev": true }, @@ -11991,6 +12242,12 @@ "integrity": "sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=", "dev": true }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true + }, "stylehacks": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-4.0.1.tgz", @@ -12064,7 +12321,7 @@ }, "tar": { "version": "2.2.1", - "resolved": "http://registry.npmjs.org/tar/-/tar-2.2.1.tgz", + "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=", "dev": true, "requires": { @@ -12083,6 +12340,49 @@ "is2": "2.0.1" } }, + "term-size": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz", + "integrity": "sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk=", + "dev": true, + "requires": { + "execa": "^0.7.0" + }, + "dependencies": { + "cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "dev": true, + "requires": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "execa": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", + "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", + "dev": true, + "requires": { + "cross-spawn": "^5.0.1", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "get-stream": { + "version": "3.0.0", + "resolved": "http://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", + "dev": true + } + } + }, "terser": { "version": "3.10.12", "resolved": "https://registry.npmjs.org/terser/-/terser-3.10.12.tgz", @@ -12307,6 +12607,12 @@ "integrity": "sha512-YwT8pjmNcAXBZqrubu22P4FYsh2D4dxRmnWBOL8Jk8bUcRUtc5326kx32tuTmFDAZtLOGEVNl8POAR8j896Iow==", "dev": true }, + "timed-out": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", + "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=", + "dev": true + }, "timers-browserify": { "version": "2.0.10", "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.10.tgz", @@ -12322,6 +12628,15 @@ "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=", "dev": true }, + "tmp": { + "version": "0.0.31", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.31.tgz", + "integrity": "sha1-jzirlDjhcxXl29izZX6L+yd65Kc=", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.1" + } + }, "to-arraybuffer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", @@ -12818,6 +13133,15 @@ "imurmurhash": "^0.1.4" } }, + "unique-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz", + "integrity": "sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo=", + "dev": true, + "requires": { + "crypto-random-string": "^1.0.0" + } + }, "universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", @@ -12881,12 +13205,36 @@ } } }, + "unzip-response": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz", + "integrity": "sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c=", + "dev": true + }, "upath": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.0.tgz", "integrity": "sha512-bzpH/oBhoS/QI/YtbkqCg6VEiPYjSZtrHQM6/QnJS6OL9pKUFLqb3aFh4Scvwm45+7iAgiMkLhSbaZxUqmrprw==", "dev": true }, + "update-notifier": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-2.5.0.tgz", + "integrity": "sha512-gwMdhgJHGuj/+wHJJs9e6PcCszpxR1b236igrOkUofGhqJuG+amlIKwApH1IW1WWl7ovZxsX49lMBWLxSdm5Dw==", + "dev": true, + "requires": { + "boxen": "^1.2.1", + "chalk": "^2.0.1", + "configstore": "^3.0.0", + "import-lazy": "^2.1.0", + "is-ci": "^1.0.10", + "is-installed-globally": "^0.1.0", + "is-npm": "^1.0.0", + "latest-version": "^3.0.0", + "semver-diff": "^2.0.0", + "xdg-basedir": "^3.0.0" + } + }, "upper-case": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz", @@ -12974,6 +13322,15 @@ "requires-port": "^1.0.0" } }, + "url-parse-lax": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", + "integrity": "sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=", + "dev": true, + "requires": { + "prepend-http": "^1.0.1" + } + }, "use": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", @@ -13185,16 +13542,6 @@ "resolved": "https://registry.npmjs.org/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.6.0.tgz", "integrity": "sha512-x3LV3wdmmERhVCYy3quqA57NJW7F3i6faas++pJQWtknWT+n7k30F4TVdHvCLn48peTJFRvCpxs3UuFPqgeELg==" }, - "vuetify": { - "version": "1.3.9", - "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-1.3.9.tgz", - "integrity": "sha512-6cgWRVEBkuUowrfAR9i4lVcTQtwpz9WLM5IcmsMmsmto2GLgf+bi3EfbVFi5xBF3UaV+TC1oHkZq+H/bqgyfhw==" - }, - "vuetify-google-autocomplete": { - "version": "2.0.0-beta.5", - "resolved": "https://registry.npmjs.org/vuetify-google-autocomplete/-/vuetify-google-autocomplete-2.0.0-beta.5.tgz", - "integrity": "sha512-+6Tf97clc1ypVEA+A7XCTonYruxD3N28i2zidp30ex6U3Y1yVB7/km4sN2MxT1gWWVLlZusg8MvF48E6LSP31g==" - }, "vuex": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/vuex/-/vuex-3.0.1.tgz", @@ -13370,9 +13717,9 @@ } }, "webpack-dev-server": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.1.10.tgz", - "integrity": "sha512-RqOAVjfqZJtQcB0LmrzJ5y4Jp78lv9CK0MZ1YJDTaTmedMZ9PU9FLMQNrMCfVu8hHzaVLVOJKBlGEHMN10z+ww==", + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.1.14.tgz", + "integrity": "sha512-mGXDgz5SlTxcF3hUpfC8hrQ11yhAttuUQWf1Wmb+6zo3x6rb7b9mIfuQvAPLdfDRCGRGvakBWHdHOa0I9p/EVQ==", "dev": true, "requires": { "ansi-html": "0.0.7", @@ -13394,23 +13741,19 @@ "portfinder": "^1.0.9", "schema-utils": "^1.0.0", "selfsigned": "^1.9.1", + "semver": "^5.6.0", "serve-index": "^1.7.2", "sockjs": "0.3.19", "sockjs-client": "1.3.0", - "spdy": "^3.4.1", + "spdy": "^4.0.0", "strip-ansi": "^3.0.0", "supports-color": "^5.1.0", + "url": "^0.11.0", "webpack-dev-middleware": "3.4.0", "webpack-log": "^2.0.0", "yargs": "12.0.2" }, "dependencies": { - "ajv-keywords": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.2.0.tgz", - "integrity": "sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo=", - "dev": true - }, "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", @@ -13435,21 +13778,6 @@ "xregexp": "4.0.0" } }, - "execa": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-0.10.0.tgz", - "integrity": "sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw==", - "dev": true, - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^3.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, "find-up": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", @@ -13459,12 +13787,6 @@ "locate-path": "^3.0.0" } }, - "get-stream": { - "version": "3.0.0", - "resolved": "http://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", - "dev": true - }, "invert-kv": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", @@ -13508,20 +13830,20 @@ } }, "os-locale": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.0.1.tgz", - "integrity": "sha512-7g5e7dmXPtzcP4bgsZ8ixDVqA7oWYuEz4lOSujeWyliPai4gfVDiFIcwBg3aGCPnmSGfzOKTK3ccPn0CKv3DBw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", + "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", "dev": true, "requires": { - "execa": "^0.10.0", + "execa": "^1.0.0", "lcid": "^2.0.0", "mem": "^4.0.0" } }, "p-limit": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.0.0.tgz", - "integrity": "sha512-fl5s52lI5ahKCernzzIyAP0QAZbGIovtVHGwpcu1Jr/EpzLVDI2myISHwGqK7m8uQFugVWSrbxH7XnhGtvEc+A==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.1.0.tgz", + "integrity": "sha512-NhURkNcrVB+8hNfLuysU8enY5xn2KXphsHBaC2YmRNTZRc7RWusw6apSpdEj3jo4CMb6W9nrF6tTnsJsJeyu6g==", "dev": true, "requires": { "p-try": "^2.0.0" @@ -13553,9 +13875,15 @@ "ajv-keywords": "^3.1.0" } }, + "semver": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", + "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==", + "dev": true + }, "strip-ansi": { "version": "3.0.1", - "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, "requires": { @@ -13713,6 +14041,15 @@ "string-width": "^1.0.2 || 2" } }, + "widest-line": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-2.0.1.tgz", + "integrity": "sha512-Ba5m9/Fa4Xt9eb2ELXt77JxVDV8w7qQrH0zS/TWSJdLyAwQjWoOzpzj5lwVftDz6n/EOu3tNACS84v509qwnJA==", + "dev": true, + "requires": { + "string-width": "^2.1.1" + } + }, "window-size": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", @@ -13805,7 +14142,7 @@ "dependencies": { "hoek": { "version": "4.2.1", - "resolved": "http://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==", "dev": true }, @@ -13822,7 +14159,7 @@ }, "topo": { "version": "2.0.2", - "resolved": "http://registry.npmjs.org/topo/-/topo-2.0.2.tgz", + "resolved": "https://registry.npmjs.org/topo/-/topo-2.0.2.tgz", "integrity": "sha1-zVYVdSU5BXwNwEkaYhw7xvvh0YI=", "dev": true, "requires": { @@ -13949,7 +14286,7 @@ }, "wrap-ansi": { "version": "2.1.0", - "resolved": "http://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", "dev": true, "requires": { @@ -13985,7 +14322,7 @@ }, "strip-ansi": { "version": "3.0.1", - "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, "requires": { @@ -14000,6 +14337,17 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true }, + "write-file-atomic": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.3.0.tgz", + "integrity": "sha512-xuPeK4OdjWqtfi59ylvVL0Yn35SF3zgcAcv7rBPFHVaEapaDr4GdGgm3j7ckTwH9wHL7fGmgfAnb0+THrHb8tA==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.2" + } + }, "ws": { "version": "6.1.2", "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.2.tgz", @@ -14009,6 +14357,12 @@ "async-limiter": "~1.0.0" } }, + "xdg-basedir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz", + "integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=", + "dev": true + }, "xml-name-validator": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", @@ -14046,7 +14400,7 @@ }, "yargs": { "version": "11.1.0", - "resolved": "http://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz", "integrity": "sha512-NwW69J42EsCSanF8kyn5upxvjp5ds+t3+udGBeTbFnERA+lF541DDpMawzo4z6W/QrzNM18D+BPMiOBibnFV5A==", "dev": true, "requires": { @@ -14135,9 +14489,9 @@ } }, "zen-observable": { - "version": "0.8.9", - "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.9.tgz", - "integrity": "sha512-Y9kPzjGvIZ5jchSlqlCpBW3I82zBBL4z+ulXDRVA1NwsKzjt5kwAi+gOYIy0htNkfuehGZZtP5mRXHRV6TjDWw==" + "version": "0.8.11", + "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.11.tgz", + "integrity": "sha512-N3xXQVr4L61rZvGMpWe8XoCGX8vhU35dPyQ4fm5CY/KDlG0F75un14hjbckPXTDuKUY6V0dqR2giT6xN8Y4GEQ==" }, "zen-observable-ts": { "version": "0.8.13", diff --git a/js/package.json b/js/package.json index eeec4ed92..1a7b32335 100644 --- a/js/package.json +++ b/js/package.json @@ -8,7 +8,8 @@ "analyze-bundle": "npm run build -- --report-json && webpack-bundle-analyzer ../priv/static/report.json", "dev": "vue-cli-service serve", "test:e2e": "vue-cli-service test:e2e", - "test:unit": "vue-cli-service test:unit" + "test:unit": "vue-cli-service test:unit", + "prepare": "patch-package" }, "dependencies": { "apollo-absinthe-upload-link": "^1.4.0", @@ -17,6 +18,7 @@ "apollo-link": "^1.2.6", "apollo-link-http": "^1.5.9", "apollo-link-state": "^0.4.2", + "buefy": "^0.7.1", "easygettext": "^2.7.0", "graphql": "^14.1.1", "graphql-tag": "^2.10.1", @@ -32,8 +34,6 @@ "vue-markdown": "^2.2.4", "vue-property-decorator": "^7.2.0", "vue-router": "^3.0.2", - "vuetify": "^1.3.9", - "vuetify-google-autocomplete": "^2.0.0-beta.5", "vuex": "^3.0.1" }, "devDependencies": { @@ -51,6 +51,7 @@ "chai": "^4.2.0", "dotenv-webpack": "^1.5.7", "node-sass": "^4.10.0", + "patch-package": "^5.1.1", "sass-loader": "^7.1.0", "tslint-config-airbnb": "^5.11.1", "typescript": "^3.0.0", diff --git a/js/patches/easygettext+2.7.0.patch b/js/patches/easygettext+2.7.0.patch new file mode 100644 index 000000000..bdb2ce213 --- /dev/null +++ b/js/patches/easygettext+2.7.0.patch @@ -0,0 +1,41 @@ +patch-package +--- a/node_modules/easygettext/src/extract-cli.js ++++ b/node_modules/easygettext/src/extract-cli.js +@@ -22,9 +22,12 @@ const endDelimiter = argv.endDelimiter === undefined ? constants.DEFAULT_DELIMIT + const extraAttribute = argv.attribute || false; + const extraFilter = argv.filter || false; + const filterPrefix = argv.filterPrefix || constants.DEFAULT_FILTER_PREFIX; ++const parseScript = argv.parseScript === undefined ? true : argv.parseScript === 'true'; + + if (!quietMode && (!files || files.length === 0)) { +- console.log('Usage:\n\tgettext-extract [--attribute EXTRA-ATTRIBUTE] [--filterPrefix FILTER-PREFIX] [--output OUTFILE] <FILES>'); ++ console.log( ++ 'Usage:\n\tgettext-extract [--attribute EXTRA-ATTRIBUTE] [--filterPrefix FILTER-PREFIX] [--parseScript BOOLEAN] [--output OUTFILE] <FILES>', ++ ); + process.exit(1); + } + +@@ -54,7 +57,7 @@ const extractor = new extract.Extractor({ + }); + + +-files.forEach(function(filename) { ++files.forEach(function (filename) { + let file = filename; + const ext = file.split('.').pop(); + if (ALLOWED_EXTENSIONS.indexOf(ext) === -1) { +@@ -63,9 +66,13 @@ files.forEach(function(filename) { + } + console.log(`[${PROGRAM_NAME}] extracting: '${filename}`); + try { +- let data = fs.readFileSync(file, {encoding: 'utf-8'}).toString(); ++ let data = fs.readFileSync(file, { encoding: 'utf-8' }).toString(); + extractor.parse(file, extract.preprocessTemplate(data, ext)); + ++ if (!parseScript) { ++ return; ++ } ++ + if (ext !== 'js') { + data = extract.preprocessScriptTags(data, ext); + } diff --git a/js/public/index.html b/js/public/index.html index 27e6f83c0..86e3871f2 100644 --- a/js/public/index.html +++ b/js/public/index.html @@ -1,17 +1,21 @@ <!DOCTYPE html> -<html> - <head> - <meta charset="utf-8"> - <meta http-equiv="X-UA-Compatible" content="IE=edge"> - <meta name="viewport" content="width=device-width,initial-scale=1.0"> - <link rel="icon" href="<%= BASE_URL %>favicon.ico"> - <title>mobilizon</title> - </head> - <body> - <noscript> - <strong>We're sorry but mobilizon doesn't work properly without JavaScript enabled. Please enable it to continue.</strong> - </noscript> - <div id="app"></div> - <!-- built files will be auto injected --> - </body> -</html> +<html class="has-navbar-fixed-top"> + +<head> + <meta charset="utf-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width,initial-scale=1.0"> + <link rel="icon" href="<%= BASE_URL %>favicon.ico"> + <link rel="stylesheet" href="//cdn.materialdesignicons.com/2.5.94/css/materialdesignicons.min.css"> + <title>mobilizon</title> +</head> + +<body> + <noscript> + <strong>We're sorry but mobilizon doesn't work properly without JavaScript enabled. Please enable it to continue.</strong> + </noscript> + <div id="app"></div> + <!-- built files will be auto injected --> +</body> + +</html> \ No newline at end of file diff --git a/js/src/App.vue b/js/src/App.vue index bd3b4b169..ac3070a21 100644 --- a/js/src/App.vue +++ b/js/src/App.vue @@ -1,152 +1,19 @@ <template> - <v-app id="libre-event"> - <v-navigation-drawer - light - clipped - fixed - app - v-model="drawer" - enable-resize-watcher - > - <v-list dense> - <v-list-group - value="false" - > - <v-list-tile avatar v-if="actor" slot="activator"> - <v-list-tile-avatar> - <img v-if="!actor.avatar" - class="img-circle elevation-7 mb-1" - src="https://picsum.photos/125/125/" - > - <img v-else - class="img-circle elevation-7 mb-1" - :src="actor.avatar" - > - </v-list-tile-avatar> - - <v-list-tile-content @click="$router.push({name: 'Account', params: { name: actor.username }})"> - <v-list-tile-title>{{ this.displayed_name }}</v-list-tile-title> - </v-list-tile-content> - </v-list-tile> - - <v-list-tile avatar v-if="actor"> - <v-list-tile-avatar> - <img - class="img-circle elevation-7 mb-1" - src="https://picsum.photos/125/125/" - > - </v-list-tile-avatar> - - <v-list-tile-content> - <v-list-tile-title>Autre identité</v-list-tile-title> - </v-list-tile-content> - </v-list-tile> - - <v-list-tile @click="$router.push({ name: 'Identities' })"> - <v-list-tile-action> - <v-icon>group</v-icon> - </v-list-tile-action> - <v-list-tile-content> - <v-list-tile-title>Identities</v-list-tile-title> - </v-list-tile-content> - </v-list-tile> - </v-list-group> - <template v-for="(item, i) in items" v-if="showMenuItem(item.role)"> - <v-layout - row - v-if="item.heading" - align-center - :key="i" - > - <v-flex xs6> - <v-subheader v-if="item.heading"> - {{ item.heading }} - </v-subheader> - </v-flex> - <v-flex xs6 class="text-xs-center"> - <a href="#!" class="body-2 black--text">EDIT</a> - </v-flex> - </v-layout> - <v-list-tile v-bind:key="item.route" v-else @click="$router.push({ name: item.route })"> - <v-list-tile-action> - <v-icon>{{ item.icon }}</v-icon> - </v-list-tile-action> - <v-list-tile-content> - <v-list-tile-title> - {{ item.text }} - </v-list-tile-title> - </v-list-tile-content> - </v-list-tile> - </template> - </v-list> - </v-navigation-drawer> - <NavBar v-bind="{toggleDrawer}"></NavBar> - <v-content> - <v-container fluid fill-height :class="{'px-0': $vuetify.breakpoint.xsOnly }"> - <v-layout xs12> - <transition name="router"> - <router-view></router-view> - </transition> - </v-layout> - </v-container> - </v-content> - <v-speed-dial - v-model="fab" - bottom - right - fixed - direction="top" - open-on-hover - transition="scale-transition" - v-if="currentUser" - > - <v-btn - slot="activator" - v-model="fab" - color="blue darken-2" - dark - fab - > - <v-icon>add</v-icon> - <v-icon>close</v-icon> - </v-btn> - <v-btn - fab - dark - small - color="pink" - @click="$router.push({name: 'CreateEvent'})" - > - <v-icon>event</v-icon> - </v-btn> - <v-btn - fab - dark - small - color="purple" - @click="$router.push({name: 'CreateGroup'})" - > - <v-icon>group</v-icon> - </v-btn> - </v-speed-dial> - <v-footer class="indigo" app> - <span - class="white--text" - v-translate="{ - date: new Date().getFullYear(), - }">© The Mobilizon Contributors %{date} - Made with Elixir, Phoenix & <a href="https://vuejs.org/">VueJS</a> & <a - href="https://www.vuetifyjs.com/">Vuetify</a> with some love and some weeks - </span> - </v-footer> - <v-snackbar - :timeout="error.timeout" - :error="true" - v-model="error.show" - > - {{ error.text }} - <v-btn dark flat @click.native="error.show = false">Close</v-btn> - </v-snackbar> - </v-app> + <div id="mobilizon"> + <NavBar></NavBar> + <main class="container"> + <router-view></router-view> + </main> + <footer class="footer"> + <div class="content has-text-centered"> + <span + v-translate="{ + date: new Date().getFullYear(), + }" + >© The Mobilizon Contributors %{date} - Made with Elixir, Phoenix, VueJS & with some love and some weeks</span> + </div> + </footer> + </div> </template> <script lang="ts"> @@ -163,31 +30,40 @@ import { ICurrentUser } from '@/types/current-user.model' } }, components: { - NavBar, - }, + NavBar + } }) export default class App extends Vue { drawer = false; fab = false; items = [ { - icon: 'poll', text: 'Events', route: 'EventList', role: null, + icon: "poll", + text: "Events", + route: "EventList", + role: null }, { - icon: 'group', text: 'Groups', route: 'GroupList', role: null, + icon: "group", + text: "Groups", + route: "GroupList", + role: null }, { - icon: 'content_copy', text: 'Categories', route: 'CategoryList', role: 'ROLE_ADMIN', + icon: "content_copy", + text: "Categories", + route: "CategoryList", + role: "ROLE_ADMIN" }, - { icon: 'settings', text: 'Settings', role: 'ROLE_USER' }, - { icon: 'chat_bubble', text: 'Send feedback', role: 'ROLE_USER' }, - { icon: 'help', text: 'Help', role: null }, - { icon: 'phonelink', text: 'App downloads', role: null }, + { icon: "settings", text: "Settings", role: "ROLE_USER" }, + { icon: "chat_bubble", text: "Send feedback", role: "ROLE_USER" }, + { icon: "help", text: "Help", role: null }, + { icon: "phonelink", text: "App downloads", role: null } ]; error = { timeout: 3000, show: false, - text: '', + text: "" }; currentUser!: ICurrentUser; @@ -199,21 +75,21 @@ export default class App extends Vue { get displayed_name () { // FIXME: load actor - return 'no implemented'; + return "no implemented"; // return this.actor.display_name === null ? this.actor.username : this.actor.display_name } - showMenuItem (elem) { + showMenuItem(elem) { // FIXME: load actor return false; // return elem !== null && this.user && this.user.roles !== undefined ? this.user.roles.includes(elem) : true } - getUser () { + getUser (): ICurrentUser|false { return this.currentUser.id ? this.currentUser : false; } - toggleDrawer () { + toggleDrawer() { this.drawer = !this.drawer; } @@ -236,16 +112,18 @@ export default class App extends Vue { </script> <style> - .router-enter-active, .router-leave-active { - transition-property: opacity; - transition-duration: .25s; - } +.router-enter-active, +.router-leave-active { + transition-property: opacity; + transition-duration: 0.25s; +} - .router-enter-active { - transition-delay: .25s; - } +.router-enter-active { + transition-delay: 0.25s; +} - .router-enter, .router-leave-active { - opacity: 0 - } +.router-enter, +.router-leave-active { + opacity: 0; +} </style> diff --git a/js/src/components/Account/Account.vue b/js/src/components/Account/Account.vue deleted file mode 100644 index 0f9b06a11..000000000 --- a/js/src/components/Account/Account.vue +++ /dev/null @@ -1,213 +0,0 @@ -<template> - <v-layout row> - <v-flex xs12 sm6 offset-sm3> - <v-progress-circular v-if="$apollo.loading" indeterminate color="primary"></v-progress-circular> - <v-card v-if="actor"> - <v-img :src="actor.banner || 'https://picsum.photos/400/'" height="300px"> - <v-layout column class="media"> - <v-card-title> - <v-btn icon @click="$router.go(-1)"> - <v-icon>chevron_left</v-icon> - </v-btn> - <v-spacer></v-spacer> - <!-- <v-btn icon class="mr-3" v-if="actor.id === actor.id"> - <v-icon>edit</v-icon> - </v-btn> --> - <v-menu bottom left> - <v-btn icon slot="activator"> - <v-icon>more_vert</v-icon> - </v-btn> - <v-list> - <!-- <v-list-tile @click="logoutUser()" v-if="actor.id === actor.id"> - <v-list-tile-title>User logout</v-list-tile-title> - </v-list-tile> - <v-list-tile @click="deleteAccount()" v-if="actor.id === actor.id"> - <v-list-tile-title>Delete</v-list-tile-title> - </v-list-tile> --> - </v-list> - </v-menu> - </v-card-title> - <v-spacer></v-spacer> - <div class="text-xs-center"> - <v-avatar size="125px"> - <img v-if="!actor.avatarUrl" - class="img-circle elevation-7 mb-1" - src="https://picsum.photos/125/125/" - > - <img v-else - class="img-circle elevation-7 mb-1" - :src="actor.avatarUrl" - > - </v-avatar> - </div> - <v-container fluid grid-list-lg> - <v-layout row> - <v-flex xs7> - <div class="headline">{{ actor.name }}</div> - <div><span class="subheading">@{{ actor.preferredUsername }}<span v-if="actor.domain">@{{ actor.domain }}</span></span> - </div> - <v-card-text v-if="actor.description" v-html="actor.description"></v-card-text> - </v-flex> - </v-layout> - </v-container> - </v-layout> - </v-img> - <v-list three-line> - <v-list-tile> - <v-list-tile-action> - <v-icon color="indigo">phone</v-icon> - </v-list-tile-action> - <v-list-tile-content> - <v-list-tile-title>(323) 555-6789</v-list-tile-title> - <v-list-tile-sub-title>Work</v-list-tile-sub-title> - </v-list-tile-content> - <v-list-tile-action> - <v-icon dark>chat</v-icon> - </v-list-tile-action> - </v-list-tile> - <v-divider inset></v-divider> - <v-list-tile> - <v-list-tile-action> - <v-icon color="indigo">mail</v-icon> - </v-list-tile-action> - <v-list-tile-content> - <v-list-tile-title>ali_connors@example.com</v-list-tile-title> - <v-list-tile-sub-title>Work</v-list-tile-sub-title> - </v-list-tile-content> - </v-list-tile> - <v-divider inset></v-divider> - <v-list-tile> - <v-list-tile-action> - <v-icon color="indigo">location_on</v-icon> - </v-list-tile-action> - <v-list-tile-content> - <v-list-tile-title>1400 Main Street</v-list-tile-title> - <v-list-tile-sub-title>Orlando, FL 79938</v-list-tile-sub-title> - </v-list-tile-content> - </v-list-tile> - </v-list> - <v-container fluid grid-list-md v-if="actor.participatingEvents && actor.participatingEvents.length > 0"> - <v-subheader>Participated at</v-subheader> - <v-layout row wrap> - <v-flex v-for="event in actor.participatingEvents" :key="event.id"> - <v-card> - <v-img - class="black--text" - height="200px" - src="https://picsum.photos/400/200/" - > - <v-container fill-height fluid> - <v-layout fill-height> - <v-flex xs12 align-end flexbox> - <span class="headline">{{ event.title }}</span> - </v-flex> - </v-layout> - </v-container> - </v-img> - <v-card-title> - <div> - <span class="grey--text">{{ event.startDate | formatDate }} à {{ event.location }}</span><br> - <p>{{ event.description }}</p> - <p v-if="event.organizer">Organisé par - <router-link :to="{name: 'Account', params: {'id': event.organizer.id}}">{{ event.organizer.username }}</router-link> - </p> - </div> - </v-card-title> - <v-card-actions> - <v-spacer></v-spacer> - <v-btn icon> - <v-icon>favorite</v-icon> - </v-btn> - <v-btn icon> - <v-icon>bookmark</v-icon> - </v-btn> - <v-btn icon> - <v-icon>share</v-icon> - </v-btn> - </v-card-actions> - </v-card> - </v-flex> - </v-layout> - </v-container> - <v-container fluid grid-list-md v-if="actor.organizedEvents && actor.organizedEvents.length > 0"> - <v-subheader>Organized events</v-subheader> - <v-layout row wrap> - <v-flex v-for="event in actor.organizedEvents" :key="event.id" md6> - <v-card> - <v-img - height="200px" - src="https://picsum.photos/400/200/" - /> - <v-card-title primary-title> - <div> - <router-link :to="{name: 'Event', params: {uuid: event.uuid}}"> - <div class="headline">{{ event.title }}</div> - </router-link> - <span class="grey--text" v-html="nl2br(event.description)"></span> - </div> - </v-card-title> - - <!-- <v-card-title> - <div> - <span class="grey--text" v-if="event.addressType === 'physical'">{{ event.startDate }} à {{ event.location }}</span><br> - <p v-if="event.organizer">Organisé par <router-link :to="{name: 'Account', params: {'id': event.organizer.id}}">{{ event.organizer.username }}</router-link></p> - </div> - </v-card-title> --> - <v-card-actions> - <v-spacer></v-spacer> - <v-btn icon> - <v-icon>favorite</v-icon> - </v-btn> - <v-btn icon> - <v-icon>bookmark</v-icon> - </v-btn> - <v-btn icon> - <v-icon>share</v-icon> - </v-btn> - </v-card-actions> - </v-card> - </v-flex> - </v-layout> - </v-container> - </v-card> - </v-flex> - </v-layout> -</template> - -<script lang="ts"> - import { FETCH_ACTOR } from '@/graphql/actor'; - import { Component, Prop, Vue, Watch } from 'vue-property-decorator'; - - @Component({ - apollo: { - actor: { - query: FETCH_ACTOR, - variables() { - return { - name: this.$route.params.name, - }; - }, - }, - }, - }) - export default class Account extends Vue { - @Prop({ type: String, required: true }) name!: string; - - actor = null; - - // call again the method if the route changes - @Watch('$route') - onRouteChange() { - // this.fetchData() - } - - logoutUser() { - // TODO : implement logout - this.$router.push({ name: 'Home' }); - } - - nl2br(text) { - return text.replace(/(?:\r\n|\r|\n)/g, '<br>'); - } - }; -</script> diff --git a/js/src/components/Account/Identities.vue b/js/src/components/Account/Identities.vue deleted file mode 100644 index 0c468c26a..000000000 --- a/js/src/components/Account/Identities.vue +++ /dev/null @@ -1,133 +0,0 @@ -<template> - <v-layout row> - <v-flex xs12 sm6 offset-sm3> - <v-progress-circular v-if="loading" indeterminate color="primary"></v-progress-circular> - <v-card v-if="!loading"> - <v-toolbar dark color="primary"> - <v-toolbar-title>Identities</v-toolbar-title> - </v-toolbar> - <v-card-text> - <v-list two-line> - <v-list-tile - v-for="actor in actors" - :key="actor.id" - avatar - @click="$router.push({ name: 'Account', params: { name: actor.username } })" - > - <v-list-tile-action> - <v-icon v-if="defaultActor === actor.username" color="pink">star</v-icon> - </v-list-tile-action> - - <v-list-tile-content> - <v-list-tile-title v-text="actor.username"></v-list-tile-title> - <v-list-tile-sub-title v-if="actor.display_name" v-text="actor.display_name"></v-list-tile-sub-title> - </v-list-tile-content> - - <v-list-tile-avatar> - <img :src="actor.avatar"> - </v-list-tile-avatar> - </v-list-tile> - </v-list> - <v-divider v-if="showForm"></v-divider> - <v-form v-if="showForm"> - <v-text-field - label="Username" - required - type="text" - v-model="newActor.preferred_username" - :rules="[rules.required]" - :error="this.state.username.status" - :error-messages="this.state.username.msg" - :suffix="this.host()" - hint="You will be able to create more identities once registered" - persistent-hint - > - </v-text-field> - <v-textarea - name="input-7-1" - label="Profile description" - hint="Will be displayed publicly on your profile" - ></v-textarea> - </v-form> - <v-btn - color="pink" - dark - absolute - bottom - right - fab - @click="toggleForm()" - > - <v-icon>{{ showForm ? 'check' : 'add' }}</v-icon> - </v-btn> - </v-card-text> - </v-card> - </v-flex> - </v-layout> -</template> - -<script lang="ts"> - import { Component, Vue } from 'vue-property-decorator'; - - @Component - export default class Identities extends Vue { - actors = []; - newActor = { - preferred_username: '', - summary: '', - }; - loading = true; - showForm = false; - rules = { - required: value => !!value || 'Required.', - }; - state = { - username: { - status: false, - msg: [], - }, - }; - - created() { - this.fetchData(); - } - - fetchData() { - // Implements eventFetch - // eventFetch('/user', this.$store) - // .then(response => response.json()) - // .then((response) => { - // this.actors = response.data.actors; - // this.loading = false; - // }); - } - - sendData() { - this.loading = true; - this.showForm = false; - - // Implements eventFetch - // eventFetch('/actors', this.$store, { - // method: 'POST', - // body: JSON.stringify({ actor: this.newActor }), - // }) - // .then(response => response.json()) - // .then((response) => { - // this.actors.push(response.data); - // this.loading = false; - // }); - } - - toggleForm() { - if (this.showForm === true) { - this.sendData(); - } else { - this.showForm = true; - } - } - - host() { - return `@${window.location.host}`; - } - } -</script> diff --git a/js/src/components/Account/Login.vue b/js/src/components/Account/Login.vue deleted file mode 100644 index f6b2b294d..000000000 --- a/js/src/components/Account/Login.vue +++ /dev/null @@ -1,151 +0,0 @@ -<template> - <v-container fluid fill-height> - <v-layout align-center justify-center> - <v-flex xs12 sm8 md4> - <v-card class="elevation-12"> - <v-toolbar dark color="primary"> - <v-toolbar-title>Login</v-toolbar-title> - <v-spacer></v-spacer> - <v-tooltip bottom> - <v-btn - slot="activator" - :to="{ name: 'Register', params: { email: this.credentials.email, password: this.credentials.password } }" - > - <!-- <v-icon large>login</v-icon> --> - <span>Register</span> - </v-btn> - <span>Register</span> - </v-tooltip> - </v-toolbar> - <v-card-text> - <div class="text-xs-center"> - <v-avatar size="80px"> - <transition name="avatar"> - <component :is="validEmail()" v-bind="{email: credentials.email}"></component> - <!-- <v-gravatar :email="credentials.email" default-img="mp" v-if="validEmail()"/> - <avatar v-else></avatar> --> - </transition> - </v-avatar> - </div> - <v-form @submit="loginAction" v-if="!validationSent"> - <v-text-field - label="Email" - required - type="text" - v-model="credentials.email" - :rules="[rules.required, rules.email]" - > - </v-text-field> - <v-text-field - label="password" - required - type="password" - v-model="credentials.password" - :rules="[rules.required]" - > - </v-text-field> - <v-btn @click="loginAction" color="blue">Login</v-btn> - <router-link :to="{ name: 'SendPasswordReset', params: { email: credentials.email } }">Password forgotten ?</router-link> - </v-form> - <div v-else> - <h2>{{ $t('registration.form.validation_sent', { email: credentials.email }) }}</h2> - <b-alert show variant="info">{{ $t('registration.form.validation_sent_info') }}</b-alert> - </div> - </v-card-text> - </v-card> - </v-flex> - </v-layout> - </v-container> -</template> - -<script lang="ts"> - - import Gravatar from 'vue-gravatar'; - import RegisterAvatar from './RegisterAvatar.vue'; - import { Component, Prop, Vue } from 'vue-property-decorator'; - import { LOGIN } from '@/graphql/auth'; - import { validateEmailField, validateRequiredField } from '@/utils/validators'; - import { saveUserData } from '@/utils/auth'; - import { ILogin } from '@/types/login.model' - import { UPDATE_CURRENT_USER_CLIENT } from '@/graphql/user' - import { onLogin } from '@/vue-apollo' - - @Component({ - components: { - 'v-gravatar': Gravatar, - avatar: RegisterAvatar, - }, - }) - export default class Login extends Vue { - @Prop({ type: String, required: false, default: '' }) email!: string; - @Prop({ type: String, required: false, default: '' }) password!: string; - - credentials = { - email: '', - password: '', - }; - validationSent = false; - error = { - show: false, - text: '', - timeout: 3000, - field: { - email: false, - password: false, - }, - }; - rules = { - required: validateRequiredField, - email: validateEmailField - }; - user: any; - - beforeCreate() { - if (this.user) { - this.$router.push('/'); - } - } - - mounted() { - this.credentials.email = this.email; - this.credentials.password = this.password; - } - - async loginAction(e: Event) { - e.preventDefault(); - this.error.show = false; - - try { - const result = await this.$apollo.mutate<{ login: ILogin }>({ - mutation: LOGIN, - variables: { - email: this.credentials.email, - password: this.credentials.password, - }, - }); - - saveUserData(result.data.login); - - await this.$apollo.mutate({ - mutation: UPDATE_CURRENT_USER_CLIENT, - variables: { - id: result.data.login.user.id, - email: this.credentials.email, - } - }); - - onLogin(this.$apollo); - - this.$router.push({ name: 'Home' }); - } catch (err) { - console.error(err); - this.error.show = true; - this.error.text = err.message; - } - } - - validEmail() { - return this.rules.email(this.credentials.email) === true ? 'v-gravatar' : 'avatar'; - } - } -</script> diff --git a/js/src/components/Account/PasswordReset.vue b/js/src/components/Account/PasswordReset.vue deleted file mode 100644 index 47ce03c7f..000000000 --- a/js/src/components/Account/PasswordReset.vue +++ /dev/null @@ -1,123 +0,0 @@ -<template> - <v-container fluid fill-height> - <v-layout align-center justify-center> - <v-flex xs12 sm8 md4> - <v-card class="elevation-12"> - <v-toolbar dark color="primary"> - <v-toolbar-title>Password Reset</v-toolbar-title> - </v-toolbar> - <v-card-text> - <v-alert type="error" :value="state.token.status === false">{{ state.token.msg }}</v-alert> - <v-form @submit="resetAction"> - <v-text-field - label="Password" - type="password" - v-model="credentials.password" - required - :error="state.password.status" - :rules="[rules.required, rules.password_length]" - > - </v-text-field> - <v-text-field - label="Password (confirmation)" - type="password" - v-model="credentials.password_confirmation" - required - :rules="[rules.required, rules.password_length, rules.password_equal]" - :error="state.password_confirmation.status" - > - </v-text-field> - <v-btn type="submit" :disabled="!samePasswords" color="blue">Reset my password</v-btn> - </v-form> - </v-card-text> - </v-card> - </v-flex> - </v-layout> - </v-container> -</template> - -<script lang="ts"> - - import { Component, Prop, Vue } from 'vue-property-decorator'; - import { validateRequiredField } from '@/utils/validators'; - import { RESET_PASSWORD } from '@/graphql/auth'; - import { saveUserData } from '@/utils/auth'; - import { ILogin } from '@/types/login.model' - - @Component - export default class PasswordReset extends Vue { - @Prop({ type: String, required: true }) token!: string; - - credentials = { - password: '', - password_confirmation: '', - }; - error = { - show: false, - }; - state = { - token: { - status: null, - msg: '', - }, - password: { - status: null, - msg: '', - }, - password_confirmation: { - status: null, - msg: '', - }, - }; - rules = { - password_length: value => value.length > 6 || 'Password must be at least 6 characters long', - required: validateRequiredField, - password_equal: value => value === this.credentials.password || 'Passwords must be the same', - }; - - get samePasswords() { - return this.rules.password_length(this.credentials.password) === true && - this.credentials.password === this.credentials.password_confirmation; - } - - async resetAction(e) { - this.resetState(); - this.error.show = false; - - e.preventDefault(); - - try { - const result = await this.$apollo.mutate<{ resetPassword: ILogin}>({ - mutation: RESET_PASSWORD, - variables: { - password: this.credentials.password, - token: this.token, - }, - }); - - saveUserData(result.data.resetPassword); - this.$router.push({ name: 'Home' }); - } catch (err) { - console.error(err); - this.error.show = true; - } - } - - resetState() { - this.state = { - token: { - status: null, - msg: '', - }, - password_confirmation: { - status: null, - msg: '', - }, - password: { - status: null, - msg: '', - }, - }; - } - }; -</script> diff --git a/js/src/components/Account/Register.vue b/js/src/components/Account/Register.vue deleted file mode 100644 index 114521dc7..000000000 --- a/js/src/components/Account/Register.vue +++ /dev/null @@ -1,185 +0,0 @@ -<template> - <v-container fluid fill-height> - <v-layout align-center justify-center> - <v-flex xs12 sm8 md4> - <v-card class="elevation-12"> - <v-toolbar dark color="primary"> - <v-toolbar-title>Register</v-toolbar-title> - <v-spacer></v-spacer> - <v-tooltip bottom> - <v-btn - slot="activator" - :to="{ name: 'Login', params: { email, password } }" - > - <!-- <v-icon large>login</v-icon> --> - <span>Login</span> - </v-btn> - <span>Login</span> - </v-tooltip> - </v-toolbar> - <v-card-text> - <div class="text-xs-center"> - <v-avatar size="80px"> - <transition name="avatar"> - <component :is="validEmail()" v-bind="{email}"></component> - <!-- <v-gravatar :email="credentials.email" default-img="mp" v-if="validEmail()"/> - <avatar v-else></avatar> --> - </transition> - </v-avatar> - </div> - <v-form @submit="submit()" v-if="!validationSent"> - <v-text-field - label="Username" - required - type="text" - v-model="username" - :rules="[rules.required]" - :error="state.username.status" - :error-messages="state.username.msg" - :suffix="host()" - hint="You will be able to create more identities once registered" - persistent-hint - > - </v-text-field> - <v-text-field - label="Email" - required - type="email" - ref="email" - v-model="email" - :rules="[rules.required, rules.email]" - :error="state.email.status" - :error-messages="state.email.msg" - > - </v-text-field> - <v-text-field - label="Password" - required - :type="showPassword ? 'text' : 'password'" - v-model="password" - :rules="[rules.required, rules.password_length]" - :error="state.password.status" - :error-messages="state.password.msg" - :append-icon="showPassword ? 'visibility_off' : 'visibility'" - @click:append="showPassword = !showPassword" - > - </v-text-field> - <v-btn @click="submit()" color="primary">Register</v-btn> - <router-link :to="{ name: 'ResendConfirmation', params: { email }}">Didn't receive the instructions ?</router-link> - </v-form> - <div v-if="validationSent"> - <h2> - <translate>A validation email was sent to %{email}</translate> - </h2> - <v-alert :value="true" type="info"> - <translate>Before you can login, you need to click on the link inside it to validate your account</translate> - </v-alert> - </div> - </v-card-text> - </v-card> - </v-flex> - </v-layout> - </v-container> -</template> - -<script lang="ts"> - import Gravatar from 'vue-gravatar'; - import RegisterAvatar from './RegisterAvatar.vue'; - import { CREATE_USER } from '@/graphql/user'; - import { Component, Prop, Vue } from 'vue-property-decorator'; - - @Component({ - components: { - 'v-gravatar': Gravatar, - avatar: RegisterAvatar, - }, - }) - export default class Register extends Vue { - @Prop({ type: String, required: false, default: '' }) default_email!: string; - @Prop({ type: String, required: false, default: '' }) default_password!: string; - - username = ''; - email = this.default_email; - password = this.default_password; - error = { - show: false, - }; - showPassword = false; - validationSent = false; - state = { - email: { - status: false, - msg: [], - }, - username: { - status: false, - msg: [], - }, - password: { - status: false, - msg: [], - }, - }; - rules = { - password_length: value => value.length > 6 || 'Password must be at least 6 characters long', - required: value => !!value || 'Required.', - email: (value: string) => value.includes('@') || 'Invalid e-mail.', - }; - - resetState() { - this.state = { - email: { - status: false, - msg: [], - }, - username: { - status: false, - msg: [], - }, - password: { - status: false, - msg: [], - }, - }; - } - - host() { - return `@${window.location.host}`; - } - - validEmail() { - return this.rules.email(this.email) === true ? 'v-gravatar' : 'avatar'; - } - - async submit() { - try { - await this.$apollo.mutate({ - mutation: CREATE_USER, - variables: { - email: this.email, - password: this.password, - username: this.username, - }, - }); - - this.validationSent = true; - } catch (error) { - console.error(error); - } - } - }; -</script> - -<style lang="scss"> - .avatar-enter-active { - transition: opacity 1s ease; - } - - .avatar-enter, .avatar-leave-to { - opacity: 0; - } - - .avatar-leave { - display: none; - } -</style> diff --git a/js/src/components/Account/RegisterAvatar.vue b/js/src/components/Account/RegisterAvatar.vue deleted file mode 100644 index db44bb544..000000000 --- a/js/src/components/Account/RegisterAvatar.vue +++ /dev/null @@ -1,12 +0,0 @@ -<template> - <img class="img-circle elevation-7 mb-1" src="@/assets/profile.svg"> -</template> - -<script lang="ts"> - import { Component, Vue } from 'vue-property-decorator'; - - @Component - export default class RegisterAvatar extends Vue { - } -</script> - diff --git a/js/src/components/Account/ResendConfirmation.vue b/js/src/components/Account/ResendConfirmation.vue deleted file mode 100644 index 886d28096..000000000 --- a/js/src/components/Account/ResendConfirmation.vue +++ /dev/null @@ -1,82 +0,0 @@ -<template> - <v-container fluid fill-height> - <v-layout align-center justify-center> - <v-flex xs12 sm8 md4> - <v-card class="elevation-12"> - <v-toolbar dark color="primary"> - <v-toolbar-title>Resend Instructions</v-toolbar-title> - </v-toolbar> - <v-card-text> - <v-form @submit="resendConfirmationAction" v-if="!validationSent"> - <v-text-field - label="Email" - type="email" - v-model="credentials.email" - required - :state="state.email.status" - :rules="[rules.required, rules.email]" - > - </v-text-field> - <v-btn type="submit" color="blue">Send instructions again</v-btn> - </v-form> - <div v-else> - <h2>Validation email sent to {{ credentials.email }}</h2> - <v-alert :value="true" type="info">Please check you spam folder if you didn't receive the email.</v-alert> - </div> - </v-card-text> - </v-card> - </v-flex> - </v-layout> - </v-container> -</template> - -<script lang="ts"> - import { Component, Prop, Vue } from 'vue-property-decorator'; - import { validateEmailField, validateRequiredField } from '@/utils/validators'; - import { RESEND_CONFIRMATION_EMAIL } from '@/graphql/auth'; - - @Component - export default class ResendConfirmation extends Vue { - @Prop({ type: String, required: false, default: '' }) email!: string; - - credentials = { - email: '', - }; - validationSent = false; - error = false; - state = { - email: { - status: null, - msg: '', - }, - }; - rules = { - required: validateRequiredField, - email: validateEmailField, - }; - - mounted() { - this.credentials.email = this.email; - } - - async resendConfirmationAction(e) { - e.preventDefault(); - this.error = false; - - try { - await this.$apollo.mutate({ - mutation: RESEND_CONFIRMATION_EMAIL, - variables: { - email: this.credentials.email, - }, - }); - - } catch (err) { - console.error(err); - this.error = true; - } finally { - this.validationSent = true; - } - } - }; -</script> diff --git a/js/src/components/Account/SendPasswordReset.vue b/js/src/components/Account/SendPasswordReset.vue deleted file mode 100644 index b71fef5e4..000000000 --- a/js/src/components/Account/SendPasswordReset.vue +++ /dev/null @@ -1,92 +0,0 @@ -<template> - <v-container fluid fill-height> - <v-layout align-center justify-center> - <v-flex xs12 sm8 md4> - <v-card class="elevation-12"> - <v-toolbar dark color="primary"> - <v-toolbar-title>Password Reset</v-toolbar-title> - </v-toolbar> - <v-card-text> - <v-form @submit="resendConfirmationAction" v-if="!validationSent"> - <v-text-field - label="Email" - type="email" - v-model="credentials.email" - required - :state="state.email.status" - :rules="[rules.required, rules.email]" - > - </v-text-field> - <v-btn type="submit" color="blue">Reset my password</v-btn> - </v-form> - <div v-else> - <h2>Validation email sent to {{ credentials.email }}</h2> - <v-alert :value="true" type="info">Please check you spam folder if you didn't receive the email.</v-alert> - </div> - </v-card-text> - </v-card> - </v-flex> - </v-layout> - </v-container> -</template> - -<script lang="ts"> - import { Component, Prop, Vue } from 'vue-property-decorator'; - import { validateEmailField, validateRequiredField } from '@/utils/validators'; - import { SEND_RESET_PASSWORD } from '@/graphql/auth'; - - @Component - export default class SendPasswordReset extends Vue { - @Prop({ type: String, required: false, default: '' }) email!: string; - - credentials = { - email: '', - }; - validationSent = false; - error = false; - state = { - email: { - status: null, - msg: '', - } as { status: boolean | null, msg: string }, - }; - - rules = { - required: validateRequiredField, - email: validateEmailField, - }; - - mounted() { - this.credentials.email = this.email; - } - - async resendConfirmationAction(e) { - e.preventDefault(); - this.error = false; - - try { - await this.$apollo.mutate({ - mutation: SEND_RESET_PASSWORD, - variables: { - email: this.credentials.email, - }, - }); - - this.validationSent = true; - } catch (err) { - console.error(err); - this.error = true; - this.state.email = { status: false, msg: err.errors }; - } - } - - resetState() { - this.state = { - email: { - status: null, - msg: '', - }, - }; - } - }; -</script> diff --git a/js/src/components/Account/Validate.vue b/js/src/components/Account/Validate.vue deleted file mode 100644 index a9d2a1f72..000000000 --- a/js/src/components/Account/Validate.vue +++ /dev/null @@ -1,60 +0,0 @@ -<template> - <v-container> - <h1 v-if="loading"> - <translate>Your account is being validated</translate> - </h1> - <div v-else> - <div v-if="failed"> - <v-alert :value="true" variant="danger"> - <translate>Error while validating account</translate> - </v-alert> - </div> - <h1 v-else> - <translate>Your account has been validated</translate> - </h1> - </div> - </v-container> -</template> - -<script lang="ts"> - import { VALIDATE_USER } from '@/graphql/user'; - import { Component, Prop, Vue } from 'vue-property-decorator'; - import { AUTH_TOKEN, AUTH_USER_ID } from '@/constants'; - - @Component - export default class Validate extends Vue { - @Prop({ type: String, required: true }) token!: string; - - loading = true; - failed = false; - - created() { - this.validateAction(); - } - - async validateAction() { - try { - const data = await this.$apollo.mutate({ - mutation: VALIDATE_USER, - variables: { - token: this.token, - }, - }); - - this.saveUserData(data.data); - this.$router.push({ name: 'Home' }); - } catch (err) { - console.error(err); - this.failed = true; - } finally { - this.loading = false; - } - } - - saveUserData({ validateUser: login }) { - localStorage.setItem(AUTH_USER_ID, login.user.id); - localStorage.setItem(AUTH_TOKEN, login.token); - } - - }; -</script> diff --git a/js/src/components/Category/Create.vue b/js/src/components/Category/Create.vue deleted file mode 100644 index c36ddec53..000000000 --- a/js/src/components/Category/Create.vue +++ /dev/null @@ -1,93 +0,0 @@ -<template> - <v-container fluid fill-height> - <v-layout align-center justify-center> - <v-flex xs12 sm8 md4> - <v-card class="elevation-12"> - <v-toolbar dark color="primary"> - <v-toolbar-title> - <translate>Create a new category</translate> - </v-toolbar-title> - </v-toolbar> - <v-card-text> - <v-form> - <v-text-field - :label="$gettext('Name of the category')" - v-model="title" - :counter="100" - required - ></v-text-field> - <v-textarea - :label="$gettext('Description')" - v-model="description" - ></v-textarea> - <v-flex xs12 class="text-xs-center text-sm-center text-md-center text-lg-center"> - <v-img :src="image.url" height="150" v-if="image.url" aspect-ratio="1" contain/> - <v-text-field label="Select Image" @click='pickFile' v-model='image.name' prepend-icon='attach_file'></v-text-field> - <input - type="file" - style="display: none" - ref="image" - accept="image/*" - @change="onFilePicked" - > - </v-flex> - <v-btn color="primary" @click="create"> - <translate>Create category</translate> - </v-btn> - </v-form> - </v-card-text> - </v-card> - </v-flex> - </v-layout> - </v-container> -</template> - -<script lang="ts"> - import { CREATE_CATEGORY } from '@/graphql/category'; - import { Component, Vue } from 'vue-property-decorator'; - - @Component - export default class CreateCategory extends Vue { - title = ''; - description = ''; - image = { - url: '', - name: '', - file: '', - }; - - create() { - this.$apollo.mutate({ - mutation: CREATE_CATEGORY, - variables: { - title: this.title, - description: this.description, - picture: (this.$refs['image'] as any).files[ 0 ], - }, - }).then((data) => { - console.log(data); - }).catch((error) => { - console.error(error); - }); - } - - pickFile() { - (this.$refs['image'] as any).click(); - } - - onFilePicked(e) { - const files = e.target.files; - if (files[ 0 ] === undefined || files[ 0 ].name.lastIndexOf('.') <= 0) { - console.error('File is incorrect'); - } - this.image.name = files[ 0 ].name; - } - - }; -</script> - -<style> - .markdown-render h1 { - font-size: 2em; - } -</style> diff --git a/js/src/components/Category/List.vue b/js/src/components/Category/List.vue deleted file mode 100644 index 072f48082..000000000 --- a/js/src/components/Category/List.vue +++ /dev/null @@ -1,70 +0,0 @@ -<template> - <v-container> - <h1>Category List</h1> - <v-container fluid grid-list-md class="grey lighten-4"> - <v-progress-circular v-if="$apollo.loading" indeterminate color="primary"></v-progress-circular> - <v-layout row wrap v-else> - <v-flex xs12 sm6 md3 v-for="category in categories" :key="category.id"> - <v-card> - <v-img v-if="category.picture.url" :src="HTTP_ENDPOINT + category.picture.url" height="200px"> - </v-img> - <v-card-title primary-title> - <div> - <h3 class="headline mb-0">{{ category.title }}</h3> - <div>{{ category.description }}</div> - </div> - </v-card-title> - <v-card-actions> - <v-btn flat class="orange--text"> - <translate>Explore</translate> - </v-btn> - <v-btn flat class="red--text" v-on:click="deleteCategory(category.id)"> - <translate>Delete</translate> - </v-btn> - </v-card-actions> - </v-card> - </v-flex> - <v-layout v-if="categories.length <= 0"> - <h3>No categories :(</h3> - </v-layout> - </v-layout> - </v-container> - - <router-link :to="{ name: 'CreateCategory' }" class="btn btn-default">Create</router-link> - </v-container> -</template> - -<script lang="ts"> - import { FETCH_CATEGORIES } from '@/graphql/category'; - import { Component, Vue } from 'vue-property-decorator'; - - // TODO : remove this hardcode - - @Component({ - apollo: { - categories: { - query: FETCH_CATEGORIES, - }, - }, - }) - export default class List extends Vue { - categories = []; - loading = true; - HTTP_ENDPOINT = 'http://localhost:4000'; - - deleteCategory(categoryId) { - const router = this.$router; - // FIXME: remove eventFetch - // eventFetch(`/categories/${categoryId}`, this.$store, { method: 'DELETE' }) - // .then(() => { - // this.categories = this.categories.filter(category => category.id !== categoryId); - // router.push('/category'); - // }); - } - }; -</script> - -<!-- Add "scoped" attribute to limit CSS to this component only --> -<style scoped> - -</style> diff --git a/js/src/components/Event/Create.vue b/js/src/components/Event/Create.vue deleted file mode 100644 index f24f5b4de..000000000 --- a/js/src/components/Event/Create.vue +++ /dev/null @@ -1,193 +0,0 @@ -<template> - <v-container fluid fill-height> - <v-layout align-center justify-center> - <v-flex xs12 sm8 md4> - <v-card class="elevation-12"> - <v-toolbar dark color="primary"> - <v-toolbar-title>Create a new event</v-toolbar-title> - </v-toolbar> - <v-card-text> - <v-form> - <v-text-field label="Title" v-model="event.title" :counter="100" required></v-text-field> - <v-date-picker v-model="event.begins_on"></v-date-picker> - <v-radio-group v-model="event.location_type" row> - <v-radio label="Address" value="physical" off-icon="place"></v-radio> - <v-radio label="Online" value="online" off-icon="link"></v-radio> - <v-radio label="Phone" value="phone" off-icon="phone"></v-radio> - <v-radio label="Other" value="other"></v-radio> - </v-radio-group> - <!-- <vuetify-google-autocomplete - v-if="event.location_type === 'physical'" - id="map" - append-icon="search" - classname="form-control" - placeholder="Start typing" - label="Location" - enable-geolocation - types="geocode" - v-on:placechanged="getAddressData" - > - </vuetify-google-autocomplete>--> - <v-text-field - v-if="event.location_type === 'online'" - label="Meeting adress" - type="url" - v-model="event.url" - :required="event.location_type === 'online'" - ></v-text-field> - <v-text-field - v-if="event.location_type === 'phone'" - label="Phone number" - type="tel" - v-model="event.phone" - :required="event.location_type === 'phone'" - ></v-text-field> - <v-autocomplete - :items="categories" - v-model="event.category" - item-text="title" - item-value="id" - label="Categories" - ></v-autocomplete> - <v-btn color="primary" @click="create">Create event</v-btn> - </v-form> - </v-card-text> - </v-card> - </v-flex> - </v-layout> - </v-container> -</template> - -<script lang="ts"> -// import Location from '@/components/Location'; -import VueMarkdown from "vue-markdown"; -import { CREATE_EVENT, EDIT_EVENT } from "@/graphql/event"; -import { FETCH_CATEGORIES } from "@/graphql/category"; -import { AUTH_USER_ACTOR } from "@/constants"; -import { Component, Prop, Vue } from "vue-property-decorator"; - -@Component({ - components: { - VueMarkdown - }, - apollo: { - categories: { - query: FETCH_CATEGORIES - } - } -}) -export default class CreateEvent extends Vue { - @Prop({ required: false, type: String }) uuid!: string; - - e1 = 0; - event = { - title: null, - organizer_actor_id: null, - description: "", - begins_on: new Date().toISOString().substr(0, 10), - ends_on: new Date(), - seats: null, - physical_address: null, - location_type: "physical", - online_address: null, - tel_num: null, - price: null, - category: null, - category_id: null, - tags: [], - participants: [] - } as any; // FIXME: correctly type an event - categories = []; - tags = []; - tagsToSend = []; - tagsFetched = []; - loading = false; - - // created() { - // if (this.uuid) { - // this.fetchEvent(); - // } - // } - - create() { - // this.event.seats = parseInt(this.event.seats, 10); - // this.tagsToSend.forEach((tag) => { - // this.event.tags.push({ - // title: tag, - // // '@type': 'Tag', - // }); - // }); - // FIXME: correctly parse actor JSON - const actor = JSON.parse(localStorage.getItem(AUTH_USER_ACTOR) || "{}"); - this.event.category_id = this.event.category; - this.event.organizer_actor_id = actor.id; - this.event.participants = [actor.id]; - // this.event.price = parseFloat(this.event.price); - - if (this.uuid === undefined) { - this.$apollo - .mutate({ - mutation: CREATE_EVENT, - variables: { - title: this.event.title, - description: this.event.description, - organizerActorId: this.event.organizer_actor_id, - categoryId: this.event.category_id, - beginsOn: this.event.begins_on - } - }) - .then(data => { - this.loading = false; - this.$router.push({ - name: "Event", - params: { uuid: data.data.uuid } - }); - }) - .catch(error => { - console.log(error); - }); - } else { - this.$apollo - .mutate({ - mutation: EDIT_EVENT - }) - .then(data => { - this.loading = false; - this.$router.push({ - name: "Event", - params: { uuid: data.data.uuid } - }); - }) - .catch(error => { - console.log(error); - }); - } - this.event.tags = []; - } - - getAddressData(addressData) { - if (addressData !== null) { - this.event.address = { - geom: { - data: { - latitude: addressData.latitude, - longitude: addressData.longitude - }, - type: "point" - }, - addressCountry: addressData.country, - addressLocality: addressData.locality, - addressRegion: addressData.administrative_area_level_1, - postalCode: addressData.postal_code, - streetAddress: `${addressData.street_number} ${addressData.route}` - }; - } - } -} -</script> - -<style> -.markdown-render h1 { - font-size: 2em; -} -</style> diff --git a/js/src/components/Event/Edit.vue b/js/src/components/Event/Edit.vue deleted file mode 100644 index cb23cc4ed..000000000 --- a/js/src/components/Event/Edit.vue +++ /dev/null @@ -1,125 +0,0 @@ -<template> - <v-container fluid grid-list-md> - <h3>Update event {{ event.title }}</h3> - <v-progress-circular v-if="loading" indeterminate color="primary"></v-progress-circular> - <v-form v-if="!loading"> - <v-stepper v-model="e1" vertical> - <v-stepper-step step="1" :complete="e1 > 1">Basic Informations - <small>Title and description</small> - </v-stepper-step> - <v-stepper-content step="1"> - <v-layout row wrap> - <v-flex xs12> - <v-text-field - label="Title" - v-model="event.title" - :counter="100" - required - ></v-text-field> - </v-flex> - <v-flex md6> - <v-text-field - label="Description" - v-model="event.description" - multiLine - required - ></v-text-field> - </v-flex> - <v-flex md6> - <vue-markdown class="markdown-render" - :watches="['show','html','breaks','linkify','emoji','typographer','toc']" - :source="event.description" - :show="true" :html="false" :breaks="true" :linkify="true" - :emoji="true" :typographer="true" :toc="false" - ></vue-markdown> - </v-flex> - <v-flex md12> - <v-select - v-bind:items="categories" - v-model="event.category" - item-text="name" - item-value="@id" - label="Categories" - single-line - bottom - ></v-select> - </v-flex> - <v-flex md12> - <!--<v-text-field - v-model="tagsToSend" - label="Tags" - ></v-text-field>--> - <v-select - v-model="tagsToSend" - label="Tags" - chips - tags - :items="tagsFetched" - ></v-select> - </v-flex> - </v-layout> - <v-btn color="primary" @click.native="e1 = 2">Next</v-btn> - </v-stepper-content> - <v-stepper-step step="2" :complete="e1 > 2">Date and place</v-stepper-step> - <v-stepper-content step="2"> - Event starts at: - <v-text-field type="datetime-local" v-model="event.startDate"></v-text-field> - Event ends at: - <v-text-field type="datetime-local" v-model="event.endDate"></v-text-field> - - <vuetify-google-autocomplete - id="map" - append-icon="search" - placeholder="Start typing" - label="Location" - enable-geolocation - v-on:placechanged="getAddressData" - > - </vuetify-google-autocomplete> - <v-btn color="primary" @click.native="e1 = 3">Next</v-btn> - </v-stepper-content> - <v-stepper-step step="3" :complete="e1 > 3">Extra informations</v-stepper-step> - <v-stepper-content step="3"> - <v-text-field - label="Number of seats" - v-model="event.seats" - ></v-text-field> - <v-text-field - label="Price" - prefix="$" - type="float" - v-model="event.price" - ></v-text-field> - </v-stepper-content> - </v-stepper> - </v-form> - <v-btn color="primary" @click="create">Create event</v-btn> - </v-container> -</template> - -<script lang="ts"> - import { Component, Prop, Vue } from 'vue-property-decorator'; - - @Component - export default class EventEdit extends Vue { - @Prop(String) id!: string; - - loading = true; - event = null; - - created() { - this.fetchData(); - } - - fetchData() { - // FIXME: remove eventFetch - // eventFetch(`/events/${this.id}`, this.$store) - // .then(response => response.json()) - // .then((data) => { - // this.loading = false; - // this.event = data; - // console.log(this.event); - // }); - } - }; -</script> diff --git a/js/src/components/Event/Event.vue b/js/src/components/Event/Event.vue deleted file mode 100644 index a7f443823..000000000 --- a/js/src/components/Event/Event.vue +++ /dev/null @@ -1,245 +0,0 @@ -<template> - <v-layout row> - <v-flex xs12 sm6 offset-sm3> - <v-progress-circular v-if="$apollo.loading" indeterminate color="primary"></v-progress-circular> - <div>{{ event }}</div> - <v-card v-if="event"> - <!-- <v-img - src="https://picsum.photos/600/400/" - height="200px" - > - <v-container fill-height fluid> - <v-layout fill-height> - <v-flex xs12 align-end flexbox> - <v-card-title> - <v-btn icon @click="$router.go(-1)" class="white--text"> - <v-icon>chevron_left</v-icon> - </v-btn> - <v-spacer></v-spacer> - <v-btn icon class="mr-3 white--text" v-if="actorIsOrganizer()" :to="{ name: 'EditEvent', params: {uuid: event.uuid}}"> - <v-icon>edit</v-icon> - </v-btn> - <v-menu bottom left> - <v-btn icon slot="activator" class="white--text"> - <v-icon>more_vert</v-icon> - </v-btn> - <v-list> - <v-list-tile @click="downloadIcsEvent()"> - <v-list-tile-title>Download</v-list-tile-title> - </v-list-tile> - <v-list-tile @click="deleteEvent()" v-if="actorIsOrganizer()"> - <v-list-tile-title>Delete</v-list-tile-title> - </v-list-tile> - </v-list> - </v-menu> - </v-card-title> - </v-flex> - </v-layout> - </v-container> - </v-img> --> - <v-container grid-list-md> - <v-layout row wrap> - <v-flex md10> - <v-spacer></v-spacer> - <span class="subheading grey--text">{{ event.begins_on | formatDay }}</span> - <h1 class="display-1">{{ event.title }}</h1> - <div> - <!-- <router-link :to="{name: 'Account', params: { name: event.organizerActor.preferredUsername } }"> - <v-avatar size="25px"> - <img class="img-circle elevation-7 mb-1" - :src="event.organizer_actor.avatarUrl" - > - </v-avatar> - </router-link> --> - <!-- <span v-if="event.organizerActor">Organisé par {{ event.organizerActor.name ? event.organizerActor.name : event.organizerActor.preferredUsername }}</span> --> - </div> - <!-- <p><router-link :to="{ name: 'Account', params: {id: event.organizer.id} }"><span class="grey--text">{{ event.organizer.username }}</span></router-link> organises {{ event.title }} <span v-if="event.address.addressLocality">in {{ event.address.addressLocality }}</span> on the {{ event.startDate | formatDate }}.</p> --> - <v-card-text v-if="event.description"> - <vue-markdown :source="event.description"></vue-markdown> - </v-card-text> - </v-flex> - <!-- <v-flex md2> - <p v-if="actorIsOrganizer()"> - Vous êtes organisateur de cet événement. - </p> - <div v-else> - <p v-if="actorIsParticipant()"> - Vous avez annoncé aller à cet événement. - </p> - <p v-else>Vous y allez ? - <span class="text--darken-2 grey--text">{{ event.participants.length }} personnes y vont.</span> - </p> - </div> - <v-card-actions v-if="!actorIsOrganizer()"> - <v-btn v-if="!actorIsParticipant()" @click="joinEvent" color="success"><v-icon>check</v-icon> Join</v-btn> - <v-btn v-if="actorIsParticipant()" @click="leaveEvent" color="error">Leave</v-btn> - </v-card-actions> - </v-flex> --> - </v-layout> - </v-container> - <v-divider></v-divider> - <v-container> - <v-layout row wrap> - <v-flex xs12 md4 order-md1> - <v-layout - column - fill-height - > - <v-list two-line> - <v-list-tile> - <v-list-tile-action> - <v-icon color="indigo">access_time</v-icon> - </v-list-tile-action> - - <v-list-tile-content> - <v-list-tile-title>{{ event.begins_on | formatDate }}</v-list-tile-title> - <v-list-tile-sub-title>{{ event.ends_on | formatDate }}</v-list-tile-sub-title> - </v-list-tile-content> - </v-list-tile> - - <v-divider inset></v-divider> - - <v-list-tile> - <v-list-tile-action> - <v-icon color="indigo">place</v-icon> - </v-list-tile-action> - - <v-list-tile-content> - <v-list-tile-title> - {{ event.physical_address.streetAddress }} - </v-list-tile-title> - <v-list-tile-sub-title>Mobile</v-list-tile-sub-title> - </v-list-tile-content> - </v-list-tile> - </v-list> - </v-layout> - </v-flex> - <v-flex md8 xs12> - <p> - <h2>Details</h2> - <vue-markdown :source="event.description" v-if="event.description" :toc-first-level="3"></vue-markdown> - </p> - <v-subheader>Participants</v-subheader> - <!-- <v-flex md2 v-for="participant in event.participants" :key="participant.actor.uuid"> - <router-link :to="{name: 'Account', params: { name: participant.actor.preferredUsername }}"> - <v-card> - <v-avatar size="75px"> - <img v-if="!participant.actor.avatarUrl" - class="img-circle elevation-7 mb-1" - src="https://picsum.photos/125/125/" - > - <img v-else - class="img-circle elevation-7 mb-1" - :src="participant.actor.avatarUrl" - > - </v-avatar> - <v-card-title> - <span>{{ participant.actor.preferredUsername }}</span> - </v-card-title> - </v-card> - </router-link> - </v-flex> --> - </v-flex> - <span v-if="event.participants.length === 0">No participants yet.</span> - </v-layout> - </v-container> - </v-card> - </v-flex> - </v-layout> -</template> - -<script lang="ts"> - import { FETCH_EVENT } from '@/graphql/event'; - import { Component, Prop, Vue } from 'vue-property-decorator'; - import VueMarkdown from 'vue-markdown'; - - @Component({ - components: { - VueMarkdown, - }, - apollo: { - event: { - query: FETCH_EVENT, - variables() { - return { - uuid: this.uuid, - }; - }, - }, - // loggedActor: { - // query: LOGGED_ACTOR, - // } - }, - }) - export default class Event extends Vue { - @Prop({ type: String, required: true }) uuid!: string; - - event = { - name: '', - slug: '', - title: '', - uuid: this.uuid, - description: '', - organizer: { - id: null, - username: null, - }, - participants: [], - }; - - deleteEvent() { - const router = this.$router; - // FIXME: remove eventFetch - // eventFetch(`/events/${this.uuid}`, this.$store, { method: 'DELETE' }) - // .then(() => router.push({ name: 'EventList' })); - } - - joinEvent() { - // FIXME: remove eventFetch - // eventFetch(`/events/${this.uuid}/join`, this.$store, { method: 'POST' }) - // .then(response => response.json()) - // .then((data) => { - // console.log(data); - // }); - } - - leaveEvent() { - // FIXME: remove eventFetch - // eventFetch(`/events/${this.uuid}/leave`, this.$store) - // .then(response => response.json()) - // .then((data) => { - // console.log(data); - // }); - } - - downloadIcsEvent() { - // FIXME: remove eventFetch - // eventFetch(`/events/${this.uuid}/ics`, this.$store, { responseType: 'arraybuffer' }) - // .then(response => response.text()) - // .then((response) => { - // const blob = new Blob([ response ], { type: 'text/calendar' }); - // const link = document.createElement('a'); - // link.href = window.URL.createObjectURL(blob); - // link.download = `${this.event.title}.ics`; - // document.body.appendChild(link); - // link.click(); - // document.body.removeChild(link); - // }); - } - - // actorIsParticipant() { - // return this.loggedActor && this.event.participants.map(participant => participant.actor.preferredUsername).includes(this.loggedActor.preferredUsername) || this.actorIsOrganizer(); - // } - // - // actorIsOrganizer() { - // return this.loggedActor && this.loggedActor.preferredUsername === this.event.organizer.preferredUsername; - // } - } -</script> - -<!-- Add "scoped" attribute to limit CSS to this component only --> -<style> - .v-card__media__background { - filter: contrast(0.4); - } -</style> diff --git a/js/src/components/Event/EventCard.vue b/js/src/components/Event/EventCard.vue new file mode 100644 index 000000000..04244a6f0 --- /dev/null +++ b/js/src/components/Event/EventCard.vue @@ -0,0 +1,44 @@ +<template> + <div class="card"> + <div class="card-image" v-if="!event.image"> + <figure class="image is-4by3"> + <img src="https://picsum.photos/g/400/200/"> + </figure> + </div> + <div class="card-content"> + <div class="content"> + <router-link :to="{ name: 'Event', params:{ uuid: event.uuid } }"> + <h2 class="title">{{ event.title }}</h2> + </router-link> + <span>{{ event.begins_on | formatDay }}</span> + </div> + <div v-if="!hideDetails"> + <div v-if="event.participants.length === 1"> + <translate + :translate-params="{name: event.participants[0].actor.preferredUsername}" + >%{name} organizes this event</translate> + </div> + <div v-else> + <span v-for="participant in event.participants" :key="participant.actor.uuid"> + {{ participant.actor.preferredUsername }} + <span v-if="participant.role === 4">(organizer)</span>, + <!-- <translate + :translate-params="{name: participant.actor.preferredUsername}" + > %{name} is in,</translate>--> + </span> + </div> + </div> + </div> + </div> +</template> + +<script lang="ts"> +import { IEvent } from "@/types/event.model"; +import { Component, Prop, Vue } from "vue-property-decorator"; + +@Component +export default class EventCard extends Vue { + @Prop({ required: true }) event!: IEvent; + @Prop({ default: false }) hideDetails!: boolean; +} +</script> diff --git a/js/src/components/Event/EventList.vue b/js/src/components/Event/EventList.vue deleted file mode 100644 index 8e3ba0408..000000000 --- a/js/src/components/Event/EventList.vue +++ /dev/null @@ -1,150 +0,0 @@ -<template> - <v-layout> - <v-flex xs12 sm8 offset-sm2> - <v-card> - <h1>{{ $t('event.list.title') }}</h1> - - <v-progress-circular v-if="loading" indeterminate color="primary"></v-progress-circular> - <v-chip close v-model="locationChip" label color="pink" text-color="white" v-if="$router.currentRoute.params.location"> - <v-icon left>location_city</v-icon> - {{ locationText }} - </v-chip> - <v-container grid-list-sm fluid> - <v-layout row wrap> - <v-flex xs4 v-for="event in events" :key="event.id"> - <v-card> - <v-card-media v-if="!event.image" - class="white--text" - height="200px" - src="https://picsum.photos/g/400/200/" - > - <v-container fill-height fluid> - <v-layout fill-height> - <v-flex xs12 align-end flexbox> - <span class="headline black--text">{{ event.title }}</span> - </v-flex> - </v-layout> - </v-container> - </v-card-media> - <v-card-title primary-title> - <div> - <span class="grey--text">{{ event.begins_on | formatDate }}</span><br> - <router-link :to="{name: 'Account', params: { name: event.organizer.username } }"> - <v-avatar size="25px"> - <img class="img-circle elevation-7 mb-1" - :src="event.organizer.avatar" - > - </v-avatar> - </router-link> - <span v-if="event.organizer">Organisé par <router-link - :to="{name: 'Account', params: {'name': event.organizer.username}}">{{ event.organizer.username }}</router-link></span> - </div> - </v-card-title> - <v-card-actions> - <v-btn flat color="orange" @click="downloadIcsEvent(event)">Share</v-btn> - <v-btn flat color="orange" @click="viewEvent(event)">Explore</v-btn> - <v-btn flat color="red" @click="deleteEvent(event)">Delete</v-btn> - </v-card-actions> - </v-card> - </v-flex> - </v-layout> - </v-container> - <router-link :to="{ name: 'CreateEvent' }" class="btn btn-default">Create</router-link> - </v-card> - </v-flex> - </v-layout> -</template> - -<script lang="ts"> - import ngeohash from 'ngeohash'; - import VueMarkdown from 'vue-markdown'; - import VCardTitle from 'vuetify/es5/components/VCard/VCardTitle'; - import { Component, Prop, Vue, Watch } from 'vue-property-decorator'; - - @Component({ - components: { - VCardTitle: VCardTitle as any, - VueMarkdown, - }, - }) - export default class EventList extends Vue { - @Prop(String) location!: string; - - events = []; - loading = true; - locationChip = false; - locationText = ''; - - created() { - this.fetchData(this.$router.currentRoute.params[ 'location' ]); - } - - beforeRouteUpdate(to, from, next) { - this.fetchData(to.params.location); - next(); - } - - @Watch('locationChip') - onLocationChipChange(val) { - if (val === false) { - this.$router.push({ name: 'EventList' }); - } - } - - geocode(lat, lon) { - console.log({ lat, lon }); - console.log(ngeohash.encode(lat, lon, 10)); - return ngeohash.encode(lat, lon, 10); - } - - fetchData(location) { - let queryString = '/events'; - if (location) { - queryString += (`?geohash=${location}`); - const { latitude, longitude } = ngeohash.decode(location); - this.locationText = `${latitude.toString()} : ${longitude.toString()}`; - } - this.locationChip = true; - // FIXME: remove eventFetch - // eventFetch(queryString, this.$store) - // .then(response => response.json()) - // .then((response) => { - // this.loading = false; - // this.events = response.data; - // console.log(this.events); - // }); - } - - deleteEvent(event) { - const router = this.$router; - // FIXME: remove eventFetch - // eventFetch(`/events/${event.uuid}`, this.$store, { method: 'DELETE' }) - // .then(() => router.push('/events')); - } - - viewEvent(event) { - this.$router.push({ name: 'Event', params: { uuid: event.uuid } }); - } - - downloadIcsEvent(event) { - // FIXME: remove eventFetch - // eventFetch(`/events/${event.uuid}/ics`, this.$store, { responseType: 'arraybuffer' }) - // .then(response => response.text()) - // .then((response) => { - // const blob = new Blob([ response ], { type: 'text/calendar' }); - // const link = document.createElement('a'); - // link.href = window.URL.createObjectURL(blob); - // link.download = `${event.title}.ics`; - // document.body.appendChild(link); - // link.click(); - // document.body.removeChild(link); - // }); - } - - }; -</script> - -<!-- Add "scoped" attribute to limit CSS to this component only --> -<style scoped> - -</style> diff --git a/js/src/components/Group/Create.vue b/js/src/components/Group/Create.vue deleted file mode 100644 index fb4dfa790..000000000 --- a/js/src/components/Group/Create.vue +++ /dev/null @@ -1,136 +0,0 @@ -<template> - <v-container> - <h3>Create a new group</h3> - <v-form> - <v-layout row wrap> - <v-flex xs12> - <v-text-field - label="Title" - v-model="group.preferred_username" - :counter="100" - required - ></v-text-field> - </v-flex> - <v-flex xs12> - <v-text-field - label="Title" - v-model="group.name" - :counter="100" - required - ></v-text-field> - </v-flex> - <v-flex md6> - <v-text-field - label="Description" - v-model="group.summary" - multiLine - required - ></v-text-field> - </v-flex> - <v-flex md6> - <vue-markdown class="markdown-render" - :watches="['show','html','breaks','linkify','emoji','typographer','toc']" - :source="group.summary" - :show="true" :html="false" :breaks="true" :linkify="true" - :emoji="true" :typographer="true" :toc="false" - ></vue-markdown> - </v-flex> - <!--<v-flex md12>--> - <!--<vuetify-google-autocomplete--> - <!--id="map"--> - <!--append-icon="search"--> - <!--classname="form-control"--> - <!--placeholder="Start typing"--> - <!--enable-geolocation--> - <!--v-on:placechanged="getAddressData"--> - <!-->--> - <!--</vuetify-google-autocomplete>--> - <!--</v-flex>--> - <!--<v-flex md12>--> - <!--<v-select--> - <!--v-bind:items="categories"--> - <!--v-model="group.category"--> - <!--item-text="title"--> - <!--item-value="@id"--> - <!--label="Categories"--> - <!--single-line--> - <!--bottom--> - <!--types="(cities)"--> - <!--></v-select>--> - <!--</v-flex>--> - </v-layout> - </v-form> - <v-btn color="primary" @click="create">Create group</v-btn> - </v-container> -</template> - -<script lang="ts"> - import VueMarkdown from 'vue-markdown'; - import VuetifyGoogleAutocomplete from 'vuetify-google-autocomplete'; - import { Component, Vue } from 'vue-property-decorator'; - - @Component({ - components: { - VueMarkdown, - VuetifyGoogleAutocomplete, - }, - }) - export default class CreateGroup extends Vue { - e1 = 0; - // FIXME: correctly type group - group: { preferred_username: string, name: string, summary: string, address?: any } = { - preferred_username: '', - name: '', - summary: '', - // category: null, - }; - categories = []; - - mounted() { - this.fetchCategories(); - } - - create() { - // this.group.organizer = "/accounts/" + this.$store.state.user.id; - - // FIXME: remove eventFetch - // eventFetch('/groups', this.$store, { method: 'POST', body: JSON.stringify({ group: this.group }) }) - // .then(response => response.json()) - // .then((data) => { - // this.loading = false; - // this.$router.push({ path: 'Group', params: { id: data.id } }); - // }); - } - - fetchCategories() { - // FIXME: remove eventFetch - // eventFetch('/categories', this.$store) - // .then(response => response.json()) - // .then((data) => { - // this.loading = false; - // this.categories = data.data; - // }); - } - - getAddressData(addressData) { - this.group.address = { - geo: { - latitude: addressData.latitude, - longitude: addressData.longitude, - }, - addressCountry: addressData.country, - addressLocality: addressData.city, - addressRegion: addressData.administrative_area_level_1, - postalCode: addressData.postal_code, - streetAddress: `${addressData.street_number} ${addressData.route}`, - }; - } - - }; -</script> - -<style> - .markdown-render h1 { - font-size: 2em; - } -</style> diff --git a/js/src/components/Group/Group.vue b/js/src/components/Group/Group.vue deleted file mode 100644 index aec8085cb..000000000 --- a/js/src/components/Group/Group.vue +++ /dev/null @@ -1,241 +0,0 @@ -<template> - <v-container> - <v-layout row> - <v-flex xs12 sm6 offset-sm3> - <v-progress-circular v-if="loading" indeterminate color="primary"></v-progress-circular> - <v-card v-if="!loading"> - <v-card-media :src="group.banner" height="400px"> - <v-layout column class="media"> - <v-card-title> - <v-btn icon @click="$router.go(-1)"> - <v-icon>chevron_left</v-icon> - </v-btn> - <v-spacer></v-spacer> - <!--<v-btn icon class="mr-3" v-if="$store.state.user && $store.state.actor.id === actor.id">--> - <!--<v-icon>edit</v-icon>--> - <!--</v-btn>--> - <v-btn icon> - <v-icon>more_vert</v-icon> - </v-btn> - </v-card-title> - <v-spacer></v-spacer> - <div class="text-xs-center"> - <v-avatar size="125px"> - <img v-if="!group.avatar" - class="img-circle elevation-7 mb-1" - src="https://picsum.photos/125/125/" - > - <img v-else - class="img-circle elevation-7 mb-1" - :src="group.avatar" - > - </v-avatar> - </div> - <v-container fluid grid-list-lg> - <v-layout row> - <v-flex xs7> - <div class="headline">{{ group.display_name }}</div> - <div> - <span class="subheading"> - ~{{ group.username }} - <span v-if="group.domain"> - @{{ group.domain }} - </span> - </span> - <v-chip color="indigo" text-color="white"> - <v-avatar> - <v-icon>group</v-icon> - </v-avatar> - Group - </v-chip> - </div> - <v-card-text v-if="group.description" v-html="group.description"></v-card-text> - </v-flex> - </v-layout> - </v-container> - </v-layout> - </v-card-media> - <v-list three-line> - <v-list-tile> - <v-list-tile-action> - <v-icon color="indigo">phone</v-icon> - </v-list-tile-action> - <v-list-tile-content> - <v-list-tile-title>(323) 555-6789</v-list-tile-title> - <v-list-tile-sub-title>Work</v-list-tile-sub-title> - </v-list-tile-content> - <v-list-tile-action> - <v-icon dark>chat</v-icon> - </v-list-tile-action> - </v-list-tile> - <v-divider inset></v-divider> - <v-list-tile> - <v-list-tile-action> - <v-icon color="indigo">mail</v-icon> - </v-list-tile-action> - <v-list-tile-content> - <v-list-tile-title>ali_connors@example.com</v-list-tile-title> - <v-list-tile-sub-title>Work</v-list-tile-sub-title> - </v-list-tile-content> - </v-list-tile> - <v-divider inset></v-divider> - <v-list-tile> - <v-list-tile-action> - <v-icon color="indigo">location_on</v-icon> - </v-list-tile-action> - <v-list-tile-content> - <v-list-tile-title>1400 Main Street</v-list-tile-title> - <v-list-tile-sub-title>Orlando, FL 79938</v-list-tile-sub-title> - </v-list-tile-content> - </v-list-tile> - </v-list> - <v-container fluid grid-list-md v-if="group.members.length > 0"> - <v-subheader>Membres</v-subheader> - <v-layout row> - <v-flex xs2 v-for="member in group.members" :key="member.actor.username"> - <router-link :to="{name: 'Account', params: { name: member.actor.username } }"> - <v-badge overlap> - <span slot="badge" v-if="member.role === 1"><v-icon>star_half</v-icon></span> - <span slot="badge" v-if="member.role === 2"><v-icon>star</v-icon></span> - <v-avatar size="75px"> - <img v-if="!member.actor.avatar" - class="img-circle elevation-7 mb-1" - src="https://picsum.photos/125/125/" - > - <img v-else - class="img-circle elevation-7 mb-1" - :src="member.actor.avatar" - > - </v-avatar> - </v-badge> - </router-link> - <span>{{ member.actor.username }}</span> - </v-flex> - </v-layout> - </v-container> - <v-container fluid grid-list-md v-if="group.participatingEvents && group.participatingEvents.length > 0"> - <v-subheader>Participated at</v-subheader> - <v-layout row wrap> - <v-flex v-for="event in group.participatingEvents" :key="event.id"> - <v-card> - <v-card-media - class="black--text" - height="200px" - src="https://picsum.photos/400/200/" - > - <v-container fill-height fluid> - <v-layout fill-height> - <v-flex xs12 align-end flexbox> - <span class="headline">{{ event.title }}</span> - </v-flex> - </v-layout> - </v-container> - </v-card-media> - <v-card-title> - <div> - <span class="grey--text">{{ event.startDate | formatDate }} à {{ event.location }}</span><br> - <p>{{ event.description }}</p> - <p v-if="event.organizer">Organisé par - <router-link :to="{name: 'Account', params: {'id': event.organizer.id}}">{{ event.organizer.username }} - </router-link> - </p> - </div> - </v-card-title> - <v-card-actions> - <v-spacer></v-spacer> - <v-btn icon> - <v-icon>favorite</v-icon> - </v-btn> - <v-btn icon> - <v-icon>bookmark</v-icon> - </v-btn> - <v-btn icon> - <v-icon>share</v-icon> - </v-btn> - </v-card-actions> - </v-card> - </v-flex> - </v-layout> - </v-container> - <v-container fluid grid-list-md v-if="group.organizingEvents && group.organizingEvents.length > 0"> - <v-subheader>Organized events</v-subheader> - <v-layout row wrap> - <v-flex v-for="event in group.organizingEvents" :key="event.id"> - <v-card> - <v-card-media - class="black--text" - height="200px" - src="https://picsum.photos/400/200/" - > - <v-container fill-height fluid> - <v-layout fill-height> - <v-flex xs12 align-end flexbox> - <span class="headline">{{ event.title }}</span> - </v-flex> - </v-layout> - </v-container> - </v-card-media> - <v-card-title> - <div> - <span class="grey--text">{{ event.startDate | formatDate }} à {{ event.location }}</span><br> - <p>{{ event.description }}</p> - <p v-if="event.organizer">Organisé par - <router-link :to="{name: 'Account', params: {'id': event.organizer.id}}">{{ event.organizer.username }} - </router-link> - </p> - </div> - </v-card-title> - <v-card-actions> - <v-spacer></v-spacer> - <v-btn icon> - <v-icon>favorite</v-icon> - </v-btn> - <v-btn icon> - <v-icon>bookmark</v-icon> - </v-btn> - <v-btn icon> - <v-icon>share</v-icon> - </v-btn> - </v-card-actions> - </v-card> - </v-flex> - </v-layout> - </v-container> - </v-card> - </v-flex> - </v-layout> - </v-container> -</template> - -<script lang="ts"> - import { Component, Prop, Vue, Watch } from 'vue-property-decorator'; - - @Component - export default class Group extends Vue { - @Prop({ type: String, required: true }) name!: string; - - group = null; - loading = true; - - created() { - this.fetchData(); - } - - @Watch('$route') - onRouteChanged() { - // call again the method if the route changes - this.fetchData(); - } - - fetchData() { - // FIXME: remove eventFetch - // eventFetch(`/actors/${this.name}`, this.$store) - // .then(response => response.json()) - // .then((response) => { - // this.group = response.data; - // this.loading = false; - // console.log(this.group); - // }); - } - }; -</script> diff --git a/js/src/components/Group/GroupCard.vue b/js/src/components/Group/GroupCard.vue new file mode 100644 index 000000000..895aee0cd --- /dev/null +++ b/js/src/components/Group/GroupCard.vue @@ -0,0 +1,30 @@ +<template> + <div class="card"> + <div class="card-image" v-if="!group.bannerUrl"> + <figure class="image is-4by3"> + <img src="https://picsum.photos/g/400/200/"> + </figure> + </div> + <div class="card-content"> + <div class="content"> + <router-link :to="{ name: 'Group', params:{ uuid: group.uuid } }"> + <h2 class="title">{{ group.name ? group.name : group.preferredUsername }}</h2> + </router-link> + </div> + <div v-if="!hideDetails"> + <p>{{ group.summary }}</p> + </div> + </div> + </div> +</template> + +<script lang="ts"> +import { Component, Prop, Vue } from "vue-property-decorator"; +import { IGroup } from "../../types/actor.model"; + +@Component +export default class GroupCard extends Vue { + @Prop({ required: true }) group!: IGroup; + @Prop({ default: false }) hideDetails!: boolean; +} +</script> diff --git a/js/src/components/Group/GroupList.vue b/js/src/components/Group/GroupList.vue deleted file mode 100644 index c35a5b13a..000000000 --- a/js/src/components/Group/GroupList.vue +++ /dev/null @@ -1,98 +0,0 @@ -<template> - <v-container> - <h1>Group List</h1> - - <v-progress-circular v-if="loading" indeterminate color="primary"></v-progress-circular> - <v-layout row wrap justify-space-around> - <v-flex xs12 md3 v-for="group in groups" :key="group.id"> - <v-card> - <v-card-media - class="black--text" - height="200px" - src="https://picsum.photos/400/200/" - > - <v-container fill-height fluid> - <v-layout fill-height> - <v-flex xs12 align-end flexbox> - <span class="headline">{{ group.username }}</span> - </v-flex> - </v-layout> - </v-container> - </v-card-media> - <v-card-title> - <div> - <p>{{ group.summary }}</p> - <p v-if="group.organizer">Organisé par - <router-link :to="{name: 'Account', params: {'id': group.organizer.id}}">{{ group.organizer.username }}</router-link> - </p> - </div> - </v-card-title> - <v-card-actions> - <v-btn flat color="green" @click="joinGroup(group)"> - <v-icon v-if="group.locked">lock</v-icon> - Join - </v-btn> - <v-btn flat color="orange" @click="viewActor(group)">Explore</v-btn> - <v-btn flat color="red" @click="deleteGroup(group)">Delete</v-btn> - </v-card-actions> - </v-card> - </v-flex> - </v-layout> - <router-link :to="{ name: 'CreateGroup' }" class="btn btn-default">Create</router-link> - </v-container> -</template> - -<script lang="ts"> - - import { Component, Vue } from 'vue-property-decorator'; - - @Component - export default class GroupList extends Vue { - groups = []; - loading = true; - - created() { - this.fetchData(); - } - - usernameWithDomain(actor) { - return actor.username + (actor.domain === null ? '' : `@${actor.domain}`); - } - - fetchData() { - // FIXME: remove eventFetch - // eventFetch('/groups', this.$store) - // .then(response => response.json()) - // .then((data) => { - // console.log(data); - // this.loading = false; - // this.groups = data.data; - // }); - } - - deleteGroup(group) { - const router = this.$router; - // FIXME: remove eventFetch - // eventFetch(`/groups/${this.usernameWithDomain(group)}`, this.$store, { method: 'DELETE' }) - // .then(response => response.json()) - // .then(() => router.push('/groups')); - } - - viewActor(actor) { - this.$router.push({ name: 'Group', params: { name: this.usernameWithDomain(actor) } }); - } - - joinGroup(group) { - const router = this.$router; - // FIXME: remove eventFetch - // eventFetch(`/groups/${this.usernameWithDomain(group)}/join`, this.$store, { method: 'POST' }) - // .then(response => response.json()) - // .then(() => router.push({ name: 'Group', params: { name: this.usernameWithDomain(group) } })); - } - }; -</script> - -<!-- Add "scoped" attribute to limit CSS to this component only --> -<style scoped> - -</style> diff --git a/js/src/components/Home.vue b/js/src/components/Home.vue deleted file mode 100644 index 0615fad08..000000000 --- a/js/src/components/Home.vue +++ /dev/null @@ -1,178 +0,0 @@ -<template> - <v-container> - <v-img - :gradient="gradient" - src="https://picsum.photos/1200/900" - dark - height="300" - v-if="!currentUser.id" - > - <v-container fill-height> - <v-layout align-center> - <v-flex text-xs-center> - <h1 class="display-3">Find events you like</h1> - <h2>Share it with Mobilizon</h2> - <v-btn :to="{ name: 'Register' }"> - <translate>Register</translate> - </v-btn> - </v-flex> - </v-layout> - </v-container> - </v-img> - <v-layout v-else> - <v-flex xs12 sm8 offset-sm2> - <v-layout row wrap> - <v-flex xs12 sm6> - <h1> - <translate :translate-params="{username: actor.preferredUsername}">Welcome back %{username}</translate> - </h1> - </v-flex> - <v-flex xs12 sm6> - <v-layout align-center> - <span class="events-nearby title">Events nearby </span> - <v-text-field - solo - append-icon="place" - :value="ipLocation()" - ></v-text-field> - </v-layout> - </v-flex> - </v-layout> - <div v-if="$apollo.loading"> - Still loading - </div> - <v-card v-if="events.length > 0"> - <v-layout row wrap> - <v-flex md4 v-for="event in events" :key="event.uuid"> - <v-card :to="{ name: 'Event', params:{ uuid: event.uuid } }"> - <v-img v-if="!event.image" - class="white--text" - height="200px" - src="https://picsum.photos/g/400/200/" - > - <v-container fill-height fluid> - <v-layout fill-height> - <v-flex xs12 align-end flexbox> - <span class="headline black--text">{{ event.title }}</span> - </v-flex> - </v-layout> - </v-container> - </v-img> - <v-card-title primary-title> - <div> - <span class="grey--text">{{ event.begins_on | formatDay }}</span><br> - <router-link :to="{name: 'Account', params: { name: event.organizerActor.preferredUsername } }"> - <v-avatar size="25px"> - <img class="img-circle elevation-7 mb-1" - :src="event.organizerActor.avatarUrl" - > - </v-avatar> - </router-link> - <span v-if="event.organizerActor">Organisé par {{ event.organizerActor.name ? event.organizerActor.name : event.organizerActor.preferredUsername }}</span> - </div> - </v-card-title> - </v-card> - </v-flex> - </v-layout> - </v-card> - <v-alert v-else :value="true" type="error"> - No events found - </v-alert> - </v-flex> - </v-layout> - </v-container> -</template> - -<script lang="ts"> - import ngeohash from 'ngeohash'; - import { AUTH_USER_ACTOR, AUTH_USER_ID } from '@/constants'; - import { FETCH_EVENTS } from '@/graphql/event'; - import { Component, Vue } from 'vue-property-decorator'; - import { ICurrentUser } from '@/types/current-user.model'; - import { CURRENT_USER_CLIENT } from '@/graphql/user'; - - @Component({ - apollo: { - events: { - query: FETCH_EVENTS, - }, - currentUser: { - query: CURRENT_USER_CLIENT, - }, - }, - }) - export default class Home extends Vue { - gradient = 'to top right, rgba(63,81,181, .7), rgba(25,32,72, .7)'; - searchTerm = null; - location_field = { - loading: false, - search: null, - }; - events = []; - locations = []; - city = { name: null }; - country = { name: null }; - // FIXME: correctly parse local storage - actor = JSON.parse(localStorage.getItem(AUTH_USER_ACTOR) || '{}'); - currentUser!: ICurrentUser; - - get displayed_name() { - return this.actor.name === null ? this.actor.preferredUsername : this.actor.name; - } - - fetchLocations() { - // FIXME: remove eventFetch - // eventFetch('/locations', this.$store) - // .then(response => (response.json())) - // .then((response) => { - // this.locations = response; - // }); - } - - geoLocalize() { - const router = this.$router; - const sessionCity = sessionStorage.getItem('City'); - if (sessionCity) { - router.push({ name: 'EventList', params: { location: sessionCity } }); - } else { - navigator.geolocation.getCurrentPosition((pos) => { - const crd = pos.coords; - - const geohash = ngeohash.encode(crd.latitude, crd.longitude, 11); - sessionStorage.setItem('City', geohash); - router.push({ name: 'EventList', params: { location: geohash } }); - }, err => console.warn(`ERROR(${err.code}): ${err.message}`), { - enableHighAccuracy: true, - timeout: 5000, - maximumAge: 0, - }); - } - } - - getAddressData(addressData) { - const geohash = ngeohash.encode(addressData.latitude, addressData.longitude, 11); - sessionStorage.setItem('City', geohash); - this.$router.push({ name: 'EventList', params: { location: geohash } }); - } - - viewEvent(event) { - this.$router.push({ name: 'Event', params: { uuid: event.uuid } }); - } - - ipLocation() { - return this.city.name ? this.city.name : this.country.name; - } - } -</script> - -<!-- Add "scoped" attribute to limit CSS to this component only --> -<style scoped> - .search-autocomplete { - border: 1px solid #dbdbdb; - color: rgba(0, 0, 0, .87); - } - - .events-nearby { - margin-bottom: 25px; - } -</style> diff --git a/js/src/components/Location.vue b/js/src/components/Location.vue deleted file mode 100644 index 82a08fe5c..000000000 --- a/js/src/components/Location.vue +++ /dev/null @@ -1,51 +0,0 @@ -<template> - <div> - - <!--<gmap-autocomplete :value="description" @input="setPlace" - @place_changed="setPlace"> - </gmap-autocomplete> - <br /> - - <gmap-map - :center="center" - :zoom="15" - style="width: 500px; height: 300px" - > - <gmap-marker - :key="index" - v-for="(m, index) in markers" - :position="m.position" - :clickable="true" - :draggable="true" - @click="center=m.position" - ></gmap-marker> - </gmap-map>--> - {{ center.lat }} - {{ center.lng }} - </div> -</template> - -<script lang="ts"> - import { Component, Prop, Vue } from 'vue-property-decorator'; - - @Component - export default class Location extends Vue { - @Prop(String) address!: string; - - description = 'Paris, France'; - center = { lat: 48.85, lng: 2.35 }; - markers: any[] = []; - - setPlace(place) { - this.center = { - lat: place.geometry.location.lat(), - lng: place.geometry.location.lng(), - }; - this.markers = [ { - position: { lat: this.center.lat, lng: this.center.lng }, - } ]; - - this.$emit('input', place.formatted_address); - } - }; - -</script> diff --git a/js/src/components/NavBar.vue b/js/src/components/NavBar.vue index 42728fd70..e9fc3917f 100644 --- a/js/src/components/NavBar.vue +++ b/js/src/components/NavBar.vue @@ -1,150 +1,98 @@ <template> - <v-toolbar - class="blue darken-3" - dark - app - :clipped-left="$vuetify.breakpoint.lgAndUp" - fixed - > - <v-toolbar-title style="width: 300px" class="ml-0 pl-3 white--text"> - <v-toolbar-side-icon @click.stop="toggleDrawer()"></v-toolbar-side-icon> - <router-link :to="{ name: 'Home' }" class="hidden-sm-and-down white--text">Mobilizon - </router-link> - </v-toolbar-title> - <v-autocomplete - :loading="$apollo.loading" - flat - solo-inverted - prepend-icon="search" - :label="$gettext('Search')" - required - item-text="label" - class="hidden-sm-and-down" - :items="items" - :search-input.sync="searchText" - @keyup.enter="enter" - v-model="model" - return-object - > - <template slot="item" slot-scope="data"> - <!-- <div>{{ data }}</div> --> - <v-list-tile v-if="data.item.__typename === 'Event'"> - <v-list-tile-avatar> - <v-icon>event</v-icon> - </v-list-tile-avatar> - <v-list-tile-content v-text="data.item.label"></v-list-tile-content> - </v-list-tile> - <v-list-tile v-else-if="data.item.__typename === 'Actor'"> - <v-list-tile-avatar> - <img :src="data.item.avatarUrl" v-if="data.item.avatarUrl"> - <v-icon v-else>account_circle</v-icon> - </v-list-tile-avatar> - <v-list-tile-content> - <v-list-tile-title v-html="username_with_domain(data.item)"></v-list-tile-title> - </v-list-tile-content> - </v-list-tile> - </template> - </v-autocomplete> - <v-spacer></v-spacer> + <nav class="navbar is-fixed-top" role="navigation" aria-label="main navigation"> + <div class="navbar-brand"> + <router-link class="navbar-item" :to="{ name: 'Home' }">Mobilizon</router-link> - <span v-if="currentUser.id" @click="logout()">Logout</span> - - <v-menu - offset-y - :close-on-content-click="false" - :nudge-width="200" - v-model="notificationMenu" - v-if="currentUser.id" - > - <v-btn icon slot="activator"> - <v-badge left color="red"> - <span slot="badge">{{ notifications.length }}</span> - <v-icon>notifications</v-icon> - </v-badge> - </v-btn> - <v-card> - <v-list two-line> - <template v-for="item in notifications"> - <v-subheader v-if="item.header" v-text="item.header" v-bind:key="item.header"></v-subheader> - <v-divider v-else-if="item.divider" v-bind:inset="item.inset" v-bind:key="item.inset"></v-divider> - <v-list-tile avatar v-else v-bind:key="item.title"> - <v-list-tile-content> - <v-list-tile-title v-html="item.title"></v-list-tile-title> - <v-list-tile-sub-title v-html="item.subtitle"></v-list-tile-sub-title> - </v-list-tile-content> - </v-list-tile> - </template> - </v-list> - <v-card-actions> - <v-spacer></v-spacer> - <v-btn flat @click="notificationMenu = false"> - <translate>Close</translate> - </v-btn> - <v-btn color="primary" flat @click="notificationMenu = false"> - <translate>Save</translate> - </v-btn> - </v-card-actions> - </v-card> - </v-menu> - <v-btn v-if="!currentUser.id" :to="{ name: 'Login' }"> - <translate>Login</translate> - </v-btn> - </v-toolbar> + <a + role="button" + class="navbar-burger burger" + aria-label="menu" + aria-expanded="false" + data-target="navbarBasicExample" + > + <span aria-hidden="true"></span> + <span aria-hidden="true"></span> + <span aria-hidden="true"></span> + </a> + </div> + <div class="navbar-end"> + <div class="navbar-item"> + <div class="buttons"> + <router-link class="button is-primary" v-if="!currentUser.id" :to="{ name: 'Register' }"> + <strong> + <translate>Sign up</translate> + </strong> + </router-link> + <router-link class="button is-light" v-if="!currentUser.id" :to="{ name: 'Login' }"> + <translate>Log in</translate> + </router-link> + <router-link + class="button is-light" + v-if="currentUser.id && loggedPerson" + :to="{ name: 'Profile', params: { name: loggedPerson.preferredUsername} }" + > + <figure class="image is-24x24"> + <img :src="loggedPerson.avatarUrl"> + </figure> + <span>{{ loggedPerson.preferredUsername }}</span> + </router-link> + </div> + </div> + </div> + </nav> </template> -<style> - nav.v-toolbar .v-input__slot { - margin-bottom: 0; - } -</style> - <script lang="ts"> - import { Component, Prop, Vue, Watch } from 'vue-property-decorator'; - import { AUTH_USER_ACTOR } from '@/constants'; - import { SEARCH } from '@/graphql/search'; - import { CURRENT_USER_CLIENT } from '@/graphql/user'; - import { onLogout } from '@/vue-apollo'; - import { deleteUserData } from '@/utils/auth'; +import { Component, Prop, Vue, Watch } from "vue-property-decorator"; +import { AUTH_USER_ACTOR } from "@/constants"; +import { SEARCH } from "@/graphql/search"; +import { CURRENT_USER_CLIENT } from "@/graphql/user"; +import { onLogout } from "@/vue-apollo"; +import { deleteUserData } from "@/utils/auth"; +import { LOGGED_PERSON } from "@/graphql/actor"; +import { IPerson } from "../types/actor.model"; - @Component({ +@Component({ apollo: { search: { query: SEARCH, variables() { return { - searchText: this.searchText, + searchText: this.searchText }; }, skip() { return !this.searchText; - }, + } }, currentUser: { query: CURRENT_USER_CLIENT + }, + loggedPerson: { + query: LOGGED_PERSON } - }, + } }) export default class NavBar extends Vue { - @Prop({ required: true, type: Function }) toggleDrawer!: Function; - - notificationMenu = false; notifications = [ - { header: 'Coucou' }, - { title: 'T\'as une notification', subtitle: 'Et elle est cool' }, + { header: "Coucou" }, + { title: "T'as une notification", subtitle: "Et elle est cool" } ]; model = null; search: any[] = []; searchText: string | null = null; searchSelect = null; - actor = localStorage.getItem(AUTH_USER_ACTOR); + loggedPerson!: IPerson; get items() { return this.search.map(searchEntry => { switch (searchEntry.__typename) { - case 'Actor': - searchEntry.label = searchEntry.preferredUsername + (searchEntry.domain === null ? '' : `@${searchEntry.domain}`); + case "Actor": + searchEntry.label = + searchEntry.preferredUsername + + (searchEntry.domain === null ? "" : `@${searchEntry.domain}`); break; - case 'Event': + case "Event": searchEntry.label = searchEntry.title; break; } @@ -152,34 +100,39 @@ export default class NavBar extends Vue { }); } - @Watch('model') + @Watch("model") onModelChanged(val) { switch (val.__typename) { - case 'Event': - this.$router.push({ name: 'Event', params: { uuid: val.uuid } }); + case "Event": + this.$router.push({ name: "Event", params: { uuid: val.uuid } }); break; - case 'Actor': - this.$router.push({ name: 'Account', params: { name: this.username_with_domain(val) } }); + case "Actor": + this.$router.push({ + name: "Profile", + params: { name: this.username_with_domain(val) } + }); break; } } username_with_domain(actor) { - return actor.preferredUsername + (actor.domain === null ? '' : `@${actor.domain}`); + return ( + actor.preferredUsername + + (actor.domain === null ? "" : `@${actor.domain}`) + ); } enter() { - console.log('enter'); - this.$apollo.queries['search'].refetch(); + console.log("enter"); + this.$apollo.queries["search"].refetch(); } logout() { - alert('logout !'); + alert("logout !"); deleteUserData(); return onLogout(this.$apollo); } - } </script> diff --git a/js/src/components/PageNotFound.vue b/js/src/components/PageNotFound.vue deleted file mode 100644 index 037ee27a3..000000000 --- a/js/src/components/PageNotFound.vue +++ /dev/null @@ -1,10 +0,0 @@ -<template> - <v-container> - <v-layout row> - <v-flex xs12 sm6 offset-sm3> - <h1>404 !</h1> - <img src="../assets/oh_no.jpg" /> - </v-flex> - </v-layout> - </v-container> -</template> diff --git a/js/src/graphql/actor.ts b/js/src/graphql/actor.ts index 45c54ca99..6fa5a65a2 100644 --- a/js/src/graphql/actor.ts +++ b/js/src/graphql/actor.ts @@ -1,14 +1,9 @@ import gql from 'graphql-tag'; -export const FETCH_ACTOR = gql` +export const FETCH_PERSON = gql` query($name:String!) { - actor(preferredUsername: $name) { + person(preferredUsername: $name) { url, - outboxUrl, - inboxUrl, - followingUrl, - followersUrl, - sharedInboxUrl, name, domain, summary, @@ -18,22 +13,36 @@ query($name:String!) { bannerUrl, organizedEvents { uuid, - title, - description, - organizer_actor { - avatarUrl, - preferred_username, - name, - } + title }, } } `; -export const LOGGED_ACTOR = gql` +export const LOGGED_PERSON = gql` query { - loggedActor { + loggedPerson { + id, avatarUrl, preferredUsername, } }`; + +export const IDENTITIES = gql` +query { + identities { + avatarUrl, + preferredUsername, + name + } +}`; + +export const CREATE_PERSON = gql` +mutation CreatePerson($preferredUsername: String!) { + createPerson(preferredUsername: $preferredUsername) { + preferredUsername, + name, + avatarUrl + } +} +` \ No newline at end of file diff --git a/js/src/graphql/event.ts b/js/src/graphql/event.ts index 1c2ef0b06..a3be313ff 100644 --- a/js/src/graphql/event.ts +++ b/js/src/graphql/event.ts @@ -3,13 +3,14 @@ import gql from 'graphql-tag'; export const FETCH_EVENT = gql` query($uuid:UUID!) { event(uuid: $uuid) { + id, uuid, url, local, title, description, - begins_on, - ends_on, + beginsOn, + endsOn, status, visibility, thumbnail, @@ -22,11 +23,11 @@ export const FETCH_EVENT = gql` preferredUsername, name, }, - attributedTo { - avatarUrl, - preferredUsername, - name, - }, + # attributedTo { + # # avatarUrl, + # preferredUsername, + # name, + # }, participants { actor { avatarUrl, @@ -45,13 +46,14 @@ export const FETCH_EVENT = gql` export const FETCH_EVENTS = gql` query { events { + id, uuid, url, local, title, description, - begins_on, - ends_on, + beginsOn, + endsOn, status, visibility, thumbnail, @@ -72,16 +74,24 @@ export const FETCH_EVENTS = gql` category { title, }, + participants { + role, + actor { + preferredUsername, + avatarUrl, + name + } } } +} `; export const CREATE_EVENT = gql` mutation CreateEvent( $title: String!, $description: String!, - $organizerActorId: Int!, - $categoryId: Int!, + $organizerActorId: String!, + $category: String!, $beginsOn: DateTime! ) { createEvent( @@ -89,8 +99,12 @@ export const CREATE_EVENT = gql` description: $description, beginsOn: $beginsOn, organizerActorId: $organizerActorId, - categoryId: $categoryId - ) + category: $category + ) { + id, + uuid, + title + } } `; @@ -106,3 +120,15 @@ export const EDIT_EVENT = gql` } } `; + +export const JOIN_EVENT = gql` + mutation JoinEvent( + $uuid: String!, + $username: String! + ) { + joinEvent( + uuid: $uuid, + username: $username + ) +} +`; diff --git a/js/src/i18n/locale/en_US/LC_MESSAGES/app.po b/js/src/i18n/locale/en_US/LC_MESSAGES/app.po index b8924e022..a4ee60e23 100644 --- a/js/src/i18n/locale/en_US/LC_MESSAGES/app.po +++ b/js/src/i18n/locale/en_US/LC_MESSAGES/app.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: mobilizon 0.1.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-10-24 16:25+0200\n" +"POT-Creation-Date: 2019-01-17 16:08+0100\n" "PO-Revision-Date: 2018-10-24 16:25+0200\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" @@ -17,14 +17,178 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: src/components/Account/Register.vue:70 +#: src/App.vue:8 +msgid "© The Mobilizon Contributors %{date} - Made with Elixir, Phoenix, VueJS & with some love and some weeks" +msgstr "" + +#: src/components/Account/Register.vue:89 msgid "A validation email was sent to %{email}" msgstr "A validation email was sent to %{email}" -#: src/components/Account/Register.vue:71 +#: src/components/Account/Register.vue:26 +msgid "About this instance" +msgstr "" + +#: src/components/Account/Register.vue:92 msgid "Before you can login, you need to click on the link inside it to validate your account" msgstr "Before you can login, you need to click on the link inside it to validate your account" -#: src/components/Home.vue:14 +#: src/components/Category/Create.vue:7 +msgid "Create a new category" +msgstr "" + +#: src/components/Category/Create.vue:34 +msgid "Create category" +msgstr "" + +#: src/components/Account/Register.vue:16 +msgid "Create your communities and your events" +msgstr "" + +#: src/components/Account/Profile.vue:48 src/components/Category/List.vue:21 +#: src/components/Event/Event.vue:41 +msgid "Delete" +msgstr "" + +#: src/components/Account/Register.vue:80 +msgid "Didn't receive the instructions ?" +msgstr "" + +#: src/components/Event/Event.vue:36 +msgid "Download" +msgstr "" + +#: src/components/Event/Event.vue:31 +msgid "Edit" +msgstr "" + +#: src/components/Account/Validate.vue:8 +msgid "Error while validating account" +msgstr "" + +#: src/components/Category/List.vue:18 +msgid "Explore" +msgstr "" + +#: src/components/Account/Register.vue:14 +msgid "Features" +msgstr "" + +#: src/components/Account/Login.vue:46 +msgid "Forgot your password ?" +msgstr "" + +#: src/components/Account/Register.vue:20 +msgid "" +"Learn more on\n" +" <a target=\"_blank\" href=\"https://joinmobilizon.org\">joinmobilizon.org</a>" +msgstr "" + +#: src/components/NavBar.vue:26 +msgid "Log in" +msgstr "" + +#: src/components/Account/Login.vue:38 +msgid "Login" +msgstr "" + +#: src/components/Account/Register.vue:32 +msgid "meditate a bit" +msgstr "" + +#: src/components/Home.vue:33 +msgid "No events found" +msgstr "" + +#: src/components/Account/Profile.vue:29 +msgid "Organized" +msgstr "" + +#: src/components/Account/Register.vue:17 +msgid "Other stuff…" +msgstr "" + +#: src/components/Account/SendPasswordReset.vue:4 +msgid "Password reset" +msgstr "" + +#: src/components/Account/Register.vue:31 +msgid "Please be nice to each other" +msgstr "" + +#: src/components/Account/ResendConfirmation.vue:21 +#: src/components/Account/SendPasswordReset.vue:22 +msgid "Please check you spam folder if you didn't receive the email." +msgstr "" + +#: src/components/Account/Register.vue:35 +msgid "Please read the full rules" +msgstr "" + +#: src/components/Account/Register.vue:72 src/components/Home.vue:9 msgid "Register" msgstr "Register" + +#: src/components/Account/Register.vue:5 +msgid "Register an account on Mobilizon!" +msgstr "" + +#: src/components/Account/ResendConfirmation.vue:4 +msgid "Resend confirmation email" +msgstr "" + +#: src/components/Account/PasswordReset.vue:26 +msgid "Reset my password" +msgstr "" + +#: src/components/Account/ResendConfirmation.vue:11 +msgid "Send confirmation email again" +msgstr "" + +#: src/components/Account/SendPasswordReset.vue:12 +msgid "Send email to reset my password" +msgstr "" + +#: src/components/NavBar.vue:22 +msgid "Sign up" +msgstr "" + +#: src/components/Account/Profile.vue:43 +msgid "User logout" +msgstr "" + +#: src/components/Event/Event.vue:50 +msgid "Vous avez annoncé aller à cet événement." +msgstr "" + +#: src/components/Event/Event.vue:46 +msgid "Vous êtes organisateur de cet événement." +msgstr "" + +#: src/components/Account/SendPasswordReset.vue:17 +msgid "We just sent an email to %{email}" +msgstr "" + +#: src/components/Account/ResendConfirmation.vue:16 +msgid "We just sent another confirmation email to %{email}" +msgstr "" + +#: src/components/Home.vue:16 +msgid "Welcome back %{username}" +msgstr "" + +#: src/components/Account/Login.vue:4 +msgid "Welcome back!" +msgstr "" + +#: src/components/Account/Validate.vue:12 +msgid "Your account has been validated" +msgstr "" + +#: src/components/Account/Validate.vue:3 +msgid "Your account is being validated" +msgstr "" + +#: src/components/Account/Register.vue:28 +msgid "Your local administrator resumed it's policy:" +msgstr "" diff --git a/js/src/i18n/locale/en_US/LC_MESSAGES/app.po~ b/js/src/i18n/locale/en_US/LC_MESSAGES/app.po~ new file mode 100644 index 000000000..b8924e022 --- /dev/null +++ b/js/src/i18n/locale/en_US/LC_MESSAGES/app.po~ @@ -0,0 +1,30 @@ +# English translations for mobilizon package. +# Copyright (C) 2018 THE mobilizon'S COPYRIGHT HOLDER +# This file is distributed under the same license as the mobilizon package. +# Automatically generated, 2018. +# +msgid "" +msgstr "" +"Project-Id-Version: mobilizon 0.1.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-10-24 16:25+0200\n" +"PO-Revision-Date: 2018-10-24 16:25+0200\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_US\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: src/components/Account/Register.vue:70 +msgid "A validation email was sent to %{email}" +msgstr "A validation email was sent to %{email}" + +#: src/components/Account/Register.vue:71 +msgid "Before you can login, you need to click on the link inside it to validate your account" +msgstr "Before you can login, you need to click on the link inside it to validate your account" + +#: src/components/Home.vue:14 +msgid "Register" +msgstr "Register" diff --git a/js/src/i18n/locale/fr_FR/LC_MESSAGES/app.po b/js/src/i18n/locale/fr_FR/LC_MESSAGES/app.po index 45b560750..6e03112b6 100644 --- a/js/src/i18n/locale/fr_FR/LC_MESSAGES/app.po +++ b/js/src/i18n/locale/fr_FR/LC_MESSAGES/app.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: mobilizon 0.1.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-10-24 16:25+0200\n" +"POT-Creation-Date: 2019-01-17 16:08+0100\n" "PO-Revision-Date: 2018-10-24 16:25+0200\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" @@ -17,14 +17,178 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" -#: src/components/Account/Register.vue:70 +#: src/App.vue:8 +msgid "© The Mobilizon Contributors %{date} - Made with Elixir, Phoenix, VueJS & with some love and some weeks" +msgstr "" + +#: src/components/Account/Register.vue:89 msgid "A validation email was sent to %{email}" msgstr "" -#: src/components/Account/Register.vue:71 +#: src/components/Account/Register.vue:26 +msgid "About this instance" +msgstr "" + +#: src/components/Account/Register.vue:92 msgid "Before you can login, you need to click on the link inside it to validate your account" msgstr "" -#: src/components/Home.vue:14 +#: src/components/Category/Create.vue:7 +msgid "Create a new category" +msgstr "" + +#: src/components/Category/Create.vue:34 +msgid "Create category" +msgstr "" + +#: src/components/Account/Register.vue:16 +msgid "Create your communities and your events" +msgstr "" + +#: src/components/Account/Profile.vue:48 src/components/Category/List.vue:21 +#: src/components/Event/Event.vue:41 +msgid "Delete" +msgstr "" + +#: src/components/Account/Register.vue:80 +msgid "Didn't receive the instructions ?" +msgstr "" + +#: src/components/Event/Event.vue:36 +msgid "Download" +msgstr "" + +#: src/components/Event/Event.vue:31 +msgid "Edit" +msgstr "" + +#: src/components/Account/Validate.vue:8 +msgid "Error while validating account" +msgstr "" + +#: src/components/Category/List.vue:18 +msgid "Explore" +msgstr "" + +#: src/components/Account/Register.vue:14 +msgid "Features" +msgstr "" + +#: src/components/Account/Login.vue:46 +msgid "Forgot your password ?" +msgstr "" + +#: src/components/Account/Register.vue:20 +msgid "" +"Learn more on\n" +" <a target=\"_blank\" href=\"https://joinmobilizon.org\">joinmobilizon.org</a>" +msgstr "" + +#: src/components/NavBar.vue:26 +msgid "Log in" +msgstr "" + +#: src/components/Account/Login.vue:38 +msgid "Login" +msgstr "" + +#: src/components/Account/Register.vue:32 +msgid "meditate a bit" +msgstr "" + +#: src/components/Home.vue:33 +msgid "No events found" +msgstr "" + +#: src/components/Account/Profile.vue:29 +msgid "Organized" +msgstr "" + +#: src/components/Account/Register.vue:17 +msgid "Other stuff…" +msgstr "" + +#: src/components/Account/SendPasswordReset.vue:4 +msgid "Password reset" +msgstr "" + +#: src/components/Account/Register.vue:31 +msgid "Please be nice to each other" +msgstr "" + +#: src/components/Account/ResendConfirmation.vue:21 +#: src/components/Account/SendPasswordReset.vue:22 +msgid "Please check you spam folder if you didn't receive the email." +msgstr "" + +#: src/components/Account/Register.vue:35 +msgid "Please read the full rules" +msgstr "" + +#: src/components/Account/Register.vue:72 src/components/Home.vue:9 msgid "Register" msgstr "S'inscrire" + +#: src/components/Account/Register.vue:5 +msgid "Register an account on Mobilizon!" +msgstr "" + +#: src/components/Account/ResendConfirmation.vue:4 +msgid "Resend confirmation email" +msgstr "" + +#: src/components/Account/PasswordReset.vue:26 +msgid "Reset my password" +msgstr "" + +#: src/components/Account/ResendConfirmation.vue:11 +msgid "Send confirmation email again" +msgstr "" + +#: src/components/Account/SendPasswordReset.vue:12 +msgid "Send email to reset my password" +msgstr "" + +#: src/components/NavBar.vue:22 +msgid "Sign up" +msgstr "" + +#: src/components/Account/Profile.vue:43 +msgid "User logout" +msgstr "" + +#: src/components/Event/Event.vue:50 +msgid "Vous avez annoncé aller à cet événement." +msgstr "" + +#: src/components/Event/Event.vue:46 +msgid "Vous êtes organisateur de cet événement." +msgstr "" + +#: src/components/Account/SendPasswordReset.vue:17 +msgid "We just sent an email to %{email}" +msgstr "" + +#: src/components/Account/ResendConfirmation.vue:16 +msgid "We just sent another confirmation email to %{email}" +msgstr "" + +#: src/components/Home.vue:16 +msgid "Welcome back %{username}" +msgstr "" + +#: src/components/Account/Login.vue:4 +msgid "Welcome back!" +msgstr "" + +#: src/components/Account/Validate.vue:12 +msgid "Your account has been validated" +msgstr "" + +#: src/components/Account/Validate.vue:3 +msgid "Your account is being validated" +msgstr "" + +#: src/components/Account/Register.vue:28 +msgid "Your local administrator resumed it's policy:" +msgstr "" diff --git a/js/src/i18n/locale/fr_FR/LC_MESSAGES/app.po~ b/js/src/i18n/locale/fr_FR/LC_MESSAGES/app.po~ index 131065ac7..45b560750 100644 --- a/js/src/i18n/locale/fr_FR/LC_MESSAGES/app.po~ +++ b/js/src/i18n/locale/fr_FR/LC_MESSAGES/app.po~ @@ -11,7 +11,7 @@ msgstr "" "PO-Revision-Date: 2018-10-24 16:25+0200\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" -"Language: fr\n" +"Language: fr_FR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" diff --git a/js/src/main.ts b/js/src/main.ts index 692908724..81d7a0dff 100644 --- a/js/src/main.ts +++ b/js/src/main.ts @@ -3,10 +3,9 @@ import Vue from 'vue'; // import * as VueGoogleMaps from 'vue2-google-maps'; import VueMarkdown from 'vue-markdown'; -import Vuetify from 'vuetify'; +import Buefy from 'buefy' +import 'buefy/dist/buefy.css'; import GetTextPlugin from 'vue-gettext'; -import 'material-design-icons/iconfont/material-icons.css'; -import 'vuetify/dist/vuetify.min.css'; import App from '@/App.vue'; import router from '@/router'; import { apolloProvider } from './vue-apollo'; @@ -16,7 +15,9 @@ const translations = require('@/i18n/translations.json'); Vue.config.productionTip = false; Vue.use(VueMarkdown); -Vue.use(Vuetify); +Vue.use(Buefy, { + defaultContainerElement: '#mobilizon' +}); const language = (window.navigator as any).userLanguage || window.navigator.language; diff --git a/js/src/router/index.ts b/js/src/router/index.ts index 1bd4a0e36..389cffbf5 100644 --- a/js/src/router/index.ts +++ b/js/src/router/index.ts @@ -1,24 +1,24 @@ import Vue from 'vue'; import Router from 'vue-router'; -import PageNotFound from '@/components/PageNotFound.vue'; -import Home from '@/components/Home.vue'; -import Event from '@/components/Event/Event.vue'; -import EventList from '@/components/Event/EventList.vue'; -import Location from '@/components/Location.vue'; -import CreateEvent from '@/components/Event/Create.vue'; -import CategoryList from '@/components/Category/List.vue'; -import CreateCategory from '@/components/Category/Create.vue'; -import Register from '@/components/Account/Register.vue'; -import Login from '@/components/Account/Login.vue'; -import Validate from '@/components/Account/Validate.vue'; -import ResendConfirmation from '@/components/Account/ResendConfirmation.vue'; -import SendPasswordReset from '@/components/Account/SendPasswordReset.vue'; -import PasswordReset from '@/components/Account/PasswordReset.vue'; -import Account from '@/components/Account/Account.vue'; -import CreateGroup from '@/components/Group/Create.vue'; -import Group from '@/components/Group/Group.vue'; -import GroupList from '@/components/Group/GroupList.vue'; -import Identities from '../components/Account/Identities.vue'; +import PageNotFound from '@/views/PageNotFound.vue'; +import Home from '@/views/Home.vue'; +import Event from '@/views/Event/Event.vue'; +import EventList from '@/views/Event/EventList.vue'; +import Location from '@/views/Location.vue'; +import CreateEvent from '@/views/Event/Create.vue'; +import CategoryList from '@/views/Category/List.vue'; +import CreateCategory from '@/views/Category/Create.vue'; +import Register from '@/views/Account/Register.vue'; +import Login from '@/views/User/Login.vue'; +import Validate from '@/views/User/Validate.vue'; +import ResendConfirmation from '@/views/User/ResendConfirmation.vue'; +import SendPasswordReset from '@/views/User/SendPasswordReset.vue'; +import PasswordReset from '@/views/User/PasswordReset.vue'; +import Profile from '@/views/Account/Profile.vue'; +import CreateGroup from '@/views/Group/Create.vue'; +import Group from '@/views/Group/Group.vue'; +import GroupList from '@/views/Group/GroupList.vue'; +import Identities from '@/views/Account/Identities.vue'; Vue.use(Router); @@ -45,7 +45,7 @@ const router = new Router({ meta: { requiredAuth: true }, }, { - path: '/events/:id(\\d+)/edit', + path: '/events/:id/edit', name: 'EditEvent', component: CreateEvent, props: true, @@ -124,7 +124,7 @@ const router = new Router({ meta: { requiredAuth: false }, }, { - path: '/group-create', + path: '/groups/create', name: 'CreateGroup', component: CreateGroup, meta: { requiredAuth: true }, @@ -138,8 +138,8 @@ const router = new Router({ }, { path: '/@:name', - name: 'Account', - component: Account, + name: 'Profile', + component: Profile, props: true, meta: { requiredAuth: false }, }, diff --git a/js/src/types/actor.model.ts b/js/src/types/actor.model.ts new file mode 100644 index 000000000..78891e493 --- /dev/null +++ b/js/src/types/actor.model.ts @@ -0,0 +1,29 @@ +export interface IActor { + id: string; + url: string; + name: string; + domain: string; + summary: string; + preferredUsername: string; + suspended: boolean; + avatarUrl: string; + bannerUrl: string; +} + +export interface IPerson extends IActor { + +} + +export interface IGroup extends IActor { + members: IMember[]; +} + +export enum MemberRole { + PENDING, MEMBER, MODERATOR, ADMIN +} + +export interface IMember { + role: MemberRole; + parent: IGroup; + actor: IActor; +} \ No newline at end of file diff --git a/js/src/types/event.model.ts b/js/src/types/event.model.ts new file mode 100644 index 000000000..9e5b25a49 --- /dev/null +++ b/js/src/types/event.model.ts @@ -0,0 +1,46 @@ +import { IActor } from "./actor.model"; + +export enum EventStatus { + TENTATIVE, CONFIRMED, CANCELLED +} + +export enum EventVisibility { + PUBLIC, PRIVATE +} + +export enum ParticipantRole { + +} + +export interface ICategory { + title: string; + description: string; + picture: string; +} + +export interface IParticipant { + role: ParticipantRole, + actor: IActor, + event: IEvent +} + +export interface IEvent { + uuid: string; + url: string; + local: boolean; + title: string; + description: string; + begins_on: Date; + ends_on: Date; + status: EventStatus; + visibility: EventVisibility; + thumbnail: string; + large_image: string; + publish_at: Date; + // online_address: Adress; + // phone_address: string; + organizerActor: IActor; + attributedTo: IActor; + participants: IParticipant[]; + category: ICategory; +} \ No newline at end of file diff --git a/js/src/views/Account/Identities.vue b/js/src/views/Account/Identities.vue new file mode 100644 index 000000000..3b70a6bfc --- /dev/null +++ b/js/src/views/Account/Identities.vue @@ -0,0 +1,92 @@ +<template> + <section> + <b-loading :active.sync="$apollo.loading"></b-loading> + <h1 class="title"> + <translate>Identities</translate> + </h1> + <a class="button is-primary" @click="showCreateProfileForm = true"> + <translate>Add a new profile</translate> + </a> + <div class="columns" v-if="showCreateProfileForm"> + <form @submit="createProfile" class="column is-half"> + <b-message title="Error" type="is-danger" v-for="error in errors" :key="error">{{ error }}</b-message> + <b-field label="Username"> + <b-input aria-required="true" required v-model="newPerson.preferredUsername"/> + </b-field> + <button class="button is-primary"> + <translate>Register</translate> + </button> + </form> + </div> + <ul> + <li v-for="identity in identities" :key="identity.id"> + <hr> + <div class="media"> + <div class="media-left"> + <figure class="image is-48x48"> + <img :src="identity.avatarUrl"> + </figure> + </div> + <div class="media-content"> + <p class="title is-5"> + {{ identity.name }} + <span + v-if="identity.preferredUsername === loggedPerson.preferredUsername" + class="tag is-primary" + > + <translate>Current</translate> + </span> + </p> + <p class="subtitle is-6">@{{ identity.preferredUsername }}</p> + </div> + </div> + </li> + </ul> + </section> +</template> + +<script lang="ts"> +import { Component, Vue } from "vue-property-decorator"; +import { IDENTITIES, LOGGED_PERSON, CREATE_PERSON } from "../../graphql/actor"; +import { IPerson } from "@/types/actor.model"; + +@Component({ + apollo: { + identities: { + query: IDENTITIES + }, + loggedPerson: { + query: LOGGED_PERSON + } + } +}) +export default class Identities extends Vue { + identities: IPerson[] = []; + loggedPerson!: IPerson; + newPerson!: IPerson; + showCreateProfileForm: boolean = false; + errors: string[] = []; + + async createProfile(e) { + e.preventDefault(); + + try { + await this.$apollo.mutate({ + mutation: CREATE_PERSON, + variables: this.newPerson + }); + this.showCreateProfileForm = false; + this.$apollo.queries.identities.refresh(); + } catch (err) { + console.error(err); + err.graphQLErrors.forEach(({ message }) => { + this.errors.push(message); + }); + } + } + + host() { + return `@${window.location.host}`; + } +} +</script> diff --git a/js/src/views/Account/Profile.vue b/js/src/views/Account/Profile.vue new file mode 100644 index 000000000..a06f9d716 --- /dev/null +++ b/js/src/views/Account/Profile.vue @@ -0,0 +1,111 @@ +<template> + <section> + <div class="columns"> + <div class="column"> + <div class="card" v-if="person"> + <div class="card-image" v-if="person.bannerUrl"> + <figure class="image"> + <img :src="person.bannerUrl"> + </figure> + </div> + <div class="card-content"> + <div class="media"> + <div class="media-left"> + <figure class="image is-48x48"> + <img :src="person.avatarUrl"> + </figure> + </div> + <div class="media-content"> + <p class="title">{{ person.name }}</p> + <p class="subtitle">@{{ person.preferredUsername }}</p> + </div> + </div> + + <div class="content"> + <p v-html="person.summary"></p> + </div> + </div> + <section v-if="person.organizedEvents.length > 0"> + <h2 class="subtitle"> + <translate>Organized</translate> + </h2> + <div class="columns"> + <EventCard + v-for="event in person.organizedEvents" + :event="event" + :hideDetails="true" + :key="event.uuid" + class="column is-one-third" + /> + </div> + <div class="field is-grouped"> + <p class="control"> + <a + class="button" + @click="logoutUser()" + v-if="loggedPerson && loggedPerson.id === person.id" + > + <translate>User logout</translate> + </a> + </p> + <p class="control"> + <a + class="button" + @click="deleteProfile()" + v-if="loggedPerson && loggedPerson.id === person.id" + > + <translate>Delete</translate> + </a> + </p> + </div> + </section> + </div> + </div> + </div> + </section> +</template> + +<script lang="ts"> +import { FETCH_PERSON, LOGGED_PERSON } from "@/graphql/actor"; +import { Component, Prop, Vue, Watch } from "vue-property-decorator"; +import EventCard from "@/components/Event/EventCard.vue"; + +@Component({ + apollo: { + person: { + query: FETCH_PERSON, + variables() { + return { + name: this.$route.params.name + }; + } + }, + loggedPerson: { + query: LOGGED_PERSON + } + }, + components: { + EventCard + } +}) +export default class Profile extends Vue { + @Prop({ type: String, required: true }) name!: string; + + person = null; + + // call again the method if the route changes + @Watch("$route") + onRouteChange() { + // this.fetchData() + } + + logoutUser() { + // TODO : implement logout + this.$router.push({ name: "Home" }); + } + + nl2br(text) { + return text.replace(/(?:\r\n|\r|\n)/g, "<br>"); + } +} +</script> diff --git a/js/src/views/Account/Register.vue b/js/src/views/Account/Register.vue new file mode 100644 index 000000000..543c23ff6 --- /dev/null +++ b/js/src/views/Account/Register.vue @@ -0,0 +1,182 @@ +<template> + <div> + <section class="hero"> + <div class="hero-body"> + <h1 class="title"> + <translate>Register an account on Mobilizon!</translate> + </h1> + </div> + </section> + <section> + <div class="container"> + <div class="columns is-mobile"> + <div class="column"> + <div class="content"> + <h2 class="subtitle" v-translate>Features</h2> + <ul> + <li v-translate>Create your communities and your events</li> + <li v-translate>Other stuff…</li> + </ul> + </div> + <p v-translate> + Learn more on + <a target="_blank" href="https://joinmobilizon.org">joinmobilizon.org</a> + </p> + <hr> + <div class="content"> + <h2 class="subtitle" v-translate>About this instance</h2> + <p> + <translate>Your local administrator resumed it's policy:</translate> + </p> + <ul> + <li v-translate>Please be nice to each other</li> + <li v-translate>meditate a bit</li> + </ul> + <p> + <translate>Please read the full rules</translate> + </p> + </div> + </div> + <div class="column"> + <form v-if="!validationSent"> + <div class="columns is-mobile is-centered"> + <div class="column is-narrow"> + <figure class="image is-64x64"> + <transition name="avatar"> + <v-gravatar v-bind="{email: credentials.email}" default-img="mp"></v-gravatar> + </transition> + </figure> + </div> + </div> + + <b-field label="Email"> + <b-input + aria-required="true" + required + type="email" + v-model="credentials.email" + @blur="showGravatar = true" + @focus="showGravatar = false" + /> + </b-field> + + <b-field label="Username"> + <b-input aria-required="true" required v-model="credentials.username"/> + </b-field> + + <b-field label="Password"> + <b-input + aria-required="true" + required + type="password" + password-reveal + minlength="6" + v-model="credentials.password" + /> + </b-field> + + <b-field grouped> + <div class="control"> + <button type="button" class="button is-primary" @click="submit()"> + <translate>Register</translate> + </button> + </div> + <div class="control"> + <router-link + class="button is-text" + :to="{ name: 'ResendConfirmation', params: { email: credentials.email }}" + > + <translate>Didn't receive the instructions ?</translate> + </router-link> + </div> + <div class="control"> + <router-link + class="button is-text" + :to="{ name: 'Login', params: { email: credentials.email, password: credentials.password }}" + :disabled="validationSent" + > + <translate>Login</translate> + </router-link> + </div> + </b-field> + </form> + + <div v-if="validationSent"> + <b-message title="Success" type="is-success"> + <h2> + <translate>A validation email was sent to %{email}</translate> + </h2> + <p> + <translate>Before you can login, you need to click on the link inside it to validate your account</translate> + </p> + </b-message> + </div> + </div> + </div> + </div> + </section> + </div> +</template> + +<script lang="ts"> +import Gravatar from "vue-gravatar"; +import { CREATE_USER } from "@/graphql/user"; +import { Component, Prop, Vue } from "vue-property-decorator"; +import { MOBILIZON_INSTANCE_HOST } from "@/api/_entrypoint"; + +@Component({ + components: { + "v-gravatar": Gravatar + } +}) +export default class Register extends Vue { + @Prop({ type: String, required: false, default: "" }) email!: string; + @Prop({ type: String, required: false, default: "" }) password!: string; + + credentials = { + username: "", + email: this.email, + password: this.password + } as { username: string; email: string; password: string }; + errors: string[] = []; + validationSent: boolean = false; + showGravatar: boolean = false; + + host() { + return MOBILIZON_INSTANCE_HOST; + } + + validEmail() { + return this.credentials.email.includes("@") === true + ? "v-gravatar" + : "avatar"; + } + + async submit() { + try { + this.validationSent = true; + await this.$apollo.mutate({ + mutation: CREATE_USER, + variables: this.credentials + }); + } catch (error) { + console.error(error); + } + } +} +</script> + +<style lang="scss"> +.avatar-enter-active { + transition: opacity 1s ease; +} + +.avatar-enter, +.avatar-leave-to { + opacity: 0; +} + +.avatar-leave { + display: none; +} +</style> diff --git a/js/src/views/Category/Create.vue b/js/src/views/Category/Create.vue new file mode 100644 index 000000000..6617bb72d --- /dev/null +++ b/js/src/views/Category/Create.vue @@ -0,0 +1,75 @@ +<template> + <section> + <h1 class="title"> + <translate>Create a new category</translate> + </h1> + <div class="columns"> + <form class="column" @submit="submit"> + <b-field :label="$gettext('Name of the category')"> + <b-input aria-required="true" required v-model="category.title"/> + </b-field> + + <b-field :label="$gettext('Description')"> + <b-input type="textarea" v-model="category.description"/> + </b-field> + + <b-field class="file"> + <b-upload v-model="file" @input="onFilePicked"> + <a class="button is-primary"> + <b-icon icon="upload"></b-icon> + <span> + <translate>Click to upload</translate> + </span> + </a> + </b-upload> + <span class="file-name" v-if="file">{{ this.image.name }}</span> + </b-field> + + <button class="button is-primary"> + <translate>Create the category</translate> + </button> + </form> + </div> + </section> +</template> + +<script lang="ts"> +import { CREATE_CATEGORY } from "@/graphql/category"; +import { Component, Vue } from "vue-property-decorator"; +import { ICategory } from "@/types/event.model"; + +/** + * TODO : No picture is uploaded ATM + */ + +@Component +export default class CreateCategory extends Vue { + category!: ICategory; + image = { + name: "" + } as { name: string }; + file: any = null; + + create() { + this.$apollo + .mutate({ + mutation: CREATE_CATEGORY, + variables: this.category + }) + .then(data => { + console.log(data); + }) + .catch(error => { + console.error(error); + }); + } + + // TODO : Check if we can upload as soon as file is picked and purge files not validated + onFilePicked(e) { + if (e === undefined || e.name.lastIndexOf(".") <= 0) { + console.error("File is incorrect"); + } + this.image.name = e.name; + } +} +</script> diff --git a/js/src/views/Category/List.vue b/js/src/views/Category/List.vue new file mode 100644 index 000000000..0fb1a6e70 --- /dev/null +++ b/js/src/views/Category/List.vue @@ -0,0 +1,55 @@ +<template> + <section> + <h1 class="title"> + <translate>Category List</translate> + </h1> + <b-loading :active.sync="$apollo.loading"></b-loading> + <div class="columns"> + <div class="column card" v-for="category in categories" :key="category.id"> + <div class="card-image"> + <figure class="image is-4by3"> + <img v-if="category.picture.url" :src="HTTP_ENDPOINT + category.picture.url"> + </figure> + </div> + <div class="card-content"> + <h2 class="title is-4">{{ category.title }}</h2> + <p>{{ category.description }}</p> + </div> + </div> + </div> + </section> +</template> + +<script lang="ts"> +import { FETCH_CATEGORIES } from "@/graphql/category"; +import { Component, Vue } from "vue-property-decorator"; + +// TODO : remove this hardcode + +@Component({ + apollo: { + categories: { + query: FETCH_CATEGORIES + } + } +}) +export default class List extends Vue { + categories = []; + loading = true; + HTTP_ENDPOINT = "http://localhost:4000"; + + deleteCategory(categoryId) { + const router = this.$router; + // FIXME: remove eventFetch + // eventFetch(`/categories/${categoryId}`, this.$store, { method: 'DELETE' }) + // .then(() => { + // this.categories = this.categories.filter(category => category.id !== categoryId); + // router.push('/category'); + // }); + } +} +</script> + +<!-- Add "scoped" attribute to limit CSS to this component only --> +<style scoped> +</style> diff --git a/js/src/views/Event/Create.vue b/js/src/views/Event/Create.vue new file mode 100644 index 000000000..b22b24a0b --- /dev/null +++ b/js/src/views/Event/Create.vue @@ -0,0 +1,165 @@ +<template> + <section> + <h1 class="title"> + <translate>Create a new event</translate> + </h1> + <div v-if="$apollo.loading">Loading...</div> + <div class="columns" v-else> + <form class="column" @submit="createEvent"> + <b-field :label="$gettext('Title')"> + <b-input aria-required="true" required v-model="event.title"/> + </b-field> + + <b-datepicker v-model="event.begins_on" inline></b-datepicker> + + <b-field :label="$gettext('Category')"> + <b-select placeholder="Select a category" v-model="event.category"> + <option + v-for="category in categories" + :value="category" + :key="category.title" + >{{ category.title }}</option> + </b-select> + </b-field> + + <button class="button is-primary"> + <translate>Create my event</translate> + </button> + </form> + </div> + </section> +</template> + +<script lang="ts"> +// import Location from '@/components/Location'; +import VueMarkdown from "vue-markdown"; +import { CREATE_EVENT, EDIT_EVENT } from "@/graphql/event"; +import { FETCH_CATEGORIES } from "@/graphql/category"; +import { AUTH_USER_ACTOR } from "@/constants"; +import { Component, Prop, Vue } from "vue-property-decorator"; +import { + IEvent, + ICategory, + EventVisibility, + EventStatus +} from "../../types/event.model"; +import { LOGGED_PERSON } from "../../graphql/actor"; +import { IPerson } from "../../types/actor.model"; + +@Component({ + components: { + VueMarkdown + }, + apollo: { + categories: { + query: FETCH_CATEGORIES + } + } +}) +export default class CreateEvent extends Vue { + @Prop({ required: false, type: String }) uuid!: string; + + loggedPerson!: IPerson; + categories: ICategory[] = []; + event!: IEvent; // FIXME: correctly type an event + + // created() { + // if (this.uuid) { + // this.fetchEvent(); + // } + // } + + async created() { + // We put initialization here because we need loggedPerson to be ready before initalizing event + const { data } = await this.$apollo.query({ query: LOGGED_PERSON }); + + this.loggedPerson = data.loggedPerson; + + this.event = { + title: "", + organizerActor: this.loggedPerson, + attributedTo: this.loggedPerson, + description: "", + begins_on: new Date(), + ends_on: new Date(), + category: this.categories[0], + participants: [], + uuid: "", + url: "", + local: true, + status: EventStatus.CONFIRMED, + visibility: EventVisibility.PUBLIC, + thumbnail: "", + large_image: "", + publish_at: new Date() + }; + } + + createEvent(e: Event) { + e.preventDefault(); + + if (this.uuid === undefined) { + this.$apollo + .mutate({ + mutation: CREATE_EVENT, + variables: { + title: this.event.title, + description: this.event.description, + beginsOn: this.event.begins_on, + category: this.event.category.title, + organizerActorId: this.event.organizerActor.id + } + }) + .then(data => { + console.log("event created", data); + this.$router.push({ + name: "Event", + params: { uuid: data.data.createEvent.uuid } + }); + }) + .catch(error => { + console.log(error); + }); + } else { + this.$apollo + .mutate({ + mutation: EDIT_EVENT + }) + .then(data => { + this.$router.push({ + name: "Event", + params: { uuid: data.data.uuid } + }); + }) + .catch(error => { + console.log(error); + }); + } + } + + // getAddressData(addressData) { + // if (addressData !== null) { + // this.event.address = { + // geom: { + // data: { + // latitude: addressData.latitude, + // longitude: addressData.longitude + // }, + // type: "point" + // }, + // addressCountry: addressData.country, + // addressLocality: addressData.locality, + // addressRegion: addressData.administrative_area_level_1, + // postalCode: addressData.postal_code, + // streetAddress: `${addressData.street_number} ${addressData.route}` + // }; + // } + // } +} +</script> + +<style> +.markdown-render h1 { + font-size: 2em; +} +</style> diff --git a/js/src/views/Event/Event.vue b/js/src/views/Event/Event.vue new file mode 100644 index 000000000..9863afee3 --- /dev/null +++ b/js/src/views/Event/Event.vue @@ -0,0 +1,196 @@ +<template> + <div class="columns is-centered"> + <div class="column is-three-quarters"> + <b-loading :active.sync="$apollo.loading"></b-loading> + <div class="card" v-if="event"> + <div class="card-image"> + <figure class="image is-4by3"> + <img src="https://picsum.photos/600/400/"> + </figure> + </div> + <div class="card-content"> + <span>{{ event.begins_on | formatDay }}</span> + <span class="tag is-primary">{{ event.category.title }}</span> + <h1 class="title">{{ event.title }}</h1> + <router-link + :to="{name: 'Profile', params: { name: event.organizerActor.preferredUsername } }" + > + <figure v-if="event.organizerActor.avatarUrl"> + <img :src="event.organizerActor.avatarUrl"> + </figure> + </router-link> + <span + v-if="event.organizerActor" + >Organisé par {{ event.organizerActor.name ? event.organizerActor.name : event.organizerActor.preferredUsername }}</span> + <div class="field has-addons"> + <p class="control"> + <router-link + v-if="actorIsOrganizer()" + class="button" + :to="{ name: 'EditEvent', params: {uuid: event.uuid}}" + > + <translate>Edit</translate> + </router-link> + </p> + <p class="control"> + <a class="button" @click="downloadIcsEvent()"> + <translate>Download</translate> + </a> + </p> + <p class="control"> + <a class="button is-danger" v-if="actorIsOrganizer()" @click="deleteEvent()"> + <translate>Delete</translate> + </a> + </p> + </div> + <div> + <span>{{ event.begins_on | formatDate }} - {{ event.ends_on | formatDate }}</span> + </div> + <p v-if="actorIsOrganizer()"> + <translate>Vous êtes organisateur de cet événement.</translate> + </p> + <div v-else> + <p v-if="actorIsParticipant()"> + <translate>Vous avez annoncé aller à cet événement.</translate> + </p> + <p v-else> + Vous y allez ? + <span>{{ event.participants.length }} personnes y vont.</span> + </p> + </div> + <div v-if="!actorIsOrganizer()"> + <a v-if="!actorIsParticipant()" @click="joinEvent" class="button"> + <translate>Join</translate> + </a> + <a v-if="actorIsParticipant()" @click="leaveEvent" color="button">Leave</a> + </div> + <h2 class="subtitle">Details</h2> + <p v-if="event.description"> + <vue-markdown :source="event.description"></vue-markdown> + </p> + <h2 class="subtitle">Participants</h2> + <span v-if="event.participants.length === 0">No participants yet.</span> + <div class="columns"> + <router-link + class="card column" + v-for="participant in event.participants" + :key="participant.preferredUsername" + :to="{name: 'Profile', params: { name: participant.actor.preferredUsername }}" + > + <div> + <figure> + <img v-if="!participant.actor.avatarUrl" src="https://picsum.photos/125/125/"> + <img v-else :src="participant.actor.avatarUrl"> + </figure> + <span>{{ participant.actor.preferredUsername }}</span> + </div> + </router-link> + </div> + </div> + </div> + </div> + </div> +</template> + +<script lang="ts"> +import { FETCH_EVENT } from "@/graphql/event"; +import { Component, Prop, Vue } from "vue-property-decorator"; +import VueMarkdown from "vue-markdown"; +import { LOGGED_PERSON } from "../../graphql/actor"; +import { IEvent } from "@/types/event.model"; +import { JOIN_EVENT } from "../../graphql/event"; +import { IPerson } from "@/types/actor.model"; + +@Component({ + components: { + VueMarkdown + }, + apollo: { + event: { + query: FETCH_EVENT, + variables() { + return { + uuid: this.uuid + }; + } + }, + loggedPerson: { + query: LOGGED_PERSON + } + } +}) +export default class Event extends Vue { + @Prop({ type: String, required: true }) uuid!: string; + + event!: IEvent; + loggedPerson!: IPerson; + validationSent: boolean = false; + + deleteEvent() { + const router = this.$router; + // FIXME: remove eventFetch + // eventFetch(`/events/${this.uuid}`, this.$store, { method: 'DELETE' }) + // .then(() => router.push({ name: 'EventList' })); + } + + async joinEvent() { + try { + this.validationSent = true; + await this.$apollo.mutate({ + mutation: JOIN_EVENT + }); + } catch (error) { + console.error(error); + } + } + + leaveEvent() { + // FIXME: remove eventFetch + // eventFetch(`/events/${this.uuid}/leave`, this.$store) + // .then(response => response.json()) + // .then((data) => { + // console.log(data); + // }); + } + + downloadIcsEvent() { + // FIXME: remove eventFetch + // eventFetch(`/events/${this.uuid}/ics`, this.$store, { responseType: 'arraybuffer' }) + // .then(response => response.text()) + // .then((response) => { + // const blob = new Blob([ response ], { type: 'text/calendar' }); + // const link = document.createElement('a'); + // link.href = window.URL.createObjectURL(blob); + // link.download = `${this.event.title}.ics`; + // document.body.appendChild(link); + // link.click(); + // document.body.removeChild(link); + // }); + } + + actorIsParticipant() { + return ( + (this.loggedPerson && + this.event.participants + .map(participant => participant.actor.preferredUsername) + .includes(this.loggedPerson.preferredUsername)) || + this.actorIsOrganizer() + ); + } + // + actorIsOrganizer() { + return ( + this.loggedPerson && + this.loggedPerson.preferredUsername === + this.event.organizerActor.preferredUsername + ); + } +} +</script> + +<!-- Add "scoped" attribute to limit CSS to this component only --> +<style> +.v-card__media__background { + filter: contrast(0.4); +} +</style> diff --git a/js/src/views/Event/EventList.vue b/js/src/views/Event/EventList.vue new file mode 100644 index 000000000..be25f0892 --- /dev/null +++ b/js/src/views/Event/EventList.vue @@ -0,0 +1,111 @@ +<template> + <section> + <h1> + <translate>Event list</translate> + </h1> + <b-loading :active.sync="$apollo.loading"></b-loading> + <div v-if="events.length > 0" class="columns is-multiline"> + <EventCard + v-for="event in events" + :key="event.uuid" + :event="event" + class="column is-one-quarter-desktop is-half-mobile" + /> + </div> + <b-message v-if-else="events.length === 0 && $apollo.loading === false" type="is-danger"> + <translate>No events found</translate> + </b-message> + </section> +</template> + +<script lang="ts"> +import ngeohash from "ngeohash"; +import VueMarkdown from "vue-markdown"; +import { Component, Prop, Vue, Watch } from "vue-property-decorator"; +import EventCard from "@/components/Event/EventCard.vue"; + +@Component({ + components: { + VueMarkdown, + EventCard + } +}) +export default class EventList extends Vue { + @Prop(String) location!: string; + + events = []; + loading = true; + locationChip = false; + locationText = ""; + + created() { + this.fetchData(this.$router.currentRoute.params["location"]); + } + + beforeRouteUpdate(to, from, next) { + this.fetchData(to.params.location); + next(); + } + + @Watch("locationChip") + onLocationChipChange(val) { + if (val === false) { + this.$router.push({ name: "EventList" }); + } + } + + geocode(lat, lon) { + console.log({ lat, lon }); + console.log(ngeohash.encode(lat, lon, 10)); + return ngeohash.encode(lat, lon, 10); + } + + fetchData(location) { + let queryString = "/events"; + if (location) { + queryString += `?geohash=${location}`; + const { latitude, longitude } = ngeohash.decode(location); + this.locationText = `${latitude.toString()} : ${longitude.toString()}`; + } + this.locationChip = true; + // FIXME: remove eventFetch + // eventFetch(queryString, this.$store) + // .then(response => response.json()) + // .then((response) => { + // this.loading = false; + // this.events = response.data; + // console.log(this.events); + // }); + } + + deleteEvent(event) { + const router = this.$router; + // FIXME: remove eventFetch + // eventFetch(`/events/${event.uuid}`, this.$store, { method: 'DELETE' }) + // .then(() => router.push('/events')); + } + + viewEvent(event) { + this.$router.push({ name: "Event", params: { uuid: event.uuid } }); + } + + downloadIcsEvent(event) { + // FIXME: remove eventFetch + // eventFetch(`/events/${event.uuid}/ics`, this.$store, { responseType: 'arraybuffer' }) + // .then(response => response.text()) + // .then((response) => { + // const blob = new Blob([ response ], { type: 'text/calendar' }); + // const link = document.createElement('a'); + // link.href = window.URL.createObjectURL(blob); + // link.download = `${event.title}.ics`; + // document.body.appendChild(link); + // link.click(); + // document.body.removeChild(link); + // }); + } +} +</script> + +<!-- Add "scoped" attribute to limit CSS to this component only --> +<style scoped> +</style> diff --git a/js/src/views/Group/Create.vue b/js/src/views/Group/Create.vue new file mode 100644 index 000000000..fdf751e01 --- /dev/null +++ b/js/src/views/Group/Create.vue @@ -0,0 +1,98 @@ +<template> + <section> + <h1> + <translate>Create a new group</translate> + </h1> + <div class="columns"> + <form class="column" @submit="createGroup"> + <b-field :label="$gettext('Group name')"> + <b-input aria-required="true" required v-model="group.preferred_username"/> + </b-field> + + <b-field :label="$gettext('Group full name')"> + <b-input aria-required="true" required v-model="group.name"/> + </b-field> + + <b-field :label="$gettext('Description')"> + <b-input aria-required="true" required v-model="group.summary" type="textarea"/> + </b-field> + + <button class="button is-primary"> + <translate>Create my group</translate> + </button> + </form> + </div> + </section> +</template> + +<script lang="ts"> +import VueMarkdown from "vue-markdown"; +import { Component, Vue } from "vue-property-decorator"; + +@Component({ + components: { + VueMarkdown + } +}) +export default class CreateGroup extends Vue { + e1 = 0; + // FIXME: correctly type group + group: { + preferred_username: string; + name: string; + summary: string; + address?: any; + } = { + preferred_username: "", + name: "", + summary: "" + // category: null, + }; + categories = []; + + mounted() { + this.fetchCategories(); + } + + createGroup() { + // this.group.organizer = "/accounts/" + this.$store.state.user.id; + // FIXME: remove eventFetch + // eventFetch('/groups', this.$store, { method: 'POST', body: JSON.stringify({ group: this.group }) }) + // .then(response => response.json()) + // .then((data) => { + // this.loading = false; + // this.$router.push({ path: 'Group', params: { id: data.id } }); + // }); + } + + fetchCategories() { + // FIXME: remove eventFetch + // eventFetch('/categories', this.$store) + // .then(response => response.json()) + // .then((data) => { + // this.loading = false; + // this.categories = data.data; + // }); + } + + getAddressData(addressData) { + this.group.address = { + geo: { + latitude: addressData.latitude, + longitude: addressData.longitude + }, + addressCountry: addressData.country, + addressLocality: addressData.city, + addressRegion: addressData.administrative_area_level_1, + postalCode: addressData.postal_code, + streetAddress: `${addressData.street_number} ${addressData.route}` + }; + } +} +</script> + +<style> +.markdown-render h1 { + font-size: 2em; +} +</style> diff --git a/js/src/views/Group/Group.vue b/js/src/views/Group/Group.vue new file mode 100644 index 000000000..018468279 --- /dev/null +++ b/js/src/views/Group/Group.vue @@ -0,0 +1,112 @@ +<template> + <section> + <div class="columns"> + <div class="column"> + <div class="card" v-if="group"> + <div class="card-image" v-if="group.bannerUrl"> + <figure class="image"> + <img :src="group.bannerUrl"> + </figure> + </div> + <div class="card-content"> + <div class="media"> + <div class="media-left"> + <figure class="image is-48x48"> + <img :src="group.avatarUrl"> + </figure> + </div> + <div class="media-content"> + <p class="title">{{ group.name }}</p> + <p class="subtitle">@{{ group.preferredUsername }}</p> + </div> + </div> + + <div class="content"> + <p v-html="group.summary"></p> + </div> + </div> + <section v-if="group.organizedEvents.length > 0"> + <h2 class="subtitle"> + <translate>Organized</translate> + </h2> + <div class="columns"> + <EventCard + v-for="event in group.organizedEvents" + :event="event" + :hideDetails="true" + :key="event.uuid" + class="column is-one-third" + /> + </div> + </section> + <section v-if="group.members.length > 0"> + <h2 class="subtitle"> + <translate>Members</translate> + </h2> + <div class="columns"> + <span + v-for="member in group.members" + :key="member" + >{{ member.actor.preferredUsername }}</span> + </div> + </section> + </div> + <b-message v-if-else="!group && $apollo.loading === false" type="is-danger"> + <translate>No group found</translate> + </b-message> + </div> + </div> + </section> +</template> + +<script lang="ts"> +import { Component, Prop, Vue, Watch } from "vue-property-decorator"; +import EventCard from "@/components/Event/EventCard.vue"; +import { FETCH_PERSON, LOGGED_PERSON } from "@/graphql/actor"; + +@Component({ + apollo: { + person: { + query: FETCH_PERSON, + variables() { + return { + name: this.$route.params.name + }; + } + }, + loggedPerson: { + query: LOGGED_PERSON + } + }, + components: { + EventCard + } +}) +export default class Group extends Vue { + @Prop({ type: String, required: true }) name!: string; + + group = null; + loading = true; + + created() { + this.fetchData(); + } + + @Watch("$route") + onRouteChanged() { + // call again the method if the route changes + this.fetchData(); + } + + fetchData() { + // FIXME: remove eventFetch + // eventFetch(`/actors/${this.name}`, this.$store) + // .then(response => response.json()) + // .then((response) => { + // this.group = response.data; + // this.loading = false; + // console.log(this.group); + // }); + } +} +</script> diff --git a/js/src/views/Group/GroupList.vue b/js/src/views/Group/GroupList.vue new file mode 100644 index 000000000..e5f5872a6 --- /dev/null +++ b/js/src/views/Group/GroupList.vue @@ -0,0 +1,75 @@ +<template> + <section> + <h1> + <translate>Group List</translate> + </h1> + <b-loading :active.sync="$apollo.loading"></b-loading> + <div class="columns"> + <GroupCard + v-for="group in groups" + :key="group.uuid" + :group="group" + class="column is-one-quarter-desktop is-half-mobile" + /> + </div> + <router-link class="button" :to="{ name: 'CreateGroup' }"> + <translate>Create group</translate> + </router-link> + </section> +</template> + +<script lang="ts"> +import { Component, Vue } from "vue-property-decorator"; + +@Component +export default class GroupList extends Vue { + groups = []; + loading = true; + + created() { + this.fetchData(); + } + + usernameWithDomain(actor) { + return actor.username + (actor.domain === null ? "" : `@${actor.domain}`); + } + + fetchData() { + // FIXME: remove eventFetch + // eventFetch('/groups', this.$store) + // .then(response => response.json()) + // .then((data) => { + // console.log(data); + // this.loading = false; + // this.groups = data.data; + // }); + } + + deleteGroup(group) { + const router = this.$router; + // FIXME: remove eventFetch + // eventFetch(`/groups/${this.usernameWithDomain(group)}`, this.$store, { method: 'DELETE' }) + // .then(response => response.json()) + // .then(() => router.push('/groups')); + } + + viewActor(actor) { + this.$router.push({ + name: "Group", + params: { name: this.usernameWithDomain(actor) } + }); + } + + joinGroup(group) { + const router = this.$router; + // FIXME: remove eventFetch + // eventFetch(`/groups/${this.usernameWithDomain(group)}/join`, this.$store, { method: 'POST' }) + // .then(response => response.json()) + // .then(() => router.push({ name: 'Group', params: { name: this.usernameWithDomain(group) } })); + } +} +</script> + +<!-- Add "scoped" attribute to limit CSS to this component only --> +<style scoped> +</style> diff --git a/js/src/views/Home.vue b/js/src/views/Home.vue new file mode 100644 index 000000000..3025b60f2 --- /dev/null +++ b/js/src/views/Home.vue @@ -0,0 +1,150 @@ +<template> + <div> + <section class="hero is-link" v-if="!currentUser.id || !loggedPerson"> + <div class="hero-body"> + <div class="container"> + <h1 class="title">Find events you like</h1> + <h2 class="subtitle">Share them with Mobilizon</h2> + <router-link class="button" :to="{ name: 'Register' }"> + <translate>Register</translate> + </router-link> + </div> + </div> + </section> + <section v-else> + <h1> + <translate + :translate-params="{username: loggedPerson.preferredUsername}" + >Welcome back %{username}</translate> + </h1> + </section> + <section> + <span class="events-nearby title">Events nearby you</span> + <b-loading :active.sync="$apollo.loading"></b-loading> + <div v-if="events.length > 0" class="columns is-multiline"> + <EventCard + v-for="event in events" + :key="event.uuid" + :event="event" + class="column is-one-quarter-desktop is-half-mobile" + /> + </div> + <b-message v-else type="is-danger"> + <translate>No events found</translate> + </b-message> + </section> + </div> +</template> + +<script lang="ts"> +import ngeohash from "ngeohash"; +import { AUTH_USER_ACTOR, AUTH_USER_ID } from "@/constants"; +import { FETCH_EVENTS } from "@/graphql/event"; +import { Component, Vue } from "vue-property-decorator"; +import EventCard from "@/components/Event/EventCard.vue"; +import { LOGGED_PERSON } from "@/graphql/actor"; +import { IPerson } from "@/types/actor.model"; +import { ICurrentUser } from "@/types/current-user.model"; +import { CURRENT_USER_CLIENT } from "@/graphql/user"; + +@Component({ + apollo: { + events: { + query: FETCH_EVENTS, + fetchPolicy: "no-cache" // Debug me: https://github.com/apollographql/apollo-client/issues/3030 + }, + loggedPerson: { + query: LOGGED_PERSON + }, + currentUser: { + query: CURRENT_USER_CLIENT + } + }, + components: { + EventCard + } +}) +export default class Home extends Vue { + searchTerm = null; + location_field = { + loading: false, + search: null + }; + events = []; + locations = []; + city = { name: null }; + country = { name: null }; + // FIXME: correctly parse local storage + loggedPerson!: IPerson; + currentUser!: ICurrentUser; + + get displayed_name() { + return this.loggedPerson.name === null + ? this.loggedPerson.preferredUsername + : this.loggedPerson.name; + } + + fetchLocations() { + // FIXME: remove eventFetch + // eventFetch('/locations', this.$store) + // .then(response => (response.json())) + // .then((response) => { + // this.locations = response; + // }); + } + + geoLocalize() { + const router = this.$router; + const sessionCity = sessionStorage.getItem("City"); + if (sessionCity) { + router.push({ name: "EventList", params: { location: sessionCity } }); + } else { + navigator.geolocation.getCurrentPosition( + pos => { + const crd = pos.coords; + + const geohash = ngeohash.encode(crd.latitude, crd.longitude, 11); + sessionStorage.setItem("City", geohash); + router.push({ name: "EventList", params: { location: geohash } }); + }, + err => console.warn(`ERROR(${err.code}): ${err.message}`), + { + enableHighAccuracy: true, + timeout: 5000, + maximumAge: 0 + } + ); + } + } + + getAddressData(addressData) { + const geohash = ngeohash.encode( + addressData.latitude, + addressData.longitude, + 11 + ); + sessionStorage.setItem("City", geohash); + this.$router.push({ name: "EventList", params: { location: geohash } }); + } + + viewEvent(event) { + this.$router.push({ name: "Event", params: { uuid: event.uuid } }); + } + + ipLocation() { + return this.city.name ? this.city.name : this.country.name; + } +} +</script> + +<!-- Add "scoped" attribute to limit CSS to this component only --> +<style scoped> +.search-autocomplete { + border: 1px solid #dbdbdb; + color: rgba(0, 0, 0, 0.87); +} + +.events-nearby { + margin-bottom: 25px; +} +</style> diff --git a/js/src/views/Location.vue b/js/src/views/Location.vue new file mode 100644 index 000000000..0c0c7dd8d --- /dev/null +++ b/js/src/views/Location.vue @@ -0,0 +1,30 @@ +<template> + <div>{{ center.lat }} - {{ center.lng }}</div> +</template> + +<script lang="ts"> +import { Component, Prop, Vue } from "vue-property-decorator"; + +@Component +export default class Location extends Vue { + @Prop(String) address!: string; + + description = "Paris, France"; + center = { lat: 48.85, lng: 2.35 }; + markers: any[] = []; + + setPlace(place) { + this.center = { + lat: place.geometry.location.lat(), + lng: place.geometry.location.lng() + }; + this.markers = [ + { + position: { lat: this.center.lat, lng: this.center.lng } + } + ]; + + this.$emit("input", place.formatted_address); + } +} +</script> diff --git a/js/src/views/PageNotFound.vue b/js/src/views/PageNotFound.vue new file mode 100644 index 000000000..14530ae31 --- /dev/null +++ b/js/src/views/PageNotFound.vue @@ -0,0 +1,8 @@ +<template> + <section> + <h1> + <translate>Page not found!</translate> + <img src="../assets/oh_no.jpg"> + </h1> + </section> +</template> diff --git a/js/src/views/User/Login.vue b/js/src/views/User/Login.vue new file mode 100644 index 000000000..12c66129e --- /dev/null +++ b/js/src/views/User/Login.vue @@ -0,0 +1,137 @@ +<template> + <div> + <section class="hero"> + <h1 class="title"> + <translate>Welcome back!</translate> + </h1> + </section> + <section> + <div class="columns is-mobile is-centered"> + <div class="column is-half card"> + <b-message title="Error" type="is-danger" v-for="error in errors" :key="error">{{ error }}</b-message> + <form @submit="loginAction"> + <b-field label="Email"> + <b-input aria-required="true" required type="email" v-model="credentials.email"/> + </b-field> + + <b-field label="Password"> + <b-input + aria-required="true" + required + type="password" + password-reveal + v-model="credentials.password" + /> + </b-field> + + <div class="control has-text-centered"> + <button class="button is-primary is-large"> + <translate>Login</translate> + </button> + </div> + <div class="control"> + <router-link + class="button is-text" + :to="{ name: 'SendPasswordReset', params: { email: credentials.email }}" + > + <translate>Forgot your password ?</translate> + </router-link> + </div> + <div class="control"> + <router-link + class="button is-text" + :to="{ name: 'Register', params: { default_email: credentials.email, default_password: credentials.password }}" + > + <translate>Register</translate> + </router-link> + </div> + </form> + </div> + </div> + </section> + </div> +</template> + +<script lang="ts"> +import Gravatar from "vue-gravatar"; +import { Component, Prop, Vue } from "vue-property-decorator"; +import { LOGIN } from "@/graphql/auth"; +import { validateEmailField, validateRequiredField } from "@/utils/validators"; +import { saveUserData } from "@/utils/auth"; +import { ILogin } from "@/types/login.model"; +import { UPDATE_CURRENT_USER_CLIENT } from "@/graphql/user"; +import { onLogin } from "@/vue-apollo"; + +@Component({ + components: { + "v-gravatar": Gravatar + } +}) +export default class Login extends Vue { + @Prop({ type: String, required: false, default: "" }) email!: string; + @Prop({ type: String, required: false, default: "" }) password!: string; + + credentials = { + email: "", + password: "" + }; + validationSent = false; + errors: string[] = []; + rules = { + required: validateRequiredField, + email: validateEmailField + }; + user: any; + + beforeCreate() { + if (this.user) { + this.$router.push("/"); + } + } + + mounted() { + this.credentials.email = this.email; + this.credentials.password = this.password; + } + + async loginAction(e: Event) { + e.preventDefault(); + this.errors.splice(0); + + try { + const result = await this.$apollo.mutate<{ login: ILogin }>({ + mutation: LOGIN, + variables: { + email: this.credentials.email, + password: this.credentials.password + } + }); + + saveUserData(result.data.login); + + await this.$apollo.mutate({ + mutation: UPDATE_CURRENT_USER_CLIENT, + variables: { + id: result.data.login.user.id, + email: this.credentials.email + } + }); + + onLogin(this.$apollo); + + this.$router.push({ name: "Home" }); + } catch (err) { + console.error(err); + err.graphQLErrors.forEach(({ message }) => { + this.errors.push(message); + }); + } + } + + validEmail() { + return this.rules.email(this.credentials.email) === true + ? "v-gravatar" + : "avatar"; + } +} +</script> diff --git a/js/src/views/User/PasswordReset.vue b/js/src/views/User/PasswordReset.vue new file mode 100644 index 000000000..1b0eae57b --- /dev/null +++ b/js/src/views/User/PasswordReset.vue @@ -0,0 +1,91 @@ +<template> + <section class="columns is-mobile is-centered"> + <div class="card column is-half-desktop"> + <h1> + <translate>Password reset</translate> + </h1> + <b-message title="Error" type="is-danger" v-for="error in errors" :key="error">{{ error }}</b-message> + <form @submit="resetAction"> + <b-field label="Password"> + <b-input + aria-required="true" + required + type="password" + password-reveal + minlength="6" + v-model="credentials.password" + /> + </b-field> + <b-field label="Password (confirmation)"> + <b-input + aria-required="true" + required + type="password" + password-reveal + minlength="6" + v-model="credentials.password_confirmation" + /> + </b-field> + <button class="button is-primary"> + <translate>Reset my password</translate> + </button> + </form> + </div> + </section> +</template> + +<script lang="ts"> +import { Component, Prop, Vue } from "vue-property-decorator"; +import { validateRequiredField } from "@/utils/validators"; +import { RESET_PASSWORD } from "@/graphql/auth"; +import { saveUserData } from "@/utils/auth"; +import { ILogin } from "@/types/login.model"; + +@Component +export default class PasswordReset extends Vue { + @Prop({ type: String, required: true }) token!: string; + + credentials = { + password: "", + password_confirmation: "" + } as { password: string; password_confirmation: string }; + errors: string[] = []; + rules = { + password_length: value => + value.length > 6 || "Password must be at least 6 characters long", + required: validateRequiredField, + password_equal: value => + value === this.credentials.password || "Passwords must be the same" + }; + + get samePasswords() { + return ( + this.rules.password_length(this.credentials.password) === true && + this.credentials.password === this.credentials.password_confirmation + ); + } + + async resetAction(e) { + e.preventDefault(); + this.errors.splice(0); + + try { + const result = await this.$apollo.mutate<{ resetPassword: ILogin }>({ + mutation: RESET_PASSWORD, + variables: { + password: this.credentials.password, + token: this.token + } + }); + + saveUserData(result.data.resetPassword); + this.$router.push({ name: "Home" }); + } catch (err) { + console.error(err); + err.graphQLErrors.forEach(({ message }) => { + this.errors.push(message); + }); + } + } +} +</script> diff --git a/js/src/views/User/ResendConfirmation.vue b/js/src/views/User/ResendConfirmation.vue new file mode 100644 index 000000000..b0e566270 --- /dev/null +++ b/js/src/views/User/ResendConfirmation.vue @@ -0,0 +1,77 @@ +<template> + <section class="columns"> + <div class="column card"> + <h1 class="title"> + <translate>Resend confirmation email</translate> + </h1> + <form v-if="!validationSent" @submit="resendConfirmationAction"> + <b-field label="Email"> + <b-input aria-required="true" required type="email" v-model="credentials.email"/> + </b-field> + <button class="button is-primary"> + <translate>Send confirmation email again</translate> + </button> + </form> + <div v-else> + <b-message type="is-success" :closable="false" title="Success"> + <translate + :translate-params="{email: credentials.email}" + >If an account with this email exists, we just sent another confirmation email to %{email}</translate> + </b-message> + <b-message type="is-info"> + <translate>Please check you spam folder if you didn't receive the email.</translate> + </b-message> + </div> + </div> + </section> +</template> + +<script lang="ts"> +import { Component, Prop, Vue } from "vue-property-decorator"; +import { validateEmailField, validateRequiredField } from "@/utils/validators"; +import { RESEND_CONFIRMATION_EMAIL } from "@/graphql/auth"; + +@Component +export default class ResendConfirmation extends Vue { + @Prop({ type: String, required: false, default: "" }) email!: string; + + credentials = { + email: "" + }; + validationSent = false; + error = false; + state = { + email: { + status: null, + msg: "" + } + }; + rules = { + required: validateRequiredField, + email: validateEmailField + }; + + mounted() { + this.credentials.email = this.email; + } + + async resendConfirmationAction(e) { + e.preventDefault(); + this.error = false; + + try { + await this.$apollo.mutate({ + mutation: RESEND_CONFIRMATION_EMAIL, + variables: { + email: this.credentials.email + } + }); + } catch (err) { + console.error(err); + this.error = true; + } finally { + this.validationSent = true; + } + } +} +</script> diff --git a/js/src/views/User/SendPasswordReset.vue b/js/src/views/User/SendPasswordReset.vue new file mode 100644 index 000000000..6b75c8b3d --- /dev/null +++ b/js/src/views/User/SendPasswordReset.vue @@ -0,0 +1,89 @@ +<template> + <section class="columns"> + <div class="card column"> + <h1 class="title"> + <translate>Password reset</translate> + </h1> + <b-message title="Error" type="is-danger" v-for="error in errors" :key="error">{{ error }}</b-message> + <form @submit="sendResetPasswordTokenAction" v-if="!validationSent"> + <b-field label="Email"> + <b-input aria-required="true" required type="email" v-model="credentials.email"/> + </b-field> + <button class="button is-primary"> + <translate>Send email to reset my password</translate> + </button> + </form> + <div v-else> + <b-message type="is-success" :closable="false" title="Success"> + <translate + :translate-params="{email: credentials.email}" + >We just sent an email to %{email}</translate> + </b-message> + <b-message type="is-info"> + <translate>Please check you spam folder if you didn't receive the email.</translate> + </b-message> + </div> + </div> + </section> +</template> + +<script lang="ts"> +import { Component, Prop, Vue } from "vue-property-decorator"; +import { validateEmailField, validateRequiredField } from "@/utils/validators"; +import { SEND_RESET_PASSWORD } from "@/graphql/auth"; + +@Component +export default class SendPasswordReset extends Vue { + @Prop({ type: String, required: false, default: "" }) email!: string; + + credentials = { + email: "" + } as { email: string }; + validationSent: boolean = false; + errors: string[] = []; + state = { + email: { + status: null, + msg: "" + } as { status: boolean | null; msg: string } + }; + + rules = { + required: validateRequiredField, + email: validateEmailField + }; + + mounted() { + this.credentials.email = this.email; + } + + async sendResetPasswordTokenAction(e) { + e.preventDefault(); + + try { + await this.$apollo.mutate({ + mutation: SEND_RESET_PASSWORD, + variables: { + email: this.credentials.email + } + }); + + this.validationSent = true; + } catch (err) { + console.error(err); + err.graphQLErrors.forEach(({ message }) => { + this.errors.push(message); + }); + } + } + + resetState() { + this.state = { + email: { + status: null, + msg: "" + } + }; + } +} +</script> diff --git a/js/src/views/User/Validate.vue b/js/src/views/User/Validate.vue new file mode 100644 index 000000000..801a13a9a --- /dev/null +++ b/js/src/views/User/Validate.vue @@ -0,0 +1,59 @@ +<template> + <section> + <h1 class="title" v-if="loading"> + <translate>Your account is being validated</translate> + </h1> + <div v-else> + <div v-if="failed"> + <b-message title="Error" type="is-danger"> + <translate>Error while validating account</translate> + </b-message> + </div> + <h1 class="title" v-else> + <translate>Your account has been validated</translate> + </h1> + </div> + </section> +</template> + +<script lang="ts"> +import { VALIDATE_USER } from "@/graphql/user"; +import { Component, Prop, Vue } from "vue-property-decorator"; +import { AUTH_TOKEN, AUTH_USER_ID } from "@/constants"; + +@Component +export default class Validate extends Vue { + @Prop({ type: String, required: true }) token!: string; + + loading = true; + failed = false; + + created() { + this.validateAction(); + } + + async validateAction() { + try { + const data = await this.$apollo.mutate({ + mutation: VALIDATE_USER, + variables: { + token: this.token + } + }); + + this.saveUserData(data.data); + this.$router.push({ name: "Home" }); + } catch (err) { + console.error(err); + this.failed = true; + } finally { + this.loading = false; + } + } + + saveUserData({ validateUser: login }) { + localStorage.setItem(AUTH_USER_ID, login.user.id); + localStorage.setItem(AUTH_TOKEN, login.token); + } +} +</script> diff --git a/js/tsconfig.json b/js/tsconfig.json index f28e98af7..38ee4457f 100644 --- a/js/tsconfig.json +++ b/js/tsconfig.json @@ -39,4 +39,4 @@ "exclude": [ "node_modules" ] -} +} \ No newline at end of file diff --git a/lib/mobilizon/actors/actor.ex b/lib/mobilizon/actors/actor.ex index 8972d000b..8dacdfa5c 100644 --- a/lib/mobilizon/actors/actor.ex +++ b/lib/mobilizon/actors/actor.ex @@ -42,7 +42,7 @@ defmodule Mobilizon.Actors.Actor do field(:shared_inbox_url, :string) field(:type, Mobilizon.Actors.ActorTypeEnum, default: :Person) field(:name, :string) - field(:domain, :string) + field(:domain, :string, default: nil) field(:summary, :string) field(:preferred_username, :string) field(:keys, :string) diff --git a/lib/mobilizon/actors/actors.ex b/lib/mobilizon/actors/actors.ex index dbc37865a..576d799ec 100644 --- a/lib/mobilizon/actors/actors.ex +++ b/lib/mobilizon/actors/actors.ex @@ -77,9 +77,30 @@ defmodule Mobilizon.Actors do Repo.all(from(a in Actor, where: a.user_id == ^user_id)) end - def get_actor_with_everything!(id) do - actor = Repo.get!(Actor, id) - Repo.preload(actor, [:organized_events, :followers, :followings]) + @spec get_actor_with_everything(integer()) :: Ecto.Query + defp do_get_actor_with_everything(id) do + from(a in Actor, where: a.id == ^id, preload: [:organized_events, :followers, :followings]) + end + + @doc """ + Returns an actor with every relation + """ + @spec get_actor_with_everything(integer()) :: Mobilizon.Actors.Actor.t() + def get_actor_with_everything(id) do + id + |> do_get_actor_with_everything + |> Repo.one() + end + + @doc """ + Returns an actor with every relation + """ + @spec get_local_actor_with_everything(integer()) :: Mobilizon.Actors.Actor.t() + def get_local_actor_with_everything(id) do + id + |> do_get_actor_with_everything + |> where([a], is_nil(a.domain)) + |> Repo.one() end @doc """ @@ -610,6 +631,19 @@ defmodule Mobilizon.Actors do {:error, hd(email_msg)} end + @doc """ + Create a new person actor + """ + def new_person(args) do + key = :public_key.generate_key({:rsa, 2048, 65_537}) + entry = :public_key.pem_entry_encode(:RSAPrivateKey, key) + pem = [entry] |> :public_key.pem_encode() |> String.trim_trailing() + args = Map.put(args, :keys, pem) + + actor = Mobilizon.Actors.Actor.registration_changeset(%Mobilizon.Actors.Actor{}, args) + Mobilizon.Repo.insert(actor) + end + def register_bot_account(%{name: name, summary: summary}) do key = :public_key.generate_key({:rsa, 2048, 65_537}) entry = :public_key.pem_entry_encode(:RSAPrivateKey, key) diff --git a/lib/mobilizon/actors/service/reset_password.ex b/lib/mobilizon/actors/service/reset_password.ex index 0b03c6cd9..cd5fe478a 100644 --- a/lib/mobilizon/actors/service/reset_password.ex +++ b/lib/mobilizon/actors/service/reset_password.ex @@ -24,10 +24,12 @@ defmodule Mobilizon.Actors.Service.ResetPassword do {:ok, user} else {:error, %Ecto.Changeset{errors: [password: {"registration.error.password_too_short", _}]}} -> - {:error, :password_too_short} + {:error, + "The password you have choosen is too short. Please make sure your password contains at least 6 charaters."} _err -> - {:error, :invalid_token} + {:error, + "The token you provided is invalid. Make sure that the URL is exactly the one provided inside the email you got."} end end diff --git a/lib/mobilizon/events/event.ex b/lib/mobilizon/events/event.ex index 6f36bcc45..58fa14ac9 100644 --- a/lib/mobilizon/events/event.ex +++ b/lib/mobilizon/events/event.ex @@ -68,11 +68,11 @@ defmodule Mobilizon.Events.Event do :large_image, :publish_at, :online_address, - :phone_address + :phone_address, + :uuid ]) |> cast_assoc(:tags) |> cast_assoc(:physical_address) - |> build_url() |> validate_required([ :title, :begins_on, @@ -82,31 +82,4 @@ defmodule Mobilizon.Events.Event do :uuid ]) end - - @spec build_url(Ecto.Changeset.t()) :: Ecto.Changeset.t() - defp build_url(%Ecto.Changeset{changes: %{url: _url}} = changeset), do: changeset - - defp build_url(%Ecto.Changeset{changes: %{organizer_actor: organizer_actor}} = changeset) do - organizer_actor - |> Actor.actor_acct_from_actor() - |> do_build_url(changeset) - end - - defp build_url(%Ecto.Changeset{changes: %{organizer_actor_id: organizer_actor_id}} = changeset) do - organizer_actor_id - |> Mobilizon.Actors.get_actor!() - |> Actor.actor_acct_from_actor() - |> do_build_url(changeset) - end - - defp build_url(%Ecto.Changeset{} = changeset), do: changeset - - @spec do_build_url(String.t(), Ecto.Changeset.t()) :: Ecto.Changeset.t() - defp do_build_url(actor_acct, changeset) do - uuid = Ecto.UUID.generate() - - changeset - |> put_change(:uuid, uuid) - |> put_change(:url, "#{MobilizonWeb.Endpoint.url()}/@#{actor_acct}/#{uuid}") - end end diff --git a/lib/mobilizon/events/events.ex b/lib/mobilizon/events/events.ex index 68c5f1464..a9b0b426d 100644 --- a/lib/mobilizon/events/events.ex +++ b/lib/mobilizon/events/events.ex @@ -220,7 +220,7 @@ defmodule Mobilizon.Events do from( e in Event, where: e.visibility == ^:public, - preload: [:organizer_actor] + preload: [:organizer_actor, :participants] ) |> paginate(page, limit) diff --git a/lib/mobilizon_web/api/events.ex b/lib/mobilizon_web/api/events.ex index a2587c088..09e79670a 100644 --- a/lib/mobilizon_web/api/events.ex +++ b/lib/mobilizon_web/api/events.ex @@ -14,12 +14,12 @@ defmodule MobilizonWeb.API.Events do %{ title: title, description: description, - organizer_actor_username: organizer_actor_username, + organizer_actor_id: organizer_actor_id, begins_on: begins_on, category: category } = args ) do - with %Actor{url: url} = actor <- Actors.get_local_actor_by_name(organizer_actor_username), + with %Actor{url: url} = actor <- Actors.get_local_actor_with_everything(organizer_actor_id), title <- String.trim(title), mentions <- Formatter.parse_mentions(description), visibility <- Map.get(args, :visibility, "public"), diff --git a/lib/mobilizon_web/resolvers/event.ex b/lib/mobilizon_web/resolvers/event.ex index f75ea6922..99c6da25a 100644 --- a/lib/mobilizon_web/resolvers/event.ex +++ b/lib/mobilizon_web/resolvers/event.ex @@ -4,7 +4,6 @@ defmodule MobilizonWeb.Resolvers.Event do """ alias Mobilizon.Service.ActivityPub alias Mobilizon.Activity - alias Mobilizon.Actors alias Mobilizon.Events.Event # We limit the max number of events that can be retrieved @@ -39,8 +38,8 @@ defmodule MobilizonWeb.Resolvers.Event do @doc """ List participants for event (through an event request) """ - def list_participants_for_event(%{uuid: uuid}, %{page: page, limit: limit}, _resolution) do - {:ok, Mobilizon.Events.list_participants_for_event(uuid, page, limit)} + def list_participants_for_event(%Event{uuid: uuid}, _args, _resolution) do + {:ok, Mobilizon.Events.list_participants_for_event(uuid, 1, 10)} end @doc """ @@ -79,16 +78,9 @@ defmodule MobilizonWeb.Resolvers.Event do @doc """ Create an event """ - def create_event(_parent, args, %{context: %{current_user: user}}) do + def create_event(_parent, args, %{context: %{current_user: _user}}) do with {:ok, %Activity{data: %{"object" => %{"type" => "Event"} = object}}} <- - args - # Set default organizer_actor_id if none set - |> Map.update( - :organizer_actor_username, - Actors.get_actor_for_user(user).preferred_username, - & &1 - ) - |> MobilizonWeb.API.Events.create_event() do + MobilizonWeb.API.Events.create_event(args) do {:ok, %Event{ title: object["name"], diff --git a/lib/mobilizon_web/resolvers/person.ex b/lib/mobilizon_web/resolvers/person.ex index 4163e2b8e..347a70a7e 100644 --- a/lib/mobilizon_web/resolvers/person.ex +++ b/lib/mobilizon_web/resolvers/person.ex @@ -3,6 +3,7 @@ defmodule MobilizonWeb.Resolvers.Person do Handles the person-related GraphQL calls """ alias Mobilizon.Actors + alias Mobilizon.Actors.Actor alias Mobilizon.Service.ActivityPub @deprecated "Use find_person/3 or find_group/3 instead" @@ -39,4 +40,28 @@ defmodule MobilizonWeb.Resolvers.Person do def get_current_person(_parent, _args, _resolution) do {:error, "You need to be logged-in to view current person"} end + + @doc """ + Returns the list of identities for the logged-in user + """ + def identities(_parent, _args, %{context: %{current_user: user}}) do + {:ok, Actors.get_actors_for_user(user)} + end + + def identities(_parent, _args, _resolution) do + {:error, "You need to be logged-in to view your list of identities"} + end + + def create_person(_parent, %{preferred_username: _preferred_username} = args, %{ + context: %{current_user: user} + }) do + args = Map.put(args, :user_id, user.id) + + with {:ok, %Actor{} = new_person} <- Actors.new_person(args) do + {:ok, new_person} + else + {:error, %Ecto.Changeset{} = _e} -> + {:error, "Unable to create a profile with this username"} + end + end end diff --git a/lib/mobilizon_web/resolvers/user.ex b/lib/mobilizon_web/resolvers/user.ex index e37d4bccb..108059228 100644 --- a/lib/mobilizon_web/resolvers/user.ex +++ b/lib/mobilizon_web/resolvers/user.ex @@ -36,7 +36,7 @@ defmodule MobilizonWeb.Resolvers.User do {:error, "User with email not found"} {:error, :unauthorized} -> - {:error, "Impossible to authenticate"} + {:error, "Impossible to authenticate, either your email or password are invalid."} end end diff --git a/lib/mobilizon_web/schema.ex b/lib/mobilizon_web/schema.ex index afdac3809..654871355 100644 --- a/lib/mobilizon_web/schema.ex +++ b/lib/mobilizon_web/schema.ex @@ -104,7 +104,7 @@ defmodule MobilizonWeb.Schema do end def plugins do - [Absinthe.Middleware.Dataloader | Absinthe.Plugin.defaults()] + [Absinthe.Middleware.Dataloader] ++ Absinthe.Plugin.defaults() end @desc """ @@ -175,6 +175,11 @@ defmodule MobilizonWeb.Schema do resolve(&Resolvers.Person.find_person/3) end + @desc "Get the persons for an user" + field :identities, list_of(:person) do + resolve(&Resolvers.Person.identities/3) + end + @desc "Get the list of categories" field :categories, non_null(list_of(:category)) do arg(:page, :integer, default_value: 1) @@ -201,7 +206,7 @@ defmodule MobilizonWeb.Schema do arg(:publish_at, :datetime) arg(:online_address, :string) arg(:phone_address, :string) - arg(:organizer_actor_username, non_null(:string)) + arg(:organizer_actor_id, non_null(:id)) arg(:category, non_null(:string)) resolve(&Resolvers.Event.create_event/3) @@ -273,6 +278,16 @@ defmodule MobilizonWeb.Schema do resolve(&Resolvers.User.change_default_actor/3) end + @desc "Create a new person for user" + field :create_person, :person do + arg(:preferred_username, non_null(:string)) + arg(:name, :string, description: "The displayed name for the new profile") + + arg(:description, :string, description: "The summary for the new profile", default_value: "") + + resolve(&Resolvers.Person.create_person/3) + end + @desc "Create a group" field :create_group, :group do arg(:preferred_username, non_null(:string), description: "The name for the group") diff --git a/lib/mobilizon_web/schema/actor.ex b/lib/mobilizon_web/schema/actor.ex index c3a051094..757252042 100644 --- a/lib/mobilizon_web/schema/actor.ex +++ b/lib/mobilizon_web/schema/actor.ex @@ -5,12 +5,14 @@ defmodule MobilizonWeb.Schema.ActorInterface do use Absinthe.Schema.Notation import Absinthe.Resolution.Helpers, only: [dataloader: 1] alias Mobilizon.Actors.Actor + alias Mobilizon.Events import_types(MobilizonWeb.Schema.Actors.FollowerType) import_types(MobilizonWeb.Schema.EventType) @desc "An ActivityPub actor" interface :actor do + field(:id, :id, description: "Internal ID for this actor") field(:url, :string, description: "The ActivityPub actor's URL") field(:type, :actor_type, description: "The type of Actor (Person, Group,…)") field(:name, :string, description: "The actor's displayed name") @@ -51,6 +53,9 @@ defmodule MobilizonWeb.Schema.ActorInterface do %Actor{type: :Group}, _ -> :group + + _, _ -> + nil end) end diff --git a/lib/mobilizon_web/schema/actors/group.ex b/lib/mobilizon_web/schema/actors/group.ex index 1dab34591..cf8316236 100644 --- a/lib/mobilizon_web/schema/actors/group.ex +++ b/lib/mobilizon_web/schema/actors/group.ex @@ -12,6 +12,7 @@ defmodule MobilizonWeb.Schema.Actors.GroupType do object :group do interfaces([:actor]) + field(:id, :id, description: "Internal ID for this group") field(:url, :string, description: "The ActivityPub actor's URL") field(:type, :actor_type, description: "The type of Actor (Person, Group,…)") field(:name, :string, description: "The actor's displayed name") diff --git a/lib/mobilizon_web/schema/actors/person.ex b/lib/mobilizon_web/schema/actors/person.ex index 46b11dece..24acaba5e 100644 --- a/lib/mobilizon_web/schema/actors/person.ex +++ b/lib/mobilizon_web/schema/actors/person.ex @@ -5,12 +5,14 @@ defmodule MobilizonWeb.Schema.Actors.PersonType do use Absinthe.Schema.Notation import Absinthe.Resolution.Helpers, only: [dataloader: 1] import_types(MobilizonWeb.Schema.UserType) + alias Mobilizon.Events @desc """ Represents a person identity """ object :person do interfaces([:actor]) + field(:id, :id, description: "Internal ID for this person") field(:user, :user, description: "The user this actor is associated to") field(:member_of, list_of(:member), description: "The list of groups this person is member of") diff --git a/lib/mobilizon_web/schema/comment.ex b/lib/mobilizon_web/schema/comment.ex index 0768ad06e..61a4569fa 100644 --- a/lib/mobilizon_web/schema/comment.ex +++ b/lib/mobilizon_web/schema/comment.ex @@ -6,6 +6,7 @@ defmodule MobilizonWeb.Schema.CommentType do @desc "A comment" object :comment do + field(:id, :id, description: "Internal ID for this comment") field(:uuid, :uuid) field(:url, :string) field(:local, :boolean) diff --git a/lib/mobilizon_web/schema/event.ex b/lib/mobilizon_web/schema/event.ex index 2ff2192c3..a06409243 100644 --- a/lib/mobilizon_web/schema/event.ex +++ b/lib/mobilizon_web/schema/event.ex @@ -3,6 +3,7 @@ defmodule MobilizonWeb.Schema.EventType do Schema representation for Event """ use Absinthe.Schema.Notation + alias Mobilizon.Actors import Absinthe.Resolution.Helpers, only: [dataloader: 1] import_types(MobilizonWeb.Schema.AddressType) import_types(MobilizonWeb.Schema.Events.ParticipantType) @@ -10,6 +11,7 @@ defmodule MobilizonWeb.Schema.EventType do @desc "An event" object :event do + field(:id, :id, description: "Internal ID for this event") field(:uuid, :uuid, description: "The Event UUID") field(:url, :string, description: "The ActivityPub Event URL") field(:local, :boolean, description: "Whether the event is local or not") @@ -28,7 +30,7 @@ defmodule MobilizonWeb.Schema.EventType do field(:online_address, :online_address, description: "Online address of the event") field(:phone_address, :phone_address, description: "Phone address for the event") - field(:organizer_actor, :person, + field(:organizer_actor, :actor, resolve: dataloader(Actors), description: "The event's organizer (as a person)" ) diff --git a/lib/service/activity_pub/activity_pub.ex b/lib/service/activity_pub/activity_pub.ex index 13979a32f..662096ab6 100644 --- a/lib/service/activity_pub/activity_pub.ex +++ b/lib/service/activity_pub/activity_pub.ex @@ -535,7 +535,8 @@ defmodule Mobilizon.Service.ActivityPub do defp ical_event_to_activity(%ExIcal.Event{} = ical_event, %Actor{} = actor, _source) do # Logger.debug(inspect ical_event) - # TODO : refactor me ! + # TODO : Use MobilizonWeb.API instead + # TODO : refactor me and move me somewhere else! # TODO : also, there should be a form of cache that allows this to be more efficient category = if is_nil(ical_event.categories) do diff --git a/lib/service/activity_pub/utils.ex b/lib/service/activity_pub/utils.ex index 84e8ee569..a24ceff08 100644 --- a/lib/service/activity_pub/utils.ex +++ b/lib/service/activity_pub/utils.ex @@ -118,7 +118,8 @@ defmodule Mobilizon.Service.ActivityPub.Utils do "organizer_actor_id" => actor_id, "begins_on" => object["begins_on"], "category_id" => Events.get_category_by_title(object["category"]).id, - "url" => object["id"] + "url" => object["id"], + "uuid" => object["uuid"] } end diff --git a/mix.exs b/mix.exs index 6c020b724..dd375869a 100644 --- a/mix.exs +++ b/mix.exs @@ -56,8 +56,8 @@ defmodule Mobilizon.Mixfile do {:phoenix_html, "~> 2.10"}, {:gettext, "~> 0.11"}, {:cowboy, "~> 1.0"}, - {:guardian, "~> 1.0"}, - {:guardian_db, "~> 1.0"}, + {:guardian, "~> 1.2"}, + {:guardian_db, "~> 1.1"}, {:comeonin, "~> 4.0"}, {:argon2_elixir, "~> 1.2"}, {:cors_plug, "~> 1.2"}, @@ -70,7 +70,7 @@ defmodule Mobilizon.Mixfile do {:icalendar, "~> 0.6"}, {:exgravatar, "~> 2.0.1"}, {:httpoison, "~> 1.0"}, - {:json_ld, "~> 0.2"}, + {:json_ld, "~> 0.3"}, {:jason, "~> 1.0"}, {:ex_crypto, "~> 0.9.0"}, {:http_sign, "~> 0.1.1"}, diff --git a/mix.lock b/mix.lock index 9cb404dc3..a09266004 100644 --- a/mix.lock +++ b/mix.lock @@ -45,7 +45,7 @@ "geo_postgis": {:hex, :geo_postgis, "2.1.0", "e0640d18276cb1dd58aeae3f5eed9a61641a5110901e1e35d0d662031d936b33", [:mix], [{:geo, "~> 3.0", [hex: :geo, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.13", [hex: :postgrex, repo: "hexpm", optional: false]}], "hexpm"}, "geolix": {:hex, :geolix, "0.17.0", "8f3f4068be08599912de67ae24372a6c148794a0152f9f83ffd5a2ffcb21d29a", [:mix], [{:mmdb2_decoder, "~> 0.3.0", [hex: :mmdb2_decoder, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.0", [hex: :poolboy, repo: "hexpm", optional: false]}], "hexpm"}, "gettext": {:hex, :gettext, "0.16.1", "e2130b25eebcbe02bb343b119a07ae2c7e28bd4b146c4a154da2ffb2b3507af2", [:mix], [], "hexpm"}, - "guardian": {:hex, :guardian, "1.1.1", "be14c4007eaf05268251ae114030cb7237ed9a9631c260022f020164ff4ed733", [:mix], [{:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.0 or ~> 1.2 or ~> 1.3", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, + "guardian": {:hex, :guardian, "1.2.1", "bdc8dd3dbf0fb7216cb6f91c11831faa1a64d39cdaed9a611e37f2413e584983", [:mix], [{:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.3", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm"}, "guardian_db": {:hex, :guardian_db, "1.1.0", "45ab94206cce38f7443dc27de6dc52966ccbdeff65ca1b1f11a6d8f3daceb556", [:mix], [{:ecto, "~> 2.2", [hex: :ecto, repo: "hexpm", optional: false]}, {:guardian, "~> 1.0", [hex: :guardian, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm"}, "hackney": {:hex, :hackney, "1.14.3", "b5f6f5dcc4f1fba340762738759209e21914516df6be440d85772542d4a5e412", [:rebar3], [{:certifi, "2.4.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.4", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, "http_sign": {:hex, :http_sign, "0.1.1", "b16edb83aa282892f3271f9a048c155e772bf36e15700ab93901484c55f8dd10", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, @@ -53,7 +53,7 @@ "icalendar": {:hex, :icalendar, "0.7.0", "6acf28c7e38ad1c4515c59e336878fb78bb646c8aa70d2ee3786ea194711a7b7", [:mix], [{:timex, "~> 3.0", [hex: :timex, repo: "hexpm", optional: false]}], "hexpm"}, "idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, - "jose": {:hex, :jose, "1.8.4", "7946d1e5c03a76ac9ef42a6e6a20001d35987afd68c2107bcd8f01a84e75aa73", [:mix, :rebar3], [{:base64url, "~> 0.0.1", [hex: :base64url, repo: "hexpm", optional: false]}], "hexpm"}, + "jose": {:hex, :jose, "1.9.0", "4167c5f6d06ffaebffd15cdb8da61a108445ef5e85ab8f5a7ad926fdf3ada154", [:mix, :rebar3], [{:base64url, "~> 0.0.1", [hex: :base64url, repo: "hexpm", optional: false]}], "hexpm"}, "json_ld": {:hex, :json_ld, "0.3.0", "92f508ca831b9e4530e3e6c950976fdafcf26323e6817c325b3e1ee78affc4bd", [:mix], [{:jason, "~> 1.1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:rdf, "~> 0.5", [hex: :rdf, repo: "hexpm", optional: false]}], "hexpm"}, "jsx": {:hex, :jsx, "2.8.3", "a05252d381885240744d955fbe3cf810504eb2567164824e19303ea59eef62cf", [:mix, :rebar3], [], "hexpm"}, "kronky": {:hex, :kronky, "0.5.0", "b2038c267f02b297044cb574f542fa96763278a88b32a97d0c37bde95c63c13b", [:mix], [{:absinthe, "~> 1.3", [hex: :absinthe, repo: "hexpm", optional: false]}, {:ecto, ">= 2.1.4", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm"}, @@ -80,7 +80,7 @@ "poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], [], "hexpm"}, "postgrex": {:hex, :postgrex, "0.13.5", "3d931aba29363e1443da167a4b12f06dcd171103c424de15e5f3fc2ba3e6d9c5", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm"}, "ranch": {:hex, :ranch, "1.3.2", "e4965a144dc9fbe70e5c077c65e73c57165416a901bd02ea899cfd95aa890986", [:rebar3], [], "hexpm"}, - "rdf": {:hex, :rdf, "0.5.3", "2990ce6ca55602db9c170e6258bf3fa39b0e4be3d49b1c09c00a740bc387029f", [:mix], [{:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm"}, + "rdf": {:hex, :rdf, "0.5.4", "57e09d4adfe7646fe0c3514b703b76eaf29d537b250b36abae75e66d7e5920cf", [:mix], [{:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm"}, "rsa_ex": {:hex, :rsa_ex, "0.4.0", "e28dd7dc5236e156df434af0e4aa822384c8866c928e17b785d4edb7c253b558", [:mix], [], "hexpm"}, "slugger": {:hex, :slugger, "0.3.0", "efc667ab99eee19a48913ccf3d038b1fb9f165fa4fbf093be898b8099e61b6ed", [:mix], [], "hexpm"}, "socket": {:hex, :socket, "0.3.13", "98a2ab20ce17f95fb512c5cadddba32b57273e0d2dba2d2e5f976c5969d0c632", [:mix], [], "hexpm"}, diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index 16117576e..b1ccb7bb6 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -21,10 +21,17 @@ actor = insert(:actor, user: user) # Insert a second actor account for the same user actor2 = insert(:actor, user: user) -# Make actor organize an event +# Make actor organize a few events event = insert(:event, organizer_actor: actor) +event2 = insert(:event, organizer_actor: actor) +event3 = insert(:event, organizer_actor: actor) +event4 = insert(:event, organizer_actor: actor2) -participant = insert(:participant, actor: actor, event: event) +participant = insert(:participant, actor: actor, event: event, role: 4) +participant = insert(:participant, actor: actor, event: event2, role: 4) +participant = insert(:participant, actor: actor, event: event3, role: 4) +participant = insert(:participant, actor: actor2, event: event4, role: 4) +participant = insert(:participant, actor: actor, event: event4, role: 1) # Insert a group group = insert(:actor, type: :Group) diff --git a/test/mobilizon/actors/actors_test.exs b/test/mobilizon/actors/actors_test.exs index e921192f3..53819cf63 100644 --- a/test/mobilizon/actors/actors_test.exs +++ b/test/mobilizon/actors/actors_test.exs @@ -69,14 +69,14 @@ defmodule Mobilizon.ActorsTest do assert actor_id == Actors.get_actor_for_user(user).id end - test "get_actor_with_everything!/1 returns the actor with it's organized events", %{ + test "get_actor_with_everything/1 returns the actor with it's organized events", %{ actor: actor } do - assert Actors.get_actor_with_everything!(actor.id).organized_events == [] + assert Actors.get_actor_with_everything(actor.id).organized_events == [] event = insert(:event, organizer_actor: actor) event_found_id = - Actors.get_actor_with_everything!(actor.id).organized_events |> hd |> Map.get(:id) + Actors.get_actor_with_everything(actor.id).organized_events |> hd |> Map.get(:id) assert event_found_id == event.id end @@ -573,15 +573,15 @@ defmodule Mobilizon.ActorsTest do test "follow/3 makes an actor follow another", %{actor: actor, target_actor: target_actor} do # Preloading followers/followings - actor = Actors.get_actor_with_everything!(actor.id) - target_actor = Actors.get_actor_with_everything!(target_actor.id) + actor = Actors.get_actor_with_everything(actor.id) + target_actor = Actors.get_actor_with_everything(target_actor.id) {:ok, follower} = Actor.follow(target_actor, actor) assert follower.actor.id == actor.id # Referesh followers/followings - actor = Actors.get_actor_with_everything!(actor.id) - target_actor = Actors.get_actor_with_everything!(target_actor.id) + actor = Actors.get_actor_with_everything(actor.id) + target_actor = Actors.get_actor_with_everything(target_actor.id) assert target_actor.followers |> Enum.map(& &1.actor_id) == [actor.id] assert actor.followings |> Enum.map(& &1.target_actor_id) == [target_actor.id] diff --git a/test/mobilizon/events/events_test.exs b/test/mobilizon/events/events_test.exs index d16e56e50..26202f671 100644 --- a/test/mobilizon/events/events_test.exs +++ b/test/mobilizon/events/events_test.exs @@ -9,7 +9,9 @@ defmodule Mobilizon.EventsTest do begins_on: "2010-04-17 14:00:00.000000Z", description: "some description", ends_on: "2010-04-17 14:00:00.000000Z", - title: "some title" + title: "some title", + url: "some url", + uuid: "b5126423-f1af-43e4-a923-002a03003ba4" } describe "events" do diff --git a/test/mobilizon/service/activity_pub/transmogrifier_test.exs b/test/mobilizon/service/activity_pub/transmogrifier_test.exs index ce269a15f..450321cf2 100644 --- a/test/mobilizon/service/activity_pub/transmogrifier_test.exs +++ b/test/mobilizon/service/activity_pub/transmogrifier_test.exs @@ -176,7 +176,7 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do assert data["type"] == "Follow" assert data["id"] == "https://social.tcit.fr/users/tcit#follows/2" - actor = Actors.get_actor_with_everything!(actor.id) + actor = Actors.get_actor_with_everything(actor.id) assert Actor.following?(Actors.get_actor_by_url!(data["actor"], true), actor) end diff --git a/test/mobilizon_web/resolvers/event_resolver_test.exs b/test/mobilizon_web/resolvers/event_resolver_test.exs index 80a3ad207..d98124444 100644 --- a/test/mobilizon_web/resolvers/event_resolver_test.exs +++ b/test/mobilizon_web/resolvers/event_resolver_test.exs @@ -5,7 +5,13 @@ defmodule MobilizonWeb.Resolvers.EventResolverTest do alias MobilizonWeb.AbsintheHelpers import Mobilizon.Factory - @event %{description: "some body", title: "some title", begins_on: Ecto.DateTime.utc()} + @event %{ + description: "some body", + title: "some title", + begins_on: Ecto.DateTime.utc(), + uuid: "b5126423-f1af-43e4-a923-002a03003ba4", + url: "some url" + } setup %{conn: conn} do {:ok, %User{default_actor: %Actor{} = actor} = user} = @@ -117,7 +123,7 @@ defmodule MobilizonWeb.Resolvers.EventResolverTest do title: "come to my event", description: "it will be fine", begins_on: "#{DateTime.utc_now() |> DateTime.to_iso8601()}", - organizer_actor_username: "#{actor.preferred_username}", + organizer_actor_id: "#{actor.id}", category: "#{category.title}" ) { title, @@ -131,6 +137,8 @@ defmodule MobilizonWeb.Resolvers.EventResolverTest do |> auth_conn(user) |> post("/api", AbsintheHelpers.mutation_skeleton(mutation)) + require Logger + Logger.error(inspect(json_response(res, 200))) assert json_response(res, 200)["data"]["createEvent"]["title"] == "come to my event" end diff --git a/test/mobilizon_web/resolvers/user_resolver_test.exs b/test/mobilizon_web/resolvers/user_resolver_test.exs index 54d26bbaa..d28d669fc 100644 --- a/test/mobilizon_web/resolvers/user_resolver_test.exs +++ b/test/mobilizon_web/resolvers/user_resolver_test.exs @@ -336,7 +336,8 @@ defmodule MobilizonWeb.Resolvers.UserResolverTest do context.conn |> post("/api", AbsintheHelpers.mutation_skeleton(mutation)) - assert hd(json_response(res, 200)["errors"])["message"] == "password_too_short" + assert hd(json_response(res, 200)["errors"])["message"] == + "The password you have choosen is too short. Please make sure your password contains at least 6 charaters." end test "test reset_password/3 with an invalid token", context do @@ -361,7 +362,8 @@ defmodule MobilizonWeb.Resolvers.UserResolverTest do context.conn |> post("/api", AbsintheHelpers.mutation_skeleton(mutation)) - assert hd(json_response(res, 200)["errors"])["message"] == "invalid_token" + assert hd(json_response(res, 200)["errors"])["message"] == + "The token you provided is invalid. Make sure that the URL is exactly the one provided inside the email you got." end end @@ -431,7 +433,8 @@ defmodule MobilizonWeb.Resolvers.UserResolverTest do context.conn |> post("/api", AbsintheHelpers.mutation_skeleton(mutation)) - assert hd(json_response(res, 200)["errors"])["message"] == "Impossible to authenticate" + assert hd(json_response(res, 200)["errors"])["message"] == + "Impossible to authenticate, either your email or password are invalid." end test "test login_user/3 with invalid email", context do diff --git a/test/support/factory.ex b/test/support/factory.ex index 3e52914ac..0fb645bf1 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -95,25 +95,29 @@ defmodule Mobilizon.Factory do def event_factory do actor = build(:actor) + start = Timex.now() + uuid = Ecto.UUID.generate() %Mobilizon.Events.Event{ title: sequence("Ceci est un événement"), description: "Ceci est une description avec une première phrase assez longue, puis sur une seconde ligne", - begins_on: nil, - ends_on: nil, + begins_on: start, + ends_on: Timex.shift(start, hours: 2), organizer_actor: actor, category: build(:category), physical_address: build(:address), visibility: :public, - url: "@#{actor.url}/#{Ecto.UUID.generate()}" + url: "#{actor.url}/#{uuid}", + uuid: uuid } end def participant_factory do %Mobilizon.Events.Participant{ event: build(:event), - actor: build(:actor) + actor: build(:actor), + role: 0 } end