postmerge

This commit is contained in:
Untone 2023-10-19 02:34:15 +03:00
commit 9c263b697e
34 changed files with 523 additions and 362 deletions

182
package-lock.json generated
View File

@ -97,6 +97,7 @@
"husky": "8.0.3", "husky": "8.0.3",
"hygen": "6.2.11", "hygen": "6.2.11",
"i18next-http-backend": "2.2.0", "i18next-http-backend": "2.2.0",
"javascript-time-ago": "2.5.9",
"jest": "29.7.0", "jest": "29.7.0",
"js-cookie": "3.0.5", "js-cookie": "3.0.5",
"lint-staged": "14.0.1", "lint-staged": "14.0.1",
@ -3808,9 +3809,9 @@
"dev": true "dev": true
}, },
"node_modules/@jridgewell/trace-mapping": { "node_modules/@jridgewell/trace-mapping": {
"version": "0.3.19", "version": "0.3.20",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz",
"integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==", "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/resolve-uri": "^3.1.0",
@ -4494,14 +4495,14 @@
} }
}, },
"node_modules/@peculiar/asn1-schema": { "node_modules/@peculiar/asn1-schema": {
"version": "2.3.6", "version": "2.3.8",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.3.6.tgz", "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.3.8.tgz",
"integrity": "sha512-izNRxPoaeJeg/AyH8hER6s+H7p4itk+03QCa4sbxI3lNdseQYCuxzgsuNK8bTXChtLTjpJz6NmXKA73qLa3rCA==", "integrity": "sha512-ULB1XqHKx1WBU/tTFIA+uARuRoBVZ4pNdOA878RDrRbBfBGcSzi5HBkdScC6ZbHn8z7L8gmKCgPC1LHRrP46tA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"asn1js": "^3.0.5", "asn1js": "^3.0.5",
"pvtsutils": "^1.3.2", "pvtsutils": "^1.3.5",
"tslib": "^2.4.0" "tslib": "^2.6.2"
} }
}, },
"node_modules/@peculiar/json-schema": { "node_modules/@peculiar/json-schema": {
@ -5332,9 +5333,9 @@
} }
}, },
"node_modules/@types/babel__core": { "node_modules/@types/babel__core": {
"version": "7.20.2", "version": "7.20.3",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.2.tgz", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.3.tgz",
"integrity": "sha512-pNpr1T1xLUc2l3xJKuPtsEky3ybxN3m4fJkknfIpTCTfIZCDW57oAg+EfCgIIp2rvCe0Wn++/FfodDS4YXxBwA==", "integrity": "sha512-54fjTSeSHwfan8AyHWrKbfBWiEUrNTZsUwPTDSNaaP1QDQIZbeNUg3a59E9D+375MzUw/x1vx2/0F5LBz+AeYA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@babel/parser": "^7.20.7", "@babel/parser": "^7.20.7",
@ -5345,18 +5346,18 @@
} }
}, },
"node_modules/@types/babel__generator": { "node_modules/@types/babel__generator": {
"version": "7.6.5", "version": "7.6.6",
"resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.5.tgz", "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.6.tgz",
"integrity": "sha512-h9yIuWbJKdOPLJTbmSpPzkF67e659PbQDba7ifWm5BJ8xTv+sDmS7rFmywkWOvXedGTivCdeGSIIX8WLcRTz8w==", "integrity": "sha512-66BXMKb/sUWbMdBNdMvajU7i/44RkrA3z/Yt1c7R5xejt8qh84iU54yUWCtm0QwGJlDcf/gg4zd/x4mpLAlb/w==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@babel/types": "^7.0.0" "@babel/types": "^7.0.0"
} }
}, },
"node_modules/@types/babel__template": { "node_modules/@types/babel__template": {
"version": "7.4.2", "version": "7.4.3",
"resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.2.tgz", "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.3.tgz",
"integrity": "sha512-/AVzPICMhMOMYoSx9MoKpGDKdBRsIXMNByh1PXSZoa+v6ZoLa8xxtsT/uLQ/NJm0XVAWl/BvId4MlDeXJaeIZQ==", "integrity": "sha512-ciwyCLeuRfxboZ4isgdNZi/tkt06m8Tw6uGbBSBgWrnnZGNXiEyM27xc/PjXGQLqlZ6ylbgHMnm7ccF9tCkOeQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@babel/parser": "^7.1.0", "@babel/parser": "^7.1.0",
@ -5364,18 +5365,18 @@
} }
}, },
"node_modules/@types/babel__traverse": { "node_modules/@types/babel__traverse": {
"version": "7.20.2", "version": "7.20.3",
"resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.2.tgz", "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.3.tgz",
"integrity": "sha512-ojlGK1Hsfce93J0+kn3H5R73elidKUaZonirN33GSmgTUMpzI/MIFfSpF3haANe3G1bEBS9/9/QEqwTzwqFsKw==", "integrity": "sha512-Lsh766rGEFbaxMIDH7Qa+Yha8cMVI3qAK6CHt3OR0YfxOIn5Z54iHiyDRycHrBqeIiqGa20Kpsv1cavfBKkRSw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@babel/types": "^7.20.7" "@babel/types": "^7.20.7"
} }
}, },
"node_modules/@types/eslint": { "node_modules/@types/eslint": {
"version": "8.44.4", "version": "8.44.6",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.4.tgz", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.6.tgz",
"integrity": "sha512-lOzjyfY/D9QR4hY9oblZ76B90MYTB3RrQ4z2vBIJKj9ROCRqdkYl2gSUx1x1a4IWPjKJZLL4Aw1Zfay7eMnmnA==", "integrity": "sha512-P6bY56TVmX8y9J87jHNgQh43h6VVU+6H7oN7hgvivV81K2XY8qJZ5vqPy/HdUoVIelii2kChYVzQanlswPWVFw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@types/estree": "*", "@types/estree": "*",
@ -5383,39 +5384,39 @@
} }
}, },
"node_modules/@types/estree": { "node_modules/@types/estree": {
"version": "1.0.2", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.2.tgz", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.3.tgz",
"integrity": "sha512-VeiPZ9MMwXjO32/Xu7+OwflfmeoRwkE/qzndw42gGtgJwZopBnzy2gD//NN1+go1mADzkDcqf/KnFRSjTJ8xJA==", "integrity": "sha512-CS2rOaoQ/eAgAfcTfq6amKG7bsN+EMcgGY4FAFQdvSj2y1ixvOZTUA9mOtCai7E1SYu283XNw7urKK30nP3wkQ==",
"dev": true "dev": true
}, },
"node_modules/@types/graceful-fs": { "node_modules/@types/graceful-fs": {
"version": "4.1.7", "version": "4.1.8",
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.7.tgz", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.8.tgz",
"integrity": "sha512-MhzcwU8aUygZroVwL2jeYk6JisJrPl/oov/gsgGCue9mkgl9wjGbzReYQClxiUgFDnib9FuHqTndccKeZKxTRw==", "integrity": "sha512-NhRH7YzWq8WiNKVavKPBmtLYZHxNY19Hh+az28O/phfp68CF45pMFud+ZzJ8ewnxnC5smIdF3dqFeiSUQ5I+pw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/istanbul-lib-coverage": { "node_modules/@types/istanbul-lib-coverage": {
"version": "2.0.4", "version": "2.0.5",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz",
"integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", "integrity": "sha512-zONci81DZYCZjiLe0r6equvZut0b+dBRPBN5kBDjsONnutYNtJMoWQ9uR2RkL1gLG9NMTzvf+29e5RFfPbeKhQ==",
"dev": true "dev": true
}, },
"node_modules/@types/istanbul-lib-report": { "node_modules/@types/istanbul-lib-report": {
"version": "3.0.1", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.2.tgz",
"integrity": "sha512-gPQuzaPR5h/djlAv2apEG1HVOyj1IUs7GpfMZixU0/0KXT3pm64ylHuMUI1/Akh+sq/iikxg6Z2j+fcMDXaaTQ==", "integrity": "sha512-8toY6FgdltSdONav1XtUHl4LN1yTmLza+EuDazb/fEmRNCwjyqNVIQWs2IfC74IqjHkREs/nQ2FWq5kZU9IC0w==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@types/istanbul-lib-coverage": "*" "@types/istanbul-lib-coverage": "*"
} }
}, },
"node_modules/@types/istanbul-reports": { "node_modules/@types/istanbul-reports": {
"version": "3.0.2", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.2.tgz", "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.3.tgz",
"integrity": "sha512-kv43F9eb3Lhj+lr/Hn6OcLCs/sSM8bt+fIaP11rCYngfV6NVjzWXJ17owQtDQTL9tQ8WSLUrGsSJ6rJz0F1w1A==", "integrity": "sha512-1nESsePMBlf0RPRffLZi5ujYh7IH1BWL4y9pr+Bn3cJBdxz+RTP8bUFljLz9HvzhhOSWKdyBZ4DIivdL6rvgZg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@types/istanbul-lib-report": "*" "@types/istanbul-lib-report": "*"
@ -5428,21 +5429,21 @@
"dev": true "dev": true
}, },
"node_modules/@types/js-yaml": { "node_modules/@types/js-yaml": {
"version": "4.0.7", "version": "4.0.8",
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.7.tgz", "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.8.tgz",
"integrity": "sha512-RJZP9WAMMr1514KbdSXkLRrKvYQacjr1+HWnY8pui/uBTBoSgD9ZGR17u/d4nb9NpERp0FkdLBe7hq8NIPBgkg==", "integrity": "sha512-m6jnPk1VhlYRiLFm3f8X9Uep761f+CK8mHyS65LutH2OhmBF0BeMEjHgg05usH8PLZMWWc/BUR9RPmkvpWnyRA==",
"dev": true "dev": true
}, },
"node_modules/@types/json-schema": { "node_modules/@types/json-schema": {
"version": "7.0.13", "version": "7.0.14",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.13.tgz", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.14.tgz",
"integrity": "sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ==", "integrity": "sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw==",
"dev": true "dev": true
}, },
"node_modules/@types/json-stable-stringify": { "node_modules/@types/json-stable-stringify": {
"version": "1.0.34", "version": "1.0.35",
"resolved": "https://registry.npmjs.org/@types/json-stable-stringify/-/json-stable-stringify-1.0.34.tgz", "resolved": "https://registry.npmjs.org/@types/json-stable-stringify/-/json-stable-stringify-1.0.35.tgz",
"integrity": "sha512-s2cfwagOQAS8o06TcwKfr9Wx11dNGbH2E9vJz1cqV+a/LOyhWNLUNd6JSRYNzvB4d29UuJX2M0Dj9vE1T8fRXw==", "integrity": "sha512-zlCWqsRBI0+ANN7dzGeDFJ4CHaVFTLqBNRS11GjR2mHCW6XxNtnMxhQzBKMzfsnjI8oI+kWq2vBwinyQpZVSsg==",
"dev": true "dev": true
}, },
"node_modules/@types/json5": { "node_modules/@types/json5": {
@ -5452,9 +5453,9 @@
"dev": true "dev": true
}, },
"node_modules/@types/minimist": { "node_modules/@types/minimist": {
"version": "1.2.3", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.3.tgz", "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.4.tgz",
"integrity": "sha512-ZYFzrvyWUNhaPomn80dsMNgMeXxNWZBdkuG/hWlUvXvbdUH8ZERNBGXnU87McuGcWDsyzX2aChCv/SVN348k3A==", "integrity": "sha512-Kfe/D3hxHTusnPNRbycJE1N77WHDsdS4AjUYIzlDzhDrS47NrwuL3YW4VITxwR7KCVpzwgy4Rbj829KSSQmwXQ==",
"dev": true "dev": true
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
@ -5464,27 +5465,27 @@
"dev": true "dev": true
}, },
"node_modules/@types/normalize-package-data": { "node_modules/@types/normalize-package-data": {
"version": "2.4.2", "version": "2.4.3",
"resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.2.tgz", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.3.tgz",
"integrity": "sha512-lqa4UEhhv/2sjjIQgjX8B+RBjj47eo0mzGasklVJ78UKGQY1r0VpB9XHDaZZO9qzEFDdy4MrXLuEaSmPrPSe/A==", "integrity": "sha512-ehPtgRgaULsFG8x0NeYJvmyH1hmlfsNLujHe9dQEia/7MAJYdzMSi19JtchUHjmBA6XC/75dK55mzZH+RyieSg==",
"dev": true "dev": true
}, },
"node_modules/@types/object.omit": { "node_modules/@types/object.omit": {
"version": "3.0.1", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/object.omit/-/object.omit-3.0.1.tgz", "resolved": "https://registry.npmjs.org/@types/object.omit/-/object.omit-3.0.2.tgz",
"integrity": "sha512-24XD34UeRWw505TsMNBrQ4bES2s8IxiFC59mmNUFhTz9IX2hAtA7gQ8wVww1i17QmhBYILg5iqYP2y7aqA3pwQ==", "integrity": "sha512-BxWU36cMP+FKD3OLFluQaj2cBev2sx2LJaHELuphHwnleq+xnEhTmuYYYx4pOT/1U/ZoR6B+RdvxWh2FD6lGGA==",
"dev": true "dev": true
}, },
"node_modules/@types/object.pick": { "node_modules/@types/object.pick": {
"version": "1.3.2", "version": "1.3.3",
"resolved": "https://registry.npmjs.org/@types/object.pick/-/object.pick-1.3.2.tgz", "resolved": "https://registry.npmjs.org/@types/object.pick/-/object.pick-1.3.3.tgz",
"integrity": "sha512-sn7L+qQ6RLPdXRoiaE7bZ/Ek+o4uICma/lBFPyJEKDTPTBP1W8u0c4baj3EiS4DiqLs+Hk+KUGvMVJtAw3ePJg==", "integrity": "sha512-qZqHmdGEALeSATMB1djT1S5szv6Wtpb7DKpHrt2XG4iyKlV7C2Xk8GmDXr1KXakOqUfX6ohw7ceruYt4NVmB1Q==",
"dev": true "dev": true
}, },
"node_modules/@types/parse-json": { "node_modules/@types/parse-json": {
"version": "4.0.0", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.1.tgz",
"integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", "integrity": "sha512-3YmXzzPAdOTVljVMkTMBdBEvlOLg2cDQaDhnnhT3nT9uDbnJzjWhKlzb+desT12Y7tGqaN6d+AbozcKzyL36Ng==",
"dev": true "dev": true
}, },
"node_modules/@types/prettier": { "node_modules/@types/prettier": {
@ -5494,15 +5495,15 @@
"dev": true "dev": true
}, },
"node_modules/@types/semver": { "node_modules/@types/semver": {
"version": "7.5.3", "version": "7.5.4",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.3.tgz", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-OxepLK9EuNEIPxWNME+C6WwbRAOOI2o2BaQEGzz5Lu2e4Z5eDnEo+/aVEDMIXywoJitJ7xWd641wrGLZdtwRyw==", "integrity": "sha512-MMzuxN3GdFwskAnb6fz0orFvhfqi752yjaXylr0Rp4oDg5H0Zn1IuyRhDVvYOwAXoJirx2xuS16I3WjxnAIHiQ==",
"dev": true "dev": true
}, },
"node_modules/@types/stack-utils": { "node_modules/@types/stack-utils": {
"version": "2.0.1", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.2.tgz",
"integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", "integrity": "sha512-g7CK9nHdwjK2n0ymT2CW698FuWJRIx+RP6embAzZ2Qi8/ilIrA1Imt2LVSeHUzKvpoi7BhmmQcXz95eS0f2JXw==",
"dev": true "dev": true
}, },
"node_modules/@types/throttle-debounce": { "node_modules/@types/throttle-debounce": {
@ -5512,27 +5513,27 @@
"dev": true "dev": true
}, },
"node_modules/@types/ws": { "node_modules/@types/ws": {
"version": "8.5.7", "version": "8.5.8",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.7.tgz", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.8.tgz",
"integrity": "sha512-6UrLjiDUvn40CMrAubXuIVtj2PEfKDffJS7ychvnPU44j+KVeXmdHHTgqcM/dxLUTHxlXHiFM8Skmb8ozGdTnQ==", "integrity": "sha512-flUksGIQCnJd6sZ1l5dqCEG/ksaoAg/eUwiLAGTJQcfgvZJKF++Ta4bJA6A5aPSJmsr+xlseHn4KLgVlNnvPTg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/yargs": { "node_modules/@types/yargs": {
"version": "17.0.28", "version": "17.0.29",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.28.tgz", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.29.tgz",
"integrity": "sha512-N3e3fkS86hNhtk6BEnc0rj3zcehaxx8QWhCROJkqpl5Zaoi7nAic3jH8q94jVD3zu5LGk+PUB6KAiDmimYOEQw==", "integrity": "sha512-nacjqA3ee9zRF/++a3FUY1suHTFKZeHba2n8WeDw9cCVdmzmHpIxyzOJBcpHvvEmS8E9KqWlSnWHUkOrkhWcvA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@types/yargs-parser": "*" "@types/yargs-parser": "*"
} }
}, },
"node_modules/@types/yargs-parser": { "node_modules/@types/yargs-parser": {
"version": "21.0.1", "version": "21.0.2",
"resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.1.tgz", "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.2.tgz",
"integrity": "sha512-axdPBuLuEJt0c4yI5OZssC19K2Mq1uKdrfZBzuxLvaztgqUtFYZUNw7lETExPYJR9jdEoIg4mb7RQKRQzOkeGQ==", "integrity": "sha512-5qcvofLPbfjmBfKaLfj/+f+Sbd6pN4zl7w7VSVI5uz7m9QZTuB2aZAa2uo1wHFBNN2x6g/SoTkXmd8mQnQF2Cw==",
"dev": true "dev": true
}, },
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
@ -6868,9 +6869,9 @@
} }
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001549", "version": "1.0.30001550",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001549.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001550.tgz",
"integrity": "sha512-qRp48dPYSCYaP+KurZLhDYdVE+yEyht/3NlmcJgVQ2VMGt6JL36ndQ/7rgspdZsJuxDPFIo/OzBT2+GmIJ53BA==", "integrity": "sha512-p82WjBYIypO0ukTsd/FG3Xxs+4tFeaY9pfT4amQL8KWtYH7H9nYwReGAbMTJ0hsmRO8IfDtsS6p3ZWj8+1c2RQ==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -7913,9 +7914,9 @@
} }
}, },
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.4.556", "version": "1.4.559",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.556.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.559.tgz",
"integrity": "sha512-6RPN0hHfzDU8D56E72YkDvnLw5Cj2NMXZGg3UkgyoHxjVhG99KZpsKgBWMmTy0Ei89xwan+rbRsVB9yzATmYzQ==", "integrity": "sha512-iS7KhLYCSJbdo3rUSkhDTVuFNCV34RKs2UaB9Ecr7VlqzjjWW//0nfsFF5dtDmyXlZQaDYYtID5fjtC/6lpRug==",
"dev": true "dev": true
}, },
"node_modules/emittery": { "node_modules/emittery": {
@ -11439,6 +11440,15 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/javascript-time-ago": {
"version": "2.5.9",
"resolved": "https://registry.npmjs.org/javascript-time-ago/-/javascript-time-ago-2.5.9.tgz",
"integrity": "sha512-pQ8mNco/9g9TqWXWWjP0EWl6i/lAQScOyEeXy5AB+f7MfLSdgyV9BJhiOD1zrIac/lrxPYOWNbyl/IW8CW5n0A==",
"dev": true,
"dependencies": {
"relative-time-format": "^1.1.6"
}
},
"node_modules/jest": { "node_modules/jest": {
"version": "29.7.0", "version": "29.7.0",
"resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz",
@ -16250,6 +16260,12 @@
"jsesc": "bin/jsesc" "jsesc": "bin/jsesc"
} }
}, },
"node_modules/relative-time-format": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/relative-time-format/-/relative-time-format-1.1.6.tgz",
"integrity": "sha512-aCv3juQw4hT1/P/OrVltKWLlp15eW1GRcwP1XdxHrPdZE9MtgqFpegjnTjLhi2m2WI9MT/hQQtE+tjEWG1hgkQ==",
"dev": true
},
"node_modules/relay-runtime": { "node_modules/relay-runtime": {
"version": "12.0.0", "version": "12.0.0",
"resolved": "https://registry.npmjs.org/relay-runtime/-/relay-runtime-12.0.0.tgz", "resolved": "https://registry.npmjs.org/relay-runtime/-/relay-runtime-12.0.0.tgz",

View File

@ -117,6 +117,7 @@
"husky": "8.0.3", "husky": "8.0.3",
"hygen": "6.2.11", "hygen": "6.2.11",
"i18next-http-backend": "2.2.0", "i18next-http-backend": "2.2.0",
"javascript-time-ago": "2.5.9",
"jest": "29.7.0", "jest": "29.7.0",
"js-cookie": "3.0.5", "js-cookie": "3.0.5",
"lint-staged": "14.0.1", "lint-staged": "14.0.1",

View File

@ -210,21 +210,17 @@
"New only": "New only", "New only": "New only",
"New password": "New password", "New password": "New password",
"New stories every day and even more!": "New stories and more are waiting for you every day!", "New stories every day and even more!": "New stories and more are waiting for you every day!",
"NotificationNewCommentText1": "{commentsCount, plural, one {New comment} other {{commentsCount} comments}} to your publication",
"NotificationNewCommentText2": "from",
"NotificationNewCommentText3": "{restUsersCount, plural, =0 {} one { one more user} other { and more {restUsersCount} users}}",
"NotificationNewReplyText1": "{commentsCount, plural, one {New reply} other {{commentsCount} replays}} to your publication",
"NotificationNewReplyText2": "from",
"NotificationNewReplyText3": "{restUsersCount, plural, =0 {} one { and one more user} other { and more {restUsersCount} users}}",
"Newsletter": "Newsletter", "Newsletter": "Newsletter",
"Night mode": "Night mode", "Night mode": "Night mode",
"No notifications yet": "No notifications yet", "No notifications yet": "No notifications yet",
"Write good articles, comment\nand it won't be so empty here": "Write good articles, comment\nand it won't be so empty here",
"Nothing here yet": "There's nothing here yet", "Nothing here yet": "There's nothing here yet",
"Nothing is here": "There is nothing here", "Nothing is here": "There is nothing here",
"NotificationNewCommentText1": "{commentsCount, plural, one {New comment} other {{commentsCount} comments}} to your publication",
"NotificationNewCommentText2": "from",
"NotificationNewCommentText3": "{restUsersCount, plural, =0 {} one { one more user} other { and more {restUsersCount} users}}",
"NotificationNewReplyText1": "{commentsCount, plural, one {New reply} other {{commentsCount} replays}} to your publication",
"NotificationNewReplyText2": "from",
"NotificationNewReplyText3": "{restUsersCount, plural, =0 {} one { and one more user} other { and more {restUsersCount} users}}",
"Notifications": "Notifications", "Notifications": "Notifications",
"Or paste a link to an image": "Or paste a link to an image", "Or paste a link to an image": "Or paste a link to an image",
"Ordered list": "Ordered list", "Ordered list": "Ordered list",
@ -369,6 +365,7 @@
"Write about the topic": "Write about the topic", "Write about the topic": "Write about the topic",
"Write an article": "Write an article", "Write an article": "Write an article",
"Write comment": "Write comment", "Write comment": "Write comment",
"Write good articles, comment\nand it won't be so empty here": "Write good articles, comment\nand it won't be so empty here",
"Write message": "Write a message", "Write message": "Write a message",
"Write to us": "Write to us", "Write to us": "Write to us",
"You can download multiple tracks at once in .mp3, .wav or .flac formats": "You can download multiple tracks at once in .mp3, .wav or .flac formats", "You can download multiple tracks at once in .mp3, .wav or .flac formats": "You can download multiple tracks at once in .mp3, .wav or .flac formats",
@ -385,6 +382,7 @@
"article": "article", "article": "article",
"author": "author", "author": "author",
"authors": "authors", "authors": "authors",
"authorsWithCount": "{count} {count, plural, one {author} other {authors}}",
"back to menu": "back to menu", "back to menu": "back to menu",
"bold": "bold", "bold": "bold",
"bookmarks": "bookmarks", "bookmarks": "bookmarks",
@ -395,10 +393,12 @@
"delimiter": "delimiter", "delimiter": "delimiter",
"discussion": "discourse", "discussion": "discourse",
"drafts": "drafts", "drafts": "drafts",
"earlier": "earlier",
"email not confirmed": "email not confirmed", "email not confirmed": "email not confirmed",
"enter": "enter", "enter": "enter",
"feed": "feed", "feed": "feed",
"follower": "follower", "follower": "follower",
"followersWithCount": "{count} {count, plural, one {follower} other {followers}}",
"general feed": "general tape", "general feed": "general tape",
"header 1": "header 1", "header 1": "header 1",
"header 2": "header 2", "header 2": "header 2",
@ -420,6 +420,7 @@
"register": "register", "register": "register",
"repeat": "repeat", "repeat": "repeat",
"shout": "post", "shout": "post",
"shoutsWithCount": "{count} {count, plural, one {post} other {posts}}",
"sign up or sign in": "sign up or sign in", "sign up or sign in": "sign up or sign in",
"slug is used by another user": "Slug is already taken by another user", "slug is used by another user": "Slug is already taken by another user",
"subscriber": "subscriber", "subscriber": "subscriber",
@ -429,8 +430,10 @@
"subscription_rp": "subscription", "subscription_rp": "subscription",
"subscriptions": "subscriptions", "subscriptions": "subscriptions",
"terms of use": "terms of use", "terms of use": "terms of use",
"today": "today",
"topics": "topics", "topics": "topics",
"user already exist": "user already exists", "user already exist": "user already exists",
"video": "video", "video": "video",
"view": "view" "view": "view",
"yesterday": "yesterday"
} }

