diff --git a/locales/CN_zh.json b/locales/CN_zh.json
new file mode 100644
index 0000000..824ea21
--- /dev/null
+++ b/locales/CN_zh.json
@@ -0,0 +1,244 @@
+{
+ "nav": {
+ "about": "关于",
+ "faq": "FAQ",
+ "docs": "文件",
+ "credits": "贡献者",
+ "progress": "进度",
+ "blog": "博客",
+ "account": "用户"
+ },
+ "hero": {
+ "subtitle": "游戏服务器",
+ "title": "重新创建",
+ "text": "Pretendo 是任天堂 3DS 和 Wii U 服务器的免费和开源替代品,允许所有人在线连接,即使在原始服务器已经关闭",
+ "buttons": {
+ "readMore": "更多"
+ }
+ },
+ "aboutUs": {
+ "title": "关于我们",
+ "paragraphs": [
+ "Pretendo 是一个开源项目,旨在使用逆向工程为 3DS 和 Wii U 重新创建 Nintendo Network。",
+ "我们的服务将是免费和开源的,它们可以在 Nintendo Network 不可避免的关闭之后还存在。"
+ ]
+ },
+ "progress": {
+ "title": "进度",
+ "paragraphs": [
+ "目前,我们正在重新创建 Miiverse,以及我们的用户服务器及相关的服务。",
+ "对于 3DS,我们也在创建《马里奥赛车 7》的服务器,并希望在可能的情况下继续为他游戏做服务器。"
+ ]
+ },
+ "faq": {
+ "title": "常见问题",
+ "text": "以下是我们被问到的一些常见问题,以获得简单的信息。",
+ "QAs": [
+ {
+ "question": "什么是 Pretendo?",
+ "answer": "Pretendo 是一个开源的 Nintendo Network 替代品,旨在为 Wii U 和 3DS 系列游戏机构建自定义服务器。我们的目标是保留这些游戏机的在线功能,让玩家可以继续尽情畅玩他们最喜欢的 Wii U 和 3DS 游戏。"
+ },
+ {
+ "question": "我现有的 NNID 可以在 Pretendo 上工作吗?",
+ "answer": "遗憾的是,不行。现有的 NNID 将无法在 Pretendo 上运行,因为只有任天堂拥有您的用户数据;虽然 NNID 到 PNID 的迁移在理论上是可能的,但它存在风险并且需要我们不希望持有的敏感用户数据。"
+ },
+ {
+ "question": "如何使用 Pretendo?",
+ "answer": "Pretendo 目前尚未处于可供公众使用的状态。但是,一旦完成,您只需在系统上运行我们的自制补丁程序即可使用 Pretendo。"
+ },
+ {
+ "question": "你知道 <功能/服务> 什么时候准备好吗?",
+ "answer": "不可以。许多 Pretendo 的功能/服务是独立开发的(例如,Miiverse 可能由一位开发人员构建,而 Accounts 和 Friends 正在由另一位开发人员构建),因此我们无法给出这需要多长时间的总体预计到达时间。"
+ },
+ {
+ "question": "Pretendo 是否适用于 Cemu/模拟器?",
+ "answer": "Pretendo 是专为 Wii U 和 3DS 设计的;目前,这些支持 NN 的硬件的唯一模拟器是 Cemu。 Cemu 不正式支持自定义服务器,但仍然可以将 Pretendo 与 Cemu 一起使用。
Pretendo 目前不支持 Cemu。"
+ },
+ {
+ "question": "如果我在 Nintendo Network 上被封账号,我在使用 Pretendo 时还会被禁吗?",
+ "answer": "我们将无法访问 Nintendo Network 的禁令,并且不会禁止所有用户使用我们的服务。但是,我们在使用服务时将遵守规则,不遵守这些规则可能会导致被禁止。"
+ },
+ {
+ "question": "Pretendo 会支持 Wii/Switch 吗?",
+ "answer": "Wii 已经有 Wiimmfi 提供的自定义服务器。我们目前不希望以 Switch 为目标,因为它既是付费的,又与 Nintendo Network 完全不同。"
+ },
+ {
+ "question": "我需要黑客来连接吗?",
+ "answer": "是的,您需要破解您的设备才能连接;但是,在 Wii U 上,您只需要访问 Homebrew Launcher(即 Haxchi、Coldboot Haxchi 甚至网络浏览器漏洞利用程序),稍后会提供有关 3DS 将如何连接的信息。"
+ }
+ ]
+ },
+ "showcase": {
+ "title": "我们做什么",
+ "text": "我们的项目有很多组件。这里是其中的一些。",
+ "cards": [
+ {
+ "title": "游戏服务器",
+ "caption": "使用自定义服务器带回您最喜爱的游戏和内容。"
+ },
+ {
+ "title": "Juxtaposition",
+ "caption": "对 Miiverse 的重新想象,仿佛它是在现代时代制造的。"
+ },
+ {
+ "title": "支持 Cemu",
+ "caption": "即使没有硬件也能玩你最喜欢的 Wii U 游戏!"
+ }
+
+ ]
+ },
+ "credits": {
+ "title": "团队",
+ "text": "认识项目背后的团队",
+ "people": [
+ {
+ "name": "Jonathan Barrow (jonbarrow)",
+ "caption": "项目所有者和首席开发人员",
+ "picture": "https://github.com/jonbarrow.png",
+ "github": "https://github.com/jonbarrow"
+ },
+ {
+ "name": "Jemma (CaramelKat)",
+ "caption": "Miiverse 研发",
+ "picture": "https://github.com/caramelkat.png",
+ "github": "https://github.com/CaramelKat"
+ },
+ {
+ "name": "Rambo6Glaz",
+ "caption": "网络安装程序和系统研究",
+ "picture": "https://github.com/Rambo6Glaz.png",
+ "github": "https://github.com/Rambo6Glaz"
+ },
+ {
+ "name": "quarky",
+ "caption": "BOSS研究和补丁开发",
+ "picture": "https://github.com/QuarkTheAwesome.png",
+ "github": "https://github.com/QuarkTheAwesome"
+ },
+ {
+ "name": "SuperMarioDaBom",
+ "caption": "控制台等系统研究",
+ "picture": "https://github.com/supermariodabom.png",
+ "github": "https://github.com/SuperMarioDaBom"
+ },
+ {
+ "name": "Jip Fr",
+ "caption": "网络开发主管",
+ "picture": "https://github.com/jipfr.png",
+ "github": "https://github.com/jipfr"
+ },
+ {
+ "name": "monty",
+ "caption": "网络开发",
+ "picture": "https://github.com/ashmonty.png",
+ "github": "https://github.com/ashmonty"
+ },
+ {
+ "name": "mrjvs",
+ "caption": "设计师",
+ "picture": "https://github.com/mrjvs.png",
+ "github": "https://github.com/mrjvs"
+ }
+ ]
+ },
+ "specialThanks": {
+ "title": "特别感谢",
+ "text": "没有他们,Pretendo 就不会是今天的样子。",
+ "people": [
+ {
+ "name": "superwhiskers",
+ "caption": "Crunch 开发",
+ "picture": "https://github.com/superwhiskers.png",
+ "github": "https://github.com/superwhiskers"
+ },
+ {
+ "name": "Stary",
+ "caption": "3DS 开发和 NEX 解剖器",
+ "picture": "https://github.com/Stary2001.png",
+ "github": "https://github.com/Stary2001"
+ },
+ {
+ "name": "Billy",
+ "caption": "保护主义者",
+ "picture": "https://github.com/InternalLoss.png",
+ "github": "https://github.com/InternalLoss"
+ },
+ {
+ "name": "Shutterbug2000",
+ "caption": "马里奥赛车 7 和 3DS 研究",
+ "picture": "https://cdn.discordapp.com/avatars/191370953807233024/0311b61e2009c1576828dd2e9a59d72e.png?size=128",
+ "github": "https://github.com/shutterbug2000"
+ },
+ {
+ "name": "Kinnay",
+ "special": "特别感谢",
+ "caption": "任天堂数据结构研究",
+ "picture": "https://cdn.discordapp.com/avatars/186572995848830987/b55c0d4e7bfd792edf0689f83a25d8ea.png?size=128",
+ "github": "https://github.com/Kinnay"
+ },
+ {
+ "name": "NinStar",
+ "caption": "Mii 编辑器和 Juxt 反应的图标",
+ "picture": "https://github.com/ninstar.png",
+ "github": "https://github.com/ninstar"
+ },
+ {
+ "name": "GitHub contributors",
+ "caption": "本地化和其他贡献",
+ "picture": "https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png",
+ "github": "https://github.com/PretendoNetwork"
+ }
+ ]
+ },
+ "discordJoin": {
+ "title": "得到我们的动态与更新",
+ "text": "加入我们的 Discord 服务器以获取该项目的最新更新。",
+ "widget": {
+ "text": "获取我们进度的实时更新",
+ "button": "加入服务器"
+ }
+ },
+ "footer": {
+ "socials": "社交",
+ "usefulLinks": "相关链接",
+ "widget": {
+ "captions": [
+ "想要知道动态?",
+ "加入我们的 Discord!"
+ ],
+ "button": "现在加入!"
+ }
+ },
+ "progressPage": {
+ "title": "我们的进度",
+ "description": "检查项目进度和目标! (大约每小时更新一次,不反映所有项目目标或进度)"
+ },
+ "blogPage": {
+ "title": "博客",
+ "description": "压缩块的最新更新。如果您想看到更频繁的更新,请考虑在 Patreon 上支持我们。"
+ },
+ "localizationPage": {
+ "title": "让我们本地化",
+ "description": "粘贴指向可公开访问的 JSON 语言环境的链接以在网站上对其进行测试",
+ "instructions": "查看本地化说明",
+ "fileInput": "要测试的文件",
+ "filePlaceholder": "https://a.link.to/the_file.json",
+ "button": "测试文件"
+ },
+ "docs": {
+ "missingInLocale": "此页面在您的语言环境中不可用。请看下面的英文版本。",
+ "quickLinks": {
+ "header": "快速链接",
+ "links": [
+ {
+ "header": "安装 Pretendo",
+ "caption": "查看设置说明"
+ },
+ {
+ "header": "有错误吗?",
+ "caption": "在这里搜索"
+ }
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/public/assets/css/account.css b/public/assets/css/account.css
index 20a0cef..1b5a6b2 100644
--- a/public/assets/css/account.css
+++ b/public/assets/css/account.css
@@ -23,31 +23,37 @@
margin: 8px 0 4px;
}
.account-sidebar .user .username {
- margin: 0px;
+ margin: 0;
}
.account-sidebar .user .tier-name {
- margin: 0px;
+ margin: 12px 0;
line-height: 1.2em;
border-radius: 1.2em;
- border-width: 5px;
+ border-width: 2px;
border-style: solid;
+ padding: 4px 16px;
}
.account-sidebar .user .tier-level-0 {
- color:gray;
- border-color: gray;
+ background: #2a2f50;
+ color: var(--text-secondary);
+ border-color: #383f6b;
}
.account-sidebar .user .tier-level-1 {
- color:red;
- border-color: red;
+ background: rgba(255, 132, 132, 0.2);
+ color: #FF8484;
+ border-color: rgba(255, 132, 132, 0.8);
}
.account-sidebar .user .tier-level-2 {
- color:darkorange;
- border-color: darkorange;
+ background: rgba(89, 201, 165, 0.3);
+ color:#59c9a5;
+ border-color: #59c9a5;
}
.account-sidebar .user .tier-level-3 {
- color:gold;
- border-color: gold;
+ background: rgba(202, 177, 251, 0.3);
+ color:#cab1fb;
+ border-color: #cab1fb;
}
+
.account-sidebar .user .mii {
width: 128px;
height: 128px;
diff --git a/public/assets/css/aprilfools.css b/public/assets/css/aprilfools.css
index 4da6b07..4dbb2db 100644
--- a/public/assets/css/aprilfools.css
+++ b/public/assets/css/aprilfools.css
@@ -79,27 +79,6 @@
color: var(--text-secondary);
}
-@media screen and (max-width: 1064px) {
- .selected-locale .locale-names {
- display: none;
- }
-
- .selected-locale {
- width: 80px;
- margin-left: auto;
- margin-right: 12px;
- }
-
- .locale-dropdown {
- width: fit-content;
- }
-
- .select-box .options-container {
- width: 150px;
- right: 12px;
- }
-}
-
@media screen and (max-width: 946px) {
header nav a:not(.keep-on-mobile) {
display: none;
diff --git a/public/assets/css/dropdown.css b/public/assets/css/dropdown.css
index 1b0860d..3da4a25 100644
--- a/public/assets/css/dropdown.css
+++ b/public/assets/css/dropdown.css
@@ -1,6 +1,5 @@
.select-box {
display: flex;
- width: 188px;
flex-direction: column;
position: relative;
user-select: none;
@@ -12,7 +11,7 @@
.select-box .options-container {
max-height: 0;
- width: 100%;
+ width: fit-content;
opacity: 0;
transition: all 0.4s;
overflow: hidden;
@@ -20,34 +19,8 @@
background-color: var(--btn-secondary);
order: 1;
position: absolute;
- top: 50px;
-}
-
-.selected-locale {
- margin-bottom: 8px;
- position: relative;
- width: 188px;
- height: 45px;
- border-radius: 5px;
- display: flex;
- align-items: center;
- background-color: var(--btn-secondary);
- color: white;
- order: 0;
-}
-
-.selected-locale::after {
- content: "";
- width: 1.2rem;
- height: 1.2rem;
- background: url("/assets/images/down-arrow.svg");
- position: absolute;
- right: 15px;
- top: 50%;
- transition: transform 150ms;
- transform: translateY(-50%);
- background-size: contain;
- background-position: center;
+ top: 48px;
+ right: 0;
}
.select-box .option .item {
@@ -67,25 +40,21 @@
overflow-y: auto;
}
-.select-box .options-container.active + .selected-locale::after {
+.select-box .options-container.active + .locale-dropdown-toggle::after {
transform: translateY(-50%) rotateX(180deg);
}
.select-box .options-container::-webkit-scrollbar {
width: 8px;
- background: #0d141f;
- background: #81878f;
- background: #f1f2f3;
+ background: var(--btn-secondary);
border-radius: 0 5px 5px 0;
}
.select-box .options-container::-webkit-scrollbar-thumb {
- background: #525861;
- background: #81878f;
+ background: var(--text-secondary);
border-radius: 0 5px 5px 0;
}
-.select-box .option,
-.selected-locale {
+.select-box .option {
padding: 12px 15px;
cursor: pointer;
}
diff --git a/public/assets/css/main.css b/public/assets/css/main.css
index f65167b..87c0375 100644
--- a/public/assets/css/main.css
+++ b/public/assets/css/main.css
@@ -94,10 +94,108 @@ header nav a:hover {
transition: color 50ms ease-in-out;
}
-.locale-dropdown {
+header .right-section {
+ display: grid;
+ grid-auto-flow: column;
+ grid-gap: 24px;
margin-left: auto;
z-index: 2;
- height: 45px;
+ color: var(--text-secondary);
+}
+
+header .locale-dropdown-toggle {
+ width: fit-content;
+ height: 24px;
+ padding: 0;
+ margin: auto;
+ transition: color 150ms;
+ cursor: pointer;
+}
+header .locale-dropdown-toggle:hover,
+header .locale-dropdown-toggle.active {
+ color: var(--text);
+}
+
+header .user-widget-wrapper {
+ height: 24px;
+}
+
+header .user-widget-wrapper a.login-link {
+ color: var(--text-secondary);
+ text-decoration: none;
+ transition: color 150ms;
+}
+header .user-widget-wrapper a.login-link:hover {
+ color: var(--text);
+}
+
+header .user-widget-wrapper.logged-in {
+ position: relative;
+ width: 32px;
+ height: 32px;
+}
+header .user-widget-wrapper.logged-in .user-widget-toggle {
+ width: 32px;
+ height: 32px;
+ background: var(--text-secondary-2);
+ border-radius: 50%;
+ overflow: hidden;
+ cursor: pointer;
+}
+header .user-widget-wrapper .user-widget-toggle img,
+header .user-widget .user-avatar img {
+ width: 100%;
+ height: 100%;
+}
+header .user-widget {
+ max-height: 0;
+ overflow: hidden;
+
+ box-sizing: border-box;
+ transition: max-height 300ms, padding 200ms, opacity 150ms;
+
+ position: absolute;
+ right: 0;
+ top: 48px;
+ padding: 0;
+ background: #2a2f50;
+ border-radius: 8px;
+ text-align: center;
+ opacity: 0;
+
+ box-shadow: 0 0 10px -2px #111531;
+}
+header .user-widget.active {
+ max-height: 100vh;
+ padding: 36px;
+ opacity: 1;
+}
+header .user-widget .user-avatar {
+ width: 128px;
+ height: 128px;
+ margin: auto;
+ background: var(--text-secondary-2);
+ border-radius: 50%;
+ overflow: hidden;
+}
+header .user-widget .user-info {
+ margin-top: 12px;
+}
+header .user-widget .user-info .mii-name {
+ color: var(--text);
+}
+header .user-widget .buttons {
+ margin-top: 12px;
+}
+header .user-widget .button {
+ margin-top: 12px;
+ width: 100%;
+ padding: 8px 60px;
+ cursor: pointer;
+}
+header .user-widget .button.logout {
+ background: #383f6b;
+ color: var(--text);
}
/* Misc */
@@ -693,6 +791,44 @@ section.update-signup div.circle {
}
/* Progress */
+
+.donation-progress {
+ padding: 50px 20px;
+ border-radius: 10px;
+ background: #151934;
+ grid-column: span 2;
+}
+.donation-progress h1 {
+ display: inline-block;
+ margin: 0;
+}
+.donation-progress span {
+ font-weight: bold;
+}
+.donation-progress a {
+ color: #9d6ff3;
+ text-decoration: none;
+ font-weight: bold;
+}
+.donation-progress a:hover {
+ text-decoration: underline;
+}
+
+.progress-bar {
+ position: relative;
+ display: block;
+ width: 100%;
+ height: 12px;
+ margin: 1rem 0;
+ border-radius: 6px;
+ background: var(--btn-secondary);
+ overflow: hidden;
+}
+.progress-bar-inner {
+ height: 100%;
+ background-color: var(--theme);
+}
+
.all-progress-lists {
margin-top: 50px;
display: grid;
@@ -928,6 +1064,9 @@ footer div.discord-server-card svg {
.feature-list-wrapper {
grid-template-columns: 100%;
}
+ .donation-progress {
+ grid-column: span 1;
+ }
header nav a:not(.keep-on-mobile) {
/* You don't really need it on mobile IMO */
@@ -1118,25 +1257,6 @@ footer div.discord-server-card svg {
margin: auto !important;
}
- .selected-locale .locale-names {
- display: none;
- }
-
- .selected-locale {
- width: 80px;
- margin-left: auto;
- margin-right: 12px;
- }
-
- .locale-dropdown {
- width: fit-content;
- }
-
- .select-box .options-container {
- width: 150px;
- right: 12px;
- }
-
footer {
grid-template-columns: 1fr;
grid-template-rows: repeat(4, fit-content(100%));
@@ -1168,13 +1288,3 @@ footer div.discord-server-card svg {
margin: 0 10px;
}
}
-
-@media screen and (max-width: 330px) {
- .locale-dropdown .selected-locale {
- width: 50px;
- }
- .locale-dropdown .selected-locale::after {
- display: none;
- }
-
-}
diff --git a/public/assets/css/upgrade.css b/public/assets/css/upgrade.css
index a2757b4..2e0229d 100644
--- a/public/assets/css/upgrade.css
+++ b/public/assets/css/upgrade.css
@@ -57,8 +57,32 @@ h1.title {
color: var(--text);
}
p.caption {
+ width: min(100%, 500px);
+ margin: 0 auto 36px;
+}
+
+.account-form-wrapper .progress-bar-wrapper {
+ justify-content: center;
width: min(100%, 500px);
margin: 0 auto 72px;
+ padding: 24px;
+ border-radius: 6px;
+ background: #1b1f3b;
+ box-sizing: border-box;
+}
+.account-form-wrapper .progress-bar-wrapper p {
+ text-align: left;
+ margin-bottom: 0;
+}
+.account-form-wrapper .progress-bar-wrapper p span {
+ color: var(--text);
+ font-weight: 600;
+}
+
+.account-form-wrapper .progress-bar {
+ height: 8px;
+ border-radius: 4px;
+ margin-top: 0;
}
form {
diff --git a/public/assets/js/header-handler.js b/public/assets/js/header-handler.js
new file mode 100644
index 0000000..263de63
--- /dev/null
+++ b/public/assets/js/header-handler.js
@@ -0,0 +1,85 @@
+/* eslint-disable no-undef, no-unused-vars */
+
+// Account widget handler
+const userWidgetToggle = document.querySelector('.user-widget-toggle') ;
+const userWidget = document.querySelector('.user-widget');
+
+// Open widget on click, close locale dropdown
+userWidgetToggle?.addEventListener('click', () => {
+ userWidget.classList.toggle('active');
+ localeOptionsContainer.classList.toggle('active');
+ localeDropdownToggle.classList.toggle('active');
+});
+
+// Locale dropdown handler
+function localeDropdownHandler(selectedLocale) {
+ document.cookie = `preferredLocale=${selectedLocale};max-age=31536000`;
+ window.location.reload();
+}
+
+const localeDropdown = document.querySelector(
+ '.locale-dropdown[data-dropdown]'
+);
+const localeDropdownOptions = document.querySelectorAll(
+ '.locale-dropdown[data-dropdown] .options-container'
+);
+const localeDropdownToggle = document.querySelector('.locale-dropdown-toggle');
+
+const localeOptionsContainer = localeDropdown.querySelector('.options-container');
+const localeOptionsList = localeDropdown.querySelectorAll('.option');
+
+// click dropdown element will open dropdown
+localeDropdownToggle.addEventListener('click', () => {
+ localeOptionsContainer.classList.toggle('active');
+ localeDropdownToggle.classList.toggle('active');
+});
+
+// clicking on any option will close dropdown and change value
+localeOptionsList.forEach((option) => {
+ option.addEventListener('click', () => {
+ localeDropdownToggle.classList.remove('active');
+ localeOptionsContainer.classList.remove('active');
+ const selectedLocale = option.querySelector('label').getAttribute('for');
+ localeDropdownHandler(selectedLocale);
+ });
+});
+
+// close all dropdowns on scroll
+document.addEventListener('scroll', () => {
+ localeDropdownOptions.forEach((el) => el.classList.remove('active'));
+ localeDropdownToggle.classList.remove('active');
+
+ userWidget.classList.remove('active');
+});
+
+// click outside of dropdown will close all dropdowns
+document.addEventListener('click', (e) => {
+ const targetElement = e.target;
+
+ let found = false;
+ if (
+ localeDropdown == targetElement ||
+ localeDropdown.contains(targetElement)
+ ) {
+ found = true;
+ userWidget.classList.remove('active');
+ }
+
+ if (
+ userWidget == targetElement ||
+ userWidget.contains(targetElement) ||
+ userWidgetToggle == targetElement ||
+ userWidgetToggle.contains(targetElement)
+ ) {
+ found = true;
+ localeDropdownToggle.classList.remove('active');
+ localeOptionsContainer.classList.remove('active');
+ }
+
+ if (found) return;
+
+ // click outside of dropdowns
+ userWidget.classList.remove('active');
+ localeDropdownToggle.classList.remove('active');
+ localeOptionsContainer.classList.remove('active');
+});
diff --git a/public/assets/js/locale-dropdown-handler.js b/public/assets/js/locale-dropdown-handler.js
deleted file mode 100644
index bebec0a..0000000
--- a/public/assets/js/locale-dropdown-handler.js
+++ /dev/null
@@ -1,64 +0,0 @@
-/* eslint-disable no-undef, no-unused-vars */
-function setDefaultDropdownLocale(localeString) {
- try {
- const selected = document.querySelector('.selected-locale');
- let item = document.querySelector(`label[for=${localeString}`);
- if (!item) { // if locale can't be found, default to en-US
- localeString = 'en-US';
- item = document.querySelector(`label[for=${localeString}`);
- }
- selected.innerHTML = item.innerHTML;
- } catch (e) {} // If it errors it's probably because there isn't a navbar in the view
-}
-
-function localeDropdownHandler(selectedLocale) {
- document.cookie = `preferredLocale=${selectedLocale};max-age=31536000`;
- window.location.reload();
-}
-
-const dropdowns = document.querySelectorAll('*[data-dropdown]');
-const dropdownOptions = document.querySelectorAll(
- '*[data-dropdown] .options-container'
-);
-
-dropdowns.forEach((el) => {
- const selected = el.querySelector('.selected-locale');
- const optionsContainer = el.querySelector('.options-container');
- const optionsList = el.querySelectorAll('.option');
-
- // click dropdown element will open dropdown
- selected.addEventListener('click', () => {
- optionsContainer.classList.toggle('active');
- });
-
- // clicking on any option will close dropdown and change value
- optionsList.forEach((option) => {
- option.addEventListener('click', () => {
- selected.innerHTML = option.querySelector('label').innerHTML;
- optionsContainer.classList.remove('active');
- const selectedLocale = option.querySelector('label').getAttribute('for');
- localeDropdownHandler(selectedLocale);
- });
- });
-});
-
-// close all dropdowns on scroll
-document.addEventListener('scroll', () => {
- dropdownOptions.forEach((el) => el.classList.remove('active'));
-});
-
-// click outside of dropdown will close all dropdowns
-document.addEventListener('click', (e) => {
- const targetElement = e.target;
-
- // check if target is from a dropdown
- let found = false;
- dropdowns.forEach((v) => {
- if (v == targetElement || v.contains(targetElement)) found = true;
- });
-
- if (found) return;
-
- // click outside of dropdowns
- dropdownOptions.forEach((el) => el.classList.remove('active'));
-});
diff --git a/src/cache.js b/src/cache.js
index 842795d..a7c343b 100644
--- a/src/cache.js
+++ b/src/cache.js
@@ -13,9 +13,18 @@ let stripeDonationCache;
async function getTrelloCache() {
const available = await trelloAPIAvailable();
if (!available) {
- return {
- update_time: Date.now(),
- sections: []
+ return trelloCache || {
+ update_time: 0,
+ sections: [{
+ title: 'Upstream API error',
+ id: '',
+ percentage_complete: '',
+ progress: {
+ not_started: [ 'Trello API unavailable' ],
+ started: [],
+ completed: []
+ }
+ }],
};
}
@@ -83,7 +92,7 @@ async function updateTrelloCache() {
async function trelloAPIAvailable() {
const { status } = await got('https://trello.status.atlassian.com/api/v2/status.json').json();
- return status.description === 'All Systems Operational';
+ return status.indicator !== 'major' && status.indicator !== 'critical';
}
async function getStripeDonationCache() {
@@ -128,6 +137,9 @@ async function updateStripeDonationCache() {
hasMore = has_more;
}
+ donationCache.goal_dollars = donationCache.goal / 100;
+ donationCache.total_dollars = donationCache.total / 100;
+
donationCache.completed_real = Math.floor((donationCache.total / donationCache.goal) * 100); // real completion amount
donationCache.completed_capped = Math.max(0, Math.min(donationCache.completed_real, 100)); // capped at 100
diff --git a/src/routers/account.js b/src/routers/account.js
index 8972573..202ebd5 100644
--- a/src/routers/account.js
+++ b/src/routers/account.js
@@ -100,6 +100,7 @@ router.get('/', async (request, response) => {
renderData.tierLevel = pnid.get('connections.stripe.tier_level');
renderData.account = account;
renderData.isTester = account.access_level > 0;
+ renderData.isLoggedIn = request.cookies.access_token && request.cookies.refresh_token && request.cookies.ph;
// Check if a Discord account is linked to the PNID
if (account.connections.discord.id && account.connections.discord.id.trim() !== '') {
@@ -117,7 +118,7 @@ router.get('/', async (request, response) => {
});
} catch (error) {
renderData.error = 'Invalid Discord refresh token. Remove account and relink';
- response.render('account/account', renderData);
+ return response.render('account/account', renderData);
}
// TODO: Add a dedicated endpoint for updating connections?
@@ -197,6 +198,8 @@ router.get('/', async (request, response) => {
renderData.discordAuthURL = discordAuthURL;
}
+ renderData.isLoggedIn = request.cookies.access_token && request.cookies.refresh_token && request.cookies.ph;
+
response.render('account/account', renderData);
});
@@ -208,6 +211,8 @@ router.get('/login', async (request, response) => {
error: request.cookies.error
};
+ renderData.redirect = request.query.redirect;
+
response.clearCookie('error', { domain: '.pretendo.network' });
response.render('account/login', renderData);
@@ -215,6 +220,7 @@ router.get('/login', async (request, response) => {
router.post('/login', async (request, response) => {
const { username, password } = request.body;
+ let { redirect } = request.body;
let apiResponse = await util.apiPostGetRequest('/v1/login', {}, {
username,
@@ -249,7 +255,11 @@ router.post('/login', async (request, response) => {
response.cookie('ph', encryptedBody.toString('hex'), { domain: '.pretendo.network' });
- response.redirect('/account');
+ if (!redirect.startsWith('/')) {
+ redirect = null;
+ }
+
+ response.redirect(redirect || '/account');
});
router.get('/register', async (request, response) => {
@@ -268,11 +278,19 @@ router.get('/register', async (request, response) => {
response.clearCookie('username', { domain: '.pretendo.network' });
response.clearCookie('mii_name', { domain: '.pretendo.network' });
+ let redirect = request.query.redirect;
+ if (!redirect.startsWith('/')) {
+ redirect = null;
+ }
+
+ renderData.redirect = redirect;
+
response.render('account/register', renderData);
});
router.post('/register', async (request, response) => {
const { email, username, mii_name, password, password_confirm, 'h-captcha-response': hCaptchaResponse } = request.body;
+ let { redirect } = request.body;
response.cookie('email', email, { domain: '.pretendo.network' });
response.cookie('username', username, { domain: '.pretendo.network' });
@@ -298,7 +316,20 @@ router.post('/register', async (request, response) => {
response.clearCookie('username', { domain: '.pretendo.network' });
response.clearCookie('mii_name', { domain: '.pretendo.network' });
- response.redirect('/account');
+ if (!redirect.startsWith('/')) {
+ redirect = null;
+ }
+
+ response.redirect(redirect || '/account');
+});
+
+router.get('/logout', async(_request, response) => {
+ response.clearCookie('refresh_token', { domain: '.pretendo.network' });
+ response.clearCookie('access_token', { domain: '.pretendo.network' });
+ response.clearCookie('token_type', { domain: '.pretendo.network' });
+ response.clearCookie('ph', { domain: '.pretendo.network' });
+
+ response.redirect('/');
});
router.get('/connect/discord', async (request, response) => {
@@ -375,7 +406,7 @@ router.get('/online-files', async (request, response) => {
// Verify the user is logged in
if (!request.cookies.access_token || !request.cookies.refresh_token|| !request.cookies.ph) {
- return response.redirect('/account/login');
+ return response.redirect('/account/login?redirect=/online-files');
}
// Attempt to get user data
@@ -519,7 +550,7 @@ router.get('/miieditor', async (request, response) => {
router.get('/upgrade', async (request, response) => {
// Verify the user is logged in
if (!request.cookies.access_token || !request.cookies.refresh_token || !request.cookies.ph) {
- return response.redirect('/account/login');
+ return response.redirect('/account/login?redirect=/account/upgrade');
}
// Attempt to get user data
diff --git a/src/routers/aprilfools.js b/src/routers/aprilfools.js
index c44c745..818070f 100644
--- a/src/routers/aprilfools.js
+++ b/src/routers/aprilfools.js
@@ -4,14 +4,20 @@ const router = new Router();
router.get('/', async (request, response) => {
- const reqLocale = request.locale;
- const locale = util.getLocale(reqLocale.region, reqLocale.language);
-
- response.render('aprilfools', {
+ const renderData = {
layout: 'main',
- locale,
- localeString: reqLocale.toString(),
- });
+ locale: util.getLocale(request.locale.region, request.locale.language),
+ localeString: request.locale.toString(),
+ };
+
+ renderData.isLoggedIn = request.cookies.access_token && request.cookies.refresh_token && request.cookies.ph;
+
+ if (renderData.isLoggedIn) {
+ const account = await util.getAccount(request, response);
+ renderData.account = account;
+ }
+
+ response.render('aprilfools', renderData);
});
module.exports = router;
diff --git a/src/routers/blog.js b/src/routers/blog.js
index c35afc8..cf1528a 100644
--- a/src/routers/blog.js
+++ b/src/routers/blog.js
@@ -36,18 +36,21 @@ const postList = () => {
};
router.get('/', async (request, response) => {
-
- const reqLocale = request.locale;
- const locale = util.getLocale(reqLocale.region, reqLocale.language);
-
- const localeString = reqLocale.toString();
-
- response.render('blog/blog', {
+ const renderData = {
layout: 'main',
- locale,
- localeString,
- postList
- });
+ locale: util.getLocale(request.locale.region, request.locale.language),
+ localeString: request.locale.toString(),
+ postList,
+ };
+
+ renderData.isLoggedIn = request.cookies.access_token && request.cookies.refresh_token && request.cookies.ph;
+
+ if (renderData.isLoggedIn) {
+ const account = await util.getAccount(request, response);
+ renderData.account = account;
+ }
+
+ response.render('blog/blog', renderData);
});
// RSS feed
@@ -69,10 +72,19 @@ router.get('/feed.xml', async (request, response) => {
router.get('/:slug', async (request, response, next) => {
- const reqLocale = request.locale;
- const locale = util.getLocale(reqLocale.region, reqLocale.language);
+ const renderData = {
+ layout: 'blog-opengraph',
+ locale: util.getLocale(request.locale.region, request.locale.language),
+ localeString: request.locale.toString(),
+ postList,
+ };
- const localeString = reqLocale.toString();
+ renderData.isLoggedIn = request.cookies.access_token && request.cookies.refresh_token && request.cookies.ph;
+
+ if (renderData.isLoggedIn) {
+ const account = await util.getAccount(request, response);
+ renderData.account = account;
+ }
// Get the name of the post from the URL
const postName = request.params.slug;
@@ -89,6 +101,7 @@ router.get('/:slug', async (request, response, next) => {
// Convert the post info into JSON and separate it and the content
// eslint-disable-next-line prefer-const
let { data: postInfo, content } = matter(rawPost);
+ renderData.postInfo = postInfo;
// Replace [yt-iframe](videoID) with the full
content = content
@@ -97,14 +110,9 @@ router.get('/:slug', async (request, response, next) => {
// Convert the content into HTML
const htmlPost = marked.parse(content);
+ renderData.htmlPost = htmlPost;
- response.render('blog/blogpost', {
- layout: 'blog-opengraph',
- locale,
- localeString,
- postInfo,
- htmlPost,
- });
+ response.render('blog/blogpost', renderData);
});
module.exports = router;
diff --git a/src/routers/docs.js b/src/routers/docs.js
index c39205c..56ae3ca 100644
--- a/src/routers/docs.js
+++ b/src/routers/docs.js
@@ -11,32 +11,45 @@ router.get('/', async (request, response) => {
});
router.get('/search', async (request, response) => {
- const reqLocale = request.locale;
- const locale = util.getLocale(reqLocale.region, reqLocale.language);
-
- const localeString = reqLocale.toString();
-
- response.render('docs/search', {
+ const renderData = {
layout: 'main',
- locale,
- localeString,
+ locale: util.getLocale(request.locale.region, request.locale.language),
+ localeString: request.locale.toString(),
currentPage: request.params.slug,
- });
+ };
+
+ renderData.isLoggedIn = request.cookies.access_token && request.cookies.refresh_token && request.cookies.ph;
+
+ if (renderData.isLoggedIn) {
+ const account = await util.getAccount(request, response);
+ renderData.account = account;
+ }
+
+ response.render('docs/search', renderData);
});
router.get('/:slug', async (request, response, next) => {
- const reqLocale = request.locale;
- const locale = util.getLocale(reqLocale.region, reqLocale.language);
+ const renderData = {
+ layout: 'main',
+ locale: util.getLocale(request.locale.region, request.locale.language),
+ localeString: request.locale.toString(),
+ currentPage: request.params.slug,
+ };
- const localeString = reqLocale.toString();
+ renderData.isLoggedIn = request.cookies.access_token && request.cookies.refresh_token && request.cookies.ph;
+
+ if (renderData.isLoggedIn) {
+ const account = await util.getAccount(request, response);
+ renderData.account = account;
+ }
// Get the name of the page from the URL
const pageName = request.params.slug;
- let markdownLocale = localeString;
+ let markdownLocale = renderData.localeString;
let missingInLocale = false;
// Check if the MD file exists in the user's locale, if not try en-US and show notice, or finally log error and show 404.
- if (fs.existsSync(path.join('docs', localeString, `${pageName}.md`))) {
+ if (fs.existsSync(path.join('docs', renderData.localeString, `${pageName}.md`))) {
null;
} else if (fs.existsSync(path.join('docs', 'en-US', `${pageName}.md`))) {
markdownLocale = 'en-US';
@@ -45,6 +58,7 @@ router.get('/:slug', async (request, response, next) => {
next();
return;
}
+ renderData.missingInLocale = missingInLocale;
let content;
// Get the markdown file corresponding to the page.
@@ -57,22 +71,16 @@ router.get('/:slug', async (request, response, next) => {
// Convert the content into HTML
content = marked.parse(content);
+ renderData.content = content;
// A boolean to show the quick links grid or not.
let showQuickLinks = false;
if (pageName === 'welcome') {
showQuickLinks = true;
}
+ renderData.showQuickLinks = showQuickLinks;
- response.render('docs/docs', {
- layout: 'main',
- locale,
- localeString,
- content,
- currentPage: request.params.slug,
- missingInLocale,
- showQuickLinks
- });
+ response.render('docs/docs', renderData);
});
module.exports = router;
diff --git a/src/routers/home.js b/src/routers/home.js
index 58025f3..f2f61e0 100644
--- a/src/routers/home.js
+++ b/src/routers/home.js
@@ -7,15 +7,26 @@ const { getTrelloCache } = require('../cache');
router.get('/', async (request, response) => {
- const reqLocale = request.locale;
- const locale = util.getLocale(reqLocale.region, reqLocale.language);
-
+ const renderData = {
+ layout: 'main',
+ boards,
+ locale: util.getLocale(request.locale.region, request.locale.language),
+ localeString: request.locale.toString(),
+ };
+
+ renderData.isLoggedIn = request.cookies.access_token && request.cookies.refresh_token && request.cookies.ph;
+
+ if (renderData.isLoggedIn) {
+ const account = await util.getAccount(request, response);
+ renderData.account = account;
+ }
+
const cache = await getTrelloCache();
// Builds the arrays of people for the special thanks section
// Shuffles the special thanks people
- let specialThanksPeople = locale.specialThanks.people.slice();
+ const specialThanksPeople = renderData.locale.specialThanks.people.slice();
function shuffleArray(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
@@ -29,7 +40,7 @@ router.get('/', async (request, response) => {
const specialThanksSecondRow = specialThanksPeople.slice(3, 7);
// Builds the final array to be sent to the view, and triples each row.
- specialThanksPeople = {
+ renderData.specialThanksPeople = {
first: specialThanksFirstRow.concat(specialThanksFirstRow).concat(specialThanksFirstRow),
second: specialThanksSecondRow.concat(specialThanksSecondRow).concat(specialThanksSecondRow)
};
@@ -74,14 +85,9 @@ router.get('/', async (request, response) => {
// Calculates global completion percentage
totalProgress.percent = Math.round(totalProgress._calc.percentageSum / cache.sections.length * 100);
- response.render('home', {
- layout: 'main',
- featuredFeatureList: totalProgress,
- boards,
- locale,
- localeString: reqLocale.toString(),
- specialThanksPeople
- });
+ renderData.featuredFeatureList = totalProgress;
+
+ response.render('home', renderData);
});
module.exports = router;
diff --git a/src/routers/localization.js b/src/routers/localization.js
index 3de3365..c4fee3c 100644
--- a/src/routers/localization.js
+++ b/src/routers/localization.js
@@ -3,15 +3,20 @@ const util = require('../util');
const router = new Router();
router.get('/', async (request, response) => {
-
- const reqLocale = request.locale;
- const locale = util.getLocale(reqLocale.region, reqLocale.language);
-
- response.render('localization', {
+ const renderData = {
layout: 'main',
- locale,
- localeString: reqLocale.toString(),
- });
+ locale: util.getLocale(request.locale.region, request.locale.language),
+ localeString: request.locale.toString(),
+ };
+
+ renderData.isLoggedIn = request.cookies.access_token && request.cookies.refresh_token && request.cookies.ph;
+
+ if (renderData.isLoggedIn) {
+ const account = await util.getAccount(request, response);
+ renderData.account = account;
+ }
+
+ response.render('localization', renderData);
});
module.exports = router;
diff --git a/src/routers/progress.js b/src/routers/progress.js
index 4aac956..4849321 100644
--- a/src/routers/progress.js
+++ b/src/routers/progress.js
@@ -3,22 +3,29 @@ const util = require('../util');
const { boards } = require('../../boards/boards.json');
const router = new Router();
-const { getTrelloCache } = require('../cache');
+const { getTrelloCache, getStripeDonationCache } = require('../cache');
router.get('/', async (request, response) => {
-
- const reqLocale = request.locale;
- const locale = util.getLocale(reqLocale.region, reqLocale.language);
-
- const cache = await getTrelloCache();
-
- response.render('progress', {
+ const renderData = {
layout: 'main',
boards,
- locale,
- localeString: reqLocale.toString(),
- progressLists: cache
- });
+ locale: util.getLocale(request.locale.region, request.locale.language),
+ localeString: request.locale.toString(),
+ };
+
+ renderData.isLoggedIn = request.cookies.access_token && request.cookies.refresh_token && request.cookies.ph;
+
+ if (renderData.isLoggedIn) {
+ const account = await util.getAccount(request, response);
+ renderData.account = account;
+ }
+
+ const trelloCache = await getTrelloCache();
+ renderData.progressLists = trelloCache;
+ const stripeDonationCache = await getStripeDonationCache();
+ renderData.donationCache = stripeDonationCache;
+
+ response.render('progress', renderData);
});
module.exports = router;
diff --git a/src/server.js b/src/server.js
index 9668b1e..4e42444 100644
--- a/src/server.js
+++ b/src/server.js
@@ -46,6 +46,7 @@ app.use(expressLocale({
/* TODO: map more regions to the available locales */
en: 'en-US', 'en-GB': 'en-US', 'en-AU': 'en-US', 'en-CA': 'en-US',
ar: 'ar-AR',
+ cn: 'zh-CN',
de: 'de-DE',
nl: 'nl-NL',
es: 'es-ES',
@@ -59,11 +60,12 @@ app.use(expressLocale({
pt: 'pt-BR',
ro: 'ro-RO',
ru: 'ru-RU',
- tr: 'tr-TR'
+ tr: 'tr-TR',
},
allowed: [
'en', 'en-US', 'en-GB', 'en-AU', 'en-CA',
'ar', 'ar-AR',
+ 'cn', 'zh-CN', 'zh-HK', 'zh-TW',
'de', 'de-DE',
'nl', 'nl-NL',
'es', 'es-ES',
diff --git a/src/util.js b/src/util.js
index b911da9..0394c6c 100644
--- a/src/util.js
+++ b/src/util.js
@@ -255,6 +255,46 @@ async function handleStripeEvent(event) {
}
}
+async function getAccount(request, response) {
+ // Attempt to get user data
+ let apiResponse = await apiGetRequest('/v1/user', {
+ 'Authorization': `${request.cookies.token_type} ${request.cookies.access_token}`
+ });
+
+ if (apiResponse.statusCode !== 200) {
+ // Assume expired, refresh and retry request
+ apiResponse = await apiPostGetRequest('/v1/login', {}, {
+ refresh_token: request.cookies.refresh_token,
+ grant_type: 'refresh_token'
+ });
+
+ if (apiResponse.statusCode !== 200) {
+ // TODO: Error message
+ return response.status(apiResponse.statusCode).json({
+ error: 'Bad'
+ });
+ }
+
+ const tokens = apiResponse.body;
+
+ apiResponse = await apiGetRequest('/v1/user', {
+ 'Authorization': `${tokens.token_type} ${tokens.access_token}`
+ });
+ }
+
+ // If still failed, something went horribly wrong
+ if (apiResponse.statusCode !== 200) {
+ // TODO: Error message
+ return response.status(apiResponse.statusCode).json({
+ error: 'Bad'
+ });
+ }
+
+ // Return user account info
+ const account = apiResponse.body;
+ return account;
+}
+
module.exports = {
fullUrl,
getLocale,
@@ -262,5 +302,6 @@ module.exports = {
apiPostGetRequest,
apiDeleteGetRequest,
nintendoPasswordHash,
- handleStripeEvent
+ handleStripeEvent,
+ getAccount
};
\ No newline at end of file
diff --git a/views/account/login.handlebars b/views/account/login.handlebars
index 2a27ba7..9507a36 100644
--- a/views/account/login.handlebars
+++ b/views/account/login.handlebars
@@ -18,9 +18,10 @@
Forgot your password?
+
Subscribing to a tier gives you access to our developer chats and, depending on the tier, - to our beta servers. Please someone reword this, it sucks. No fr.
++ Reaching the monthly goal will make Pretendo a full time job, providing better quality updates at a faster rate. +
+ +