Merge pull request #121 from PretendoNetwork/feature-stripe-tiers-styling

add user popout in navbar, add redirect-after-login, add donation goal progressbar, style tier names in account page, fix trello and discord bugs
This commit is contained in:
Jonathan Barrow
2022-07-10 07:30:13 -04:00
committed by GitHub
27 changed files with 905 additions and 365 deletions

244
locales/CN_zh.json Normal file
View File

@@ -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 一起使用。<br>Pretendo 目前不支持 Cemu。"
},
{
"question": "如果我在 Nintendo Network 上被封账号,我在使用 Pretendo 时还会被禁吗?",
"answer": "我们将无法访问 Nintendo Network 的禁令,并且不会禁止所有用户使用我们的服务。但是,我们在使用服务时将遵守规则,不遵守这些规则可能会导致被禁止。"
},
{
"question": "Pretendo 会支持 Wii/Switch 吗?",
"answer": "Wii 已经有 <a href=\"https://wiimmfi.de/\" target=\"_blank\">Wiimmfi</a> 提供的自定义服务器。我们目前不希望以 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": "压缩块的最新更新。如果您想看到更频繁的更新,请考虑在 <a href=\"https://www.patreon.com/PretendoNetwork\" target=\"_blank\">Patreon</a> 上支持我们。"
},
"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": "在这里搜索"
}
]
}
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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 {

View File

@@ -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');
});

View File

@@ -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'));
});

View File

@@ -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

View File

@@ -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

View File

@@ -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;

View File

@@ -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 <iframe />
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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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',

View File

@@ -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
};

View File

@@ -18,9 +18,10 @@
<a href="/account/passwordreset" class="pwdreset">Forgot your password?</a>
</div>
<input name="grant_type" id="grant_type" type="hidden" value="password">
<input name="redirect" id="redirect" type="hidden" value="{{redirect}}">
<div class="buttons">
<button type="submit">Login</button>
<a href="/account/register" class="register">Don't have an account?</a>
<a href="/account/register{{#if redirect}}?redirect={{redirect}}{{/if}}" class="register">Don't have an account?</a>
</div>
</form>
</div>

View File

@@ -30,9 +30,10 @@
<input name="password_confirm" id="password_confirm" type="password" autocomplete="new-password" required>
</div>
<div class="h-captcha" data-sitekey="cf3fd74e-93ca-47e6-9fa0-5fc439de06d4"></div>
<input name="redirect" id="redirect" type="hidden" value="{{redirect}}">
<div class="buttons">
<button type="submit">Register</button>
<a href="/account/login" class="register">Already have an account?</a>
<a href="/account/login{{#if redirect}}?redirect={{redirect}}{{/if}}" class="register">Already have an account?</a>
</div>
</form>
</div>

View File

