mirror of
https://github.com/PretendoNetwork/website.git
synced 2026-07-30 22:57:36 -05:00
Merge branch 'dev' of https://github.com/PretendoNetwork/website into dev
This commit is contained in:
commit
b9f4da3f55
|
|
@ -16,5 +16,6 @@
|
|||
"tester_roles": [
|
||||
"role id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"aes_key": "hex key here"
|
||||
}
|
||||
2474
package-lock.json
generated
2474
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
|
@ -17,6 +17,7 @@
|
|||
},
|
||||
"homepage": "https://github.com/PretendoNetwork/website#readme",
|
||||
"dependencies": {
|
||||
"adm-zip": "^0.5.9",
|
||||
"colors": "^1.4.0",
|
||||
"cookie-parser": "^1.4.5",
|
||||
"discord-oauth2": "github:ryanblenis/discord-oauth2",
|
||||
|
|
@ -28,7 +29,8 @@
|
|||
"gray-matter": "^4.0.3",
|
||||
"marked": "^3.0.4",
|
||||
"morgan": "^1.10.0",
|
||||
"trello": "^0.11.0"
|
||||
"trello": "^0.11.0",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^7.32.0"
|
||||
|
|
|
|||
|
|
@ -25,6 +25,17 @@
|
|||
border-radius: 100%;
|
||||
background: var(--btn-secondary);
|
||||
}
|
||||
.account-sidebar .user #download-cemu-files {
|
||||
width: 100%;
|
||||
padding: 4px 16px;
|
||||
cursor: pointer;
|
||||
background: #383f6b;
|
||||
font-size: 0.9rem;
|
||||
margin: 8px 0 0;
|
||||
}
|
||||
.account-sidebar .user .cemu-warning {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
/* Settings */
|
||||
.settings-wrapper {
|
||||
|
|
|
|||
95
public/assets/css/register.css
Normal file
95
public/assets/css/register.css
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
.wrapper {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.register-wrapper {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
form.register {
|
||||
padding: 48px;
|
||||
background-color: #151934;
|
||||
border-radius: 12px;
|
||||
max-width: calc(90vw - 92px);
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
form.register input {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
display: block;
|
||||
border: 0;
|
||||
font-family: Poppins, Arial, Helvetica, sans-serif;
|
||||
font-size: 1rem;
|
||||
background-color: var(--btn-secondary);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
color: var(--text);
|
||||
width: calc(100% - 24px);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
form.register input::placeholder {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
form.register input:focus {
|
||||
background-color: #fff;
|
||||
color: var(--btn-secondary);
|
||||
transition: 200ms;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
form.register button {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
display: block;
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
font-family: Poppins, Arial, Helvetica, sans-serif;
|
||||
font-size: 1rem;
|
||||
color: var(--text);
|
||||
padding: 12px;
|
||||
background: var(--btn);
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
form.register .login {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
form.register .login button {
|
||||
background: var(--btn-secondary);
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
footer {
|
||||
margin-top: 23px;
|
||||
}
|
||||
|
||||
@keyframes banner-notice {
|
||||
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;
|
||||
}
|
||||
.banner-notice div {
|
||||
padding: 4px 36px;
|
||||
border-radius: 5px;
|
||||
z-index: 3;
|
||||
}
|
||||
.banner-notice.error div {
|
||||
background: #A9375B;
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
|
||||
document.getElementById('remove-discord-connection').addEventListener('click', () => {
|
||||
document.getElementById('remove-discord-connection')?.addEventListener('click', () => {
|
||||
// TODO: Refresh access token if expired (move this to the backend maybe?)
|
||||
|
||||
const tokenType = document.cookie.split('; ').find(row => row.startsWith('token_type=')).split('=')[1];
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
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({
|
||||
|
|
@ -15,7 +18,7 @@ const discordOAuth = new DiscordOauth2({
|
|||
|
||||
router.get('/', async (request, response) => {
|
||||
// Verify the user is logged in
|
||||
if (!request.cookies.access_token || !request.cookies.refresh_token) {
|
||||
if (!request.cookies.access_token || !request.cookies.refresh_token || !request.cookies.ph) {
|
||||
return response.redirect('/account/login');
|
||||
}
|
||||
|
||||
|
|
@ -29,8 +32,8 @@ router.get('/', async (request, response) => {
|
|||
};
|
||||
|
||||
// Reset message cookies
|
||||
response.clearCookie('linked');
|
||||
response.clearCookie('error');
|
||||
response.clearCookie('linked', { domain: '.pretendo.network' });
|
||||
response.clearCookie('error', { domain: '.pretendo.network' });
|
||||
|
||||
// Attempt to get user data
|
||||
let apiResponse = await util.apiGetRequest('/v1/user', {
|
||||
|
|
@ -183,7 +186,7 @@ router.get('/login', async (request, response) => {
|
|||
error: request.cookies.error
|
||||
};
|
||||
|
||||
response.clearCookie('error');
|
||||
response.clearCookie('error', { domain: '.pretendo.network' });
|
||||
|
||||
response.render('account_login', renderData);
|
||||
});
|
||||
|
|
@ -191,7 +194,7 @@ router.get('/login', async (request, response) => {
|
|||
router.post('/login', async (request, response) => {
|
||||
const { username, password } = request.body;
|
||||
|
||||
const apiResponse = await util.apiPostGetRequest('/v1/login', {}, {
|
||||
let apiResponse = await util.apiPostGetRequest('/v1/login', {}, {
|
||||
username,
|
||||
password,
|
||||
grant_type: 'password'
|
||||
|
|
@ -208,6 +211,71 @@ router.post('/login', async (request, response) => {
|
|||
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 } = 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
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
|
|
@ -255,9 +323,9 @@ router.get('/connect/discord', async (request, response) => {
|
|||
|
||||
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.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}`
|
||||
|
|
@ -281,4 +349,91 @@ router.get('/connect/discord', async (request, response) => {
|
|||
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());
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
19
src/util.js
19
src/util.js
|
|
@ -1,5 +1,6 @@
|
|||
const fs = require('fs-extra');
|
||||
const got = require('got');
|
||||
const crypto = require('crypto');
|
||||
const logger = require('./logger');
|
||||
|
||||
function fullUrl(request) {
|
||||
|
|
@ -52,10 +53,26 @@ function apiDeleteGetRequest(path, headers, json) {
|
|||
});
|
||||
}
|
||||
|
||||
function nintendoPasswordHash(password, pid) {
|
||||
const pidBuffer = Buffer.alloc(4);
|
||||
pidBuffer.writeUInt32LE(pid);
|
||||
|
||||
const unpacked = Buffer.concat([
|
||||
pidBuffer,
|
||||
Buffer.from('\x02\x65\x43\x46'),
|
||||
Buffer.from(password)
|
||||
]);
|
||||
|
||||
const hashed = crypto.createHash('sha256').update(unpacked).digest().toString('hex');
|
||||
|
||||
return hashed;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
fullUrl,
|
||||
getLocale,
|
||||
apiGetRequest,
|
||||
apiPostGetRequest,
|
||||
apiDeleteGetRequest
|
||||
apiDeleteGetRequest,
|
||||
nintendoPasswordHash
|
||||
};
|
||||
|
|
@ -9,7 +9,9 @@
|
|||
<div class="user">
|
||||
<img src="{{account.mii.image_url}}" class="mii" />
|
||||
<p class="miiname">{{account.mii.name}}</p>
|
||||
<p class="username">PNID: {{account.username}}</p>
|
||||
<p class="username" value="{{account.username}}">PNID: {{account.username}}</p>
|
||||
<a class="button secondary" id="download-cemu-files" href="/account/online-files" download>Download account files</a>
|
||||
<p class="cemu-warning">(will not work with Nintendo Network)</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
47
views/account_register.handlebars
Normal file
47
views/account_register.handlebars
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
<link rel="stylesheet" href="/assets/css/register.css" />
|
||||
|
||||
<div class="wrapper">
|
||||
|
||||
{{> header}}
|
||||
|
||||
|
||||
<div class="register-wrapper">
|
||||
<h1 class="title dot">Register</h1>
|
||||
<form action="/account/register" method="post" class="register">
|
||||
<div>
|
||||
<input name="email" id="email" type="email" placeholder="Email" value="{{ email }}" required>
|
||||
</div>
|
||||
<div>
|
||||
<input name="username" id="username" placeholder="Username" value="{{ username }}" minlength=6 maxlength=16 required>
|
||||
</div>
|
||||
<div>
|
||||
<input name="mii_name" id="mii_name" placeholder="Mii Name (Max 10 characters)" value="{{ mii_name }}" maxlength=10 required>
|
||||
</div>
|
||||
<div>
|
||||
<input name="password" id="password" type="password" placeholder="Password" autocomplete="new-password" required>
|
||||
</div>
|
||||
<div>
|
||||
<input name="password_confirm" id="password_confirm" type="password" placeholder="Confirm Password" required>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit">Register</button>
|
||||
</div>
|
||||
<div>
|
||||
<a href="/account/login" class="login">
|
||||
<button type="button">Already have an account?</button>
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{{> footer }}
|
||||
|
||||
</div>
|
||||
|
||||
{{#if error}}
|
||||
<div class="banner-notice error">
|
||||
<div>
|
||||
<p>{{ error }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
|
@ -116,18 +116,18 @@
|
|||
<div class="grid">
|
||||
<div class="item highlight">
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="book" class="svg-inline--fa fa-book fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M448 360V24c0-13.3-10.7-24-24-24H96C43 0 0 43 0 96v320c0 53 43 96 96 96h328c13.3 0 24-10.7 24-24v-16c0-7.5-3.5-14.3-8.9-18.7-4.2-15.4-4.2-59.3 0-74.7 5.4-4.3 8.9-11.1 8.9-18.6zM128 134c0-3.3 2.7-6 6-6h212c3.3 0 6 2.7 6 6v20c0 3.3-2.7 6-6 6H134c-3.3 0-6-2.7-6-6v-20zm0 64c0-3.3 2.7-6 6-6h212c3.3 0 6 2.7 6 6v20c0 3.3-2.7 6-6 6H134c-3.3 0-6-2.7-6-6v-20zm253.4 250H96c-17.7 0-32-14.3-32-32 0-17.6 14.4-32 32-32h285.4c-1.9 17.1-1.9 46.9 0 64z"></path></svg>
|
||||
<h1>Juxtaposition</h1>
|
||||
<p>A Miiverse replacement</p>
|
||||
<h1>Game Servers</h1>
|
||||
<p>Bringing back your favorite games and content using custom servers</p>
|
||||
</div>
|
||||
<div class="item">
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="book" class="svg-inline--fa fa-book fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M448 360V24c0-13.3-10.7-24-24-24H96C43 0 0 43 0 96v320c0 53 43 96 96 96h328c13.3 0 24-10.7 24-24v-16c0-7.5-3.5-14.3-8.9-18.7-4.2-15.4-4.2-59.3 0-74.7 5.4-4.3 8.9-11.1 8.9-18.6zM128 134c0-3.3 2.7-6 6-6h212c3.3 0 6 2.7 6 6v20c0 3.3-2.7 6-6 6H134c-3.3 0-6-2.7-6-6v-20zm0 64c0-3.3 2.7-6 6-6h212c3.3 0 6 2.7 6 6v20c0 3.3-2.7 6-6 6H134c-3.3 0-6-2.7-6-6v-20zm253.4 250H96c-17.7 0-32-14.3-32-32 0-17.6 14.4-32 32-32h285.4c-1.9 17.1-1.9 46.9 0 64z"></path></svg>
|
||||
<h1>Juxtaposition</h1>
|
||||
<p>A Miiverse replacement</p>
|
||||
<p>A re-imagining of Miiverse, as if it were made in the modern era</p>
|
||||
</div>
|
||||
<div class="item">
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="book" class="svg-inline--fa fa-book fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M448 360V24c0-13.3-10.7-24-24-24H96C43 0 0 43 0 96v320c0 53 43 96 96 96h328c13.3 0 24-10.7 24-24v-16c0-7.5-3.5-14.3-8.9-18.7-4.2-15.4-4.2-59.3 0-74.7 5.4-4.3 8.9-11.1 8.9-18.6zM128 134c0-3.3 2.7-6 6-6h212c3.3 0 6 2.7 6 6v20c0 3.3-2.7 6-6 6H134c-3.3 0-6-2.7-6-6v-20zm0 64c0-3.3 2.7-6 6-6h212c3.3 0 6 2.7 6 6v20c0 3.3-2.7 6-6 6H134c-3.3 0-6-2.7-6-6v-20zm253.4 250H96c-17.7 0-32-14.3-32-32 0-17.6 14.4-32 32-32h285.4c-1.9 17.1-1.9 46.9 0 64z"></path></svg>
|
||||
<h1>Juxtaposition</h1>
|
||||
<p>A Miiverse replacement</p>
|
||||
<h1>Cemu Support</h1>
|
||||
<p>Play your favorite Wii U titles even without a console!</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
<footer>
|
||||
<div>
|
||||
<svg class="logotype" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 39.876" preserveAspectRatio="xMinYMin meet"
|
||||
>
|
||||
<div>
|
||||
<svg class="logotype" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 39.876" preserveAspectRatio="xMinYMin meet">
|
||||
<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" />
|
||||
|
|
@ -23,26 +22,25 @@
|
|||
</text>
|
||||
</g>
|
||||
</svg>
|
||||
<p>Copyright 2021</p>
|
||||
<p>Site by <a href="https://mrjvs.com/" target="_blank">mrjvs</a>, <a href="https://jipfr.nl/" target="_blank">jipfr</a> & <a href="https://ashmonty.com/" target="_blank">monty</a></p>
|
||||
</div>
|
||||
<div>
|
||||
<h1>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>
|
||||
<div>
|
||||
<h1>Useful links</h1>
|
||||
<a href="/docs" style="display: none;">Docs</a>
|
||||
<p>Copyright 2021</p>
|
||||
<p>Site by <a href="https://mrjvs.com/" target="_blank">mrjvs</a>, <a href="https://jipfr.nl/" target="_blank">jipfr</a> & <a href="https://ashmonty.com/" target="_blank">monty</a></p>
|
||||
</div>
|
||||
<div>
|
||||
<h1>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://invite.gg/pretendo" target="_blank">Discord</a>
|
||||
<a href="https://github.com/PretendoNetwork" target="_blank">GitHub</a>
|
||||
</div>
|
||||
<div>
|
||||
<h1>Useful links</h1>
|
||||
<a style="display: none;" href="/docs">Docs</a>
|
||||
<a href="/faq">FAQ</a>
|
||||
<a href="/progress">Progress</a>
|
||||
<a href="/blog">Blog</a>
|
||||
<a href="/account" style="display: none; /* Hiding for now since it's not ready */">Account</a>
|
||||
</div>
|
||||
<div class="discord-server-card">
|
||||
<a href="/progress">Progress</a>
|
||||
<a href="/blog">Blog</a>
|
||||
<a href="/account" style="display: none; /* Hiding for now since it's not ready */">Account</a>
|
||||
</div>
|
||||
<div class="discord-server-card">
|
||||
<h1>Want to keep updated?</h1>
|
||||
<h2>Join our Discord server!</h2>
|
||||
<a href="https://invite.gg/pretendo">
|
||||
|
|
@ -51,6 +49,5 @@
|
|||
<polyline points="12 5 19 12 12 19"></polyline>
|
||||
</svg>
|
||||
Join now!</a>
|
||||
|
||||
</div>
|
||||
</footer>
|
||||
|
|
@ -28,7 +28,7 @@
|
|||
<a href="/#about">{{ localeHelper locale "nav" "about" }}</a>
|
||||
<a href="/#faq">{{ localeHelper locale "nav" "faq" }}</a>
|
||||
<a href="/#credits">{{ localeHelper locale "nav" "credits" }}</a>
|
||||
<a href="/docs" style="display: none;">{{ localeHelper locale "nav" "docs"}}</a>
|
||||
<a style="display: none;" href="/docs">{{ localeHelper locale "nav" "docs"}}</a>
|
||||
<a href="/progress" class="keep-on-mobile">{{ localeHelper locale "nav" "progress"}}</a>
|
||||
<a href="/blog" class="keep-on-mobile">{{ localeHelper locale "nav" "blog" }}</a>
|
||||
</nav>
|
||||
|
|
@ -118,5 +118,4 @@
|
|||
<div class="item"><span class="lang"></span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</header>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user