Merge branch 'dev' into master

This commit is contained in:
ash 2022-07-30 15:27:17 +02:00 committed by GitHub
commit 519cbd8e84
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
93 changed files with 12757 additions and 10434 deletions

15
.editorconfig Normal file
View File

@ -0,0 +1,15 @@
# http://editorconfig.org
root = true
[*]
indent_style = tab
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.js]
indent_size = 4
[*.{css,handlebars,json}]
indent_size = 2

View File

@ -1,2 +1,2 @@
# web javascript causes a lot of eslint warnings
assets/js
assets/js

View File

@ -1,18 +1,18 @@
{
"env": {
"browser": true,
"node": true,
"commonjs": true,
"es6": true
},
"globals": {
"document": true
},
"env": {
"browser": true,
"node": true,
"commonjs": true,
"es6": true
},
"globals": {
"document": true
},
"parserOptions": {
"ecmaVersion": 2021
},
"extends": "eslint:recommended",
"rules": {
"ecmaVersion": 2021
},
"extends": "eslint:recommended",
"rules": {
"no-case-declarations": "off",
"no-empty": "off",
"no-console": "off",
@ -23,20 +23,20 @@
"error",
"never"
],
"indent": [
"error",
"indent": [
"error",
"tab",
{
"SwitchCase": 1
}
],
"quotes": [
"error",
"single"
],
"semi": [
"error",
"always"
]
}
}
],
"quotes": [
"error",
"single"
],
"semi": [
"error",
"always"
]
}
}

2
.gitignore vendored
View File

@ -66,4 +66,4 @@ static-text.json
.DS_Store
# keep browserified files out
*.bundled.js
*.bundled.js

View File

@ -1,10 +1,10 @@
language: node_js
node_js:
- "7"
- "8"
- "9"
- "7"
- "8"
- "9"
sudo: false
script:
- "npm run lint"
- "npm run lint"

View File

@ -7,5 +7,5 @@ Contributions should go in the [dev branch](https://github.com/PretendoNetwork/w
Visit the [live version](https://pretendo.network)
<a href="https://discord.gg/DThgbba" target="_blank">
<img src="https://discordapp.com/api/guilds/408718485913468928/widget.png?style=banner3">
<img src="https://discordapp.com/api/guilds/408718485913468928/widget.png?style=banner3">
</a>

View File

@ -11,11 +11,24 @@
"discord": {
"client_id": "client_id",
"client_secret": "client_secret",
"guild_id": "Guild ID",
"bot_token": "token",
"tester_roles": [
"role id"
]
"bot_token": "token"
},
"aes_key": "hex key here"
}
"stripe": {
"secret_key": "sk_secret",
"webhook_secret": "whsec_secret"
},
"database": {
"account": {
"uri": "mongodb://127.0.0.1:27017",
"database": "pretendo",
"options": {
"useNewUrlParser": true,
"useUnifiedTopology": true
}
}
},
"gmail": {
"user": "email@gmail.com",
"pass": "app-password"
}
}

View File

@ -169,6 +169,12 @@
"picture": "https://cdn.discordapp.com/avatars/191370953807233024/0311b61e2009c1576828dd2e9a59d72e.png?size=128",
"github": "https://github.com/shutterbug2000"
},
{
"name": "rverse",
"caption": "Miiverse information sharing",
"picture": "https://github.com/rverseTeam.png",
"github": "https://twitter.com/rverseClub"
},
{
"name": "Kinnay",
"special": "شكر خاص",
@ -216,6 +222,15 @@
"title": "البلوج",
"description": "آخر التحديثات في أجزاء مكثفة. إذا كنت ترغب في رؤية المزيد من التحديثات المتكررة ، ففكر في دعمنا على <a href=\"https://www.patreon.com/PretendoNetwork\" target=\"_blank\"> Patreon </a>. "
},
"account": {
"accountLevel": [
"Standard",
"Tester",
"Moderator",
"Developer"
],
"banned": "Banned"
},
"localizationPage": {
"title": "!لنترجم",
"description": "الصق رابطا إلي لغة الزيسون يمكن الوصول إليها بشكل عام لاختبارها على موقع الويب",

View File

@ -169,6 +169,12 @@
"picture": "https://cdn.discordapp.com/avatars/191370953807233024/0311b61e2009c1576828dd2e9a59d72e.png?size=128",
"github": "https://github.com/shutterbug2000"
},
{
"name": "rverse",
"caption": "Miiverse information sharing",
"picture": "https://github.com/rverseTeam.png",
"github": "https://twitter.com/rverseClub"
},
{
"name": "Kinnay",
"special": "Agradecimentos especiais",
@ -217,6 +223,15 @@
"title": "Blog",
"description": "As atualizações mais recentes em postagens condensadas. Se você quiser ver atualizações com mais frequência, considere nos ajudar no <a href=\"https://www.patreon.com/PretendoNetwork\" target=\"_blank\">Patreon</a>."
},
"account": {
"accountLevel": [
"Standard",
"Tester",
"Moderator",
"Developer"
],
"banned": "Banned"
},
"localizationPage": {
"title": "Vamos traduzir",
"description": "Cole um link para um arquivo JSON acessível publicamente para testá-lo no site",

259
locales/CN_zh.json Normal file
View File

@ -0,0 +1,259 @@
{
"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": "rverse",
"caption": "Miiverse information sharing",
"picture": "https://github.com/rverseTeam.png",
"github": "https://twitter.com/rverseClub"
},
{
"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> 上支持我们。"
},
"account": {
"accountLevel": [
"Standard",
"Tester",
"Moderator",
"Developer"
],
"banned": "Banned"
},
"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

@ -169,6 +169,12 @@
"picture": "https://cdn.discordapp.com/avatars/191370953807233024/0311b61e2009c1576828dd2e9a59d72e.png?size=128",
"github": "https://github.com/shutterbug2000"
},
{
"name": "rverse",
"caption": "Miiverse information sharing",
"picture": "https://github.com/rverseTeam.png",
"github": "https://twitter.com/rverseClub"
},
{
"name": "Kinnay",
"special": "Besonderer Dank",
@ -217,6 +223,15 @@
"title": "Blog",
"description": "Die letzten Updates in Kurzform. Wenn du Updates häufiger einsehen möchtest, unterstütze uns doch bei <a href=\"https://www.patreon.com/PretendoNetwork\" target=\"_blank\">Patreon</a>."
},
"account": {
"accountLevel": [
"Standard",
"Tester",
"Moderator",
"Developer"
],
"banned": "Banned"
},
"localizationPage": {
"title": "Let's localize",
"description": "Paste a link to a publicly accessible JSON locale to test it on the website",

View File

@ -56,7 +56,7 @@
},
{
"question": "Si estoy baneado en Nintendo Network, ¿seguiré baneado al usar Pretendo?",
"answer": "No tenemos acceso a los baneos de Nintendo Network motivo por el nadie estará baneado en nuestros servidores al principio. Sin embargo, tendremos reglas a seguir al acceder a nuestros servicios y no hacerlo resultará en un baneo."
"answer": "No tenemos acceso a los baneos de Nintendo Network motivo por el nadie estará baneado en nuestros servidores al principio. Sin embargo, tendremos reglas a seguir al acceder a nuestros servicios y no hacerlo resultará en un baneo."
},
{
"question": "¿Pretendo soportará Wii/Switch?",
@ -169,6 +169,12 @@
"picture": "https://cdn.discordapp.com/avatars/191370953807233024/0311b61e2009c1576828dd2e9a59d72e.png?size=128",
"github": "https://github.com/shutterbug2000"
},
{
"name": "rverse",
"caption": "Miiverse information sharing",
"picture": "https://github.com/rverseTeam.png",
"github": "https://twitter.com/rverseClub"
},
{
"name": "Kinnay",
"special": "Agradecimientos",
@ -217,6 +223,15 @@
"title": "Blog",
"description": "Las últimas actualizaciones en pequeñas cantidades. Si quieres ver actualizaciones más frecuentes, considera apoyarnos en <a href=\"https://www.patreon.com/PretendoNetwork\" target=\"_blank\">Patreon</a>."
},
"account": {
"accountLevel": [
"Standard",
"Tester",
"Moderator",
"Developer"
],
"banned": "Banned"
},
"localizationPage": {
"title": "Vamos a localizar",
"description": "Pega un enlace a una localización JSON accesible pulicamente para probarla en el sitio",

View File

@ -171,6 +171,12 @@
"picture": "https://cdn.discordapp.com/avatars/191370953807233024/0311b61e2009c1576828dd2e9a59d72e.png?size=128",
"github": "https://github.com/shutterbug2000"
},
{
"name": "rverse",
"caption": "Miiverse information sharing",
"picture": "https://github.com/rverseTeam.png",
"github": "https://twitter.com/rverseClub"
},
{
"name": "Kinnay",
"special": "Remerciement spéciaux",
@ -219,6 +225,15 @@
"title": "Blog",
"description": "Les dernières mises à jour en blocs condensés. Si vous souhaitez voir des mises à jour plus fréquentes, envisagez de nous soutenir sur <a href=\"https://www.patreon.com/PretendoNetwork\" target=\"_blank\">Patreon</a>."
},
"account": {
"accountLevel": [
"Standard",
"Tester",
"Moderator",
"Developer"
],
"banned": "Banned"
},
"localizationPage": {
"title": "Localisons",
"description": "Collez un lien vers un paramètre régional JSON accessible au public pour le tester sur le site Web",

View File

@ -170,6 +170,12 @@
"picture": "https://cdn.discordapp.com/avatars/191370953807233024/0311b61e2009c1576828dd2e9a59d72e.png?size=128",
"github": "https://github.com/shutterbug2000"
},
{
"name": "rverse",
"caption": "Condivisione di informazioni su Miiverse",
"picture": "https://github.com/rverseTeam.png",
"github": "https://twitter.com/rverseClub"
},
{
"name": "Kinnay",
"special": "Ringraziamenti speciali",
@ -179,7 +185,7 @@
},
{
"name": "NinStar",
"caption": "Icons for the Mii Editor and Juxt reactions",
"caption": "Icone per l'editor Mii e le reazioni su Juxt",
"picture": "https://github.com/ninstar.png",
"github": "https://github.com/ninstar"
},
@ -218,6 +224,15 @@
"title": "Blog",
"description": "Gli ultimi aggiornamenti, condensati. Se sei interessato a ricevere aggiornamenti più frequenti, considera l'idea di supportarci su <a href=\"https://www.patreon.com/PretendoNetwork\" target=\"_blank\">Patreon</a>."
},
"account": {
"accountLevel": [
"Standard",
"Tester",
"Moderatore",
"Sviluppatore"
],
"banned": "Bannato"
},
"localizationPage": {
"title": "Localizziamo",
"description": "Incolla il link di una localizzazione in formato JSON accessibile al pubblico per testarla sul sito",
@ -242,4 +257,4 @@
]
}
}
}
}

View File

@ -169,6 +169,12 @@
"picture": "https://cdn.discordapp.com/avatars/191370953807233024/0311b61e2009c1576828dd2e9a59d72e.png?size=128",
"github": "https://github.com/shutterbug2000"
},
{
"name": "rverse",
"caption": "Miiverse information sharing",
"picture": "https://github.com/rverseTeam.png",
"github": "https://twitter.com/rverseClub"
},
{
"name": "Kinnay",
"special": "Special thanks",
@ -217,6 +223,15 @@
"title": "ブログ(英語)",
"description": "最新のアップデートをまとめてお届けします。もっと多くのアップデートを貰いたい方は、<a href=\"https://www.patreon.com/PretendoNetwork\" target=\"_blank\">Patreon</a>での寄付をご検討ください。"
},
"account": {
"accountLevel": [
"Standard",
"Tester",
"Moderator",
"Developer"
],
"banned": "Banned"
},
"localizationPage": {
"title": "レッツローカライズ",
"description": "公開されているJSONへのリンクを貼り付けてこのサイトに試してみて下さい",

View File

@ -169,6 +169,12 @@
"picture": "https://cdn.discordapp.com/avatars/191370953807233024/0311b61e2009c1576828dd2e9a59d72e.png?size=128",
"github": "https://github.com/shutterbug2000"
},
{
"name": "rverse",
"caption": "Miiverse information sharing",
"picture": "https://github.com/rverseTeam.png",
"github": "https://twitter.com/rverseClub"
},
{
"name": "Kinnay",
"special": "Special Thanks",
@ -217,6 +223,15 @@
"title": "블로그",
"description": "내용이 압축된 최신 업데이트입니다. 만약 더 많은 업데이트를 원하신다면, <a href=\"https://www.patreon.com/PretendoNetwork\" target=\"_blank\">Patreon</a>에 기부하는 것을 검토해 주세요."
},
"account": {
"accountLevel": [
"Standard",
"Tester",
"Moderator",
"Developer"
],
"banned": "Banned"
},
"localizationPage": {
"title": "같이 번역해 주세요!",
"description": "액세스 가능한 JSON의 링크를 붙여넣어서, 웹사이트에서 테스트해 보세요!",

View File

@ -169,6 +169,12 @@
"picture": "https://cdn.discordapp.com/avatars/191370953807233024/0311b61e2009c1576828dd2e9a59d72e.png?size=128",
"github": "https://github.com/shutterbug2000"
},
{
"name": "rverse",
"caption": "Miiverse information sharing",
"picture": "https://github.com/rverseTeam.png",
"github": "https://twitter.com/rverseClub"
},
{
"name": "Kinnay",
"special": "Special thanks",
@ -217,6 +223,15 @@
"title": "Blog",
"description": "De meest recente samenvattingen. Als je vaker nieuws wil, overweeg ons op <a href=\"https://www.patreon.com/PretendoNetwork\" target=\"_blank\">Patreon</a> te ondersteunen."
},
"account": {
"accountLevel": [
"Standard",
"Tester",
"Moderator",
"Developer"
],
"banned": "Banned"
},
"localizationPage": {
"title": "Laten we meer talen toe voegen!",
"description": "Voeg hier een link in voor een openbaar JSON bestand om het op de site te testen.",
@ -241,4 +256,4 @@
]
}
}
}
}

View File

@ -169,6 +169,12 @@
"picture": "https://cdn.discordapp.com/avatars/191370953807233024/0311b61e2009c1576828dd2e9a59d72e.png?size=128",
"github": "https://github.com/shutterbug2000"
},
{
"name": "rverse",
"caption": "Miiverse information sharing",
"picture": "https://github.com/rverseTeam.png",
"github": "https://twitter.com/rverseClub"
},
{
"name": "Kinnay",
"special": "Spesiell takk",
@ -217,6 +223,15 @@
"title": "Blogg",
"description": "De nyeste oppdateringen er i kondensert biter. Hvis du har lyst til å se mer hyppig oppdateringen, tenk om å støtte oss på <a href=\"https://www.patreon.com/PretendoNetwork\" target=\"_blank\">Patreon</a>."
},
"account": {
"accountLevel": [
"Standard",
"Tester",
"Moderator",
"Developer"
],
"banned": "Banned"
},
"localizationPage": {
"title": "La oss lokalisere",
"description": "Lim inn en lenke til en offentlig JSON lokal til å teste det på nettsiden",
@ -241,4 +256,4 @@
]
}
}
}
}

View File

@ -169,6 +169,12 @@
"picture": "https://cdn.discordapp.com/avatars/191370953807233024/0311b61e2009c1576828dd2e9a59d72e.png?size=128",
"github": "https://github.com/shutterbug2000"
},
{
"name": "rverse",
"caption": "Miiverse information sharing",
"picture": "https://github.com/rverseTeam.png",
"github": "https://twitter.com/rverseClub"
},
{
"name": "Kinnay",
"special": "Specjalne podziękowania",
@ -216,6 +222,15 @@
"title": "Blog",
"description": "Najnowsze informacje w krótkich tekstach. Jeśli chcesz otrzymywać częstsze aktualizacje, pomyśl nad wsparciem nas na <a href=\"https://www.patreon.com/PretendoNetwork\" target=\"_blank\">Patreon</a>."
},
"account": {
"accountLevel": [
"Standard",
"Tester",
"Moderator",
"Developer"
],
"banned": "Banned"
},
"localizationPage": {
"title": "Tłumaczmy",
"description": "Wklej link do publicznego pliku JSON, aby przetestować go na stronie",

View File

@ -169,6 +169,12 @@
"picture": "https://cdn.discordapp.com/avatars/191370953807233024/0311b61e2009c1576828dd2e9a59d72e.png?size=128",
"github": "https://github.com/shutterbug2000"
},
{
"name": "rverse",
"caption": "Miiverse information sharing",
"picture": "https://github.com/rverseTeam.png",
"github": "https://twitter.com/rverseClub"
},
{
"name": "Kinnay",
"special": "Mulțumiri speciale",
@ -217,6 +223,15 @@
"title": "Blog",
"description": "Ultimele actualizări în bucăți condensate. Dacă vrei să vezi mai multe actualizări, consideră să ne susții pe <a href=\"https://www.patreon.com/PretendoNetwork\" target=\"_blank\">Patreon</a>."
},
"account": {
"accountLevel": [
"Standard",
"Tester",
"Moderator",
"Developer"
],
"banned": "Banned"
},
"localizationPage": {
"title": "Hai să localizăm",
"description": "Inserați un link către un JSON public pentru a testa traducerea pe site",

View File

@ -169,6 +169,12 @@
"picture": "https://cdn.discordapp.com/avatars/191370953807233024/0311b61e2009c1576828dd2e9a59d72e.png?size=128",
"github": "https://github.com/shutterbug2000"
},
{
"name": "rverse",
"caption": "Miiverse information sharing",
"picture": "https://github.com/rverseTeam.png",
"github": "https://twitter.com/rverseClub"
},
{
"name": "Kinnay",
"special": "Особая благодарность",
@ -217,6 +223,15 @@
"title": "Blog",
"description": "The latest updates in condensed chunks. If you want to see more frequent updates, consider supporting us on <a href=\"https://www.patreon.com/PretendoNetwork\" target=\"_blank\">Patreon</a>."
},
"account": {
"accountLevel": [
"Standard",
"Tester",
"Moderator",
"Developer"
],
"banned": "Banned"
},
"localizationPage": {
"title": "Let's localize",
"description": "Paste a link to a publicly accessible JSON locale to test it on the website",

View File

@ -169,6 +169,12 @@
"picture": "https://cdn.discordapp.com/avatars/191370953807233024/0311b61e2009c1576828dd2e9a59d72e.png?size=128",
"github": "https://github.com/shutterbug2000"
},
{
"name": "rverse",
"caption": "Miiverse information sharing",
"picture": "https://github.com/rverseTeam.png",
"github": "https://twitter.com/rverseClub"
},
{
"name": "Kinnay",
"special": "Özel teşekkür",
@ -217,6 +223,15 @@
"title": "Blog",
"description": "The latest updates in condensed chunks. If you want to see more frequent updates, consider supporting us on <a href=\"https://www.patreon.com/PretendoNetwork\" target=\"_blank\">Patreon</a>."
},
"account": {
"accountLevel": [
"Standard",
"Tester",
"Moderator",
"Developer"
],
"banned": "Banned"
},
"localizationPage": {
"title": "Let's localize",
"description": "Paste a link to a publicly accessible JSON locale to test it on the website",

View File

@ -169,6 +169,12 @@
"picture": "https://cdn.discordapp.com/avatars/191370953807233024/0311b61e2009c1576828dd2e9a59d72e.png?size=128",
"github": "https://github.com/shutterbug2000"
},
{
"name": "rverse",
"caption": "Miiverse information sharing",
"picture": "https://github.com/rverseTeam.png",
"github": "https://twitter.com/rverseClub"
},
{
"name": "Kinnay",
"special": "Special thanks",
@ -217,6 +223,15 @@
"title": "Blog",
"description": "The latest updates in condensed chunks. If you want to see more frequent updates, consider supporting us on <a href=\"https://www.patreon.com/PretendoNetwork\" target=\"_blank\">Patreon</a>."
},
"account": {
"accountLevel": [
"Standard",
"Tester",
"Moderator",
"Developer"
],
"banned": "Banned"
},
"localizationPage": {
"title": "Let's localize",
"description": "Paste a link to a publicly accessible JSON locale to test it on the website",
@ -241,4 +256,4 @@
]
}
}
}
}

15388
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,40 +1,45 @@
{
"name": "website",
"version": "1.0.0",
"description": "",
"main": "src/server.js",
"scripts": {
"start": "browserify ./public/assets/js/miieditor.js -o ./public/assets/js/miieditor.bundled.js && node src/server.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/PretendoNetwork/website.git"
},
"author": "",
"license": "ISC",
"bugs": {
"url": "https://github.com/PretendoNetwork/website/issues"
},
"homepage": "https://github.com/PretendoNetwork/website#readme",
"dependencies": {
"adm-zip": "^0.5.9",
"browserify": "^17.0.0",
"colors": "^1.4.0",
"cookie-parser": "^1.4.5",
"discord-oauth2": "github:ryanblenis/discord-oauth2",
"express": "^4.17.1",
"express-handlebars": "^5.3.1",
"express-locale": "^2.0.0",
"fs-extra": "^9.1.0",
"got": "^11.8.2",
"gray-matter": "^4.0.3",
"kaitai-struct": "^0.9.0",
"marked": "^4.0.10",
"morgan": "^1.10.0",
"trello": "^0.11.0",
"uuid": "^8.3.2"
},
"devDependencies": {
"eslint": "^7.32.0"
}
"name": "website",
"version": "1.0.0",
"description": "",
"main": "src/server.js",
"scripts": {
"start": "browserify ./public/assets/js/miieditor.js -o ./public/assets/js/miieditor.bundled.js && node src/server.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/PretendoNetwork/website.git"
},
"author": "",
"license": "ISC",
"bugs": {
"url": "https://github.com/PretendoNetwork/website/issues"
},
"homepage": "https://github.com/PretendoNetwork/website#readme",
"dependencies": {
"@discordjs/rest": "^0.5.0",
"adm-zip": "^0.5.9",
"browserify": "^17.0.0",
"colors": "^1.4.0",
"cookie-parser": "^1.4.5",
"discord-api-types": "^0.36.1",
"discord-oauth2": "github:ryanblenis/discord-oauth2",
"express": "^4.17.1",
"express-handlebars": "^5.3.1",
"express-locale": "^2.0.0",
"fs-extra": "^9.1.0",
"got": "^11.8.5",
"gray-matter": "^4.0.3",
"kaitai-struct": "^0.9.0",
"marked": "^4.0.10",
"mongoose": "^6.4.0",
"morgan": "^1.10.0",
"nodemailer": "^6.7.5",
"stripe": "^9.9.0",
"trello": "^0.11.0",
"uuid": "^8.3.2"
},
"devDependencies": {
"eslint": "^7.32.0"
}
}

View File

