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