From c029f2ab843b3c712df9d1323d869c9602610459 Mon Sep 17 00:00:00 2001 From: Matthew Date: Tue, 12 Sep 2023 23:57:57 -0400 Subject: [PATCH] Rebase: Master -> CSS Rewrite (#14) * Feature: Moderation (#9) * feature: Added banning, and, forced hidden button * feature: Added hiding and shadow hiding users * feature: Added badges to user profile * feature: Added hiding from the front page when hidden * fix: Fixed code related to front page hiding * Feature: Full-Site Multilanguage support (#13) * Feature: Moderation (#9) (#12) * feature: Added banning, and, forced hidden button * feature: Added hiding and shadow hiding users * feature: Added badges to user profile * feature: Added hiding from the front page when hidden * fix: Fixed code related to front page hiding * feature: Began adding the framework for multi-language support * feature: Full localization support and all pages and nav bars --- jsconfig.json | 3 +- locales/en.json | 50 +++ locales/jp.json | 49 +++ package-lock.json | 225 +++++++++++++ package.json | 1 + .../20230912142371_translation/migration.sql | 2 + prisma/schema.prisma | 1 + src/components/edit/FontCard.jsx | 3 +- src/components/edit/GeneralCard.jsx | 89 ++--- src/components/edit/ImagesCard.jsx | 212 ++++++------ src/components/index/LinkTagCarousel.jsx | 49 +-- src/components/shared/AppFooter.jsx | 84 ++--- src/components/shared/AppNavbar.jsx | 87 +++-- src/components/shared/LanguageContext.jsx | 13 + src/components/shared/LocalizedString.jsx | 29 ++ src/components/shared/UserMenu.jsx | 114 ++++--- src/components/user/PlayLog.jsx | 67 ++-- src/components/user/UserInformationCard.jsx | 148 ++++----- src/pages/_app.jsx | 2 - src/pages/account.jsx | 23 +- src/pages/admin.jsx | 5 + src/pages/api/account/language.js | 53 +++ src/pages/edit.jsx | 313 +++++++++--------- src/pages/index.jsx | 39 ++- src/pages/user/[username]/admin.jsx | 15 +- src/pages/user/[username]/index.jsx | 109 +++--- 26 files changed, 1174 insertions(+), 611 deletions(-) create mode 100644 locales/en.json create mode 100644 locales/jp.json create mode 100644 prisma/migrations/20230912142371_translation/migration.sql create mode 100644 src/components/shared/LanguageContext.jsx create mode 100644 src/components/shared/LocalizedString.jsx create mode 100644 src/pages/api/account/language.js diff --git a/jsconfig.json b/jsconfig.json index 68a0c88..161a0b5 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -4,7 +4,8 @@ "paths": { "@/components/*": ["components/*"], "@/lib/*": ["lib/*"], - "@/styles/*": ["styles/*"] + "@/styles/*": ["styles/*"], + "@/locales/*": ["locales/*"], } }, "exclude": ["node_modules"] diff --git a/locales/en.json b/locales/en.json new file mode 100644 index 0000000..e511d86 --- /dev/null +++ b/locales/en.json @@ -0,0 +1,50 @@ +{ + "welcome": "Welcome to LinkTag!", + "join_others": "Join {0} other gamers that have played games {1} times!", + "credits": "Credits", + "discord": "Login with Discord", + "carasol_linktag": "{1}'s LinkTag", + "supported_platforms": "Supported Platforms", + "banned_reason": "This account has been banned. Reason: {0}", + "game_id": "Game / ID", + "play_time": "Play Time", + "last_played": "Last Played", + "play_count": "Play Count", + "user_info": "User Information", + "banned": "Banned", + "hidden": "Hidden", + "administrator": "Administrator", + "moderator": "Moderator", + "play_log": "Play Log", + + "registered_on": "Registered", + "overlay": "Overlay", + "background": "Background", + "coin": "Coin", + "font": "Font", + "flag": "Flag", + "cover_region": "Cover Region", + "cover_type": "Cover Type", + + "edit_tag": "Edit Tag", + + "general": "General", + "display_name": "Display Name", + "friend_code": "Friend Code", + + "cover_region_explanation": "Linktag will try the game's region and fallback to English\nif it can't find a cover.", + "show_avatar": "Show Avatar", + + "select_overlay": "Select Overlay", + "select_background": "Select Background", + "select_coin": "Select Coin", + "select_font": "Select Font", + + "images": "Images", + + "home": "Home", + "profile": "Profile", + "account": "My Account", + "edit_mii": "Edit Mii", + "logout": "Logout" +} \ No newline at end of file diff --git a/locales/jp.json b/locales/jp.json new file mode 100644 index 0000000..e971d46 --- /dev/null +++ b/locales/jp.json @@ -0,0 +1,49 @@ +{ + "welcome": "LinkTagへようこそ!", + "join_others": "ゲームを{0}回プレイしたことがある他の{1}人のゲーマーに参加しよう!", + "discord": "Discordでログイン", + "carasol_linktag": "{1}のLinkTag", + "supported_platforms": "対応プラットホーム", + "banned_reason": "このユーザは追放されました。 理由: {0}", + "game_id": "ゲーム / ID", + "play_time": "プレイタイム", + "last_played": "最後にプレイした", + "play_count": "プレイ回数", + "user_info": "ユーザー情報", + "banned": "追放", + "hidden": "非表示", + "administrator": "管理者", + "moderator": "モデレーター", + "play_log": "プレイログ", + + "registered_on": "登録日", + "overlay": "オーバーレイ", + "background": "背景", + "coin": "コイン", + "font": "フォント", + "flag": "国旗", + "cover_region": "カバー地域", + "cover_type": "カバータイプ", + + "edit_tag": "タグを編集", + + "general": "一般", + "display_name": "表示名", + "friend_code": "フレンドコード", + + "cover_region_explanation": "カバー地域は、ゲームのカバーに表示される地域です。データベースで無かったら英語にします", + "show_avatar": "アバターを表示", + + "select_overlay": "オーバーレイを選択", + "select_background": "背景を選択", + "select_coin": "コインを選択", + "select_font": "フォントを選択", + + "images": "画像", + + "home": "ホーム", + "profile": "プロファイル", + "account": "マイアカウント", + "edit_mii": "Miiを編集", + "logout": "ログアウト" +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 9f0f61f..24fbb75 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "form-data": "4.0.0", "formidable": "2.1.1", "formik": "2.2.9", + "html-react-parser": "^4.2.2", "iron-session": "6.3.1", "lru-cache": "7.14.1", "moment": "^2.29.4", @@ -2240,6 +2241,57 @@ "csstype": "^3.0.2" } }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/dotenv": { "version": "16.0.0", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.0.tgz", @@ -2302,6 +2354,17 @@ "node": ">=10.13.0" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es-abstract": { "version": "1.20.5", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.5.tgz", @@ -3786,6 +3849,47 @@ "react-is": "^16.7.0" } }, + "node_modules/html-dom-parser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-dom-parser/-/html-dom-parser-4.0.0.tgz", + "integrity": "sha512-TUa3wIwi80f5NF8CVWzkopBVqVAtlawUzJoLwVLHns0XSJGynss4jiY0mTWpiDOsuyw+afP+ujjMgRh9CoZcXw==", + "dependencies": { + "domhandler": "5.0.3", + "htmlparser2": "9.0.0" + } + }, + "node_modules/html-react-parser": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/html-react-parser/-/html-react-parser-4.2.2.tgz", + "integrity": "sha512-lh0wEGISnFZEAmvQqK4xc0duFMUh/m9YYyAhFursWxdtNv+hCZge0kj1y4wep6qPB5Zm33L+2/P6TcGWAJJbjA==", + "dependencies": { + "domhandler": "5.0.3", + "html-dom-parser": "4.0.0", + "react-property": "2.0.0", + "style-to-js": "1.1.4" + }, + "peerDependencies": { + "react": "0.14 || 15 || 16 || 17 || 18" + } + }, + "node_modules/htmlparser2": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.0.0.tgz", + "integrity": "sha512-uxbSI98wmFT/G4P2zXx4OVx04qWUmyFPrD2/CNepa2Zo3GPNaCaaxElDgwUrwYWkK1nr9fft0Ya8dws8coDLLQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "entities": "^4.5.0" + } + }, "node_modules/https-proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", @@ -3892,6 +3996,11 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "node_modules/inline-style-parser": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz", + "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==" + }, "node_modules/internal-slot": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", @@ -5345,6 +5454,11 @@ "react": "^16 || ^17 || ^18" } }, + "node_modules/react-property": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/react-property/-/react-property-2.0.0.tgz", + "integrity": "sha512-kzmNjIgU32mO4mmH5+iUyrqlpFQhF8K2k7eZ4fdLSOPFrD1XgEuSBv9LDEgxRXTMBqMd8ppT0x6TIzqE5pdGdw==" + }, "node_modules/react-toastify": { "version": "9.1.1", "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.1.1.tgz", @@ -5836,6 +5950,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/style-to-js": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.4.tgz", + "integrity": "sha512-zEeU3vy9xL/hdLBFmzqjhm+2vJ1Y35V0ctDeB2sddsvN1856OdMZUCOOfKUn3nOjjEKr6uLhOnY4CrX6gLDRrA==", + "dependencies": { + "style-to-object": "0.4.2" + } + }, + "node_modules/style-to-object": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.4.2.tgz", + "integrity": "sha512-1JGpfPB3lo42ZX8cuPrheZbfQ6kqPPnPHlKMyeRYtfKD+0jG+QsXgXN57O/dvJlzlB2elI6dGmrPnl5VPQFPaA==", + "dependencies": { + "inline-style-parser": "0.1.1" + } + }, "node_modules/styled-jsx": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", @@ -7848,6 +7978,39 @@ "csstype": "^3.0.2" } }, + "dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + } + }, + "domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==" + }, + "domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "requires": { + "domelementtype": "^2.3.0" + } + }, + "domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "requires": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + } + }, "dotenv": { "version": "16.0.0", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.0.tgz", @@ -7898,6 +8061,11 @@ "tapable": "^2.2.0" } }, + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" + }, "es-abstract": { "version": "1.20.5", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.5.tgz", @@ -8928,6 +9096,37 @@ "react-is": "^16.7.0" } }, + "html-dom-parser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-dom-parser/-/html-dom-parser-4.0.0.tgz", + "integrity": "sha512-TUa3wIwi80f5NF8CVWzkopBVqVAtlawUzJoLwVLHns0XSJGynss4jiY0mTWpiDOsuyw+afP+ujjMgRh9CoZcXw==", + "requires": { + "domhandler": "5.0.3", + "htmlparser2": "9.0.0" + } + }, + "html-react-parser": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/html-react-parser/-/html-react-parser-4.2.2.tgz", + "integrity": "sha512-lh0wEGISnFZEAmvQqK4xc0duFMUh/m9YYyAhFursWxdtNv+hCZge0kj1y4wep6qPB5Zm33L+2/P6TcGWAJJbjA==", + "requires": { + "domhandler": "5.0.3", + "html-dom-parser": "4.0.0", + "react-property": "2.0.0", + "style-to-js": "1.1.4" + } + }, + "htmlparser2": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.0.0.tgz", + "integrity": "sha512-uxbSI98wmFT/G4P2zXx4OVx04qWUmyFPrD2/CNepa2Zo3GPNaCaaxElDgwUrwYWkK1nr9fft0Ya8dws8coDLLQ==", + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "entities": "^4.5.0" + } + }, "https-proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", @@ -8999,6 +9198,11 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "inline-style-parser": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz", + "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==" + }, "internal-slot": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", @@ -10062,6 +10266,11 @@ "prop-types": "^15" } }, + "react-property": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/react-property/-/react-property-2.0.0.tgz", + "integrity": "sha512-kzmNjIgU32mO4mmH5+iUyrqlpFQhF8K2k7eZ4fdLSOPFrD1XgEuSBv9LDEgxRXTMBqMd8ppT0x6TIzqE5pdGdw==" + }, "react-toastify": { "version": "9.1.1", "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.1.1.tgz", @@ -10402,6 +10611,22 @@ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==" }, + "style-to-js": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.4.tgz", + "integrity": "sha512-zEeU3vy9xL/hdLBFmzqjhm+2vJ1Y35V0ctDeB2sddsvN1856OdMZUCOOfKUn3nOjjEKr6uLhOnY4CrX6gLDRrA==", + "requires": { + "style-to-object": "0.4.2" + } + }, + "style-to-object": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.4.2.tgz", + "integrity": "sha512-1JGpfPB3lo42ZX8cuPrheZbfQ6kqPPnPHlKMyeRYtfKD+0jG+QsXgXN57O/dvJlzlB2elI6dGmrPnl5VPQFPaA==", + "requires": { + "inline-style-parser": "0.1.1" + } + }, "styled-jsx": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", diff --git a/package.json b/package.json index 42c203e..1cf44ab 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "form-data": "4.0.0", "formidable": "2.1.1", "formik": "2.2.9", + "html-react-parser": "^4.2.2", "iron-session": "6.3.1", "lru-cache": "7.14.1", "moment": "^2.29.4", diff --git a/prisma/migrations/20230912142371_translation/migration.sql b/prisma/migrations/20230912142371_translation/migration.sql new file mode 100644 index 0000000..cbba102 --- /dev/null +++ b/prisma/migrations/20230912142371_translation/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE `user` ADD COLUMN `language` VARCHAR(11) NOT NULL DEFAULT 'en'; \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index bd24f2b..cab8e37 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -93,6 +93,7 @@ model user { isBanned Boolean @default(false) isPublic Boolean @default(true) publicOverride Boolean? + language String @default("en") @db.VarChar(11) accounts account[] following following[] playlog playlog[] diff --git a/src/components/edit/FontCard.jsx b/src/components/edit/FontCard.jsx index 62ae1f6..d04dcae 100644 --- a/src/components/edit/FontCard.jsx +++ b/src/components/edit/FontCard.jsx @@ -4,11 +4,12 @@ import { FONTS } from '@/lib/constants/forms/fonts' import SelectFontModal from '@/components/edit/SelectFontModal' import { Alert, Card, Col, Row } from 'react-bootstrap' import { Field } from 'formik' +import LocalizedString from '../shared/LocalizedString' function FontCard ({ values, errors }) { return ( - Font + diff --git a/src/components/edit/GeneralCard.jsx b/src/components/edit/GeneralCard.jsx index edda3e3..739863d 100644 --- a/src/components/edit/GeneralCard.jsx +++ b/src/components/edit/GeneralCard.jsx @@ -4,6 +4,8 @@ import { createOptionNodes } from '@/lib/utils/utils' import { COVER_REGIONS } from '@/lib/constants/forms/coverRegions' import { COVER_TYPES } from '@/lib/constants/forms/coverTypes' import { Card, Col, Form, Row } from 'react-bootstrap' +import LocalizedString from '../shared/LocalizedString' +import LanguageContext from '../shared/LanguageContext' const coverRegions = createOptionNodes(COVER_REGIONS) const coverTypes = createOptionNodes(COVER_TYPES) @@ -11,12 +13,12 @@ const coverTypes = createOptionNodes(COVER_TYPES) function GeneralCard ({ values, errors, handleChange }) { return ( - General + - Name on linktag + - Comment / Friend Code + - Cover Type + - Cover Region + - linktag will try the game's region and fallback to English - if it can't find a cover. + - - - - - - - - - - - You can edit it your Mii on the " - - Edit Mii - - " page. - - - - + + {(lang) => ( + + + + + + + + + + + You can edit it your Mii on the " + + Edit Mii + + " page. + + + + + )} + ) diff --git a/src/components/edit/ImagesCard.jsx b/src/components/edit/ImagesCard.jsx index 5431ba6..96986e0 100644 --- a/src/components/edit/ImagesCard.jsx +++ b/src/components/edit/ImagesCard.jsx @@ -9,6 +9,8 @@ import SelectCoinModal from '@/components/edit/SelectCoinModal' import { Alert, Card, Col, Form, Row } from 'react-bootstrap' import SelectModal from './SelectModal' import { Field } from 'formik' +import LocalizedString from '../shared/LocalizedString' +import LanguageContext from '../shared/LanguageContext' const flags = createOptionNodes(FLAGS) @@ -19,115 +21,113 @@ const backgrounds = BACKGROUNDS.map((background) => ({ function ImagesCard ({ values, errors, handleChange }) { return ( - - Images - - - -

Overlay

- `/img/overlay/${value}.png`} - name='overlay' - options={OVERLAYS} - /> -