@ -1,279 +1,320 @@
/* Removing until it's done */
.setting-card a.edit,
.sign-in-history a {
display: none;
display: none;
}
.account-wrapper {
display: grid;
column-gap: 48px;
margin-top: 80px;
color: var(--text-secondary);
display: grid;
column-gap: 48px;
margin-top: 80px;
color: var(--text-shade-1);
}
/* Account settings sidebar */
.account-sidebar .user {
text-align: center;
margin: 55px auto;
width: fit-content;
text-align: center;
margin: 55px auto;
width: fit-content;
}
.account-sidebar .user .miiname {
font-size: 1.2rem;
color: var(--text);
margin: 8px 0 4px;
font-size: 1.2rem;
color: var(--text-shade-3);
margin: 8px 0 4px;
}
.account-sidebar .user .username {
margin: 0px;
margin: 0;
}
.account-sidebar .user .tier-name {
margin: 12px 0;
line-height: 1.2em;
border-radius: 1.2em;
border-width: 2px;
border-style: solid;
padding: 4px 16px;
}
.account-sidebar .user .tier-level-0,
.account-sidebar .user .access-level-0 {
background: #2a2f50;
color: var(--text-shade-1);
border-color: #383f6b;
}
.account-sidebar .user .tier-level-1 {
background: rgba(255, 132, 132, 0.2);
color: #FF8484;
border-color: rgba(255, 132, 132, 0.8);
}
.account-sidebar .user .tier-level-2 {
background: rgba(89, 201, 165, 0.3);
color:#59c9a5;
border-color: #59c9a5;
}
.account-sidebar .user .tier-level-3 {
background: rgba(202, 177, 251, 0.3);
color:var(--accent-shade-3);
border-color: var(--accent-shade-3);
}
.account-sidebar .user .access-level-banned {
background: rgba(255, 63, 0, 0.1);
color:#FF3F00;
border-color: rgba(255, 63, 0, 0.8);
}
.account-sidebar .user .access-level-1 {
background: rgba(100, 247, 239, 0.3);
color: #64F7EF;
border-color: #64F7EF;
}
.account-sidebar .user .access-level-2 {
background: rgba(255, 199, 89, 0.3);
color: #FFC759;
border-color: #FFC759;
}
.account-sidebar .user .access-level-3 {
background: rgba(90, 255, 21, 0.3);
color:#5AFF15;
border-color: #5AFF15;
}
.account-sidebar .user .mii {
width: 128px;
height: 128px;
border-radius: 100%;
background: var(--btn-secondary);
width: 128px;
height: 128px;
border-radius: 100%;
background: var(--bg-shade-3);
}
.account-sidebar .user #download-cemu-files {
display: flex;
flex-flow: column;
align-items: center;
padding: 24px;
background: #383f6b;
margin: 24px 0 0;
text-decoration: none;
.account-sidebar .buttons a {
display: flex;
flex-flow: column;
align-items: center;
padding: 20px 24px;
margin: 20px 0 0;
text-decoration: none;
text-align: center;
}
.account-sidebar .user #download-cemu-files p.download-caption {
margin: 15px 0 0;
.account-sidebar .buttons a svg {
margin-bottom: 16px;
}
.account-sidebar .user p.cemu-warning {
margin: 4px 0 0;
font-size: 0.7rem;
color: var(--text-secondary);
.account-sidebar .buttons a p.caption {
margin: 0;
}
.account-sidebar .buttons p.cemu-warning {
margin: 4px 0 0;
font-size: 0.7rem;
color: var(--text-shade-1);
}
/* Settings */
.settings-wrapper {
display: grid;
grid-column-start: 2;
grid-template-columns: 1fr 1fr;
column-gap: 20px;
display: grid;
grid-column-start: 2;
grid-template-columns: 1fr 1fr;
column-gap: 20px;
}
.settings-wrapper a {
color: #9d6ff3;
text-decoration: none;
font-weight: bold;
color: var(--accent-shade-1);
text-decoration: none;
font-weight: bold;
}
.settings-wrapper a:hover {
text-decoration: underline;
text-decoration: underline;
}
.settings-wrapper h2.section-header {
margin-top: 40px;
grid-column: 1 / 3;
color: var(--text);
margin-top: 40px;
grid-column: 1 / 3;
color: var(--text-shade-3);
}
.setting-card {
display: grid;
grid-template-rows: 35px repeat(2, auto);
row-gap: 24px;
position: relative;
border-radius: 10px;
background: #2a2f50;
padding: 48px 60px;
display: grid;
grid-template-rows: 35px repeat(2, auto);
row-gap: 24px;
position: relative;
border-radius: 10px;
background: var(--bg-shade-2);
padding: 48px 60px;
}
.setting-card * {
margin: 0;
margin: 0;
}
.setting-card .edit {
color: var(--text-secondary);
background: #383f6b;
border-radius: 100%;
position: absolute;
top: 42px;
right: 48px;
width: 24px;
height: 24px;
padding: 12px;
color: var(--text-shade-1);
background: var(--bg-shade-3);
border-radius: 100%;
position: absolute;
top: 42px;
right: 48px;
width: 24px;
height: 24px;
padding: 12px;
}
.setting-card .edit:hover {
background: #383f6b;
color: var(--text);
background: var(--bg-shade-3);
color: var(--text-shade-3);
}
.setting-card .header {
color: var(--text);
color: var(--text-shade-3);
}
.setting-card .setting-list {
display: grid;
grid-template-columns: repeat(2, auto);
gap: 24px;
list-style: none;
padding: 0;
display: grid;
grid-template-columns: repeat(2, auto);
gap: 24px;
list-style: none;
padding: 0;
}
.setting-card .setting-list p.label {
color: var(--text);
margin-bottom: 4px;
color: var(--text-shade-3);
margin-bottom: 4px;
}
fieldset {
height: min-content;
padding: 0;
border: none;
}
fieldset:disabled form label {
cursor: not-allowed;
position: relative;
height: min-content;
padding: 0;
border: none;
}
.setting-card .server-selection {
display: flex;
border-radius: 5px;
overflow: hidden;
background: #383f6b;
display: flex;
border-radius: 5px;
overflow: hidden;
background: var(--bg-shade-3);
}
.setting-card .server-selection input {
display: none;
display: none;
}
.server-selection input + label {
display: flex;
flex-flow: column;
align-items: center;
flex: 50%;
color: var(--text-secondary);
padding: 40px;
justify-content: space-between;
cursor: pointer;
display: flex;
flex-flow: column;
align-items: center;
flex: 50%;
color: var(--text-shade-1);
padding: 40px;
justify-content: space-between;
cursor: pointer;
}
.server-selection input + label h2 {
margin-top: 12px;
color: var(--text-secondary);
margin-top: 12px;
color: var(--text-shade-1);
}
.server-selection input:checked + label,
.server-selection input:checked + label h2 {
background: var(--theme);
color: var(--text);
background: var(--accent-shade-0);
color: var(--text-shade-3);
}
.setting-card #remove-discord-connection {
width: 100%;
padding: 12px 48px;
cursor: pointer;
background: #383f6b;
.setting-card #link-discord-account {
width: 100%;
padding: 12px 48px;
cursor: pointer;
background: var(--bg-shade-3);
}
.setting-card.sign-in-history a button {
width: 100%;
padding: 12px 48px;
cursor: pointer;
background: #383f6b;
}
.setting-card input[type="checkbox"] {
appearance: none;
-webkit-appearance: none;
background: #383f6b;
padding: 12px;
margin: 4px;
margin-left: 0;
border-radius: 4px;
vertical-align: -65%;
}
.setting-card input[type="checkbox"]:checked {
background: no-repeat center/contain url(../images/check.svg), var(--theme);
.setting-card button {
width: 100%;
height: fit-content;
padding: 12px 48px;
align-self: flex-end;
cursor: pointer;
background: var(--bg-shade-3);
}
.setting-card.span-both-columns {
grid-column: 1 / span 2;
grid-column: 1 / span 2;
}
@keyframes banner-notice {
0% {
top: -150px;
}
20% {
top: 35px;
}
80% {
top: 35px;
}
100% {
top: -150px;
}
0% {
top: -150px;
}
20% {
top: 35px;
}
80% {
top: 35px;
}
100% {
top: -150px;
}
}
.banner-notice {
display: flex;
justify-content: center;
position: fixed;
top: -150px;
width: 100%;
animation: banner-notice 5s;
display: flex;
justify-content: center;
position: fixed;
top: -150px;
width: 100%;
animation: banner-notice 5s;
}
.banner-notice div {
padding: 4px 36px;
border-radius: 5px;
z-index: 3;
padding: 4px 36px;
border-radius: 5px;
z-index: 3;
}
.banner-notice.success div {
background: #37a985;
background: var(--green-shade-0);
}
.banner-notice.error div {
background: #a9375b;
background: var(--red-shade-1);
}
footer {
margin-top: 80px;
margin-top: 80px;
}
@media screen and (max-width: 1300px) {
.account-wrapper {
margin: 20px 0;
}
.account-wrapper {
margin: 20px 0;
}
.settings-wrapper {
grid-column-start: 1;
}
.settings-wrapper {
grid-column-start: 1;
}
.account-sidebar {
margin: 0;
}
.account-sidebar {
margin: 0;
}
.account-sidebar .user .mii {
width: 128px;
height: 128px;
}
.account-sidebar .user .mii {
width: 128px;
height: 128px;
}
}
@media screen and (max-width: 1000px) {
.settings-wrapper {
display: block;
width: 100%;
}
.settings-wrapper {
display: block;
width: 100%;
}
.setting-card {
margin-bottom: 24px;
}
.setting-card {
margin-bottom: 24px;
}
}
@media screen and (max-width: 550px) {
.setting-card {
padding: 24px;
width: calc(100vw - 48px);
margin-left: -5vw;
margin-right: -2.5vw;
border-radius: 0;
margin-bottom: 12px;
}
.setting-card .edit {
top: 20px;
right: 20px;
transform: scale(0.85);
}
.setting-card {
padding: 24px;
width: calc(100vw - 48px);
margin-left: -5vw;
margin-right: -2.5vw;
border-radius: 0;
margin-bottom: 12px;
}
.setting-card .server-selection {
flex-flow: column;
}
.setting-card .edit {
top: 20px;
right: 20px;
transform: scale(0.85);
}
.setting-card .server-selection {
flex-flow: column;
}
}
@media screen and (max-width: 350px) {
.setting-card .setting-list {
grid-template-columns: auto;
}
.setting-card .setting-list {
grid-template-columns: auto;
}
}

View File

@ -1,113 +1,92 @@
.new-font {
font-family: museo-sans, sans-serif;
font-family: museo-sans, sans-serif;
}
.pretendo {
font-family: Poppins, Arial, Helvetica, sans-serif;
font-weight: 700;
font-family: Poppins, Arial, Helvetica, sans-serif;
font-weight: 700;
}
.announcement-hero {
position: relative;
text-align: center;
padding: 96px 0;
margin: 36px 0 24px;
position: relative;
text-align: center;
padding: 96px 0;
margin: 36px 0 24px;
}
.announcement-hero p {
font-size: 24px;
margin: 0;
margin-bottom: 24px;
font-size: 24px;
margin: 0;
margin-bottom: 24px;
}
.announcement-hero h1 {
font-size: 450%;
margin: 0;
font-size: 450%;
margin: 0;
}
.announcement-hero::before {
content: "";
position: absolute;
width: 500vw;
height: 100%;
top: 0;
left: -50vw;
background: var(--theme);
z-index: -1;
content: "";
position: absolute;
width: 500vw;
height: 100%;
top: 0;
left: -50vw;
background: var(--accent-shade-0);
z-index: -1;
}
.bro-what.subscribe {
padding-top: 0;
display: flex;
padding-top: 0;
display: flex;
}
.bro-what.subscribe h1 {
margin: 0;
margin-right: 12px;
width: fit-content;
margin: 0;
margin-right: 12px;
width: fit-content;
}
.buy-now {
margin-left:auto;
margin-left:auto;
}
.buy-now button {
cursor: pointer;
width: max-content;
height: 100%;
cursor: pointer;
width: max-content;
height: 100%;
}
.bro-what {
padding: 48px;
padding: 48px;
}
.bro-what a {
color: inherit;
text-decoration: none;
font-weight: 700;
color: inherit;
text-decoration: none;
font-weight: 700;
}
.dotted-bg {
position: relative;
position: relative;
}
.dotted-bg::before {
content: "";
position: absolute;
width: 500vw;
height: 100%;
top: 0;
left: -50vw;
background:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='100%25' width='100%25'%3E%3Cdefs%3E%3Cpattern id='doodad' width='6' height='6' viewBox='0 0 40 40' patternUnits='userSpaceOnUse' patternTransform=''%3E%3Crect width='100%25' height='100%25' fill='rgba(27, 31, 59,1)'/%3E%3Ccircle cx='20' cy='20' r='11' fill='rgba(103, 61, 182,0.4)'/%3E%3Cpath d='M9 20aInfinityInfinity 0 0 0InfinityNaNaInfinityInfinity 0 0 0-InfinityNaN' fill='%23ecc94b'/%3E%3C/pattern%3E%3C/defs%3E%3Crect fill='url(%23doodad)' height='200%25' width='200%25'/%3E%3C/svg%3E ");
z-index: -1;
content: "";
position: absolute;
width: 500vw;
height: 100%;
top: 0;
left: -50vw;
background:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='100%25' width='100%25'%3E%3Cdefs%3E%3Cpattern id='doodad' width='6' height='6' viewBox='0 0 40 40' patternUnits='userSpaceOnUse' patternTransform=''%3E%3Crect width='100%25' height='100%25' fill='rgba(27, 31, 59,1)'/%3E%3Ccircle cx='20' cy='20' r='11' fill='rgba(103, 61, 182,0.4)'/%3E%3Cpath d='M9 20aInfinityInfinity 0 0 0InfinityNaNaInfinityInfinity 0 0 0-InfinityNaN' fill='%23ecc94b'/%3E%3C/pattern%3E%3C/defs%3E%3Crect fill='url(%23doodad)' height='200%25' width='200%25'/%3E%3C/svg%3E ");
z-index: -1;
}
.footnotes {
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;
}
color: var(--text-shade-1);
}
@media screen and (max-width: 946px) {
header nav a:not(.keep-on-mobile) {
header nav a:not(.keep-on-mobile) {
display: none;
}
.announcement-hero h1 {
font-size: 350%;
}
.announcement-hero h1 {
font-size: 350%;
}
}
@media screen and (max-width: 724px) {
@ -126,35 +105,35 @@
margin: 0 10px;
}
.announcement-hero h1 {
font-size: 250%;
}
.announcement-hero p {
font-size: 18px;
}
.announcement-hero h1 {
font-size: 250%;
}
.announcement-hero p {
font-size: 18px;
}
}
@media screen and (max-width: 600px) {
.bro-what.subscribe {
flex-flow: column;
}
.bro-what.subscribe {
flex-flow: column;
}
.bro-what a,
.buy-now button {
width: 100%;
}
.bro-what a,
.buy-now button {
width: 100%;
}
.bro-what a {
margin-top: 24px;
}
.bro-what a {
margin-top: 24px;
}
.announcement-hero {
padding: 72px 0;
}
.announcement-hero {
padding: 72px 0;
}
}
@media screen and (max-width: 480px) {
.bro-what {
padding: 36px 0;
}
}
.bro-what {
padding: 36px 0;
}
}

View File

@ -1,118 +1,119 @@
.blog-card {
display: flex;
flex-flow: row nowrap;
padding: 0;
margin: 0 auto;
max-width: 1000px;
margin-bottom: 30px;
text-decoration: none;
position: relative;
border-radius: 10px;
overflow: hidden;
display: flex;
flex-flow: row nowrap;
padding: 0;
margin: 0 auto;
max-width: 1000px;
margin-bottom: 30px;
text-decoration: none;
position: relative;
border-radius: 10px;
overflow: hidden;
}
.blog-card .post-info {
flex: 50%;
padding: 40px;
display: flex;
flex-flow: column;
color: var(--text-secondary);
flex: 50%;
padding: 40px;
display: flex;
flex-flow: column;
color: var(--text-shade-1);
}
.blog-card .post-info .title {
color: var(--text);
margin: 0;
color: var(--text-shade-3);
margin: 0;
}
.blog-card .post-info .caption {
margin: 4px 0 32px 0;
margin: 4px 0 32px 0;
}
.blog-card .pub-info {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: left;
margin-top: auto;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: left;
margin-top: auto;
}
.blog-card .pub-info .date {
font-weight: bold;
color: var(--text);
font-weight: bold;
color: var(--text-shade-3);
}
.blog-card .pub-info > * {
margin-right: 0.5em;
margin-top: 0.2em;
margin-right: 0.5em;
margin-top: 0.2em;
}
.blog-card .profile {
display: inline-grid;
grid-template-columns: 30px auto;
grid-gap: 10px;
font-weight: bold;
color: var(--text);
align-items: center;
height: 32px;
margin-right: 0.3em;
display: inline-grid;
grid-template-columns: 30px auto;
grid-gap: 10px;
font-weight: bold;
color: var(--text-shade-3);
align-items: center;
height: 32px;
margin-right: 0.3em;
}
.blog-card .profile img {
border-radius: 4px;
border: 1px solid var(--border);
max-width: 100%;
border-radius: 4px;
border: 1px solid var(--border);
max-width: 100%;
}
.blog-card .cover {
flex: 50%;
border: 3px solid #151934;
border-radius: 0 10px 10px 0;
flex: 50%;
border: 3px solid var(--bg-shade-0);
border-radius: 0 10px 10px 0;
}
.progress-hero a,
.progress-hero a * {
color: #9d6ff3;
text-decoration: none;
font-weight: bold;
color: var(--accent-shade-1);
text-decoration: none;
font-weight: bold;
}
.progress-hero a:hover,
.progress-hero a:hover {
text-decoration: underline;
text-decoration: underline;
}
.buttons {
margin: 10vh auto;
width: min-content;
margin: 10vh auto;
width: min-content;
}
.buttons button.secondary.icon-btn {
cursor: pointer;
width: 35px;
height: 35px;
cursor: pointer;
width: 35px;
height: 35px;
padding: 0;
}
footer {
margin-top: 160px;
margin-top: 160px;
}
@media screen and (max-width: 900px) {
.blog-card {
flex-flow: column;
}
.blog-card .post-info {
padding: 30px;
}
.blog-card .cover {
order: -1;
min-height: 250px;
border-radius: 10px 10px 0 0;
}
footer {
margin-top: 100px;
}
.blog-card {
flex-flow: column;
}
.blog-card .post-info {
padding: 30px;
}
.blog-card .cover {
order: -1;
min-height: 250px;
border-radius: 10px 10px 0 0;
}
footer {
margin-top: 100px;
}
}
@media screen and (max-width: 450px) {
.blog-card .cover {
min-height: 200px;
}
.blog-card .cover {
min-height: 200px;
}
}

View File

@ -1,22 +1,22 @@
.wrapper {
display: flex;
flex-direction: column;
width: 95%;
min-height: 100vh;
display: flex;
flex-direction: column;
width: 95%;
min-height: 100vh;
}
header {
width: 100%;
width: 100%;
}
.card-wrap {
width: 100%;
width: 100%;
}
.blog-card {
padding: 60px;
max-width: 1100px;
margin: 50px auto;
color: var(--text-secondary);
padding: 60px;
max-width: 1100px;
margin: 50px auto;
color: var(--text-shade-1);
}
.blog-card h1,
@ -25,201 +25,201 @@ header {
.blog-card h4,
.blog-card h5,
.blog-card h6 {
margin: 40px 0 10px;
color: var(--text);
margin: 40px 0 10px;
color: var(--text-shade-3);
}
.blog-card strong {
color: var(--text);
color: var(--text-shade-3);
}
.blog-card a,
.blog-card a * {
color: #9d6ff3;
text-decoration: none;
font-weight: bold;
color: var(--accent-shade-1);
text-decoration: none;
font-weight: bold;
}
.blog-card a:hover,
.blog-card a:hover {
text-decoration: underline;
text-decoration: underline;
}
.blog-card del {
text-decoration: line-through;
text-decoration: line-through;
}
.blog-card .title {
margin: 0;
margin-bottom: 8px;
margin: 0;
margin-bottom: 8px;
}
.blog-card .pub-info {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: left;
margin-top: auto;
margin-bottom: 30px;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: left;
margin-top: auto;
margin-bottom: 30px;
}
.blog-card .pub-info .date {
font-weight: bold;
color: var(--text);
font-weight: bold;
color: var(--text-shade-3);
}
.blog-card .pub-info > * {
margin-right: 0.5em;
margin-top: 0.2em;
margin-right: 0.5em;
margin-top: 0.2em;
}
.blog-card .profile {
display: inline-grid;
grid-template-columns: 30px auto;
grid-gap: 10px;
font-weight: bold;
color: var(--text);
align-items: center;
height: 32px;
margin-right: 0.3em;
display: inline-grid;
grid-template-columns: 30px auto;
grid-gap: 10px;
font-weight: bold;
color: var(--text-shade-3);
align-items: center;
height: 32px;
margin-right: 0.3em;
}
.blog-card .profile img {
margin: 0;
border-radius: 4px;
border: 1px solid var(--border);
max-width: 100%;
margin: 0;
border-radius: 4px;
border: 1px solid var(--border);
max-width: 100%;
}
.blog-card p,
.post-info {
color: var(--text-secondary);
color: var(--text-shade-1);
}
.blog-card img {
max-width: 100%;
max-height: 800px;
margin: 10px auto;
display: block;
border-radius: 4px;
border: 1px solid var(--border);
max-width: 100%;
max-height: 800px;
margin: 10px auto;
display: block;
border-radius: 4px;
border: 1px solid var(--border);
}
.blog-card img.emoji {
display: inline;
margin: 0;
border: none;
display: inline;
margin: 0;
border: none;
}
.blog-card video {
width: 100%;
border-radius: 4px;
border: 1px solid var(--border);
width: 100%;
border-radius: 4px;
border: 1px solid var(--border);
}
.blog-card iframe {
width: 100%;
aspect-ratio: 16/9;
border-radius: 4px;
border: 1px solid var(--border);
width: 100%;
aspect-ratio: 16/9;
border-radius: 4px;
border: 1px solid var(--border);
}
/* Fallback for aspect-ratio since it's unsupported by some browsers (looking at you Safari) */
@supports not (aspect-ratio: 16/9) {
.blog-card .aspectratio-fallback {
position: relative;
height: 0;
padding-top: 56.25%;
}
.blog-card iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: 0;
}
.blog-card .aspectratio-fallback {
position: relative;
height: 0;
padding-top: 56.25%;
}
.blog-card iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: 0;
}
}
/* Some twitter iframe specific stuff */
.blog-card .twitter-tweet {
margin: auto;
margin: auto;
}
.blog-card .twitter-tweet iframe {
border: none; /* Fixes the double border */
border: none; /* Fixes the double border */
}
.blog-card table {
border-radius: 4px;
border-collapse: collapse;
background: #31375e;
margin-bottom: 30px;
overflow: hidden;
border-radius: 4px;
border-collapse: collapse;
background: var(--bg-shade-3);
margin-bottom: 30px;
overflow: hidden;
}
.blog-card table th {
padding: 8px 12px;
background: #3f4778;
color: var(--text);
padding: 8px 12px;
background: var(--bg-shade-4);
color: var(--text-shade-3);
}
.blog-card table td {
padding: 8px 12px;
vertical-align: top;
border-radius: inherit;
padding: 8px 12px;
vertical-align: top;
border-radius: inherit;
}
.blog-card table tr:nth-child(even) {
background: #2a2f50;
background: var(--bg-shade-2);
}
.blog-card pre code {
border-radius: 4px;
margin-bottom: 30px;
border-radius: 4px;
margin-bottom: 30px;
}
.blog-card input[type="checkbox"] {
appearance: none;
-webkit-appearance: none;
display: inline-block;
background: var(--btn-secondary);
padding: 12px;
margin: 4px;
border-radius: 4px;
vertical-align: -60%;
appearance: none;
-webkit-appearance: none;
display: inline-block;
background: var(--bg-shade-3);
padding: 12px;
margin: 4px;
border-radius: 4px;
vertical-align: -60%;
}
.blog-card input[type="checkbox"]:checked {
content: "checkboxtest";
background: no-repeat center/contain url(../images/check.svg),
var(--btn-secondary);
content: "checkboxtest";
background: no-repeat center/contain url(../images/check.svg),
var(--bg-shade-3);
}
.blog-card hr {
border: 1px solid var(--text-secondary);
margin: 30px 0;
border: 1px solid var(--text-shade-1);
margin: 30px 0;
}
.blog-card blockquote {
border-left: 2px solid var(--text-secondary);
padding: 8px 24px;
margin: 0;
margin-bottom: 30px;
border-left: 2px solid var(--text-shade-1);
padding: 8px 24px;
margin: 0;
margin-bottom: 30px;
}
@media screen and (max-width: 800px) {
.blog-card {
padding: 40px;
}
.blog-card {
padding: 40px;
}
}
@media screen and (max-width: 600px) {
.wrapper {
width: 100%;
}
.wrapper {
width: 100%;
}
header {
width: 90%;
margin: 35px auto;
}
header {
width: 90%;
margin: 35px auto;
}
.blog-card {
padding: 40px 5vw;
border-radius: 0;
margin-top: 0;
}
.blog-card {
padding: 40px 5vw;
border-radius: 0;
margin-top: 0;
}
footer {
width: 95%;
margin: auto auto 40px;
}
footer {
width: 95%;
margin: auto auto 40px;
}
}

View File

@ -0,0 +1,146 @@
/* BUTTONS */
button,
.button {
appearance: none;
-webkit-appearance: none;
border: 0;
border-radius: 6px;
font-family: Poppins, Arial, Helvetica, sans-serif;
font-size: 1rem;
color: var(--text-shade-3);
padding: 12px 48px;
background: var(--bg-shade-3);
cursor: pointer;
display: block;
text-align: center;
}
button:hover,
.button:hover {
background: var(--bg-shade-4);
}
button.primary,
.button.primary {
background: var(--accent-shade-0);
}
button.primary:hover,
.button.primary:hover {
background: var(--accent-shade-1);
}
button.secondary.icon-btn,
.button.secondary.icon-btn {
width: 50px;
height: 50px;
display: flex;
justify-content: center;
align-items: center;
}
button svg,
.button svg {
width: 30px;
height: 30px;
display: block;
}
/* MODALS */
body.modal-open {
overflow: hidden;
}
div.modal-wrapper {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background: rgba(0, 0, 0, 0.6);
z-index: 10;
}
div.modal-wrapper.hidden {
display: none;
}
div.modal {
background: var(--bg-shade-3);
padding: 48px;
border-radius: 8px;
text-align: left;
width: min(660px, 90%);
box-sizing: border-box;
}
div.modal h1 {
margin-top: 0;
}
p.modal-caption {
color: var(--text-shade-1);
}
p.modal-caption span,
p.switch-tier-modal-caption span {
color: var(--text-shade-3);
}
.modal-button-wrapper {
margin-top: 24px;
display: flex;
justify-content: flex-end;
}
.modal-button-wrapper button {
margin-left: 12px;
width: fit-content;
}
.modal-button-wrapper button.cancel {
background: none;
}
.modal-button-wrapper button {
padding: 12px 24px;
}
@media screen and (max-width: 600px) {
div.modal {
padding: 24px;
}
}
/* MISC FORM COMPONENTS */
input[type="checkbox"] {
appearance: none;
-webkit-appearance: none;
background: var(--bg-shade-3);
padding: 12px;
margin: 4px;
margin-left: 0;
border-radius: 4px;
vertical-align: -65%;
width: fit-content;
cursor: pointer;
}
input[type="checkbox"]:checked {
background: no-repeat center/contain url(../images/check.svg), var(--accent-shade-0);
}
input {
appearance: none;
-webkit-appearance: none;
display: block;
font-family: Poppins, Arial, Helvetica, sans-serif;
font-size: 1rem;
background-color: var(--bg-shade-3);
border: none;
border-radius: 4px;
padding: 12px;
color: var(--text-shade-3);
width: calc(100% - 24px);
}
input:focus {
background-color: var(--bg-shade-4);
outline: none;
transition: 150ms;
}