View File

@ -220,22 +220,18 @@
"New only": "Только новые", "New only": "Только новые",
"New password": "Новый пароль", "New password": "Новый пароль",
"New stories every day and even more!": "Каждый день вас ждут новые истории и ещё много всего интересного!", "New stories every day and even more!": "Каждый день вас ждут новые истории и ещё много всего интересного!",
"NotificationNewCommentText1": "{commentsCount, plural, one {Новый комментарий} few {{commentsCount} новых комментария} other {{commentsCount} новых комментариев}} к вашей публикации",
"NotificationNewCommentText2": "от",
"NotificationNewCommentText3": "{restUsersCount, plural, =0 {} one { и ещё 1 пользователя} few { и ещё {restUsersCount} пользователей} other { и ещё {restUsersCount} пользователей}}",
"NotificationNewReplyText1": "{commentsCount, plural, one {Новый ответ} few {{commentsCount} новых ответа} other {{commentsCount} новых ответов}} на ваш комментарий к публикации",
"NotificationNewReplyText2": "от",
"NotificationNewReplyText3": "{restUsersCount, plural, =0 {} one { и ещё 1 пользователя} few { и ещё {restUsersCount} пользователей} other { и ещё {restUsersCount} пользователей}}",
"Newsletter": "Рассылка", "Newsletter": "Рассылка",
"Night mode": "Ночная тема", "Night mode": "Ночная тема",
"No notifications yet": "Уведомлений пока нет", "No notifications yet": "Уведомлений пока нет",
"Write good articles, comment\nand it won't be so empty here": "Пишите хорошие статьи, комментируйте,\nи здесь станет не так пусто",
"No such account, please try to register": "Такой адрес не найден, попробуйте зарегистрироваться", "No such account, please try to register": "Такой адрес не найден, попробуйте зарегистрироваться",
"Nothing here yet": "Здесь пока ничего нет", "Nothing here yet": "Здесь пока ничего нет",
"Nothing is here": "Здесь ничего нет", "Nothing is here": "Здесь ничего нет",
"NotificationNewCommentText1": "{commentsCount, plural, one {Новый комментарий} few {{commentsCount} новых комментария} other {{commentsCount} новых комментариев}} к вашей публикации",
"NotificationNewCommentText2": "от",
"NotificationNewCommentText3": "{restUsersCount, plural, =0 {} one { и ещё 1 пользователя} few { и ещё {restUsersCount} пользователей} other { и ещё {restUsersCount} пользователей}}",
"NotificationNewReplyText1": "{commentsCount, plural, one {Новый ответ} few {{commentsCount} новых ответа} other {{commentsCount} новых ответов}} на ваш комментарий к публикации",
"NotificationNewReplyText2": "от",
"NotificationNewReplyText3": "{restUsersCount, plural, =0 {} one { и ещё 1 пользователя} few { и ещё {restUsersCount} пользователей} other { и ещё {restUsersCount} пользователей}}",
"Notifications": "Уведомления", "Notifications": "Уведомления",
"Or paste a link to an image": "Или вставьте ссылку на изображение", "Or paste a link to an image": "Или вставьте ссылку на изображение",
"Ordered list": "Нумерованный список", "Ordered list": "Нумерованный список",
@ -389,6 +385,7 @@
"Write about the topic": "Написать в тему", "Write about the topic": "Написать в тему",
"Write an article": "Написать статью", "Write an article": "Написать статью",
"Write comment": "Написать комментарий", "Write comment": "Написать комментарий",
"Write good articles, comment\nand it won't be so empty here": "Пишите хорошие статьи, комментируйте,\nи здесь станет не так пусто",
"Write message": "Написать сообщение", "Write message": "Написать сообщение",
"Write to us": "Напишите нам", "Write to us": "Напишите нам",
"You can download multiple tracks at once in .mp3, .wav or .flac formats": "Можно загрузить сразу несколько треков в форматах .mp3, .wav или .flac", "You can download multiple tracks at once in .mp3, .wav or .flac formats": "Можно загрузить сразу несколько треков в форматах .mp3, .wav или .flac",
@ -405,6 +402,7 @@
"article": "статья", "article": "статья",
"author": "автор", "author": "автор",
"authors": "авторы", "authors": "авторы",
"authorsWithCount": "{count} {count, plural, one {автор} few {автора} other {авторов}}",
"back to menu": "назад в меню", "back to menu": "назад в меню",
"bold": "жирный", "bold": "жирный",
"bookmarks": "закладки", "bookmarks": "закладки",
@ -418,10 +416,12 @@
"discourse_theme": "Тема дискурса", "discourse_theme": "Тема дискурса",
"discussion": "дискурс", "discussion": "дискурс",
"drafts": "черновики", "drafts": "черновики",
"earlier": "ранее",
"email not confirmed": "email не подтвержден", "email not confirmed": "email не подтвержден",
"enter": "войдите", "enter": "войдите",
"feed": "лента", "feed": "лента",
"follower": "подписчик", "follower": "подписчик",
"followersWithCount": "{count} {count, plural, one {подписчик} few {подписчика} other {подписчиков}}",
"general feed": "Общая лента", "general feed": "Общая лента",
"header 1": "заголовок 1", "header 1": "заголовок 1",
"header 2": "заголовок 2", "header 2": "заголовок 2",
@ -444,6 +444,7 @@
"register": "зарегистрируйтесь", "register": "зарегистрируйтесь",
"repeat": "повторить", "repeat": "повторить",
"shout": "пост", "shout": "пост",
"shoutsWithCount": "{count} {count, plural, one {пост} few {поста} other {постов}}",
"sign in": "войти", "sign in": "войти",
"sign up": "зарегистрироваться", "sign up": "зарегистрироваться",
"sign up or sign in": "зарегистрироваться или войти", "sign up or sign in": "зарегистрироваться или войти",
@ -453,8 +454,10 @@
"subscriber_rp": "подписчика", "subscriber_rp": "подписчика",
"subscribers": "подписчиков", "subscribers": "подписчиков",
"terms of use": "правилами пользования сайтом", "terms of use": "правилами пользования сайтом",
"today": "сегодня",
"topics": "темы", "topics": "темы",
"user already exist": "пользователь уже существует", "user already exist": "пользователь уже существует",
"video": "видео", "video": "видео",
"view": "просмотр" "view": "просмотр",
"yesterday": "вчера"
} }