- - This is the overlay that will be shown above the image and will - contain your info. - -

- {errors.overlay && ( - - {errors.overlay} - - )} - - - Overlay Preview - -
+ + {(lang) => ( + + + + + +

+ `/img/overlay/${value}.png`} + name='overlay' + options={OVERLAYS} + /> + {errors.overlay && ( + + {errors.overlay} + + )} + + + Overlay Preview + +
- - -

Background

- `/img/background/${value}`} - name='background' - options={backgrounds} - /> - {errors.background && ( - - {errors.background} - - )} - - - Background Preview - -
+ + +

+ `/img/background/${value}`} + name='background' + options={backgrounds} + /> + {errors.background && ( + + {errors.background} + + )} + + + Background Preview + +
- - -

Coin

- - {errors.coin && ( - - {errors.coin} - - )} - - {values.coin !== 'default' && ( - - Coin Preview - - )} -
+ + +

+ + {errors.coin && ( + + {errors.coin} + + )} + + {values.coin !== 'default' && ( + + Coin Preview + + )} +
- - - - Flag - - {flags} - - - {errors.flag} - - - - - Flag Preview - - -
-
+ + + + + + {flags} + + + {errors.flag} + + + + + Flag Preview + + +
+
+ )} + ) } diff --git a/src/components/index/LinkTagCarousel.jsx b/src/components/index/LinkTagCarousel.jsx index 8d9de24..e761801 100644 --- a/src/components/index/LinkTagCarousel.jsx +++ b/src/components/index/LinkTagCarousel.jsx @@ -1,31 +1,36 @@ import React from 'react' import PropTypes from 'prop-types' import { Carousel } from 'react-bootstrap' -import Link from 'next/link' +import LanguageContext from '../shared/LanguageContext' +import LocalizedString from '../shared/LocalizedString' function LinkTagCarousel ({ randomUsers }) { return ( - - {randomUsers.map((randomUser) => ( - - {`linktag - -

- LinkTag of{' '} - - {randomUser.display_name} - -

-
-
- ))} -
+ + {(lang) => ( + + {randomUsers.map((randomUser) => ( + + {`linktag + +

+ +

+
+
+ ))} +
+ )} +
) } diff --git a/src/components/shared/AppFooter.jsx b/src/components/shared/AppFooter.jsx index 86545cd..c9fa82a 100644 --- a/src/components/shared/AppFooter.jsx +++ b/src/components/shared/AppFooter.jsx @@ -2,48 +2,54 @@ import React from 'react' import ENV from '@/lib/constants/environmentVariables' import { Col, Container, Row } from 'react-bootstrap' import Link from 'next/link' +import LanguageContext from './LanguageContext' +import LocalizedString from './LocalizedString' function AppFooter () { return ( - - - - © LinkTag 2023 - {new Date().getFullYear()} -   - -{' '} - - Credits - {' '} - -{' '} - - About - - - {ENV.STAGING === 'true' && ( - - This is a PREVIEW. Data may be deleted at any time! - - )} - - - Privacy Policy - {' '} - -{' '} - - Terms of Service - {' '} - -{' '} - - Source - - - - + + {(lang) => ( + + + + © LinkTag 2023 - {new Date().getFullYear()} + {' '} + -{' '} + + + {' '} + -{' '} + + About + + + {ENV.STAGING === 'true' && ( + + This is a PREVIEW. Data may be deleted at any time! + + )} + + + Privacy Policy + {' '} + -{' '} + + Terms of Service + {' '} + -{' '} + + Source + + + + + )} + ) } diff --git a/src/components/shared/AppNavbar.jsx b/src/components/shared/AppNavbar.jsx index f7c399a..4d53597 100644 --- a/src/components/shared/AppNavbar.jsx +++ b/src/components/shared/AppNavbar.jsx @@ -1,40 +1,77 @@ import React from 'react' import { useRouter } from 'next/router' -import { Container, Nav, Navbar } from 'react-bootstrap' +import { Container, Nav, NavDropdown, Navbar } from 'react-bootstrap' import Link from 'next/link' import UserMenu from './UserMenu' +import LanguageContext from './LanguageContext' +import LocalizedString from './LocalizedString' + +const flags = { + en: '/img/flag/us.png', + jp: '/img/flag/jp.png' +} import styles from './AppNavbar.module.css' function AppNavbar () { const router = useRouter() + function updateLanguage (lang) { + fetch('/api/account/language', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ language: lang }) + }).then(() => { window.location.reload() }) + } + return ( - - - - LinkTag - - - - - - - - - + + + + + + + + )} + ) } diff --git a/src/components/shared/LanguageContext.jsx b/src/components/shared/LanguageContext.jsx new file mode 100644 index 0000000..9a7173f --- /dev/null +++ b/src/components/shared/LanguageContext.jsx @@ -0,0 +1,13 @@ +import React from 'react' + +const languages = { + en: require('../../../locales/en.json'), + jp: require('../../../locales/jp.json') +} + +const Helper = React.createContext('en') + +export default { + Helper, + languages +} diff --git a/src/components/shared/LocalizedString.jsx b/src/components/shared/LocalizedString.jsx new file mode 100644 index 0000000..69f6a45 --- /dev/null +++ b/src/components/shared/LocalizedString.jsx @@ -0,0 +1,29 @@ +import React from 'react' +import PropTypes from 'prop-types' +import LanguageContext from './LanguageContext' +import parse from 'html-react-parser' + +function LocalizedString ({ string, values = [] }) { + return ( + + {(lang) => { + return parse(formatString(LanguageContext.languages[lang][string], values)) + }} + + ) +} + +function formatString (string, values) { + let formattedString = string + for (let i = 0; i < values.length; i++) { + formattedString = formattedString.replace(`{${i}}`, values[i]) + } + return formattedString +} + +LocalizedString.propTypes = { + string: PropTypes.string.isRequired, + values: PropTypes.array +} + +export default LocalizedString diff --git a/src/components/shared/UserMenu.jsx b/src/components/shared/UserMenu.jsx index f6b358f..d43eecf 100644 --- a/src/components/shared/UserMenu.jsx +++ b/src/components/shared/UserMenu.jsx @@ -14,6 +14,8 @@ import useRouterRefresh from '../../hooks/useRefreshRoute' import { Button, Nav, NavDropdown } from 'react-bootstrap' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import Link from 'next/link' +import LanguageContext from './LanguageContext' +import LocalizedString from './LocalizedString' function UserMenu () { const { user, isLoading, isError, mutate } = useInfo() @@ -42,61 +44,65 @@ function UserMenu () { } return ( - + )} + ) } diff --git a/src/components/user/PlayLog.jsx b/src/components/user/PlayLog.jsx index a1faa4e..dd594b8 100644 --- a/src/components/user/PlayLog.jsx +++ b/src/components/user/PlayLog.jsx @@ -2,6 +2,8 @@ import React from 'react' import PropTypes from 'prop-types' import { Table } from 'react-bootstrap' import moment from 'moment/moment' +import LanguageContext from '../shared/LanguageContext' +import LocalizedString from '../shared/LocalizedString' import styles from './UserPanel.module.scss' @@ -11,32 +13,47 @@ function PlayLog ({ playlog, current }) { return duration.humanize() } + const [hydrated, setHydrated] = React.useState(false) + React.useEffect(() => { + // This forces a rerender, so the date is rendered + // the second time but not the first + setHydrated(true) + }, []) + if (!hydrated) { + // Returns null on first render, so the client and server match + return null + } + return ( -
-
Play Log
-
- - - - - - - - - - - {playlog.map((log) => ( - - - - - - - ))} - -
Game / IDPlay TimePlay CountLast Played
{log.game.name === null ? log.game.game_id : log.game.name}{log.play_time > 0 ? `${getTimeStamp(log.play_time)}` : 'Not Tracked'}{log.play_count} times{current && current.game_id === log.game.game_id ? 'Now' : moment(log.played_on).from()}
-
-
+ + {(lang) => ( + + + + + + + + + + + + + + {playlog.map((log) => ( + + + + + + + ))} + +
{log.game.name === null ? log.game.game_id : log.game.name}{log.play_time > 0 ? `${getTimeStamp(log.play_time)}` : 'Not Tracked'}{log.play_count} times{current && current.game_id === log.game.game_id ? 'Now' : moment(log.played_on).from()}
+
+
+ )} +
) } diff --git a/src/components/user/UserInformationCard.jsx b/src/components/user/UserInformationCard.jsx index 060bef8..812ae38 100644 --- a/src/components/user/UserInformationCard.jsx +++ b/src/components/user/UserInformationCard.jsx @@ -17,88 +17,82 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import Link from 'next/link' import BanAccountButton from '../account/BanAccountButton' import ForceHiddenAccountButton from '../account/ForceHiddenAccountButton' +import LanguageContext from '../shared/LanguageContext' +import LocalizedString from '../shared/LocalizedString' function UserInformationCard ({ user, isLoggedIn, isAdmin, isMod }) { return ( - - User Information - - {user.isBanned && (Banned)} - {(user.publicOverride === false || (user.publicOverride === true && isMod)) && (Hidden)} - {user.role === 'admin' && (Administrator)} - {user.role === 'mod' && (Moderator)} -
    -
  • - Name: {user.display_name} -
  • - {dayjs(user.created_at).format(DATE_FORMAT) !== '4th August 2022' ? (
  • Registered:{' '} {dayjs(user.created_at).format(DATE_FORMAT)}
  • ) : ('')} -
  • - Overlay:{' '} - {OVERLAYS.find((overlay) => overlay.value === user.overlay).label} -
  • -
  • - Background: {user.background} -
  • -
  • - Coin:{' '} - {COINS.find((coin) => coin.value === user.coin).label} -
  • -
  • - Flag:{' '} - {FLAGS.find((flag) => flag.value === user.flag).label} -
  • -
  • - Font:{' '} - {FONTS.find((font) => font.value === user.font).label} -
  • -
  • - Cover Region:{' '} - { - COVER_REGIONS.find( - (coverRegion) => coverRegion.value === user.cover_region - ).label - } -
  • -
  • - Cover Type:{' '} - { - COVER_TYPES.find( - (coverType) => coverType.value === user.cover_type - ).label - } -
  • -
- {(isLoggedIn || isAdmin) && ( -
-
-
- - - -
- {isMod && ( -
- - - - + + {(lang) => ( + + + + {user.isBanned && ()} + {(user.publicOverride === false || (user.publicOverride === true && isMod)) && ()} + {user.role === 'admin' && ()} + {user.role === 'mod' && ()} +
    +
  • + : {user.display_name} +
  • +
  • + : {dayjs(user.created_at).format(DATE_FORMAT)} +
  • +
  • + : {OVERLAYS.find((overlay) => overlay.value === user.overlay).label} +
  • +
  • + : {user.background} +
  • +
  • + : {COINS.find((coin) => coin.value === user.coin).label} +
  • +
  • + : {FLAGS.find((flag) => flag.value === user.flag).label} +
  • +
  • + : {FONTS.find((font) => font.value === user.font).label} +
  • +
  • + : {COVER_REGIONS.find((coverRegion) => coverRegion.value === user.cover_region).label} +
  • +
  • + : {COVER_TYPES.find((coverType) => coverType.value === user.cover_type).label} +
  • +
+ {(isLoggedIn || isAdmin) && ( +
+
+
+ + + +
+ {isMod && ( +
+ + + + +
+ )} + {isAdmin && ( +
+ + + +
+ )}
)} - {isAdmin && ( -
- - - -
- )} -
- )} - - + + + )} + ) } diff --git a/src/pages/_app.jsx b/src/pages/_app.jsx index 413df3a..e1a694e 100644 --- a/src/pages/_app.jsx +++ b/src/pages/_app.jsx @@ -6,7 +6,6 @@ import { ToastContainer } from 'react-toastify' import NextNProgress from 'nextjs-progressbar' import { DefaultSeo } from 'next-seo' import { SSRProvider } from 'react-bootstrap' -import AppNavbar from '@/components/shared/AppNavbar' import AppFooter from '@/components/shared/AppFooter' import ENV from '@/lib/constants/environmentVariables' @@ -20,7 +19,6 @@ function App ({ Component, pageProps }) { }} > - { const username = req.session?.username @@ -22,6 +24,17 @@ export const getServerSideProps = withSession(async ({ req }) => { } } + const loggedInUser = username != null + ? await prisma.user.findUnique({ + where: { + username + }, + select: { + language: true + } + }) + : { role: 'guest' } + const accountInfo = await prisma.user.findFirst({ where: { username @@ -38,11 +51,13 @@ export const getServerSideProps = withSession(async ({ req }) => { } }) - return { props: { accountInfo: JSON.parse(safeJsonStringify(accountInfo)) } } + return { props: { accountInfo: JSON.parse(safeJsonStringify(accountInfo)), language: loggedInUser.language } } }) -function AccountPage ({ accountInfo }) { +function AccountPage ({ accountInfo, language }) { return ( + + + ) } AccountPage.propTypes = { - accountInfo: PropTypes.object.isRequired + accountInfo: PropTypes.object.isRequired, + language: PropTypes.string.isRequired } export default AccountPage diff --git a/src/pages/admin.jsx b/src/pages/admin.jsx index d082832..4ac0e32 100644 --- a/src/pages/admin.jsx +++ b/src/pages/admin.jsx @@ -9,6 +9,8 @@ import PrivacyPolicyCard from '@/components/admin/PrivacyPolicyCard' import TermsOfServiceCard from '@/components/admin/TermsOfServiceCard' import ENV from '@/lib/constants/environmentVariables' import AboutCard from '@/components/admin/AboutCard' +import LanguageContext from '@/components/shared/LanguageContext' +import AppNavbar from '@/components/shared/AppNavbar' export const getServerSideProps = withSession(async ({ req }) => { const username = req.session?.username @@ -58,6 +60,8 @@ export const getServerSideProps = withSession(async ({ req }) => { function AdminPage ({ systemInfo }) { return ( + + + ) } diff --git a/src/pages/api/account/language.js b/src/pages/api/account/language.js new file mode 100644 index 0000000..d7afae8 --- /dev/null +++ b/src/pages/api/account/language.js @@ -0,0 +1,53 @@ +import HTTP_CODE from '@/lib/constants/httpStatusCodes' +import { ncWithSession } from '@/lib/routing' +import { userIsMod } from '@/lib/utils/databaseUtils' +import prisma from '@/lib/db' +import logger from '@/lib/logger' +import { isBlank } from '@/lib/utils/utils' + +async function exportData (request, response) { + const loggedInUser = request.session?.username + const { + language + } = request.body + + if ( + isBlank(language) + ) { + return response + .status(HTTP_CODE.BAD_REQUEST) + .send({ error: 'Invalid data' }) + } + + if (!loggedInUser) { + return response + .status(HTTP_CODE.UNAUTHORIZED) + .json({ error: 'Unauthorized' }) + } + + if (!(await userIsMod(loggedInUser))) { + return response + .status(HTTP_CODE.UNAUTHORIZED) + .json({ error: 'Unauthorized' }) + } + + try { + await prisma.user.update({ + data: { + language + }, + where: { + username: loggedInUser + } + }) + + return response.status(HTTP_CODE.OK).send(null) + } catch (error) { + logger.error(error) + return response.status(HTTP_CODE.INTERNAL_SERVER_ERROR).send() + } +} + +const handler = ncWithSession().post(exportData) + +export default handler diff --git a/src/pages/edit.jsx b/src/pages/edit.jsx index c80bd30..906dbc6 100644 --- a/src/pages/edit.jsx +++ b/src/pages/edit.jsx @@ -19,10 +19,23 @@ import GeneralCard from '@/components/edit/GeneralCard' import FontCard from '@/components/edit/FontCard' import ImagesCard from '@/components/edit/ImagesCard' import ENV from '@/lib/constants/environmentVariables' +import LanguageContext from '@/components/shared/LanguageContext' +import AppNavbar from '@/components/shared/AppNavbar' export const getServerSideProps = withSession(async ({ req }) => { const username = req.session?.username + const loggedInUser = username != null + ? await prisma.user.findUnique({ + where: { + username + }, + select: { + language: true + } + }) + : { role: 'guest' } + if (!username) { return { redirect: { @@ -51,162 +64,166 @@ export const getServerSideProps = withSession(async ({ req }) => { } }) - return { props: { tagInfo } } + return { props: { tagInfo, language: loggedInUser?.language || 'en' } } }) -function EditPage ({ tagInfo }) { +function EditPage ({ tagInfo, language }) { return ( - { - const errors = {} - - if (!values.nameOnlinktag) { - errors.nameOnlinktag = 'Required' - } else if (values.nameOnlinktag.length > 20) { - errors.nameOnlinktag = 'Name must be < 20 characters.' - } - - if (values.comment && values.comment.length > 50) { - errors.comment = 'Comment must be < 50 characters.' - } - - if (!values.coverRegion) { - errors.coverRegion = 'Required' - } else if (isValidCoverRegion(values.coverRegion) === false) { - errors.coverRegion = 'Invalid Cover Region' - } - - if (!values.coverType) { - errors.coverType = 'Required' - } else if (isValidCoverType(values.coverType) === false) { - errors.coverType = 'Invalid Cover Type' - } - - if (!values.overlay) { - errors.overlay = 'Required' - } else if (isValidOverlay(values.overlay) === false) { - errors.overlay = 'Invalid Overlay' - } - - if (!values.background) { - errors.background = 'Required' - } else if (BACKGROUNDS.includes(values.background) === false) { - errors.background = 'Invalid Background' - } - - if (!values.flag) { - errors.flag = 'Required' - } else if (isValidFlag(values.flag) === false) { - errors.flag = 'Invalid Flag' - } - - if (!values.coin) { - errors.coin = 'Required' - } else if (isValidCoin(values.coin) === false) { - errors.coin = 'Invalid Coin' - } - - if (!values.font) { - errors.font = 'Required' - } else if (isValidFont(values.font) === false) { - errors.font = 'Invalid Font' - } - - return errors - }} - onSubmit={async (values, { setSubmitting }) => { - await toast.promise( - fetch('/api/account/tag', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(values) - }), - { - pending: 'Updating & regenerating your tag...', - success: { - render ({ data, toastProps }) { - if (data.status !== 200) { - toastProps.type = 'error' - return 'An error occured, please try again later' - } - return 'Saved!' - } - }, - error: 'An error occured, please try again later.' + + + { + const errors = {} + + if (!values.nameOnlinktag) { + errors.nameOnlinktag = 'Required' + } else if (values.nameOnlinktag.length > 20) { + errors.nameOnlinktag = 'Name must be < 20 characters.' + } + + if (values.comment && values.comment.length > 50) { + errors.comment = 'Comment must be < 50 characters.' + } + + if (!values.coverRegion) { + errors.coverRegion = 'Required' + } else if (isValidCoverRegion(values.coverRegion) === false) { + errors.coverRegion = 'Invalid Cover Region' + } + + if (!values.coverType) { + errors.coverType = 'Required' + } else if (isValidCoverType(values.coverType) === false) { + errors.coverType = 'Invalid Cover Type' + } + + if (!values.overlay) { + errors.overlay = 'Required' + } else if (isValidOverlay(values.overlay) === false) { + errors.overlay = 'Invalid Overlay' } - ) - - setSubmitting(false) - }} - > - {({ values, errors, handleChange, handleSubmit, isSubmitting }) => ( -
- - - - - - - - - - - - - - - -
- )} -
+ + if (!values.background) { + errors.background = 'Required' + } else if (BACKGROUNDS.includes(values.background) === false) { + errors.background = 'Invalid Background' + } + + if (!values.flag) { + errors.flag = 'Required' + } else if (isValidFlag(values.flag) === false) { + errors.flag = 'Invalid Flag' + } + + if (!values.coin) { + errors.coin = 'Required' + } else if (isValidCoin(values.coin) === false) { + errors.coin = 'Invalid Coin' + } + + if (!values.font) { + errors.font = 'Required' + } else if (isValidFont(values.font) === false) { + errors.font = 'Invalid Font' + } + + return errors + }} + onSubmit={async (values, { setSubmitting }) => { + await toast.promise( + fetch('/api/account/tag', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(values) + }), + { + pending: 'Updating & regenerating your tag...', + success: { + render ({ data, toastProps }) { + if (data.status !== 200) { + toastProps.type = 'error' + return 'An error occured, please try again later' + } + return 'Saved!' + } + }, + error: 'An error occured, please try again later.' + } + ) + + setSubmitting(false) + }} + > + {({ values, errors, handleChange, handleSubmit, isSubmitting }) => ( +
+ + + + + + + + + + + + + + + +
+ )} +
+ ) } EditPage.propTypes = { - tagInfo: PropTypes.object.isRequired + tagInfo: PropTypes.object.isRequired, + language: PropTypes.string.isRequired } export default EditPage diff --git a/src/pages/index.jsx b/src/pages/index.jsx index b10ee17..3a04a60 100644 --- a/src/pages/index.jsx +++ b/src/pages/index.jsx @@ -11,14 +11,31 @@ import useInfo from '@/lib/swr-hooks/useInfo' import prisma from '@/lib/db' import LinkTagCarousel from '@/components/index/LinkTagCarousel' import safeJsonStringify from 'safe-json-stringify' +import { withSession } from '@/lib/iron-session' +import LanguageContext from '@/components/shared/LanguageContext' +import LocalizedString from '@/components/shared/LocalizedString' +import AppNavbar from '@/components/shared/AppNavbar' + import AstroContainer from '@/components/index/Astro/AstroContainer' -export async function getStaticProps () { + +export const getServerSideProps = withSession(async ({ req, query }) => { const userCount = await prisma.user.count() const playCountResult = await prisma.$queryRaw` SELECT SUM(coins) FROM user ` + + const loggedInUsername = req.session?.username + const loggedInUser = await prisma.user.findFirst({ + where: { + username: loggedInUsername + }, + select: { + language: true + } + }) + const playCount = Number(playCountResult[0]['SUM(coins)']) const randomUsers = await prisma.$queryRaw` SELECT user.username, user.display_name, user.updated_at @@ -32,13 +49,13 @@ export async function getStaticProps () { props: { userCount, playCount, + language: loggedInUser.language, randomUsers: JSON.parse(safeJsonStringify(randomUsers)) - }, - revalidate: 10 + } } -} +}) -function IndexPage ({ userCount, playCount, randomUsers }) { +function IndexPage ({ userCount, playCount, language, randomUsers }) { const router = useRouter() const { user, isLoading } = useInfo() @@ -53,12 +70,14 @@ function IndexPage ({ userCount, playCount, randomUsers }) { }, [router.query.error]) return ( + + -

Welcome to LinkTag!

+

@@ -81,9 +100,7 @@ function IndexPage ({ userCount, playCount, randomUsers }) {

- Join {userCount}{' '} - {userCount === 1 ? 'other gamer' : 'other gamers'} that have played games {playCount}{' '} - {playCount === 1 ? 'time' : 'times'}! + {' '}

@@ -129,7 +146,7 @@ function IndexPage ({ userCount, playCount, randomUsers }) {

- Platforms Supported +

@@ -155,12 +172,14 @@ function IndexPage ({ userCount, playCount, randomUsers }) {
+
) } IndexPage.propTypes = { userCount: PropTypes.number.isRequired, randomUsers: PropTypes.arrayOf(PropTypes.object).isRequired, + language: PropTypes.string.isRequired, playCount: PropTypes.number.isRequired } diff --git a/src/pages/user/[username]/admin.jsx b/src/pages/user/[username]/admin.jsx index 2e33498..e8b0e8e 100644 --- a/src/pages/user/[username]/admin.jsx +++ b/src/pages/user/[username]/admin.jsx @@ -5,6 +5,8 @@ import PropTypes from 'prop-types' import { withSession } from '@/lib/iron-session' import prisma from '@/lib/db' import GeneralUserAdminCard from '@/components/user/admin/GeneralUserAdminCard' +import LanguageContext from '@/components/shared/LanguageContext' +import AppNavbar from '@/components/shared/AppNavbar' export const getServerSideProps = withSession(async ({ req, query }) => { const { username } = query @@ -52,11 +54,14 @@ export const getServerSideProps = withSession(async ({ req, query }) => { function ProfileAdminPage ({ user, isLoggedIn }) { return ( - - - + + + + + + ) } diff --git a/src/pages/user/[username]/index.jsx b/src/pages/user/[username]/index.jsx index 0c73007..9125782 100644 --- a/src/pages/user/[username]/index.jsx +++ b/src/pages/user/[username]/index.jsx @@ -11,6 +11,9 @@ import ShowYourTagCard from '@/components/user/ShowYourTagCard' import ENV from '@/lib/constants/environmentVariables' import PlayLog from '@/components/user/PlayLog' import PlayingStatus from '@/components/user/PlayingStatus' +import LanguageContext from '@/components/shared/LanguageContext' +import LocalizedString from '@/components/shared/LocalizedString' +import AppNavbar from '@/components/shared/AppNavbar' import styles from './index.module.css' @@ -101,7 +104,8 @@ export const getServerSideProps = withSession(async ({ req, query }) => { username: loggedInUsername }, select: { - role: true + role: true, + language: true } }) : { role: 'guest' } @@ -115,6 +119,7 @@ export const getServerSideProps = withSession(async ({ req, query }) => { user: JSON.parse(safeJsonStringify(user)), isLoggedIn: user.username === loggedInUsername, loggedInUser, + language: loggedInUser?.language || 'en', banReason: JSON.parse(safeJsonStringify(banReason)), event: JSON.parse(safeJsonStringify(event)), playlog: JSON.parse(safeJsonStringify(playlog)), @@ -124,58 +129,59 @@ export const getServerSideProps = withSession(async ({ req, query }) => { } }) -function ProfilePage ({ user, isLoggedIn, banReason, loggedInUser, event, playlog, session, game }) { +function ProfilePage ({ user, isLoggedIn, banReason, loggedInUser, event, playlog, language, session, game }) { return ( - - + + + -
- - {user.isBanned === true ? This account has been banned. Reason: {banReason.reason} : ''} - - {user.isBanned === false - ? -
- -
- - - - : ''} - - - { event && An event is currently ongoing: {event.name}.
Until {event.date}, you will recieve {event.bonus + 1}x more coins.
} - { session && } - - - {isLoggedIn && } - -
-
-
+ }} + /> + + {user.isBanned === true ? : ''} + + {user.isBanned === false + ? +
+ +
+ + + + : ''} + + + {event && An event is currently ongoing: {event.name}.
Until {event.date}, you will recieve {event.bonus + 1}x more coins.
} + {session && } + + + {isLoggedIn && } + +
+
+ ) } @@ -183,6 +189,7 @@ ProfilePage.propTypes = { user: PropTypes.object.isRequired, isLoggedIn: PropTypes.bool.isRequired, loggedInUser: PropTypes.object.isRequired, + language: PropTypes.string.isRequired, event: PropTypes.object.isRequired, banReason: PropTypes.object.isRequired, playlog: PropTypes.array.isRequired,