View File

@ -1,221 +1,221 @@
html,
body {
background: #131730;
background: var(--bg-shade-0);
}
a.logo-link {
margin: auto;
margin-left: 36px;
height: 40px;
text-decoration: none;
margin: auto;
margin-left: 36px;
height: 40px;
text-decoration: none;
}
header {
width: calc(100% - 72px);
background: #131730;
padding: 12px 36px;
margin: 0;
.docs-wrapper header {
width: calc(100% - 72px);
background: var(--bg-shade-0);
padding: 12px 36px;
margin: 18px 0;
}
header a.logo-link {
display: none;
display: none;
}
header nav a:first-child {
margin-left: -24px;
margin-left: -24px;
}
.docs-wrapper {
display: grid;
grid-template-columns: repeat(2, fit-content(100%));
grid-template-rows: repeat(2, fit-content(100%));
height: 100vh;
display: grid;
grid-template-columns: repeat(2, fit-content(100%));
grid-template-rows: repeat(2, fit-content(100%));
height: 100vh;
}
.docs-wrapper .sidebar {
display: flex;
flex-flow: column;
align-items: center;
width: clamp(270px, 30vw, 500px);
background: #161931;
max-height: calc(100vh - 69px);
overflow-y: scroll;
min-height: 100%;
display: flex;
flex-flow: column;
align-items: center;
width: clamp(270px, 30vw, 500px);
background: var(--bg-shade-0);
max-height: calc(100vh - 69px);
overflow-y: scroll;
min-height: 100%;
}
.docs-wrapper .sidebar .section {
display: flex;
flex-flow: column;
width: 200px;
margin-left: 138px;
margin-bottom: 72px;
display: flex;
flex-flow: column;
width: 200px;
margin-left: 138px;
margin-bottom: 72px;
}
.docs-wrapper .sidebar .section:first-child {
margin-top: 72px;
margin-top: 72px;
}
.docs-wrapper .sidebar .section h5 {
margin: 0;
font-weight: normal;
text-transform: uppercase;
color: var(--text-secondary-2);
margin-bottom: 12px;
margin: 0;
font-weight: normal;
text-transform: uppercase;
color: var(--text-shade-0);
margin-bottom: 12px;
}
.docs-wrapper .sidebar .section a {
position: relative;
text-decoration: none;
color: var(--text-secondary);
width: fit-content;
margin-bottom: 12px;
position: relative;
text-decoration: none;
color: var(--text-shade-1);
width: fit-content;
margin-bottom: 12px;
}
.docs-wrapper .sidebar .section a.active,
.docs-wrapper .sidebar .section a:hover {
color: var(--text);
color: var(--text-shade-3);
}
.docs-wrapper .sidebar .section a.active::before {
/* This filter thing is jank, if anyone knows a better way to do this please fix */
filter: invert(51%) sepia(12%) saturate(2930%) hue-rotate(218deg)
brightness(99%) contrast(92%);
position: absolute;
left: -30px;
content: url(../images/arrow-right.svg);
/* This filter thing is jank, if anyone knows a better way to do this please fix */
filter: invert(51%) sepia(12%) saturate(2930%) hue-rotate(218deg)
brightness(99%) contrast(92%);
position: absolute;
left: -30px;
content: url(../images/arrow-right.svg);
}
.docs-wrapper .content {
width: calc(100vw - clamp(270px, 30vw, 500px) - (72px * 2));
background: var(--background);
padding: 72px;
max-height: calc(100vh - 69px - (72px * 2));
overflow-y: scroll;
width: calc(100vw - clamp(270px, 30vw, 500px) - (72px * 2));
background: var(--bg-shade-1);
padding: 72px;
max-height: calc(100vh - 69px - (72px * 2));
overflow-y: scroll;
}
.docs-wrapper .content-inner {
max-width: 900px;
max-width: 900px;
}
.docs-wrapper .content p {
color: var(--text-secondary);
color: var(--text-shade-1);
}
.docs-wrapper .content h1:first-child {
margin-top: 0;
margin-top: 0;
}
.docs-wrapper .content .quick-links-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-gap: 24px;
margin-bottom: 60px;
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-gap: 24px;
margin-bottom: 60px;
}
.docs-wrapper .quick-links-grid a {
text-decoration: none;
background: #252a51;
border-radius: 6px;
color: var(--text-secondary);
display: flex;
align-items: center;
padding: 20px;
text-decoration: none;
background: var(--bg-shade-2);
border-radius: 6px;
color: var(--text-shade-1);
display: flex;
align-items: center;
padding: 20px;
}
.docs-wrapper .quick-links-grid svg:first-child {
height: 36px;
margin-right: 24px;
margin-left: 4px;
color: var(--theme-light);
height: 36px;
margin-right: 24px;
margin-left: 4px;
color: var(--accent-shade-2);
}
.docs-wrapper .quick-links-grid p.header {
font-size: 22px;
font-weight: 600;
color: var(--text);
margin: 0;
font-size: 22px;
font-weight: 600;
color: var(--text-shade-3);
margin: 0;
}
.docs-wrapper .quick-links-grid p {
margin: 0;
margin: 0;
}
.docs-wrapper .quick-links-grid svg:last-child {
height: 36px;
margin-left: auto;
height: 36px;
margin-left: auto;
}
/* Scrollbar styling 'cause it's fancy */
.docs-wrapper .sidebar::-webkit-scrollbar,
.docs-wrapper .content::-webkit-scrollbar,
.docs-wrapper .content pre code::-webkit-scrollbar {
width: 12px;
height: 12px;
width: 12px;
height: 12px;
}
.docs-wrapper .sidebar::-webkit-scrollbar-track,
.docs-wrapper .content::-webkit-scrollbar-track,
.docs-wrapper .content pre code::-webkit-scrollbar-track {
background: none;
background: none;
}
.docs-wrapper .sidebar::-webkit-scrollbar-thumb,
.docs-wrapper .content::-webkit-scrollbar-thumb,
.docs-wrapper .content pre code::-webkit-scrollbar-thumb {
background-color: var(--text-secondary-2);
border-radius: 24px;
border: 3px solid #161931;
background-color: var(--text-shade-0);
border-radius: 24px;
border: 3px solid var(--bg-shade-1);
}
.docs-wrapper .content::-webkit-scrollbar-thumb {
border: 3px solid var(--background);
border: 3px solid var(--bg-shade-1);
}
.docs-wrapper .content pre code::-webkit-scrollbar-thumb {
border: 3px solid #0d0f20;
border: 3px solid var(--bg-shade-0);
}
.docs-wrapper .sidebar {
scrollbar-width: thin;
scrollbar-color: var(--text-secondary-2) #161931;
scrollbar-width: thin;
scrollbar-color: var(--text-shade-0) var(--bg-shade-1);
}
.docs-wrapper .content {
scrollbar-width: thin;
scrollbar-color: var(--text-secondary-2) var(--background);
scrollbar-width: thin;
scrollbar-color: var(--text-shade-0) var(--bg-shade-1);
}
.docs-wrapper .content pre codear {
scrollbar-width: thin;
scrollbar-color: var(--text-secondary-2) #0d0f20;
.docs-wrapper .content pre code {
scrollbar-width: thin;
scrollbar-color: var(--text-shade-0) var(--bg-shade-0);
}
.docs-wrapper .content .missing-in-locale-notice {
background: #252a51;
padding: 24px;
border-radius: 6px;
background: var(--bg-shade-2);
padding: 24px;
border-radius: 6px;
}
.input-wrapper {
display: flex;
margin-top: 8px;
display: flex;
margin-top: 8px;
}
.localization-form input {
appearance: none;
-webkit-appearance: none;
border: 0;
font-family: Poppins, Arial, Helvetica, sans-serif;
font-size: 1rem;
background-color: var(--btn-secondary);
border: none;
border-radius: 4px 0 0 4px;
padding: 12px 24px;
color: var(--text-secondary);
width: 20px;
flex: 2 10%;
appearance: none;
-webkit-appearance: none;
border: 0;
font-family: Poppins, Arial, Helvetica, sans-serif;
font-size: 1rem;
background-color: var(--bg-shade-3);
border: none;
border-radius: 4px 0 0 4px;
padding: 12px 24px;
color: var(--text-shade-1);
width: 20px;
flex: 2 10%;
}
.search input::placeholder {
color: var(--text-secondary-2);
color: var(--text-shade-0);
}
.search input:focus {
background-color: #fff;
color: var(--btn-secondary);
transition: 200ms;
outline: none;
background-color: #fff;
color: var(--bg-shade-3);
transition: 200ms;
outline: none;
}
.search button {
appearance: none;
-webkit-appearance: none;
border: 0;
font-family: Poppins, Arial, Helvetica, sans-serif;
font-size: 1rem;
color: var(--text);
padding: 12px 36px;
background: var(--btn);
cursor: pointer;
appearance: none;
-webkit-appearance: none;
border: 0;
font-family: Poppins, Arial, Helvetica, sans-serif;
font-size: 1rem;
color: var(--text-shade-3);
padding: 12px 36px;
background: var(--accent-shade-0);
cursor: pointer;
}
@media screen and (max-width: 1080px) {
.docs-wrapper .sidebar .section {
margin-left: 60px;
width: 184px;
}
.docs-wrapper .sidebar .section {
margin-left: 60px;
width: 184px;
}
}

View File

@ -1,106 +1,76 @@
.select-box {
display: flex;
width: 188px;
flex-direction: column;
position: relative;
user-select: none;
display: flex;
flex-direction: column;
position: relative;
user-select: none;
}
.select-box > * {
box-sizing: border-box;
box-sizing: border-box;
}
.select-box .options-container {
max-height: 0;
width: 100%;
opacity: 0;
transition: all 0.4s;
overflow: hidden;
border-radius: 5px;
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;
max-height: 0;
width: fit-content;
opacity: 0;
transition: all 0.4s;
overflow: hidden;
border-radius: 5px;
background-color: var(--bg-shade-3);
order: 1;
position: absolute;
top: 48px;
right: 0;
}
.select-box .option .item {
color: #afb5dd;
color: var(--text-shade-2);
}
.select-box .lang {
width: 1.3rem;
height: 1rem;
margin-right: .2rem;
display: inline-block;
width: 1.3rem;
height: 1rem;
margin-right: .2rem;
display: inline-block;
}
.select-box .options-container.active {
max-height: 240px;
opacity: 1;
overflow-y: auto;
max-height: 240px;
opacity: 1;
overflow-y: auto;
}
.select-box .options-container.active + .selected-locale::after {
transform: translateY(-50%) rotateX(180deg);
.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;
border-radius: 0 5px 5px 0;
width: 8px;
background: var(--bg-shade-3);
border-radius: 0 5px 5px 0;
}
.select-box .options-container::-webkit-scrollbar-thumb {
background: #525861;
background: #81878f;
border-radius: 0 5px 5px 0;
background: var(--text-shade-1);
border-radius: 0 5px 5px 0;
}
.select-box .option,
.selected-locale {
padding: 12px 15px;
cursor: pointer;
.select-box .option {
padding: 12px 15px;
cursor: pointer;
border-radius: 5px;
}
.select-box .option:hover {
background: #2f345b;
background: var(--bg-shade-4);
}
.select-box .option:hover .item {
color: white;
color: white;
}
.select-box label {
cursor: pointer;
cursor: pointer;
}
.select-box .option .radio {
display: none;
display: none;
}

View File

@ -1,137 +1,137 @@
pre code.hljs {
display: block;
overflow-x: auto;
padding: 36px;
border-radius: 10px;
font-family: Poppins, Arial, Helvetica, sans-serif;
display: block;
overflow-x: auto;
padding: 36px;
border-radius: 10px;
font-family: Poppins, Arial, Helvetica, sans-serif;
}
code.hljs {
padding: 3px 5px;
padding: 3px 5px;
}
.hljs {
background: #0D0F20;
color: #d6deeb;
background: var(--bg-shade-0);
color: #d6deeb;
}
.hljs-keyword {
color: #c792ea;
font-style: italic;
color: var(--accent-shade-2);
font-style: italic;
}
.hljs-built_in {
color: #addb67;
font-style: italic;
color: #addb67;
font-style: italic;
}
.hljs-type {
color: #82aaff;
color: #82aaff;
}
.hljs-literal {
color: #ff5874;
color: #ff5874;
}
.hljs-number {
color: #f78c6c;
color: #f78c6c;
}
.hljs-regexp {
color: #5ca7e4;
color: #5ca7e4;
}
.hljs-string {
color: #ecc48d;
color: #ecc48d;
}
.hljs-subst {
color: #d3423e;
color: #d3423e;
}
.hljs-symbol {
color: #82aaff;
color: #82aaff;
}
.hljs-class {
color: #ffcb8b;
color: #ffcb8b;
}
.hljs-function {
color: #82aaff;
color: #82aaff;
}
.hljs-title {
color: #dcdcaa;
font-style: italic;
color: #dcdcaa;
font-style: italic;
}
.hljs-params {
color: #7fdbca;
color: #7fdbca;
}
.hljs-comment {
color: #637777;
font-style: italic;
color: #637777;
font-style: italic;
}
.hljs-doctag {
color: #7fdbca;
color: #7fdbca;
}
.hljs-meta,
.hljs-meta .hljs-keyword {
color: #82aaff;
color: #82aaff;
}
.hljs-meta .hljs-string {
color: #ecc48d;
color: #ecc48d;
}
.hljs-section {
color: #82b1ff;
color: #82b1ff;
}
.hljs-attr,
.hljs-name,
.hljs-tag {
color: #7fdbca;
color: #7fdbca;
}
.hljs-attribute {
color: #80cbc4;
color: #80cbc4;
}
.hljs-variable {
color: #addb67;
color: #addb67;
}
.hljs-bullet {
color: #d9f5dd;
color: #d9f5dd;
}
.hljs-code {
color: #80cbc4;
color: #80cbc4;
}
.hljs-emphasis {
color: #c792ea;
font-style: italic;
color: #c792ea;
font-style: italic;
}
.hljs-strong {
color: #addb67;
font-weight: 700;
color: #addb67;
font-weight: 700;
}
.hljs-formula {
color: #c792ea;
color: #c792ea;
}
.hljs-link {
color: #ff869a;
color: #ff869a;
}
.hljs-quote {
color: #697098;
font-style: italic;
color: #697098;
font-style: italic;
}
.hljs-selector-tag {
color: #ff6363;
color: #ff6363;
}
.hljs-selector-id {
color: #fad430;
color: #fad430;
}
.hljs-selector-class {
color: #addb67;
font-style: italic;
color: #addb67;
font-style: italic;
}
.hljs-selector-attr,
.hljs-selector-pseudo {
color: #c792ea;
font-style: italic;
color: #c792ea;
font-style: italic;
}
.hljs-template-tag {
color: #c792ea;
color: #c792ea;
}
.hljs-template-variable {
color: #addb67;
color: #addb67;
}
.hljs-addition {
color: #addb67ff;
font-style: italic;
color: #addb67ff;
font-style: italic;
}
.hljs-deletion {
color: #ef535090;
font-style: italic;
color: #ef535090;
font-style: italic;
}

View File

@ -1,95 +1,95 @@
.localization-wrapper {
width: 100%;
min-height: calc(100vh - 155px);
margin: 0;
text-align: left;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
min-height: calc(100vh - 155px);
margin: 0;
text-align: left;
display: flex;
justify-content: center;
align-items: center;
}
.localization-widget {
max-width: 600px;
width: 100%;
max-width: 600px;
width: 100%;
}
.caption {
color: var(--text-secondary);
max-width: 400px;
margin: 20px 0;
color: var(--text-shade-1);
max-width: 400px;
margin: 20px 0;
}
.title.dot {
margin: 0;
margin: 0;
}
.localization-instr,
.localization-instr:visited {
display: flex;
align-items: center;
color: var(--theme-light);
text-decoration: none;
position: relative;
left: -4px;
width: fit-content;
display: flex;
align-items: center;
color: var(--accent-shade-2);
text-decoration: none;
position: relative;
left: -4px;
width: fit-content;
}
.localization-instr svg {
height: 1.3em;
margin-right: 4px;
height: 1.3em;
margin-right: 4px;
}
.localization-form {
padding: 36px;
background-color: #151934;
border-radius: 12px;
margin-top: 36px;
padding: 36px;
background-color: var(--bg-shade-0);
border-radius: 12px;
margin-top: 36px;
}
.input-wrapper {
display: flex;
margin-top: 8px;
display: flex;
margin-top: 8px;
}
.localization-form input {
appearance: none;
-webkit-appearance: none;
border: 0;
font-family: Poppins, Arial, Helvetica, sans-serif;
font-size: 1rem;
background-color: var(--btn-secondary);
border: none;
border-radius: 4px 0 0 4px;
padding: 12px 24px;
color: var(--text-secondary);
width: 20px;
flex: 2 10%;
appearance: none;
-webkit-appearance: none;
border: 0;
font-family: Poppins, Arial, Helvetica, sans-serif;
font-size: 1rem;
background-color: var(--bg-shade-3);
border: none;
border-radius: 4px 0 0 4px;
padding: 12px 24px;
color: var(--text-shade-1);
width: 20px;
flex: 2 10%;
}
.localization-form input::placeholder {
color: var(--text-secondary-2);
color: var(--text-shade-0);
}
.localization-form input:focus {
background-color: #fff;
color: var(--btn-secondary);
transition: 200ms;
outline: none;
background-color: var(--bg-shade-4);
color: var(--bg-shade-3);
transition: 200ms;
outline: none;
}
.localization-form button {
appearance: none;
-webkit-appearance: none;
border: 0;
border-radius: 0 4px 4px 0;
font-family: Poppins, Arial, Helvetica, sans-serif;
font-size: 1rem;
color: var(--text);
padding: 12px 36px;
background: var(--btn);
cursor: pointer;
appearance: none;
-webkit-appearance: none;
border: 0;
border-radius: 0 4px 4px 0;
font-family: Poppins, Arial, Helvetica, sans-serif;
font-size: 1rem;
color: var(--text-shade-3);
padding: 12px 36px;
background: var(--accent-shade-0);
cursor: pointer;
}
footer {
margin-top: auto;
margin-top: auto;
}

View File

@ -1,146 +1,132 @@
.wrapper {
display: flex;
min-height: 100vh;
display: flex;
flex-flow: column;
min-height: 100vh;
}
header {
margin: 35px 0;
}
.account-form-wrapper {
margin: auto;
width: fit-content;
overflow: hidden;
}
.account-form-wrapper .logotype {
display: block;
position: relative;
left: -6px;
margin-bottom: 30px;
}
.account-form-wrapper .logotype svg {
width: 140px;
}
.account-form-wrapper .logotype svg text {
font-size: 20px;
transform: translate(593px, 495px);
margin: auto;
width: fit-content;
overflow: hidden;
}
form.account {
display: block;
padding: 40px 48px;
background-color: #292E53;
color: var(--text-secondary);
border-radius: 12px;
width: 360px;
max-width: calc(90vw - 96px);
display: block;
padding: 40px 48px;
background-color: var(--bg-shade-2);
color: var(--text-shade-1);
border-radius: 12px;
width: min(480px, 90vw);
box-sizing: border-box;
}
form.account h2 {
margin: 0;
color: var(--text)
margin: 0;
color: var(--text-shade-3);
}
form.account p {
margin: 12px 0;
margin: 12px 0;
}
form.account div {
margin-top: 24px;
}
form.account input {
appearance: none;
-webkit-appearance: none;
display: block;
font-family: Poppins, Arial, Helvetica, sans-serif;
font-size: 1rem;
background-color: #353C6A;
border: none;
border-radius: 4px;
padding: 12px;
color: var(--text);
width: calc(100% - 24px);
}
form.account input:focus {
background-color: #4B5595;
outline: none;
transition: 150ms;
margin-top: 24px;
}
form.account label {
display: block;
margin-bottom: 6px;
text-transform: uppercase;
font-size: 12px;
display: block;
margin-bottom: 6px;
text-transform: uppercase;
font-size: 12px;
}
form.account button {
appearance: none;
-webkit-appearance: none;
display: block;
border: none;
border-radius: 4px;
font-family: Poppins, Arial, Helvetica, sans-serif;
font-size: 1rem;
color: var(--text);
padding: 12px 30px;
background: #353C6A;
margin-top: 18px;
cursor: pointer;
width: 100%;
text-decoration: none;
}
form.account button[type="submit"] {
background: var(--btn);
margin: auto;
width: 100%;
background: var(--accent-shade-0);
}
form.account a {
text-decoration: none;
display: block;
color: var(--text-secondary);
text-align: right;
margin: 6px 0;
width: fit-content;
text-decoration: none;
display: block;
color: var(--text-shade-1);
text-align: right;
margin: 6px 0;
width: fit-content;
}
form.account a:hover {
color: var(--text);
color: var(--text-shade-3);
}
form.account a.pwdreset {
margin-left: auto;
font-size: 14px;
margin-left: auto;
font-size: 14px;
}
form.account a.register {
margin:auto;
margin-top: 18px;
}
form.account.register div:last-child {
margin-top: 42px;
margin: auto;
margin-top: 18px;
}
@keyframes banner-notice {
0% {top: -150px}
20% {top: 35px}
80% {top: 35px}
100% {top: -150px}
0% {
top: -150px;
}
20% {
top: 35px;
}
80% {
top: 35px;
}
100% {
top: -150px;
}
}
.banner-notice {
display: flex;
justify-content: center;
position: fixed;
top: -150px;
width: 100%;
animation: banner-notice 5s;
display: flex;
justify-content: center;
position: fixed;
top: -150px;
width: 100%;
animation: banner-notice 5s;
}
.banner-notice div {
padding: 4px 36px;
border-radius: 5px;
z-index: 3;
padding: 4px 36px;
border-radius: 5px;
z-index: 3;
}
.banner-notice.error div {
background: #A9375B;
}
background: var(--red-shade-1);
}
form.account.register {
display: grid;
grid-template-columns: repeat(2, 1fr);
width: min(780px, 90vw);
column-gap: 24px;
margin-bottom: 48px;
}
form.account.register div.h-captcha {
grid-column: 1 / span 2;
display: flex;
justify-content: center;
}
form.account.register p,
form.account.register div.email,
form.account.register div.buttons {
grid-column: 1 / span 2;
}
@media screen and (max-width: 720px) {
form.account.register {
grid-template-columns: 1fr;
}
form.account.register div.h-captcha,
form.account.register p,
form.account.register div.email,
form.account.register div.buttons {
grid-column: unset;
}
}

View File

@ -1,18 +1,32 @@
:root {
--background: #1B1F3B;
--text: white;
--text-secondary: #A1A8D9;
--btn: #673DB6;
--btn-secondary: #333960;
--theme: var(--btn);
--theme-light: #A185D6;
--text-secondary-2: #8990C1;
/* 1 is the base color, <1 are darker variations and >1 are lighter */
--bg-shade-0: #131733;
--bg-shade-1: #1B1F3B;
--bg-shade-2: #23274A;
--bg-shade-3: #373C65;
--bg-shade-4: #494F81;
--accent-shade-0: #673DB6;
--accent-shade-1: #9D6FF3;
--accent-shade-2: #A185D6;
--accent-shade-3: #CAB1FB;
--text-shade-0: #8990C1;
--text-shade-1: #A1A8D9;
--text-shade-2: #CAC1F5;
--text-shade-3: #fff;
--green-shade-0: #37a985;
--green-shade-1: #59c9a5;
--red-shade-1: #a9375b;
--border: rgba(255, 255, 255, 0.1);
}
body {
background: var(--background);
background: var(--bg-shade-1);
padding-bottom: env(safe-area-inset-bottom);
}
body, .main-body {
@ -21,7 +35,7 @@ body, .main-body {
position: relative; /* This fixes overflow-x not hiding on Safari on mobile */
overflow-x: hidden;
margin: 0;
color: var(--text);
color: var(--text-shade-3);
justify-content: center;
font-family: Poppins, Arial, Helvetica, sans-serif;
}
@ -55,49 +69,17 @@ body, .main-body {
}
h1.dot:not([data-title-suffix]):after, h2.dot:not([data-title-suffix]):after {
/* content: ".";
color: #9D6FF3; */
content: "";
background-color: #9D6FF3;
width: 0.6rem;
height: 0.6rem;
border-radius: 50%;
display: inline-block;
background-color: var(--accent-shade-1);
width: 0.6rem;
height: 0.6rem;
border-radius: 50%;
display: inline-block;
}
h1.dot[data-title-suffix]:after, h2.dot[data-title-suffix]:after {
content: attr(data-title-suffix);
display: inline-block;
color: #9D6FF3;
}
header {
display: flex;
align-items: center;
margin-top: 35px;
}
header a {
text-decoration: none;
}
header .logo-link, header .logo-link svg {
display: block;
}
header nav a:first-child {
margin-left: 40px;
}
header nav a {
color: var(--text-secondary);
margin: 0 17px;
text-decoration: none;
}
header nav a:hover {
color: var(--text);
transition: color 50ms ease-in-out;
}
.locale-dropdown {
margin-left: auto;
z-index: 2;
height: 45px;
color: var(--accent-shade-1);
}
/* Misc */
@ -106,14 +88,13 @@ header nav a:hover {
z-index: -2;
width: 2100px;
height: 1700px;
background: #111531;
background: var(--bg-shade-0);
border-radius: 50%;
top: -250px;
right: -1420px;
/* left: 60vw; */
}
.light-purple-circle {
background: #CAB1FB;
background: var(--accent-shade-3);
width: 500px;
height: 500px;
position: relative;
@ -180,7 +161,7 @@ img.emoji {
max-width: 423px;
}
.progress-hero .text a {
color: #9d6ff3;
color: var(--accent-shade-1);
text-decoration: none;
font-weight: bold;
}
@ -218,7 +199,7 @@ img.emoji {
}
.text {
font-size: 1rem;
color: var(--text-secondary);
color: var(--text-shade-1);
width: 86%;
line-height: 1.8;
margin: 0;
@ -235,26 +216,11 @@ img.emoji {
align-items: flex-start;
}
.hero .buttons a {
display: block;
text-decoration: none;
}
.hero .buttons a > * {
cursor: pointer;
}
.button {
appearance: none;
-webkit-appearance: none;
border: 0;
border-radius: 6px;
font-family: Poppins, Arial, Helvetica, sans-serif;
font-size: 1rem;
color: var(--text);
}
.button.primary {
padding: 12px 48px;
background: var(--btn);
}
.button.secondary {
background: var(--btn-secondary);
.hero .button.secondary {
padding: 0;
}
.button.secondary.discord svg {
margin-bottom: -1px;
@ -263,18 +229,6 @@ img.emoji {
margin-bottom: 15px;
margin-right: 15px;
}
.button.secondary.icon-btn {
width: 50px;
height: 50px;
display: flex;
justify-content: center;
align-items: center;
}
.button svg {
width: 30px;
height: 30px;
display: block;
}
/* Non-hero sections */
.sect .title {
@ -302,13 +256,13 @@ section.progress .left, section.progress .right {
section.progress .right {
position: relative;
padding-left: 80px;
background: var(--theme);
background: var(--accent-shade-0);
border-top-left-radius: 10px;
border-bottom-left-radius: 10px;
}
section.progress .right .title a {
text-decoration: none;
color: var(--text);
color: var(--text-shade-3);
}
section.progress .right .title a:hover {
text-decoration: underline;
@ -321,7 +275,7 @@ section.progress .right:before {
background: yellow;
left: 90%;
margin-top: -50px;
background: var(--theme);
background: var(--accent-shade-0);
z-index: -1;
}
@ -339,23 +293,23 @@ section.faq {
.question-and-answer summary {
font-size: 1.5625rem;
color: #B8BDDF;
color: var(--text-shade-2);
}
.question-and-answer[open] summary {
color: var(--text);
color: var(--text-shade-3);
}
.question-and-answer summary:hover {
color: var(--text);
color: var(--text-shade-3);
text-decoration: underline;
cursor: pointer;
}
.question-and-answer .text {
margin: 5px 0;
line-height: 1.85;
color: var(--text-secondary-2);
color: var(--text-shade-0);
}
.question-and-answer .text a {
color: #9d6ff3;
color: var(--accent-shade-1);
text-decoration: none;
font-weight: bold;
}
@ -373,11 +327,11 @@ section.showcase::before {
content: "";
width: 400vw;
margin-left: -50vw;
background: linear-gradient(180deg, rgba(19, 22, 36, 0) 0%, #131624 100%);
background: linear-gradient(180deg, rgba(19, 22, 36, 0) 0%, var(--bg-shade-0) 100%);
position: absolute;
top: 0;
bottom: 0;
z-index: -1;
z-index: -1;
}
section.showcase .text {
max-width: 504px;
@ -389,16 +343,16 @@ section.showcase .grid {
grid-gap: 24px;
}
section.showcase .grid .item {
background: #252A51;
background: var(--bg-shade-2);
border-radius: 10px;
padding: 42px 36px;
}
section.showcase .grid .item.highlight {
border: 2px solid #9D6FF3;
border: 2px solid var(--accent-shade-1);
}
section.showcase .grid .item svg {
height: 36px;
color: #9D6FF3;
color: var(--accent-shade-1);
}
section.showcase .grid .item h1 {
margin-top: 14px;
@ -407,7 +361,7 @@ section.showcase .grid .item h1 {
}
section.showcase .grid .item p {
margin: 0;
color: var(--text-secondary)
color: var(--text-shade-1)
}
section.team {
@ -446,7 +400,7 @@ section.team {
display: grid;
grid-template-columns: 110px 1fr;
grid-gap: 20px;
background: #16192D;
background: var(--bg-shade-0);
grid-column: span 2;
border-radius: 10px;
align-items: center;
@ -464,7 +418,7 @@ section.team {
margin: 0;
}
.card .sub {
color: #59C9A5;
color: var(--green-shade-1);
font-size: .875rem;
}
.card .title {
@ -483,7 +437,7 @@ section.team {
display: block;
}
.card .text {
color: var(--text-secondary);
color: var(--text-shade-1);
}
section.team-helpers {
@ -496,30 +450,27 @@ section.team-helpers .text {
}
section.team-helpers .team-helpers-cards {
display: inline-grid;
grid-template-columns: repeat(9, 1fr);
grid-template-columns: repeat(12, 1fr);
grid-gap: 20px;
margin-right: 20px;
color: var(--text-secondary);
color: var(--text-shade-1);
width: max-content;
}
section.team-helpers .row.second .team-helpers-cards {
grid-template-columns: repeat(12, 1fr);
}
section.team-helpers .row {
width: fit-content;
}
section.team-helpers .row.first {
animation: infiniteScrollRow1 15s linear infinite; /* Set the duration to 5s times the number of cards */
animation: infiniteScrollRow1 25s linear infinite;
}
section.team-helpers .row.second {
animation: infiniteScrollRow2 20s linear infinite; /* Set the duration to 5s times the number of cards */
animation: infiniteScrollRow2 25s linear infinite;
}
@keyframes infiniteScrollRow1 {
0% { transform: translate3d(0); }
@keyframes infiniteScrollRow1 {
0% { transform: translate3d(0); }
100% { transform: translate3d(calc(100% / -3), 0, 0); }
}
@keyframes infiniteScrollRow2 {
0% { transform: translate3d(calc(100% / -3), 0, 0); }
@keyframes infiniteScrollRow2 {
0% { transform: translate3d(calc(100% / -3), 0, 0); }
100% { transform: translate3d(0, 0, 0); }
}
section.team-helpers .animation-wrapper {
@ -535,7 +486,7 @@ section.team-helpers .animation-wrapper::after {
width: 101%; /* If set to 100% it doesn't cover it completely */
left: -0.5%;
height: 100%;
background: linear-gradient(90deg, var(--background) 0%, rgba(27, 31, 59, 0) 20%, rgba(27, 31, 59, 0) 80%, var(--background) 100%);
background: linear-gradient(90deg, var(--bg-shade-1) 0%, rgba(27, 31, 59, 0) 20%, rgba(27, 31, 59, 0) 80%, var(--bg-shade-1) 100%);
z-index: 1;
pointer-events: none;
}
@ -555,7 +506,7 @@ section.team-helpers .animation-wrapper .row-wrapper::before {
width: 100%;
left: calc(-100% - 20px);
height: 100%;
background: var(--background);
background: var(--bg-shade-1);
z-index: 2;
pointer-events: none;
}
@ -566,7 +517,7 @@ section.team-helpers .animation-wrapper .row-wrapper::after {
width: 100%;
left: calc(100% - 20px);
height: 100%;
background: var(--background);
background: var(--bg-shade-1);
z-index: 2;
pointer-events: none;
}
@ -583,12 +534,12 @@ section.team-helpers .helper-card {
flex-direction: row;
align-items: center;
padding: 14px 24px;
background: #16192D;
background: var(--bg-shade-0);
border-radius: 12px;
min-width: 480px;
}
section.team-helpers .helper-card:hover {
background: #252A51;
background: var(--bg-shade-2);
transition: 200ms;
}
section.team-helpers .helper-card.special {
@ -620,12 +571,12 @@ section.team-helpers .helper-card .pfp {
height: 48px;
}
section.team-helpers .helper-card span {
color: var(--text);
color: var(--text-shade-3);
font-weight: 600;
margin-right: 0.6ch;
}
section.team-helpers .helper-card p {
color: var(--text-secondary);
color: var(--text-shade-1);
}
section.update-signup {
@ -656,21 +607,21 @@ section.update-signup .floating-serverjoin {
width: 100%;
max-width: 576px;
background: #15182D;
background: var(--bg-shade-0);
border-radius: 6px;
padding: 18px;
overflow-x: hidden;
}
section.update-signup .floating-serverjoin p {
color: var(--text-secondary);
color: var(--text-shade-1);
margin: auto;
margin-left: 0;
}
section.update-signup .floating-serverjoin a {
color: var(--text);
color: var(--text-shade-3);
text-decoration: none;
background: var(--theme);
background: var(--accent-shade-0);
margin: 0;
margin-left: auto;
padding: 9px 18px;
@ -680,7 +631,7 @@ section.update-signup .floating-serverjoin a {
}
section.update-signup div.circle {
display: block;
background: #202442;
background: var(--bg-shade-2);
/* the next 4 lines make it so the circle is always the same aspect ratio and covers enough of the screen */
width: 2591px;
height: 1454px;
@ -693,6 +644,44 @@ section.update-signup div.circle {
}
/* Progress */
.donation-progress {
padding: 50px 20px;
border-radius: 10px;
background: var(--bg-shade-0);
grid-column: span 2;
}
.donation-progress h1 {
display: inline-block;
margin: 0;
}
.donation-progress span {
font-weight: bold;
}
.donation-progress a {
color: var(--accent-shade-1);
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(--bg-shade-3);
overflow: hidden;
}
.progress-bar-inner {
height: 100%;
background-color: var(--accent-shade-0);
}
.all-progress-lists {
margin-top: 50px;
display: grid;
@ -726,7 +715,7 @@ section.update-signup div.circle {
}
.progress-title a.github {
margin-top: 10px;
color: #6D73A2;
color: var(--text-shade-0);
display: inline-flex;
align-items: center;
opacity: .75;
@ -734,7 +723,7 @@ section.update-signup div.circle {
transition: color 50ms ease-in-out;
}
.progress-title a.github:focus, .progress-title a.github:hover, .progress-title a.github:visited {
color: #6D73A2;
color: var(--text-shade-0);
text-decoration: none;
}
.progress-title a.github:hover {
@ -745,7 +734,7 @@ section.update-signup div.circle {
margin-right: .4rem;
}
.feature-list-wrapper.purple .progress-title a.github, .feature-list-wrapper.purple .progress-title a.github:focus, .feature-list-wrapper.purple .progress-title a.github:hover, .feature-list-wrapper.purple .progress-title a.github:visited {
color: #c69cf9;
color: var(--accent-shade-3);
}
.feature-list-wrapper .core > .progress-title a.github:hover {
color: white;
@ -786,8 +775,8 @@ section.update-signup div.circle {
.custom-checkbox {
width: 1.5rem;
height: 1.5rem;
background: #31365A;
color: var(--text);
background: var(--bg-shade-3);
color: var(--text-shade-3);
border-radius: 2px;
display: flex;
justify-content: center;
@ -814,90 +803,7 @@ section.update-signup div.circle {
.purple-card {
padding: 50px 20px;
border-radius: 10px;
background: #151934;
}
/* Footer */
footer {
width: 100%;
display: grid;
grid-template-columns: repeat(3, fit-content(100%)) 1fr;
gap: 7.7vw;
color: var(--text-secondary);
margin-top: 120px;
position: relative;
padding: 60px 0;
}
footer::after {
content: "";
width: 400vw;
height: 100%;
position: absolute;
top: 0;
left: -50vw;
background: #15182D;
z-index: -1;
}
footer div {
display: flex;
flex-flow: column;
width: fit-content;
}
footer svg.logotype {
height: 56px;
width: fit-content;
margin: -10px 0 24px -10px;
}
footer p {
margin: 0;
}
footer h1 {
font-size: 20px;
margin-top: 0;
color: var(--text);
}
footer a {
color: var(--text-secondary);
text-decoration: none;
width: fit-content;
}
footer a:hover {
color: var(--text);
text-decoration: underline;
}
footer div.discord-server-card {
background: #222641;
border-radius: 12px;
padding: 30px 90px 30px 36px;
justify-self: end;
}
footer div.discord-server-card h1 {
font-size: 25px;
margin: 0;
}
footer div.discord-server-card h2 {
color: var(--text);
font-size: 22px;
margin: 0;
}
footer div.discord-server-card a {
display: flex;
align-items: center;
color: #CAB1FB;
font-size: 22px;
text-decoration: none;
width: fit-content;
margin-left: -2px;
margin-top: 12px;
}
footer div.discord-server-card a:hover {
text-decoration: underline;
}
footer div.discord-server-card svg {
height: 24px;
stroke-width: 3px;
margin-right: 4px;
background: var(--bg-shade-0);
}
@media screen and (min-width: 701px) and (max-width: 1500px) {
@ -916,8 +822,6 @@ footer div.discord-server-card svg {
right: auto;
left: 60vw;
}
}
@media screen and (max-width: 900px) {
@ -928,20 +832,14 @@ footer div.discord-server-card svg {
.feature-list-wrapper {
grid-template-columns: 100%;
}
header nav a:not(.keep-on-mobile) {
/* You don't really need it on mobile IMO */
display: none;
}
header .logo-link {
margin-right: 20px;
.donation-progress {
grid-column: span 1;
}
.hero-meta {
margin-top: 100px;
}
.wrapper {
width: 90%;
}
@ -994,15 +892,15 @@ footer div.discord-server-card svg {
font-size: 1.1rem;
}
section.showcase {
margin-top: 0;
margin-top: 0;
}
section.showcase .grid {
grid-template-columns: 1fr;
grid-template-rows: repeat(3, 1fr);
}
section.team,
section.team-helpers {
margin-top: 100px;
@ -1039,20 +937,6 @@ footer div.discord-server-card svg {
padding-bottom: 90px;
}
footer {
margin-top: 100px;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(2, fit-content(100%));
}
footer div {
justify-self: center;
}
footer div.discord-server-card {
grid-column: 1 / span 4;
width: calc(100% - 126px);
justify-self: normal;
}
.text {
width: 100%;
}
@ -1077,7 +961,7 @@ footer div.discord-server-card svg {
}
section.showcase {
padding: 160px 0;
padding: 160px 0;
}
section.showcase p.text {
margin-bottom: 82px;
@ -1100,7 +984,7 @@ footer div.discord-server-card svg {
section.team-helpers .team-helpers-cards {
grid-gap: 12px;
margin-right: 12px;
margin-right: 12px;
}
section.team-helpers .animation-wrapper .helper-card {
padding: 7px 18px;
@ -1118,63 +1002,4 @@ 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%));
}
footer div {
justify-self: start;
}
footer div.discord-server-card {
grid-column: 1 / span 1;
padding: 30px;
width: calc(100% - 60px);
}
}
@media screen and (max-width: 480px) {
header .logo-link svg text {
display: none;
}
header .logo-link svg {
width: 39.876px;
}
header .logo-link {
margin-right: 10px;
}
header nav a {
margin: 0 10px;
}
}
@media screen and (max-width: 330px) {
.locale-dropdown .selected-locale {
width: 50px;
}
.locale-dropdown .selected-locale::after {
display: none;
}
}

View File

@ -1,252 +1,250 @@
body,
div.main-body,
.miieditor-wrapper {
z-index: -1;
user-select: none;
z-index: -1;
user-select: none;
}
svg.logotype {
position: absolute;
width: 120px;
top:42px;
left:162px;
position: absolute;
width: 120px;
top:42px;
left:162px;
}
.miieditor-wrapper {
position: relative;
display: grid;
grid-template-columns: 2fr 3fr;
background: #0d0f20;
width: 100vw;
height: 100vh;
gap: 0 48px;
position: relative;
display: grid;
grid-template-columns: 2fr 3fr;
background: var(--bg-shade-0);
width: 100vw;
height: 100vh;
gap: 0 48px;
}
.miieditor-wrapper::after {
content: "";
display: block;
position: absolute;
background: radial-gradient(closest-side, #161d40 0%, transparent 100%);
width: 200vh;
height: 200vh;
top: -100vh;
left: -100vh;
z-index: -1;
content: "";
display: block;
position: absolute;
background: radial-gradient(closest-side, var(--bg-shade-1) 0%, transparent 100%);
width: 200vh;
height: 200vh;
top: -100vh;
left: -100vh;
z-index: -1;
}
div.mii-img-wrapper {
position: relative;
margin: auto;
max-width: 512px;
width: 26vw;
height: auto;
position: relative;
margin: auto;
max-width: 512px;
width: 26vw;
height: auto;
}
img#mii-img {
position: relative;
width: 512px;
height: auto;
z-index: 2;
position: relative;
width: 512px;
height: auto;
z-index: 2;
}
div.mii-img-wrapper .shadow {
position: absolute;
bottom: -18px;
left: 6px;
height: 72px;
width: 512px;
background: #1a214c;
background: radial-gradient(farthest-side, #1a214c 0%, transparent 100%);
position: absolute;
bottom: -18px;
left: 6px;
height: 72px;
width: 512px;
background: radial-gradient(farthest-side, var(--bg-shade-2) 0%, transparent 100%);
}
div.params-wrapper {
position: relative;
overflow-x: visible;
margin: auto;
margin-top: 150px;
margin-right: 100px;
display: grid;
z-index: 3;
position: relative;
overflow-x: visible;
margin: auto;
margin-top: 150px;
margin-right: 100px;
display: grid;
z-index: 3;
}
div.tabs {
display: grid;
grid-auto-flow: column;
width: fit-content;
gap: 2px;
background: #0A0C19;
padding: 6px;
border-radius: 6px;
margin-bottom: 32px;
display: grid;
grid-auto-flow: column;
width: fit-content;
gap: 2px;
background: #0A0C19;
padding: 6px;
border-radius: 6px;
margin-bottom: 32px;
}
div.tabs .tabbtn {
min-width: 42px;
min-height: 42px;
border: none;
border-radius: 6px;
cursor: pointer;
background: none;
color: var(--text);
min-width: 42px;
min-height: 42px;
border: none;
border-radius: 6px;
cursor: pointer;
background: none;
color: var(--text-shade-3);
}
div.tabs .tabbtn:hover,
div.tabs .tabbtn.active {
background: #1d203d;
background: var(--bg-shade-1);
}
div.subtabs {
position: relative;
grid-column: 1 / span 2;
position: relative;
grid-column: 1 / span 2;
}
div.subtabs .subtabbtn {
position: relative;
border: none;
padding: 12px;
border-radius: 6px;
cursor: pointer;
background: none;
color: var(--text);
position: relative;
border: none;
padding: 12px;
border-radius: 6px;
cursor: pointer;
background: none;
color: var(--text-shade-3);
}
div.subtabs .subtabbtn.active::before,
div.subtabs .subtabbtn.active:hover::before {
content: "";
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 5px;
background: #9D6FF3;
border-radius: 6px;
content: "";
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 5px;
background: var(--accent-shade-1);
border-radius: 6px;
}
.subtab.has-sliders {
grid-template-columns: 1fr 1fr;
grid-template-columns: 1fr 1fr;
}
form.params {
grid-template-columns: repeat(2, auto);
grid-template-columns: repeat(2, auto);
}
form.params .tab {
display: none;
grid-template-columns: 534px 258px;
gap: 48px;
display: none;
grid-template-columns: 534px 258px;
gap: 48px;
}
form.params .tab.active {
display: grid;
display: grid;
}
fieldset {
appearance: none;
border: none;
padding: 0;
margin: 0;
display: none;
grid-template-columns: repeat(4, 1fr);
gap: 18px;
width: fit-content;
height: fit-content;
grid-row: 2;
appearance: none;
border: none;
padding: 0;
margin: 0;
display: none;
grid-template-columns: repeat(4, 1fr);
gap: 18px;
width: fit-content;
height: fit-content;
grid-row: 2;
}
fieldset.active {
display: grid;
display: grid;
}
fieldset.color {
grid-template-columns: repeat(2, 1fr);
display: grid;
grid-column: 2;
grid-template-columns: repeat(2, 1fr);
display: grid;
grid-column: 2;
}
fieldset input[type="radio"] {
display: none;
display: none;
}
fieldset input[type="radio"] + label {
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
border-radius: 18px;
background: #393b5f;
width: 120px;
height: 120px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
border-radius: 18px;
background: var(--bg-shade-3);
width: 120px;
height: 120px;
}
fieldset input[type="radio"]:checked + label {
background: #3f4480;
font-weight: bold;
box-shadow: inset 0 0 0 4px #9D70F1;
background: var(--bg-shade-4);
font-weight: bold;
box-shadow: inset 0 0 0 4px var(--accent-shade-1);
}
fieldset.color input[type="radio"]:checked + label {
box-shadow: inset 0 0 0 4px #9D70F1, inset 0 0 0 6px #181B33;
box-shadow: inset 0 0 0 4px var(--accent-shade-1), inset 0 0 0 6px var(--bg-shade-1);
}
div.colorSidebar {
margin: auto;
margin: auto;
}
fieldset.has-subpages.active {
display: block;
display: block;
}
fieldset.has-subpages .subpage {
display: none;
grid-template-columns: repeat(4, 1fr);
gap: 18px;
width: fit-content;
height: fit-content;
display: none;
grid-template-columns: repeat(4, 1fr);
gap: 18px;
width: fit-content;
height: fit-content;
}
fieldset.has-subpages .subpage.active {
display: grid;
display: grid;
}
input[type="range"].invert {
direction: rtl;
direction: rtl;
}
.pagination {
display: flex;
flex-flow: row;
width: max-content;
grid-column: 3 / span 2;
grid-row: 4;
margin-left: auto;
align-items: center;
font-size: 18px;
color: var(--text-secondary);
display: flex;
flex-flow: row;
width: max-content;
grid-column: 3 / span 2;
grid-row: 4;
margin-left: auto;
align-items: center;
font-size: 18px;
color: var(--text-shade-1);
}
.pagination .current-page-index {
display: inline-block;
font-weight: bold;
color: var(--text);
width: 18px;
margin-right: 0.7ch;
text-align: right;
display: inline-block;
font-weight: bold;
color: var(--text-shade-3);
width: 18px;
margin-right: 0.7ch;
text-align: right;
}
.page-btn {
appearance: none;
border: none;
background: none;
cursor: pointer;
appearance: none;
border: none;
background: none;
cursor: pointer;
}
.page-btn svg{
height: 36px;
margin: 6px
height: 36px;
margin: 6px
}
.page-btn.disabled {
pointer-events: none;
pointer-events: none;
}
.page-btn.disabled svg path {
fill: #393B5F;
fill: var(--bg-shade-3);
}
button * {
pointer-events: none;
pointer-events: none;
}
.miieditor-wrapper::before {
content: "";
display: block;
position: absolute;
background: #181B33;
border-radius: 100%;
width: 1308px;
height: 1707px;
top: 50%;
transform: translateY(-50%);
right: -200px;
z-index: 0;
content: "";
display: block;
position: absolute;
background: var(--bg-shade-1);
border-radius: 100%;
width: 1308px;
height: 1707px;
top: 50%;
transform: translateY(-50%);
right: -200px;
z-index: 0;
}

View File

@ -0,0 +1,111 @@
footer {
width: 100%;
display: grid;
grid-template-columns: repeat(3, fit-content(100%)) 1fr;
gap: 7.7vw;
color: var(--text-shade-1);
margin-top: 120px;
position: relative;
padding: 60px 0;
}
footer::after {
content: "";
width: 400vw;
height: 100%;
position: absolute;
top: 0;
left: -50vw;
background: var(--bg-shade-0);
z-index: -1;
}
footer div {
display: flex;
flex-flow: column;
width: fit-content;
}
footer svg.logotype {
height: 56px;
width: fit-content;
margin: -10px 0 24px -10px;
}
footer p {
margin: 0;
}
footer h1 {
font-size: 20px;
margin-top: 0;
color: var(--text-shade-3);
}
footer a {
color: var(--text-shade-1);
text-decoration: none;
width: fit-content;
}
footer a:hover {
color: var(--text-shade-3);
text-decoration: underline;
}
footer div.discord-server-card {
background: var(--bg-shade-2);
border-radius: 12px;
padding: 30px 90px 30px 36px;
justify-self: end;
}
footer div.discord-server-card h1 {
font-size: 25px;
margin: 0;
}
footer div.discord-server-card h2 {
color: var(--text-shade-3);
font-size: 22px;
margin: 0;
}
footer div.discord-server-card a {
display: flex;
align-items: center;
color: var(--accent-shade-3);
font-size: 22px;
text-decoration: none;
width: fit-content;
margin-left: -2px;
margin-top: 12px;
}
footer div.discord-server-card a:hover {
text-decoration: underline;
}
footer div.discord-server-card svg {
height: 24px;
stroke-width: 3px;
margin-right: 4px;
}
@media screen and (max-width: 900px) {
footer {
margin-top: 100px;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(2, fit-content(100%));
}
footer div {
justify-self: center;
}
footer div.discord-server-card {
grid-column: 1 / span 4;
width: calc(100% - 126px);
justify-self: normal;
}
}
@media screen and (max-width: 580px) {
footer {
grid-template-columns: 1fr;
grid-template-rows: repeat(4, fit-content(100%));
}
footer div {
justify-self: start;
}
footer div.discord-server-card {
grid-column: 1 / span 1;
padding: 30px;
width: calc(100% - 60px);
}
}

View File

@ -0,0 +1,155 @@
header {
display: flex;
align-items: center;
margin-top: 35px;
}
header a {
text-decoration: none;
}
header .logo-link,
header .logo-link svg {
display: block;
}
header nav a:first-child {
margin-left: 40px;
}
header nav a {
color: var(--text-shade-1);
margin: 0 17px;
text-decoration: none;
}
header nav a:hover {
color: var(--text-shade-3);
transition: color 50ms ease-in-out;
}
header .right-section {
display: grid;
grid-auto-flow: column;
grid-gap: 24px;
margin-left: auto;
z-index: 2;
color: var(--text-shade-1);
}
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-shade-3);
}
header .user-widget-wrapper {
height: 24px;
}
header .user-widget-wrapper a.login-link {
color: var(--text-shade-1);
text-decoration: none;
transition: color 150ms;
}
header .user-widget-wrapper a.login-link:hover {
color: var(--text-shade-3);
}
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-shade-0);
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: var(--bg-shade-2);
border-radius: 8px;
text-align: center;
opacity: 0;
box-shadow: 0 0 10px -2px var(--bg-shade-0);
}
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-shade-0);
border-radius: 50%;
overflow: hidden;
}
header .user-widget .user-info {
margin-top: 12px;
}
header .user-widget .user-info .mii-name {
color: var(--text-shade-3);
}
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: var(--bg-shade-3);
color: var(--text-shade-3);
}
@media screen and (max-width: 900px) {
header nav a:not(.keep-on-mobile) {
display: none;
}
header .logo-link {
margin-right: 20px;
}
}
@media screen and (max-width: 480px) {
header .logo-link svg text {
display: none;
}
header .logo-link svg {
width: 39.876px;
}
header .logo-link {
margin-right: 10px;
}
header nav a {
margin: 0 10px;
}
}

View File

@ -1,3 +1,3 @@
/*
MOVE PROGRESS CSS HERE
*/
*/

View File

@ -0,0 +1,308 @@
.wrapper {
display: flex;
justify-content: center;
text-align: center;
min-height: 100vh;
}
.wrapper::before {
position: absolute;
top: -800px;
content: "";
background: var(--bg-shade-0);
border-radius: 100%;
width: 1600px;
height: 1400px;
}
.back-arrow {
position: absolute;
display: flex;
justify-content: center;
top: 36px;
left: max(calc((100vw - 1590px) / 2), 2.5vw);
padding: 6px 10px;
background: var(--bg-shade-3);
border-radius: 24px;
transition: filter 150ms;
text-decoration: none;
color: var(--text-shade-3);
z-index: 5;
}
.back-arrow:hover {
filter: brightness(1.5)
}
.back-arrow svg {
width: 24px;
height: 24px;
}
.back-arrow span {
margin: 0 4px;
}
.account-form-wrapper {
display: flex;
flex-flow: column;
width: min(1200px, 100%);
color: var(--text-shade-1);
margin: 0 auto 48px;
z-index: 1;
}
.account-form-wrapper .logotype {
margin: 36px auto 0;
width: fit-content;
}
h1.title {
color: var(--text-shade-3);
}
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: var(--bg-shade-2);
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-shade-3);
font-weight: 600;
}
.account-form-wrapper .progress-bar {
height: 8px;
border-radius: 4px;
margin-top: 0;
}
form {
box-sizing: border-box;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1.3rem;
}
form .tier-radio {
display: none;
}
form .tier-radio:checked + label::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
box-shadow: inset 0 0 0 4px var(--accent-shade-1);
border-radius: 10px;
}
form .tier-radio:checked + label::after {
content: url(/assets/images/check.svg);
display: flex;
justify-content: center;
background: var(--accent-shade-1);
width: 24px;
height: 24px;
border-radius: 100%;
position: absolute;
top: -16px;
right: -16px;
padding: 6px;
}
label.tier {
display: flex;
flex-flow: column;
position: relative;
border-radius: 10px;
align-items: center;
padding-top: calc(50px + 1rem);
background: var(--bg-shade-3);
cursor: pointer;
transition: all 150ms;
margin-top: 50px;
text-align: center;
}
label.tier p {
margin: 0;
margin-bottom: 0.5rem;
}
label.tier .tier-thumbnail {
height: 100px;
width: 100px;
display: flex;
align-items: center;
overflow: hidden;
border-radius: 8px;
position: absolute;
top: -50px;
z-index: 2;
background: var(--bg-shade-4);
}
form .tier-radio:checked + label .tier-thumbnail::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
box-shadow: inset 0 0 0 4px var(--accent-shade-1);
border-radius: 8px;
}
label.tier .tier-text {
display: flex;
flex-flow: column;
margin-bottom: auto;
}
label.tier .tier-name {
color: var(--text-shade-3);
font-weight: bold;
font-size: 1.2rem;
}
label.tier .tier-perks {
text-align: left;
width: 70%;
margin: 24px auto 48px;
}
label.tier .tier-perks div {
display: grid;
grid-template-columns: 16px auto;
gap: 8px;
}
label.tier .tier-perks svg {
stroke-width: 5px;
stroke: var(--green-shade-1);
stroke-linecap: square;
width: 16px;
height: 16px;
vertical-align: top;
margin-top: 0.5ex;
}
label.tier p.price {
display: flex;
width: 100%;
justify-content: center;
align-items: center;
background: var(--bg-shade-4);
margin: 0;
padding: 1.5rem 1rem;
box-sizing: border-box;
border-radius: 0 0 10px 10px;
}
label.tier p.price span {
font-size: 2rem;
color: var(--text-shade-3);
font-weight: bold;
margin-right: 0.5ch;
}
form .button-wrapper {
grid-column: 2 / span 1;
position: relative;
margin-top: 24px;
}
button {
appearance: none;
-webkit-appearance: none;
display: block;
font-family: Poppins, Arial, Helvetica, sans-serif;
font-size: 1rem;
height: fit-content;
background: var(--accent-shade-0);
border: none;
border-radius: 4px;
padding: 12px;
color: var(--text-shade-3);
width: 100%;
transition: filter 300ms;
pointer-events: all;
cursor: pointer;
filter: none;
}
form button.disabled {
pointer-events: none;
filter: brightness(0.75) saturate(0.75); /* not using opacity here 'cause in the mobile layout you would see the cards under it */
cursor: default;
}
form button.unsubscribe {
position: relative;
background: none;
color: var(--text-shade-1);
margin-top: 12px;
padding: 0;
}
form button.unsubscribe.hidden {
position: absolute;
top: 0;
pointer-events: none;
z-index: -1;
}
form button.unsubscribe:hover {
color: var(--text-shade-3);
}
@media screen and (max-width: 900px) {
.account-form-wrapper {
width: min(500px, 100%);
margin-bottom: 172px;
}
form {
grid-template-columns: 1fr;
gap: 2.4rem;
}
form button {
position: relative;
width: 100%;
}
form .button-wrapper {
grid-column: 1 / span 1;
position: fixed;
bottom: 24px;
width: min(500px, 90%);
z-index: 5;
}
form .button-wrapper::before {
content: "";
position: absolute;
top: -24px;
left: -100vw;
width: 200vw;
height: 300%;
background: var(--bg-shade-0);
}
}
@media screen and (max-width: 380px) {
label.tier .tier-perks {
width: 80%;
}
.back-arrow {
padding: 6px;
}
.back-arrow span {
display: none;
}
}

View File

@ -1 +1 @@
<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-arrow-right"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>
<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-arrow-right"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>

Before

Width:  |  Height:  |  Size: 314 B

After

Width:  |  Height:  |  Size: 315 B

View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-check"><polyline points="20 6 9 17 4 12"></polyline></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-check"><polyline points="20 6 9 17 4 12"></polyline></svg>

Before

Width:  |  Height:  |  Size: 254 B

After

Width:  |  Height:  |  Size: 255 B

View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevron-down"><polyline points="6 9 12 15 18 9"></polyline></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevron-down"><polyline points="6 9 12 15 18 9"></polyline></svg>

Before

Width:  |  Height:  |  Size: 261 B

After

Width:  |  Height:  |  Size: 262 B

View File

@ -211,4 +211,4 @@ class Mii extends KaitaiStream {
}
}
module.exports = Mii;
module.exports = Mii;

View File

@ -19,4 +19,43 @@ document.getElementById('remove-discord-connection')?.addEventListener('click',
}
})
.catch(console.log);
});
});
const onlineFilesModal = document.querySelector('.modal-wrapper#onlinefiles');
const onlineFilesModalButtonConfirm = document.getElementById('onlineFilesConfirmButton');
const onlineFilesModalButtonClose = document.getElementById('onlineFilesCloseButton');
const onlineFilesModalPasswordInput = document.getElementById('password');
document.getElementById('download-cemu-files')?.addEventListener('click', event => {
event.preventDefault();
onlineFilesModal.classList.remove('hidden');
});
onlineFilesModalButtonConfirm?.addEventListener('click', () => {
fetch('/account/online-files', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
password: onlineFilesModalPasswordInput.value
})
})
.then(response => response.blob())
.then(blob => URL.createObjectURL(blob))
.then(blobUrl => {
const a = document.createElement('a');
a.href = blobUrl;
a.setAttribute('download', 'Cemu Pretendo Online Files.zip');
a.click();
onlineFilesModal.classList.add('hidden');
})
.catch(console.log);
});
onlineFilesModalButtonClose?.addEventListener('click', () => {
onlineFilesModal.classList.add('hidden');
});

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

@ -1,6 +1,6 @@
// This file gets automatically bundled with browserify when running the start script. This also means that after any update you're gonna need to restart the server.
// Prevent the user from reloading or leaving the page
// Prevent the user from reloading or leaving the page
window.addEventListener('beforeunload', function (e) {
e.preventDefault();
e.returnValue = '';

View File

@ -34,7 +34,7 @@ document.querySelectorAll('.feature-list-wrapper').forEach(progressListElement =
datasets: [
{
data,
backgroundColor: isInBrightCard ? ['white', 'rgba(195, 178, 227, 0.5)'] : ['#9D6FF3', '#4C5174']
backgroundColor: isInBrightCard ? ['white', 'rgba(195, 178, 227, 0.5)'] : ['#9D6FF3', '#31365A']
}
]
},

114
public/assets/js/upgrade.js Normal file
View File

@ -0,0 +1,114 @@
const buttons = {
submit: document.getElementById('submitButton'),
unsubModal: {
show: document.getElementById('unsubModalShowButton'),
close: document.getElementById('unsubModalCloseButton'),
confirm: document.getElementById('unsubModalConfirmButton'),
},
switchTierModal: {
show: document.getElementById('switchTierShowButton'),
close: document.getElementById('switchTierCloseButton'),
confirm: document.getElementById('switchTierConfirmButton'),
},
};
const currentTierID = document.querySelector('form').dataset.currentTier || undefined;
const currentTierElement = document.querySelector(`#${currentTierID}`) || undefined;
// if the condition is met, we disable the submit button and enable the unsubscribe button
function conditionalSubmitButton(condition, target) {
if (condition) {
buttons.submit.innerText = 'Already subscribed to this tier';
buttons.unsubModal.show.innerText = `Unsubscribe from ${currentTierElement.dataset.tierName}`;
buttons.submit.disabled = true;
buttons.submit.classList.add('disabled');
buttons.unsubModal.show.classList.remove('hidden');
} else {
buttons.submit.classList.remove('disabled');
buttons.unsubModal.show.classList.add('hidden');
buttons.submit.disabled = false;
buttons.submit.innerText = `Subscribe to ${target.dataset.tierName}`;
}
}
function submitForm(cancel) {
const form = document.querySelector('form');
if (cancel) {
form.action = '/account/stripe/unsubscribe';
} else {
const selectedTier = form.querySelector('input[type="radio"]:checked').value;
form.action = `/account/stripe/checkout/${selectedTier}`;
}
form.submit();
}
// If the currect tier exists, select it from the list and disable the submit button.
if (currentTierElement) {
currentTierElement.click();
conditionalSubmitButton(true);
}
// If a tier is selected, conditionally enable the submit button.
document.querySelector('form').addEventListener('change', function(e) {
e.preventDefault();
// If the selected tier is the current tier, set the button to disabled. Else we enable the button
conditionalSubmitButton(e.target.value === currentTierElement?.value, e.target);
});
// handle the submit button
buttons.submit.addEventListener('click', function(e) {
e.preventDefault();
// If the user is already subscribed to another tier, we show the confirm modal, else if this is a new subscription we submit the form.
if (currentTierElement) {
const oldTierNameSpan = document.querySelector('#switchtier .modal-caption span.oldtier');
const newTierNameSpan = document.querySelector('#switchtier .modal-caption span.newtier');
oldTierNameSpan.innerText = currentTierElement.dataset.tierName;
newTierNameSpan.innerText = document.querySelector('input[name="tier"]:checked').dataset.tierName;
document.body.classList.add('modal-open');
document.querySelector('.modal-wrapper#switchtier').classList.remove('hidden');
} else {
submitForm();
}
});
buttons.unsubModal.show.addEventListener('click', function(e) {
e.preventDefault();
const tierNameSpan = document.querySelector('#unsub .modal-caption span');
tierNameSpan.innerText = currentTierElement.dataset.tierName;
// Show the unsubscribe modal
document.body.classList.add('modal-open');
document.querySelector('.modal-wrapper#unsub').classList.remove('hidden');
});
buttons.unsubModal.close.addEventListener('click', function(e) {
e.preventDefault();
// Hide the unsubscribe modal
document.body.classList.remove('modal-open');
document.querySelector('.modal-wrapper#unsub').classList.add('hidden');
});
buttons.unsubModal.confirm.addEventListener('click', function(e) {
e.preventDefault();
submitForm(true);
});
buttons.switchTierModal.close.addEventListener('click', function(e) {
e.preventDefault();
// Hide the switch tier modal
document.body.classList.remove('modal-open');
document.querySelector('.modal-wrapper#switchtier').classList.add('hidden');
});
buttons.switchTierModal.confirm.addEventListener('click', function(e) {
e.preventDefault();
submitForm(false);
});

View File

@ -1,19 +1,19 @@
{
"name": "Pretendo Network",
"short_name": "Pretendo Network",
"icons": [
{
"src": "https://pretendo.network/assets/images/icons/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "https://pretendo.network/assets/images/icons/android-chrome-384x384.png",
"sizes": "384x384",
"type": "image/png"
}
],
"theme_color": "#1b1f3b",
"background_color": "#1b1f3b",
"display": "standalone"
"name": "Pretendo Network",
"short_name": "Pretendo Network",
"icons": [
{
"src": "https://pretendo.network/assets/images/icons/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "https://pretendo.network/assets/images/icons/android-chrome-384x384.png",
"sizes": "384x384",
"type": "image/png"
}
],
"theme_color": "#1b1f3b",
"background_color": "#1b1f3b",
"display": "standalone"
}

View File

@ -1,19 +1,19 @@
{
"name": "Pretendo",
"short_name": "Pretendo",
"icons": [
{
"src": "/assets/icons/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/assets/icons/android-chrome-384x384.png",
"sizes": "384x384",
"type": "image/png"
}
],
"theme_color": "#673db6",
"background_color": "#673db6",
"display": "standalone"
"name": "Pretendo",
"short_name": "Pretendo",
"icons": [
{
"src": "/assets/icons/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/assets/icons/android-chrome-384x384.png",
"sizes": "384x384",
"type": "image/png"
}
],
"theme_color": "#673db6",
"background_color": "#673db6",
"display": "standalone"
}

View File

@ -1,15 +1,19 @@
const Trello =require('trello');
const Trello = require('trello');
const Stripe = require('stripe');
const got = require('got');
const config = require('../config.json');
const trello = new Trello(config.trello.api_key, config.trello.api_token);
const stripe = new Stripe(config.stripe.secret_key);
const VALID_LIST_NAMES = ['Not Started', 'Started', 'Completed'];
let cache;
let trelloCache;
let stripeDonationCache;
async function getTrelloCache() {
const available = await trelloAPIAvailable();
if (!available) {
return cache || {
return trelloCache || {
update_time: 0,
sections: [{
title: 'Upstream API error',
@ -24,19 +28,19 @@ async function getTrelloCache() {
};
}
if (!cache) {
cache = await updateTrelloCache();
if (!trelloCache) {
trelloCache = await updateTrelloCache();
}
if (cache.update_time < Date.now() - (1000 * 60 * 60)) {
cache = await updateTrelloCache();
if (trelloCache.update_time < Date.now() - (1000 * 60 * 60)) {
trelloCache = await updateTrelloCache();
}
return cache;
return trelloCache;
}
async function updateTrelloCache() {
const progressData = {
const progressCache = {
update_time: Date.now(),
sections: []
};
@ -79,11 +83,11 @@ async function updateTrelloCache() {
}
if (meta.progress.not_started.length !== 0 || meta.progress.started.length !== 0 || meta.progress.completed.length !== 0) {
progressData.sections.push(meta);
progressCache.sections.push(meta);
}
}
return progressData;
return progressCache;
}
async function trelloAPIAvailable() {
@ -91,7 +95,58 @@ async function trelloAPIAvailable() {
return status.indicator !== 'major' && status.indicator !== 'critical';
}
async function getStripeDonationCache() {
if (!stripeDonationCache) {
stripeDonationCache = await updateStripeDonationCache();
}
if (stripeDonationCache.update_time < Date.now() - (1000 * 60 * 60)) {
stripeDonationCache = await updateStripeDonationCache();
}
return stripeDonationCache;
}
async function updateStripeDonationCache() {
const donationCache = {
update_time: Date.now(),
goal: config.stripe.goal_cents,
total: 0,
donators: 0,
completed_real: 0,
completed_capped: 0
};
let hasMore = true;
let lastId;
while (hasMore) {
const { data: activeSubscriptions, has_more } = await stripe.subscriptions.list({
limit: 100,
status: 'active',
starting_after: lastId
});
donationCache.donators += activeSubscriptions.length;
for (const subscription of activeSubscriptions) {
donationCache.total += subscription.plan.amount;
lastId = subscription.id;
}
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
return donationCache;
}
module.exports = {
getTrelloCache,
updateTrelloCache
};
getStripeDonationCache
};

27
src/database.js Normal file
View File

@ -0,0 +1,27 @@
const mongoose = require('mongoose');
const PNIDSchema = require('./schema/pnid');
const config = require('../config.json');
const accountServerConfig = config.database.account;
const { uri, database, options } = accountServerConfig;
let accountServerDBConnection;
let PNID;
async function connect() {
accountServerDBConnection = await mongoose.createConnection(`${uri}/${database}`, options);
accountServerDBConnection.on('error', console.error.bind(console, 'Mongoose connection error:'));
accountServerDBConnection.on('close', () => {
accountServerDBConnection.removeAllListeners();
});
await accountServerDBConnection.asPromise();
PNID = accountServerDBConnection.model('PNID', PNIDSchema);
module.exports.PNID = PNID;
}
module.exports = {
connect,
PNID
};

View File

@ -49,4 +49,4 @@ module.exports = {
error,
warn,
info
};
};

22
src/mailer.js Normal file
View File

@ -0,0 +1,22 @@
const nodemailer = require('nodemailer');
const config = require('../config.json');
const transporter = nodemailer.createTransport({
host: 'smtp.gmail.com',
port: 587,
secure: false,
auth: {
user: config.gmail.user,
pass: config.gmail.pass
}
});
async function sendMail(options) {
options.from = config.gmail.from;
return await transporter.sendMail(options);
}
module.exports = {
sendMail
};

View File

@ -0,0 +1,13 @@
async function redirectMiddleware(request, response, next) {
if (request.method === 'POST') {
request.redirect = request.body.redirect?.startsWith('/') ? request.body.redirect : null;
}
if (request.query.redirect) {
response.locals.redirect = request.query.redirect?.startsWith('/') ? request.query.redirect : null;
}
return next();
}
module.exports = redirectMiddleware;

View File

@ -0,0 +1,43 @@
const util = require('../util');
const database = require('../database');
async function renderDataMiddleware(request, response, next) {
if (request.path.startsWith('/assets')) {
return next();
}
// Get user local
const reqLocale = request.locale;
const locale = util.getLocale(reqLocale.region, reqLocale.language);
response.locals.locale = locale;
response.locals.localeString = reqLocale.toString();
// Get message cookies
response.locals.success_message = request.cookies.success_message;
response.locals.error_message = request.cookies.error_message;
// Reset message cookies
response.clearCookie('success_message', { domain: '.pretendo.network' });
response.clearCookie('error_message', { domain: '.pretendo.network' });
response.locals.isLoggedIn = request.cookies.access_token && request.cookies.refresh_token;
if (response.locals.isLoggedIn) {
try {
response.locals.account = await util.getUserAccountData(request, response);
request.pnid = await database.PNID.findOne({ pid: response.locals.account.pid });
request.account = response.locals.account;
return next();
} catch (error) {
response.cookie('error_message', error.message, { domain: '.pretendo.network' });
return response.redirect('/account/login');
}
} else {
return next();
}
}
module.exports = renderDataMiddleware;

View File

@ -0,0 +1,10 @@
async function requireLoginMiddleware(request, response, next) {
// Verify the user is logged in
if (!request.cookies.access_token || !request.cookies.refresh_token) {
return response.redirect(`/account/login?redirect=${request.originalUrl}`);
}
return next();
}
module.exports = requireLoginMiddleware;

View File

@ -1,497 +0,0 @@
const { Router } = require('express');
const crypto = require('crypto');
const DiscordOauth2 = require('discord-oauth2');
const { v4: uuidv4 } = require('uuid');
const AdmZip = require('adm-zip');
const util = require('../util');
const config = require('../../config.json');
const router = new Router();
const aesKey = Buffer.from(config.aes_key, 'hex');
// Create OAuth client
const discordOAuth = new DiscordOauth2({
clientId: config.discord.client_id,
clientSecret: config.discord.client_secret,
redirectUri: `${config.http.base_url}/account/connect/discord`,
version: 'v9'
});
router.get('/', 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');
}
// Setup the data to be sent to the handlebars renderer
const renderData = {
layout: 'main',
locale: util.getLocale(request.locale.region, request.locale.language),
localeString: request.locale.toString(),
linked: request.cookies.linked,
error: request.cookies.error
};
// Reset message cookies
response.clearCookie('linked', { domain: '.pretendo.network' });
response.clearCookie('error', { domain: '.pretendo.network' });
// Attempt to get user data
let apiResponse = await util.apiGetRequest('/v1/user', {
'Authorization': `${request.cookies.token_type} ${request.cookies.access_token}`
});
if (apiResponse.statusCode !== 200) {
// Assume expired, refresh and retry request
apiResponse = await util.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;
response.cookie('refresh_token', tokens.refresh_token, { domain : '.pretendo.network' });
response.cookie('access_token', tokens.access_token, { domain : '.pretendo.network' });
response.cookie('token_type', tokens.token_type, { domain : '.pretendo.network' });
apiResponse = await util.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'
});
}
// Set user account info to render data
const account = apiResponse.body;
renderData.account = account;
renderData.isTester = account.access_level > 0;
// Check if a Discord account is linked to the PNID
if (account.connections.discord.id && account.connections.discord.id.trim() !== '') {
// If Discord account is linked, then get user info
try {
renderData.discordUser = await discordOAuth.getUser(account.connections.discord.access_token);
} catch (error) {
// Assume expired, refresh and retry Discord request
let tokens;
try {
tokens = await discordOAuth.tokenRequest({
scope: 'identify guilds',
grantType: 'refresh_token',
refreshToken: account.connections.discord.refresh_token,
});
} catch (error) {
renderData.error = 'Invalid Discord refresh token. Remove account and relink';
response.render('account/account', renderData);
}
// TODO: Add a dedicated endpoint for updating connections?
apiResponse = await util.apiPostGetRequest('/v1/connections/add/discord', {
'Authorization': `${request.cookies.token_type} ${request.cookies.access_token}`
}, {
data: {
id: account.connections.discord.id,
access_token: tokens.access_token,
refresh_token: tokens.refresh_token,
}
});
if (apiResponse.statusCode !== 200) {
// Assume expired, refresh and retry request
apiResponse = await util.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;
response.cookie('refresh_token', tokens.refresh_token, { domain : '.pretendo.network' });
response.cookie('access_token', tokens.access_token, { domain : '.pretendo.network' });
response.cookie('token_type', tokens.token_type, { domain : '.pretendo.network' });
apiResponse = await util.apiPostGetRequest('/v1/connections/add/discord', {
'Authorization': `${tokens.token_type} ${tokens.access_token}`
}, {
data: {
id: account.connections.discord.id,
access_token: tokens.access_token,
refresh_token: tokens.refresh_token,
}
});
// If still failed, something went horribly wrong
if (apiResponse.statusCode !== 200) {
// TODO: Error message
return response.status(apiResponse.statusCode).json({
error: 'Bad'
});
}
}
account.connections.discord.access_token = tokens.access_token;
account.connections.discord.refresh_token = tokens.refresh_token;
}
// Get the users Discord roles to check if they are a tester
const { roles } = await discordOAuth.getMemberRolesForGuild({
userId: account.connections.discord.id,
guildId: config.discord.guild_id,
botToken: config.discord.bot_token
});
// Only run this check if not already a tester (edge case)
if (!renderData.isTester) {
// 409116477212459008 = Developer
// 882247322933801030 = Super Mario (Patreon tier)
renderData.isTester = roles.some(role => config.discord.tester_roles.includes(role));
}
} else {
// If no Discord account linked, generate an auth URL
const discordAuthURL = discordOAuth.generateAuthUrl({
scope: ['identify', 'guilds'],
state: crypto.randomBytes(16).toString('hex'),
});
renderData.discordAuthURL = discordAuthURL;
}
response.render('account/account', renderData);
});
router.get('/login', async (request, response) => {
const renderData = {
layout: 'main',
locale: util.getLocale(request.locale.region, request.locale.language),
localeString: request.locale.toString(),
error: request.cookies.error
};
response.clearCookie('error', { domain: '.pretendo.network' });
response.render('account/login', renderData);
});
router.post('/login', async (request, response) => {
const { username, password } = request.body;
let apiResponse = await util.apiPostGetRequest('/v1/login', {}, {
username,
password,
grant_type: 'password'
});
if (apiResponse.statusCode !== 200) {
response.cookie('error', apiResponse.body.error, { domain: '.pretendo.network' });
return response.redirect('/account/login');
}
const tokens = apiResponse.body;
response.cookie('refresh_token', tokens.refresh_token, { domain : '.pretendo.network' });
response.cookie('access_token', tokens.access_token, { domain : '.pretendo.network' });
response.cookie('token_type', tokens.token_type, { domain : '.pretendo.network' });
apiResponse = await util.apiGetRequest('/v1/user', {
'Authorization': `${tokens.token_type} ${tokens.access_token}`
});
const account = apiResponse.body;
const hashedPassword = util.nintendoPasswordHash(password, account.pid);
const hashedPasswordBuffer = Buffer.from(hashedPassword, 'hex');
const cipher = crypto.createCipheriv('aes-256-cbc', aesKey, Buffer.alloc(16));
let encryptedBody = cipher.update(hashedPasswordBuffer);
encryptedBody = Buffer.concat([encryptedBody, cipher.final()]);
response.cookie('ph', encryptedBody.toString('hex'), { domain: '.pretendo.network' });
response.redirect('/account');
});
router.get('/register', async (request, response) => {
const renderData = {
layout: 'main',
locale: util.getLocale(request.locale.region, request.locale.language),
localeString: request.locale.toString(),
error: request.cookies.error,
email: request.cookies.email,
username: request.cookies.username,
mii_name: request.cookies.mii_name,
};
response.clearCookie('error', { domain: '.pretendo.network' });
response.clearCookie('email', { domain: '.pretendo.network' });
response.clearCookie('username', { domain: '.pretendo.network' });
response.clearCookie('mii_name', { domain: '.pretendo.network' });
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;
response.cookie('email', email, { domain: '.pretendo.network' });
response.cookie('username', username, { domain: '.pretendo.network' });
response.cookie('mii_name', mii_name, { domain: '.pretendo.network' });
const apiResponse = await util.apiPostGetRequest('/v1/register', {}, {
email, username, mii_name, password, password_confirm, hCaptchaResponse
});
if (apiResponse.statusCode !== 200) {
response.cookie('error', apiResponse.body.error, { domain: '.pretendo.network' });
return response.redirect('/account/register');
}
const tokens = apiResponse.body;
response.cookie('refresh_token', tokens.refresh_token, { domain: '.pretendo.network' });
response.cookie('access_token', tokens.access_token, { domain: '.pretendo.network' });
response.cookie('token_type', tokens.token_type, { domain: '.pretendo.network' });
response.clearCookie('error', { domain: '.pretendo.network' });
response.clearCookie('email', { domain: '.pretendo.network' });
response.clearCookie('username', { domain: '.pretendo.network' });
response.clearCookie('mii_name', { domain: '.pretendo.network' });
response.redirect('/account');
});
router.get('/connect/discord', async (request, response) => {
let tokens;
try {
// Attempt to get OAuth2 tokens
tokens = await discordOAuth.tokenRequest({
code: request.query.code,
scope: 'identify guilds',
grantType: 'authorization_code',
});
} catch (error) {
response.cookie('error', 'Invalid Discord authorization code. Please try again', { domain: '.pretendo.network' });
return response.redirect('/account');
}
// Get Discord user data
const user = await discordOAuth.getUser(tokens.access_token);
// Link the Discord account to the PNID
let apiResponse = await util.apiPostGetRequest('/v1/connections/add/discord', {
'Authorization': `${request.cookies.token_type} ${request.cookies.access_token}`
}, {
data: {
id: user.id,
access_token: tokens.access_token,
refresh_token: tokens.refresh_token,
}
});
if (apiResponse.statusCode !== 200) {
// Assume expired, refresh and retry request
apiResponse = await util.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;
response.cookie('refresh_token', tokens.refresh_token, { domain: '.pretendo.network' });
response.cookie('access_token', tokens.access_token, { domain: '.pretendo.network' });
response.cookie('token_type', tokens.token_type, { domain: '.pretendo.network' });
apiResponse = await util.apiPostGetRequest('/v1/connections/add/discord', {
'Authorization': `${tokens.token_type} ${tokens.access_token}`
}, {
data: {
id: user.id,
access_token: tokens.access_token,
refresh_token: tokens.refresh_token,
}
});
// If still failed, something went horribly wrong
if (apiResponse.statusCode !== 200) {
// TODO: Error message
return response.status(apiResponse.statusCode).json({
error: 'Bad'
});
}
}
response.cookie('linked', 'Discord', { domain: '.pretendo.network' }).redirect('/account');
});
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');
}
// Attempt to get user data
let apiResponse = await util.apiGetRequest('/v1/user', {
'Authorization': `${request.cookies.token_type} ${request.cookies.access_token}`
});
if (apiResponse.statusCode !== 200) {
// Assume expired, refresh and retry request
apiResponse = await util.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;
response.cookie('refresh_token', tokens.refresh_token, { domain: '.pretendo.network' });
response.cookie('access_token', tokens.access_token, { domain: '.pretendo.network' });
response.cookie('token_type', tokens.token_type, { domain: '.pretendo.network' });
apiResponse = await util.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'
});
}
const account = apiResponse.body;
const decipher = crypto.createDecipheriv('aes-256-cbc', aesKey, Buffer.alloc(16));
let decryptedPasswordHash = decipher.update(Buffer.from(request.cookies.ph, 'hex'));
decryptedPasswordHash = Buffer.concat([decryptedPasswordHash, decipher.final()]);
const miiNameBuffer = Buffer.alloc(0x16);
const miiName = Buffer.from(account.mii.name, 'utf16le').swap16();
miiName.copy(miiNameBuffer);
let accountDat = 'AccountInstance_00000000\n';
accountDat += 'PersistentId=80000001\n';
accountDat += 'TransferableIdBase=0\n';
accountDat += `Uuid=${uuidv4().replace(/-/g, '')}\n`;
accountDat += `MiiData=${Buffer.from(account.mii.data, 'base64').toString('hex')}\n`;
accountDat += `MiiName=${miiNameBuffer.toString('hex')}\n`;
accountDat += `AccountId=${account.username}\n`;
accountDat += 'BirthYear=0\n';
accountDat += 'BirthMonth=0\n';
accountDat += 'BirthDay=0\n';
accountDat += 'Gender=0\n';
accountDat += `EmailAddress=${account.email.address}\n`;
accountDat += 'Country=0\n';
accountDat += 'SimpleAddressId=0\n';
accountDat += `PrincipalId=${account.pid.toString(16)}\n`;
accountDat += 'IsPasswordCacheEnabled=1\n';
accountDat += `AccountPasswordCache=${decryptedPasswordHash.toString('hex')}`;
const onlineFiles = new AdmZip();
onlineFiles.addFile('mlc01/usr/save/system/act/80000001/account.dat', Buffer.from(accountDat)); // Minimal account.dat
onlineFiles.addFile('otp.bin', Buffer.alloc(0x400)); // nulled OTP
onlineFiles.addFile('seeprom.bin', Buffer.alloc(0x200)); // nulled SEEPROM
response.status(200);
response.set('Content-Disposition', 'attachment; filename="Online Files.zip');
response.set('Content-Type', 'application/zip');
response.end(onlineFiles.toBuffer());
});
router.get('/miieditor', async (request, response) => {
const reqLocale = request.locale;
const locale = util.getLocale(reqLocale.region, reqLocale.language);
// Should obviously be the user's
const encodedUserMiiData = 'AwAAQOlVognnx0GC2X0LLQOzuI0n2QAAAUBiAGUAbABsAGEAAABFAAAAAAAAAEBAEgCBAQRoQxggNEYUgRIXaA0AACkDUkhQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP6G';
// Adapted from https://www.3dbrew.org/wiki/Mii#Mapped_Editor_.3C-.3E_Hex_values
const editorToHex = {
'face': [
0x00,0x01,0x08,0x02,0x03,0x09,0x04,0x05,0x0a,0x06,0x07,0x0b
],
'hairs': [
[0x21,0x2f,0x28,0x25,0x20,0x6b,0x30,0x33,0x37,0x46,0x2c,0x42],
[0x34,0x32,0x26,0x31,0x2b,0x1f,0x38,0x44,0x3e,0x73,0x4c,0x77],
[0x40,0x51,0x74,0x79,0x16,0x3a,0x3c,0x57,0x7d,0x75,0x49,0x4b],
[0x2a,0x59,0x39,0x36,0x50,0x22,0x17,0x56,0x58,0x76,0x27,0x24],
[0x2d,0x43,0x3b,0x41,0x29,0x1e,0x0c,0x10,0x0a,0x52,0x80,0x81],
[0x0e,0x5f,0x69,0x64,0x06,0x14,0x5d,0x66,0x1b,0x04,0x11,0x6e],
[0x7b,0x08,0x6a,0x48,0x03,0x15,0x00,0x62,0x3f,0x5a,0x0b,0x78],
[0x05,0x4a,0x6c,0x5e,0x7c,0x19,0x63,0x45,0x23,0x0d,0x7a,0x71],
[0x35,0x18,0x55,0x53,0x47,0x83,0x60,0x65,0x1d,0x07,0x0f,0x70],
[0x4f,0x01,0x6d,0x7f,0x5b,0x1a,0x3d,0x67,0x02,0x4d,0x12,0x5c],
[0x54,0x09,0x13,0x82,0x61,0x68,0x2e,0x4e,0x1c,0x72,0x7e,0x6f]
],
'eyebrows': [
[0x06,0x00,0x0c,0x01,0x09,0x13,0x07,0x15,0x08,0x11,0x05,0x04],
[0x0b,0x0a,0x02,0x03,0x0e,0x14,0x0f,0x0d,0x16,0x12,0x10,0x17]
],
'eyes': [
[0x02,0x04,0x00,0x08,0x27,0x11,0x01,0x1a,0x10,0x0f,0x1b,0x14],
[0x21,0x0b,0x13,0x20,0x09,0x0c,0x17,0x22,0x15,0x19,0x28,0x23],
[0x05,0x29,0x0d,0x24,0x25,0x06,0x18,0x1e,0x1f,0x12,0x1c,0x2e],
[0x07,0x2c,0x26,0x2a,0x2d,0x1d,0x03,0x2b,0x16,0x0a,0x0e,0x2f],
[0x30,0x31,0x32,0x35,0x3b,0x38,0x36,0x3a,0x39,0x37,0x33,0x34]
],
'nose': [
[0x01,0x0a,0x02,0x03,0x06,0x00, 0x05,0x04,0x08,0x09,0x07,0x0B],
[0x0d,0x0e,0x0c,0x11,0x10,0x0f]
],
'mouth': [
[0x17,0x01,0x13,0x15,0x16,0x05,0x00,0x08,0x0a,0x10,0x06,0x0d],
[0x07,0x09,0x02,0x11,0x03,0x04,0x0f,0x0b,0x14,0x12,0x0e,0x0c],
[0x1b,0x1e,0x18,0x19,0x1d,0x1c,0x1a,0x23,0x1f,0x22,0x21,0x20]
]
};
response.render('account/miieditor', {
layout: 'main',
locale,
localeString: reqLocale.toString(),
encodedUserMiiData,
editorToHex
});
});
module.exports = router;

View File

@ -1,17 +0,0 @@
const { Router } = require('express');
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('aprilfools', {
layout: 'main',
locale,
localeString: reqLocale.toString(),
});
});
module.exports = router;

View File

@ -1,17 +0,0 @@
const { Router } = require('express');
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', {
layout: 'main',
locale,
localeString: reqLocale.toString(),
});
});
module.exports = router;

View File

@ -1,24 +0,0 @@
const { Router } = require('express');
const util = require('../util');
const { boards } = require('../../boards/boards.json');
const router = new Router();
const { getTrelloCache } = require('../trello');
router.get('/', async (request, response) => {
const reqLocale = request.locale;
const locale = util.getLocale(reqLocale.region, reqLocale.language);
const cache = await getTrelloCache();
response.render('progress', {
layout: 'main',
boards,
locale,
localeString: reqLocale.toString(),
progressLists: cache
});
});
module.exports = router;

424
src/routes/account.js Normal file
View File

@ -0,0 +1,424 @@
const express = require('express');
const crypto = require('crypto');
const DiscordOauth2 = require('discord-oauth2');
const { v4: uuidv4 } = require('uuid');
const AdmZip = require('adm-zip');
const Stripe = require('stripe');
const { REST: DiscordRest } = require('@discordjs/rest');
const { Routes: DiscordRoutes } = require('discord-api-types/v10');
const requireLoginMiddleware = require('../middleware/require-login');
const database = require('../database');
const cache = require('../cache');
const util = require('../util');
const logger = require('../logger');
const config = require('../../config.json');
const { Router } = express;
const stripe = new Stripe(config.stripe.secret_key);
const router = new Router();
const discordRest = new DiscordRest({ version: '10' }).setToken(config.discord.bot_token);
// Create OAuth client
const discordOAuth = new DiscordOauth2({
clientId: config.discord.client_id,
clientSecret: config.discord.client_secret,
redirectUri: `${config.http.base_url}/account/connect/discord`,
version: 'v10'
});
router.get('/', requireLoginMiddleware, async (request, response) => {
// Setup the data to be sent to the handlebars renderer
const renderData = {};
// Check for Stripe messages
const { upgrade_success } = request.query;
if (upgrade_success === 'true') {
renderData.success_message = 'Account upgraded successfully';
} else if (upgrade_success === 'false') {
renderData.error_message = 'Account upgrade failed';
}
const { account } = request;
const { pnid } = request;
renderData.tierName = pnid.get('connections.stripe.tier_name');
renderData.tierLevel = pnid.get('connections.stripe.tier_level');
renderData.account = account;
renderData.isTester = account.access_level > 0;
// Check if a Discord account is linked to the PNID
if (account.connections.discord.id && account.connections.discord.id.trim() !== '') {
try {
renderData.discordUser = await discordRest.get(DiscordRoutes.user(account.connections.discord.id));
} catch (error) {
response.cookie('error_message', error.message, { domain: '.pretendo.network' });
}
} else {
// If no Discord account linked, generate an auth URL
const discordAuthURL = discordOAuth.generateAuthUrl({
scope: ['identify', 'guilds'],
state: crypto.randomBytes(16).toString('hex'),
});
renderData.discordAuthURL = discordAuthURL;
}
response.render('account/account', renderData);
});
router.get('/login', async (request, response) => {
response.render('account/login');
});
router.post('/login', async (request, response) => {
const { username, password } = request.body;
try {
const tokens = await util.login(username, password);
response.cookie('refresh_token', tokens.refresh_token, { domain: '.pretendo.network' });
response.cookie('access_token', tokens.access_token, { domain: '.pretendo.network' });
response.cookie('token_type', tokens.token_type, { domain: '.pretendo.network' });
response.redirect(request.redirect || '/account');
} catch (error) {
console.log(error);
response.cookie('error_message', error.message, { domain: '.pretendo.network' });
return response.redirect('/account/login');
}
});
router.get('/register', async (request, response) => {
const renderData = {
email: request.cookies.email,
username: request.cookies.username,
mii_name: request.cookies.mii_name,
};
response.clearCookie('email', { domain: '.pretendo.network' });
response.clearCookie('username', { domain: '.pretendo.network' });
response.clearCookie('mii_name', { domain: '.pretendo.network' });
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;
response.cookie('email', email, { domain: '.pretendo.network' });
response.cookie('username', username, { domain: '.pretendo.network' });
response.cookie('mii_name', mii_name, { domain: '.pretendo.network' });
try {
const tokens = await util.register({
email,
username,
mii_name,
password,
password_confirm,
hCaptchaResponse
});
response.cookie('refresh_token', tokens.refresh_token, { domain: '.pretendo.network' });
response.cookie('access_token', tokens.access_token, { domain: '.pretendo.network' });
response.cookie('token_type', tokens.token_type, { domain: '.pretendo.network' });
response.clearCookie('email', { domain: '.pretendo.network' });
response.clearCookie('username', { domain: '.pretendo.network' });
response.clearCookie('mii_name', { domain: '.pretendo.network' });
response.redirect(request.redirect || '/account');
} catch (error) {
response.cookie('error_message', error.message, { domain: '.pretendo.network' });
return response.redirect('/account/register');
}
});
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.redirect('/');
});
router.get('/connect/discord', requireLoginMiddleware, async (request, response) => {
let tokens;
try {
// Attempt to get OAuth2 tokens
tokens = await discordOAuth.tokenRequest({
code: request.query.code,
scope: 'identify guilds',
grantType: 'authorization_code',
});
} catch (error) {
response.cookie('error_message', 'Invalid Discord authorization code. Please try again', { domain: '.pretendo.network' });
return response.redirect('/account');
}
// Get Discord user data
const discordUser = await discordOAuth.getUser(tokens.access_token);
try {
await util.updateDiscordConnection(discordUser, request, response);
response.cookie('success_message', 'Discord account linked successfully', { domain: '.pretendo.network' });
response.redirect('/account');
} catch (error) {
response.cookie('error_message', error.message, { domain: '.pretendo.network' });
return response.redirect('/account');
}
});
router.post('/online-files', requireLoginMiddleware, async (request, response) => {
const { account } = request;
const { password } = request.body;
const hashedPassword = util.nintendoPasswordHash(password, account.pid);
const miiNameBuffer = Buffer.alloc(0x16);
const miiName = Buffer.from(account.mii.name, 'utf16le').swap16();
miiName.copy(miiNameBuffer);
let accountDat = 'AccountInstance_00000000\n';
accountDat += 'PersistentId=80000001\n';
accountDat += 'TransferableIdBase=0\n';
accountDat += `Uuid=${uuidv4().replace(/-/g, '')}\n`;
accountDat += `MiiData=${Buffer.from(account.mii.data, 'base64').toString('hex')}\n`;
accountDat += `MiiName=${miiNameBuffer.toString('hex')}\n`;
accountDat += `AccountId=${account.username}\n`;
accountDat += 'BirthYear=0\n';
accountDat += 'BirthMonth=0\n';
accountDat += 'BirthDay=0\n';
accountDat += 'Gender=0\n';
accountDat += `EmailAddress=${account.email.address}\n`;
accountDat += 'Country=0\n';
accountDat += 'SimpleAddressId=0\n';
accountDat += `PrincipalId=${account.pid.toString(16)}\n`;
accountDat += 'IsPasswordCacheEnabled=1\n';
accountDat += `AccountPasswordCache=${hashedPassword}`;
const onlineFiles = new AdmZip();
onlineFiles.addFile('mlc01/usr/save/system/act/80000001/account.dat', Buffer.from(accountDat)); // Minimal account.dat
onlineFiles.addFile('otp.bin', Buffer.alloc(0x400)); // nulled OTP
onlineFiles.addFile('seeprom.bin', Buffer.alloc(0x200)); // nulled SEEPROM
response.status(200);
response.set('Content-Disposition', 'attachment; filename="Cemu Pretendo Online Files.zip');
response.set('Content-Type', 'application/zip');
response.end(onlineFiles.toBuffer());
});
router.get('/miieditor', requireLoginMiddleware, async (request, response) => {
const { account } = request;
// Adapted from https://www.3dbrew.org/wiki/Mii#Mapped_Editor_.3C-.3E_Hex_values
const editorToHex = {
'face': [
0x00,0x01,0x08,0x02,0x03,0x09,0x04,0x05,0x0a,0x06,0x07,0x0b
],
'hairs': [
[0x21,0x2f,0x28,0x25,0x20,0x6b,0x30,0x33,0x37,0x46,0x2c,0x42],
[0x34,0x32,0x26,0x31,0x2b,0x1f,0x38,0x44,0x3e,0x73,0x4c,0x77],
[0x40,0x51,0x74,0x79,0x16,0x3a,0x3c,0x57,0x7d,0x75,0x49,0x4b],
[0x2a,0x59,0x39,0x36,0x50,0x22,0x17,0x56,0x58,0x76,0x27,0x24],
[0x2d,0x43,0x3b,0x41,0x29,0x1e,0x0c,0x10,0x0a,0x52,0x80,0x81],
[0x0e,0x5f,0x69,0x64,0x06,0x14,0x5d,0x66,0x1b,0x04,0x11,0x6e],
[0x7b,0x08,0x6a,0x48,0x03,0x15,0x00,0x62,0x3f,0x5a,0x0b,0x78],
[0x05,0x4a,0x6c,0x5e,0x7c,0x19,0x63,0x45,0x23,0x0d,0x7a,0x71],
[0x35,0x18,0x55,0x53,0x47,0x83,0x60,0x65,0x1d,0x07,0x0f,0x70],
[0x4f,0x01,0x6d,0x7f,0x5b,0x1a,0x3d,0x67,0x02,0x4d,0x12,0x5c],
[0x54,0x09,0x13,0x82,0x61,0x68,0x2e,0x4e,0x1c,0x72,0x7e,0x6f]
],
'eyebrows': [
[0x06,0x00,0x0c,0x01,0x09,0x13,0x07,0x15,0x08,0x11,0x05,0x04],
[0x0b,0x0a,0x02,0x03,0x0e,0x14,0x0f,0x0d,0x16,0x12,0x10,0x17]
],
'eyes': [
[0x02,0x04,0x00,0x08,0x27,0x11,0x01,0x1a,0x10,0x0f,0x1b,0x14],
[0x21,0x0b,0x13,0x20,0x09,0x0c,0x17,0x22,0x15,0x19,0x28,0x23],
[0x05,0x29,0x0d,0x24,0x25,0x06,0x18,0x1e,0x1f,0x12,0x1c,0x2e],
[0x07,0x2c,0x26,0x2a,0x2d,0x1d,0x03,0x2b,0x16,0x0a,0x0e,0x2f],
[0x30,0x31,0x32,0x35,0x3b,0x38,0x36,0x3a,0x39,0x37,0x33,0x34]
],
'nose': [
[0x01,0x0a,0x02,0x03,0x06,0x00, 0x05,0x04,0x08,0x09,0x07,0x0B],
[0x0d,0x0e,0x0c,0x11,0x10,0x0f]
],
'mouth': [
[0x17,0x01,0x13,0x15,0x16,0x05,0x00,0x08,0x0a,0x10,0x06,0x0d],
[0x07,0x09,0x02,0x11,0x03,0x04,0x0f,0x0b,0x14,0x12,0x0e,0x0c],
[0x1b,0x1e,0x18,0x19,0x1d,0x1c,0x1a,0x23,0x1f,0x22,0x21,0x20]
]
};
response.render('account/miieditor', {
encodedUserMiiData: account.mii.data,
editorToHex
});
});
router.get('/upgrade', requireLoginMiddleware, async (request, response) => {
// Set user account info to render data
const { pnid } = request;
const renderData = {
error: request.cookies.error,
currentTier: pnid.get('connections.stripe.price_id'),
donationCache: await cache.getStripeDonationCache()
};
const { data: prices } = await stripe.prices.list();
const { data: products } = await stripe.products.list();
renderData.tiers = products
.filter(product => product.active)
.sort((a, b) => +a.metadata.tier_level - +b.metadata.tier_level)
.map(product => {
const price = prices.find(price => price.product === product.id);
const perks = [];
if (product.metadata.discord_read === 'true') {
perks.push('Read-only access to select dev channels on Discord');
}
if (product.metadata.beta === 'true') {
perks.push('Access the beta servers');
}
return {
price_id: price.id,
thumbnail: product.images[0],
name: product.name,
description: product.description,
perks,
price: (price.unit_amount / 100).toLocaleString('en-US', { style: 'currency', currency: 'USD' }),
};
});
response.render('account/upgrade', renderData);
});
router.post('/stripe/checkout/:priceId', requireLoginMiddleware, async (request, response) => {
// Set user account info to render data
const { account } = request;
const pid = account.pid;
let customer;
const { data: searchResults } = await stripe.customers.search({
query: `metadata['pnid_pid']:'${pid}'`
});
if (searchResults.length !== 0) {
customer = searchResults[0];
} else {
customer = await stripe.customers.create({
email: account.email.address,
metadata: {
pnid_pid: pid
}
});
}
await database.PNID.updateOne({ pid }, {
$set: {
'connections.stripe.customer_id': customer.id, // ensure PNID always has latest customer ID
'connections.stripe.latest_webhook_timestamp': 0
}
}, { upsert: true }).exec();
const priceId = request.params.priceId;
const pnid = await database.PNID.findOne({ pid });
if (pnid.get('access_level') >= 2) {
response.cookie('error_message', 'Staff members do not need to purchase tiers', { domain: '.pretendo.network' });
return response.redirect('/account');
}
try {
const session = await stripe.checkout.sessions.create({
line_items: [
{
price: priceId,
quantity: 1,
},
],
customer: customer.id,
mode: 'subscription',
success_url: `${config.http.base_url}/account?upgrade_success=true`,
cancel_url: `${config.http.base_url}/account?upgrade_success=false`
});
return response.redirect(303, session.url);
} catch (error) {
// Maybe we need a dedicated error page?
// Or handle this as not cookies?
response.cookie('error_message', error.message, { domain: '.pretendo.network' });
return response.redirect('/account');
}
});
router.post('/stripe/unsubscribe', requireLoginMiddleware, async (request, response) => {
// Set user account info to render data
const { pnid } = request;
const pid = pnid.get('pid');
const subscriptionId = pnid.get('connections.stripe.subscription_id');
const tierName = pnid.get('connections.stripe.tier_name');
if (subscriptionId) {
try {
await stripe.subscriptions.del(subscriptionId);
const updateData = {
'connections.stripe.subscription_id': null,
'connections.stripe.price_id': null,
'connections.stripe.tier_level': 0,
'connections.stripe.tier_name': null,
};
if (pnid.get('access_level') < 2) {
// Fail-safe for if staff members reach here
// Mostly only useful during testing
updateData.access_level = 0;
}
await database.PNID.updateOne({ pid }, { $set: updateData }).exec();
} catch (error) {
logger.error(`Error canceling old user subscription | ${pnid.get('connections.stripe.customer_id')}, ${pid}, ${subscriptionId} | - ${error.message}`);
response.cookie('error_message', 'Error canceling subscription! Contact support if issue persists', { domain: '.pretendo.network' });
return response.redirect('/account');
}
}
response.cookie('success', `Unsubscribed from ${tierName}`, { domain: '.pretendo.network' });
return response.redirect('/account');
});
router.post('/stripe/webhook', express.raw({ type: 'application/json' }), async (request, response) => {
const stripeSignature = request.headers['stripe-signature'];
let event;
try {
event = stripe.webhooks.constructEvent(request.body, stripeSignature, config.stripe.webhook_secret);
} catch (err) {
return response.status(400).send(`Webhook Error: ${err.message}`);
}
await util.handleStripeEvent(event);
response.json({ received: true });
});
module.exports = router;

8
src/routes/aprilfools.js Normal file
View File

@ -0,0 +1,8 @@
const { Router } = require('express');
const router = new Router();
router.get('/', async (request, response) => {
response.render('aprilfools');
});
module.exports = router;

View File

@ -1,5 +1,4 @@
const { Router } = require('express');
const util = require('../util');
const logger = require('../logger');
const router = new Router();
@ -36,18 +35,11 @@ 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', {
layout: 'main',
locale,
localeString,
const renderData = {
postList
});
};
response.render('blog/blog', renderData);
});
// RSS feed
@ -69,10 +61,10 @@ 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 localeString = reqLocale.toString();
const renderData = {
layout: 'blog-opengraph',
postList,
};
// Get the name of the post from the URL
const postName = request.params.slug;
@ -89,6 +81,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 +90,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

@ -1,5 +1,4 @@
const { Router } = require('express');
const util = require('../util');
const router = new Router();
const fs = require('fs');
@ -11,32 +10,25 @@ router.get('/', async (request, response) => {
});
router.get('/search', async (request, response) => {
const reqLocale = request.locale;
const locale = util.getLocale(reqLocale.region, reqLocale.language);
const renderData = {
currentPage: request.params.slug
};
const localeString = reqLocale.toString();
response.render('docs/search', {
layout: 'main',
locale,
localeString,
currentPage: request.params.slug,
});
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 localeString = reqLocale.toString();
const renderData = {
currentPage: request.params.slug
};
// Get the name of the page from the URL
const pageName = request.params.slug;
let markdownLocale = localeString;
let markdownLocale = response.locals.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', markdownLocale, `${pageName}.md`))) {
null;
} else if (fs.existsSync(path.join('docs', 'en-US', `${pageName}.md`))) {
markdownLocale = 'en-US';
@ -45,6 +37,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 +50,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

@ -1,21 +1,21 @@
const { Router } = require('express');
const util = require('../util');
const { boards } = require('../../boards/boards.json');
const router = new Router();
const { getTrelloCache } = require('../trello');
const { getTrelloCache } = require('../cache');
router.get('/', async (request, response) => {
const reqLocale = request.locale;
const locale = util.getLocale(reqLocale.region, reqLocale.language);
const renderData = {
boards
};
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 = response.locals.locale.specialThanks.people.slice();
function shuffleArray(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
@ -25,11 +25,11 @@ router.get('/', async (request, response) => {
shuffleArray(specialThanksPeople);
// Slices the array in half
const specialThanksFirstRow = specialThanksPeople.slice(0, 3);
const specialThanksSecondRow = specialThanksPeople.slice(3, 7);
const specialThanksFirstRow = specialThanksPeople.slice(0, 4);
const specialThanksSecondRow = specialThanksPeople.slice(4);
// 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)
};
@ -75,14 +75,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

@ -0,0 +1,8 @@
const { Router } = require('express');
const router = new Router();
router.get('/', async (request, response) => {
response.render('localization');
});
module.exports = router;

20
src/routes/progress.js Normal file
View File

@ -0,0 +1,20 @@
const { Router } = require('express');
const { boards } = require('../../boards/boards.json');
const router = new Router();
const { getTrelloCache, getStripeDonationCache } = require('../cache');
router.get('/', async (request, response) => {
const renderData = {
boards
};
const trelloCache = await getTrelloCache();
renderData.progressLists = trelloCache;
const stripeDonationCache = await getStripeDonationCache();
renderData.donationCache = stripeDonationCache;
response.render('progress', renderData);
});
module.exports = router;

26
src/schema/pnid.js Normal file
View File

@ -0,0 +1,26 @@
const { Schema } = require('mongoose');
// Only define what we will be using
const PNIDSchema = new Schema({
pid: {
type: Number,
unique: true
},
server_access_level: String,
access_level: Number,
connections: {
discord: {
id: String
},
stripe: {
customer_id: String,
subscription_id: String,
price_id: String,
tier_level: Number,
tier_name: String,
latest_webhook_timestamp: Number
}
}
});
module.exports = PNIDSchema;

View File

@ -5,37 +5,26 @@ const handlebars = require('express-handlebars');
const morgan = require('morgan');
const expressLocale = require('express-locale');
const cookieParser = require('cookie-parser');
const logger = require('./logger');
const Stripe = require('stripe');
const redirectMiddleware = require('./middleware/redirect');
const renderDataMiddleware = require('./middleware/render-data');
const database = require('./database');
const util = require('./util');
const logger = require('./logger');
const config = require('../config.json');
const defaultLocale = require('../locales/US_en.json');
const { http: { port } } = config;
const app = express();
const stripe = new Stripe(config.stripe.secret_key);
logger.info('Setting up Middleware');
app.use(morgan('dev'));
app.use(express.urlencoded({ extended: true }));
logger.info('Setting up static public folder');
app.use(express.static('public'));
logger.info('Importing page routers');
const routers = {
home: require('./routers/home'),
faq: require('./routers/faq'),
docs: require('./routers/docs'),
progress: require('./routers/progress'),
account: require('./routers/account'),
blog: require('./routers/blog'),
localization: require('./routers/localization'),
aprilfools: require('./routers/aprilfools')
};
app.use(express.json());
app.use(express.urlencoded({
extended: true
}));
app.use(cookieParser());
// Locale express middleware setup
app.use(expressLocale({
'priority': ['cookie', 'accept-language', 'map', 'default'],
cookie: { name: 'preferredLocale' },
@ -44,6 +33,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',
@ -57,11 +47,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',
@ -78,15 +69,32 @@ app.use(expressLocale({
],
'default': 'en-US'
}));
app.use(redirectMiddleware);
app.use(renderDataMiddleware);
app.use('/', routers.home);
app.use('/faq', routers.faq);
app.use('/docs', routers.docs);
app.use('/progress', routers.progress);
app.use('/account', routers.account);
app.use('/localization', routers.localization);
app.use('/blog', routers.blog);
app.use('/nso-legacy-pack', routers.aprilfools);
logger.info('Setting up static public folder');
app.use(express.static('public'));
logger.info('Importing routes');
const routes = {
home: require('./routes/home'),
faq: require('./routes/faq'),
docs: require('./routes/docs'),
progress: require('./routes/progress'),
account: require('./routes/account'),
blog: require('./routes/blog'),
localization: require('./routes/localization'),
aprilfools: require('./routes/aprilfools')
};
app.use('/', routes.home);
app.use('/faq', routes.faq);
app.use('/docs', routes.docs);
app.use('/progress', routes.progress);
app.use('/account', routes.account);
app.use('/localization', routes.localization);
app.use('/blog', routes.blog);
app.use('/nso-legacy-pack', routes.aprilfools);
logger.info('Creating 404 status handler');
// This works because it is the last router created
@ -136,7 +144,7 @@ app.engine('handlebars', handlebars({
* get the string in the user's locale. If not available, it will return it in
* the default locale.
*/
args.slice(1, -1).forEach(arg => {
userLocaleString = userLocaleString?.[arg];
});
@ -156,6 +164,16 @@ app.engine('handlebars', handlebars({
app.set('view engine', 'handlebars');
logger.info('Starting server');
app.listen(port, () => {
logger.success(`Server listening on http://localhost:${port}`);
database.connect().then(() => {
app.listen(port, async () => {
const events = await stripe.events.list({
delivery_success: false // failed webhooks
});
for (const event of events.data) {
await util.handleStripeEvent(event);
}
logger.success(`Server listening on http://localhost:${port}`);
});
});

View File

@ -1,7 +1,13 @@
const fs = require('fs-extra');
const got = require('got');
const crypto = require('crypto');
const Stripe = require('stripe');
const mailer = require('./mailer');
const database = require('./database');
const logger = require('./logger');
const config = require('../config.json');
const stripe = new Stripe(config.stripe.secret_key);
function fullUrl(request) {
return `${request.protocol}://${request.hostname}${request.originalUrl}`;
@ -53,6 +59,86 @@ function apiDeleteGetRequest(path, headers, json) {
});
}
async function register(registerData) {
const apiResponse = await apiPostGetRequest('/v1/register', {}, registerData);
if (apiResponse.statusCode !== 200) {
throw new Error(apiResponse.body.error);
}
return apiResponse.body;
}
async function login(username, password) {
const apiResponse = await apiPostGetRequest('/v1/login', {}, {
username,
password,
grant_type: 'password'
});
if (apiResponse.statusCode !== 200) {
throw new Error(apiResponse.body.error);
}
return apiResponse.body;
}
async function refreshLogin(request, response) {
const apiResponse = await apiPostGetRequest('/v1/login', {}, {
refresh_token: request.cookies.refresh_token,
grant_type: 'refresh_token'
});
if (apiResponse.statusCode !== 200) {
// TODO: Error message
throw new Error('Bad');
}
const tokens = apiResponse.body;
response.cookie('refresh_token', tokens.refresh_token, { domain: '.pretendo.network' });
response.cookie('access_token', tokens.access_token, { domain: '.pretendo.network' });
response.cookie('token_type', tokens.token_type, { domain: '.pretendo.network' });
}
async function getUserAccountData(request, response, fromRetry=false) {
const apiResponse = await apiGetRequest('/v1/user', {
'Authorization': `${request.cookies.token_type} ${request.cookies.access_token}`
});
if (apiResponse.statusCode !== 200 && fromRetry === true) {
// TODO: Error message
throw new Error('Bad');
}
if (apiResponse.statusCode !== 200) {
await refreshLogin(request, response);
return await getUserAccountData(request, response, true);
}
return apiResponse.body;
}
async function updateDiscordConnection(discordUser, request, response, fromRetry=false) {
const apiResponse = await apiPostGetRequest('/v1/connections/add/discord', {
'Authorization': `${request.cookies.token_type} ${request.cookies.access_token}`
}, {
data: {
id: discordUser.id
}
});
if (apiResponse.statusCode !== 200 && fromRetry === true) {
// TODO: Error message
throw new Error('Bad');
}
if (apiResponse.statusCode !== 200) {
await refreshLogin(request, response);
await updateDiscordConnection(discordUser, request, response, true);
}
}
function nintendoPasswordHash(password, pid) {
const pidBuffer = Buffer.alloc(4);
pidBuffer.writeUInt32LE(pid);
@ -62,17 +148,204 @@ function nintendoPasswordHash(password, pid) {
Buffer.from('\x02\x65\x43\x46'),
Buffer.from(password)
]);
const hashed = crypto.createHash('sha256').update(unpacked).digest().toString('hex');
return hashed;
}
async function handleStripeEvent(event) {
if (event.type === 'customer.subscription.updated' || event.type === 'customer.subscription.deleted') {
const subscription = event.data.object;
const product = await stripe.products.retrieve(subscription.plan.product);
const customer = await stripe.customers.retrieve(subscription.customer);
if (!customer?.metadata?.pnid_pid) {
// No PNID PID linked to customer
if (subscription.status !== 'canceled' && subscription.status !== 'unpaid') {
// Abort and refund!
logger.error(`Stripe user ${customer.id} has no PNID linked! Refunding order`);
await stripe.subscriptions.del(subscription.id);
const invoice = await stripe.invoices.retrieve(subscription.latest_invoice);
await stripe.refunds.create({
payment_intent: invoice.payment_intent
});
try {
await mailer.sendMail({
to: customer.email,
subject: 'Pretendo Network Subscription Failed - No Linked PNID',
text: `Your recent subscription to Pretendo Network has failed.\nThis is due to no PNID PID being linked to the Stripe customer account used. The subscription has been canceled and refunded. Please contact Jon immediately.\nStripe Customer ID: ${customer.id}`
});
} catch (error) {
logger.error(`Error sending email | ${customer.id}, ${customer.email} | - ${error.message}`);
}
} else {
logger.error(`Stripe user ${customer.id} has no PNID linked!`);
}
return;
}
const pid = Number(customer.metadata.pnid_pid);
const pnid = await database.PNID.findOne({ pid });
const latestWebhookTimestamp = pnid.get('connections.stripe.latest_webhook_timestamp');
if (latestWebhookTimestamp && latestWebhookTimestamp > event.created) {
// Do nothing, this webhook is older than the latest seen
return;
}
if (!pnid && subscription.status !== 'canceled' && subscription.status !== 'unpaid') {
// PNID does not exist. Abort and refund!
logger.error(`PNID PID ${pid} does not exist! Found on Stripe user ${customer.id}! Refunding order`);
await stripe.subscriptions.del(subscription.id);
const invoice = await stripe.invoices.retrieve(subscription.latest_invoice);
await stripe.refunds.create({
payment_intent: invoice.payment_intent
});
try {
await mailer.sendMail({
to: customer.email,
subject: 'Pretendo Network Subscription Failed - PNID Not Found',
text: `Your recent subscription to Pretendo Network has failed.\nThis is due to the provided PNID not being found. The subscription has been canceled and refunded. Please contact Jon immediately.\nStripe Customer ID: ${customer.id}\nPNID PID: ${pid}`
});
} catch (error) {
logger.error(`Error sending email | ${customer.id}, ${customer.email} | - ${error.message}`);
}
return;
}
const currentSubscriptionId = pnid.get('connections.stripe.subscription_id');
if (subscription.status === 'canceled' && currentSubscriptionId && subscription.id !== currentSubscriptionId) {
// Canceling old subscription, do nothing but update webhook date
const updateData = {
'connections.stripe.latest_webhook_timestamp': event.created
};
await database.PNID.updateOne({
pid,
'connections.stripe.latest_webhook_timestamp': {
$lte: event.created
}
}, { $set: updateData }).exec();
return;
}
const updateData = {
'connections.stripe.subscription_id': subscription.status === 'active' ? subscription.id : null,
'connections.stripe.price_id': subscription.status === 'active' ? subscription.plan.id : null,
'connections.stripe.tier_level': subscription.status === 'active' ? Number(product.metadata.tier_level || 0) : 0,
'connections.stripe.tier_name': subscription.status === 'active' ? product.name : null,
'connections.stripe.latest_webhook_timestamp': event.created,
};
if (product.metadata.beta === 'true') {
switch (subscription.status) {
case 'active':
if (pnid.access_level < 2) { // only change access level if not staff member
updateData.access_level = 1;
}
break;
case 'canceled': // Subscription was canceled
case 'unpaid': // User missed too many payments
if (pnid.access_level < 2) { // only change access level if not staff member
updateData.access_level = 0;
}
break;
default:
break;
}
}
await database.PNID.updateOne({
pid,
'connections.stripe.latest_webhook_timestamp': {
$lte: event.created
}
}, { $set: updateData }).exec();
if (subscription.status === 'active') {
// Get all the customers active subscriptions
const { data: activeSubscriptions } = await stripe.subscriptions.list({
limit: 100,
status: 'active',
customer: customer.id
});
// Order subscriptions by creation time and remove the latest one
const orderedActiveSubscriptions = activeSubscriptions.sort((a, b) => b.created - a.created);
const pastSubscriptions = orderedActiveSubscriptions.slice(1);
// Remove any old past subscriptions that might still be hanging around
for (const pastSubscription of pastSubscriptions) {
try {
await stripe.subscriptions.del(pastSubscription.id);
} catch (error) {
logger.error(`Error canceling old user subscription | ${customer.id}, ${pid}, ${pastSubscription.id} | - ${error.message}`);
}
}
try {
await mailer.sendMail({
to: customer.email,
subject: 'Pretendo Network Subscription - Active',
text: `Thank you for purchasing the ${product.name} tier! We greatly value your support, thank you for helping keep Pretendo Network alive!\nIt may take a moment for your account dashboard to reflect these changes. Please wait a moment and refresh the dashboard to see them!`
});
} catch (error) {
logger.error(`Error sending email | ${customer.id}, ${customer.email}, ${pid} | - ${error.message}`);
}
}
if (subscription.status === 'canceled') {
try {
await mailer.sendMail({
to: customer.email,
subject: 'Pretendo Network Subscription - Canceled',
text: `Your subscription for the ${product.name} tier has been canceled. We thank for your previous support, and hope you still enjoy the network! `
});
} catch (error) {
logger.error(`Error sending email | ${customer.id}, ${customer.email}, ${pid} | - ${error.message}`);
}
}
if (subscription.status === 'unpaid') {
try {
await mailer.sendMail({
to: customer.email,
subject: 'Pretendo Network Subscription - Unpaid',
text: `Your subscription for the ${product.name} tier has been canceled due to non payment. We thank for your previous support, and hope you still enjoy the network! `
});
} catch (error) {
logger.error(`Error sending email | ${customer.id}, ${customer.email}, ${pid} | - ${error.message}`);
}
}
}
}
module.exports = {
fullUrl,
getLocale,
apiGetRequest,
apiPostGetRequest,
apiDeleteGetRequest,
nintendoPasswordHash
};
register,
login,
refreshLogin,
getUserAccountData,
updateDiscordConnection,
nintendoPasswordHash,
handleStripeEvent
};

View File

@ -10,11 +10,26 @@
<img src="{{account.mii.image_url}}" class="mii" />
<p class="miiname">{{account.mii.name}}</p>
<p class="username" value="{{account.username}}">PNID: {{account.username}}</p>
{{#if tierName}}
<p class="tier-name tier-level-{{tierLevel}}" value="{{tierName}}">{{tierName}}</p>
{{else}}
{{#if (neq account.access_level -1)}}
<p class="tier-name access-level-{{account.access_level}}" value="{{ localeHelper locale "account" "accountLevel" account.access_level }}">{{ localeHelper locale "account" "accountLevel" account.access_level }}</p>
{{else}}
<p class="tier-name access-level-banned" value="{{ localeHelper locale "account" "banned" }}">{{ localeHelper locale "account" "banned" }}</p>
{{/if}}
{{/if}}
</div>
<div class="buttons">
<a class="button secondary" id="download-cemu-files" href="/account/online-files" download>
<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-download"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>
<p class="download-caption">Download account files</p>
<p class="caption">Download account files</p>
<p class="cemu-warning">(will not work on Nintendo Network)</p>
</a>
<a class="button secondary" id="account-upgrade" href="/account/upgrade">
<p class="caption">Upgrade account</p>
</a>
</div>
</div>
@ -52,8 +67,8 @@
</div>
<div class="setting-card">
<h2 class="header">Servers</h2>
<fieldset {{#if isTester}}{{else}}disabled{{/if}}>
<h2 class="header">Server environment</h2>
<fieldset {{#unless isTester}}disabled{{/unless}}>
<form class="server-selection" id="server">
<input type="radio" id="prod" name="server_selection" value="prod" checked="{{ eq account.server_access_level 'prod'}}">
<label for="prod">
@ -67,6 +82,7 @@
</svg>
<h2>Production</h2>
</label>
{{#if isTester}}
<input type="radio" id="beta" name="server_selection" value="beta" checked="{{ neq account.server_access_level 'prod'}}">
<label for="beta">
<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">
@ -74,20 +90,16 @@
</svg>
<h2>Beta</h2>
</label>
{{/if}}
</form>
</fieldset>
{{#if discordUser}}
{{#if isTester }}
<p>Connected as {{ discordUser.username }}#{{ discordUser.discriminator }}</p>
<button class="button secondary" id="remove-discord-connection">Remove Discord account</button>
{{else}}
<p>Beta servers are exclusive to testers. To become a tester, check us out on <a href="https://www.patreon.com/PretendoNetwork" target="_blank">Patreon</a> and link your Discord account to your Patreon and PNID accounts</p>
{{/if}}
{{else}}
<p>Beta servers are exclusive to testers. If you're already a tester, connect your Discord account <a href="{{ discordAuthURL }}">here</a>.</p>
{{/if}}
{{#unless isTester}}
<p>Beta servers are exclusive to beta testers.<br>To become a beta tester, upgrade to a higher account tier.</p>
{{else}}
<p>Your current tier gives you beta server access. Cool!</p>
{{/unless}}
</div>
<h2 class="section-header" id="security">Sign in and security</h2>
<div class="setting-card">
<h2 class="header">Account</h2>
@ -125,7 +137,17 @@
</div>
<h2 class="section-header" id="other">Other settings</h2>
<div class="setting-card span-both-columns">
<div class="setting-card">
<h2 class="header">Discord</h2>
{{#if discordUser}}
<p>Connected to Discord as {{ discordUser.username }}#{{ discordUser.discriminator }}</p>
<button class="button secondary" id="remove-discord-connection">Remove Discord account</button>
{{else}}
<p>No Discord account linked. <a href="{{ discordAuthURL }}">Link Discord account here</a></p>
{{/if}}
</div>
<div class="setting-card">
<h2 class="header">Newsletter</h2>
<form id="other">
<input type="checkbox" id="marketing" name="marketing" {{#if account.flags.marketing}}checked{{/if}}>
@ -138,20 +160,32 @@
</div>
{{#if linked}}
{{#if success_message}}
<div class="banner-notice success">
<div>
<p>{{ linked }} account linked successfully</p>
<p>{{success_message}}</p>
</div>
</div>
{{/if}}
{{#if error}}
{{#if error_message}}
<div class="banner-notice error">
<div>
<p>{{ error }}</p>
<p>{{error_message}}</p>
</div>
</div>
{{/if}}
<script src="/assets/js/account.js"></script>
<div class="modal-wrapper hidden" id="onlinefiles">
<div class="modal">
<h1 class="title dot">Password</h1>
<p class="modal-caption">Enter your PNID password to download Cemu files</p>
<input name="password" id="password" type="password" required />
<div class="modal-button-wrapper">
<button class="button cancel" id="onlineFilesCloseButton">Cancel</button>
<button class="button primary confirm" id="onlineFilesConfirmButton">Confirm</button>
</div>
</div>
</div>
<script src="/assets/js/account.js"></script>

View File

@ -1,32 +1,10 @@
<link rel="stylesheet" href="/assets/css/login.css" />
<div class="wrapper">
<div class="account-form-wrapper">
<a class="logotype" href="/">
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="39.876">
<g id="logo_type" data-name="logo type" transform="translate(-553 -467)">
<g id="logo" transform="translate(553 467)">
<rect id="XMLID_158_" width="39.876" height="39.876" fill="#9d6ff3" opacity="0" />
<g id="XMLID_6_" transform="translate(8.222 1.418)">
<path id="XMLID_15_"
d="M69.149,28.312c-1.051.553-.129,2.139.922,1.585a12.365,12.365,0,0,1,8.794-.571,10.829,10.829,0,0,1,6.342,4.166c.645,1,2.231.074,1.585-.922C83.308,27.169,74.7,25.436,69.149,28.312Z"
transform="translate(-64.246 -23.389)" fill="#9d6ff3" />
<path id="XMLID_14_"
d="M82.64,14.608A15.565,15.565,0,0,0,73.5,8.45a17.535,17.535,0,0,0-12.647.9c-1.051.553-.129,2.139.922,1.585,3.411-1.788,7.6-1.714,11.209-.719,3.1.848,6.268,2.544,8.038,5.309C81.681,16.543,83.267,15.622,82.64,14.608Z"
transform="translate(-57.476 -7.693)" fill="#9d6ff3" />
<path id="XMLID_9_"
d="M55.68,47.8a10.719,10.719,0,0,0-6.71,2.3H45.983A1.336,1.336,0,0,0,44.6,51.376V75.84a1.431,1.431,0,0,0,1.383,1.383h3.023a1.367,1.367,0,0,0,1.309-1.383V68.392A10.993,10.993,0,1,0,55.68,47.8Zm0,17.182a6.213,6.213,0,1,1,6.213-6.213A6.216,6.216,0,0,1,55.68,64.982Z"
transform="translate(-44.6 -40.406)" fill="#9d6ff3" />
</g>
</g>
<text id="Pretendo" transform="translate(593 492)" fill="#fff" font-size="17"
font-family="Poppins-Bold, Poppins" font-weight="700">
<tspan x="0" y="0">Pretendo</tspan>
</text>
</g>
</svg>
</a>
{{> header}}
<div class="account-form-wrapper">
<form action="/account/login" method="post" class="account">
<h2>Sign in</h2>
<p>Enter your account details below</p>
@ -40,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>
@ -54,4 +33,4 @@
<p>{{ error }}</p>
</div>
</div>
{{/if}}
{{/if}}

File diff suppressed because it is too large Load Diff

View File

@ -2,36 +2,14 @@
<link rel="stylesheet" href="/assets/css/login.css" />
<div class="wrapper">
<div class="account-form-wrapper">
<a class="logotype" href="/">
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="39.876">
<g id="logo_type" data-name="logo type" transform="translate(-553 -467)">
<g id="logo" transform="translate(553 467)">
<rect id="XMLID_158_" width="39.876" height="39.876" fill="#9d6ff3" opacity="0" />
<g id="XMLID_6_" transform="translate(8.222 1.418)">
<path id="XMLID_15_"
d="M69.149,28.312c-1.051.553-.129,2.139.922,1.585a12.365,12.365,0,0,1,8.794-.571,10.829,10.829,0,0,1,6.342,4.166c.645,1,2.231.074,1.585-.922C83.308,27.169,74.7,25.436,69.149,28.312Z"
transform="translate(-64.246 -23.389)" fill="#9d6ff3" />
<path id="XMLID_14_"
d="M82.64,14.608A15.565,15.565,0,0,0,73.5,8.45a17.535,17.535,0,0,0-12.647.9c-1.051.553-.129,2.139.922,1.585,3.411-1.788,7.6-1.714,11.209-.719,3.1.848,6.268,2.544,8.038,5.309C81.681,16.543,83.267,15.622,82.64,14.608Z"
transform="translate(-57.476 -7.693)" fill="#9d6ff3" />
<path id="XMLID_9_"
d="M55.68,47.8a10.719,10.719,0,0,0-6.71,2.3H45.983A1.336,1.336,0,0,0,44.6,51.376V75.84a1.431,1.431,0,0,0,1.383,1.383h3.023a1.367,1.367,0,0,0,1.309-1.383V68.392A10.993,10.993,0,1,0,55.68,47.8Zm0,17.182a6.213,6.213,0,1,1,6.213-6.213A6.216,6.216,0,0,1,55.68,64.982Z"
transform="translate(-44.6 -40.406)" fill="#9d6ff3" />
</g>
</g>
<text id="Pretendo" transform="translate(593 492)" fill="#fff" font-size="17"
font-family="Poppins-Bold, Poppins" font-weight="700">
<tspan x="0" y="0">Pretendo</tspan>
</text>
</g>
</svg>
</a>
{{> header}}
<div class="account-form-wrapper">
<form action="/account/register" method="post" class="account register">
<h2>Register</h2>
<p>Enter your account details below</p>
<div>
<div class="email">
<label for="email">Email</label>
<input name="email" id="email" type="email" value="{{ email }}" required>
</div>
@ -52,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>
@ -66,4 +45,4 @@
<p>{{ error }}</p>
</div>
</div>
{{/if}}
{{/if}}

View File

@ -0,0 +1,104 @@
<link rel="stylesheet" href="/assets/css/upgrade.css" />
<div class="wrapper">
<a href="/account" class="back-arrow">
<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-arrow-left"><line x1="19" y1="12" x2="5" y2="12"></line><polyline points="12 19 5 12 12 5"></polyline></svg>
<span>Back</span>
</a>
<div class="account-form-wrapper">
<a class="logotype" href="/">
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="39.876">
<g id="logo_type" data-name="logo type" transform="translate(-553 -467)">
<g id="logo" transform="translate(553 467)">
<rect id="XMLID_158_" width="39.876" height="39.876" fill="#9d6ff3" opacity="0" />
<g id="XMLID_6_" transform="translate(8.222 1.418)">
<path id="XMLID_15_"
d="M69.149,28.312c-1.051.553-.129,2.139.922,1.585a12.365,12.365,0,0,1,8.794-.571,10.829,10.829,0,0,1,6.342,4.166c.645,1,2.231.074,1.585-.922C83.308,27.169,74.7,25.436,69.149,28.312Z"
transform="translate(-64.246 -23.389)" fill="#9d6ff3" />
<path id="XMLID_14_"
d="M82.64,14.608A15.565,15.565,0,0,0,73.5,8.45a17.535,17.535,0,0,0-12.647.9c-1.051.553-.129,2.139.922,1.585,3.411-1.788,7.6-1.714,11.209-.719,3.1.848,6.268,2.544,8.038,5.309C81.681,16.543,83.267,15.622,82.64,14.608Z"
transform="translate(-57.476 -7.693)" fill="#9d6ff3" />
<path id="XMLID_9_"
d="M55.68,47.8a10.719,10.719,0,0,0-6.71,2.3H45.983A1.336,1.336,0,0,0,44.6,51.376V75.84a1.431,1.431,0,0,0,1.383,1.383h3.023a1.367,1.367,0,0,0,1.309-1.383V68.392A10.993,10.993,0,1,0,55.68,47.8Zm0,17.182a6.213,6.213,0,1,1,6.213-6.213A6.216,6.216,0,0,1,55.68,64.982Z"
transform="translate(-44.6 -40.406)" fill="#9d6ff3" />
</g>
</g>
<text id="Pretendo" transform="translate(593 492)" fill="#fff" font-size="17"
font-family="Poppins-Bold, Poppins" font-weight="700">
<tspan x="0" y="0">Pretendo</tspan>
</text>
</g>
</svg>
</a>
<h1 class="title dot">Upgrade</h1>
<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}}" />
<label class="tier" for="{{this.price_id}}">
<div class="tier-thumbnail">
<img src="{{this.thumbnail}}" width="100%" height="auto" alt="Tier icon" />
</div>
<div class="tier-text">
<p class="tier-name">{{this.name}}</p>
<div class="tier-perks">
{{#each this.perks}}
<div>
<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-plus"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
<p>{{this}}</p>
</div>
{{/each}}
</div>
</div>
<p class="price">
<span>{{this.price}}</span> / month
</p>
</label>
{{/each}}
<div class="button-wrapper">
<button class="disabled" type="submit" id="submitButton">Select a tier</button>
<button class="unsubscribe hidden" id="unsubModalShowButton">Unsubscribe</button>
</div>
</form>
</div>
<div class="modal-wrapper hidden" id="unsub">
<div class="modal">
<h1 class="title dot">Unsubscribe</h1>
<p class="modal-caption">Are you sure you want to unsubscribe from <span>tiername</span>?
You will lose access to the perks associated with that tier.</p>
<div class="modal-button-wrapper">
<button class="button cancel" id="unsubModalCloseButton">Cancel</button>
<button class="button primary confirm" id="unsubModalConfirmButton">Unsubscribe</button>
</div>
</div>
</div>
<div class="modal-wrapper hidden" id="switchtier">
<div class="modal">
<h1 class="title dot">Change tier</h1>
<p class="modal-caption">Are you sure you want to unsubscribe
from <span class="oldtier">oldtiername</span> and subscribe to <span class="newtier">newtiername</span>?</p>
<div class="modal-button-wrapper">
<button class="button cancel" id="switchTierCloseButton">Cancel</button>
<button class="button primary confirm" id="switchTierConfirmButton">Confirm</button>
</div>
</div>
</div>
</div>
<script src="/assets/js/upgrade.js" />

View File

@ -40,7 +40,7 @@
</a>
library of first party games like never before, with fan-favorite titles such as Mario Kart 8 and Super Mario Maker making a return, and challenge your friends on the go with the Nintendo 3DS's Super Smash Bros. and Miitopia!</p>
</div>
<div class="bro-what dotted-bg new-font">
<h1>Why subscribe to Nintendo Switch Online + Legacy Pack when the online servers are still up and most first-party games have already been ported to the Nintendo Switch?</h1>
@ -78,4 +78,4 @@
document.querySelector("#Pretendo").setAttribute("font-family", "museo-sans, sans-serif");
document.querySelector("#Pretendo > tspan").innerHTML = "Nintendo Switch Online + Legacy Pack";
document.querySelector(".logo-link > svg").setAttribute("width", 350);
</script>
</script>

View File

@ -12,7 +12,7 @@
</div>
</div>
{{#each postList }}
{{#each postList }}
<a href="/blog/{{this.slug}}" class="purple-card blog-card">
<div class="post-info">
<h2 class="title">{{{ this.postInfo.title }}}</h2>
@ -43,4 +43,4 @@
</div>
{{> footer }}
</div>
</div>

View File

@ -2,7 +2,7 @@
<div class="docs-wrapper">
<a href="/" class="logo-link">
<a href="/" class="logo-link">
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="39.876">
<g id="logo_type" data-name="logo type" transform="translate(-553 -467)">
<g id="logo" transform="translate(553 467)">
@ -27,25 +27,25 @@
</svg>
</a>
{{> header}}
{{> docs-sidebar}}
{{> header}}
<div class="content">
<div class="content-inner">
{{#if missingInLocale}}
<p class="missing-in-locale-notice">{{ localeHelper locale "docs" "missingInLocale" }}</p>
{{/if}}
{{> docs-sidebar}}
<div class="content">
<div class="content-inner">
{{#if missingInLocale}}
<p class="missing-in-locale-notice">{{ localeHelper locale "docs" "missingInLocale" }}</p>
{{/if}}
{{#if showQuickLinks}}
<h1>{{ localeHelper locale "docs" "quickLinks" "header" }}</h1>
<div class="quick-links-grid">
<a href="/docs/troubleshoot-errors">
<a href="/docs/troubleshoot-errors">
<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-download"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg> <div>
<p class="header">{{ localeHelper locale "docs" "quickLinks" "links" 0 "header" }}</p>
<p>{{ localeHelper locale "docs" "quickLinks" "links" 0 "caption" }}</p>
</div>
<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-chevron-right"><polyline points="9 18 15 12 9 6"></polyline></svg>
<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-chevron-right"><polyline points="9 18 15 12 9 6"></polyline></svg>
</a>
<a href="/docs/beans">
@ -53,18 +53,17 @@
<p class="header">{{ localeHelper locale "docs" "quickLinks" "links" 1 "header" }}</p>
<p>{{ localeHelper locale "docs" "quickLinks" "links" 1 "caption" }}</p>
</div>
<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-chevron-right"><polyline points="9 18 15 12 9 6"></polyline></svg>
<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-chevron-right"><polyline points="9 18 15 12 9 6"></polyline></svg>
</a>
</div>
{{/if}}
{{{ content }}}
</div>
</div>
{{{ content }}}
</div>
</div>
</div>
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.3.1/styles/default.min.css">
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.3.1/highlight.min.js"></script>
<link rel="stylesheet" href="/assets/css/highlightjs.css">
<script>hljs.highlightAll();</script>

View File

@ -2,7 +2,7 @@
<div class="docs-wrapper">
<a href="/" class="logo-link">
<a href="/" class="logo-link">
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="39.876">
<g id="logo_type" data-name="logo type" transform="translate(-553 -467)">
<g id="logo" transform="translate(553 467)">
@ -27,14 +27,13 @@
</svg>
</a>
{{> header}}
{{> docs-sidebar}}
{{> header}}
<div class="content">
<div class="content-inner">
<div class="card"></div>
</div>
</div>
{{> docs-sidebar}}
<div class="content">
<div class="content-inner">
<div class="card"></div>
</div>
</div>
</div>

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>
@ -37,7 +37,7 @@
<div class="light-purple-circle">
<img class="n2ds" src="/assets/images/n2ds.png">
<div class="deco">
<svg xmlns="http://www.w3.org/2000/svg" width="839.371" height="893.406" viewBox="0 0 839.371 893.406">
<g id="deco" transform="translate(-1064.958 -142.958)">
<g id="Ellipse_12" data-name="Ellipse 12" transform="translate(1314 265)" fill="none" stroke="#9d6ff3" stroke-width="26">
@ -131,7 +131,7 @@
</div>
</div>
</section>
<section class="team">
<div class="sect-top sect team-top">
<h2 class="dot title" id="credits" {{#if locale.credits.titleSuffix}}data-title-suffix="{{locale.credits.titleSuffix}}"{{/if}}>{{ localeHelper locale "credits" "title" }}</h2>
@ -149,7 +149,7 @@
<span>{{ name }}</span>
<a href="{{ github }}" class="github" target="_blank">
<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>
</a>
</a>
</h3>
<p class="text">{{ caption }}</p>
</div>
@ -209,5 +209,5 @@
</section>
{{> footer }}
</div>
</div>

View File

@ -7,14 +7,14 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="manifest" href="/assets/site.webmanifest">
<meta name="msapplication-config" content="/assets/browserconfig.xml">
<!-- windows/ios/chrome -->
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta name="apple-mobile-web-app-title" content="Pretendo Network">
<meta name="application-name" content="Pretendo Network">
<meta name="msapplication-TileColor" content="#1b1f3b">
<meta name="theme-color" content="#1b1f3b">
<!-- open graph/embeds -->
<meta property="og:title" content="{{ postInfo.title }} | Pretendo Network Blog">
<meta property="og:description" content="{{ postInfo.caption }}">
@ -23,7 +23,7 @@
<meta property="og:image" content="{{ postInfo.cover_image }}">
<meta property="og:image:alt" content="Pretendo Network">
<meta property="og:site_name" content="Pretendo Network">
<!-- twitter embeds -->
<meta name="twitter:url" content="https://pretendo.network/">
<meta name="twitter:card" content="summary_large_image">
@ -31,14 +31,14 @@
<meta name="twitter:title" content="{{ postInfo.title }} | Pretendo Network Blog">
<meta name="twitter:description" content="{{ postInfo.caption }}">
<meta name="twitter:image" content="{{ postInfo.cover_image }}">
<!-- google seo -->
<meta name="description" content="An open source Nintendo Network replacement that aims to build custom servers for the WiiU and 3DS family of consoles">
<meta name="robots" content="index, follow">
<!-- RSS feed -->
<link rel="alternate" type="application/rss+xml" title="Pretendo Network Blog" href="/blog/feed.xml">
<!-- favicon -->
<link rel="apple-touch-icon" sizes="180x180" href="/assets/images/icons/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/assets/images/icons/favicon-32x32.png">
@ -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>
</html>

View File

@ -7,14 +7,14 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="manifest" href="/assets/site.webmanifest">
<meta name="msapplication-config" content="/assets/browserconfig.xml">
<!-- windows/ios/chrome -->
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta name="apple-mobile-web-app-title" content="Pretendo Network">
<meta name="application-name" content="Pretendo Network">
<meta name="msapplication-TileColor" content="#1b1f3b">
<meta name="theme-color" content="#1b1f3b">
<!-- open graph/embeds -->
<meta property="og:title" content="Pretendo Network">
<meta property="og:description" content="An open source Nintendo Network replacement that aims to build custom servers for the WiiU and 3DS family of consoles">
@ -23,7 +23,7 @@
<meta property="og:image" content="https://pretendo.network/assets/images/opengraph/opengraph-image.png">
<meta property="og:image:alt" content="Pretendo Network">
<meta property="og:site_name" content="Pretendo Network">
<!-- twitter embeds -->
<meta name="twitter:url" content="https://pretendo.network/">
<meta name="twitter:card" content="summary_large_image">
@ -31,14 +31,14 @@
<meta name="twitter:title" content="Pretendo Network">
<meta name="twitter:description" content="An open source Nintendo Network replacement that aims to build custom servers for the WiiU and 3DS family of consoles">
<meta name="twitter:image" content="https://pretendo.network/assets/images/opengraph/opengraph-image.png">
<!-- google seo -->
<meta name="description" content="An open source Nintendo Network replacement that aims to build custom servers for the WiiU and 3DS family of consoles">
<meta name="robots" content="index, follow">
<!-- RSS feed -->
<link rel="alternate" type="application/rss+xml" title="Pretendo Network Blog" href="/blog/feed.xml">
<!-- favicon -->
<link rel="apple-touch-icon" sizes="180x180" href="/assets/images/icons/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/assets/images/icons/favicon-32x32.png">
@ -48,7 +48,8 @@
<!-- css files -->
<link rel="stylesheet" href="/assets/css/dropdown.css" />
<link rel="stylesheet" href="/assets/css/main.css">
<link rel="stylesheet" href="/assets/css/main.css" />
<link rel="stylesheet" href="/assets/css/components.css" />
<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;700&display=swap" rel="stylesheet">
@ -67,8 +68,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>
</html>

View File

@ -9,7 +9,7 @@
<h1 class="title dot">{{ localeHelper locale "localizationPage" "title" }}</h1>
<p class="caption">{{ localeHelper locale "localizationPage" "description" }}</p>
<a href="https://github.com/PretendoNetwork/Pretendo/blob/master/CONTRIBUTING.md#localization" target="_blank" class="localization-instr">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="var(--theme-light)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-right"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="var(--accent-shade-2)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-right"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>
{{ localeHelper locale "localizationPage" "instructions" }}
</a>
<form class="localization-form" autocomplete="off">
@ -25,4 +25,4 @@
{{> footer }}
<script src="/assets/js/locale-tester-handler.js" />
<script src="/assets/js/locale-tester-handler.js" />

View File

@ -1,50 +1,50 @@
<div class="sidebar">
<div class="section">
<h5>Getting started</h5>
<a href="/docs/welcome">Welcome</a>
<a href="/docs/installing-juxt" >Installing Juxt</a>
<a href="/docs/search">Search</a>
</div>
<div class="section">
<h5>Getting started</h5>
<a href="/docs/welcome">Welcome</a>
<a href="/docs/installing-juxt" >Installing Juxt</a>
<a href="/docs/search">Search</a>
</div>
<div class="section">
<h5>Error codes - Juxt</h5>
<a href="/docs/JXT-598-0000">JXT-598-0000</a>
<a href="/docs/JXT-598-0001">JXT-598-0001</a>
<a href="/docs/JXT-598-0002">JXT-598-0002</a>
<a href="/docs/JXT-598-0003">JXT-598-0003</a>
<a href="/docs/JXT-598-0009">JXT-598-0009</a>
<a href="/docs/JXT-598-0010">JXT-598-0010</a>
<a href="/docs/JXT-598-0011">JXT-598-0011</a>
<a href="/docs/JXT-598-0015">JXT-598-0015</a>
<a href="/docs/JXT-598-0020">JXT-598-0020</a>
<a href="/docs/JXT-598-1XXX">JXT-598-1XXX</a>
<a href="/docs/JXT-598-2XXX">JXT-598-2XXX</a>
<a href="/docs/JXT-598-3XXX">JXT-598-3XXX</a>
<a href="/docs/JXT-598-4XXX">JXT-598-4XXX</a>
<a href="/docs/JXT-598-5XXX">JXT-598-5XXX</a>
</div>
<div class="section">
<h5>Error codes - Juxt</h5>
<a href="/docs/JXT-598-0000">JXT-598-0000</a>
<a href="/docs/JXT-598-0001">JXT-598-0001</a>
<a href="/docs/JXT-598-0002">JXT-598-0002</a>
<a href="/docs/JXT-598-0003">JXT-598-0003</a>
<a href="/docs/JXT-598-0009">JXT-598-0009</a>
<a href="/docs/JXT-598-0010">JXT-598-0010</a>
<a href="/docs/JXT-598-0011">JXT-598-0011</a>
<a href="/docs/JXT-598-0015">JXT-598-0015</a>
<a href="/docs/JXT-598-0020">JXT-598-0020</a>
<a href="/docs/JXT-598-1XXX">JXT-598-1XXX</a>
<a href="/docs/JXT-598-2XXX">JXT-598-2XXX</a>
<a href="/docs/JXT-598-3XXX">JXT-598-3XXX</a>
<a href="/docs/JXT-598-4XXX">JXT-598-4XXX</a>
<a href="/docs/JXT-598-5XXX">JXT-598-5XXX</a>
</div>
<div class="section">
<h5>Error codes - Beans</h5>
<a href="/docs/Black Beans">Black Beans</a>
<a href="/docs/black-eyed-peas">Black-Eyed Peas</a>
<a href="/docs/Cannellini Beans">Cannellini Beans</a>
<a href="/docs/Chickpeas (Garbanzo Beans)">Chickpeas (Garbanzo Beans)</a>
<a href="/docs/Great Northern Beans">Great Northern Beans</a>
<a href="/docs/Kidney Beans">Kidney Beans</a>
<a href="/docs/Lima Beans">Lima Beans</a>
<a href="/docs/Pinto Beans">Pinto Beans</a>
<a href="/docs/Fava Beans">Fava Beans</a>
<a href="/docs/Navy Beans">Navy Beans</a>
</div>
<div class="section">
<h5>Error codes - Beans</h5>
<a href="/docs/Black Beans">Black Beans</a>
<a href="/docs/black-eyed-peas">Black-Eyed Peas</a>
<a href="/docs/Cannellini Beans">Cannellini Beans</a>
<a href="/docs/Chickpeas (Garbanzo Beans)">Chickpeas (Garbanzo Beans)</a>
<a href="/docs/Great Northern Beans">Great Northern Beans</a>
<a href="/docs/Kidney Beans">Kidney Beans</a>
<a href="/docs/Lima Beans">Lima Beans</a>
<a href="/docs/Pinto Beans">Pinto Beans</a>
<a href="/docs/Fava Beans">Fava Beans</a>
<a href="/docs/Navy Beans">Navy Beans</a>
</div>
</div>
<script>
function selectSidebarElement(element) {
element.scrollIntoView({ block: "center" });
element.classList.add('active');
element.scrollIntoView({ block: "center" });
element.classList.add('active');
}
selectSidebarElement(document.querySelector("div.sidebar a[href='/docs/{{currentPage}}']"));
</script>
</script>

View File

@ -1,3 +1,5 @@
<link rel="stylesheet" href="/assets/css/partials/footer.css" /></link>
<footer>
<div>
<svg class="logotype" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 39.876" preserveAspectRatio="xMinYMin meet">
@ -28,8 +30,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 +39,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>
@ -52,4 +52,4 @@
{{ localeHelper locale "footer" "widget" "button" }}
</a>
</div>
</footer>
</footer>

View File

@ -1,3 +1,5 @@
<link rel="stylesheet" href="/assets/css/partials/header.css" ></link>
<header>
<a href="/" class="logo-link">
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="39.876">
@ -33,101 +35,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

@ -51,4 +51,4 @@
{{/each}}
</div>
</div>
</div>
</div>

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 }}
@ -20,4 +30,4 @@
{{> footer }}
</div>
</div>