View File

@ -1,7 +1,6 @@
import { Show } from 'solid-js' import { Show } from 'solid-js'
import { Icon } from '../_shared/Icon' import { Icon } from '../_shared/Icon'
import type { Reaction } from '../../graphql/types.gen' import type { Reaction } from '../../graphql/types.gen'
import { formatDate } from '../../utils'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import styles from './CommentDate.module.scss' import styles from './CommentDate.module.scss'
@ -13,9 +12,9 @@ type Props = {
} }
export const CommentDate = (props: Props) => { export const CommentDate = (props: Props) => {
const { t } = useLocalize() const { t, formatDate } = useLocalize()
const formattedDate = (date) => { const formattedDate = (date: number) => {
const formatDateOptions: Intl.DateTimeFormatOptions = props.isShort const formatDateOptions: Intl.DateTimeFormatOptions = props.isShort
? { month: 'long', day: 'numeric', year: 'numeric' } ? { month: 'long', day: 'numeric', year: 'numeric' }
: { hour: 'numeric', minute: 'numeric' } : { hour: 'numeric', minute: 'numeric' }

View File

@ -8,8 +8,7 @@ import { useSession } from '../../context/session'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { useReactions } from '../../context/reactions' import { useReactions } from '../../context/reactions'
import { MediaItem } from '../../pages/types' import { MediaItem } from '../../pages/types'
import { router, useRouter } from '../../stores/router' import { DEFAULT_HEADER_OFFSET, router, useRouter } from '../../stores/router'
import { formatDate } from '../../utils'
import { getDescription } from '../../utils/meta' import { getDescription } from '../../utils/meta'
import { imageProxy } from '../../utils/imageProxy' import { imageProxy } from '../../utils/imageProxy'
import { AuthorCard } from '../Author/AuthorCard' import { AuthorCard } from '../Author/AuthorCard'
@ -42,14 +41,14 @@ const scrollTo = (el: HTMLElement) => {
const { top } = el.getBoundingClientRect() const { top } = el.getBoundingClientRect()
window.scrollTo({ window.scrollTo({
top: top + window.scrollY - 96, top: top + window.scrollY - DEFAULT_HEADER_OFFSET,
left: 0, left: 0,
behavior: 'smooth' behavior: 'smooth'
}) })
} }
export const FullArticle = (props: Props) => { export const FullArticle = (props: Props) => {
const { t } = useLocalize() const { t, formatDate } = useLocalize()
const { const {
user, user,
isAuthenticated, isAuthenticated,

View File

@ -3,7 +3,6 @@ import styles from './AuthorBadge.module.scss'
import { Userpic } from '../Userpic' import { Userpic } from '../Userpic'
import { Author, FollowingEntity } from '../../../graphql/types.gen' import { Author, FollowingEntity } from '../../../graphql/types.gen'
import { createMemo, createSignal, Match, Show, Switch } from 'solid-js' import { createMemo, createSignal, Match, Show, Switch } from 'solid-js'
import { formatDate } from '../../../utils'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { Button } from '../../_shared/Button' import { Button } from '../../_shared/Button'
import { useSession } from '../../../context/session' import { useSession } from '../../../context/session'
@ -21,7 +20,7 @@ export const AuthorBadge = (props: Props) => {
actions: { loadSession, requireAuthentication } actions: { loadSession, requireAuthentication }
} = useSession() } = useSession()
const { t } = useLocalize() const { t, formatDate } = useLocalize()
const subscribed = createMemo<boolean>(() => { const subscribed = createMemo<boolean>(() => {
return session()?.news?.authors?.some((u) => u === props.author.slug) || false return session()?.news?.authors?.some((u) => u === props.author.slug) || false
}) })

View File

@ -1,8 +1,7 @@
import type { Author } from '../../../graphql/types.gen' import type { Author } from '../../../graphql/types.gen'
import { Userpic } from '../Userpic' import { Userpic } from '../Userpic'
import { Icon } from '../../_shared/Icon' import { Icon } from '../../_shared/Icon'
import styles from './AuthorCard.module.scss' import { createEffect, createMemo, createSignal, For, Show } from 'solid-js'
import { createEffect, createMemo, createSignal, For, Match, Show, Switch } from 'solid-js'
import { translit } from '../../../utils/ru2en' import { translit } from '../../../utils/ru2en'
import { follow, unfollow } from '../../../stores/zine/common' import { follow, unfollow } from '../../../stores/zine/common'
import { clsx } from 'clsx' import { clsx } from 'clsx'
@ -20,7 +19,7 @@ import { AuthorBadge } from '../AuthorBadge'
import { TopicBadge } from '../../Topic/TopicBadge' import { TopicBadge } from '../../Topic/TopicBadge'
import { Button } from '../../_shared/Button' import { Button } from '../../_shared/Button'
import { getShareUrl, SharePopup } from '../../Article/SharePopup' import { getShareUrl, SharePopup } from '../../Article/SharePopup'
import stylesHeader from '../../Nav/Header/Header.module.scss' import styles from './AuthorCard.module.scss'
type Props = { type Props = {
caption?: string caption?: string

View File

@ -2,8 +2,6 @@ import { clsx } from 'clsx'
import styles from './Draft.module.scss' import styles from './Draft.module.scss'
import type { Shout } from '../../graphql/types.gen' import type { Shout } from '../../graphql/types.gen'
import { Icon } from '../_shared/Icon' import { Icon } from '../_shared/Icon'
import { formatDate } from '../../utils'
import formatDateTime from '../../utils/formatDateTime'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { useConfirm } from '../../context/confirm' import { useConfirm } from '../../context/confirm'
import { useSnackbar } from '../../context/snackbar' import { useSnackbar } from '../../context/snackbar'
@ -18,7 +16,7 @@ type Props = {
} }
export const Draft = (props: Props) => { export const Draft = (props: Props) => {
const { t } = useLocalize() const { t, formatDate } = useLocalize()
const { const {
actions: { showConfirm } actions: { showConfirm }
} = useConfirm() } = useConfirm()
@ -51,8 +49,8 @@ export const Draft = (props: Props) => {
return ( return (
<div class={clsx(props.class)}> <div class={clsx(props.class)}>
<div class={styles.created}> <div class={styles.created}>
<Icon name="pencil-outline" class={styles.icon} /> {formatDate(new Date(props.shout.createdAt))} <Icon name="pencil-outline" class={styles.icon} />{' '}
&nbsp;{formatDateTime(props.shout.createdAt)()} {formatDate(new Date(props.shout.createdAt), { hour: '2-digit', minute: '2-digit' })}
</div> </div>
<div class={styles.titleContainer}> <div class={styles.titleContainer}>
<span class={styles.title}>{props.shout.title || t('Unnamed draft')}</span> {props.shout.subtitle} <span class={styles.title}>{props.shout.title || t('Unnamed draft')}</span> {props.shout.subtitle}

View File

@ -1,6 +1,5 @@
import { createMemo, createSignal, For, Show } from 'solid-js' import { createMemo, createSignal, For, Show } from 'solid-js'
import type { Shout } from '../../graphql/types.gen' import type { Shout } from '../../graphql/types.gen'
import { capitalize, formatDate } from '../../utils'
import { Icon } from '../_shared/Icon' import { Icon } from '../_shared/Icon'
import styles from './ArticleCard.module.scss' import styles from './ArticleCard.module.scss'
import { clsx } from 'clsx' import { clsx } from 'clsx'
@ -17,6 +16,7 @@ import { imageProxy } from '../../utils/imageProxy'
import { Popover } from '../_shared/Popover' import { Popover } from '../_shared/Popover'
import { AuthorCard } from '../Author/AuthorCard' import { AuthorCard } from '../Author/AuthorCard'
import { useSession } from '../../context/session' import { useSession } from '../../context/session'
import { capitalize } from '../../utils/capitalize'
interface ArticleCardProps { interface ArticleCardProps {
settings?: { settings?: {
@ -44,7 +44,12 @@ interface ArticleCardProps {
article: Shout article: Shout
} }
const getTitleAndSubtitle = (article: Shout): { title: string; subtitle: string } => { const getTitleAndSubtitle = (
article: Shout
): {
title: string
subtitle: string
} => {
let title = article.title let title = article.title
let subtitle = article.subtitle let subtitle = article.subtitle
@ -66,14 +71,14 @@ const getTitleAndSubtitle = (article: Shout): { title: string; subtitle: string
} }
export const ArticleCard = (props: ArticleCardProps) => { export const ArticleCard = (props: ArticleCardProps) => {
const { t, lang } = useLocalize() const { t, lang, formatDate } = useLocalize()
const { user } = useSession() const { user } = useSession()
const mainTopic = const mainTopic =
props.article.topics.find((articleTopic) => articleTopic.slug === props.article.mainTopic) || props.article.topics.find((articleTopic) => articleTopic.slug === props.article.mainTopic) ||
props.article.topics[0] props.article.topics[0]
const formattedDate = createMemo<string>(() => { const formattedDate = createMemo<string>(() => {
return formatDate(new Date(props.article.createdAt), { month: 'long', day: 'numeric', year: 'numeric' }) return formatDate(new Date(props.article.createdAt))
}) })
const { title, subtitle } = getTitleAndSubtitle(props.article) const { title, subtitle } = getTitleAndSubtitle(props.article)

View File

@ -2,7 +2,6 @@ import { Show, Switch, Match, createMemo } from 'solid-js'
import DialogAvatar from './DialogAvatar' import DialogAvatar from './DialogAvatar'
import type { ChatMember } from '../../graphql/types.gen' import type { ChatMember } from '../../graphql/types.gen'
import GroupDialogAvatar from './GroupDialogAvatar' import GroupDialogAvatar from './GroupDialogAvatar'
import formattedTime from '../../utils/formatDateTime'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import styles from './DialogCard.module.scss' import styles from './DialogCard.module.scss'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
@ -20,7 +19,7 @@ type DialogProps = {
} }
const DialogCard = (props: DialogProps) => { const DialogCard = (props: DialogProps) => {
const { t } = useLocalize() const { t, formatTime } = useLocalize()
const companions = createMemo( const companions = createMemo(
() => props.members && props.members.filter((member) => member.id !== props.ownId) () => props.members && props.members.filter((member) => member.id !== props.ownId)
) )
@ -64,7 +63,7 @@ const DialogCard = (props: DialogProps) => {
<Show when={!props.isChatHeader}> <Show when={!props.isChatHeader}>
<div class={styles.activity}> <div class={styles.activity}>
<Show when={props.lastUpdate}> <Show when={props.lastUpdate}>
<div class={styles.time}>{formattedTime(props.lastUpdate * 1000)()}</div> <div class={styles.time}>{formatTime(new Date(props.lastUpdate * 1000))}</div>
</Show> </Show>
<Show when={props.counter > 0}> <Show when={props.counter > 0}>
<div class={styles.counter}> <div class={styles.counter}>

View File

@ -3,10 +3,10 @@ import { clsx } from 'clsx'
import styles from './Message.module.scss' import styles from './Message.module.scss'
import DialogAvatar from './DialogAvatar' import DialogAvatar from './DialogAvatar'
import type { Message as MessageType, ChatMember } from '../../graphql/types.gen' import type { Message as MessageType, ChatMember } from '../../graphql/types.gen'
import formattedTime from '../../utils/formatDateTime'
import { Icon } from '../_shared/Icon' import { Icon } from '../_shared/Icon'
import { MessageActionsPopup } from './MessageActionsPopup' import { MessageActionsPopup } from './MessageActionsPopup'
import QuotedMessage from './QuotedMessage' import QuotedMessage from './QuotedMessage'
import { useLocalize } from '../../context/localize'
type Props = { type Props = {
content: MessageType content: MessageType
@ -18,6 +18,7 @@ type Props = {
} }
export const Message = (props: Props) => { export const Message = (props: Props) => {
const { formatTime } = useLocalize()
const isOwn = props.ownId === Number(props.content.author) const isOwn = props.ownId === Number(props.content.author)
const user = props.members?.find((m) => m.id === Number(props.content.author)) const user = props.members?.find((m) => m.id === Number(props.content.author))
const [isPopupVisible, setIsPopupVisible] = createSignal<boolean>(false) const [isPopupVisible, setIsPopupVisible] = createSignal<boolean>(false)
@ -47,7 +48,7 @@ export const Message = (props: Props) => {
<div innerHTML={props.content.body} /> <div innerHTML={props.content.body} />
</div> </div>
</div> </div>
<div class={styles.time}>{formattedTime(props.content.createdAt * 1000)()}</div> <div class={styles.time}>{formatTime(new Date(props.content.createdAt * 1000))}</div>
</div> </div>
) )
} }

View File

@ -36,4 +36,9 @@
.timeContainer { .timeContainer {
margin-left: auto; margin-left: auto;
padding-left: 16px; padding-left: 16px;
color: var(--black-400);
font-size: 12px;
font-weight: 500;
line-height: 16px;
align-self: flex-start;
} }

View File

@ -1,17 +1,19 @@
import { clsx } from 'clsx' import { clsx } from 'clsx'
import styles from './NotificationView.module.scss'
import { formatDate } from '../../../utils'
import { createMemo, createSignal, onMount, Show } from 'solid-js' import { createMemo, createSignal, onMount, Show } from 'solid-js'
import { Author } from '../../../graphql/types.gen' import { Author } from '../../../graphql/types.gen'
import { openPage } from '@nanostores/router' import { openPage } from '@nanostores/router'
import { router } from '../../../stores/router' import { router, useRouter } from '../../../stores/router'
import { ServerNotification, useNotifications } from '../../../context/notifications' import { ServerNotification, useNotifications } from '../../../context/notifications'
import { Userpic } from '../../Author/Userpic' import { Userpic } from '../../Author/Userpic'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import type { ArticlePageSearchParams } from '../../Article/FullArticle'
import { TimeAgo } from '../../_shared/TimeAgo'
import styles from './NotificationView.module.scss'
type Props = { type Props = {
notification: ServerNotification notification: ServerNotification
onClick: () => void onClick: () => void
dateTimeFormat: 'ago' | 'time' | 'date'
class?: string class?: string
} }
@ -49,9 +51,11 @@ export const NotificationView = (props: Props) => {
const { const {
actions: { markNotificationAsRead } actions: { markNotificationAsRead }
} = useNotifications() } = useNotifications()
const { t } = useLocalize()
const [data, setData] = createSignal<ServerNotification>(null) const [data, setData] = createSignal<ServerNotification>(null)
const [kind, setKind] = createSignal<NotificationType>() const [kind, setKind] = createSignal<NotificationType>()
const { changeSearchParam } = useRouter<ArticlePageSearchParams>()
const { t, formatDate, formatTime } = useLocalize()
onMount(() => { onMount(() => {
setTimeout(() => setData(props.notification)) setTimeout(() => setData(props.notification))
}) })
@ -110,6 +114,20 @@ export const NotificationView = (props: Props) => {
props.onClick() props.onClick()
} }
const formattedDateTime = createMemo(() => {
switch (props.dateTimeFormat) {
case 'ago': {
return <TimeAgo date={props.notification.timestamp} />
}
case 'time': {
return formatTime(new Date(props.notification.timestamp))
}
case 'date': {
return formatDate(new Date(props.notification.timestamp), { month: 'numeric', year: '2-digit' })
}
}
})
return ( return (
<Show when={data()}> <Show when={data()}>
<div <div
@ -120,9 +138,7 @@ export const NotificationView = (props: Props) => {
> >
<Userpic name={lastUser().name} userpic={lastUser().userpic} class={styles.userpic} /> <Userpic name={lastUser().name} userpic={lastUser().userpic} class={styles.userpic} />
<div>{content()}</div> <div>{content()}</div>
<div class={styles.timeContainer}> <div class={styles.timeContainer}>{formattedDateTime()}</div>
{/*{formatDate(new Date(props.notification.timestamp), { month: 'numeric' })}*/}
</div>
</div> </div>
</Show> </Show>
) )

View File

@ -64,3 +64,12 @@ $transition-duration: 200ms;
.emptyMessageContainer { .emptyMessageContainer {
text-align: center; text-align: center;
} }
.periodTitle {
// TODO: check markup
margin: 32px 0 16px 0;
color: var(--black-400);
font-size: 12px;
font-weight: 500;
line-height: 14px;
}

View File

@ -4,7 +4,7 @@ import { useEscKeyDownHandler } from '../../utils/useEscKeyDownHandler'
import { useOutsideClickHandler } from '../../utils/useOutsideClickHandler' import { useOutsideClickHandler } from '../../utils/useOutsideClickHandler'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { Icon } from '../_shared/Icon' import { Icon } from '../_shared/Icon'
import { createEffect, For } from 'solid-js' import { createEffect, createMemo, For, Show } from 'solid-js'
import { useNotifications } from '../../context/notifications' import { useNotifications } from '../../context/notifications'
import { NotificationView } from './NotificationView' import { NotificationView } from './NotificationView'
import { EmptyMessage } from './EmptyMessage' import { EmptyMessage } from './EmptyMessage'
@ -14,6 +14,30 @@ type Props = {
onClose: () => void onClose: () => void
} }
const getYesterdayStart = () => {
const now = new Date()
return new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1, 0, 0, 0, 0)
}
const isSameDate = (date1: Date, date2: Date) =>
date1.getDate() === date2.getDate() &&
date1.getMonth() === date2.getMonth() &&
date1.getFullYear() === date2.getFullYear()
const isToday = (date: Date) => {
return isSameDate(date, new Date())
}
const isYesterday = (date: Date) => {
const yesterday = getYesterdayStart()
return isSameDate(date, yesterday)
}
const isEarlier = (date: Date) => {
const yesterday = getYesterdayStart()
return date.getTime() < yesterday.getTime()
}
export const NotificationsPanel = (props: Props) => { export const NotificationsPanel = (props: Props) => {
const { t } = useLocalize() const { t } = useLocalize()
const { sortedNotifications } = useNotifications() const { sortedNotifications } = useNotifications()
@ -55,6 +79,18 @@ export const NotificationsPanel = (props: Props) => {
handleHide() handleHide()
} }
const todayNotifications = createMemo(() => {
return sortedNotifications().filter((notification) => isToday(new Date(notification.createdAt)))
})
const yesterdayNotifications = createMemo(() => {
return sortedNotifications().filter((notification) => isYesterday(new Date(notification.createdAt)))
})
const earlierNotifications = createMemo(() => {
return sortedNotifications().filter((notification) => isEarlier(new Date(notification.createdAt)))
})
return ( return (
<div <div
class={clsx(styles.container, { class={clsx(styles.container, {
@ -67,15 +103,47 @@ export const NotificationsPanel = (props: Props) => {
<Icon name="close" /> <Icon name="close" />
</div> </div>
<div class={styles.title}>{t('Notifications')}</div> <div class={styles.title}>{t('Notifications')}</div>
<For each={sortedNotifications()} fallback={<EmptyMessage />}> <Show when={sortedNotifications().length > 0} fallback={<EmptyMessage />}>
<Show when={todayNotifications().length > 0}>
<div class={styles.periodTitle}>{t('today')}</div>
<For each={todayNotifications()}>
{(notification) => ( {(notification) => (
<NotificationView <NotificationView
notification={notification} notification={notification}
class={styles.notificationView} class={styles.notificationView}
onClick={handleNotificationViewClick} onClick={handleNotificationViewClick}
dateTimeFormat={'ago'}
/> />
)} )}
</For> </For>
</Show>
<Show when={yesterdayNotifications().length > 0}>
<div class={styles.periodTitle}>{t('yesterday')}</div>
<For each={yesterdayNotifications()}>
{(notification) => (
<NotificationView
notification={notification}
class={styles.notificationView}
onClick={handleNotificationViewClick}
dateTimeFormat={'time'}
/>
)}
</For>
</Show>
<Show when={earlierNotifications().length > 0}>
<div class={styles.periodTitle}>{t('earlier')}</div>
<For each={earlierNotifications()}>
{(notification) => (
<NotificationView
notification={notification}
class={styles.notificationView}
onClick={handleNotificationViewClick}
dateTimeFormat={'date'}
/>
)}
</For>
</Show>
</Show>
</div> </div>
</div> </div>
) )

View File

@ -1,5 +1,3 @@
import { capitalize } from '../../utils'
import styles from './Card.module.scss'
import { createMemo, createSignal, Show } from 'solid-js' import { createMemo, createSignal, Show } from 'solid-js'
import type { Topic } from '../../graphql/types.gen' import type { Topic } from '../../graphql/types.gen'
import { FollowingEntity } from '../../graphql/types.gen' import { FollowingEntity } from '../../graphql/types.gen'
@ -12,6 +10,9 @@ import { Icon } from '../_shared/Icon'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { CardTopic } from '../Feed/CardTopic' import { CardTopic } from '../Feed/CardTopic'
import { CheckButton } from '../_shared/CheckButton' import { CheckButton } from '../_shared/CheckButton'
import { capitalize } from '../../utils/capitalize'
import styles from './Card.module.scss'
interface TopicProps { interface TopicProps {
topic: Topic topic: Topic
@ -109,14 +110,6 @@ export const TopicCard = (props: TopicProps) => {
{props.topic.body} {props.topic.body}
</div> </div>
</Show> </Show>
<Show when={props.showDescription && !props.topic?.body && props.topic.stat?.shouts > 0}>
<div
class={clsx(styles.topicDescription)}
classList={{ [styles.topicDescriptionShort]: props.shortDescription }}
>
{props.topic.stat?.shouts} публикаций
</div>
</Show>
</div> </div>
<div <div
class={styles.controlContainer} class={styles.controlContainer}

View File

@ -1,4 +1,4 @@
.allTopicsPage { .allAuthorsPage {
.group { .group {
@include font-size(1.6rem); @include font-size(1.6rem);
@ -32,10 +32,6 @@
} }
} }
.stats {
margin-top: 2.4rem;
}
.loadMoreContainer { .loadMoreContainer {
margin-top: 48px; margin-top: 48px;
text-align: center; text-align: center;
@ -52,6 +48,7 @@
.alphabet { .alphabet {
@include font-size(1.5rem); @include font-size(1.5rem);
color: rgba(0 0 0 / 20%); color: rgba(0 0 0 / 20%);
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@ -71,6 +68,7 @@
.articlesCounter { .articlesCounter {
@include font-size(1.2rem); @include font-size(1.2rem);
margin-left: 0.5em; margin-left: 0.5em;
vertical-align: super; vertical-align: super;
} }

View File

@ -6,12 +6,13 @@ import { useRouter } from '../../stores/router'
import { AuthorCard } from '../Author/AuthorCard' import { AuthorCard } from '../Author/AuthorCard'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { useSession } from '../../context/session' import { useSession } from '../../context/session'
import styles from '../../styles/AllTopics.module.scss'
import { SearchField } from '../_shared/SearchField' import { SearchField } from '../_shared/SearchField'
import { scrollHandler } from '../../utils/scroll' import { scrollHandler } from '../../utils/scroll'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { dummyFilter } from '../../utils/dummyFilter' import { dummyFilter } from '../../utils/dummyFilter'
import styles from './AllAuthors.module.scss'
type AllAuthorsPageSearchParams = { type AllAuthorsPageSearchParams = {
by: '' | 'name' | 'shouts' | 'followers' by: '' | 'name' | 'shouts' | 'followers'
} }
@ -109,7 +110,7 @@ export const AllAuthorsView = (props: AllAuthorsViewProps) => {
) )
return ( return (
<div class={clsx(styles.allTopicsPage, 'wide-container')}> <div class={clsx(styles.allAuthorsPage, 'wide-container')}>
<Show when={sortedAuthors().length > 0}> <Show when={sortedAuthors().length > 0}>
<div class="offset-md-5"> <div class="offset-md-5">
<AllAuthorsHead /> <AllAuthorsHead />

View File

@ -0,0 +1,115 @@
.allTopicsPage {
.group {
@include font-size(1.6rem);
margin: 3em 0 9.6rem;
@include media-breakpoint-down(sm) {
margin-bottom: 6.4rem;
}
h2 {
margin-bottom: 3.2rem;
text-transform: capitalize;
@include media-breakpoint-down(sm) {
margin-bottom: 1.6rem;
}
}
.topic {
margin-bottom: 2.4rem;
}
}
.container {
width: auto;
.search-input {
display: inline-block;
width: 100px !important;
}
}
}
.stats {
@include font-size(1.7rem);
color: #9fa1a7;
display: flex;
margin: 0 0 1em;
@include media-breakpoint-down(md) {
flex-wrap: wrap;
}
@include media-breakpoint-down(sm) {
margin-top: 0.5em;
}
.statsItem {
@include font-size(1.5rem);
margin-right: 1.6rem;
white-space: nowrap;
&:last-child {
margin-right: 0;
}
&.compact {
font-size: small;
}
&.followers {
word-break: keep-all;
}
&.button {
float: right;
}
}
}
.loadMoreContainer {
margin-top: 48px;
text-align: center;
.loadMoreButton {
padding: 0.6em 3em;
width: 100%;
@include media-breakpoint-up(sm) {
width: auto;
}
}
}
.alphabet {
@include font-size(1.5rem);
color: rgba(0 0 0 / 20%);
display: flex;
flex-wrap: wrap;
font-weight: 700;
margin: 1.5em -3% 0 0;
li {
min-width: 1.5em;
margin-right: 3%;
color: rgb(0 0 0 / 30%);
}
a {
border: none;
}
}
.articlesCounter {
@include font-size(1.2rem);
margin-left: 0.5em;
vertical-align: super;
}
.viewSwitcher {
margin-bottom: 2rem;
}

View File

@ -6,13 +6,13 @@ import { useRouter } from '../../stores/router'
import { TopicCard } from '../Topic/Card' import { TopicCard } from '../Topic/Card'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { useSession } from '../../context/session' import { useSession } from '../../context/session'
import styles from '../../styles/AllTopics.module.scss'
import { SearchField } from '../_shared/SearchField' import { SearchField } from '../_shared/SearchField'
import { scrollHandler } from '../../utils/scroll' import { scrollHandler } from '../../utils/scroll'
import { StatMetrics } from '../_shared/StatMetrics'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { dummyFilter } from '../../utils/dummyFilter' import { dummyFilter } from '../../utils/dummyFilter'
import styles from './AllTopics.module.scss'
type AllTopicsPageSearchParams = { type AllTopicsPageSearchParams = {
by: 'shouts' | 'authors' | 'title' | '' by: 'shouts' | 'authors' | 'title' | ''
} }
@ -168,7 +168,17 @@ export const AllTopicsView = (props: AllTopicsViewProps) => {
showPublications={true} showPublications={true}
showDescription={true} showDescription={true}
/> />
<StatMetrics fields={['shouts', 'authors', 'followers']} stat={topic.stat} /> <div class={styles.stats}>
<span class={styles.statsItem}>
{t('shoutsWithCount', { count: topic.stat.shouts })}
</span>
<span class={styles.statsItem}>
{t('authorsWithCount', { count: topic.stat.authors })}
</span>
<span class={styles.statsItem}>
{t('followersWithCount', { count: topic.stat.followers })}
</span>
</div>
</> </>
)} )}
</For> </For>

View File

@ -1,36 +0,0 @@
.statMetrics {
@include font-size(1.7rem);
color: #9fa1a7;
display: flex;
margin: 0 0 1em;
@include media-breakpoint-down(md) {
flex-wrap: wrap;
}
@include media-breakpoint-down(sm) {
margin-top: 0.5em;
}
}
.statMetricsItem {
@include font-size(1.5rem);
margin-right: 1.6rem;
white-space: nowrap;
&:last-child {
margin-right: 0;
}
&.compact {
font-size: small;
}
&.followers {
word-break: keep-all;
}
&.button {
float: right;
}
}

View File

@ -1,45 +0,0 @@
import { For } from 'solid-js'
import type { Stat, TopicStat } from '../../graphql/types.gen'
import { plural } from '../../utils'
import styles from './Stat.module.scss'
import { useLocalize } from '../../context/localize'
interface StatMetricsProps {
fields?: string[]
stat: Stat | TopicStat
compact?: boolean
}
const pseudonames = {
// topics: 'topics' # amount of topics for community💥
followed: 'follower',
followers: 'follower',
rating: 'like',
viewed: 'view',
views: 'view',
reacted: 'involving',
reactions: 'involving',
commented: 'discussion',
comments: 'discussion',
shouts: 'post',
authors: 'author'
}
export const StatMetrics = (props: StatMetricsProps) => {
const { t, lang } = useLocalize()
return (
<div class={styles.statMetrics}>
<For each={props.fields}>
{(entity: string) => (
<span class={styles.statMetricsItem} classList={{ compact: props.compact }}>
{props.stat[entity] +
' ' +
t(pseudonames[entity] || entity.slice(-1)) +
plural(props.stat[entity] || 0, lang() === 'ru' ? ['ов', '', 'а'] : ['s', '', 's'])}
</span>
)}
</For>
</div>
)
}

View File

@ -0,0 +1,3 @@
.TimeAgo {
white-space: nowrap;
}

View File

@ -0,0 +1,37 @@
import { clsx } from 'clsx'
import { useLocalize } from '../../../context/localize'
import { createSignal, onCleanup, onMount } from 'solid-js'
import styles from './TimeAgo.module.scss'
type Props = {
date: any
class?: string
}
export const TimeAgo = (props: Props) => {
const { formatDate, formatTimeAgo } = useLocalize()
const [formattedTimeAgo, setFormattedTimeAgo] = createSignal(formatTimeAgo(new Date(props.date)))
onMount(() => {
let timerId: NodeJS.Timeout
const updateTimeAgo = () => {
timerId = setTimeout(() => {
setFormattedTimeAgo(formatTimeAgo(new Date(props.date)))
updateTimeAgo()
}, 1000)
}
updateTimeAgo()
onCleanup(() => clearTimeout(timerId))
})
return (
<div
class={clsx(styles.TimeAgo, props.class)}
title={formatDate(new Date(props.date), { month: '2-digit', hour: '2-digit', minute: '2-digit' })}
>
{formattedTimeAgo()}
</div>
)
}

View File

@ -0,0 +1 @@
export { TimeAgo } from './TimeAgo'

View File

@ -1,14 +1,23 @@
import type { i18n } from 'i18next' import type { i18n } from 'i18next'
import type { Accessor, JSX } from 'solid-js' import type { Accessor, JSX } from 'solid-js'
import { createContext, createEffect, createSignal, Show, useContext } from 'solid-js' import { createContext, createEffect, createMemo, createSignal, Show, useContext } from 'solid-js'
import { useRouter } from '../stores/router' import { useRouter } from '../stores/router'
import i18next, { changeLanguage, t } from 'i18next' import i18next, { changeLanguage, t } from 'i18next'
import Cookie from 'js-cookie' import Cookie from 'js-cookie'
import TimeAgo from 'javascript-time-ago'
import en from 'javascript-time-ago/locale/en'
import ru from 'javascript-time-ago/locale/ru'
TimeAgo.addLocale(en)
TimeAgo.addLocale(ru)
type LocalizeContextType = { type LocalizeContextType = {
t: i18n['t'] t: i18n['t']
lang: Accessor<Language> lang: Accessor<Language>
setLang: (lang: Language) => void setLang: (lang: Language) => void
formatTime: (date: Date, options?: Intl.DateTimeFormatOptions) => string
formatDate: (date: Date, options?: Intl.DateTimeFormatOptions) => string
formatTimeAgo: (date: Date) => string
} }
export type Language = 'ru' | 'en' export type Language = 'ru' | 'en'
@ -21,7 +30,9 @@ export function useLocalize() {
export const LocalizeProvider = (props: { children: JSX.Element }) => { export const LocalizeProvider = (props: { children: JSX.Element }) => {
const [lang, setLang] = createSignal<Language>(i18next.language === 'en' ? 'en' : 'ru') const [lang, setLang] = createSignal<Language>(i18next.language === 'en' ? 'en' : 'ru')
const { searchParams, changeSearchParam } = useRouter<{ lng: string }>() const { searchParams, changeSearchParam } = useRouter<{
lng: string
}>()
createEffect(() => { createEffect(() => {
if (!searchParams().lng) { if (!searchParams().lng) {
@ -36,7 +47,43 @@ export const LocalizeProvider = (props: { children: JSX.Element }) => {
changeSearchParam({ lng: null }, true) changeSearchParam({ lng: null }, true)
}) })
const value: LocalizeContextType = { t, lang, setLang } const formatTime = (date: Date, options: Intl.DateTimeFormatOptions = {}) => {
const opts = Object.assign(
{},
{
hour: '2-digit',
minute: '2-digit'
},
options
)
return date.toLocaleTimeString(lang(), opts)
}
const formatDate = (date: Date, options: Intl.DateTimeFormatOptions = {}) => {
const opts = Object.assign(
{},
{
month: 'long',
day: 'numeric',
year: 'numeric'
},
options
)
let result = date.toLocaleDateString(lang(), opts)
if (lang() === 'ru') {
result = result.replace(' г.', '')
}
return result
}
const timeAgo = createMemo(() => new TimeAgo(lang()))
const formatTimeAgo = (date: Date) => timeAgo().format(date)
const value: LocalizeContextType = { t, lang, setLang, formatTime, formatDate, formatTimeAgo }
return ( return (
<LocalizeContext.Provider value={value}> <LocalizeContext.Provider value={value}>

View File

@ -95,7 +95,7 @@ export const NotificationsProvider = (props: { children: JSX.Element }) => {
loadNotifications() loadNotifications()
const token = getToken() const token = getToken()
const eventSource = new EventSource(`https://chat.discours.io/connect/?token=${token}`) const eventSource = new EventSource(`https://connect.discours.io/${token}`)
eventSource.onmessage = (event) => { eventSource.onmessage = (event) => {
console.log('[context.notifications] Received event:', event) console.log('[context.notifications] Received event:', event)

View File

@ -4,10 +4,11 @@ import { App } from '../components/App'
import { initRouter } from '../stores/router' import { initRouter } from '../stores/router'
import type { PageContext } from './types' import type { PageContext } from './types'
import { MetaProvider, renderTags } from '@solidjs/meta' import { MetaProvider, renderTags } from '@solidjs/meta'
import i18next, { changeLanguage, init as initI18next } from 'i18next' import i18next from 'i18next'
import ru from '../../public/locales/ru/translation.json' import ru from '../../public/locales/ru/translation.json'
import en from '../../public/locales/en/translation.json' import en from '../../public/locales/en/translation.json'
import type { Language } from '../context/localize' import type { Language } from '../context/localize'
import ICU from 'i18next-icu'
export const passToClient = ['pageProps', 'lng', 'documentProps', 'is404'] export const passToClient = ['pageProps', 'lng', 'documentProps', 'is404']
@ -32,7 +33,7 @@ export const render = async (pageContext: PageContext) => {
if (!i18next.isInitialized) { if (!i18next.isInitialized) {
// eslint-disable-next-line import/no-named-as-default-member // eslint-disable-next-line import/no-named-as-default-member
await initI18next({ await i18next.use(ICU).init({
// debug: true, // debug: true,
supportedLngs: ['ru', 'en'], supportedLngs: ['ru', 'en'],
fallbackLng: lng, fallbackLng: lng,
@ -44,7 +45,7 @@ export const render = async (pageContext: PageContext) => {
} }
}) })
} else if (i18next.language !== lng) { } else if (i18next.language !== lng) {
await changeLanguage(lng) await i18next.changeLanguage(lng)
} }
if (pageContext.is404) { if (pageContext.is404) {

View File

@ -357,7 +357,7 @@ export const apiClient = {
/* /*
getNotifications: async (params: NotificationsQueryParams): Promise<NotificationsQueryResult> => { getNotifications: async (params: NotificationsQueryParams): Promise<NotificationsQueryResult> => {
const resp = await privateGraphQLClient.query(notifications, params).toPromise() const resp = await privateGraphQLClient.query(notifications, params).toPromise()
console.debug(resp.data) // console.debug(resp.data)
return resp.data.loadNotifications return resp.data.loadNotifications
}, },
markNotificationAsRead: async (notificationId: number): Promise<void> => { markNotificationAsRead: async (notificationId: number): Promise<void> => {

9
src/utils/capitalize.ts Normal file
View File

@ -0,0 +1,9 @@
export const capitalize = (originalString: string, firstonly = false) => {
const s = originalString.trim()
return firstonly
? s.charAt(0).toUpperCase() + s.slice(1)
: s
.split(' ')
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
.join(' ')
}

View File

@ -1,7 +1,6 @@
import { translit } from './ru2en' import { translit } from './ru2en'
import { Author, Topic } from '../graphql/types.gen' import { Author, Topic } from '../graphql/types.gen'
import { isAuthor } from './isAuthor'
type SearchData = Array<Author | Topic>
const prepareQuery = (searchQuery, lang) => { const prepareQuery = (searchQuery, lang) => {
const q = searchQuery.toLowerCase() const q = searchQuery.toLowerCase()
@ -14,9 +13,16 @@ const stringMatches = (str, q, lang) => {
return preparedStr.split(' ').some((word) => word.startsWith(q)) return preparedStr.split(' ').some((word) => word.startsWith(q))
} }
export const dummyFilter = (data: SearchData, searchQuery: string, lang: 'ru' | 'en'): SearchData => { export const dummyFilter = <T extends Topic | Author>(
data: T[],
searchQuery: string,
lang: 'ru' | 'en'
): T[] => {
const q = prepareQuery(searchQuery, lang) const q = prepareQuery(searchQuery, lang)
if (q.length === 0) return data
if (q.length === 0) {
return data
}
return data.filter((item) => { return data.filter((item) => {
const slugMatches = item.slug && item.slug.split('-').some((w) => w.startsWith(q)) const slugMatches = item.slug && item.slug.split('-').some((w) => w.startsWith(q))
@ -26,9 +32,10 @@ export const dummyFilter = (data: SearchData, searchQuery: string, lang: 'ru' |
return stringMatches(item.title, q, lang) return stringMatches(item.title, q, lang)
} }
if ('name' in item) { if (isAuthor(item)) {
return stringMatches(item.name, q, lang) || (item.bio && stringMatches(item.bio, q, lang)) return stringMatches(item.name, q, lang) || (item.bio && stringMatches(item.bio, q, lang))
} }
// If it does not match any of the 'slug', 'title', 'name' , 'bio' fields // If it does not match any of the 'slug', 'title', 'name' , 'bio' fields
// current element should not be included in the filtered array // current element should not be included in the filtered array
return false return false

View File

@ -1,17 +0,0 @@
import { Accessor, createMemo } from 'solid-js'
import { useLocalize } from '../context/localize'
// unix timestamp in seconds
const formattedTime = (time: number): Accessor<string> => {
// FIXME: maybe it's better to move it from here
const { lang } = useLocalize()
return createMemo<string>(() => {
return new Date(time).toLocaleTimeString(lang(), {
hour: 'numeric',
minute: 'numeric'
})
})
}
export default formattedTime

View File

@ -1,83 +0,0 @@
export const reflow = () => document.body.clientWidth
export const unique = (v) => {
const s = new Set(v)
return [...s]
}
export const preventSmoothScrollOnTabbing = () => {
document.addEventListener('keydown', (e) => {
if (e.key !== 'Tab') return
document.documentElement.style.scrollBehavior = ''
setTimeout(() => {
document.documentElement.style.scrollBehavior = 'smooth'
})
})
}
export const capitalize = (originalString: string, firstonly = false) => {
const s = originalString.trim()
return firstonly
? s.charAt(0).toUpperCase() + s.slice(1)
: s
.split(' ')
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
.join(' ')
}
export const plural = (amount: number, w: string[]) => {
try {
const a = amount.toString()
const x = Number.parseInt(a.at(-1))
const xx = Number.parseInt(a.at(-2) + a.at(-1))
if (xx > 5 && xx < 20) return w[0]
if (x === 1) return w[1]
if (x > 1 && x < 5) return w[2]
} catch (error) {
console.error('[utils] plural error', error)
}
return w[0]
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const shuffle = (items: any[]) => {
const cached = [...items]
let temp
let i = cached.length
let rand
while (--i) {
rand = Math.floor(i * Math.random())
temp = cached[rand]
cached[rand] = cached[i]
cached[i] = temp
}
return cached
}
export const snake2camel = (s: string) =>
s
.split(/(?=[A-Z])/)
.join('-')
.toLowerCase()
export const formatDate = (date: Date, options: Intl.DateTimeFormatOptions = {}) => {
const opts = Object.assign(
{},
{
month: 'long',
day: 'numeric',
year: 'numeric'
},
options
)
return date.toLocaleDateString('ru', opts).replace(' г.', '')
}