@@ -33,8 +33,18 @@
</a>
<h1 class="title dot">Upgrade</h1>
<p class="caption">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.</p>
<p class="caption">
Reaching the monthly goal will make Pretendo a full time job, providing better quality updates at a faster rate.
</p>
<div class="progress-bar-wrapper">
<div class="progress-bar">
<div class="progress-bar-inner" style="width: {{ donationCache.completed_capped }}%;" ></div>
</div>
<p>
<span>${{donationCache.total_dollars}}</span> of <span>${{donationCache.goal_dollars}}</span>/month, <span>${{donationCache.completed_real}}%</span> of the monthly goal.
</p>
</div>
<form method="post" data-current-tier="{{currentTier}}">
{{#each tiers}}
<input type="radio" class="tier-radio" data-tier-name="{{this.name}}" name="tier" value="{{this.price_id}}" id="{{this.price_id}}" />

View File

@@ -26,9 +26,9 @@
<svg data-prefix="fab" data-icon="github" class="svg-inline--fa fa-github fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512"><path fill="currentColor" d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z"></path></svg>
</button>
</a>
<a href="https://patreon.com/PretendoNetwork/" target="_blank">
<a href="/account/upgrade" target="_blank">
<button class="button secondary github icon-btn">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path fill="currentColor" d="M512 194.8c0 101.3-82.4 183.8-183.8 183.8-101.7 0-184.4-82.4-184.4-183.8 0-101.6 82.7-184.3 184.4-184.3C429.6 10.5 512 93.2 512 194.8zM0 501.5h90v-491H0v491z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-heart"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path></svg>
</button>
</a>
</div>

View File

@@ -70,7 +70,6 @@
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.5.1/dist/chart.min.js"></script>
<script src="/assets/js/progress-charts.js"></script>
<script src="/assets/js/locale-dropdown-handler.js"></script>
<script>setDefaultDropdownLocale("{{localeString}}")</script>
</body>
</html>

View File

@@ -67,8 +67,6 @@
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.5.1/dist/chart.min.js"></script>
<script src="/assets/js/progress-charts.js"></script>
<script src="/assets/js/locale-dropdown-handler.js"></script>
<script>setDefaultDropdownLocale("{{localeString}}")</script>
</body>
</html>

View File

@@ -28,8 +28,6 @@
<div>
<h1>{{ localeHelper locale "footer" "socials" }}</h1>
<a href="https://twitter.com/PretendoNetwork/" target="_blank">Twitter</a>
<a href="https://patreon.com/PretendoNetwork" target="_blank">Patreon</a>
<a href="https://ko-fi.com/PretendoNetwork" target="_blank">Ko-fi</a>
<a href="https://invite.gg/pretendo" target="_blank">Discord</a>
<a href="https://github.com/PretendoNetwork" target="_blank">GitHub</a>
</div>
@@ -39,7 +37,7 @@
<a href="/#faq">{{ localeHelper locale "nav" "faq" }}</a>
<a href="/progress">{{ localeHelper locale "nav" "progress" }}</a>
<a href="/blog">{{ localeHelper locale "nav" "blog" }}</a>
<a href="/account" style="display: none; /* Hiding for now since it's not ready */">{{ localeHelper locale "nav" "account" }}</a>
<a href="/account">{{ localeHelper locale "nav" "account" }}</a>
</div>
<div class="discord-server-card">
<h1>{{ localeHelper locale "footer" "widget" "captions" 0 }}</h1>

View File

@@ -33,101 +33,145 @@
<a href="/blog" class="keep-on-mobile">{{ localeHelper locale "nav" "blog" }}</a>
</nav>
<!-- Ordered the locales in the same way Google orders them -->
<div class="select-box locale-dropdown" data-dropdown>
<div class="options-container">
<div class="option">
<input type="radio" class="radio" id="en-US" name="category" />
<label for="en-US">
<div class="item"><span class="lang">🇺🇸</span><span class="locale-names">English</span></div>
</label>
<div class="right-section">
<!-- Ordered the locales in the same way Google orders them -->
<div class="select-box locale-dropdown" data-dropdown>
<div class="options-container">
<div class="option">
<input type="radio" class="radio" id="en-US" name="category" />
<label for="en-US">
<div class="item"><span class="locale-names">English</span></div>
</label>
</div>
<div class="option">
<input type="radio" class="radio" id="de-DE" name="category" />
<label for="de-DE">
<div class="item"><span class="locale-names">Deutsch</span></div>
</label>
</div>
<div class="option">
<input type="radio" class="radio" id="es-ES" name="category" />
<label for="es-ES">
<div class="item"><span class="locale-names">Español</span></div>
</label>
</div>
<div class="option">
<input type="radio" class="radio" id="fr-FR" name="category" />
<label for="fr-FR">
<div class="item"><span class="locale-names">Français</span></div>
</label>
</div>
<div class="option">
<input type="radio" class="radio" id="it-IT" name="category" />
<label for="it-IT">
<div class="item"><span class="locale-names">Italiano</span></div>
</label>
</div>
<div class="option">
<input type="radio" class="radio" id="nl-NL" name="category" />
<label for="nl-NL">
<div class="item"><span class="locale-names">Nederlands</span></div>
</label>
</div>
<div class="option">
<input type="radio" class="radio" id="nb-NO" name="category" />
<label for="nb-NO">
<div class="item"><span class="locale-names">Norsk</span></div>
</label>
</div>
<div class="option">
<input type="radio" class="radio" id="pl-PL" name="category" />
<label for="pl-PL">
<div class="item"><span class="locale-names">Polski</span></div>
</label>
</div>
<div class="option">
<input type="radio" class="radio" id="pt-BR" name="category" />
<label for="pt-BR">
<div class="item"><span class="locale-names">Português</span></div>
</label>
</div>
<div class="option">
<input type="radio" class="radio" id="ro-RO" name="category" />
<label for="ro-RO">
<div class="item"><span class="locale-names">Română</span></div>
</label>
</div>
<div class="option">
<input type="radio" class="radio" id="tr-TR" name="category" />
<label for="tr-TR">
<div class="item"><span class="locale-names">Türkçe</span></div>
</label>
</div>
<div class="option">
<input type="radio" class="radio" id="ru-RU" name="category" />
<label for="ru-RU">
<div class="item"><span class="locale-names">Pусский</span></div>
</label>
</div>
<div class="option">
<input type="radio" class="radio" id="ar-AR" name="category" />
<label for="ar-AR">
<div class="item"><span class="locale-names">اَلْعَرَبِيَّةُ</span></div>
</label>
</div>
<div class="option">
<input type="radio" class="radio" id="zh-CN" name="category" />
<label for="zh-CN">
<div class="item"><span class="locale-names">中文(简体)</span></div>
</div>
<div class="option">
<input type="radio" class="radio" id="ja-JP" name="category" />
<label for="ja-JP">
<div class="item"><span class="locale-names">日本語</span></div>
</div>
<div class="option">
<input type="radio" class="radio" id="ko-KR" name="category" />
<label for="ko-KR">
<div class="item"><span class="locale-names">한국어</span></div>
</label>
</div>
</div>
<div class="option">
<input type="radio" class="radio" id="de-DE" name="category" />
<label for="de-DE">
<div class="item"><span class="lang">🇩🇪</span><span class="locale-names">Deutsch</span></div>
</label>
</div>
<div class="option">
<input type="radio" class="radio" id="es-ES" name="category" />
<label for="es-ES">
<div class="item"><span class="lang">🇪🇸</span><span class="locale-names">Español</span></div>
</label>
</div>
<div class="option">
<input type="radio" class="radio" id="fr-FR" name="category" />
<label for="fr-FR">
<div class="item"><span class="lang">🇫🇷</span><span class="locale-names">Français</span></div>
</label>
</div>
<div class="option">
<input type="radio" class="radio" id="it-IT" name="category" />
<label for="it-IT">
<div class="item"><span class="lang">🇮🇹</span><span class="locale-names">Italiano</span></div>
</label>
</div>
<div class="option">
<input type="radio" class="radio" id="nl-NL" name="category" />
<label for="nl-NL">
<div class="item"><span class="lang">🇳🇱</span><span class="locale-names">Nederlands</span></div>
</label>
</div>
<div class="option">
<input type="radio" class="radio" id="nb-NO" name="category" />
<label for="nb-NO">
<div class="item"><span class="lang">🇳🇴</span><span class="locale-names">Norsk</span></div>
</label>
</div>
<div class="option">
<input type="radio" class="radio" id="pl-PL" name="category" />
<label for="pl-PL">
<div class="item"><span class="lang">🇵🇱</span><span class="locale-names">Polski</span></div>
</label>
</div>
<div class="option">
<input type="radio" class="radio" id="pt-BR" name="category" />
<label for="pt-BR">
<div class="item"><span class="lang">🇧🇷</span><span class="locale-names">Português</span></div>
</label>
</div>
<div class="option">
<input type="radio" class="radio" id="ro-RO" name="category" />
<label for="ro-RO">
<div class="item"><span class="lang">🇷🇴</span><span class="locale-names">Română</span></div>
</label>
</div>
<div class="option">
<input type="radio" class="radio" id="tr-TR" name="category" />
<label for="tr-TR">
<div class="item"><span class="lang">🇹🇷</span><span class="locale-names">Türkçe</span></div>
</label>
</div>
<div class="option">
<input type="radio" class="radio" id="ru-RU" name="category" />
<label for="ru-RU">
<div class="item"><span class="lang">🇷🇺</span><span class="locale-names">Pусский</span></div>
</label>
</div>
<div class="option">
<input type="radio" class="radio" id="ar-AR" name="category" />
<label for="ar-AR">
<div class="item"><span class="lang">🇸🇦</span><span class="locale-names">اَلْعَرَبِيَّةُ</span></div>
</label>
</div>
<div class="option">
<input type="radio" class="radio" id="ja-JP" name="category" />
<label for="ja-JP">
<div class="item"><span class="lang">🇯🇵</span><span class="locale-names">日本語</span></div>
</div>
<div class="option">
<input type="radio" class="radio" id="ko-KR" name="category" />
<label for="ko-KR">
<div class="item"><span class="lang">🇰🇷</span><span class="locale-names">한국어</span></div>
</label>
<div class="locale-dropdown-toggle">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-globe"><circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg>
</div>
</div>
<div class="selected-locale">
<div class="item"><span class="lang"></span></div>
</div>
{{#if isLoggedIn}}
<div class="user-widget-wrapper logged-in">
<div class="user-widget-toggle">
<img src="{{ account.mii.image_url }}" alt="{{ account.mii.name }}" />
</div>
<div class="user-widget">
<div class="user-avatar">
<img src="{{ account.mii.image_url }}" alt="{{ account.mii.name }}" />
</div>
<div class="user-info">
<div class="mii-name">{{ account.mii.name }}</div>
<div class="pnid">{{ account.username }}</div>
</div>
<div class="buttons">
<a href="/account">
<button class="button primary">
Settings
</button>
</a>
<a href="/account/logout">
<button class="button logout">
Logout
</button>
</a>
</div>
</div>
</div>
{{else}}
<div class="user-widget-wrapper">
<a class="login-link" href="/account/login">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-user"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></svg>
</a>
</div>
{{/if}}
</div>
</header>
<script src="/assets/js/header-handler.js"></script>

View File

@@ -10,7 +10,17 @@
</div>
</div>
<div class="all-progress-lists">
<div class="donation-progress">
<h1 class="title dot">Donation goal</h1>
<div class="progress-bar">
<div class="progress-bar-inner" style="width: {{ donationCache.completed_capped }}%;" ></div>
</div>
<p><span>${{donationCache.total_dollars}}</span> of <span>${{donationCache.goal_dollars}}</span>/month, <span>${{donationCache.completed_real}}%</span> of the monthly goal. To become a subscriber and gain access to cool perks, visit the <a href="/account/upgrade">upgrade page</a>.</p>
</div>
{{#each progressLists.sections}}
<div class="purple-card">
{{> progress-list data=this boards=@root.boards }}