diff --git a/.eslintrc.json b/.eslintrc.json index 76b5de0..7b455b9 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -28,11 +28,9 @@ "eol-last": ["error", "always"], "object-shorthand": ["error", "always"], "tsdoc/syntax": "warn", - "react/function-component-definition": ["warn", { - "namedComponents": "arrow-function", - "unnamedComponents": "arrow-function" - }], - "react/display-name": ["error", { "ignoreTranspilerName": false }], + "react/display-name": 0, + "react/prop-types": 0, + "@typescript-eslint/no-explicit-any": 0, "@typescript-eslint/explicit-module-boundary-types": ["warn", { "allowArgumentsExplicitlyTypedAsAny": true, "allowDirectConstAssertionInArrowFunctions": true, diff --git a/.vscode/settings.json b/.vscode/settings.json index f5306a5..e76f3d0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,6 +5,7 @@ "Inequal", "Lngs", "Sider", + "Transpiler", "antd", "anticon", "arraybuffer", @@ -12,8 +13,11 @@ "clientside", "languagedetector", "localisation", + "multiline", "pnpm", "privatekeys", + "singleline", + "submenu", "tsdoc" ] } diff --git a/craco.config.js b/craco.config.js index 55b5702..b3383f1 100644 --- a/craco.config.js +++ b/craco.config.js @@ -1,6 +1,14 @@ const CracoLessPlugin = require("craco-less"); module.exports = { + style: { + css: { + loaderOptions: { + url: false + } + } + }, + plugins: [ { plugin: CracoLessPlugin, diff --git a/package.json b/package.json index a31e54f..ab3f270 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,8 @@ "@ant-design/icons": "^4.5.0", "antd": "^4.12.3", "base64-arraybuffer": "^0.2.0", + "csv-stringify": "^5.6.1", + "file-saver": "^2.0.5", "i18next": "^19.7.0", "i18next-browser-languagedetector": "^6.0.1", "i18next-http-backend": "^1.0.20", @@ -54,6 +56,7 @@ }, "devDependencies": { "@craco/craco": "^6.1.1", + "@types/file-saver": "^2.0.1", "@types/node": "^12.19.16", "@types/react": "^17.0.1", "@types/react-dom": "^17.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b840281..59d9ed9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2,6 +2,8 @@ '@ant-design/icons': 4.5.0_react-dom@17.0.1+react@17.0.1 antd: 4.12.3_react-dom@17.0.1+react@17.0.1 base64-arraybuffer: 0.2.0 + csv-stringify: 5.6.1 + file-saver: 2.0.5 i18next: 19.8.7 i18next-browser-languagedetector: 6.0.1 i18next-http-backend: 1.1.0 @@ -18,6 +20,7 @@ web-vitals: 1.1.0 devDependencies: '@craco/craco': 6.1.1_react-scripts@4.0.2 + '@types/file-saver': 2.0.1 '@types/node': 12.20.0 '@types/react': 17.0.2 '@types/react-dom': 17.0.1 @@ -1932,6 +1935,10 @@ dev: true resolution: integrity: sha512-laIjwTQaD+5DukBZaygQ79K1Z0jb1bPEMRrkXSLjtCcZm+abyp5YbrqpSLzD42FwWW6gK/aS4NYpJ804nG2brg== + /@types/file-saver/2.0.1: + dev: true + resolution: + integrity: sha512-g1QUuhYVVAamfCifK7oB7G3aIl4BbOyzDOqVyUfEr4tfBKrXfeH+M+Tg7HKCXSrbzxYdhyCP7z9WbKo0R2hBCw== /@types/glob/7.1.3: dependencies: '@types/minimatch': 3.0.3 @@ -4246,6 +4253,10 @@ dev: true resolution: integrity: sha512-+ZAmfyWMT7TiIlzdqJgjMb7S4f1beorDbWbsocyK4RaiqA5RTX3K14bnBWmmA9QEM0gRdsjyyrEmcyga8Zsxmw== + /csv-stringify/5.6.1: + dev: false + resolution: + integrity: sha512-JlQlNZMiuRGSFbLXFNGoBtsORXlkqf4Dfq8Ee0Jo4RVJj3YAUzevagUx24mDrQJLDF7aYz6Ne8kqA8WWBaYt2A== /cyclist/1.0.1: dev: true resolution: @@ -5447,6 +5458,10 @@ webpack: ^4.0.0 || ^5.0.0 resolution: integrity: sha512-Klt8C4BjWSXYQAfhpYYkG4qHNTna4toMHEbWrI5IuVoxbU6uiDKeKAP99R8mmbJi3lvewn/jQBOgU4+NS3tDQw== + /file-saver/2.0.5: + dev: false + resolution: + integrity: sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA== /file-uri-to-path/1.0.0: dev: true optional: true @@ -13197,6 +13212,7 @@ specifiers: '@ant-design/icons': ^4.5.0 '@craco/craco': ^6.1.1 + '@types/file-saver': ^2.0.1 '@types/node': ^12.19.16 '@types/react': ^17.0.1 '@types/react-dom': ^17.0.0 @@ -13209,10 +13225,12 @@ antd: ^4.12.3 base64-arraybuffer: ^0.2.0 craco-less: ^1.17.1 + csv-stringify: ^5.6.1 eslint: ^7.20.0 eslint-config-prettier: ^7.2.0 eslint-plugin-react: ^7.22.0 eslint-plugin-tsdoc: ^0.2.11 + file-saver: ^2.0.5 i18next: ^19.7.0 i18next-browser-languagedetector: ^6.0.1 i18next-http-backend: ^1.0.20 diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 2b1c6fe..1f0a5db 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -13,7 +13,9 @@ "search": "Search the Krist network", "send": "Send", - "request": "Request" + "request": "Request", + + "settings": "Settings" }, "sidebar": { @@ -43,6 +45,7 @@ "pageWithTotal": "Page {{page}} of {{total}}" }, + "error": "Error", "loading": "Loading...", "typeahead": { @@ -123,5 +126,40 @@ "translatorsDescription": "This project was translated by the following amazing contributors:", "translateButton": "Translate KristWeb", "tmpim": "Created by tmpim" + }, + + "settings": { + "siteTitle": "Settings", + "title": "Settings", + + "subMenuDebug": "Debug settings", + "menuTranslations": "Translations", + + "subTitleTranslations": "Translations", + + "translations": { + "errorMissingLanguages": "The languages.json file seems to be missing. Was KristWeb compiled correctly?", + "errorNoKeys": "No translation keys", + + "columnLanguageCode": "Code", + "columnLanguage": "Language", + "columnKeys": "Keys", + "columnMissingKeys": "Missing keys", + "columnProgress": "Progress", + + "tableUntranslatedKeys": "Untranslated keys", + "columnKey": "Key", + "columnEnglishString": "English string", + + "exportCSV": "Export CSV" + } + }, + + "breadcrumb": { + "dashboard": "Dashboard", + + "settings": "Settings", + "settingsDebug": "Debug", + "settingsTranslations": "Translations" } } diff --git a/src/App.less b/src/App.less index ae465d3..708b05a 100644 --- a/src/App.less +++ b/src/App.less @@ -1,2 +1,3 @@ @import "~antd/dist/antd.dark.less"; @import "./style/theme.less"; +@import "./style/components.less"; diff --git a/src/App.tsx b/src/App.tsx index 1a5a187..5a57049 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -22,14 +22,14 @@ ); export type AppDispatch = typeof store.dispatch; -function App() { +function App(): JSX.Element { return {/* TODO */} - + ; } export default App; diff --git a/src/components/Flag.css b/src/components/Flag.css new file mode 100644 index 0000000..be629f0 --- /dev/null +++ b/src/components/Flag.css @@ -0,0 +1 @@ +span.flag{width:44px;height:30px;display:inline-block}img.flag{width:30px}.flag{background:url(/img/flags_responsive.png) no-repeat;background-size:100%}.flag-ad{background-position:0 .413223%}.flag-ae{background-position:0 .826446%}.flag-af{background-position:0 1.239669%}.flag-ag{background-position:0 1.652893%}.flag-ai{background-position:0 2.066116%}.flag-al{background-position:0 2.479339%}.flag-am{background-position:0 2.892562%}.flag-an{background-position:0 3.305785%}.flag-ao{background-position:0 3.719008%}.flag-aq{background-position:0 4.132231%}.flag-ar{background-position:0 4.545455%}.flag-as{background-position:0 4.958678%}.flag-at{background-position:0 5.371901%}.flag-au{background-position:0 5.785124%}.flag-aw{background-position:0 6.198347%}.flag-az{background-position:0 6.61157%}.flag-ba{background-position:0 7.024793%}.flag-bb{background-position:0 7.438017%}.flag-bd{background-position:0 7.85124%}.flag-be{background-position:0 8.264463%}.flag-bf{background-position:0 8.677686%}.flag-bg{background-position:0 9.090909%}.flag-bh{background-position:0 9.504132%}.flag-bi{background-position:0 9.917355%}.flag-bj{background-position:0 10.330579%}.flag-bm{background-position:0 10.743802%}.flag-bn{background-position:0 11.157025%}.flag-bo{background-position:0 11.570248%}.flag-br{background-position:0 11.983471%}.flag-bs{background-position:0 12.396694%}.flag-bt{background-position:0 12.809917%}.flag-bv{background-position:0 13.22314%}.flag-bw{background-position:0 13.636364%}.flag-by{background-position:0 14.049587%}.flag-bz{background-position:0 14.46281%}.flag-ca{background-position:0 14.876033%}.flag-cc{background-position:0 15.289256%}.flag-cd{background-position:0 15.702479%}.flag-cf{background-position:0 16.115702%}.flag-cg{background-position:0 16.528926%}.flag-ch{background-position:0 16.942149%}.flag-ci{background-position:0 17.355372%}.flag-ck{background-position:0 17.768595%}.flag-cl{background-position:0 18.181818%}.flag-cm{background-position:0 18.595041%}.flag-cn{background-position:0 19.008264%}.flag-co{background-position:0 19.421488%}.flag-cr{background-position:0 19.834711%}.flag-cu{background-position:0 20.247934%}.flag-cv{background-position:0 20.661157%}.flag-cx{background-position:0 21.07438%}.flag-cy{background-position:0 21.487603%}.flag-cz{background-position:0 21.900826%}.flag-de{background-position:0 22.31405%}.flag-dj{background-position:0 22.727273%}.flag-dk{background-position:0 23.140496%}.flag-dm{background-position:0 23.553719%}.flag-do{background-position:0 23.966942%}.flag-dz{background-position:0 24.380165%}.flag-ec{background-position:0 24.793388%}.flag-ee{background-position:0 25.206612%}.flag-eg{background-position:0 25.619835%}.flag-eh{background-position:0 26.033058%}.flag-er{background-position:0 26.446281%}.flag-es{background-position:0 26.859504%}.flag-et{background-position:0 27.272727%}.flag-fi{background-position:0 27.68595%}.flag-fj{background-position:0 28.099174%}.flag-fk{background-position:0 28.512397%}.flag-fm{background-position:0 28.92562%}.flag-fo{background-position:0 29.338843%}.flag-fr{background-position:0 29.752066%}.flag-ga{background-position:0 30.165289%}.flag-gd{background-position:0 30.578512%}.flag-ge{background-position:0 30.991736%}.flag-gf{background-position:0 31.404959%}.flag-gh{background-position:0 31.818182%}.flag-gi{background-position:0 32.231405%}.flag-gl{background-position:0 32.644628%}.flag-gm{background-position:0 33.057851%}.flag-gn{background-position:0 33.471074%}.flag-gp{background-position:0 33.884298%}.flag-gq{background-position:0 34.297521%}.flag-gr{background-position:0 34.710744%}.flag-gs{background-position:0 35.123967%}.flag-gt{background-position:0 35.53719%}.flag-gu{background-position:0 35.950413%}.flag-gw{background-position:0 36.363636%}.flag-gy{background-position:0 36.77686%}.flag-hk{background-position:0 37.190083%}.flag-hm{background-position:0 37.603306%}.flag-hn{background-position:0 38.016529%}.flag-hr{background-position:0 38.429752%}.flag-ht{background-position:0 38.842975%}.flag-hu{background-position:0 39.256198%}.flag-id{background-position:0 39.669421%}.flag-ie{background-position:0 40.082645%}.flag-il{background-position:0 40.495868%}.flag-in{background-position:0 40.909091%}.flag-io{background-position:0 41.322314%}.flag-iq{background-position:0 41.735537%}.flag-ir{background-position:0 42.14876%}.flag-is{background-position:0 42.561983%}.flag-it{background-position:0 42.975207%}.flag-jm{background-position:0 43.38843%}.flag-jo{background-position:0 43.801653%}.flag-jp{background-position:0 44.214876%}.flag-ke{background-position:0 44.628099%}.flag-kg{background-position:0 45.041322%}.flag-kh{background-position:0 45.454545%}.flag-ki{background-position:0 45.867769%}.flag-km{background-position:0 46.280992%}.flag-kn{background-position:0 46.694215%}.flag-kp{background-position:0 47.107438%}.flag-kr{background-position:0 47.520661%}.flag-kw{background-position:0 47.933884%}.flag-ky{background-position:0 48.347107%}.flag-kz{background-position:0 48.760331%}.flag-la{background-position:0 49.173554%}.flag-lb{background-position:0 49.586777%}.flag-lc{background-position:0 50%}.flag-li{background-position:0 50.413223%}.flag-lk{background-position:0 50.826446%}.flag-lr{background-position:0 51.239669%}.flag-ls{background-position:0 51.652893%}.flag-lt{background-position:0 52.066116%}.flag-lu{background-position:0 52.479339%}.flag-lv{background-position:0 52.892562%}.flag-ly{background-position:0 53.305785%}.flag-ma{background-position:0 53.719008%}.flag-mc{background-position:0 54.132231%}.flag-md{background-position:0 54.545455%}.flag-me{background-position:0 54.958678%}.flag-mg{background-position:0 55.371901%}.flag-mh{background-position:0 55.785124%}.flag-mk{background-position:0 56.198347%}.flag-ml{background-position:0 56.61157%}.flag-mm{background-position:0 57.024793%}.flag-mn{background-position:0 57.438017%}.flag-mo{background-position:0 57.85124%}.flag-mp{background-position:0 58.264463%}.flag-mq{background-position:0 58.677686%}.flag-mr{background-position:0 59.090909%}.flag-ms{background-position:0 59.504132%}.flag-mt{background-position:0 59.917355%}.flag-mu{background-position:0 60.330579%}.flag-mv{background-position:0 60.743802%}.flag-mw{background-position:0 61.157025%}.flag-mx{background-position:0 61.570248%}.flag-my{background-position:0 61.983471%}.flag-mz{background-position:0 62.396694%}.flag-na{background-position:0 62.809917%}.flag-nc{background-position:0 63.22314%}.flag-ne{background-position:0 63.636364%}.flag-nf{background-position:0 64.049587%}.flag-ng{background-position:0 64.46281%}.flag-ni{background-position:0 64.876033%}.flag-nl{background-position:0 65.289256%}.flag-no{background-position:0 65.702479%}.flag-np{background-position:0 66.115702%}.flag-nr{background-position:0 66.528926%}.flag-nu{background-position:0 66.942149%}.flag-nz{background-position:0 67.355372%}.flag-om{background-position:0 67.768595%}.flag-pa{background-position:0 68.181818%}.flag-pe{background-position:0 68.595041%}.flag-pf{background-position:0 69.008264%}.flag-pg{background-position:0 69.421488%}.flag-ph{background-position:0 69.834711%}.flag-pk{background-position:0 70.247934%}.flag-pl{background-position:0 70.661157%}.flag-pm{background-position:0 71.07438%}.flag-pn{background-position:0 71.487603%}.flag-pr{background-position:0 71.900826%}.flag-pt{background-position:0 72.31405%}.flag-pw{background-position:0 72.727273%}.flag-py{background-position:0 73.140496%}.flag-qa{background-position:0 73.553719%}.flag-re{background-position:0 73.966942%}.flag-ro{background-position:0 74.380165%}.flag-rs{background-position:0 74.793388%}.flag-ru{background-position:0 75.206612%}.flag-rw{background-position:0 75.619835%}.flag-sa{background-position:0 76.033058%}.flag-sb{background-position:0 76.446281%}.flag-sc{background-position:0 76.859504%}.flag-sd{background-position:0 77.272727%}.flag-se{background-position:0 77.68595%}.flag-sg{background-position:0 78.099174%}.flag-sh{background-position:0 78.512397%}.flag-si{background-position:0 78.92562%}.flag-sj{background-position:0 79.338843%}.flag-sk{background-position:0 79.752066%}.flag-sl{background-position:0 80.165289%}.flag-sm{background-position:0 80.578512%}.flag-sn{background-position:0 80.991736%}.flag-so{background-position:0 81.404959%}.flag-sr{background-position:0 81.818182%}.flag-ss{background-position:0 82.231405%}.flag-st{background-position:0 82.644628%}.flag-sv{background-position:0 83.057851%}.flag-sy{background-position:0 83.471074%}.flag-sz{background-position:0 83.884298%}.flag-tc{background-position:0 84.297521%}.flag-td{background-position:0 84.710744%}.flag-tf{background-position:0 85.123967%}.flag-tg{background-position:0 85.53719%}.flag-th{background-position:0 85.950413%}.flag-tj{background-position:0 86.363636%}.flag-tk{background-position:0 86.77686%}.flag-tl{background-position:0 87.190083%}.flag-tm{background-position:0 87.603306%}.flag-tn{background-position:0 88.016529%}.flag-to{background-position:0 88.429752%}.flag-tp{background-position:0 88.842975%}.flag-tr{background-position:0 89.256198%}.flag-tt{background-position:0 89.669421%}.flag-tv{background-position:0 90.082645%}.flag-tw{background-position:0 90.495868%}.flag-ty{background-position:0 90.909091%}.flag-tz{background-position:0 91.322314%}.flag-ua{background-position:0 91.735537%}.flag-ug{background-position:0 92.14876%}.flag-gb,.flag-uk{background-position:0 92.561983%}.flag-um{background-position:0 92.975207%}.flag-us{background-position:0 93.38843%}.flag-uy{background-position:0 93.801653%}.flag-uz{background-position:0 94.214876%}.flag-va{background-position:0 94.628099%}.flag-vc{background-position:0 95.041322%}.flag-ve{background-position:0 95.454545%}.flag-vg{background-position:0 95.867769%}.flag-vi{background-position:0 96.280992%}.flag-vn{background-position:0 96.694215%}.flag-vu{background-position:0 97.107438%}.flag-wf{background-position:0 97.520661%}.flag-ws{background-position:0 97.933884%}.flag-ye{background-position:0 98.347107%}.flag-za{background-position:0 98.760331%}.flag-zm{background-position:0 99.173554%}.flag-zr{background-position:0 99.586777%}.flag-zw{background-position:0 100%} diff --git a/src/components/Flag.less b/src/components/Flag.less deleted file mode 100644 index be629f0..0000000 --- a/src/components/Flag.less +++ /dev/null @@ -1 +0,0 @@ -span.flag{width:44px;height:30px;display:inline-block}img.flag{width:30px}.flag{background:url(/img/flags_responsive.png) no-repeat;background-size:100%}.flag-ad{background-position:0 .413223%}.flag-ae{background-position:0 .826446%}.flag-af{background-position:0 1.239669%}.flag-ag{background-position:0 1.652893%}.flag-ai{background-position:0 2.066116%}.flag-al{background-position:0 2.479339%}.flag-am{background-position:0 2.892562%}.flag-an{background-position:0 3.305785%}.flag-ao{background-position:0 3.719008%}.flag-aq{background-position:0 4.132231%}.flag-ar{background-position:0 4.545455%}.flag-as{background-position:0 4.958678%}.flag-at{background-position:0 5.371901%}.flag-au{background-position:0 5.785124%}.flag-aw{background-position:0 6.198347%}.flag-az{background-position:0 6.61157%}.flag-ba{background-position:0 7.024793%}.flag-bb{background-position:0 7.438017%}.flag-bd{background-position:0 7.85124%}.flag-be{background-position:0 8.264463%}.flag-bf{background-position:0 8.677686%}.flag-bg{background-position:0 9.090909%}.flag-bh{background-position:0 9.504132%}.flag-bi{background-position:0 9.917355%}.flag-bj{background-position:0 10.330579%}.flag-bm{background-position:0 10.743802%}.flag-bn{background-position:0 11.157025%}.flag-bo{background-position:0 11.570248%}.flag-br{background-position:0 11.983471%}.flag-bs{background-position:0 12.396694%}.flag-bt{background-position:0 12.809917%}.flag-bv{background-position:0 13.22314%}.flag-bw{background-position:0 13.636364%}.flag-by{background-position:0 14.049587%}.flag-bz{background-position:0 14.46281%}.flag-ca{background-position:0 14.876033%}.flag-cc{background-position:0 15.289256%}.flag-cd{background-position:0 15.702479%}.flag-cf{background-position:0 16.115702%}.flag-cg{background-position:0 16.528926%}.flag-ch{background-position:0 16.942149%}.flag-ci{background-position:0 17.355372%}.flag-ck{background-position:0 17.768595%}.flag-cl{background-position:0 18.181818%}.flag-cm{background-position:0 18.595041%}.flag-cn{background-position:0 19.008264%}.flag-co{background-position:0 19.421488%}.flag-cr{background-position:0 19.834711%}.flag-cu{background-position:0 20.247934%}.flag-cv{background-position:0 20.661157%}.flag-cx{background-position:0 21.07438%}.flag-cy{background-position:0 21.487603%}.flag-cz{background-position:0 21.900826%}.flag-de{background-position:0 22.31405%}.flag-dj{background-position:0 22.727273%}.flag-dk{background-position:0 23.140496%}.flag-dm{background-position:0 23.553719%}.flag-do{background-position:0 23.966942%}.flag-dz{background-position:0 24.380165%}.flag-ec{background-position:0 24.793388%}.flag-ee{background-position:0 25.206612%}.flag-eg{background-position:0 25.619835%}.flag-eh{background-position:0 26.033058%}.flag-er{background-position:0 26.446281%}.flag-es{background-position:0 26.859504%}.flag-et{background-position:0 27.272727%}.flag-fi{background-position:0 27.68595%}.flag-fj{background-position:0 28.099174%}.flag-fk{background-position:0 28.512397%}.flag-fm{background-position:0 28.92562%}.flag-fo{background-position:0 29.338843%}.flag-fr{background-position:0 29.752066%}.flag-ga{background-position:0 30.165289%}.flag-gd{background-position:0 30.578512%}.flag-ge{background-position:0 30.991736%}.flag-gf{background-position:0 31.404959%}.flag-gh{background-position:0 31.818182%}.flag-gi{background-position:0 32.231405%}.flag-gl{background-position:0 32.644628%}.flag-gm{background-position:0 33.057851%}.flag-gn{background-position:0 33.471074%}.flag-gp{background-position:0 33.884298%}.flag-gq{background-position:0 34.297521%}.flag-gr{background-position:0 34.710744%}.flag-gs{background-position:0 35.123967%}.flag-gt{background-position:0 35.53719%}.flag-gu{background-position:0 35.950413%}.flag-gw{background-position:0 36.363636%}.flag-gy{background-position:0 36.77686%}.flag-hk{background-position:0 37.190083%}.flag-hm{background-position:0 37.603306%}.flag-hn{background-position:0 38.016529%}.flag-hr{background-position:0 38.429752%}.flag-ht{background-position:0 38.842975%}.flag-hu{background-position:0 39.256198%}.flag-id{background-position:0 39.669421%}.flag-ie{background-position:0 40.082645%}.flag-il{background-position:0 40.495868%}.flag-in{background-position:0 40.909091%}.flag-io{background-position:0 41.322314%}.flag-iq{background-position:0 41.735537%}.flag-ir{background-position:0 42.14876%}.flag-is{background-position:0 42.561983%}.flag-it{background-position:0 42.975207%}.flag-jm{background-position:0 43.38843%}.flag-jo{background-position:0 43.801653%}.flag-jp{background-position:0 44.214876%}.flag-ke{background-position:0 44.628099%}.flag-kg{background-position:0 45.041322%}.flag-kh{background-position:0 45.454545%}.flag-ki{background-position:0 45.867769%}.flag-km{background-position:0 46.280992%}.flag-kn{background-position:0 46.694215%}.flag-kp{background-position:0 47.107438%}.flag-kr{background-position:0 47.520661%}.flag-kw{background-position:0 47.933884%}.flag-ky{background-position:0 48.347107%}.flag-kz{background-position:0 48.760331%}.flag-la{background-position:0 49.173554%}.flag-lb{background-position:0 49.586777%}.flag-lc{background-position:0 50%}.flag-li{background-position:0 50.413223%}.flag-lk{background-position:0 50.826446%}.flag-lr{background-position:0 51.239669%}.flag-ls{background-position:0 51.652893%}.flag-lt{background-position:0 52.066116%}.flag-lu{background-position:0 52.479339%}.flag-lv{background-position:0 52.892562%}.flag-ly{background-position:0 53.305785%}.flag-ma{background-position:0 53.719008%}.flag-mc{background-position:0 54.132231%}.flag-md{background-position:0 54.545455%}.flag-me{background-position:0 54.958678%}.flag-mg{background-position:0 55.371901%}.flag-mh{background-position:0 55.785124%}.flag-mk{background-position:0 56.198347%}.flag-ml{background-position:0 56.61157%}.flag-mm{background-position:0 57.024793%}.flag-mn{background-position:0 57.438017%}.flag-mo{background-position:0 57.85124%}.flag-mp{background-position:0 58.264463%}.flag-mq{background-position:0 58.677686%}.flag-mr{background-position:0 59.090909%}.flag-ms{background-position:0 59.504132%}.flag-mt{background-position:0 59.917355%}.flag-mu{background-position:0 60.330579%}.flag-mv{background-position:0 60.743802%}.flag-mw{background-position:0 61.157025%}.flag-mx{background-position:0 61.570248%}.flag-my{background-position:0 61.983471%}.flag-mz{background-position:0 62.396694%}.flag-na{background-position:0 62.809917%}.flag-nc{background-position:0 63.22314%}.flag-ne{background-position:0 63.636364%}.flag-nf{background-position:0 64.049587%}.flag-ng{background-position:0 64.46281%}.flag-ni{background-position:0 64.876033%}.flag-nl{background-position:0 65.289256%}.flag-no{background-position:0 65.702479%}.flag-np{background-position:0 66.115702%}.flag-nr{background-position:0 66.528926%}.flag-nu{background-position:0 66.942149%}.flag-nz{background-position:0 67.355372%}.flag-om{background-position:0 67.768595%}.flag-pa{background-position:0 68.181818%}.flag-pe{background-position:0 68.595041%}.flag-pf{background-position:0 69.008264%}.flag-pg{background-position:0 69.421488%}.flag-ph{background-position:0 69.834711%}.flag-pk{background-position:0 70.247934%}.flag-pl{background-position:0 70.661157%}.flag-pm{background-position:0 71.07438%}.flag-pn{background-position:0 71.487603%}.flag-pr{background-position:0 71.900826%}.flag-pt{background-position:0 72.31405%}.flag-pw{background-position:0 72.727273%}.flag-py{background-position:0 73.140496%}.flag-qa{background-position:0 73.553719%}.flag-re{background-position:0 73.966942%}.flag-ro{background-position:0 74.380165%}.flag-rs{background-position:0 74.793388%}.flag-ru{background-position:0 75.206612%}.flag-rw{background-position:0 75.619835%}.flag-sa{background-position:0 76.033058%}.flag-sb{background-position:0 76.446281%}.flag-sc{background-position:0 76.859504%}.flag-sd{background-position:0 77.272727%}.flag-se{background-position:0 77.68595%}.flag-sg{background-position:0 78.099174%}.flag-sh{background-position:0 78.512397%}.flag-si{background-position:0 78.92562%}.flag-sj{background-position:0 79.338843%}.flag-sk{background-position:0 79.752066%}.flag-sl{background-position:0 80.165289%}.flag-sm{background-position:0 80.578512%}.flag-sn{background-position:0 80.991736%}.flag-so{background-position:0 81.404959%}.flag-sr{background-position:0 81.818182%}.flag-ss{background-position:0 82.231405%}.flag-st{background-position:0 82.644628%}.flag-sv{background-position:0 83.057851%}.flag-sy{background-position:0 83.471074%}.flag-sz{background-position:0 83.884298%}.flag-tc{background-position:0 84.297521%}.flag-td{background-position:0 84.710744%}.flag-tf{background-position:0 85.123967%}.flag-tg{background-position:0 85.53719%}.flag-th{background-position:0 85.950413%}.flag-tj{background-position:0 86.363636%}.flag-tk{background-position:0 86.77686%}.flag-tl{background-position:0 87.190083%}.flag-tm{background-position:0 87.603306%}.flag-tn{background-position:0 88.016529%}.flag-to{background-position:0 88.429752%}.flag-tp{background-position:0 88.842975%}.flag-tr{background-position:0 89.256198%}.flag-tt{background-position:0 89.669421%}.flag-tv{background-position:0 90.082645%}.flag-tw{background-position:0 90.495868%}.flag-ty{background-position:0 90.909091%}.flag-tz{background-position:0 91.322314%}.flag-ua{background-position:0 91.735537%}.flag-ug{background-position:0 92.14876%}.flag-gb,.flag-uk{background-position:0 92.561983%}.flag-um{background-position:0 92.975207%}.flag-us{background-position:0 93.38843%}.flag-uy{background-position:0 93.801653%}.flag-uz{background-position:0 94.214876%}.flag-va{background-position:0 94.628099%}.flag-vc{background-position:0 95.041322%}.flag-ve{background-position:0 95.454545%}.flag-vg{background-position:0 95.867769%}.flag-vi{background-position:0 96.280992%}.flag-vn{background-position:0 96.694215%}.flag-vu{background-position:0 97.107438%}.flag-wf{background-position:0 97.520661%}.flag-ws{background-position:0 97.933884%}.flag-ye{background-position:0 98.347107%}.flag-za{background-position:0 98.760331%}.flag-zm{background-position:0 99.173554%}.flag-zr{background-position:0 99.586777%}.flag-zw{background-position:0 100%} diff --git a/src/components/Flag.tsx b/src/components/Flag.tsx index 12ba617..8b233f2 100644 --- a/src/components/Flag.tsx +++ b/src/components/Flag.tsx @@ -1,16 +1,16 @@ -import { HTMLProps } from "react"; +import React, { HTMLProps } from "react"; -import "./Flag.less"; +import "./Flag.css"; interface Props extends HTMLProps { name?: string; code?: string; } -export function Flag({ name, code, ...rest }: Props) { +export function Flag({ name, code, ...rest }: Props): JSX.Element { return ; } diff --git a/src/components/KristSymbol.tsx b/src/components/KristSymbol.tsx index a999c4d..b7acfab 100644 --- a/src/components/KristSymbol.tsx +++ b/src/components/KristSymbol.tsx @@ -1,9 +1,10 @@ +import React from "react"; import Icon from "@ant-design/icons"; -export const KristSymbolSvg = () => ( +export const KristSymbolSvg = (): JSX.Element => ( ); - -export const KristSymbol = (props: any) => ; +export const KristSymbol = (props: any): JSX.Element => + ; diff --git a/src/components/KristValue.tsx b/src/components/KristValue.tsx index ae2f4ee..e3e89c7 100644 --- a/src/components/KristValue.tsx +++ b/src/components/KristValue.tsx @@ -11,7 +11,7 @@ type Props = React.HTMLProps & OwnProps; -export const KristValue = ({ value, long, ...props }: Props) => ( +export const KristValue = ({ value, long, ...props }: Props): JSX.Element => ( {(value || 0).toLocaleString()} diff --git a/src/components/auth/AuthMasterPasswordPopover.tsx b/src/components/auth/AuthMasterPasswordPopover.tsx index cf12ad6..e0e8e5a 100644 --- a/src/components/auth/AuthMasterPasswordPopover.tsx +++ b/src/components/auth/AuthMasterPasswordPopover.tsx @@ -1,4 +1,4 @@ -import { useState, useRef, FunctionComponent } from "react"; +import React, { useState, useRef, FunctionComponent } from "react"; import { Popover, Button, Input, Form } from "antd"; import { useTranslation } from "react-i18next"; @@ -80,5 +80,5 @@ } > {children} - -} + ; +}; diff --git a/src/components/auth/AuthorisedAction.tsx b/src/components/auth/AuthorisedAction.tsx index 71c9c59..3ba3b67 100644 --- a/src/components/auth/AuthorisedAction.tsx +++ b/src/components/auth/AuthorisedAction.tsx @@ -1,4 +1,4 @@ -import { FunctionComponent, useState } from "react"; +import React, { FunctionComponent, useState } from "react"; import { useSelector, shallowEqual } from "react-redux"; import { RootState } from "../../store"; @@ -16,22 +16,26 @@ const { isAuthed, hasMasterPassword } = useSelector((s: RootState) => s.walletManager, shallowEqual); + // Don't render the modal and popover unless we absolutely have to + const [clicked, setClicked] = useState(false); const [modalVisible, setModalVisible] = useState(false); if (isAuthed) { // The user is authed with their master password, just perform the action // directly: - return
{children}
+ return
{children}
; } else if (!hasMasterPassword) { // The user does not yet have a master password, prompt them to create one: return <> -
setModalVisible(true)}>{children}
- { if (!clicked) { setClicked(true); } setModalVisible(true); }}> + {children} + + {clicked && setModalVisible(false)} onSubmit={() => { setModalVisible(false); if (onAuthed) onAuthed(); }} - /> - + />} + ; } else { // The user has a master password set but is not logged in, prompt them to // enter it: @@ -39,6 +43,6 @@ onSubmit={() => { if (onAuthed) onAuthed(); }} > {children} - + ; } }; diff --git a/src/components/auth/MasterPasswordInput.tsx b/src/components/auth/MasterPasswordInput.tsx index d1e02b9..598f656 100644 --- a/src/components/auth/MasterPasswordInput.tsx +++ b/src/components/auth/MasterPasswordInput.tsx @@ -9,16 +9,16 @@ } /// Fake username field for master password inputs, to trick autofill. -export function FakeUsernameInput() { +export function FakeUsernameInput(): JSX.Element { return + />; } -export function getMasterPasswordInput({ inputRef, placeholder, tabIndex, autoFocus }: Props) { +export function getMasterPasswordInput({ inputRef, placeholder, tabIndex, autoFocus }: Props): JSX.Element { return + />; } diff --git a/src/components/auth/SetMasterPasswordModal.tsx b/src/components/auth/SetMasterPasswordModal.tsx index a433a76..114ac94 100644 --- a/src/components/auth/SetMasterPasswordModal.tsx +++ b/src/components/auth/SetMasterPasswordModal.tsx @@ -1,4 +1,4 @@ -import { useRef } from "react"; +import React, { useRef } from "react"; import { Modal, Form, Input, Button } from "antd"; import { useTranslation, Trans } from "react-i18next"; import { useDispatch } from "react-redux"; @@ -13,7 +13,7 @@ onSubmit: () => void; } -export function SetMasterPasswordModal({ visible, onCancel, onSubmit }: Props) { +export function SetMasterPasswordModal({ visible, onCancel, onSubmit }: Props): JSX.Element { const dispatch = useDispatch(); const { t } = useTranslation(); @@ -41,8 +41,8 @@

Enter a master password to encrypt your wallet privatekeys. They - will be saved in your browser's local storage, and you will be asked for - the master password to decrypt them once per session. + will be saved in your browser's local storage, and you will be + asked for the master password to decrypt them once per session.

@@ -63,7 +63,7 @@ ]} style={{ marginBottom: 8 }} > - {getMasterPasswordInput({ inputRef, placeholder: t("masterPassword.passwordPlaceholder"), autoFocus: true })} + {getMasterPasswordInput({ inputRef, placeholder: t("masterPassword.passwordPlaceholder"), autoFocus: true })} {/* Password confirm input */} diff --git a/src/index.tsx b/src/index.tsx index fb94a90..a68daeb 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -6,7 +6,7 @@ ReactDOM.render( // - , + , // , document.getElementById("root") ); diff --git a/src/layout/AppLayout.less b/src/layout/AppLayout.less index f8dcd0f..23563b2 100644 --- a/src/layout/AppLayout.less +++ b/src/layout/AppLayout.less @@ -130,6 +130,7 @@ .ant-input-search-button { border: none; + background: transparent; } } @@ -142,6 +143,10 @@ .site-header-settings { border-left: 1px solid @kw-border-color-darker; + + .anticon { + margin-right: 0; + } } } @@ -293,7 +298,3 @@ margin-left: 0; } } - -.page-layout { - padding: 2rem; -} diff --git a/src/layout/AppLayout.tsx b/src/layout/AppLayout.tsx index 27a508b..33a4bd5 100644 --- a/src/layout/AppLayout.tsx +++ b/src/layout/AppLayout.tsx @@ -3,19 +3,17 @@ import { SendOutlined, DownloadOutlined, MenuOutlined, SettingOutlined } from "@ant-design/icons"; import { useTranslation } from "react-i18next"; -import { Switch, Route } from "react-router-dom"; +import { Link } from "react-router-dom"; import { Brand } from "./Brand"; import { Sidebar } from "./sidebar/Sidebar"; - -import { DashboardPage } from "../pages/DashboardPage"; -import { CreditsPage } from "../pages/credits/CreditsPage"; +import { AppRouter } from "./AppRouter"; import "./AppLayout.less"; const { useBreakpoint } = Grid; -export function AppLayout() { +export function AppLayout(): JSX.Element { const { t } = useTranslation(); const [sidebarCollapsed, setSidebarCollapsed] = useState(true); const bps = useBreakpoint(); @@ -36,8 +34,8 @@ {/* Send and receive buttons */} {bps.md && - {t("nav.send")} - {t("nav.request")} + }>{t("nav.send")} + }>{t("nav.request")} } {/* Spacer to push search box to the right */} @@ -52,8 +50,8 @@ {/* Settings button */} - setSidebarCollapsed(!sidebarCollapsed)}> - + } title={t("nav.settings")}> + @@ -68,12 +66,8 @@ />} - - - - Not found - + - + ; } diff --git a/src/layout/AppRouter.tsx b/src/layout/AppRouter.tsx new file mode 100644 index 0000000..098b1e9 --- /dev/null +++ b/src/layout/AppRouter.tsx @@ -0,0 +1,35 @@ +import React from "react"; +import { Switch, Route } from "react-router-dom"; + +import { DashboardPage } from "../pages/DashboardPage"; + +import { SettingsPage } from "../pages/settings/SettingsPage"; +import { SettingsTranslations } from "../pages/settings/SettingsTranslations"; + +import { CreditsPage } from "../pages/credits/CreditsPage"; + +interface AppRoute { + path: string; + name: string; + component?: React.ReactNode; +} + +export const APP_ROUTES: AppRoute[] = [ + { path: "/", name: "dashboard", component: }, + + { path: "/settings", name: "settings", component: }, + { path: "/settings/debug", name: "settingsDebug" }, + { path: "/settings/debug/translations", name: "settings", component: }, + + { path: "/credits", name: "credits", component: }, +]; + +export function AppRouter(): JSX.Element { + return + {APP_ROUTES.map(({ path, component }, key) => ( + component && {component} + ))} + + Not found + ; +} diff --git a/src/layout/Brand.tsx b/src/layout/Brand.tsx index 552d3f1..52b4c0b 100644 --- a/src/layout/Brand.tsx +++ b/src/layout/Brand.tsx @@ -1,5 +1,6 @@ -import { Link } from "react-router-dom"; +import React from "react"; +import { Link } from "react-router-dom"; import { useTranslation } from "react-i18next"; import semverMajor from "semver/functions/major"; @@ -18,7 +19,7 @@ "rc": "green" }; -export function Brand() { +export function Brand(): JSX.Element { const { t } = useTranslation(); const version = packageJson.version; @@ -41,5 +42,5 @@ v{major}.{minor}.{patch} {tag} - + ; } diff --git a/src/layout/PageLayout.less b/src/layout/PageLayout.less new file mode 100644 index 0000000..24cb28d --- /dev/null +++ b/src/layout/PageLayout.less @@ -0,0 +1,11 @@ +@import (reference) "../App.less"; + +.page-layout { + .page-layout-header.ant-page-header { + padding-bottom: 0; + } + + .page-layout-contents { + padding: @padding-lg; + } +} diff --git a/src/layout/PageLayout.tsx b/src/layout/PageLayout.tsx index 06f5565..93721ee 100644 --- a/src/layout/PageLayout.tsx +++ b/src/layout/PageLayout.tsx @@ -1,21 +1,59 @@ import React, { FunctionComponent, useEffect } from "react"; -import { useTranslation } from "react-i18next"; +import { PageHeader } from "antd"; -type Props = React.HTMLProps & { +import { useTranslation } from "react-i18next"; +import { useHistory } from "react-router-dom"; + +import "./PageLayout.less"; + +export type PageLayoutProps = React.HTMLProps & { + siteTitle?: string; + siteTitleKey?: string; title?: string; titleKey?: string; + subTitle?: string; + subTitleKey?: string; + + extra?: React.ReactNode; + noHeader?: boolean; + className?: string; } -export const PageLayout: FunctionComponent = ({ title, titleKey, className, children, ...rest }) => { +export const PageLayout: FunctionComponent = ({ + siteTitle, siteTitleKey, + title, titleKey, + subTitle, subTitleKey, + + extra, noHeader, + + className, + + children, ...rest +}) => { const { t } = useTranslation(); + const history = useHistory(); useEffect(() => { - if (title) document.title = `${title} - KristWeb`; - else if (titleKey) document.title = `${t(titleKey)} - KristWeb`; + if (siteTitle) document.title = `${siteTitle} - KristWeb`; + else if (siteTitleKey) document.title = `${t(siteTitleKey)} - KristWeb`; }); - // TODO: breadcrumbs and stuff + return
+ {/* Page header */} + {!noHeader && (title || titleKey) && {children}
-} + title={title || (titleKey ? t(titleKey) : undefined)} + subTitle={subTitle || (subTitleKey ? t(subTitleKey) : undefined)} + extra={extra} + + onBack={() => history.goBack()} + />} + + {/* Page contents */} +
+ {children} +
+ ; +}; diff --git a/src/layout/sidebar/Sidebar.tsx b/src/layout/sidebar/Sidebar.tsx index acc6aa1..fdbcc69 100644 --- a/src/layout/sidebar/Sidebar.tsx +++ b/src/layout/sidebar/Sidebar.tsx @@ -41,7 +41,7 @@ )); } -export function Sidebar({ collapsed }: { collapsed: boolean }) { +export function Sidebar({ collapsed }: { collapsed: boolean }): JSX.Element { const { t } = useTranslation(); const location = useLocation(); @@ -57,7 +57,7 @@ width={240} className={"site-sidebar " + (collapsed ? "collapsed" : "")} > - + {getSidebarItems(t)} @@ -68,5 +68,5 @@ - + ; } diff --git a/src/layout/sidebar/SidebarFooter.tsx b/src/layout/sidebar/SidebarFooter.tsx index 7e985d7..28d5f3b 100644 --- a/src/layout/sidebar/SidebarFooter.tsx +++ b/src/layout/sidebar/SidebarFooter.tsx @@ -6,7 +6,7 @@ const req = require.context("../../", false, /\.\/host.json$/); -export function SidebarFooter() { +export function SidebarFooter(): JSX.Element { const { t } = useTranslation(); const authorName = packageJson.author || "Lemmmy"; diff --git a/src/layout/sidebar/SidebarTotalBalance.tsx b/src/layout/sidebar/SidebarTotalBalance.tsx index 856d053..79ef446 100644 --- a/src/layout/sidebar/SidebarTotalBalance.tsx +++ b/src/layout/sidebar/SidebarTotalBalance.tsx @@ -1,8 +1,9 @@ +import React from "react"; import { useTranslation } from "react-i18next"; import { KristValue } from "../../components/KristValue"; -export function SidebarTotalBalance({ balance }: { balance: number }) { +export function SidebarTotalBalance({ balance }: { balance: number }): JSX.Element { const { t } = useTranslation(); return ( diff --git a/src/pages/DashboardPage.tsx b/src/pages/DashboardPage.tsx index 1901807..06670b1 100644 --- a/src/pages/DashboardPage.tsx +++ b/src/pages/DashboardPage.tsx @@ -1,3 +1,4 @@ +import React from "react"; import { Button, message } from "antd"; import { useSelector, shallowEqual } from "react-redux"; @@ -6,11 +7,11 @@ import { PageLayout } from "../layout/PageLayout"; import { AuthorisedAction } from "../components/auth/AuthorisedAction"; -export function DashboardPage() { - const { isAuthed, salt, tester, hasMasterPassword } +export function DashboardPage(): JSX.Element { + const { isAuthed, hasMasterPassword } = useSelector((s: RootState) => s.walletManager, shallowEqual); - return {/* TODO */} + return {/* TODO */}

Is authed: {isAuthed ? "yes" : "no"}

Has master password: {hasMasterPassword ? "yes" : "no"}

@@ -25,5 +26,5 @@ message.success("Something authed happened!")}> -
+
; } diff --git a/src/pages/credits/CreditsPage.tsx b/src/pages/credits/CreditsPage.tsx index 7b9230a..5c162e6 100644 --- a/src/pages/credits/CreditsPage.tsx +++ b/src/pages/credits/CreditsPage.tsx @@ -1,3 +1,4 @@ +import React from "react"; import { Typography, Divider } from "antd"; import { useTranslation, Trans } from "react-i18next"; @@ -10,13 +11,13 @@ const { Title } = Typography; -export function CreditsPage() { +export function CreditsPage(): JSX.Element { const { t } = useTranslation(); const authorName = packageJson.author || "Lemmmy"; const authorURL = `https://github.com/${authorName}`; - return + return KristWeb v2

Made by {{authorName}} @@ -32,5 +33,5 @@ {t("credits.tmpim")} - + ; } diff --git a/src/pages/credits/Supporters.tsx b/src/pages/credits/Supporters.tsx index c8de89f..043596b 100644 --- a/src/pages/credits/Supporters.tsx +++ b/src/pages/credits/Supporters.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from "react"; +import React, { useState, useEffect } from "react"; import { Space, Spin, Button } from "antd"; import { DollarOutlined } from "@ant-design/icons"; import { useTranslation } from "react-i18next"; @@ -15,7 +15,7 @@ supporters?: Supporter[]; } -export function Supporters() { +export function Supporters(): JSX.Element | null { const { supportURL, supportersURL } = packageJson; const { t } = useTranslation(); @@ -61,5 +61,5 @@ - + ; } diff --git a/src/pages/credits/Translators.tsx b/src/pages/credits/Translators.tsx index 3efdf71..652496a 100644 --- a/src/pages/credits/Translators.tsx +++ b/src/pages/credits/Translators.tsx @@ -1,37 +1,23 @@ +import React from "react"; import { Space, Button, List, Typography } from "antd"; import { GlobalOutlined } from "@ant-design/icons"; import { useTranslation } from "react-i18next"; import { Flag } from "../../components/Flag"; +import { getLanguages } from "../../utils/i18n"; import packageJson from "../../../package.json"; const { Text } = Typography; -// Find languages.json -const req = require.context("../../../", false, /\.\/languages.json$/); - -interface Language { - name: string; - nativeName?: string; - country?: string; - contributors: Contributor[]; -}; - -interface Contributor { - name: string; - url?: string; -} - -export function Translators() { +export function Translators(): JSX.Element | null { const { t } = useTranslation(); const { translateURL } = packageJson; if (!translateURL) return null; - // Get the translator information from languages.json - if (!req.keys().includes("./languages.json")) return null; - const languages: { [key: string]: Language } = req("./languages.json"); + const languages = getLanguages(); + if (!languages) return null; return {/* Description */} @@ -41,7 +27,7 @@ code !== "en" && lang.contributors.length > 0)} - renderItem={([code, lang]) => + renderItem={([, lang]) => } @@ -61,5 +47,5 @@ - + ; } diff --git a/src/pages/settings/SettingsPage.tsx b/src/pages/settings/SettingsPage.tsx new file mode 100644 index 0000000..36f03ab --- /dev/null +++ b/src/pages/settings/SettingsPage.tsx @@ -0,0 +1,36 @@ +import React, { FunctionComponent } from "react"; +import { Menu } from "antd"; +import { BugOutlined } from "@ant-design/icons"; + +import { useTranslation } from "react-i18next"; +import { Link } from "react-router-dom"; + +import { PageLayout, PageLayoutProps } from "../../layout/PageLayout"; + +interface SettingsPageLayoutProps extends PageLayoutProps { + pageName?: string; +} +export const SettingsPageLayout: FunctionComponent = ({ pageName, children, ...rest }) => { + return + {children} + ; +}; + +export function SettingsPage(): JSX.Element { + const { t } = useTranslation(); + + return +

+ } title={t("settings.subMenuDebug")}> + + {t("settings.menuTranslations")} + + + + ; +} diff --git a/src/pages/settings/SettingsTranslations.tsx b/src/pages/settings/SettingsTranslations.tsx new file mode 100644 index 0000000..ff151ac --- /dev/null +++ b/src/pages/settings/SettingsTranslations.tsx @@ -0,0 +1,247 @@ +import React, { useState, useEffect } from "react"; +import { Table, Progress, Result, Typography, Tooltip, Button } from "antd"; +import { ExclamationCircleOutlined, FileExcelOutlined } from "@ant-design/icons"; + +import { useTranslation } from "react-i18next"; +import { getLanguages, Language } from "../../utils/i18n"; + +import csvStringify from "csv-stringify"; +import { saveAs } from "file-saver"; + +import { Flag } from "../../components/Flag"; +import { SettingsPageLayout } from "./SettingsPage"; + +const { Text } = Typography; + +interface LangKeys { [key: string]: string } +interface AnalysedLanguage { + code: string; + language: Language; + + error?: string; + keys?: LangKeys; + keyCount: number; + missingKeys?: { k: string; v: string }[]; +} + +const IGNORE_KEYS = /_(?:plural|interval|male|female|\d+)$/; +async function getLanguage([code, language]: [string, Language]): Promise { + const res = await fetch(`/locales/${code}/translation.json`); + if (!res.ok) throw new Error(res.statusText); + + const translation = await res.json(); + + const isObject = (val: any) => typeof val === "object" && !Array.isArray(val); + const addDelimiter = (a: string, b: string) => a ? `${a}.${b}` : b; + + // Find all translation keys recursively + const keys = (obj: any = {}, head = ""): LangKeys => + Object.entries(obj) + .reduce((product, [key, value]) => { + // Ignore plurals, etc. + if (IGNORE_KEYS.test(key)) return product; + + const fullPath = addDelimiter(head, key); + return isObject(value as any) + ? { ...product, ...keys(value, fullPath) } + : {...product, [fullPath]: value }; + }, {}); + + const langKeys = keys(translation); + + return { code, language, keys: langKeys, keyCount: Object.keys(langKeys).length }; +} + +interface CSVRow { + Code: string; + Language?: string; + Key: string; + Value?: string; +} +async function generateLanguageCSV(languages: AnalysedLanguage[]): Promise { + return new Promise((resolve, reject) => { + const en = languages.find(l => l.code === "en"); + if (!en) return reject("en missing"); + const enKeyNames = Object.keys(en.keys || {}); + + // Merge all the languages and their keys together into one array + const data = languages.reduce((out, lang) => { + const { code, language, keys } = lang; + const languageName = language.name; + if (!keys) return out; + + // Keys from both en and this language + const combinedKeys = [...new Set([...enKeyNames, ...Object.keys(keys)])]; + // Find the value for this key from the language, or null if not + const keysWithValues = combinedKeys.map(k => [k, keys[k]]); + + // Generate all the rows for this language + return [ + ...out, + ...keysWithValues.map(([k, v]) => ({ + "Code": code, + "Language": languageName, + "Key": k, "Value": v + })) + ]; + }, [] as CSVRow[]); + + csvStringify(data, { header: true, quoted: true }, (err, data) => { + if (err) return reject(err); + resolve(data); + }); + }); +} + +export function SettingsTranslations(): JSX.Element { + const { t } = useTranslation(); + + const [fetched, setFetched] = useState(false); + const [loading, setLoading] = useState(true); + const [analysed, setAnalysed] = useState<{ + enKeyCount: number; + languages: AnalysedLanguage[]; + } | undefined>(); + + const languages = getLanguages(); + if (!languages) return ; + + async function loadLanguages() { + if (!languages) return; + + // Fetch the locale file for each language code + const codes = Object.entries(languages); + const languageData = await Promise.allSettled(codes.map(getLanguage)); + + const en = languageData.find(l => l.status === "fulfilled" && l.value.code === "en"); + const enKeys = en?.status === "fulfilled" ? en?.value.keys || {} : {}; + const enKeyCount = enKeys ? Object.keys(enKeys).length : 1; + + setLoading(false); + setAnalysed({ + enKeyCount, + languages: languageData.map((result, i) => result.status === "fulfilled" + ? { + code: codes[i][0], + language: codes[i][1], + keys: result.value.keys, + keyCount: result.value.keyCount, + missingKeys: result.value.keys + ? Object.entries(enKeys) + .filter(([k]) => result.value.keys && !result.value.keys[k]) + .map(([k, v]) => ({ k, v })) + : [] + } + : { code: codes[i][0], language: codes[i][1], keyCount: 0, error: result.reason.toString() }) + }); + } + + async function exportCSV() { + if (!analysed) return; + + const data = await generateLanguageCSV(analysed.languages); + const blob = new Blob([data], { type: "text/csv;charset=utf-8" }); + saveAs(blob, "kristweb-translations.csv"); + } + + useEffect(() => { + if (fetched) return; + setFetched(true); + loadLanguages(); + }, [fetched]); + + return } onClick={exportCSV}> + {t("settings.translations.exportCSV")} + + }> + <> + + {code} + , + + width: 64, + }, + { + title: t("settings.translations.columnLanguage"), + dataIndex: ["language", "name"], + key: "language", + render: (_, row) => <> + {row?.language?.name} + {row?.language?.nativeName &&  ({row.language.nativeName})} + + }, + { + title: t("settings.translations.columnKeys"), + key: "keys", + render: (_, row) => row.error || !row.keys + ? ( + + + + ) + : <>{row.keyCount.toLocaleString()}, + sorter: (a, b) => a.keyCount - b.keyCount + }, + { + title: t("settings.translations.columnMissingKeys"), + key: "missingKeys", + render: (_, row) => (!row.error && row.keys && + <>{Math.max((analysed?.enKeyCount || 1) - row.keyCount, 0).toLocaleString()}), + sorter: (a, b) => b.keyCount - a.keyCount + }, + { + title: t("settings.translations.columnProgress"), + key: "progress", + render: (_, row) => (!row.error && row.keys && + ) + } + ]} + + expandable={{ + expandedRowRender: row => row.missingKeys &&
t("settings.translations.tableUntranslatedKeys")} + size="small" + + dataSource={row.missingKeys} + rowKey="k" + + columns={[ + { + title: t("settings.translations.columnKey"), + dataIndex: "k", + key: "k", + render: k => {k} + }, + { + title: t("settings.translations.columnEnglishString"), + dataIndex: "v", + key: "v", + render: v => {v} + } + ]} + /> + }} + /> + ; +} diff --git a/src/reportWebVitals.ts b/src/reportWebVitals.ts index 49a2a16..41784a3 100644 --- a/src/reportWebVitals.ts +++ b/src/reportWebVitals.ts @@ -1,8 +1,8 @@ -import { ReportHandler } from 'web-vitals'; +import { ReportHandler } from "web-vitals"; -const reportWebVitals = (onPerfEntry?: ReportHandler) => { +const reportWebVitals = (onPerfEntry?: ReportHandler): void => { if (onPerfEntry && onPerfEntry instanceof Function) { - import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { + import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { getCLS(onPerfEntry); getFID(onPerfEntry); getFCP(onPerfEntry); diff --git a/src/store/reducers/WalletManagerReducer.ts b/src/store/reducers/WalletManagerReducer.ts index f03c662..5b2316f 100644 --- a/src/store/reducers/WalletManagerReducer.ts +++ b/src/store/reducers/WalletManagerReducer.ts @@ -21,7 +21,7 @@ readonly hasMasterPassword?: boolean; } -export function getInitialWalletManagerState() { +export function getInitialWalletManagerState(): State { // Salt and tester from local storage (or undefined) const salt = localStorage.getItem("salt") || undefined; const tester = localStorage.getItem("tester") || undefined; diff --git a/src/style/components.less b/src/style/components.less new file mode 100644 index 0000000..85a5be1 --- /dev/null +++ b/src/style/components.less @@ -0,0 +1,44 @@ +.big-menu.ant-menu.ant-menu-inline { + width: 100%; + + border: 1px solid @border-color-split; +} + +.ant-btn { + color: @btn-default-color; + border-color: transparent; + + &:hover, &:focus { + color: @btn-default-color; + border-color: transparent; + background: lighten(@kw-lighter, 5%); + } + + &:focus { + background: lighten(@kw-lighter, 10%); + } + + &.ant-btn-primary { + background: @primary-color; + + &:hover, &:focus { + background: lighten(@primary-color, 5%); + } + + &:focus { + background: lighten(@primary-color, 10%); + } + } + + &.ant-btn-dangerous { + background: @error-color; + + &:hover, &:focus { + background: lighten(@error-color, 5%); + } + + &:focus { + background: lighten(@error-color, 10%); + } + } +} diff --git a/src/style/theme.less b/src/style/theme.less index 423e373..af6e173 100644 --- a/src/style/theme.less +++ b/src/style/theme.less @@ -37,6 +37,11 @@ @border-radius-base: 6px; +@component-background: @kw-darkest; + +@tooltip-bg: @kw-light; +@tooltip-color: @text-color; + @popover-background: @kw-light; .ant-popover-title { border-bottom-color: @kw-dark; @@ -45,6 +50,14 @@ @layout-header-background: @body-background; @layout-header-height: 50px; +@menu-item-color: @text-color; +@menu-highlight-color: @text-color; +@menu-bg: @kw-light; +@menu-item-active-bg: @kw-slighter; +@menu-item-active-border-width: 0; +@menu-item-vertical-margin: 0; +@menu-inline-submenu-bg: @kw-darker; + @menu-dark-bg: @kw-darker; @menu-dark-color: @text-color; @menu-dark-item-active-bg: mix(@kw-darker, @kw-secondary, 40%); @@ -56,8 +69,24 @@ @input-color: @text-color; @input-placeholder-color: @text-color-secondary; +@btn-default-color: @text-color; +@btn-default-bg: @kw-lighter; +@btn-default-border: transparent; + @btn-font-size-sm: @font-size-sm; +@table-header-bg: @kw-darker; +@table-header-sort-bg: @kw-darkest; +@table-header-filter-active-bg: @kw-darkest; +@table-header-sort-active-bg: @kw-darkest; +@table-bg: @body-background; +@table-body-sort-bg: mix(@table-bg, @kw-light, 20%); +@table-row-hover-bg: @kw-light; +@table-border-color: @kw-border-color-division; +.ant-table .ant-table-tbody > tr:last-child > td { + border-bottom: 0; +} + // general theme // --- @kw-sidebar-bg: @kw-darker; diff --git a/src/utils/i18n.ts b/src/utils/i18n.ts index acd3337..e31bc18 100644 --- a/src/utils/i18n.ts +++ b/src/utils/i18n.ts @@ -7,6 +7,26 @@ import packageJson from "../../package.json"; +// Find languages.json +const req = require.context("../../", false, /\.\/languages.json$/); + +export interface Language { + name: string; + nativeName?: string; + country?: string; + contributors: Contributor[]; +}; + +export interface Contributor { + name: string; + url?: string; +} + +export function getLanguages(): { [key: string]: Language } | null { + if (!req.keys().includes("./languages.json")) return null; + return req("./languages.json"); +} + i18n .use(Backend) .use(LanguageDetector)