mirror of
https://github.com/PretendoNetwork/website.git
synced 2026-08-23 09:07:40 -05:00
Merge pull request #446 from PretendoNetwork/nuxt-refactor
Refactor: move to nuxt 4
This commit is contained in:
32
.docker/assets/garage-init.sh
Normal file
32
.docker/assets/garage-init.sh
Normal file
@@ -0,0 +1,32 @@
|
||||
# Taken from: https://spwoodcock.dev/blog/2026-03-03-automate-garage-standalone/
|
||||
# Modified to add environment variable support
|
||||
|
||||
# chroot into the s3 container's filesystem so the garage binary runs
|
||||
# with its own libs/linker, while the shared network namespace keeps
|
||||
# 127.0.0.1:3901 pointing at the live Garage server
|
||||
G="chroot /proc/1/root /garage -c /etc/garage.toml"
|
||||
|
||||
# Wait for server
|
||||
for i in $(seq 1 20); do
|
||||
if $G node id -q 2>/dev/null; then break; fi
|
||||
echo "Waiting for Garage RPC... ($i/20)"
|
||||
sleep 3
|
||||
done
|
||||
$G node id -q || { echo "Garage RPC not ready"; exit 1; }
|
||||
|
||||
# Init garage nodes
|
||||
if $G status 2>&1 | grep -q 'NO ROLE ASSIGNED'; then
|
||||
NODE_ID=$($G node id -q | cut -c1-16)
|
||||
$G layout assign "$NODE_ID" -z local -c 1G
|
||||
$G layout apply --version 1
|
||||
fi
|
||||
|
||||
# Create S3 bucket
|
||||
$G key import --yes -n "$S3_ACCESS_KEY_NAME" \
|
||||
"$S3_ACCESS_KEY_ID" \
|
||||
"$S3_ACCESS_KEY_SECRET" || true
|
||||
$G bucket create "$S3_BUCKET" || true
|
||||
$G bucket allow "$S3_BUCKET" --key "$S3_ACCESS_KEY_NAME" --read --write --owner || true
|
||||
$G bucket website --allow "$S3_BUCKET"
|
||||
|
||||
echo "Garage initialized."
|
||||
17
.docker/assets/garage.toml
Normal file
17
.docker/assets/garage.toml
Normal file
@@ -0,0 +1,17 @@
|
||||
metadata_dir = "/var/lib/garage/meta"
|
||||
data_dir = "/var/lib/garage/data"
|
||||
|
||||
replication_factor = 1
|
||||
db_engine = "sqlite"
|
||||
|
||||
rpc_bind_addr = "[::]:3901"
|
||||
rpc_public_addr = "127.0.0.1:3901"
|
||||
rpc_secret = "fbb4c8c25cdb7766ef9c2913cee0a6b52a927c03fafea2aa21e8658c68cc14c7"
|
||||
|
||||
[s3_api]
|
||||
s3_region = "garage"
|
||||
api_bind_addr = "[::]:3900"
|
||||
|
||||
[s3_web]
|
||||
bind_addr = "[::]:3902"
|
||||
root_domain = ".localhost"
|
||||
97
.docker/docker-compose.yml
Normal file
97
.docker/docker-compose.yml
Normal file
@@ -0,0 +1,97 @@
|
||||
name: "pn-website-dev"
|
||||
|
||||
services:
|
||||
redis:
|
||||
image: redis:8.2-alpine
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "6379:6379" # Redis port
|
||||
mailpit:
|
||||
image: axllent/mailpit
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- net
|
||||
ports:
|
||||
- 8025:8025 # Web port
|
||||
- 1025:1025 # SMTP port
|
||||
mongo:
|
||||
image: mongo:8.0
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- net
|
||||
command: ["--replSet", "rs0", "--bind_ip_all", "--port", "27017"]
|
||||
ports:
|
||||
- 27017:27017 # MongoDB port
|
||||
# This funky stuff is needed for making a replicaset
|
||||
healthcheck:
|
||||
test: echo "try { rs.status() } catch (err) { rs.initiate({_id:'rs0',members:[{_id:0,host:'mongo:27017'}]}) }" | mongosh --port 27017 --quiet
|
||||
interval: 5s
|
||||
timeout: 30s
|
||||
start_period: 0s
|
||||
start_interval: 1s
|
||||
retries: 30
|
||||
volumes:
|
||||
- "mongo_data:/data/db"
|
||||
garage:
|
||||
image: dxflrs/garage:v2.3.0
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- net
|
||||
ports:
|
||||
- "3900:3900" # S3 API port
|
||||
- "3902:3902" # Web port
|
||||
healthcheck:
|
||||
test: ["CMD", "/garage", "status"]
|
||||
start_period: 5s
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
volumes:
|
||||
- ./assets/garage.toml:/etc/garage.toml:ro
|
||||
- garage_data:/var/lib/garage
|
||||
garage-init:
|
||||
image: alpine:3.23
|
||||
depends_on:
|
||||
garage:
|
||||
condition: service_healthy
|
||||
network_mode: service:garage
|
||||
pid: service:garage
|
||||
environment:
|
||||
GARAGE_ADMIN_TOKEN: garage-admin-token
|
||||
S3_ACCESS_KEY_NAME: pretendo-key
|
||||
S3_ACCESS_KEY_ID: GK3515373e4c851ebaad366558
|
||||
S3_ACCESS_KEY_SECRET: 7d37d093435a41f2aab8f13c19ba067d9776c90215f56614adad6ece597dbb34
|
||||
S3_BUCKET: pretendo
|
||||
restart: "on-failure:2"
|
||||
entrypoint: "/bin/sh -eu /etc/init.sh"
|
||||
volumes:
|
||||
- "./assets/garage-init.sh:/etc/init.sh"
|
||||
account:
|
||||
image: ghcr.io/pretendonetwork/account:sha-edb4b02
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- net
|
||||
ports:
|
||||
- "8123:8123" # grpc
|
||||
- "8056:8056" # http
|
||||
environment:
|
||||
PN_ACT_CONFIG_HTTP_PORT: 8056
|
||||
PN_ACT_CONFIG_GRPC_PORT: 8123
|
||||
PN_ACT_CONFIG_MONGO_CONNECTION_STRING: "mongodb://mongo:27017/account?directConnection=true"
|
||||
PN_ACT_CONFIG_S3_ENDPOINT: "http://garage:3900"
|
||||
PN_ACT_CONFIG_S3_REGION: "garage"
|
||||
PN_ACT_CONFIG_S3_BUCKET: "pretendo"
|
||||
PN_ACT_CONFIG_S3_ACCESS_KEY: "GK3515373e4c851ebaad366558"
|
||||
PN_ACT_CONFIG_S3_ACCESS_SECRET: "7d37d093435a41f2aab8f13c19ba067d9776c90215f56614adad6ece597dbb34"
|
||||
PN_ACT_CONFIG_S3_FORCE_PATH_STYLE: "true"
|
||||
PN_ACT_CONFIG_CDN_BASE_URL: "http://pretendo.localhost:3902/"
|
||||
PN_ACT_CONFIG_AES_KEY: "1234567812345678123456781234567812345678123456781234567812345678"
|
||||
PN_ACT_CONFIG_GRPC_MASTER_API_KEY_ACCOUNT: "12345678123456781234567812345678"
|
||||
PN_ACT_CONFIG_GRPC_MASTER_API_KEY_API: "12345678123456781234567812345678"
|
||||
|
||||
networks:
|
||||
net:
|
||||
|
||||
volumes:
|
||||
mongo_data:
|
||||
garage_data:
|
||||
@@ -1,5 +1,5 @@
|
||||
.git
|
||||
.git/
|
||||
node_modules/
|
||||
.env
|
||||
node_modules
|
||||
dist
|
||||
logs
|
||||
.nuxt
|
||||
.output
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
# http://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
[*.{ts,js,vue,json}]
|
||||
indent_style = tab
|
||||
indent_size = 2
|
||||
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
|
||||
|
||||
3
.gitattributes
vendored
3
.gitattributes
vendored
@@ -1,2 +1 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
* text=auto eol=lf
|
||||
62
.github/workflows/docker.yml
vendored
62
.github/workflows/docker.yml
vendored
@@ -8,18 +8,21 @@ on:
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
SHOULD_PUSH_IMAGE: ${{ (github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/dev')) || github.event_name == 'workflow_dispatch' }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
build-publish:
|
||||
env:
|
||||
SHOULD_PUSH_IMAGE: ${{ (github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/dev')) || github.event_name == 'workflow_dispatch' }}
|
||||
build-publish-amd64:
|
||||
name: Build and Publish (amd64)
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU for Docker
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
@@ -48,9 +51,52 @@ jobs:
|
||||
id: build-and-push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
platforms: linux/amd64
|
||||
push: ${{ env.SHOULD_PUSH_IMAGE }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
build-publish-arm64:
|
||||
name: Build and Publish (arm64)
|
||||
runs-on: ubuntu-24.04-arm
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU for Docker
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log into the container registry
|
||||
if: ${{ env.SHOULD_PUSH_IMAGE == 'true' }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=raw,value=latest-arm,enable=${{ github.ref == 'refs/heads/master' }}
|
||||
type=raw,value=edge-arm,enable=${{ github.ref == 'refs/heads/dev' }}
|
||||
type=sha,suffix=-arm
|
||||
|
||||
- name: Build and push Docker image
|
||||
id: build-and-push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
platforms: linux/arm64
|
||||
push: ${{ env.SHOULD_PUSH_IMAGE }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
10
.github/workflows/lint.yml
vendored
10
.github/workflows/lint.yml
vendored
@@ -14,10 +14,16 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
node-version: "24"
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Prepare linting
|
||||
run: npm run prepare
|
||||
|
||||
- name: Typecheck
|
||||
run: npm run typecheck
|
||||
|
||||
- name: Lint
|
||||
run: npm run lint
|
||||
run: npm run lint -- --max-warnings=0
|
||||
|
||||
81
.gitignore
vendored
81
.gitignore
vendored
@@ -1,76 +1,11 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Typescript v1 declaration files
|
||||
typings/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# vscode settings
|
||||
.vscode
|
||||
|
||||
# JetBrains settings
|
||||
.idea
|
||||
|
||||
# dotenv environment variables file
|
||||
.output
|
||||
.data
|
||||
.nuxt
|
||||
.nitro
|
||||
.cache
|
||||
dist
|
||||
node_modules
|
||||
.env
|
||||
|
||||
# keep config and blog posts out of this
|
||||
# Old configuration ignored for safety
|
||||
config.json
|
||||
static-text.json
|
||||
.DS_Store
|
||||
|
||||
# keep browserified files out
|
||||
*.bundled.js
|
||||
|
||||
# remove nuxt files from passing from nuxt-refactor to master
|
||||
.nuxt
|
||||
.data
|
||||
|
||||
10
.travis.yml
10
.travis.yml
@@ -1,10 +0,0 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- "7"
|
||||
- "8"
|
||||
- "9"
|
||||
|
||||
sudo: false
|
||||
|
||||
script:
|
||||
- "npm run lint"
|
||||
7
.vscode/extensions.json
vendored
Normal file
7
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"editorconfig.editorconfig",
|
||||
"vue.volar"
|
||||
]
|
||||
}
|
||||
6
.vscode/settings.json
vendored
Normal file
6
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit",
|
||||
},
|
||||
"eslint.format.enable": true
|
||||
}
|
||||
13
Dockerfile
13
Dockerfile
@@ -4,7 +4,7 @@ ARG app_dir="/home/node/app"
|
||||
|
||||
|
||||
# * Base Node.js image
|
||||
FROM node:20-alpine AS base
|
||||
FROM node:24-alpine AS base
|
||||
ARG app_dir
|
||||
WORKDIR ${app_dir}
|
||||
|
||||
@@ -35,14 +35,15 @@ RUN npm run build
|
||||
FROM base AS final
|
||||
ARG app_dir
|
||||
|
||||
RUN mkdir -p ${app_dir}/logs && chown node:node ${app_dir}/logs
|
||||
|
||||
ENV NODE_ENV=production
|
||||
USER node
|
||||
ENV NODE_ENV=production
|
||||
ENV NITRO_HOST=0.0.0.0
|
||||
ENV NITRO_PORT=8080
|
||||
EXPOSE 8080
|
||||
|
||||
COPY package.json .
|
||||
|
||||
COPY --from=dependencies ${app_dir}/node_modules ${app_dir}/node_modules
|
||||
COPY --from=build ${app_dir} ${app_dir}
|
||||
COPY --from=build ${app_dir}/.output ${app_dir}/.output
|
||||
|
||||
CMD ["node", "."]
|
||||
CMD ["node", "--enable-source-maps", ".output/server/index.mjs"]
|
||||
|
||||
70
README.md
70
README.md
@@ -1,18 +1,64 @@
|
||||
# Website
|
||||
# Pretendo website
|
||||
|
||||
This repository contains the source code for [our website](https://pretendo.network). All contributions should go in the [dev branch](https://github.com/PretendoNetwork/website/tree/dev).
|
||||
This repository contains the source code for [our website](https://pretendo.network).
|
||||
|
||||
### Localization
|
||||
If you'd like to help localize Pretendo Network, you can check out our project on [Weblate](https://hosted.weblate.org/engage/pretendonetwork/).
|
||||
# Running locally for development
|
||||
|
||||
<a href="https://hosted.weblate.org/engage/pretendonetwork/">
|
||||
<img src="https://hosted.weblate.org/widgets/pretendonetwork/-/website/multi-auto.svg" alt="Translation status" />
|
||||
</a>
|
||||
Prerequisites:
|
||||
- Clone the repository
|
||||
- Have Docker Desktop installed (or Docker engine)
|
||||
- Have NodeJS 24 or higher installed
|
||||
|
||||
* * *
|
||||
Then follow these steps:
|
||||
- Run `docker compose up -d` inside `/.docker`
|
||||
- Create a file called `.env` in the root, fill it with the contents of `example.env`
|
||||
- Install dependencies with `npm i`
|
||||
- Run the app with `npm run dev`
|
||||
|
||||
Join our Discord:
|
||||
# Translation
|
||||
|
||||
<a href="https://discord.gg/DThgbba" target="_blank">
|
||||
<img src="https://discordapp.com/api/guilds/408718485913468928/widget.png?style=banner3">
|
||||
</a>
|
||||
If you'd like to help localize Pretendo Network, you can contribute to the translations on our project on [Weblate](https://hosted.weblate.org/engage/pretendonetwork/).
|
||||
|
||||
# Configuration
|
||||
|
||||
The application can be configured with environment variables. `.env` files are available for development.
|
||||
There are no fully required configuration variables, the app can runs minimally without any configuration:
|
||||
|
||||
| Feature | Variable | Description | Default |
|
||||
| ---------------------------- | -------------------------------------- | --------------------------------------------- | ---------------------------- |
|
||||
| Core | `PN_WEBSITE_PUBLIC_BASE_URL` | Base URL of the app | `https://pretendo.network` |
|
||||
| | `PN_WEBSITE_PUBLIC_CDN_BASE_URL` | Base URL for the CDN | `https://r2-cdn.pretendo.cc` |
|
||||
| | `PN_WEBSITE_PUBLIC_COOKIE_SECURE` | Should Secure be enabled for auth cookies | `true` |
|
||||
| | `PN_WEBSITE_TRUST_PROXY` | Should X-Forwarded-* headers be trusted? | `false` |
|
||||
| | `PN_WEBSITE_REDIS_URL` | Redis URL to use for caching & ratelimits | (No distributed KV) |
|
||||
| | | | |
|
||||
| Authentication | `PN_WEBSITE_GRPC_HOST` | Account server GRPC host + port | - |
|
||||
| | `PN_WEBSITE_GRPC_API_KEY` | Account server GRPC API key | - |
|
||||
| | `PN_WEBSITE_API_BASE` | Base URL of the account server | `https://api.pretendo.cc` |
|
||||
| | `PN_WEBSITE_API_BASE_HOST` | Hostname of the account server | `api.pretendo.cc` |
|
||||
| | | | |
|
||||
| Progress tracking | `PN_WEBSITE_GITHUB_API_TOKEN` | Github API token | - |
|
||||
| | | | |
|
||||
| Discord | `PN_WEBSITE_DISCORD_BOT_TOKEN` | Discord bot token | - |
|
||||
| | `PN_WEBSITE_DISCORD_CLIENT_ID` | Discord OAuth client ID | - |
|
||||
| | `PN_WEBSITE_DISCORD_CLIENT_SECRET` | Discord OAuth client secret | - |
|
||||
| | `PN_WEBSITE_DISCORD_GUILD_ID` | Discord server ID for role linking | - |
|
||||
| | `PN_WEBSITE_DISCORD_TESTER_ROLE_ID` | Role to give for tester access | (No role) |
|
||||
| | `PN_WEBSITE_DISCORD_SUPPORTER_ROLE_ID` | Role to give for supporter access | (No role) |
|
||||
| | | | |
|
||||
| Payments | `PN_WEBSITE_STRIPE_SECRET_KEY` | Stripe secret key | - |
|
||||
| (Requires `discord` feature) | `PN_WEBSITE_STRIPE_WEBHOOK_SECRET` | Stripe webhook signing key | - |
|
||||
| | `PN_WEBSITE_STRIPE_NOTIFICATION_EMAIL` | Email address to send stripe notifications to | (No notifications) |
|
||||
| | `PN_WEBSITE_MONGO_CONNECTION_STRING` | MongoDB connection string for account server | - |
|
||||
| | `PN_WEBSITE_SMTP_HOST` | Host for the SMTP server | - |
|
||||
| | `PN_WEBSITE_SMTP_PORT` | Port for the SMTP server | `587` |
|
||||
| | `PN_WEBSITE_SMTP_SECURE` | Use a secure SMTP connection | `true` |
|
||||
| | `PN_WEBSITE_SMTP_USER` | Username for the SMTP server | (No SMTP auth) |
|
||||
| | `PN_WEBSITE_SMTP_PASSWORD` | Password for the SMTP server | (No SMTP auth) |
|
||||
| | `PN_WEBSITE_SMTP_FROM_EMAIL` | Email to sent emails from | - |
|
||||
| | `PN_WEBSITE_SMTP_FROM_NAME` | Display of the FROM email adress | - |
|
||||
| | | | |
|
||||
| Captcha | `PN_WEBSITE_HCAPTCHA_SECRET_KEY` | HCaptcha secret key | - |
|
||||
| | `PN_WEBSITE_PUBLIC_HCAPTCHA_SITE_KEY` | HCaptcha site key | - |
|
||||
| | | | |
|
||||
| Discourse SSO | `PN_WEBSITE_DISCOURSE_SSO_SECRET` | Discourse SSO secret | - |
|
||||
|
||||
95
content.config.ts
Normal file
95
content.config.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import {
|
||||
defineContentConfig,
|
||||
defineCollection,
|
||||
defineCollectionSource,
|
||||
z
|
||||
} from '@nuxt/content';
|
||||
import { getAllErrors, getErrorInfo } from '@pretendonetwork/error-codes';
|
||||
|
||||
const errorCodeSource = defineCollectionSource({
|
||||
getKeys: () => {
|
||||
return getAllErrors().map((key: string) => `${key}.json`);
|
||||
},
|
||||
getItem: (key: string) => {
|
||||
const errorString = key.split('.')[0];
|
||||
const sysmodule = errorString.split('-')[0];
|
||||
const code = errorString.split('-')[1];
|
||||
const errorInfo = getErrorInfo(sysmodule, code, 'en-US');
|
||||
if (errorInfo) {
|
||||
errorInfo.code = errorString;
|
||||
}
|
||||
|
||||
return errorInfo;
|
||||
}
|
||||
});
|
||||
|
||||
export default defineContentConfig({
|
||||
collections: {
|
||||
blog: defineCollection({
|
||||
type: 'page',
|
||||
source: 'blog/*.md',
|
||||
schema: z.object({
|
||||
author: z.string(),
|
||||
author_image: z.string(),
|
||||
date: z.string(),
|
||||
caption: z.string(),
|
||||
cover_image: z.string()
|
||||
})
|
||||
}),
|
||||
docs: defineCollection({
|
||||
type: 'page',
|
||||
source: 'docs/**/*.md',
|
||||
schema: z.object({
|
||||
description: z.string()
|
||||
})
|
||||
}),
|
||||
terms: defineCollection({
|
||||
type: 'page',
|
||||
source: 'terms/*.md'
|
||||
}),
|
||||
errorcodes: defineCollection({
|
||||
type: 'data',
|
||||
source: errorCodeSource,
|
||||
schema: z.object({
|
||||
name: z.string(),
|
||||
message: z.string(),
|
||||
short_description: z.string(),
|
||||
long_description: z.string(),
|
||||
short_solution: z.string(),
|
||||
long_solution: z.string(),
|
||||
support_link: z.string(),
|
||||
module: z.object({
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
system: z.string()
|
||||
}),
|
||||
code: z.string()
|
||||
})
|
||||
}),
|
||||
team: defineCollection({
|
||||
type: 'data',
|
||||
source: 'team.json',
|
||||
schema: z.object({
|
||||
people: z.array(z.object({
|
||||
name: z.string(),
|
||||
captionKey: z.string(),
|
||||
picture: z.string().url(),
|
||||
github: z.string().url()
|
||||
}))
|
||||
})
|
||||
}),
|
||||
specialThanks: defineCollection({
|
||||
type: 'data',
|
||||
source: 'specialthanks.json',
|
||||
schema: z.object({
|
||||
people: z.array(z.object({
|
||||
name: z.string(),
|
||||
captionKey: z.string(),
|
||||
picture: z.string().url(),
|
||||
github: z.string().url(),
|
||||
isSpecial: z.boolean().default(false)
|
||||
}))
|
||||
})
|
||||
})
|
||||
}
|
||||
});
|
||||
@@ -83,7 +83,8 @@ In the latest version of the Cemu 2.0 experimental builds, support for Pretendo
|
||||
|
||||
If you haven't yet seen this video by Good Vibes Gaming, go check it out! I won't go anywhere...
|
||||
|
||||
[yt-iframe](Xtc9DJ6LYas)
|
||||
::md-iframe{video-id="Xtc9DJ6LYas"}
|
||||
::
|
||||
|
||||
All done? Welcome back! Yes, you aren't dreaming - Wii U Chat is finally here! The journey leading up to this point has been an adventurous one, so grab your popcorn as we go back to the beginning.
|
||||
|
||||
@@ -33,19 +33,22 @@ Both Mario Kart 7 and Mario Kart 8 have started going online and can play matche
|
||||
|
||||

|
||||
|
||||
[yt-iframe](W974FEDIoAA)
|
||||
::md-iframe{video-id="W974FEDIoAA"}
|
||||
::
|
||||
|
||||
## Hello YouTube!
|
||||
|
||||
An unintended side effect of working on Miiverse support in Mario Kart 8 was YouTube uploading being re-enabled! You can now upload race clips from Mario Kart 8 to YouTube again. Patrons who run the Miiverse patch can try this feature out right now
|
||||
|
||||
[yt-iframe](d3Bq7auupV0)
|
||||
::md-iframe{video-id="d3Bq7auupV0"}
|
||||
::
|
||||
|
||||
## Squid Game
|
||||
|
||||
Splatoon multiplayer battles now works! As of now only private friend battles have been tested, but they seem to be working without issue
|
||||
|
||||
[yt-iframe](d_qFnXrP7a4)
|
||||
::md-iframe{video-id="d_qFnXrP7a4"}
|
||||
::
|
||||
|
||||
## Friendship is magic
|
||||
|
||||
@@ -32,7 +32,8 @@ Juxt started out before I actually even joined the Pretendo Network development
|
||||
|
||||
From there we quickly realized that the scope of this project was going to be much larger than we though, and moved on to experimenting with the Miiverse Applet itself
|
||||
|
||||
[yt-iframe](d9VAr9sEvCo)
|
||||
::md-iframe{video-id="d9VAr9sEvCo"}
|
||||
::
|
||||
|
||||
> Check out this ancient video of the first demo website that was running in the Miiverse Applet.
|
||||
|
||||
@@ -40,11 +41,13 @@ Shortly after this the project was absorbed into the Pretendo Network, and our f
|
||||
|
||||
2020 was a big year for Juxt, going from the simple api server before, to rapidly building out its web interface and features across both the 3DS and Wii u
|
||||
|
||||
[yt-iframe](NrfaOx5xcJY)
|
||||
::md-iframe{video-id="NrfaOx5xcJY"}
|
||||
::
|
||||
|
||||
> First attempt at an interface for the 3DS
|
||||
|
||||
[yt-iframe](IXnJOacx_gE)
|
||||
::md-iframe{video-id="IXnJOacx_gE"}
|
||||
::
|
||||
|
||||
> Community page Demo on the Wii U
|
||||
|
||||
@@ -3,11 +3,11 @@ title: "Test"
|
||||
author: "limes.pink"
|
||||
author_image: "https://github.com/limes.pink.png"
|
||||
date: "January 20, 2038"
|
||||
caption: "A post to test the styling of the various elements we might use (rename to _test.md before deploying the blog section)"
|
||||
caption: "A post to test the styling of the various elements we might use"
|
||||
cover_image: "https://media.discordapp.net/attachments/413884110667251722/886474243662037062/image1.jpg"
|
||||
---
|
||||
|
||||
A post to test the styling of the various elements we might use (rename to _test.md before deploying the blog section)
|
||||
A post to test the styling of the various elements we might use
|
||||
|
||||
**bold**
|
||||
|
||||
@@ -35,23 +35,23 @@ _italic_
|
||||
|
||||
---
|
||||
|
||||
| Element | Description |
|
||||
| :----------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| table | The table HTML element represents tabular data — that is, information presented in a two-dimensional table comprised of rows and columns of cells containing data. |
|
||||
| tuble | The tuble HTML element represents tubular data — that is, information presented in a totally gnarly and radical way. |
|
||||
| table | A table is an item of furniture with a flat top and one or more legs, used as a surface for working at, eating from or on which to place things. |
|
||||
| Element | Description |
|
||||
| :------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| table | The table HTML element represents tabular data — that is, information presented in a two-dimensional table comprised of rows and columns of cells containing data. |
|
||||
| tuble | The tuble HTML element represents tubular data — that is, information presented in a totally gnarly and radical way. |
|
||||
| table | A table is an item of furniture with a flat top and one or more legs, used as a surface for working at, eating from or on which to place things. |
|
||||
|
||||
| Element | Description |
|
||||
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| table | The table HTML element represents tabular data — that is, information presented in a two-dimensional table comprised of rows and columns of cells containing data. |
|
||||
| tuble | The tuble HTML element represents tubular data — that is, information presented in a totally gnarly and radical way. |
|
||||
| table | A table is an item of furniture with a flat top and one or more legs, used as a surface for working at, eating from or on which to place things. |
|
||||
| Element | Description |
|
||||
| ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| table | The table HTML element represents tabular data — that is, information presented in a two-dimensional table comprised of rows and columns of cells containing data. |
|
||||
| tuble | The tuble HTML element represents tubular data — that is, information presented in a totally gnarly and radical way. |
|
||||
| table | A table is an item of furniture with a flat top and one or more legs, used as a surface for working at, eating from or on which to place things. |
|
||||
|
||||
| Element | Description |
|
||||
| -----------: | -----------------------------------------------------------------------------------------------------------------------------------------------------------------: |
|
||||
| table | The table HTML element represents tabular data — that is, information presented in a two-dimensional table comprised of rows and columns of cells containing data. |
|
||||
| tuble | The tuble HTML element represents tubular data — that is, information presented in a totally gnarly and radical way. |
|
||||
| table | A table is an item of furniture with a flat top and one or more legs, used as a surface for working at, eating from or on which to place things. |
|
||||
| Element | Description |
|
||||
| ------: | -----------------------------------------------------------------------------------------------------------------------------------------------------------------: |
|
||||
| table | The table HTML element represents tabular data — that is, information presented in a two-dimensional table comprised of rows and columns of cells containing data. |
|
||||
| tuble | The tuble HTML element represents tubular data — that is, information presented in a totally gnarly and radical way. |
|
||||
| table | A table is an item of furniture with a flat top and one or more legs, used as a surface for working at, eating from or on which to place things. |
|
||||
|
||||
Yee haw 🤠
|
||||
|
||||
@@ -126,14 +126,22 @@ console.log(trueOrFalseJSON);
|
||||
|
||||
<cite>Adapted from [blockquote: The Block Quotation element, from MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/blockquote)</cite>
|
||||
|
||||
[yt-iframe](djV11Xbc914)
|
||||
::md-iframe{video-id="djV11Xbc914"}
|
||||
::
|
||||
|
||||
```[yt-iframe](djV11Xbc914)```
|
||||
```
|
||||
::md-iframe{video-id="djV11Xbc914"}
|
||||
::
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
<a href="https://commons.wikimedia.org/wiki/File:View_of_the_Turin_Metro_tracks_from_Fermi_terminal.jpg">Wikilimes</a> (that's me!), <a href="https://creativecommons.org/licenses/by-sa/4.0">CC BY-SA 4.0</a>, via Wikimedia Commons
|
||||
|
||||
***
|
||||
|
||||
<video controls>
|
||||
<source src="https://cdn.discordapp.com/attachments/413884110667251722/878216238940160040/video0.mov">
|
||||
<source src="https://upload.wikimedia.org/wikipedia/commons/transcoded/5/5f/Steamboat_Willie_%281928%29_by_Walt_Disney.webm/Steamboat_Willie_%281928%29_by_Walt_Disney.webm.720p.vp9.webm">
|
||||
</video>
|
||||
|
||||
Blogposts whose filename starts with a \_ will not show up on the /blogs page, but will still be accessible from the url (keep in mind that the file is still going to be publicly accessible on GitHub).
|
||||
Blogposts with filename starting with \_ will not show up on the /blogs page, but will still be accessible via the url.
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
description: "Instructions on how to set up Pretendo on the 3DS/2DS family of consoles."
|
||||
---
|
||||
|
||||
# 3DS/2DS Family
|
||||
|
||||
<div class="tip red">
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
description: "Instructions on how to set up Pretendo on Azahar, the 3DS/2DS emulator."
|
||||
---
|
||||
|
||||
# Azahar
|
||||
|
||||
<div class="tip red">
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
description: "Instructions on how to set up Pretendo on Cemu, the Wii U emulator."
|
||||
---
|
||||
|
||||
<div class="tip green">This Guide may be missing some info or incomplete.</div>
|
||||
|
||||
# Cemu
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
description: "Citra is not supported. Please use Azahar instead."
|
||||
---
|
||||
|
||||
# Citra
|
||||
|
||||
Citra is not supported. Please use <a href="/docs/install/azahar" target="_blank">Azahar</a> instead.
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
description: "Instructions on how to install Juxtaposition."
|
||||
---
|
||||
|
||||
# Installing Juxtaposition
|
||||
|
||||
<div class="tip">
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
description: "Instructions on how to set up Pretendo on the Wii U console."
|
||||
---
|
||||
|
||||
# Wii U
|
||||
|
||||
<div class="tip red">
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
description: "Instructions on how to gather network dumps of games."
|
||||
---
|
||||
|
||||
# Network Dumps
|
||||
One of the best ways to support the project is to help in gathering network dumps of games. These network dumps can be used by developers to help understand how the servers operate in a live environment. This kind of data greatly simplifies development of the servers, as it removes a large amount of guesswork and reverse engineering.
|
||||
|
||||
83
content/specialthanks.json
Normal file
83
content/specialthanks.json
Normal file
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"people": [
|
||||
{
|
||||
"name": "superwhiskers",
|
||||
"captionKey": "forCrunch",
|
||||
"picture": "https://github.com/superwhiskers.png",
|
||||
"github": "https://github.com/superwhiskers"
|
||||
},
|
||||
{
|
||||
"name": "Stary",
|
||||
"captionKey": "forCtrDev",
|
||||
"picture": "https://github.com/Stary2001.png",
|
||||
"github": "https://github.com/Stary2001"
|
||||
},
|
||||
{
|
||||
"name": "rverse",
|
||||
"captionKey": "forMiiverseHelp",
|
||||
"picture": "https://github.com/rverseTeam.png",
|
||||
"github": "https://twitter.com/rverseClub"
|
||||
},
|
||||
{
|
||||
"name": "Kinnay",
|
||||
"isSpecial": true,
|
||||
"captionKey": "forResearch",
|
||||
"picture": "https://cdn.discordapp.com/avatars/186572995848830987/b55c0d4e7bfd792edf0689f83a25d8ea.png?size=128",
|
||||
"github": "https://github.com/Kinnay"
|
||||
},
|
||||
{
|
||||
"name": "NinStar",
|
||||
"captionKey": "forIcons",
|
||||
"picture": "https://github.com/ninstar.png",
|
||||
"github": "https://github.com/ninstar"
|
||||
},
|
||||
{
|
||||
"name": "Rambo6Glaz",
|
||||
"captionKey": "forServerDev",
|
||||
"picture": "https://github.com/EpicUsername12.png",
|
||||
"github": "https://github.com/EpicUsername12"
|
||||
},
|
||||
{
|
||||
"name": "GaryOderNichts",
|
||||
"captionKey": "forPatches",
|
||||
"picture": "https://github.com/GaryOderNichts.png",
|
||||
"github": "https://github.com/GaryOderNichts"
|
||||
},
|
||||
{
|
||||
"name": "zaksabeast",
|
||||
"captionKey": "forPatches",
|
||||
"picture": "https://cdn.discordapp.com/avatars/219324395707957248/c62573fbd4d26c8b4724f54413df6960.png?size=128",
|
||||
"github": "https://github.com/zaksabeast"
|
||||
},
|
||||
{
|
||||
"name": "mrjvs",
|
||||
"captionKey": "forServers",
|
||||
"picture": "https://github.com/mrjvs.png",
|
||||
"github": "https://github.com/mrjvs"
|
||||
},
|
||||
{
|
||||
"name": "binaryoverload",
|
||||
"captionKey": "forServers",
|
||||
"picture": "https://github.com/binaryoverload.png",
|
||||
"github": "https://github.com/binaryoverload"
|
||||
},
|
||||
{
|
||||
"name": "Simonx22",
|
||||
"captionKey": "forSplatoon",
|
||||
"picture": "https://github.com/Simonx22.png",
|
||||
"github": "https://github.com/Simonx22"
|
||||
},
|
||||
{
|
||||
"name": "OatmealDome",
|
||||
"captionKey": "forSplatoon",
|
||||
"picture": "https://github.com/OatmealDome.png",
|
||||
"github": "https://github.com/OatmealDome"
|
||||
},
|
||||
{
|
||||
"name": "GitHub contributors",
|
||||
"captionKey": "general",
|
||||
"picture": "https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png",
|
||||
"github": "https://github.com/PretendoNetwork"
|
||||
}
|
||||
]
|
||||
}
|
||||
76
content/team.json
Normal file
76
content/team.json
Normal file
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"people": [
|
||||
{
|
||||
"name": "Jonathan Barrow (jonbarrow)",
|
||||
"captionKey": "owner",
|
||||
"picture": "https://github.com/jonbarrow.png",
|
||||
"github": "https://github.com/jonbarrow"
|
||||
},
|
||||
{
|
||||
"name": "Jemma (CaramelKat)",
|
||||
"captionKey": "miiverseDev",
|
||||
"picture": "https://github.com/caramelkat.png",
|
||||
"github": "https://github.com/CaramelKat"
|
||||
},
|
||||
{
|
||||
"name": "quarky",
|
||||
"captionKey": "wiiuResearchAndPatchDev",
|
||||
"picture": "https://github.com/ashquarky.png",
|
||||
"github": "https://github.com/ashquarky"
|
||||
},
|
||||
{
|
||||
"name": "SuperMarioDaBom",
|
||||
"captionKey": "researchAndServerArch",
|
||||
"picture": "https://github.com/supermariodabom.png",
|
||||
"github": "https://github.com/SuperMarioDaBom"
|
||||
},
|
||||
{
|
||||
"name": "limes.pink",
|
||||
"captionKey": "webDev",
|
||||
"picture": "https://github.com/limesdotpink.png",
|
||||
"github": "https://github.com/limesdotpink"
|
||||
},
|
||||
{
|
||||
"name": "Shutterbug2000",
|
||||
"captionKey": "researchAndGameDev",
|
||||
"picture": "https://cdn.discordapp.com/avatars/191370953807233024/0311b61e2009c1576828dd2e9a59d72e.png?size=128",
|
||||
"github": "https://github.com/shutterbug2000"
|
||||
},
|
||||
{
|
||||
"name": "Billy",
|
||||
"captionKey": "preserveAndServerArch",
|
||||
"picture": "https://github.com/InternalLoss.png",
|
||||
"github": "https://github.com/InternalLoss"
|
||||
},
|
||||
{
|
||||
"name": "DaniElectra",
|
||||
"captionKey": "researchAndGameDev",
|
||||
"picture": "https://github.com/danielectra.png",
|
||||
"github": "https://github.com/DaniElectra"
|
||||
},
|
||||
{
|
||||
"name": "niko",
|
||||
"captionKey": "webDevAndGameDev",
|
||||
"picture": "https://github.com/hauntii.png",
|
||||
"github": "https://github.com/hauntii"
|
||||
},
|
||||
{
|
||||
"name": "MatthewL246",
|
||||
"captionKey": "devopsAndCommunityWork",
|
||||
"picture": "https://github.com/MatthewL246.png",
|
||||
"github": "https://github.com/MatthewL246"
|
||||
},
|
||||
{
|
||||
"name": "wolfendale",
|
||||
"captionKey": "gameDevAndOptimise",
|
||||
"picture": "https://github.com/wolfendale.png",
|
||||
"github": "https://github.com/wolfendale"
|
||||
},
|
||||
{
|
||||
"name": "TraceEntertains",
|
||||
"captionKey": "ctrResearchAndPatchDev",
|
||||
"picture": "https://github.com/TraceEntertains.png",
|
||||
"github": "https://github.com/TraceEntertains"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
# Familia 3DS/2DS
|
||||
|
||||
<div class="tip red">
|
||||
<strong>¡PRECAUCIÓN!</strong>
|
||||
LAS TRANSFERENCIAS DE SISTEMA NO ESTÁN ACTUALMENTE SOPORTADAS POR NUESTROS SERVIDORES. INTENTAR REALIZAR UNA TRANSFERENCIA DE SISTEMA PUEDE EVITAR QUE PUEDAS CONECTARTE EN LÍNEA EN EL FUTURO. EL SOPORTE PARA LAS TRANSFERENCIAS DE SISTEMA ESTÁ EN DESARROLLO.
|
||||
</div>
|
||||
|
||||
<div class="tip">
|
||||
ℹ️ Esta guía asume que tienes un <b>Sistema con Homebrew ejecutando la última versión de Luma3DS (13+)</b>. Si no es así, por favor sigue esta <a href="https://3ds.hacks.guide/" target="_blank">guía</a> para hacer Homebrew en tu sistema primero.
|
||||
</div>
|
||||
|
||||
Los siguientes pasos son necesarios para que te puedas conectar a la Red Pretendo:
|
||||
1. [Descargar Nimbus](#downloading-nimbus)
|
||||
2. [Habilitar parches de Luma](#luma-patches)
|
||||
3. [Nimbus](#using-nimbus)
|
||||
|
||||
## Descargar Nimbus
|
||||
|
||||
<div class="tip">
|
||||
ℹ️ Nimbus también está disponible en <a href="https://db.universal-team.net/3ds/nimbus" target="_blank">Universal-Updater</a>. Si no tienes Universal-Updater, puedes seguir esta <a href="https://universal-team.net/projects/universal-updater.html" target="_blank">guía</a>. Puedes descargar los archivos requeridos desde allí en lugar de GitHub, o instalar/actualizar la aplicación directamente desde tu consola.
|
||||
<br>
|
||||
<br>
|
||||
ℹ️ Si lo instalas directamente desde tu consola por primera vez, aún necesitarás instalar los parches IPS asociados desde GitHub. Una vez instalado, las actualizaciones pueden gestionarse exclusivamente desde Universal-Updater.
|
||||
</div>
|
||||
|
||||
Antes de comenzar, apaga tu consola e inserta la tarjeta SD en tu computadora.
|
||||
|
||||
Una vez insertada, descarga la última versión de [Nimbus](https://github.com/PretendoNetwork/Nimbus/releases/latest).
|
||||
|
||||
Nimbus está disponible tanto como una aplicación 3DSX como un CIA instalable. La página de versiones ofrece descargas de ambas opciones. Selecciona la versión que prefieras utilizar, o selecciona el archivo `combined.[versión].zip` para utilizar ambos.
|
||||
|
||||
<img src="/assets/images/docs/install/3ds/zip-highlight.png" width=100% height=auto/>
|
||||
|
||||
Extrae el contenido del archivo zip a la raíz de tu tarjeta SD. Si se te pregunta si deseas fusionar o sobrescribir archivos, acepta los cambios.
|
||||
|
||||
Asegúrate de que tu tarjeta SD tenga todos los siguientes archivos:
|
||||
|
||||
- `SD:/luma/titles/000400300000BC02/code.ips` (Miiverse, JPN)
|
||||
- `SD:/luma/titles/000400300000BD02/code.ips` (Miiverse, USA)
|
||||
- `SD:/luma/titles/000400300000BE02/code.ips` (Miiverse, EUR)
|
||||
- `SD:/luma/sysmodules/0004013000002F02.ips` (SSL)
|
||||
- `SD:/luma/sysmodules/0004013000003202.ips` (FRD/Friends)
|
||||
- `SD:/luma/sysmodules/0004013000003802.ips` (ACT/NNID)
|
||||
- `SD:/3ds/juxt-prod.pem` (certificado de Juxtaposition)
|
||||
|
||||
Si no lo instalaste a través de Universal-Updater, asegúrate de que al menos uno de los siguientes también esté presente:
|
||||
|
||||
- `SD:/cias/nimbus.cia`
|
||||
- `SD:/3ds/nimbus.3dsx`
|
||||
|
||||
Vuelve a insertar tu tarjeta SD en la consola.
|
||||
|
||||
## Parches de Luma
|
||||
|
||||
<div class="tip">
|
||||
ℹ️ <b>Salta este paso si ya has habilitado los parches necesarios en tu consola para la Red Pretendo.</b>
|
||||
</div>
|
||||
|
||||
Para usar el servicio de Red Pretendo, necesitarás habilitar los parches de Luma en tu consola. Mantén presionado el botón `SELECT` en tu 3DS y enciéndela.
|
||||
|
||||
En la pantalla que se muestra, asegúrate de que las siguientes opciones estén habilitadas:
|
||||
|
||||
- `Habilitar carga de firmwares y módulos externos`
|
||||
- `Habilitar parcheo de juegos`
|
||||
|
||||
Presiona `START` para guardar y continuar con estos cambios.
|
||||
|
||||
## Instalación de Nimbus en el Menú HOME
|
||||
|
||||
<div class="tip">
|
||||
ℹ️ <b>Salta este paso si solo descargaste el archivo zip de 3DSX.</b>
|
||||
</div>
|
||||
|
||||
Si descargaste los archivos combinados o CIA, puedes instalar Nimbus en el Menú HOME para un acceso rápido y sencillo.
|
||||
|
||||
Abre FBI. Si no tienes FBI, descarga la última versión desde [GitHub](https://github.com/lifehackerhansol/FBI/releases/latest). Selecciona `SD`, luego `cias`. Encuentra y selecciona `nimbus.cia`. Selecciona `Instalar CIA` o `Instalar y eliminar CIA`.
|
||||
|
||||
Una vez que haya terminado la instalación, presiona el botón HOME y sal de FBI. Deberías ver un mensaje indicando que se ha añadido una nueva aplicación al Menú HOME. Haz clic en OK y ahora tendrás Nimbus en tu Menú HOME.
|
||||
|
||||
## Usando Nimbus
|
||||
|
||||
Dependiendo de cómo hayas instalado Nimbus, ábrelo a través del Homebrew Launcher o el Menú HOME de 3DS. Selecciona `Pretendo` o `Nintendo` para cambiar entre los servicios.
|
||||
|
||||
Tu selección persistirá entre reinicios.
|
||||
|
||||
## Iniciar sesión en tu PNID
|
||||
|
||||
El 3DS no depende de los NNID para la gran mayoría de los servidores de juegos. Debido a esto, el uso de un PNID tampoco es necesario para la mayoría de los juegos<sup><a>[[1]](#footnote-1)</a></sup>.
|
||||
|
||||
Configurar un PNID en el 3DS es similar a configurar un NNID. Puedes crear el PNID en tu consola o registrarlo desde una cuenta en [nuestro sitio web](/account/register) y vincularlo a tu consola en una fecha posterior.
|
||||
|
||||
Se recomienda registrar el PNID en tu dispositivo en este momento, ya que registrar en el sitio web actualmente no te permite cambiar tus datos de usuario.
|
||||
|
||||
## Otra información
|
||||
|
||||
### ¿Cómo funciona Nimbus?
|
||||
Nimbus creará una segunda cuenta local establecida en el entorno `test` NASC. Los parches IPS establecerán que las URLs del entorno `test` NASC apunten a Pretendo. Puedes alternar libremente entre Pretendo y Nintendo. Tu modo seleccionado persistirá entre reinicios.
|
||||
|
||||
### ¿Segunda cuenta local?
|
||||
Quizás te hayas preguntado: _"¿Segunda cuenta local? ¿Qué es eso? Pensé que la 3DS solo tenía una cuenta."_ Y estarías medio en lo correcto. La 3DS normalmente solo admite una cuenta, y solo puedes tener una cuenta activa a la vez. Sin embargo, Nintendo implementó soporte para múltiples cuentas locales en la 3DS/2DS que permanece sin usar en todas las unidades minoristas. En una unidad minorista normal, solo se crea una cuenta local, que se establece en el entorno `prod` NASC. Nimbus utiliza esta función no utilizada para crear cuentas locales en un entorno sandbox con diferentes entornos.
|
||||
|
||||
<ul id="footnotes">
|
||||
<li id="footnote-1"><sup>[1]</sup> Algunos juegos pueden requerir un PNID para ciertas acciones, como compras en eShop. El único juego conocido que requiere un PNID para uso general es Nintendo Badge Arcade, que aún no es compatible.</li>
|
||||
</ul>
|
||||
@@ -1,26 +1,45 @@
|
||||
import pluginVue from 'eslint-plugin-vue';
|
||||
import eslintConfig from '@pretendonetwork/eslint-config';
|
||||
import globals from 'globals';
|
||||
import withNuxt from './.nuxt/eslint.config.mjs';
|
||||
|
||||
export default [
|
||||
export default withNuxt([
|
||||
...eslintConfig,
|
||||
...pluginVue.configs['flat/recommended'],
|
||||
{
|
||||
files: ['public/**'],
|
||||
rules: {
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@eslint-community/eslint-comments/disable-enable-pair': 'off',
|
||||
'no-restricted-imports': 'off'
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['*.vue', '**/*.vue'],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
parser: '@typescript-eslint/parser'
|
||||
},
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node
|
||||
...globals.browser
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
'vue/multi-word-component-names': 'off'
|
||||
}
|
||||
},
|
||||
{
|
||||
settings: {
|
||||
'import/resolver': {
|
||||
typescript: {
|
||||
alwaysTryTypes: true,
|
||||
project: [
|
||||
'./.nuxt/tsconfig.json'
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['src/**'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
ignores: ['**/*.bundled.js']
|
||||
ignores: ['.output', '.nuxt', '.old']
|
||||
}
|
||||
];
|
||||
]);
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
{
|
||||
"http": {
|
||||
"port": 3000,
|
||||
"base_url": "http://localhost:3000"
|
||||
},
|
||||
"github": {
|
||||
"graphql_token": "ghp_..."
|
||||
},
|
||||
"discord": {
|
||||
"roles": {
|
||||
"supporter": "role_id",
|
||||
"tester": "role_id"
|
||||
},
|
||||
"guild_id": "guild_id",
|
||||
"client_id": "client_id",
|
||||
"client_secret": "client_secret",
|
||||
"bot_token": "bot_token"
|
||||
},
|
||||
"stripe": {
|
||||
"goal_cents": 300000,
|
||||
"secret_key": "secret_key",
|
||||
"webhook_secret": "webhook_secret",
|
||||
"notification_emails": []
|
||||
},
|
||||
"database": {
|
||||
"account": {
|
||||
"uri": "mongodb://127.0.0.1:27017",
|
||||
"database": "database_name",
|
||||
"connection_string": "mongodb://127.0.0.1:27017",
|
||||
"options": {
|
||||
"useNewUrlParser": true,
|
||||
"useUnifiedTopology": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"email": {
|
||||
"ses": {
|
||||
"region": "AWS SES us-east-1",
|
||||
"key": "AWS SES access key",
|
||||
"secret": "AWS SES secret key"
|
||||
},
|
||||
"from": "Firstname Lastname <user@domain.com>"
|
||||
},
|
||||
"trello": {
|
||||
"api_key": "api_key",
|
||||
"api_token": "api_token",
|
||||
"board_name": "board_name"
|
||||
},
|
||||
"api_base": "https://api.domain.com"
|
||||
}
|
||||
49
example.env
Normal file
49
example.env
Normal file
@@ -0,0 +1,49 @@
|
||||
# The defaults in this file are meant for the docker-compose based setup.
|
||||
|
||||
# Core
|
||||
PN_WEBSITE_PUBLIC_BASE_URL=http://localhost:3000
|
||||
PN_WEBSITE_PUBLIC_CDN_BASE_URL=http://pretendo.localhost:3902
|
||||
PN_WEBSITE_PUBLIC_COOKIE_SECURE=false
|
||||
PN_WEBSITE_PUBLIC_REDIRECT_HOSTS=localhost:3000
|
||||
PN_WEBSITE_TRUST_PROXY=false
|
||||
PN_WEBSITE_REDIS_URL=redis://localhost:6379
|
||||
|
||||
# Optional - Authentication
|
||||
PN_WEBSITE_GRPC_HOST=localhost:8123
|
||||
PN_WEBSITE_GRPC_API_KEY=12345678123456781234567812345678
|
||||
PN_WEBSITE_API_BASE=http://localhost:8056
|
||||
PN_WEBSITE_API_BASE_HOST=api.pretendo.cc
|
||||
|
||||
|
||||
# --- Partially configured features ---
|
||||
# You will need to fill in some parts in this section before these features start working
|
||||
|
||||
# Optional - Github progress tracking:
|
||||
PN_WEBSITE_GITHUB_API_TOKEN=
|
||||
|
||||
# Optional - Discord:
|
||||
PN_WEBSITE_DISCORD_BOT_TOKEN=
|
||||
PN_WEBSITE_DISCORD_CLIENT_ID=
|
||||
PN_WEBSITE_DISCORD_CLIENT_SECRET=
|
||||
PN_WEBSITE_DISCORD_GUILD_ID=
|
||||
PN_WEBSITE_DISCORD_TESTER_ROLE_ID=
|
||||
PN_WEBSITE_DISCORD_SUPPORTER_ROLE_ID=
|
||||
|
||||
# Optional - Donation features + rewards (Requires discord):
|
||||
PN_WEBSITE_STRIPE_SECRET_KEY=
|
||||
PN_WEBSITE_STRIPE_WEBHOOK_SECRET=
|
||||
PN_WEBSITE_STRIPE_NOTIFICATION_EMAIL=notifs@example.com
|
||||
PN_WEBSITE_MONGO_CONNECTION_STRING=mongodb://localhost:27017/account?directConnection=true
|
||||
PN_WEBSITE_SMTP_HOST=localhost
|
||||
PN_WEBSITE_SMTP_USER=localhost
|
||||
PN_WEBSITE_SMTP_PORT=1025
|
||||
PN_WEBSITE_SMTP_SECURE=false
|
||||
PN_WEBSITE_SMTP_FROM_EMAIL=pretendo@example.com
|
||||
PN_WEBSITE_SMTP_FROM_NAME=Pretendo Network
|
||||
|
||||
# Optional - Captchas:
|
||||
PN_WEBSITE_HCAPTCHA_SECRET_KEY=
|
||||
PN_WEBSITE_PUBLIC_HCAPTCHA_SITE_KEY=
|
||||
|
||||
# Optional - Discourse SSO:
|
||||
PN_WEBSITE_DISCOURSE_SSO_SECRET=
|
||||
4
i18n.config.ts
Normal file
4
i18n.config.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default {
|
||||
fallbackLocale: 'en-US',
|
||||
warnHtmlMessage: false
|
||||
};
|
||||
252
nuxt.config.ts
Normal file
252
nuxt.config.ts
Normal file
@@ -0,0 +1,252 @@
|
||||
export default defineNuxtConfig({
|
||||
compatibilityDate: '2026-08-07',
|
||||
srcDir: './src',
|
||||
dir: {
|
||||
public: './src/public'
|
||||
},
|
||||
|
||||
nitro: {
|
||||
prerender: {
|
||||
routes: ['/blog/feed.xml']
|
||||
}
|
||||
},
|
||||
|
||||
vite: {
|
||||
server: {
|
||||
allowedHosts: ['pretendo.network']
|
||||
}
|
||||
},
|
||||
|
||||
modules: [
|
||||
'@pinia/nuxt',
|
||||
'@nuxt/eslint',
|
||||
'@nuxt/fonts',
|
||||
'@nuxt/icon',
|
||||
'@nuxt/content',
|
||||
'@nuxtjs/i18n'
|
||||
],
|
||||
|
||||
eslint: {
|
||||
config: {
|
||||
standalone: false
|
||||
}
|
||||
},
|
||||
|
||||
routeRules: {
|
||||
'/docs': { redirect: '/docs/welcome' }
|
||||
},
|
||||
|
||||
runtimeConfig: {
|
||||
nitro: {
|
||||
envPrefix: 'PN_WEBSITE_'
|
||||
},
|
||||
|
||||
trustProxy: false,
|
||||
githubApiToken: '',
|
||||
stripeSecretKey: '',
|
||||
stripeWebhookSecret: '',
|
||||
stripeNotificationEmail: '',
|
||||
hcaptchaSecretKey: '',
|
||||
grpcHost: '',
|
||||
grpcApiKey: '',
|
||||
mongoConnectionString: '',
|
||||
smtpHost: '',
|
||||
smtpPort: 587,
|
||||
smtpUser: '',
|
||||
smtpPassword: '',
|
||||
smtpSecure: true,
|
||||
smtpFromEmail: '',
|
||||
smtpFromName: '',
|
||||
discordBotToken: '',
|
||||
discordClientId: '',
|
||||
discordClientSecret: '',
|
||||
discordGuildId: '',
|
||||
discordTesterRoleId: '',
|
||||
discordSupporterRoleId: '',
|
||||
discourseSsoSecret: '',
|
||||
apiBase: 'https://api.pretendo.cc',
|
||||
apiBaseHost: 'api.pretendo.cc',
|
||||
redisUrl: '',
|
||||
|
||||
public: {
|
||||
baseUrl: 'https://pretendo.network',
|
||||
cdnBaseUrl: 'https://r2-cdn.pretendo.cc',
|
||||
redirectHosts: 'pretendo.network',
|
||||
hcaptchaSiteKey: '',
|
||||
cookieSecure: true
|
||||
}
|
||||
},
|
||||
|
||||
css: ['~/assets/css/main.css'],
|
||||
|
||||
fonts: {
|
||||
defaults: {
|
||||
weights: [400, 700],
|
||||
styles: ['normal', 'italic']
|
||||
}
|
||||
},
|
||||
|
||||
icon: {
|
||||
clientBundle: {
|
||||
scan: true
|
||||
},
|
||||
provider: 'none',
|
||||
serverBundle: 'local'
|
||||
},
|
||||
|
||||
components: [
|
||||
{
|
||||
path: '~/components',
|
||||
pathPrefix: false
|
||||
}
|
||||
],
|
||||
|
||||
content: {
|
||||
build: {
|
||||
markdown: {
|
||||
highlight: {
|
||||
theme: 'github-dark'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
app: {
|
||||
head: {
|
||||
link: [
|
||||
{
|
||||
rel: 'icon',
|
||||
type: 'image/x-icon',
|
||||
href: '/assets/images/icons/favicon.ico'
|
||||
},
|
||||
{
|
||||
rel: 'apple-touch-icon',
|
||||
sizes: '180x180',
|
||||
href: '/assets/images/icons/apple-touch-icon.png'
|
||||
},
|
||||
{
|
||||
rel: 'icon',
|
||||
sizes: '32x32',
|
||||
type: 'image/png',
|
||||
href: '/assets/images/icons/favicon-32x32.png'
|
||||
},
|
||||
{
|
||||
rel: 'icon',
|
||||
sizes: '16x16',
|
||||
type: 'image/png',
|
||||
href: '/assets/images/icons/favicon-16x16.png'
|
||||
},
|
||||
{
|
||||
rel: 'mask-icon',
|
||||
href: '/assets/images/icons/safari-pinned-tab.svg',
|
||||
color: '#1b1f3b'
|
||||
},
|
||||
{ rel: 'manifest', href: '/assets/site.webmanifest' },
|
||||
{
|
||||
rel: 'alternate',
|
||||
type: 'application/rss+xml',
|
||||
title: 'Pretendo Network Blog',
|
||||
href: '/blog/feed.xml'
|
||||
}
|
||||
],
|
||||
meta: [
|
||||
{ name: 'msapplication-config', content: '/assets/browserconfig.xml' },
|
||||
{ 'http-equiv': 'X-UA-Compatible', 'content': 'ie=edge' },
|
||||
{ name: 'apple-mobile-web-app-title', content: 'Pretendo Network' },
|
||||
{ name: 'application-name', content: 'Pretendo Network' },
|
||||
{ name: 'msapplication-TileColor', content: '#1b1f3b' },
|
||||
{ name: 'theme-color', content: '#1b1f3b' },
|
||||
{
|
||||
property: 'og:description',
|
||||
content:
|
||||
'An open source Nintendo Network replacement that aims to build custom servers for the WiiU and 3DS family of consoles'
|
||||
},
|
||||
{ property: 'og:type', content: 'website' },
|
||||
{ property: 'og:url', content: 'https://pretendo.network' },
|
||||
{
|
||||
property: 'og:image',
|
||||
content:
|
||||
'https://pretendo.network/assets/images/opengraph/opengraph-image.png'
|
||||
},
|
||||
{ property: 'og:image:alt', content: '' },
|
||||
{ property: 'og:site_name', content: 'Pretendo Network' },
|
||||
{ property: 'twitter:url', content: 'https://pretendo.network/' },
|
||||
{ property: 'twitter:card', content: 'summary_large_image' },
|
||||
{ property: 'twitter:site', content: '@PretendoNetwork' },
|
||||
{
|
||||
property: 'twitter:description',
|
||||
content:
|
||||
'An open source Nintendo Network replacement that aims to build custom servers for the WiiU and 3DS family of consoles'
|
||||
},
|
||||
{
|
||||
property: 'twitter:image',
|
||||
content:
|
||||
'https://pretendo.network/assets/images/opengraph/opengraph-image.png'
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
content:
|
||||
'An open source Nintendo Network replacement that aims to build custom servers for the WiiU and 3DS family of consoles'
|
||||
},
|
||||
{ property: 'robots', content: 'index, follow' }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
i18n: {
|
||||
compilation: {
|
||||
strictMessage: false
|
||||
},
|
||||
restructureDir: 'src',
|
||||
strategy: 'no_prefix',
|
||||
defaultLocale: 'en-US',
|
||||
vueI18n: '../i18n.config.ts',
|
||||
locales: [
|
||||
{ code: 'ar-AR', name: 'العربية', file: 'ar_AR.json' },
|
||||
{ code: 'ast', name: 'Asturianu', file: 'ast.json' },
|
||||
{ code: 'be-BY', name: 'Беларуская', file: 'be_BY.json' },
|
||||
{ code: 'ca-ES', name: 'Català', file: 'ca_ES.json' },
|
||||
{ code: 'cs-CZ', name: 'Čeština', file: 'cs_CZ.json' },
|
||||
{ code: 'cy-GB', name: 'Cymraeg', file: 'cy_GB.json' },
|
||||
{ code: 'da-DK', name: 'Dansk', file: 'da_DK.json' },
|
||||
{ code: 'de-DE', name: 'Deutsch', file: 'de_DE.json' },
|
||||
{ code: 'el-GR', name: 'Ελληνικά', file: 'el_GR.json' },
|
||||
{ code: 'en-GB', name: 'English (United Kingdom)', file: 'en_GB.json' },
|
||||
{ code: 'en-US', name: 'English (United States)', file: 'en_US.json' },
|
||||
{ code: 'en@uwu', name: 'English (lolcat)', file: 'en@uwu.json' },
|
||||
{ code: 'eo-XX', name: 'Esperanto', file: 'eo_XX.json' },
|
||||
{ code: 'es-ES', name: 'Español', file: 'es_ES.json' },
|
||||
{ code: 'fi-FI', name: 'Suomi', file: 'fi_FI.json' },
|
||||
{ code: 'fr-CA', name: 'Français (Canada)', file: 'fr_CA.json' },
|
||||
{ code: 'fr-FR', name: 'Français', file: 'fr_FR.json' },
|
||||
{ code: 'ga-IE', name: 'Gaeilge', file: 'ga_IE.json' },
|
||||
{ code: 'gd-GB', name: 'Gàidhlig', file: 'gd_GB.json' },
|
||||
{ code: 'gl-ES', name: 'Galego', file: 'gl_ES.json' },
|
||||
{ code: 'hr-HR', name: 'Hrvatski', file: 'hr_HR.json' },
|
||||
{ code: 'hu-HU', name: 'Magyar', file: 'hu_HU.json' },
|
||||
{ code: 'id-ID', name: 'Bahasa Indonesia', file: 'id_ID.json' },
|
||||
{ code: 'it-IT', name: 'Italiano', file: 'it_IT.json' },
|
||||
{ code: 'ja-JP', name: '日本語', file: 'ja_JP.json' },
|
||||
{ code: 'kk-KZ', name: 'Қазақша', file: 'kk_KZ.json' },
|
||||
{ code: 'ko-KR', name: '한국어', file: 'ko_KR.json' },
|
||||
{ code: 'lt-LT', name: 'Lietuvių', file: 'lt_LT.json' },
|
||||
{ code: 'lv-LV', name: 'Latviešu', file: 'lv_LV.json' },
|
||||
{ code: 'nb-NO', name: 'Norsk bokmål', file: 'nb_NO.json' },
|
||||
{ code: 'nl-NL', name: 'Nederlands', file: 'nl_NL.json' },
|
||||
{ code: 'pl-PL', name: 'Polski', file: 'pl_PL.json' },
|
||||
{ code: 'pt-BR', name: 'Português (Brasil)', file: 'pt_BR.json' },
|
||||
{ code: 'pt-PT', name: 'Português (Portugal)', file: 'pt_PT.json' },
|
||||
{ code: 'ro-RO', name: 'Română', file: 'ro_RO.json' },
|
||||
{ code: 'ru-RU', name: 'Русский', file: 'ru_RU.json' },
|
||||
{ code: 'sk-SK', name: 'Slovenčina', file: 'sk_SK.json' },
|
||||
{ code: 'sr-RS', name: 'Српски', file: 'sr_RS.json' },
|
||||
{ code: 'sv-SE', name: 'Svenska', file: 'sv_SE.json' },
|
||||
{ code: 'tr-TR', name: 'Türkçe', file: 'tr_TR.json' },
|
||||
{ code: 'uk-UA', name: 'Українська', file: 'uk_UA.json' },
|
||||
{ code: 'tr-TR', name: 'Türkçe', file: 'tr_TR.json' },
|
||||
{ code: 'uk-UA', name: 'Українська', file: 'uk_UA.json' },
|
||||
{ code: 'zh-CN', name: '中文 (简体)', file: 'zh_CN.json' },
|
||||
{ code: 'zh-Hant', name: '中文 (繁體)', file: 'zh_Hant.json' }
|
||||
]
|
||||
}
|
||||
});
|
||||
29908
package-lock.json
generated
29908
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
103
package.json
103
package.json
@@ -1,57 +1,70 @@
|
||||
{
|
||||
"name": "website",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "src/server.js",
|
||||
"description": "Website for Pretendo Network",
|
||||
"license": "AGPL-3.0-only",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node src/server.js",
|
||||
"build": "npm run browserify",
|
||||
"start": "node --env-file=.env --enable-source-maps .output/server/index.mjs",
|
||||
"build": "nuxt build",
|
||||
"prepare": "nuxt prepare",
|
||||
"dev": "nuxt dev",
|
||||
"dev:host": "nuxt dev --host",
|
||||
"typecheck": "nuxt typecheck",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"browserify": "npm run browserify-miieditor && npm run browserify-reset-password",
|
||||
"browserify-miieditor": "browserify ./public/assets/js/miieditor.js -o ./public/assets/js/miieditor.bundled.js",
|
||||
"browserify-reset-password": "browserify ./public/assets/js/reset-password.js -o ./public/assets/js/reset-password.bundled.js"
|
||||
"lint:fix": "eslint . --fix"
|
||||
},
|
||||
"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": {
|
||||
"@aws-sdk/client-ses": "^3.515.0",
|
||||
"@discordjs/rest": "^2.6.3",
|
||||
"@node-saml/node-saml": "^5.0.0",
|
||||
"@pretendonetwork/error-codes": "^1.0.3",
|
||||
"@pretendonetwork/grpc": "^2.2.3",
|
||||
"browserify": "^17.0.0",
|
||||
"colors": "^1.4.0",
|
||||
"cookie-parser": "^1.4.5",
|
||||
"discord-api-types": "^0.38.52",
|
||||
"discord-oauth2": "github:ryanblenis/discord-oauth2",
|
||||
"express": "^5.2.1",
|
||||
"express-handlebars": "^5.3.1",
|
||||
"express-locale": "^2.0.0",
|
||||
"fs-extra": "^9.1.0",
|
||||
"got": "^11.8.5",
|
||||
"graphql-request": "^4.3.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"marked": "^4.0.10",
|
||||
"mii-js": "github:PretendoNetwork/mii-js#v1.0.4",
|
||||
"mongoose": "^6.4.0",
|
||||
"morgan": "^1.10.0",
|
||||
"nice-grpc": "^2.1.16",
|
||||
"nodemailer": "^6.7.5",
|
||||
"stripe": "^9.9.0"
|
||||
"@hcaptcha/vue3-hcaptcha": "^1.3.0",
|
||||
"@nuxt/content": "^3.4.0",
|
||||
"@nuxt/eslint": "^1.3.0",
|
||||
"@nuxt/fonts": "^0.14.0",
|
||||
"@nuxt/icon": "^2.5.0",
|
||||
"@nuxtjs/i18n": "^10.6.0",
|
||||
"@pinia/nuxt": "^1.0.1",
|
||||
"@pretendonetwork/error-codes": "^1.2.2",
|
||||
"@pretendonetwork/grpc": "^2.5.4",
|
||||
"@pretendonetwork/mii-js": "^1.0.11",
|
||||
"@vueuse/core": "^14.4.0",
|
||||
"better-sqlite3": "^12.11.1",
|
||||
"discord-api-types": "^0.38.53",
|
||||
"eslint": "^9.39.5",
|
||||
"feed": "^6.0.0",
|
||||
"hcaptcha": "^0.2.0",
|
||||
"ioredis": "^5.11.1",
|
||||
"mlly": "^1.8.2",
|
||||
"mongodb": "^7.5.0",
|
||||
"nice-grpc": "^2.1.17",
|
||||
"nodemailer": "^9.0.5",
|
||||
"nuxt": "^4.5.2",
|
||||
"octokit": "^5.0.5",
|
||||
"papr": "^17.1.1",
|
||||
"rate-limiter-flexible": "^11.2.0",
|
||||
"reka-ui": "^2.10.3",
|
||||
"stripe": "^22.4.0",
|
||||
"text-mask-core": "^5.1.2",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^5.2.0",
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@pretendonetwork/eslint-config": "^0.0.6",
|
||||
"eslint": "^9.18.0",
|
||||
"globals": "^15.14.0"
|
||||
"@iconify-json/fa7-brands": "^1.2.4",
|
||||
"@iconify-json/ph": "^1.2.2",
|
||||
"@pretendonetwork/eslint-config": "^0.1.4",
|
||||
"@types/nodemailer": "^8.0.1",
|
||||
"eslint-plugin-vue": "^10.0.0",
|
||||
"sass-embedded": "^1.86.3",
|
||||
"vue-tsc": "^3.3.10"
|
||||
},
|
||||
"allowScripts": {
|
||||
"better-sqlite3@12.11.1": true,
|
||||
"esbuild@0.28.1": true,
|
||||
"esbuild@0.25.12": true,
|
||||
"protobufjs@7.6.5": true,
|
||||
"unrs-resolver@1.12.2": true,
|
||||
"@parcel/watcher@2.6.0": true,
|
||||
"vue-demi@0.14.10": true,
|
||||
"esbuild@0.27.2": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<browserconfig>
|
||||
<msapplication>
|
||||
<tile>
|
||||
<square150x150logo src="https://pretendo.network/assets/images/icons/mstile-150x150.png"/>
|
||||
<TileColor>#1b1f3b</TileColor>
|
||||
</tile>
|
||||
</msapplication>
|
||||
</browserconfig>
|
||||
@@ -1,24 +0,0 @@
|
||||
.status {
|
||||
text-align: center;
|
||||
font-size: 8rem;
|
||||
padding-top: 60px;
|
||||
color: var(--text-shade-1);
|
||||
}
|
||||
.description {
|
||||
text-align: center;
|
||||
font-size: 1.7rem;
|
||||
margin-top: -10px;
|
||||
color: var(--text-shade-1);
|
||||
}
|
||||
.shocked-bandwidth {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-top: 70px;
|
||||
margin-bottom: -120px;
|
||||
}
|
||||
@media screen and (max-width: 900px) {
|
||||
.shocked-bandwidth {
|
||||
margin-bottom: -100px;
|
||||
}
|
||||
}
|
||||
@@ -1,350 +0,0 @@
|
||||
/* Removing until it's done */
|
||||
.sign-in-history a {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.account-wrapper {
|
||||
display: grid;
|
||||
column-gap: 48px;
|
||||
margin-top: 80px;
|
||||
color: var(--text-shade-1);
|
||||
}
|
||||
|
||||
/* Account settings sidebar */
|
||||
.account-sidebar .user {
|
||||
margin: 55px auto;
|
||||
width: fit-content;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
align-items: center;
|
||||
}
|
||||
.account-sidebar .user .miiname {
|
||||
font-size: 1.2rem;
|
||||
color: var(--text-shade-3);
|
||||
margin: 8px 0 4px;
|
||||
}
|
||||
.account-sidebar .user .username {
|
||||
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 a.mii {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
overflow: hidden;
|
||||
border-radius: 100%;
|
||||
background: var(--bg-shade-3);
|
||||
}
|
||||
.account-sidebar .user a.mii::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: no-repeat center/40% url("/assets/images/edit.svg"), rgba(55, 60, 101, 0.7);
|
||||
opacity: 0;
|
||||
transition: opacity 150ms;
|
||||
}
|
||||
.account-sidebar .user a.mii:hover::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.account-sidebar .user .mii {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.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 .buttons a svg {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.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);
|
||||
}
|
||||
|
||||
.account-sidebar .buttons #account-delete {
|
||||
background-color: #F44336;
|
||||
}
|
||||
|
||||
/* Settings */
|
||||
.settings-wrapper {
|
||||
display: grid;
|
||||
grid-column-start: 2;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
column-gap: 20px;
|
||||
}
|
||||
.settings-wrapper a {
|
||||
color: var(--accent-shade-1);
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
}
|
||||
.settings-wrapper a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.settings-wrapper h2.section-header {
|
||||
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: var(--bg-shade-2);
|
||||
padding: 48px 60px;
|
||||
}
|
||||
.setting-card * {
|
||||
margin: 0;
|
||||
}
|
||||
.setting-card .edit {
|
||||
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: var(--bg-shade-3);
|
||||
color: var(--text-shade-3);
|
||||
}
|
||||
.setting-card .edit svg {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.setting-card .header {
|
||||
color: var(--text-shade-3);
|
||||
}
|
||||
|
||||
.setting-card .setting-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, auto);
|
||||
gap: 24px;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
.setting-card .setting-list p.label {
|
||||
color: var(--text-shade-3);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
position: relative;
|
||||
height: min-content;
|
||||
padding: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.setting-card .server-selection {
|
||||
display: flex;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
background: var(--bg-shade-3);
|
||||
}
|
||||
.setting-card .server-selection input {
|
||||
display: none;
|
||||
}
|
||||
.server-selection input + label {
|
||||
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-shade-1);
|
||||
}
|
||||
.server-selection input:checked + label,
|
||||
.server-selection input:checked + label h2 {
|
||||
background: var(--accent-shade-0);
|
||||
color: var(--text-shade-3);
|
||||
}
|
||||
|
||||
.setting-card #link-discord-account {
|
||||
width: 100%;
|
||||
padding: 12px 48px;
|
||||
cursor: pointer;
|
||||
background: var(--bg-shade-3);
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
@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.success div {
|
||||
background: var(--green-shade-0);
|
||||
}
|
||||
.banner-notice.error div {
|
||||
background: var(--red-shade-1);
|
||||
}
|
||||
|
||||
footer {
|
||||
margin-top: 80px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1300px) {
|
||||
.account-wrapper {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.settings-wrapper {
|
||||
grid-column-start: 1;
|
||||
}
|
||||
|
||||
.account-sidebar {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.account-sidebar .user .mii {
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1000px) {
|
||||
.settings-wrapper {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.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 .server-selection {
|
||||
flex-flow: column;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 350px) {
|
||||
.setting-card .setting-list {
|
||||
grid-template-columns: auto;
|
||||
}
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
.new-font {
|
||||
font-family: museo-sans, sans-serif;
|
||||
}
|
||||
|
||||
.pretendo {
|
||||
font-family: Poppins, Arial, Helvetica, sans-serif;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.announcement-hero {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
padding: 96px 0;
|
||||
margin: 36px 0 24px;
|
||||
}
|
||||
.announcement-hero p {
|
||||
font-size: 24px;
|
||||
margin: 0;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.announcement-hero h1 {
|
||||
font-size: 450%;
|
||||
margin: 0;
|
||||
}
|
||||
.announcement-hero::before {
|
||||
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;
|
||||
}
|
||||
.bro-what.subscribe h1 {
|
||||
margin: 0;
|
||||
margin-right: 12px;
|
||||
width: fit-content;
|
||||
}
|
||||
.buy-now {
|
||||
margin-left:auto;
|
||||
}
|
||||
.buy-now button {
|
||||
cursor: pointer;
|
||||
width: max-content;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.bro-what {
|
||||
padding: 48px;
|
||||
}
|
||||
.bro-what a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
|
||||
.dotted-bg {
|
||||
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;
|
||||
}
|
||||
|
||||
.footnotes {
|
||||
color: var(--text-shade-1);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 946px) {
|
||||
header nav a:not(.keep-on-mobile) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.announcement-hero h1 {
|
||||
font-size: 350%;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 724px) {
|
||||
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;
|
||||
}
|
||||
|
||||
.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 a,
|
||||
.buy-now button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.bro-what a {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.announcement-hero {
|
||||
padding: 72px 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 480px) {
|
||||
.bro-what {
|
||||
padding: 36px 0;
|
||||
}
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
.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;
|
||||
}
|
||||
|
||||
.blog-card .post-info {
|
||||
flex: 50%;
|
||||
padding: 40px;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
color: var(--text-shade-1);
|
||||
}
|
||||
|
||||
.blog-card .post-info .title {
|
||||
color: var(--text-shade-3);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.blog-card .post-info .caption {
|
||||
margin: 4px 0 32px 0;
|
||||
}
|
||||
|
||||
.blog-card .pub-info {
|
||||
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-shade-3);
|
||||
}
|
||||
|
||||
.blog-card .pub-info > * {
|
||||
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-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%;
|
||||
}
|
||||
|
||||
.blog-card .cover {
|
||||
flex: 50%;
|
||||
border: 3px solid var(--bg-shade-0);
|
||||
border-radius: 0 10px 10px 0;
|
||||
}
|
||||
|
||||
.progress-hero a,
|
||||
.progress-hero a * {
|
||||
color: var(--accent-shade-1);
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.progress-hero a:hover,
|
||||
.progress-hero a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
margin: 10vh auto;
|
||||
width: min-content;
|
||||
}
|
||||
.buttons button.secondary.icon-btn {
|
||||
cursor: pointer;
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
footer {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 450px) {
|
||||
.blog-card .cover {
|
||||
min-height: 200px;
|
||||
}
|
||||
}
|
||||
@@ -1,236 +0,0 @@
|
||||
.wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 95%;
|
||||
min-height: 100vh;
|
||||
}
|
||||
header {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card-wrap {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.blog-card {
|
||||
padding: 60px;
|
||||
max-width: 1100px;
|
||||
margin: 50px auto;
|
||||
color: var(--text-shade-1);
|
||||
}
|
||||
|
||||
.blog-card h1,
|
||||
.blog-card h2,
|
||||
.blog-card h3,
|
||||
.blog-card h4,
|
||||
.blog-card h5,
|
||||
.blog-card h6 {
|
||||
margin: 40px 0 10px;
|
||||
color: var(--text-shade-3);
|
||||
}
|
||||
|
||||
.blog-card strong {
|
||||
color: var(--text-shade-3);
|
||||
}
|
||||
|
||||
.blog-card a,
|
||||
.blog-card a * {
|
||||
color: var(--accent-shade-1);
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.blog-card a:hover,
|
||||
.blog-card a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.blog-card del {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.blog-card .title {
|
||||
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;
|
||||
}
|
||||
.blog-card .pub-info .date {
|
||||
font-weight: bold;
|
||||
color: var(--text-shade-3);
|
||||
}
|
||||
.blog-card .pub-info > * {
|
||||
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-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%;
|
||||
}
|
||||
|
||||
.blog-card p,
|
||||
.post-info {
|
||||
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);
|
||||
}
|
||||
.blog-card img.emoji {
|
||||
display: inline;
|
||||
margin: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.blog-card video {
|
||||
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);
|
||||
}
|
||||
/* 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;
|
||||
}
|
||||
}
|
||||
/* Some twitter iframe specific stuff */
|
||||
.blog-card .twitter-tweet {
|
||||
margin: auto;
|
||||
}
|
||||
.blog-card .twitter-tweet iframe {
|
||||
border: none; /* Fixes the double border */
|
||||
}
|
||||
|
||||
.blog-card table {
|
||||
border-radius: 4px;
|
||||
border-collapse: collapse;
|
||||
background: var(--bg-shade-3);
|
||||
margin-bottom: 30px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.blog-card table th {
|
||||
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;
|
||||
}
|
||||
.blog-card table tr:nth-child(even) {
|
||||
background: var(--bg-shade-2);
|
||||
}
|
||||
|
||||
.blog-card pre code {
|
||||
border-radius: 4px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.blog-card input[type="checkbox"] {
|
||||
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(--bg-shade-3);
|
||||
}
|
||||
|
||||
.blog-card hr {
|
||||
border: 1px solid var(--text-shade-1);
|
||||
margin: 30px 0;
|
||||
}
|
||||
|
||||
.blog-card blockquote {
|
||||
border-left: 2px solid var(--text-shade-1);
|
||||
padding: 8px 24px;
|
||||
margin: 0;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 901px) {
|
||||
.blog-card h1,
|
||||
.blog-card h2,
|
||||
.blog-card h3,
|
||||
.blog-card h4,
|
||||
.blog-card h5,
|
||||
.blog-card h6 {
|
||||
scroll-margin-top: 110px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
.blog-card {
|
||||
padding: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
.wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
header {
|
||||
width: 90%;
|
||||
margin: 35px auto;
|
||||
}
|
||||
|
||||
.blog-card {
|
||||
padding: 40px 5vw;
|
||||
border-radius: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
footer {
|
||||
width: 95%;
|
||||
margin: auto auto 40px;
|
||||
}
|
||||
}
|
||||
@@ -1,190 +0,0 @@
|
||||
/* 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.inactive {
|
||||
pointer-events: none;
|
||||
}
|
||||
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;
|
||||
}
|
||||
input[type="range"] {
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
input[type="range"]::-webkit-slider-runnable-track {
|
||||
background: var(--bg-shade-3);
|
||||
height: 1rem;
|
||||
border-radius: 1rem;
|
||||
}
|
||||
input[type="range"]::-moz-range-track {
|
||||
background: var(--bg-shade-3);
|
||||
border-radius: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
input[type="range"]::-webkit-slider-thumb {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
width: 1.5rem;
|
||||
height: 3rem;
|
||||
margin-top: -1rem;
|
||||
background-color: var(--accent-shade-1);
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
input[type="range"]::-moz-range-thumb {
|
||||
width: 1.5rem;
|
||||
height: 3rem;
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
background-color: var(--accent-shade-1);
|
||||
}
|
||||
input[type="range"]:focus {
|
||||
outline: none;
|
||||
}
|
||||
input[type="range"]:focus::-webkit-slider-thumb {
|
||||
background-color: var(--accent-shade-3);
|
||||
}
|
||||
input[type="range"]:focus::-moz-range-thumb {
|
||||
background-color: var(--accent-shade-3);
|
||||
}
|
||||
@@ -1,428 +0,0 @@
|
||||
html,
|
||||
body,
|
||||
div.main-body {
|
||||
height: 100%;
|
||||
background: var(--bg-shade-0);
|
||||
}
|
||||
|
||||
a.logo-link {
|
||||
margin: auto;
|
||||
margin-left: 36px;
|
||||
height: 40px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
button#openSidebar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.docs-wrapper .content:not(.search) a {
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
color: var(--accent-shade-1);
|
||||
}
|
||||
|
||||
.docs-wrapper header {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
margin: 20px;
|
||||
margin-left: 0;
|
||||
}
|
||||
.docs-wrapper header::before {
|
||||
content: none;
|
||||
background: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
.docs-wrapper header a.logo-link {
|
||||
display: none;
|
||||
}
|
||||
.docs-wrapper header nav a:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.docs-wrapper {
|
||||
display: grid;
|
||||
grid-template-columns: fit-content(100%) auto;
|
||||
grid-template-rows: fit-content(100%) auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.docs-wrapper .sidebar {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
align-items: center;
|
||||
width: clamp(270px, 25vw, 500px);
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
min-height: 100%;
|
||||
}
|
||||
.docs-wrapper .sidebar .section {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
width: 200px;
|
||||
margin-left: clamp(60px, 10vw, 138px);
|
||||
margin-bottom: 72px;
|
||||
}
|
||||
.docs-wrapper .sidebar .section:first-child {
|
||||
margin-top: 72px;
|
||||
}
|
||||
.docs-wrapper .sidebar .section h5 {
|
||||
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-shade-1);
|
||||
width: fit-content;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.docs-wrapper .sidebar .section a.active,
|
||||
.docs-wrapper .sidebar .section a:hover {
|
||||
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/docs/arrow-right.svg);
|
||||
}
|
||||
|
||||
.docs-wrapper .content {
|
||||
background: var(--bg-shade-1);
|
||||
padding: 72px;
|
||||
max-height: 100%;
|
||||
overflow-y: scroll;
|
||||
border-top-left-radius: 8px;
|
||||
}
|
||||
.docs-wrapper .content-inner {
|
||||
max-width: 900px;
|
||||
}
|
||||
.docs-wrapper .content p {
|
||||
color: var(--text-shade-1);
|
||||
}
|
||||
.docs-wrapper .content h1:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
.docs-wrapper .content .quick-links-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-gap: 24px;
|
||||
margin-bottom: 60px;
|
||||
}
|
||||
.docs-wrapper .quick-links-grid a {
|
||||
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(--accent-shade-2);
|
||||
}
|
||||
.docs-wrapper .quick-links-grid p.header {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
color: var(--text-shade-3);
|
||||
margin: 0;
|
||||
}
|
||||
.docs-wrapper .quick-links-grid p {
|
||||
margin: 0;
|
||||
}
|
||||
.docs-wrapper .quick-links-grid svg:last-child {
|
||||
height: 36px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.docs-wrapper .content-inner div.tip {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding: 36px;
|
||||
background: var(--bg-shade-2);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
border: var(--accent-shade-2);
|
||||
margin: 24px 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.docs-wrapper .content-inner div.tip::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 12px;
|
||||
background: var(--accent-shade-2);
|
||||
opacity: 1;
|
||||
}
|
||||
.docs-wrapper .content-inner div.tip.yellow::after {
|
||||
background: var(--yellow-shade-1);
|
||||
}
|
||||
.docs-wrapper .content-inner div.tip.red::after {
|
||||
background: var(--red-shade-1);
|
||||
}
|
||||
.docs-wrapper .content-inner div.tip.green::after {
|
||||
background: var(--green-shade-1);
|
||||
}
|
||||
|
||||
.docs-wrapper .content .missing-in-locale-notice {
|
||||
background: var(--bg-shade-2);
|
||||
padding: 24px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.search .purple-card {
|
||||
padding: 36px;
|
||||
}
|
||||
.search .purple-card h1 {
|
||||
margin-top: 0;
|
||||
}
|
||||
.search .purple-card p {
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
.search .purple-card input::placeholder {
|
||||
color: var(--text-shade-0);
|
||||
}
|
||||
.search .purple-card input:focus {
|
||||
background-color: var(--bg-shade-4);
|
||||
color: #fff;
|
||||
transition: 200ms;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.search .input-wrapper {
|
||||
position: relative;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.search .input-wrapper .matches {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
font-size: 1rem;
|
||||
background-color: var(--bg-shade-2);
|
||||
border: none;
|
||||
border-radius: 0 0 4px 4px;
|
||||
max-height: 204px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.search .input-wrapper .matches * {
|
||||
padding: 12px;
|
||||
margin: 0;
|
||||
text-decoration: none;
|
||||
color: var(--text-shade-3);
|
||||
}
|
||||
.search .input-wrapper .matches a:hover {
|
||||
background-color: var(--bg-shade-1);
|
||||
}
|
||||
.search input.has-matches {
|
||||
border-radius: 4px 4px 0 0;
|
||||
}
|
||||
|
||||
.platform-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 24px;
|
||||
margin-top: 36px;
|
||||
}
|
||||
|
||||
.docs-wrapper .platform-grid a {
|
||||
text-decoration: none;
|
||||
background: var(--bg-shade-3);
|
||||
border-radius: 12px;
|
||||
color: var(--text-shade-4) !important;
|
||||
display: grid;
|
||||
grid-template-rows: auto fit-content(100%);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding: 36px;
|
||||
padding-bottom: 24px;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.platform-grid a img {
|
||||
width: 180px;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
.platform-grid a span {
|
||||
margin-top: auto;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1296px) {
|
||||
.docs-wrapper .content {
|
||||
padding: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1080px) {
|
||||
.docs-wrapper .header-wrapper {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
button#openSidebar {
|
||||
display: block;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
margin-left: 20px;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.docs-wrapper header {
|
||||
margin-left: 20px;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.docs-wrapper {
|
||||
margin-top: 80px;
|
||||
height: calc(100% - 80px);
|
||||
}
|
||||
|
||||
a.logo-link {
|
||||
display: none;
|
||||
}
|
||||
.docs-wrapper header a.logo-link {
|
||||
display: block;
|
||||
height: 40px;
|
||||
margin: 0;
|
||||
margin-right: 34px;
|
||||
}
|
||||
|
||||
.docs-wrapper .sidebar {
|
||||
grid-column: 1 / span 1;
|
||||
grid-row: 2 / span 1;
|
||||
width: 0;
|
||||
transition: width 250ms;
|
||||
}
|
||||
|
||||
.docs-wrapper .sidebar.open {
|
||||
width: min(300px, 100vw);
|
||||
}
|
||||
|
||||
.docs-wrapper .content {
|
||||
width: 100vw;
|
||||
box-sizing: border-box;
|
||||
border-top-left-radius: 0;
|
||||
grid-column: 2 / span 1;
|
||||
grid-row: 2 / span 1;
|
||||
}
|
||||
.docs-wrapper .content.open-sidebar {
|
||||
border-top-left-radius: 8px;
|
||||
}
|
||||
.docs-wrapper .content-inner {
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 900px) {
|
||||
.docs-wrapper header button.dropdown-button#mobile-button {
|
||||
display: none;
|
||||
}
|
||||
.docs-wrapper header .logo-link svg text {
|
||||
display: block;
|
||||
}
|
||||
.docs-wrapper header .logo-link svg {
|
||||
width: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 820px) {
|
||||
.docs-wrapper .content .quick-links-grid,
|
||||
.platform-grid {
|
||||
grid-template-columns: 1fr;
|
||||
grid-auto-rows: 1fr;
|
||||
}
|
||||
|
||||
.docs-wrapper header a.logo-link {
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 576px) {
|
||||
.docs-wrapper header div.dropdown {
|
||||
left: calc(-39.876px - 6px - 30px - 40px);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 492px) {
|
||||
.docs-wrapper .content {
|
||||
padding: 36px;
|
||||
}
|
||||
|
||||
header .logo-link svg text {
|
||||
display: none;
|
||||
}
|
||||
header .logo-link svg {
|
||||
width: 39.876px;
|
||||
}
|
||||
.docs-wrapper header a.logo-link {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 360px) {
|
||||
.docs-wrapper .content {
|
||||
padding: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Scrollbar styling 'cause it's fancy */
|
||||
.docs-wrapper .sidebar::-webkit-scrollbar,
|
||||
.docs-wrapper .content::-webkit-scrollbar,
|
||||
.docs-wrapper .content pre code::-webkit-scrollbar,
|
||||
.search .input-wrapper .matches::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
.docs-wrapper .sidebar::-webkit-scrollbar-track,
|
||||
.docs-wrapper .content::-webkit-scrollbar-track,
|
||||
.docs-wrapper .content pre code::-webkit-scrollbar-track,
|
||||
.search .input-wrapper .matches::-webkit-scrollbar-track {
|
||||
background: none;
|
||||
}
|
||||
.docs-wrapper .sidebar::-webkit-scrollbar-thumb,
|
||||
.docs-wrapper .content::-webkit-scrollbar-thumb,
|
||||
.docs-wrapper .content pre code::-webkit-scrollbar-thumb,
|
||||
.search .input-wrapper .matches::-webkit-scrollbar-thumb {
|
||||
background-color: var(--text-shade-0);
|
||||
border-radius: 24px;
|
||||
border: 3px solid var(--bg-shade-0);
|
||||
}
|
||||
.docs-wrapper .content::-webkit-scrollbar-thumb {
|
||||
border: 3px solid var(--bg-shade-1);
|
||||
}
|
||||
.docs-wrapper .content pre code::-webkit-scrollbar-thumb,
|
||||
.search .input-wrapper .matches::-webkit-scrollbar-thumb {
|
||||
border: 3px solid var(--bg-shade-2);
|
||||
}
|
||||
|
||||
.docs-wrapper .sidebar,
|
||||
.search .input-wrapper .matches {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--text-shade-0) var(--bg-shade-1);
|
||||
}
|
||||
.docs-wrapper .content {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--text-shade-0) var(--bg-shade-1);
|
||||
}
|
||||
.docs-wrapper .content pre code {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--text-shade-0) var(--bg-shade-0);
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
.select-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.select-box > * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.select-box .options-container {
|
||||
max-height: 0;
|
||||
width: min(90vw, 240px);
|
||||
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: var(--text-shade-2);
|
||||
}
|
||||
|
||||
.select-box .lang {
|
||||
width: 1.3rem;
|
||||
height: 1rem;
|
||||
margin-right: .2rem;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.select-box .options-container.active {
|
||||
max-height: 320px;
|
||||
opacity: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.select-box .options-container.active + .locale-dropdown-toggle::after {
|
||||
transform: translateY(-50%) rotateX(180deg);
|
||||
}
|
||||
|
||||
.select-box .options-container::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
background: var(--bg-shade-3);
|
||||
border-radius: 0 5px 5px 0;
|
||||
}
|
||||
|
||||
.select-box .options-container::-webkit-scrollbar-thumb {
|
||||
background: var(--text-shade-1);
|
||||
border-radius: 0 5px 5px 0;
|
||||
}
|
||||
.select-box .option {
|
||||
padding: 12px 15px;
|
||||
cursor: pointer;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.select-box .option:hover {
|
||||
background: var(--bg-shade-4);
|
||||
}
|
||||
.select-box .option:hover .item {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.select-box label {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.select-box .option .radio {
|
||||
display: none;
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
pre code.hljs {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
padding: 36px;
|
||||
border-radius: 10px;
|
||||
font-family: Poppins, Arial, Helvetica, sans-serif;
|
||||
}
|
||||
code.hljs {
|
||||
padding: 3px 5px;
|
||||
}
|
||||
.hljs {
|
||||
background: var(--bg-shade-0);
|
||||
color: #d6deeb;
|
||||
}
|
||||
.hljs-keyword {
|
||||
color: var(--accent-shade-2);
|
||||
font-style: italic;
|
||||
}
|
||||
.hljs-built_in {
|
||||
color: #addb67;
|
||||
font-style: italic;
|
||||
}
|
||||
.hljs-type {
|
||||
color: #82aaff;
|
||||
}
|
||||
.hljs-literal {
|
||||
color: #ff5874;
|
||||
}
|
||||
.hljs-number {
|
||||
color: #f78c6c;
|
||||
}
|
||||
.hljs-regexp {
|
||||
color: #5ca7e4;
|
||||
}
|
||||
.hljs-string {
|
||||
color: #ecc48d;
|
||||
}
|
||||
.hljs-subst {
|
||||
color: #d3423e;
|
||||
}
|
||||
.hljs-symbol {
|
||||
color: #82aaff;
|
||||
}
|
||||
.hljs-class {
|
||||
color: #ffcb8b;
|
||||
}
|
||||
.hljs-function {
|
||||
color: #82aaff;
|
||||
}
|
||||
.hljs-title {
|
||||
color: #dcdcaa;
|
||||
font-style: italic;
|
||||
}
|
||||
.hljs-params {
|
||||
color: #7fdbca;
|
||||
}
|
||||
.hljs-comment {
|
||||
color: #637777;
|
||||
font-style: italic;
|
||||
}
|
||||
.hljs-doctag {
|
||||
color: #7fdbca;
|
||||
}
|
||||
.hljs-meta,
|
||||
.hljs-meta .hljs-keyword {
|
||||
color: #82aaff;
|
||||
}
|
||||
.hljs-meta .hljs-string {
|
||||
color: #ecc48d;
|
||||
}
|
||||
.hljs-section {
|
||||
color: #82b1ff;
|
||||
}
|
||||
.hljs-attr,
|
||||
.hljs-name,
|
||||
.hljs-tag {
|
||||
color: #7fdbca;
|
||||
}
|
||||
.hljs-attribute {
|
||||
color: #80cbc4;
|
||||
}
|
||||
.hljs-variable {
|
||||
color: #addb67;
|
||||
}
|
||||
.hljs-bullet {
|
||||
color: #d9f5dd;
|
||||
}
|
||||
.hljs-code {
|
||||
color: #80cbc4;
|
||||
}
|
||||
.hljs-emphasis {
|
||||
color: #c792ea;
|
||||
font-style: italic;
|
||||
}
|
||||
.hljs-strong {
|
||||
color: #addb67;
|
||||
font-weight: 700;
|
||||
}
|
||||
.hljs-formula {
|
||||
color: #c792ea;
|
||||
}
|
||||
.hljs-link {
|
||||
color: #ff869a;
|
||||
}
|
||||
.hljs-quote {
|
||||
color: #697098;
|
||||
font-style: italic;
|
||||
}
|
||||
.hljs-selector-tag {
|
||||
color: #ff6363;
|
||||
}
|
||||
.hljs-selector-id {
|
||||
color: #fad430;
|
||||
}
|
||||
.hljs-selector-class {
|
||||
color: #addb67;
|
||||
font-style: italic;
|
||||
}
|
||||
.hljs-selector-attr,
|
||||
.hljs-selector-pseudo {
|
||||
color: #c792ea;
|
||||
font-style: italic;
|
||||
}
|
||||
.hljs-template-tag {
|
||||
color: #c792ea;
|
||||
}
|
||||
.hljs-template-variable {
|
||||
color: #addb67;
|
||||
}
|
||||
.hljs-addition {
|
||||
color: #addb67ff;
|
||||
font-style: italic;
|
||||
}
|
||||
.hljs-deletion {
|
||||
color: #ef535090;
|
||||
font-style: italic;
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
.localization-wrapper {
|
||||
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%;
|
||||
}
|
||||
|
||||
.caption {
|
||||
color: var(--text-shade-1);
|
||||
max-width: 400px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.title.dot {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.localization-instr,
|
||||
.localization-instr:visited {
|
||||
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;
|
||||
}
|
||||
|
||||
.localization-form {
|
||||
padding: 36px;
|
||||
background-color: var(--bg-shade-0);
|
||||
border-radius: 12px;
|
||||
margin-top: 36px;
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
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(--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-shade-0);
|
||||
}
|
||||
|
||||
.localization-form input:focus {
|
||||
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-shade-3);
|
||||
padding: 12px 36px;
|
||||
background: var(--accent-shade-0);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
footer {
|
||||
margin-top: auto;
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
.wrapper {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
header {
|
||||
margin: 35px 0;
|
||||
}
|
||||
|
||||
.account-form-wrapper {
|
||||
margin: auto;
|
||||
width: fit-content;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
form.account {
|
||||
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-shade-3);
|
||||
}
|
||||
|
||||
form.account p {
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
form.account div {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
form.account label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
text-transform: uppercase;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
form.account button {
|
||||
width: 100%;
|
||||
background: var(--accent-shade-0);
|
||||
}
|
||||
|
||||
form.account a {
|
||||
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-shade-3);
|
||||
}
|
||||
|
||||
form.account a.pwdreset {
|
||||
margin-left: auto;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
form.account a.register {
|
||||
margin: auto;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
@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: 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;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,709 +0,0 @@
|
||||
html,
|
||||
body,
|
||||
div.main-body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body,
|
||||
div.main-body,
|
||||
.miieditor-wrapper {
|
||||
z-index: -1;
|
||||
user-select: none;
|
||||
background: var(--bg-shade-0);
|
||||
}
|
||||
|
||||
svg.logotype {
|
||||
position: absolute;
|
||||
width: 120px;
|
||||
top: 42px;
|
||||
left: 60px;
|
||||
}
|
||||
|
||||
.miieditor-wrapper {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
|
||||
width: 95vw;
|
||||
max-width: 1920px;
|
||||
height: 100%;
|
||||
margin: auto;
|
||||
gap: 0 120px;
|
||||
}
|
||||
.params-wrapper::before {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
background: var(--bg-shade-1);
|
||||
border-radius: 100% 0 0 100%;
|
||||
width: 1300px;
|
||||
height: 1700px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
left: -200px;
|
||||
z-index: -1;
|
||||
}
|
||||
.miieditor-wrapper::after {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
background: radial-gradient(
|
||||
closest-side,
|
||||
var(--bg-shade-1) 0%,
|
||||
rgba(27, 31, 59, 0) 100%
|
||||
);
|
||||
width: 200vh;
|
||||
height: 200vh;
|
||||
top: -100vh;
|
||||
left: -100vh;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.canvas-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
canvas#miiCanvas {
|
||||
width: auto;
|
||||
height: auto;
|
||||
transform-origin: center;
|
||||
transition: transform 200ms, filter 200ms;
|
||||
}
|
||||
|
||||
div.mii-img-wrapper::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: -22px;
|
||||
height: 72px;
|
||||
width: 100%;
|
||||
background: radial-gradient(
|
||||
farthest-side,
|
||||
var(--bg-shade-2) 0%,
|
||||
rgba(35, 39, 74, 0) 100%
|
||||
);
|
||||
}
|
||||
|
||||
div.params-wrapper {
|
||||
position: relative;
|
||||
margin: auto;
|
||||
display: grid;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
div.tabs {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(11, 1fr);
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
background: #0a0c19;
|
||||
padding: 6px;
|
||||
gap: 6px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
div.tabs .tabbtn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
aspect-ratio: 1;
|
||||
border-radius: 6px;
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
div.tabs .tabbtn::after {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: url("/assets/images/miieditor.svg");
|
||||
background-position: calc(var(--assetcol) * -12px) -312px;
|
||||
transform: scale(2.9);
|
||||
}
|
||||
div.tabs .tabbtn:hover,
|
||||
div.tabs .tabbtn.active {
|
||||
background: var(--bg-shade-2);
|
||||
}
|
||||
|
||||
div.subtabs {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
}
|
||||
div.subtabs .subtabbtn {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 60px;
|
||||
border-radius: 6px;
|
||||
background: none;
|
||||
padding: 0;
|
||||
color: var(--text-shade-3);
|
||||
aspect-ratio: 1;
|
||||
}
|
||||
div.subtabs .subtabbtn::after {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: url("/assets/images/miieditor.svg");
|
||||
background-position: calc(var(--assetcol) * -12px) -324px;
|
||||
transform: scale(2.9);
|
||||
}
|
||||
div.subtabs .subtabbtn.active::before,
|
||||
div.subtabs .subtabbtn.active:hover::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
left: 5%;
|
||||
width: 90%;
|
||||
height: 5px;
|
||||
background: var(--accent-shade-1);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.has-sliders {
|
||||
grid-template-columns: 60px auto;
|
||||
gap: 12px;
|
||||
}
|
||||
.has-sliders label {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.has-sliders label::after {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: url("/assets/images/miieditor.svg");
|
||||
background-position: calc(var(--assetcol) * -16px) -336px;
|
||||
transform: scale(3);
|
||||
}
|
||||
|
||||
.has-textinput {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: auto;
|
||||
grid-auto-rows: auto;
|
||||
gap: 24px;
|
||||
}
|
||||
.has-textinput label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
text-transform: uppercase;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.has-textinput .icons {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.has-textinput .icons input[type="checkbox"] {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
height: 49px;
|
||||
width: 49px;
|
||||
}
|
||||
|
||||
input[type="checkbox"]#allowCopying:checked {
|
||||
background: no-repeat center/80% url(../images/copy.svg), var(--accent-shade-0);
|
||||
}
|
||||
input[type="checkbox"]#disableSharing:checked {
|
||||
background: no-repeat center/80% url(../images/share.svg), var(--accent-shade-0);
|
||||
}
|
||||
input[type="checkbox"]#favorite:checked {
|
||||
background: no-repeat center/80% url(../images/star.svg), var(--accent-shade-0);
|
||||
}
|
||||
|
||||
form.params {
|
||||
grid-template-columns: repeat(2, auto);
|
||||
height: 618px;
|
||||
width: 582px;
|
||||
}
|
||||
form.params .tab {
|
||||
display: none;
|
||||
gap: 4rem 0;
|
||||
}
|
||||
form.params .tab.active {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
fieldset,
|
||||
fieldset.has-subpages .subpage {
|
||||
appearance: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: none;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
grid-template-rows: repeat(4, 1fr);
|
||||
gap: 18px;
|
||||
width: 100%;
|
||||
height: fit-content;
|
||||
}
|
||||
fieldset.active {
|
||||
display: grid;
|
||||
}
|
||||
fieldset input[type="radio"] {
|
||||
display: none;
|
||||
}
|
||||
fieldset input[type="radio"] + label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
border-radius: 18px;
|
||||
background: var(--bg-shade-3);
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
}
|
||||
fieldset.has-subpages.active {
|
||||
display: block;
|
||||
}
|
||||
fieldset.has-subpages .subpage.active {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
fieldset:not(.color, #favoriteColor) input[type="radio"] + label::after {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: url("/assets/images/miieditor.svg");
|
||||
background-position: calc(
|
||||
((var(--assetcol) + (var(--subpage, 0) * 12)) * -24px)
|
||||
)
|
||||
calc(var(--assetrow) * -24px);
|
||||
transform: scale(4.5);
|
||||
}
|
||||
|
||||
fieldset input[type="radio"]:checked + label {
|
||||
background: var(--bg-shade-4);
|
||||
box-shadow: inset 0 0 0 4px var(--accent-shade-1);
|
||||
}
|
||||
fieldset.color input[type="radio"]:checked + label,
|
||||
fieldset#favoriteColor input[type="radio"]:checked + label {
|
||||
box-shadow: inset 0 0 0 4px var(--accent-shade-1),
|
||||
inset 0 0 0 6px var(--bg-shade-1);
|
||||
}
|
||||
|
||||
input[type="range"].invert {
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
width: max-content;
|
||||
height: fit-content;
|
||||
grid-column: 1 / span 4;
|
||||
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-shade-3);
|
||||
width: 18px;
|
||||
margin-right: 0.5ch;
|
||||
text-align: right;
|
||||
}
|
||||
.page-btn {
|
||||
appearance: none;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
margin-left: 8px;
|
||||
}
|
||||
.page-btn:hover {
|
||||
background: none;
|
||||
}
|
||||
.page-btn:hover svg path {
|
||||
fill: var(--accent-shade-3);
|
||||
}
|
||||
.page-btn svg {
|
||||
height: 36px;
|
||||
margin: 6px;
|
||||
}
|
||||
.page-btn.disabled {
|
||||
pointer-events: none;
|
||||
}
|
||||
.page-btn.disabled svg path {
|
||||
fill: var(--bg-shade-3);
|
||||
}
|
||||
|
||||
.tab#saveTab {
|
||||
gap: 2rem 0;
|
||||
}
|
||||
.tab#saveTab p.save-prompt {
|
||||
margin-bottom: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mii-comparison-animation-wrapper {
|
||||
position: relative;
|
||||
height: fit-content;
|
||||
}
|
||||
.mii-comparison {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, auto);
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
.mii-comparison.confirmed {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.mii-comparison img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
background: var(--bg-shade-3);
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
.mii-comparison .new-mii-wrapper {
|
||||
position: relative;
|
||||
transition: right 500ms, transform 500ms;
|
||||
right: 0;
|
||||
}
|
||||
.mii-comparison .new-mii-wrapper::after {
|
||||
position: absolute;
|
||||
content: "";
|
||||
display: block;
|
||||
box-shadow: inset 0 0 0 8px var(--accent-shade-1);
|
||||
border-radius: 24px;
|
||||
margin: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.mii-comparison svg {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
}
|
||||
.mii-comparison svg path {
|
||||
fill: var(--accent-shade-1);
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
animation: fadeIn 0.25s forwards;
|
||||
}
|
||||
.fade-out {
|
||||
animation: fadeOut 0.5s forwards;
|
||||
}
|
||||
.mii-comparison div.new-mii-wrapper.centered-mii-img {
|
||||
position: absolute;
|
||||
right: 50%;
|
||||
transform: translateX(50%);
|
||||
height: 100%;
|
||||
aspect-ratio: 1;
|
||||
width: auto;
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@keyframes fadeOut {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
button * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1400px) {
|
||||
form.params {
|
||||
height: 562px;
|
||||
width: 512px;
|
||||
}
|
||||
|
||||
fieldset,
|
||||
fieldset.has-subpages .subpage {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
div.params-wrapper {
|
||||
margin-right: 24px;
|
||||
}
|
||||
|
||||
.params-wrapper::before {
|
||||
left: -150px;
|
||||
}
|
||||
|
||||
div.tabs {
|
||||
padding: 4px;
|
||||
gap: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1280px) {
|
||||
form.params {
|
||||
height: 492px;
|
||||
width: 480px;
|
||||
}
|
||||
|
||||
fieldset,
|
||||
fieldset.has-subpages .subpage {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
div.params-wrapper {
|
||||
margin-right: 24px;
|
||||
}
|
||||
|
||||
.params-wrapper::before {
|
||||
left: -150px;
|
||||
}
|
||||
|
||||
fieldset:not(.color, #favoriteColor) input[type="radio"] + label::after {
|
||||
transform: scale(4);
|
||||
}
|
||||
|
||||
div.subtabs .subtabbtn::after,
|
||||
div.tabs .tabbtn::after {
|
||||
transform: scale(2.4);
|
||||
}
|
||||
div.subtabs .subtabbtn {
|
||||
width: 48px;
|
||||
}
|
||||
|
||||
.params-wrapper::before {
|
||||
left: -100px;
|
||||
}
|
||||
|
||||
div.tabs {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
form.params .tab {
|
||||
gap: 2rem 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1120px) {
|
||||
form.params {
|
||||
height: 444px;
|
||||
width: 420px;
|
||||
}
|
||||
|
||||
fieldset,
|
||||
fieldset.has-subpages .subpage {
|
||||
gap: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1080px) {
|
||||
.canvas-wrapper {
|
||||
height: calc(100% - 12px);
|
||||
}
|
||||
|
||||
svg.logotype {
|
||||
left: 0;
|
||||
}
|
||||
svg.logotype text#Pretendo {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.miieditor-wrapper {
|
||||
grid-template-columns: auto;
|
||||
grid-template-rows: auto fit-content(100%);
|
||||
margin: auto;
|
||||
max-width: 360px;
|
||||
width: 90vw;
|
||||
}
|
||||
|
||||
div.params-wrapper {
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
}
|
||||
div.tabs,
|
||||
div.subtabs {
|
||||
order: 2;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
form.params {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
order: 1;
|
||||
}
|
||||
fieldset input[type="radio"] + label {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
aspect-ratio: 1;
|
||||
}
|
||||
fieldset:not(.color, #favoriteColor) input[type="radio"] + label::after {
|
||||
transform: scale(3.5);
|
||||
}
|
||||
|
||||
.has-sliders {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
height: 100%;
|
||||
gap: 2px 6px;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--text-shade-1) var(--bg-shade-3);
|
||||
}
|
||||
.has-sliders::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
background: var(--bg-shade-3);
|
||||
border-radius: 9px;
|
||||
}
|
||||
.has-sliders::-webkit-scrollbar-thumb {
|
||||
background: var(--text-shade-1);
|
||||
border-radius: 9px;
|
||||
}
|
||||
|
||||
.params-wrapper::before {
|
||||
top: -12px;
|
||||
left: -100vw;
|
||||
width: 300vw;
|
||||
border-radius: 0;
|
||||
background: var(--bg-shade-2);
|
||||
transform: none;
|
||||
}
|
||||
|
||||
fieldset,
|
||||
fieldset.has-subpages .subpage {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
grid-template-rows: repeat(3, 1fr) 48px;
|
||||
width: 100%;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
grid-column: 1 / span 4;
|
||||
grid-row: 4;
|
||||
}
|
||||
|
||||
fieldset:not(.has-sliders, .has-textinput) {
|
||||
position: relative;
|
||||
margin-bottom: -60px;
|
||||
}
|
||||
|
||||
div.tabs {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
form.params .tab {
|
||||
gap: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
div.subtabs {
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
form.params {
|
||||
grid-template-columns: repeat(2, auto);
|
||||
}
|
||||
|
||||
div.tabs .tabbtn::after,
|
||||
div.subtabs .subtabbtn::after,
|
||||
.has-sliders label::after {
|
||||
transform: scale(2);
|
||||
}
|
||||
|
||||
div.subtabs .subtabbtn {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 424px) {
|
||||
fieldset:not(.color, #favoriteColor) input[type="radio"] + label::after {
|
||||
transform: scale(2.8);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 396px) {
|
||||
div.tabs .tabbtn::after,
|
||||
div.subtabs .subtabbtn::after,
|
||||
.has-sliders label::after {
|
||||
transform: scale(1.5);
|
||||
}
|
||||
|
||||
div.tabs .tabbtn {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
div.subtabs .subtabbtn {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
div.pagination {
|
||||
transform: scale(0.7);
|
||||
transform-origin: right;
|
||||
}
|
||||
|
||||
.has-textinput {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 360px) {
|
||||
fieldset,
|
||||
fieldset.has-subpages .subpage {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
fieldset input[type="radio"]:checked + label {
|
||||
box-shadow: inset 0 0 0 3px var(--accent-shade-1);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 344px) {
|
||||
fieldset:not(.color, #favoriteColor) input[type="radio"] + label::after {
|
||||
transform: scale(2);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 320px) {
|
||||
div.tabs .tabbtn {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
div.tabs .tabbtn::after {
|
||||
transform: scale(1.25);
|
||||
}
|
||||
}
|
||||
@@ -1,197 +0,0 @@
|
||||
footer {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, fit-content(100%)) 1fr;
|
||||
gap: min(48px, 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;
|
||||
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-wrapper {
|
||||
z-index: 2;
|
||||
justify-self: end;
|
||||
position: relative;
|
||||
}
|
||||
footer div.discord-server-card {
|
||||
background: var(--bg-shade-2);
|
||||
border-radius: 12px;
|
||||
padding: 30px 90px 30px 36px;
|
||||
}
|
||||
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;
|
||||
}
|
||||
footer div.discord-server-card-wrapper .bandwidth-raccoon-wrapper {
|
||||
position: absolute;
|
||||
top: -120px;
|
||||
right: 0px;
|
||||
z-index: -1;
|
||||
}
|
||||
footer div.discord-server-card-wrapper img.bandwidth-raccoon {
|
||||
width: 192px;
|
||||
height: 192px;
|
||||
cursor: pointer;
|
||||
transform: none;
|
||||
transition: transform 150ms;
|
||||
}
|
||||
footer div.bandwidth-raccoon-wrapper.speak img.bandwidth-raccoon {
|
||||
transform: rotate(12deg) translateY(-12px);
|
||||
}
|
||||
footer .bandwidth-raccoon-wrapper .text-bubble {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
margin: 0 auto;
|
||||
top: -24px;
|
||||
max-width: min(200%, 90vw);
|
||||
width: max-content;
|
||||
background: var(--bg-shade-3);
|
||||
padding: 18px;
|
||||
align-self: center;
|
||||
margin-bottom: 12px;
|
||||
border-radius: 12px;
|
||||
box-sizing: border-box;
|
||||
transform: translateY(-100%);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 250ms;
|
||||
}
|
||||
footer .bandwidth-raccoon-wrapper.speak .text-bubble {
|
||||
opacity: 1;
|
||||
}
|
||||
footer .bandwidth-raccoon-wrapper .text-bubble:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
display: block;
|
||||
width: 0;
|
||||
z-index: 1;
|
||||
border-style: solid;
|
||||
border-color: var(--bg-shade-3) transparent;
|
||||
border-width: 12px 12px 0;
|
||||
bottom: -9px;
|
||||
right: 60px;
|
||||
margin-left: -10px;
|
||||
}
|
||||
|
||||
@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-wrapper {
|
||||
grid-column: 1 / span 4;
|
||||
width: 100%;
|
||||
justify-self: normal;
|
||||
}
|
||||
footer div.discord-server-card-wrapper::before {
|
||||
content: "";
|
||||
width: 100%;
|
||||
height: 60px;
|
||||
position: absolute;
|
||||
bottom: -60px;
|
||||
left: 0;
|
||||
background: var(--bg-shade-0);
|
||||
z-index: 2;
|
||||
}
|
||||
footer div.discord-server-card-wrapper .bandwidth-raccoon-wrapper {
|
||||
bottom: -72px;
|
||||
top: unset;
|
||||
z-index: 0;
|
||||
}
|
||||
footer div.discord-server-card {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
@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-wrapper {
|
||||
grid-column: 1 / span 1;
|
||||
}
|
||||
footer div.discord-server-card {
|
||||
padding: 30px;
|
||||
overflow: visible;
|
||||
}
|
||||
footer div.discord-server-card-wrapper .bandwidth-raccoon-wrapper {
|
||||
bottom: unset;
|
||||
top: -120px;
|
||||
z-index: -1;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 320px) {
|
||||
footer div.discord-server-card-wrapper .bandwidth-raccoon-wrapper {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -1,470 +0,0 @@
|
||||
header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 2.5%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 95%;
|
||||
margin-top: 35px;
|
||||
z-index: 60;
|
||||
transition: box-shadow 180ms, background 180ms;
|
||||
}
|
||||
header * {
|
||||
z-index: 1;
|
||||
}
|
||||
header::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -35px;
|
||||
left: -10vw;
|
||||
width: 120vw;
|
||||
height: calc(100% + 35px + 35px);
|
||||
background: rgba(27, 31, 59, 0.98);
|
||||
transition: background 180ms;
|
||||
}
|
||||
header.transparent,
|
||||
header.transparent::before {
|
||||
background: rgba(27, 31, 59, 0);
|
||||
}
|
||||
header.dropdown-active {
|
||||
background: rgba(27, 31, 59, 0.98);
|
||||
box-shadow: 0 0 0 600vw rgba(27, 31, 59, 0.8);
|
||||
}
|
||||
header .dropdown-arrow {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
width: 0;
|
||||
border-style: solid;
|
||||
border-color: var(--bg-shade-3) transparent;
|
||||
border-width: 0 14px 14px;
|
||||
bottom: -26px;
|
||||
margin-left: -10px;
|
||||
margin-bottom: -10px;
|
||||
transition: left 180ms, margin-bottom 180ms, opacity 180ms;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
header.dropdown-active .dropdown-arrow {
|
||||
opacity: 1;
|
||||
display: block;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
header a {
|
||||
text-decoration: none;
|
||||
}
|
||||
header .logo-link,
|
||||
header .logo-link svg {
|
||||
display: block;
|
||||
}
|
||||
|
||||
header div.left-section {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
}
|
||||
|
||||
header nav {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
margin-left: 40px;
|
||||
}
|
||||
header button.dropdown-button#mobile-button {
|
||||
display: none;
|
||||
background: none;
|
||||
padding: 0;
|
||||
margin-right: 12px;
|
||||
transition: transform 200ms;
|
||||
}
|
||||
header button.dropdown-button#mobile-button.active {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
header div.dropdown-button-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
}
|
||||
/* these are safezones where the dropdown will not close */
|
||||
header div.dropdown-button-wrapper::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -16px;
|
||||
bottom: 100%;
|
||||
left: 0;
|
||||
right: -12px;
|
||||
}
|
||||
header div.dropdown-button-wrapper::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
bottom: -32px;
|
||||
left: 0;
|
||||
right: -12px;
|
||||
}
|
||||
|
||||
header nav button {
|
||||
background: none;
|
||||
color: var(--text-shade-1);
|
||||
margin: 0 17px;
|
||||
padding: 0;
|
||||
}
|
||||
header nav button:hover,
|
||||
header nav button.active {
|
||||
background: none;
|
||||
color: var(--text-shade-3);
|
||||
}
|
||||
header nav a.donate button {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-weight: bold;
|
||||
background: #332b61;
|
||||
color: var(--accent-shade-3);
|
||||
padding: 2px 12px;
|
||||
border-radius: 24px;
|
||||
}
|
||||
header nav a.donate button svg {
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
}
|
||||
header nav a.donate button:hover {
|
||||
background: var(--accent-shade-0);
|
||||
color: #fff;
|
||||
}
|
||||
header nav a.donate button.dropdown-button::after {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
align-items: center;
|
||||
width: 16px;
|
||||
height: 8px;
|
||||
bottom: 0;
|
||||
margin-left: 0.3rem;
|
||||
background: no-repeat center url("/assets/images/down-arrow.svg");
|
||||
filter: brightness(0) invert(78%) sepia(2%) saturate(5488%) hue-rotate(197deg)
|
||||
brightness(88%) contrast(93%);
|
||||
|
||||
transition: transform 100ms;
|
||||
}
|
||||
header nav button.dropdown-button.active::after {
|
||||
transform: scaleY(-1);
|
||||
filter: none;
|
||||
}
|
||||
|
||||
header div.dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
margin-top: 24px;
|
||||
left: 0;
|
||||
display: block;
|
||||
background: var(--bg-shade-3);
|
||||
border-radius: 8px;
|
||||
width: 420px;
|
||||
height: 0;
|
||||
overflow-y: hidden;
|
||||
transition: height 180ms;
|
||||
}
|
||||
header div.dropdown * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
header div.dropdown-content {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 180ms;
|
||||
}
|
||||
header div.dropdown-content.show {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
header div.dropdown .top {
|
||||
padding: 32px 18px;
|
||||
}
|
||||
header div.dropdown .top a {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
gap: 16px;
|
||||
justify-content: start;
|
||||
align-items: center;
|
||||
color: var(--text-shade-1);
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
header div.dropdown .top a .icon {
|
||||
background: var(--bg-shade-2);
|
||||
color: var(--accent-shade-3);
|
||||
height: 56px;
|
||||
width: 56px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
header div.dropdown .top a:hover .icon {
|
||||
background: #151b44;
|
||||
color: var(--accent-shade-1);
|
||||
}
|
||||
header div.dropdown .top a .icon svg {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin: 12px;
|
||||
}
|
||||
header div.dropdown .top a .title {
|
||||
margin: 0;
|
||||
font-weight: bold;
|
||||
color: var(--text-shade-3);
|
||||
}
|
||||
header div.dropdown .top a .caption {
|
||||
margin: 0;
|
||||
}
|
||||
header div.dropdown .top a:hover {
|
||||
background: var(--bg-shade-2-5);
|
||||
}
|
||||
header div.dropdown .top a:hover::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 24px;
|
||||
width: 24px;
|
||||
height: 100%;
|
||||
background: no-repeat center url("/assets/images/arrow-right.svg");
|
||||
/* garbage to make it look the same color */
|
||||
filter: brightness(0) invert(60%) sepia(70%) saturate(453%) hue-rotate(208deg)
|
||||
brightness(113%) contrast(97%);
|
||||
}
|
||||
header div.dropdown .top a:hover .title {
|
||||
color: var(--accent-shade-3);
|
||||
}
|
||||
|
||||
header div.dropdown .bottom {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
justify-content: center;
|
||||
gap: 24px;
|
||||
background: var(--bg-shade-3-5);
|
||||
padding: 22px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
header div.dropdown .bottom a {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: var(--bg-shade-3);
|
||||
color: var(--text-shade-3);
|
||||
border-radius: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
header div.dropdown .bottom a:hover {
|
||||
background: var(--bg-shade-2);
|
||||
}
|
||||
header div.dropdown .bottom a svg {
|
||||
width: 24px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
header div.dropdown .top a.show-on-mobile {
|
||||
display: none;
|
||||
}
|
||||
|
||||
header .right-section {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
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: auto;
|
||||
}
|
||||
|
||||
header .user-widget-wrapper a.login-link {
|
||||
color: var(--text-shade-1);
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
height: 32px;
|
||||
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;
|
||||
word-break: keep-all;
|
||||
}
|
||||
header .user-widget .button.logout {
|
||||
background: var(--bg-shade-3);
|
||||
color: var(--text-shade-3);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 900px) {
|
||||
header {
|
||||
position: relative;
|
||||
justify-content: flex-end;
|
||||
width: 90%;
|
||||
left: 5%;
|
||||
}
|
||||
|
||||
header .hide-on-mobile {
|
||||
display: none !important;
|
||||
}
|
||||
header button.dropdown-button#mobile-button {
|
||||
display: block;
|
||||
}
|
||||
header .left-section {
|
||||
margin-right: auto;
|
||||
}
|
||||
header .right-section {
|
||||
margin-left: 24px;
|
||||
}
|
||||
header nav a.donate button {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
header div.dropdown {
|
||||
left: unset;
|
||||
right: -104px;
|
||||
width: 90vw;
|
||||
}
|
||||
header div.dropdown .top {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
header div.dropdown .top {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
header div.dropdown .top {
|
||||
padding: 16px 9px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 500px) {
|
||||
header nav a.donate button span {
|
||||
display: none;
|
||||
}
|
||||
header nav a.donate button {
|
||||
padding: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 420px) {
|
||||
header nav a.donate button {
|
||||
display: none;
|
||||
}
|
||||
header .right-section {
|
||||
margin-left: 0;
|
||||
}
|
||||
header div.dropdown {
|
||||
right: -80px;
|
||||
}
|
||||
header div.dropdown .top a {
|
||||
padding: 8px;
|
||||
}
|
||||
header div.dropdown .top a .icon {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
}
|
||||
header div.dropdown .top a .icon svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 330px) {
|
||||
header .logo-link svg text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
header .logo-link svg {
|
||||
width: 39.876px;
|
||||
}
|
||||
|
||||
header .logo-link {
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
/*
|
||||
MOVE PROGRESS CSS HERE
|
||||
*/
|
||||
#quick-nav a {
|
||||
color: var(--text-shade-1);
|
||||
text-decoration: none;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
#quick-nav a:hover {
|
||||
color: var(--text-shade-3);
|
||||
text-decoration: underline;
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
.wrapper {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
header {
|
||||
margin: 35px 0;
|
||||
}
|
||||
|
||||
.account-form-wrapper {
|
||||
margin: auto;
|
||||
width: fit-content;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
form.account {
|
||||
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-shade-3);
|
||||
}
|
||||
|
||||
form.account p {
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
form.account div {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
form.account label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
text-transform: uppercase;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
form.account button {
|
||||
width: 100%;
|
||||
background: var(--accent-shade-0);
|
||||
}
|
||||
|
||||
form.account a {
|
||||
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-shade-3);
|
||||
}
|
||||
|
||||
form.account a.pwdreset {
|
||||
margin-left: auto;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
form.account a.register {
|
||||
margin: auto;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
@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.success div {
|
||||
background: var(--green-shade-0);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,310 +0,0 @@
|
||||
.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);
|
||||
padding: 8px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
# How to optimise images
|
||||
|
||||
WebP is the best format to use for most images, as it provides a good compression over PNG and JPEG. It is supported by all modern browsers (https://caniuse.com/webp) and is the recommended format for images on the web.
|
||||
|
||||
## Using imagemagick (Recommended)
|
||||
|
||||
This is the recommended method to convert images to WebP as it is faster and can be done in bulk.
|
||||
|
||||
Install imagemagick from: https://imagemagick.org/script/download.php
|
||||
|
||||
In the folder you would like to convert images, use the appropriate command:
|
||||
|
||||
```bash
|
||||
mogrify -format webp *.png
|
||||
mogrify -format webp *.jpg
|
||||
```
|
||||
|
||||
## Using a web service
|
||||
|
||||
If you don't want to install imagemagick, you can use towebp.io to convert images to WebP.
|
||||
|
||||
Go to https://towebp.io/ and upload your images. You can convert multiple images at once.
|
||||
|
||||
To match the default quality of imagemagick, use a quality of 90 (imagemagick uses 92 by default).
|
||||
@@ -1 +0,0 @@
|
||||
<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-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: 306 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#fff" d="M5 19h1.4l8.625-8.625l-1.4-1.4L5 17.6ZM19.3 8.925l-4.25-4.2l1.4-1.4q.575-.575 1.413-.575q.837 0 1.412.575l1.4 1.4q.575.575.6 1.388q.025.812-.55 1.387ZM4 21q-.425 0-.712-.288Q3 20.425 3 20v-2.825q0-.2.075-.387q.075-.188.225-.338l10.3-10.3l4.25 4.25l-10.3 10.3q-.15.15-.337.225q-.188.075-.388.075ZM14.325 9.675l-.7-.7l1.4 1.4Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 438 B |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 571 KiB |
@@ -1,76 +0,0 @@
|
||||
const updateServerEnvironmentForm = document.querySelector('form.server-selection');
|
||||
const serverSelectionSaveButton = document.querySelector('#save-server-selection');
|
||||
const editSettingsModal = document.querySelector('.modal-wrapper#edit-settings');
|
||||
const editSettingsModalButtonClose = document.getElementById('editSettingsCloseButton');
|
||||
const deleteAccountButton = document.getElementById('account-delete');
|
||||
const deletePNIDConfirmModal = document.querySelector('.modal-wrapper#confirm-delete');
|
||||
const deletePNIDConfirmModalButtonConfirm = document.getElementById('confirmDeleteConfirmButton');
|
||||
const deletePNIDConfirmModalButtonClose = document.getElementById('confirmDeleteCloseButton');
|
||||
|
||||
editSettingsModalButtonClose?.addEventListener('click', () => {
|
||||
editSettingsModal.classList.add('hidden');
|
||||
});
|
||||
|
||||
document.addEventListener('click', (event) => {
|
||||
if (event.target.classList.contains('edit')) {
|
||||
event.preventDefault();
|
||||
|
||||
editSettingsModal.classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
serverSelectionSaveButton?.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
const checkedInput = updateServerEnvironmentForm.querySelector('input:checked');
|
||||
|
||||
try {
|
||||
const tokenType = document.cookie.split('; ').find(row => row.startsWith('token_type=')).split('=')[1];
|
||||
const accessToken = document.cookie.split('; ').find(row => row.startsWith('access_token=')).split('=')[1];
|
||||
|
||||
fetch('https://api.pretendo.cc/v1/user', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `${tokenType} ${decodeURIComponent(accessToken)}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
environment: checkedInput.value
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then((json) => {
|
||||
if (!json.error) {
|
||||
// TODO - Make this prettier
|
||||
alert('Saved server environment');
|
||||
} else {
|
||||
console.log(json.error);
|
||||
alert('Failed to server environment');
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
// TODO - Make this prettier
|
||||
alert('Failed to server environment');
|
||||
});
|
||||
} catch (error) {
|
||||
alert(error);
|
||||
}
|
||||
});
|
||||
|
||||
deleteAccountButton?.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
deletePNIDConfirmModal.classList.remove('hidden');
|
||||
});
|
||||
|
||||
deletePNIDConfirmModalButtonConfirm?.addEventListener('click', async () => {
|
||||
await fetch('/account/delete', {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
deletePNIDConfirmModal.classList.add('hidden');
|
||||
});
|
||||
|
||||
deletePNIDConfirmModalButtonClose?.addEventListener('click', () => {
|
||||
deletePNIDConfirmModal.classList.add('hidden');
|
||||
});
|
||||
@@ -1,7 +0,0 @@
|
||||
const openSidebarBtn = document.querySelector('#openSidebar');
|
||||
const content = document.querySelector('div.content');
|
||||
openSidebarBtn.addEventListener('click', function () {
|
||||
const sidebar = document.querySelector('.sidebar');
|
||||
sidebar.classList.toggle('open');
|
||||
content.classList.toggle('open-sidebar');
|
||||
});
|
||||
@@ -1,197 +0,0 @@
|
||||
const header = document.querySelector('header');
|
||||
const dropdownButtonWrapper = document.querySelector('.dropdown-button-wrapper');
|
||||
const dropdown = document.querySelector('header div.dropdown');
|
||||
const allDropdownButtons = document.querySelectorAll('button.dropdown-button');
|
||||
const desktopDropdownBtns = document.querySelectorAll('header .dropdown-button-wrapper button.dropdown-button');
|
||||
const mobileDropdownBtn = document.querySelector('.dropdown-button#mobile-button');
|
||||
let dropdownContent;
|
||||
|
||||
function isDropdownOpen() {
|
||||
return header.classList.contains('dropdown-active');
|
||||
}
|
||||
|
||||
function closeDropdown() {
|
||||
dropdown.style.height = '0';
|
||||
header.classList.remove('dropdown-active');
|
||||
// deselect all buttons
|
||||
allDropdownButtons.forEach((button) => {
|
||||
button.classList.remove('active');
|
||||
});
|
||||
}
|
||||
window.addEventListener('resize', () => {
|
||||
if (isDropdownOpen() && dropdownContent) {
|
||||
// set the dropdown height to the height of the content
|
||||
dropdown.style.height = `${dropdownContent.offsetHeight}px`;
|
||||
}
|
||||
});
|
||||
|
||||
function navbarDropdownHandler(buttonID) {
|
||||
const allDropdownContents = dropdown.querySelectorAll('div.dropdown-content');
|
||||
dropdownContent = document.querySelector(`.dropdown-content#${buttonID}-dropdown-content`);
|
||||
const dropdownButton = document.querySelector(`button.dropdown-button#${buttonID}-button`);
|
||||
const dropdownArrow = document.querySelector('.dropdown-arrow#navbar-dropdown-arrow');
|
||||
|
||||
// if on mobile, reclicking the button should close the dropdown
|
||||
if (buttonID === 'mobile' && dropdownButton.classList.contains('active')) {
|
||||
closeDropdown();
|
||||
return;
|
||||
}
|
||||
|
||||
// hide all contents
|
||||
allDropdownContents.forEach((content) => {
|
||||
content.classList.remove('show');
|
||||
});
|
||||
// deselect all buttons
|
||||
allDropdownButtons.forEach((button) => {
|
||||
button.classList.remove('active');
|
||||
});
|
||||
// show the content of the clicked button
|
||||
dropdownContent.classList.add('show');
|
||||
// select the clicked button
|
||||
dropdownButton.classList.add('active');
|
||||
// set the dropdown height to the height of the content
|
||||
dropdown.style.height = `${dropdownContent.offsetHeight}px`;
|
||||
|
||||
// move the arrow to the selected button
|
||||
dropdownArrow.style.left = `${dropdownButton.offsetLeft + dropdownButton.offsetWidth / 2 - 5}px`;
|
||||
|
||||
// dim the rest of the page
|
||||
header.classList.add('dropdown-active');
|
||||
}
|
||||
|
||||
const dropdownAnchors = document.querySelectorAll('.dropdown-content a');
|
||||
dropdownAnchors.forEach((a) => {
|
||||
a.addEventListener('click', () => {
|
||||
closeDropdown();
|
||||
});
|
||||
});
|
||||
|
||||
// make the header background transparent if near the top of the page
|
||||
function makeHeaderBackgroundTransparent() {
|
||||
if (window.pageYOffset < 100) {
|
||||
header.classList.add('transparent');
|
||||
} else {
|
||||
header.classList.remove('transparent');
|
||||
}
|
||||
}
|
||||
makeHeaderBackgroundTransparent();
|
||||
window.addEventListener('scroll', () => {
|
||||
makeHeaderBackgroundTransparent();
|
||||
});
|
||||
|
||||
desktopDropdownBtns.forEach((btn) => {
|
||||
['click', 'mouseover'].forEach((event) => {
|
||||
btn.addEventListener(event, () => {
|
||||
const id = btn.id.replace('-button', '');
|
||||
navbarDropdownHandler(id);
|
||||
});
|
||||
});
|
||||
});
|
||||
mobileDropdownBtn.addEventListener('click', () => {
|
||||
const id = 'mobile';
|
||||
navbarDropdownHandler(id, true);
|
||||
});
|
||||
|
||||
/* if on desktop: we check if the element the mouse moves to is part of the ignored element (keep the dropdown open) or not (close the dropdown)
|
||||
* if on mobile: do nothing
|
||||
*/
|
||||
function dropdownOnMouseLeave(e, ignoredElement) {
|
||||
if (window.innerWidth > 900) {
|
||||
const targetElement = e.relatedTarget || e.toElement;
|
||||
if (targetElement !== ignoredElement && !ignoredElement.contains(targetElement)) {
|
||||
closeDropdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
dropdownButtonWrapper.addEventListener('mouseleave', (e) => {
|
||||
dropdownOnMouseLeave(e, dropdown);
|
||||
});
|
||||
dropdown.addEventListener('mouseleave', (e) => {
|
||||
dropdownOnMouseLeave(e, dropdownButtonWrapper);
|
||||
});
|
||||
|
||||
// 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');
|
||||
});
|
||||
@@ -1,523 +0,0 @@
|
||||
/**
|
||||
* Compilation note:
|
||||
* 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 to see any changes.
|
||||
*
|
||||
* browserify is needed for the use of require() in the browser
|
||||
*/
|
||||
const Mii = require('mii-js');
|
||||
const newMiiData = 'AwAAQOlVognnx0GC2qjhdwOzuI0n2QAAAGBzAHQAZQB2AGUAAAAAAAAAAAAAAEBAAAAhAQJoRBgmNEYUgRIXaA0AACkAUkhQAAAAAAAAAAAAAAAAAAAAAAAAAAAAANeC';
|
||||
|
||||
// Prevent the user from reloading or leaving the page
|
||||
window.onbeforeunload = function (e) {
|
||||
e?.preventDefault();
|
||||
e.returnValue = '';
|
||||
};
|
||||
|
||||
// this makes it so the canvas fits in the target element
|
||||
function setCanvasScale() {
|
||||
let targetX;
|
||||
let targetY;
|
||||
|
||||
if (window.innerWidth <= 1080) {
|
||||
const canvasWrapper = document.querySelector('.canvas-wrapper');
|
||||
|
||||
targetX = canvasWrapper.offsetWidth;
|
||||
targetY = canvasWrapper.offsetHeight;
|
||||
} else {
|
||||
targetX = window.innerWidth * 0.9;
|
||||
targetY = window.innerHeight * 0.9;
|
||||
}
|
||||
|
||||
const canvas = document.querySelector('canvas#miiCanvas');
|
||||
const XScale = targetX / canvas.width;
|
||||
const YScale = targetY / canvas.height;
|
||||
canvas.style.transform = `scale(${Math.min(XScale, YScale)})`;
|
||||
}
|
||||
|
||||
setCanvasScale();
|
||||
window.addEventListener('resize', () => {
|
||||
setCanvasScale();
|
||||
});
|
||||
|
||||
let mii; // global mii object
|
||||
|
||||
// this initalizes a mii for editing
|
||||
// returns if mii data was parsed successfully
|
||||
function initializeMiiData(encodedUserMiiData) {
|
||||
console.group('Initalizing Mii data');
|
||||
console.log('encoded mii data:', encodedUserMiiData);
|
||||
|
||||
// We initialize the Mii object
|
||||
try {
|
||||
console.log('Attempting to parse mii data');
|
||||
mii = new Mii(Buffer.from(encodedUserMiiData, 'base64'));
|
||||
} catch (err) {
|
||||
console.error('failed to decode mii data', err);
|
||||
console.groupEnd();
|
||||
return false;
|
||||
}
|
||||
|
||||
// We set the img sources for the unedited miis in the save animation
|
||||
console.log('grabbing rendered miis for later use');
|
||||
const miiStudioNeutralUrl = mii.studioUrl({
|
||||
width: 512,
|
||||
bgColor: '13173300'
|
||||
});
|
||||
const miiStudioSorrowUrl = mii.studioUrl({
|
||||
width: 512,
|
||||
bgColor: '13173300',
|
||||
expression: 'sorrow'
|
||||
});
|
||||
document.querySelector('.mii-comparison img.old-mii').setAttribute('data-src', miiStudioNeutralUrl);
|
||||
document.querySelector('.mii-comparison.confirmed img.old-mii').setAttribute('data-src', miiStudioSorrowUrl);
|
||||
console.log('initialization complete');
|
||||
console.groupEnd();
|
||||
return true;
|
||||
}
|
||||
|
||||
// The Mii data is stored in a script tag in the HTML, so we can just grab it and then remove the element
|
||||
const encodedUserMiiData = document.querySelector(
|
||||
'script#encodedUserMiiData'
|
||||
).textContent;
|
||||
document.querySelector('script#encodedUserMiiData').remove();
|
||||
|
||||
// is valid mii data
|
||||
const validMiiData = initializeMiiData(encodedUserMiiData);
|
||||
if (!validMiiData) {
|
||||
const shouldContinue = window.confirm('Found corrupted mii data, want to continue with a new Mii?');
|
||||
if (!shouldContinue) {
|
||||
window.location.assign('/account');
|
||||
}
|
||||
initializeMiiData(newMiiData);
|
||||
}
|
||||
|
||||
// we keeep the images here so we can cache them when we need to change the build/height
|
||||
const miiFaceImg = new Image();
|
||||
const miiBodyImg = new Image();
|
||||
|
||||
// Initial mii render
|
||||
renderMii();
|
||||
// This function renders the Mii on the canvas
|
||||
function renderMii(heightOverride, buildOverride) {
|
||||
const canvas = document.querySelector('canvas#miiCanvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const height = heightOverride || mii.height;
|
||||
const build = buildOverride || mii.build;
|
||||
|
||||
// if there isn't an override or the images haven't been cached, we load the images
|
||||
if ((!heightOverride && !buildOverride) || !miiFaceImg.src || !miiBodyImg.src) {
|
||||
canvas.style.filter = 'blur(4px) brightness(70%)';
|
||||
|
||||
// request a face only render with split depth
|
||||
miiFaceImg.src = mii.studioUrl({
|
||||
width: 512,
|
||||
bgColor: '13173300',
|
||||
type: 'face_only',
|
||||
splitMode: 'both' // this will be twice the height
|
||||
});
|
||||
miiBodyImg.src = mii.studioAssetUrlBody();
|
||||
}
|
||||
|
||||
// misc calculations
|
||||
const bodyWidth = (build * 1.7 + 220) * (0.003 * height + 0.6);
|
||||
const bodyHeight = height * 3.5 + 227;
|
||||
const bodyXPos = (canvas.width - bodyWidth) / 2;
|
||||
const bodyYPos = canvas.height - bodyHeight;
|
||||
const headYPos = bodyYPos - 408;
|
||||
|
||||
// we make sure every image is loaded before rendering
|
||||
if (miiFaceImg.complete) {
|
||||
onMiiFaceImgLoad();
|
||||
} else {
|
||||
miiFaceImg.onload = () => {
|
||||
onMiiFaceImgLoad();
|
||||
};
|
||||
}
|
||||
function onMiiFaceImgLoad_() {
|
||||
if (miiBodyImg.complete) {
|
||||
onBodyImgLoad();
|
||||
} else {
|
||||
miiBodyImg.onload = () => {
|
||||
onBodyImgLoad();
|
||||
};
|
||||
}
|
||||
}
|
||||
function onBodyImgLoad() {
|
||||
if (miiFaceImg.complete) {
|
||||
onMiiFaceImgLoad_();
|
||||
} else {
|
||||
miiFaceImg.onload = () => {
|
||||
onMiiFaceImgLoad_();
|
||||
};
|
||||
}
|
||||
}
|
||||
function onMiiFaceImgLoad() {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// in mii studio split depth mode, the back half is on the top
|
||||
// and the front half is on the bottom
|
||||
// the back half needs to be drawn, then the body, then the front
|
||||
const halfHeight = miiFaceImg.height / 2;
|
||||
// top half of the image / back half of the head
|
||||
ctx.drawImage(miiFaceImg, 0, 0, miiFaceImg.width, halfHeight, 0, headYPos, miiFaceImg.width, halfHeight);
|
||||
|
||||
// draw body on top
|
||||
ctx.drawImage(miiBodyImg, bodyXPos, bodyYPos, bodyWidth, bodyHeight);
|
||||
|
||||
// we draw a portion of the bald mii on top of the normal mii to hide the mii's neck (see https://i.imgur.com/U0fpkwi.png)
|
||||
// ctx.drawImage(baldMiiFaceImg, 186, 384, 140, 120, 186, headYPos + 384, 140, 120);
|
||||
|
||||
// draw bottom half of the image / front half of the head
|
||||
ctx.drawImage(miiFaceImg, 0, halfHeight, miiFaceImg.width, halfHeight, 0, headYPos, miiFaceImg.width, halfHeight);
|
||||
|
||||
canvas.style.filter = '';
|
||||
}
|
||||
|
||||
if (!heightOverride && !buildOverride) {
|
||||
const faceMiiStudioUrl = mii.studioUrl({
|
||||
width: 512,
|
||||
bgColor: '13173300'
|
||||
});
|
||||
|
||||
const faceMiiStudioSmileUrl = mii.studioUrl({
|
||||
width: 512,
|
||||
bgColor: '13173300',
|
||||
expression: 'smile'
|
||||
});
|
||||
|
||||
// sets the new mii in the save tab to the new mii
|
||||
document.querySelector('.mii-comparison img.new-mii').setAttribute('data-src', faceMiiStudioUrl);
|
||||
document.querySelector('.mii-comparison.confirmed img.new-mii').setAttribute('data-src', faceMiiStudioSmileUrl);
|
||||
}
|
||||
}
|
||||
|
||||
// This function updates a prop of the Mii and rerenders it
|
||||
function updateMii(e) {
|
||||
const prop = e.target.name;
|
||||
let value = e.target.value || e.target.defaultValue;
|
||||
|
||||
// if the value comes from a checkbox, we use the checked property
|
||||
if (value === 'on' || value === 'off') {
|
||||
value = e.target.checked;
|
||||
}
|
||||
|
||||
// if the prop is disableSharing, we set the value to the opposite of the current value
|
||||
if (prop === 'disableSharing') {
|
||||
value = !value;
|
||||
}
|
||||
|
||||
// Handle booleans, on/offs and strings
|
||||
if (value === 'true' || value === 'false') {
|
||||
mii[prop] = value === 'true';
|
||||
} else if (value === 'on' || value === 'off') {
|
||||
mii[prop] = value === 'on';
|
||||
} else if (isNaN(parseInt(value))) {
|
||||
mii[prop] = value;
|
||||
} else {
|
||||
mii[prop] = parseInt(value);
|
||||
}
|
||||
|
||||
// if the user is editing the height or the build, we render the mii with the correct override, else we do a straight render
|
||||
if (prop === 'height') {
|
||||
renderMii(value, false);
|
||||
} else if (prop === 'build') {
|
||||
renderMii(false, value);
|
||||
} else {
|
||||
renderMii();
|
||||
console.log(mii);
|
||||
}
|
||||
}
|
||||
|
||||
function handleCalendar(e) {
|
||||
const valueArray = e.target.value.split('-');
|
||||
const day = valueArray[2];
|
||||
const month = valueArray[1];
|
||||
|
||||
mii.birthDay = parseInt(day);
|
||||
mii.birthMonth = parseInt(month);
|
||||
}
|
||||
|
||||
function preventEmpty(e) {
|
||||
if (e.target.value !== '') {
|
||||
return;
|
||||
}
|
||||
|
||||
e.target.value = e.target.defaultValue;
|
||||
}
|
||||
|
||||
document.querySelectorAll('fieldset').forEach((fieldset) => {
|
||||
fieldset.addEventListener('change', updateMii);
|
||||
});
|
||||
document.querySelectorAll('input[type=\'range\']').forEach((input) => {
|
||||
input.addEventListener('input', updateMii);
|
||||
});
|
||||
document
|
||||
.querySelectorAll('input[type=\'text\'], input[type=\'number\']')
|
||||
.forEach((input) => {
|
||||
input.addEventListener('blur', preventEmpty);
|
||||
});
|
||||
document
|
||||
.querySelector('input[type=\'date\']#birthDate')
|
||||
.addEventListener('change', handleCalendar);
|
||||
|
||||
// FORM
|
||||
|
||||
// Here we preselect the options corresponding to the Mii's current values
|
||||
[
|
||||
'faceType',
|
||||
'skinColor',
|
||||
'makeupType',
|
||||
'wrinklesType',
|
||||
'hairType',
|
||||
'hairColor',
|
||||
'eyebrowType',
|
||||
'eyebrowColor',
|
||||
'eyeType',
|
||||
'eyeColor',
|
||||
'noseType',
|
||||
'mouthType',
|
||||
'mouthColor',
|
||||
'glassesType',
|
||||
'glassesColor',
|
||||
'beardType',
|
||||
'facialHairColor',
|
||||
'mustacheType',
|
||||
'moleEnabled',
|
||||
'gender',
|
||||
'favoriteColor'
|
||||
].forEach((prop) => {
|
||||
const el = document.querySelector(`#${prop}${mii[prop]}`);
|
||||
if (el) {
|
||||
el.checked = true;
|
||||
}
|
||||
console.log(`[info] preselected value for ${prop}`);
|
||||
});
|
||||
|
||||
['favorite', 'allowCopying'].forEach((prop) => {
|
||||
const el = document.querySelector(`#${prop}`);
|
||||
if (el) {
|
||||
el.checked = mii[prop];
|
||||
}
|
||||
console.log(`[info] preselected value for ${prop}`);
|
||||
});
|
||||
|
||||
document.querySelector('#disableSharing').checked = !mii.disableSharing;
|
||||
console.log('[info] preselected value for disableSharing');
|
||||
|
||||
[
|
||||
'eyebrowYPosition',
|
||||
'eyebrowSpacing',
|
||||
'eyebrowRotation',
|
||||
'eyebrowScale',
|
||||
'eyebrowVerticalStretch',
|
||||
'eyeYPosition',
|
||||
'eyeSpacing',
|
||||
'eyeRotation',
|
||||
'eyeScale',
|
||||
'eyeVerticalStretch',
|
||||
'noseYPosition',
|
||||
'noseScale',
|
||||
'mouthYPosition',
|
||||
'mouthScale',
|
||||
'mouthHorizontalStretch',
|
||||
'glassesYPosition',
|
||||
'glassesScale',
|
||||
'mustacheYPosition',
|
||||
'mustacheScale',
|
||||
'moleYPosition',
|
||||
'moleXPosition',
|
||||
'moleScale',
|
||||
'height',
|
||||
'build',
|
||||
'miiName',
|
||||
'creatorName'
|
||||
].forEach((prop) => {
|
||||
document.querySelector(`#${prop}`).value = mii[prop];
|
||||
document.querySelector(`#${prop}`).defaultValue = mii[prop];
|
||||
console.log(`[info] preselected value for ${prop}`);
|
||||
});
|
||||
|
||||
const paddedBirthDay = mii.birthDay.toString().padStart(2, '0');
|
||||
const paddedBirthMonth = mii.birthMonth.toString().padStart(2, '0');
|
||||
document.querySelector(
|
||||
'input[type=\'date\']#birthDate'
|
||||
).value = `2024-${paddedBirthMonth}-${paddedBirthDay}`;
|
||||
console.log('[info] preselected value for birthMonth && birthDay');
|
||||
|
||||
// TABS, SUBTABS, AND ALL THE INHERENT JANK
|
||||
|
||||
function openTab(e, tabType) {
|
||||
e.preventDefault();
|
||||
|
||||
// Deselect all subpages
|
||||
document
|
||||
.querySelectorAll('.subtab.has-subpages .subpage.active')
|
||||
.forEach((el) => {
|
||||
el.classList?.remove('active');
|
||||
});
|
||||
document.querySelectorAll('.subtab.active').forEach((el) => {
|
||||
el.classList?.remove('active');
|
||||
});
|
||||
|
||||
const buttonReplacement =
|
||||
tabType.charAt(0).toUpperCase() + tabType.slice(1);
|
||||
|
||||
document.querySelectorAll(`.${tabType}.active`).forEach((el) => {
|
||||
el?.classList?.remove('active');
|
||||
});
|
||||
document.querySelectorAll(`.${tabType}btn.active`).forEach((el) => {
|
||||
el?.classList?.remove('active');
|
||||
});
|
||||
|
||||
const elementID = e.target?.id;
|
||||
document.querySelector(`#${elementID}`).classList.add('active');
|
||||
const selectedID = elementID
|
||||
.replace('SubButton', '')
|
||||
.replace('Button', buttonReplacement);
|
||||
document.querySelector(`#${selectedID}`).classList.add('active');
|
||||
|
||||
// if you selected the save tab...
|
||||
if (selectedID === 'saveTab') {
|
||||
// set data-src on images that have it to src
|
||||
// effectively loading them right now (lazy load)
|
||||
document
|
||||
.querySelectorAll('#saveTab img[data-src]')
|
||||
.forEach((e) => {
|
||||
if (e.getAttribute('data-src') !== e.src) {
|
||||
e.setAttribute('src', e.getAttribute('data-src'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (tabType === 'tab') {
|
||||
// Click the first subtab button, if there is one
|
||||
document.querySelector(`#${selectedID} .subtabbtn`)?.click();
|
||||
}
|
||||
|
||||
setCanvasScale();
|
||||
|
||||
// We hide all subpages
|
||||
document.querySelectorAll('.subpage').forEach((el) => {
|
||||
el.classList.remove('active');
|
||||
});
|
||||
|
||||
// Selects the first subpage if there is one
|
||||
document.querySelector(`#${selectedID} .subpage`)?.classList?.add('active');
|
||||
}
|
||||
|
||||
// Here we bind all of the functions to the corresponding buttons
|
||||
document.querySelectorAll('.tabs button.tabbtn').forEach((el) => {
|
||||
el.addEventListener('click', e => openTab(e, 'tab'));
|
||||
});
|
||||
document.querySelectorAll('.subtabs button.subtabbtn').forEach((el) => {
|
||||
el.addEventListener('click', e => openTab(e, 'subtab'));
|
||||
});
|
||||
|
||||
// SUBPAGES
|
||||
|
||||
function paginationHandler(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// We hide all subpages
|
||||
document.querySelectorAll('.subpage').forEach((el) => {
|
||||
el.classList.remove('active');
|
||||
});
|
||||
|
||||
// We get the current subpage
|
||||
const currentPageIndex = parseInt(
|
||||
e.target.classList[2].replace('index-', '')
|
||||
);
|
||||
let newPageIndex = currentPageIndex;
|
||||
|
||||
// We calculate the new subpage
|
||||
if (e.target.classList.contains('next')) {
|
||||
newPageIndex += 1;
|
||||
} else {
|
||||
newPageIndex -= 1;
|
||||
}
|
||||
|
||||
// We find the new subpage and activate it
|
||||
e.target.parentNode.parentNode.parentNode.children[
|
||||
newPageIndex
|
||||
].classList.add('active');
|
||||
}
|
||||
|
||||
// This adds 1 to the rendered page indexes to make them start from 1 instead of 0
|
||||
document.querySelectorAll('span.current-page-index').forEach((el) => {
|
||||
el.textContent = parseInt(el.textContent) + 1;
|
||||
});
|
||||
|
||||
// Here we bind the functions to the corresponding buttons
|
||||
document.querySelectorAll('button.page-btn').forEach((el) => {
|
||||
el.addEventListener('click', paginationHandler);
|
||||
});
|
||||
|
||||
// mii saving business (animation jank & actual saving)
|
||||
document
|
||||
.querySelector('#saveTab #saveButton')
|
||||
.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
document
|
||||
.querySelector('#saveTab #saveButton')
|
||||
.classList.add('inactive', 'fade-out');
|
||||
document.querySelector('.tabs').style.pointerEvents = 'none';
|
||||
document.querySelector('.mii-comparison.confirmed').style.opacity = 1;
|
||||
document
|
||||
.querySelector('#saveTab p.save-prompt')
|
||||
.classList.add('fade-out');
|
||||
|
||||
setTimeout(() => {
|
||||
document.querySelector(
|
||||
'.mii-comparison.unconfirmed'
|
||||
).style.opacity = 0;
|
||||
}, 500);
|
||||
|
||||
setTimeout(() => {
|
||||
document
|
||||
.querySelector('.mii-comparison.confirmed .old-mii')
|
||||
.classList.add('fade-out');
|
||||
document
|
||||
.querySelector('.mii-comparison.confirmed svg')
|
||||
.classList.add('fade-out');
|
||||
}, 1500);
|
||||
|
||||
setTimeout(() => {
|
||||
document
|
||||
.querySelector('.mii-comparison.confirmed .new-mii-wrapper')
|
||||
.classList.add('centered-mii-img');
|
||||
}, 2000);
|
||||
|
||||
try {
|
||||
const miiData = mii.encode().toString('base64');
|
||||
const tokenType = document.cookie.split('; ').find(row => row.startsWith('token_type=')).split('=')[1];
|
||||
const accessToken = document.cookie.split('; ').find(row => row.startsWith('access_token=')).split('=')[1];
|
||||
|
||||
fetch('https://api.pretendo.cc/v1/user', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `${tokenType} ${decodeURIComponent(accessToken)}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
mii: {
|
||||
name: mii.miiName,
|
||||
primary: 'Y',
|
||||
data: miiData
|
||||
}
|
||||
})
|
||||
}).then(({ status }) => {
|
||||
// TODO - Make this prettier
|
||||
alert('Mii has been updated. It may take some time for the cached image on the website to update');
|
||||
|
||||
if (status === 200) {
|
||||
window.onbeforeunload = null;
|
||||
window.location.assign('/account');
|
||||
}
|
||||
}).catch(console.log);
|
||||
} catch (error) {
|
||||
alert(error);
|
||||
}
|
||||
});
|
||||
@@ -1,54 +0,0 @@
|
||||
/* global Chart -- chart.js */
|
||||
|
||||
document.querySelectorAll('.feature-list-wrapper').forEach((progressListElement) => {
|
||||
// Find and generate all relevant data
|
||||
const percentageOverride = progressListElement.querySelector('canvas.percentage-chart').dataset.percentageoverride;
|
||||
const allFeatureNodes = progressListElement.querySelectorAll('.feature');
|
||||
const allDoneFeatureNodes = progressListElement.querySelectorAll('.feature .done');
|
||||
const allStartedFeatureNodes = progressListElement.querySelectorAll('.feature .ongoing');
|
||||
|
||||
// Use percentage override data attribute if present, else calculate
|
||||
const progressPercentage = Math.round(percentageOverride) || Math.round(Math.min((allDoneFeatureNodes.length + allStartedFeatureNodes.length * 0.5) / allFeatureNodes.length * 100, 100)) || 0;
|
||||
const remainingPercentage = 100 - progressPercentage;
|
||||
|
||||
// Set inner paragraph
|
||||
progressListElement.querySelectorAll('.percentage-label').forEach((p) => {
|
||||
if (progressPercentage === 0) {
|
||||
p.innerText = progressPercentage.toString() + '%';
|
||||
} else {
|
||||
p.innerText = progressPercentage.toString().padStart(2, '0') + '%';
|
||||
}
|
||||
});
|
||||
|
||||
// Create chart
|
||||
const data = [progressPercentage, remainingPercentage];
|
||||
Chart.defaults.plugins.legend = {
|
||||
display: false
|
||||
};
|
||||
Chart.defaults.plugins.tooltip = {
|
||||
enabled: false
|
||||
};
|
||||
|
||||
const isInBrightCard = !!progressListElement.closest('.right.sect');
|
||||
|
||||
new Chart(progressListElement.querySelector('canvas'), {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: ['Done', 'Todo'],
|
||||
datasets: [
|
||||
{
|
||||
data,
|
||||
backgroundColor: isInBrightCard ? ['white', 'rgba(195, 178, 227, 0.5)'] : ['#9D6FF3', '#31365A']
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
elements: {
|
||||
arc: {
|
||||
borderWidth: 0
|
||||
}
|
||||
},
|
||||
cutout: '70%'
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,30 +0,0 @@
|
||||
const passwordInput = document.querySelector('#password');
|
||||
const passwordConfirmInput = document.querySelector('#password_confirm');
|
||||
const tokenInput = document.querySelector('#token');
|
||||
|
||||
document.querySelector('form').addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
|
||||
fetch('https://api.pretendo.cc/v1/reset-password', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
password: passwordInput.value,
|
||||
password_confirm: passwordConfirmInput.value,
|
||||
token: tokenInput.value
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then((body) => {
|
||||
if (body.error) {
|
||||
alert(`Error: ${body.error}. TODO: red error message thing`);
|
||||
} else {
|
||||
alert('Password reset. TODO: reword this and green success');
|
||||
window.location.assign('/account/login');
|
||||
}
|
||||
})
|
||||
.catch(console.log);
|
||||
});
|
||||
@@ -1,114 +0,0 @@
|
||||
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);
|
||||
});
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 592 KiB |
59
server/api/account/checkout.post.ts
Normal file
59
server/api/account/checkout.post.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { usePapr } from '~~/server/utils/papr';
|
||||
import { CheckoutSchema } from '~~/shared/api-types';
|
||||
import type { ApiAccountCheckoutLink } from '~~/shared/api-types';
|
||||
|
||||
export default defineEventHandler(async (event): Promise<ApiAccountCheckoutLink> => {
|
||||
const auth = enforceLoggedIn(event);
|
||||
const papr = await usePapr(event);
|
||||
const stripe = useStripe(event);
|
||||
const config = useRuntimeConfig(event);
|
||||
if (!stripe || !papr) {
|
||||
throw createApiError('INTEGRATION_DISABLED');
|
||||
}
|
||||
|
||||
const body = await readZodBody(event, CheckoutSchema);
|
||||
const { data: searchResults } = await stripe.customers.search({
|
||||
query: `metadata['pnid_pid']:'${auth.pid}'`
|
||||
});
|
||||
let customer = searchResults[0];
|
||||
if (!customer) {
|
||||
customer = await stripe.customers.create({
|
||||
email: auth.email,
|
||||
metadata: {
|
||||
pnid_pid: auth.pid
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ensure PNID always has latest customer ID
|
||||
if (auth.accessLevel >= 2) {
|
||||
throw createApiError('STAFF_NO_DONATE');
|
||||
}
|
||||
await papr.Pnid.updateOne({ pid: auth.pid }, {
|
||||
$set: {
|
||||
'connections.stripe.customer_id': customer.id,
|
||||
'connections.stripe.latest_webhook_timestamp': 0
|
||||
}
|
||||
});
|
||||
|
||||
const priceId = body.priceId;
|
||||
const session = await stripe.checkout.sessions.create({
|
||||
line_items: [
|
||||
{
|
||||
price: priceId,
|
||||
quantity: 1
|
||||
}
|
||||
],
|
||||
customer: customer.id,
|
||||
mode: 'subscription',
|
||||
success_url: new URL('/account?upgrade_success=true', config.public.baseUrl).toString(),
|
||||
cancel_url: new URL('/account?upgrade_success=false', config.public.baseUrl).toString()
|
||||
});
|
||||
if (!session.url) {
|
||||
throw new Error('Failed to create session');
|
||||
}
|
||||
|
||||
return {
|
||||
url: session.url
|
||||
};
|
||||
});
|
||||
6
server/api/account/delete.post.ts
Normal file
6
server/api/account/delete.post.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export default defineEventHandler(async (event): Promise<void> => {
|
||||
const auth = enforceLoggedIn(event);
|
||||
|
||||
const grpc = useApiGrpc(event);
|
||||
await grpc.deleteAccount({ pid: auth.pid });
|
||||
});
|
||||
22
server/api/account/discord-link.get.ts
Normal file
22
server/api/account/discord-link.get.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { useDiscord } from '~~/server/utils/discord';
|
||||
import type { ApiAccountDiscordLink } from '~~/shared/api-types';
|
||||
|
||||
export default defineEventHandler(async (event): Promise<ApiAccountDiscordLink> => {
|
||||
enforceLoggedIn(event);
|
||||
const discord = useDiscord(event);
|
||||
if (!discord) {
|
||||
throw createApiError('INTEGRATION_DISABLED');
|
||||
}
|
||||
|
||||
const redirectUrl = discord.makeCallbackUrl();
|
||||
const url = new URL('https://discord.com/oauth2/authorize');
|
||||
url.searchParams.set('response_type', 'code');
|
||||
url.searchParams.set('client_id', discord.clientId);
|
||||
url.searchParams.set('scope', 'identify');
|
||||
url.searchParams.set('redirect_uri', redirectUrl);
|
||||
url.searchParams.set('prompt', 'scope');
|
||||
url.searchParams.set('integration_type', '1');
|
||||
return {
|
||||
url: url.toString()
|
||||
};
|
||||
});
|
||||
33
server/api/account/discord-unlink.post.ts
Normal file
33
server/api/account/discord-unlink.post.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { removeDiscordMemberSupporterRole, removeDiscordMemberTesterRole } from '~~/server/utils/discord';
|
||||
|
||||
export default defineEventHandler(async (event): Promise<void> => {
|
||||
const auth = enforceLoggedIn(event);
|
||||
const discord = useDiscord(event);
|
||||
if (!discord) {
|
||||
throw createApiError('INTEGRATION_DISABLED');
|
||||
}
|
||||
|
||||
const grpc = useApiGrpcWithToken(event, auth.token);
|
||||
const oldUserData = await grpc.getUserData({});
|
||||
const oldDiscordId = oldUserData.connections?.discord?.id;
|
||||
await grpc.setDiscordConnectionData({
|
||||
id: ''
|
||||
});
|
||||
|
||||
const priceId = oldUserData.connections?.stripe?.priceId;
|
||||
const stripe = useStripe(event);
|
||||
if (stripe) {
|
||||
if (priceId && oldDiscordId) {
|
||||
const price = await stripe.prices.retrieve(priceId);
|
||||
const product = await stripe.products.retrieve(price.product as string);
|
||||
const discordRoleId = product.metadata.discord_role_id;
|
||||
|
||||
if (discordRoleId) {
|
||||
await removeDiscordMemberSupporterRole(discord, oldDiscordId, discordRoleId);
|
||||
}
|
||||
if (product.metadata.beta === 'true') {
|
||||
await removeDiscordMemberTesterRole(discord, oldDiscordId);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
45
server/api/account/tiers.get.ts
Normal file
45
server/api/account/tiers.get.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import type { ApiAccountTiers, TierItem } from '~~/shared/api-types';
|
||||
|
||||
export default defineEventHandler(async (event): Promise<ApiAccountTiers> => {
|
||||
enforceLoggedIn(event);
|
||||
const stripe = useStripe(event);
|
||||
if (!stripe) {
|
||||
throw createApiError('INTEGRATION_DISABLED');
|
||||
}
|
||||
|
||||
const prices = await stripe.prices.list().autoPagingToArray({ limit: 10 });
|
||||
const products = await stripe.products.list().autoPagingToArray({ limit: 10 });
|
||||
|
||||
const tiers: TierItem[] = [];
|
||||
|
||||
for (const product of products) {
|
||||
if (!product.active) {
|
||||
continue;
|
||||
}
|
||||
const price = prices.find(price => price.id === product.default_price);
|
||||
if (!price) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const tierLevel = Number(product.metadata.tier_level ?? '0');
|
||||
const hasDiscordReadPerk = product.metadata.discord_read === 'true';
|
||||
const hasBetaAccessPerk = product.metadata.beta === 'true';
|
||||
|
||||
tiers.push({
|
||||
priceId: price.id,
|
||||
tierLevel,
|
||||
priceCents: price.unit_amount ?? 0,
|
||||
thumbnailUrl: product.images[0] ?? null,
|
||||
name: product.name,
|
||||
description: product.description,
|
||||
perks: {
|
||||
discordRead: hasDiscordReadPerk,
|
||||
beta: hasBetaAccessPerk
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
tiers
|
||||
};
|
||||
});
|
||||
33
server/api/account/unsubscribe.post.ts
Normal file
33
server/api/account/unsubscribe.post.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { usePapr } from '~~/server/utils/papr';
|
||||
|
||||
export default defineEventHandler(async (event): Promise<void> => {
|
||||
const auth = enforceLoggedIn(event);
|
||||
const papr = await usePapr(event);
|
||||
const stripe = useStripe(event);
|
||||
if (!stripe || !papr) {
|
||||
throw createApiError('INTEGRATION_DISABLED');
|
||||
}
|
||||
|
||||
if (!auth.stripeSubscriptionId) {
|
||||
return; // No subscription, do nothing
|
||||
}
|
||||
|
||||
await stripe.subscriptions.cancel(auth.stripeSubscriptionId);
|
||||
|
||||
let newAccessLevel = 0;
|
||||
if (auth.accessLevel >= 2) {
|
||||
newAccessLevel = auth.accessLevel; // Staff shouldn't be downgraded on unsubscribe
|
||||
}
|
||||
|
||||
await papr.Pnid.updateOne({ pid: auth.pid }, {
|
||||
$unset: {
|
||||
'connections.stripe.subscription_id': 1,
|
||||
'connections.stripe.price_id': 1,
|
||||
'connections.stripe.tier_name': 1
|
||||
},
|
||||
$set: {
|
||||
'connections.stripe.tier_level': 0,
|
||||
'access_level': newAccessLevel
|
||||
}
|
||||
});
|
||||
});
|
||||
19
server/api/account/update.patch.ts
Normal file
19
server/api/account/update.patch.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { AccountUpdateSchema } from '~~/shared/api-types';
|
||||
|
||||
export default defineEventHandler(async (event): Promise<void> => {
|
||||
const body = await readZodBody(event, AccountUpdateSchema);
|
||||
const auth = enforceLoggedIn(event);
|
||||
const apiFetch = useHttpApi(event, auth.token);
|
||||
|
||||
// There's no equivalent GRPC endpoint to use, so we're using the HTTP api
|
||||
await apiFetch('/v1/user', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
mii: body.mii,
|
||||
environment: body.environment
|
||||
}),
|
||||
headers: {
|
||||
'Content-type': 'application/json'
|
||||
}
|
||||
});
|
||||
});
|
||||
16
server/api/auth/forgot-password.post.ts
Normal file
16
server/api/auth/forgot-password.post.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { hcaptchaVerify } from '~~/server/utils/hcaptcha';
|
||||
import { ForgotPasswordSchema } from '~~/shared/api-types';
|
||||
|
||||
export default defineEventHandler(async (event): Promise<void> => {
|
||||
const body = await readZodBody(event, ForgotPasswordSchema);
|
||||
const grpc = useApiGrpc(event);
|
||||
|
||||
const captchaResult = await hcaptchaVerify(event, body.captchaResponse);
|
||||
if (!captchaResult) {
|
||||
throw createApiError('INVALID_CAPTCHA');
|
||||
}
|
||||
|
||||
await grpc.forgotPassword({
|
||||
emailAddressOrUsername: body.emailOrPassword
|
||||
});
|
||||
});
|
||||
44
server/api/auth/login.post.ts
Normal file
44
server/api/auth/login.post.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { ClientError } from 'nice-grpc';
|
||||
import { LoginSchema } from '#shared/api-types';
|
||||
import type { ApiErrorCodes } from '~~/shared/errors';
|
||||
import type { ApiAuthLogin } from '#shared/api-types';
|
||||
|
||||
const bucket = createRatelimitBucket({
|
||||
id: 'login',
|
||||
points: 30,
|
||||
durationSec: 5 * 60, // 5 minutes
|
||||
blockDurationSec: 1 * 60 * 60 // 1 hour
|
||||
});
|
||||
|
||||
const errors: Record<string, ApiErrorCodes> = {
|
||||
'INVALID_ARGUMENT: User not found': 'INVALID_USERNAME',
|
||||
'INVALID_ARGUMENT: Password is incorrect': 'INVALID_PASSWORD',
|
||||
'UNAUTHENTICATED: Account has been deleted': 'ACCOUNT_DELETED'
|
||||
};
|
||||
|
||||
export default defineEventHandler(async (event): Promise<ApiAuthLogin> => {
|
||||
await enforceRatelimit(event, bucket);
|
||||
const body = await readZodBody(event, LoginSchema);
|
||||
const grpc = useApiGrpc(event);
|
||||
|
||||
try {
|
||||
const res = await grpc.login({
|
||||
username: body.username,
|
||||
password: body.password,
|
||||
grantType: 'password'
|
||||
});
|
||||
|
||||
return {
|
||||
accessToken: res.accessToken,
|
||||
refreshToken: res.refreshToken
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof ClientError) {
|
||||
const errorCode = errors[error.details];
|
||||
if (errorCode) {
|
||||
throw createApiError(errorCode);
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
28
server/api/auth/me-connections.get.ts
Normal file
28
server/api/auth/me-connections.get.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { getDiscordUser } from '~~/server/utils/discord';
|
||||
import type { GetApiAuthMeConections } from '~~/shared/api-types';
|
||||
import type { DiscordUser } from '~~/server/utils/discord';
|
||||
|
||||
export default defineEventHandler(async (event): Promise<GetApiAuthMeConections> => {
|
||||
const auth = enforceLoggedIn(event);
|
||||
const discord = useDiscord(event);
|
||||
|
||||
const apiGrpc = useApiGrpcWithToken(event, auth.token);
|
||||
const data = await apiGrpc.getUserData({});
|
||||
let discordUser: DiscordUser | null = null;
|
||||
if (data.connections?.discord?.id && discord) {
|
||||
discordUser = await getDiscordUser(discord, data.connections.discord.id);
|
||||
}
|
||||
|
||||
return {
|
||||
pid: data.pid,
|
||||
discord: discordUser
|
||||
? {
|
||||
id: discordUser.id,
|
||||
username: discordUser.username,
|
||||
discriminator: discordUser.discriminator,
|
||||
avatar: discordUser.avatar ?? null,
|
||||
avatarUrl: discordUser.avatar ? `https://cdn.discordapp.com/avatars/${discordUser.id}/${discordUser.avatar}.png` : null
|
||||
}
|
||||
: null
|
||||
};
|
||||
});
|
||||
36
server/api/auth/me.get.ts
Normal file
36
server/api/auth/me.get.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import type { GetApiAuthMe } from '#shared/api-types';
|
||||
|
||||
export default defineEventHandler(async (event): Promise<GetApiAuthMe> => {
|
||||
const config = useRuntimeConfig(event);
|
||||
const auth = enforceLoggedIn(event);
|
||||
|
||||
const apiGrpc = useApiGrpcWithToken(event, auth.token);
|
||||
const data = await apiGrpc.getUserData({ });
|
||||
return {
|
||||
pid: data.pid,
|
||||
username: data.username,
|
||||
accessLevel: data.accessLevel,
|
||||
birthday: data.birthday,
|
||||
gender: data.gender,
|
||||
country: data.country,
|
||||
timezone: data.timezone,
|
||||
emailAddress: data.emailAddress,
|
||||
serverAccessLevel: data.serverAccessLevel as any,
|
||||
discordId: data.connections?.discord?.id ?? null,
|
||||
stripeTier: data.connections?.stripe?.subscriptionId
|
||||
? {
|
||||
subscriptionId: data.connections.stripe.subscriptionId,
|
||||
priceId: data.connections.stripe.priceId ?? '',
|
||||
tierName: data.connections.stripe.tierName ?? '',
|
||||
tierLevel: data.connections.stripe.tierLevel ?? ''
|
||||
}
|
||||
: null,
|
||||
mii: data.mii
|
||||
? {
|
||||
imageUrl: `${config.public.cdnBaseUrl}/mii/${data.pid}/normal_face.png`,
|
||||
name: data.mii.name,
|
||||
data: data.mii.data
|
||||
}
|
||||
: null
|
||||
};
|
||||
});
|
||||
35
server/api/auth/refresh.post.ts
Normal file
35
server/api/auth/refresh.post.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { ClientError } from 'nice-grpc';
|
||||
import { RefreshSchema } from '#shared/api-types';
|
||||
import type { ApiAuthLogin } from '#shared/api-types';
|
||||
|
||||
const bucket = createRatelimitBucket({
|
||||
id: 'refresh',
|
||||
points: 10,
|
||||
durationSec: 1 * 60, // 1 minute
|
||||
blockDurationSec: 1 * 60 * 60 // 1 hour
|
||||
});
|
||||
|
||||
export default defineEventHandler(async (event): Promise<ApiAuthLogin> => {
|
||||
await enforceRatelimit(event, bucket);
|
||||
const body = await readZodBody(event, RefreshSchema);
|
||||
const grpc = useApiGrpc(event);
|
||||
|
||||
try {
|
||||
const res = await grpc.login({
|
||||
refreshToken: body.token,
|
||||
grantType: 'refresh_token'
|
||||
});
|
||||
|
||||
return {
|
||||
accessToken: res.accessToken,
|
||||
refreshToken: res.refreshToken
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof ClientError) {
|
||||
if (error.details === 'INVALID_ARGUMENT: Invalid or missing refresh token') {
|
||||
throw createApiError('UNAUTHENTICATED');
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
78
server/api/auth/register.post.ts
Normal file
78
server/api/auth/register.post.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { ClientError } from 'nice-grpc';
|
||||
import { RegisterSchema } from '#shared/api-types';
|
||||
import type { ApiErrorCodes } from '~~/shared/errors';
|
||||
import type { ApiAuthLogin } from '#shared/api-types';
|
||||
|
||||
const bucket = createRatelimitBucket({
|
||||
id: 'register',
|
||||
points: 15,
|
||||
durationSec: 5 * 60, // 5 minutes
|
||||
blockDurationSec: 1 * 60 * 60 // 1 hour
|
||||
});
|
||||
|
||||
const errors: Record<string, ApiErrorCodes> = {
|
||||
'INVALID_ARGUMENT: Captcha verification failed': 'INVALID_CAPTCHA',
|
||||
'INVALID_ARGUMENT: Invalid email address': 'INVALID_EMAIL',
|
||||
'INVALID_ARGUMENT: Username is too short': 'USERNAME_TOO_SHORT',
|
||||
'INVALID_ARGUMENT: Username is too long': 'USERNAME_TOO_LONG',
|
||||
'INVALID_ARGUMENT: Username contains invalid characters': 'USERNAME_INVALID_CHARS',
|
||||
'INVALID_ARGUMENT: Username cannot begin with punctuation characters': 'USERNAME_INVALID_CHARS',
|
||||
'INVALID_ARGUMENT: Username cannot end with punctuation characters': 'USERNAME_INVALID_CHARS',
|
||||
'INVALID_ARGUMENT: Two or more punctuation characters cannot be used in a row': 'USERNAME_INVALID_CHARS',
|
||||
'INVALID_ARGUMENT: PNID already in use': 'USERNAME_IN_USE',
|
||||
'INVALID_ARGUMENT: Mii name too long': 'MIINAME_TOO_LONG',
|
||||
'INVALID_ARGUMENT: Password must be between 6 and 16 characters long': 'INVALID_PASSWORD_INPUT',
|
||||
'INVALID_ARGUMENT: Password cannot be the same as username': 'INVALID_PASSWORD_INPUT',
|
||||
'INVALID_ARGUMENT: Password must have combination of letters, numbers, and/or punctuation characters': 'INVALID_PASSWORD_INPUT',
|
||||
'INVALID_ARGUMENT: Password may not have 3 repeating characters': 'INVALID_PASSWORD_INPUT',
|
||||
'INVALID_ARGUMENT: Passwords do not match': 'INVALID_PASSWORD_NO_MATCH'
|
||||
};
|
||||
|
||||
function getCutoffDateForAge(today: Date, age: number) {
|
||||
return new Date(today.getFullYear() - age, today.getMonth(), today.getDate());
|
||||
}
|
||||
|
||||
function assertAge(birthDate: string | undefined) {
|
||||
if (!birthDate) {
|
||||
throw createApiError('INVALID_INPUT');
|
||||
}
|
||||
const date = new Date(birthDate);
|
||||
const today = new Date();
|
||||
|
||||
// Prevent users below 13
|
||||
if (date > getCutoffDateForAge(today, 13)) {
|
||||
throw createApiError('UNDER_THIRTEEN');
|
||||
}
|
||||
}
|
||||
|
||||
export default defineEventHandler(async (event): Promise<ApiAuthLogin> => {
|
||||
await enforceRatelimit(event, bucket);
|
||||
const body = await readZodBody(event, RegisterSchema);
|
||||
const grpc = useApiGrpc(event);
|
||||
assertAge(body.birthday);
|
||||
|
||||
try {
|
||||
// TODO Add ip
|
||||
const res = await grpc.register({
|
||||
email: body.email,
|
||||
miiName: body.miiName,
|
||||
captchaResponse: body.captchaResponse,
|
||||
username: body.username,
|
||||
password: body.password,
|
||||
passwordConfirm: body.passwordConfirm
|
||||
});
|
||||
|
||||
return {
|
||||
accessToken: res.accessToken,
|
||||
refreshToken: res.refreshToken
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof ClientError) {
|
||||
const errorCode = errors[error.details];
|
||||
if (errorCode) {
|
||||
throw createApiError(errorCode);
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
19
server/api/auth/reset-password.post.ts
Normal file
19
server/api/auth/reset-password.post.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { ResetPasswordSchema } from '~~/shared/api-types';
|
||||
|
||||
export default defineEventHandler(async (event): Promise<void> => {
|
||||
const body = await readZodBody(event, ResetPasswordSchema);
|
||||
const apiFetch = useHttpApi(event);
|
||||
|
||||
// The GRPC version requires a login token, which the user doesnt have when resetting password, so we're using the HTTP api
|
||||
await apiFetch('/v1/reset-password', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
password: body.password,
|
||||
password_confirm: body.passwordConfirm,
|
||||
token: body.resetToken
|
||||
}),
|
||||
headers: {
|
||||
'Content-type': 'application/json'
|
||||
}
|
||||
});
|
||||
});
|
||||
42
server/api/progress.get.ts
Normal file
42
server/api/progress.get.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { getGithubProjects } from '../utils/getGithubProgress';
|
||||
import { getStripeDonations } from '../utils/getStripeDonations';
|
||||
import { useCacher } from '../utils/cache';
|
||||
import type { GetProgress, ProgressItem } from '#shared/api-types';
|
||||
|
||||
const donationGoalCents = 3000 * 100;
|
||||
|
||||
export default defineEventHandler(async (event): Promise<GetProgress> => {
|
||||
const octokit = useOctokit(event);
|
||||
const stripe = useStripe(event);
|
||||
const cacher = useCacher(event);
|
||||
const donationData = await getStripeDonations(cacher, stripe);
|
||||
const { projects } = await getGithubProjects(cacher, octokit);
|
||||
|
||||
const items: ProgressItem[] = projects.map((v) => {
|
||||
const totalTasks = v.tasks.length;
|
||||
const completedTasks = v.tasks.filter(v => v.status === 'completed').length;
|
||||
const halfCompletedTasks = v.tasks.filter(v => v.status === 'inprogress').length;
|
||||
const percentage = Math.round((completedTasks + (halfCompletedTasks * 0.5)) / totalTasks * 100);
|
||||
|
||||
return {
|
||||
title: v.title,
|
||||
githubUrl: v.url,
|
||||
completion: percentage,
|
||||
tasks: v.tasks.map(task => ({
|
||||
status: task.status,
|
||||
title: task.title
|
||||
}))
|
||||
};
|
||||
});
|
||||
const summedCompletion = items.reduce((a, v) => a + v.completion, 0);
|
||||
const completionPercentage = Math.round(summedCompletion / items.length);
|
||||
|
||||
return {
|
||||
completion: completionPercentage,
|
||||
donations: {
|
||||
currentCents: donationData.totalDonationsCents,
|
||||
goalCents: donationGoalCents
|
||||
},
|
||||
items
|
||||
};
|
||||
});
|
||||
29
server/middleware/auth.ts
Normal file
29
server/middleware/auth.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
export default defineEventHandler(async (event) => {
|
||||
const authHeader = getRequestHeader(event, 'authorization');
|
||||
setAuthContext(event, null);
|
||||
if (authHeader) {
|
||||
try {
|
||||
const [type, token] = authHeader.split(' ', 2);
|
||||
if (type !== 'Bearer') {
|
||||
throw new Error('Invalid token type');
|
||||
}
|
||||
if (!token) {
|
||||
throw new Error('Invalid token');
|
||||
}
|
||||
const grpc = useLegacyApiGrpcWithToken(event, token);
|
||||
const userData = await grpc.getUserData({});
|
||||
|
||||
setAuthContext(event, {
|
||||
pid: userData.pid,
|
||||
username: userData.username,
|
||||
token: token,
|
||||
accessLevel: userData.accessLevel,
|
||||
email: userData.emailAddress,
|
||||
stripeSubscriptionId: userData.connections?.stripe?.subscriptionId ?? null
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Failed to request user data: ', err);
|
||||
return; // Continue like nothing happened, further steps will validate if authed
|
||||
}
|
||||
}
|
||||
});
|
||||
79
server/routes/account/connect/discord.get.ts
Normal file
79
server/routes/account/connect/discord.get.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { assignDiscordMemberSupporterRole, assignDiscordMemberTesterRole } from '~~/server/utils/discord';
|
||||
|
||||
type DiscordTokenResponse = {
|
||||
access_token: string;
|
||||
token_type: string;
|
||||
expires_in: number;
|
||||
refresh_token: string;
|
||||
scope: string;
|
||||
};
|
||||
|
||||
type DiscordUserResponse = {
|
||||
user?: {
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
|
||||
// Discord oauth callback
|
||||
export default defineEventHandler(async (event) => {
|
||||
const discord = useDiscord(event);
|
||||
if (!discord) {
|
||||
return sendRedirect(event, '/');
|
||||
}
|
||||
|
||||
const discordFetch = $fetch.create({
|
||||
baseURL: discord.baseUrl
|
||||
});
|
||||
const accessTokenCookie = getCookie(event, 'access_token');
|
||||
const query = getQuery(event);
|
||||
|
||||
const authCode = query.code?.toString();
|
||||
if (!authCode || !accessTokenCookie) {
|
||||
return sendRedirect(event, '/');
|
||||
}
|
||||
|
||||
const tokens = await discordFetch<DiscordTokenResponse>('/oauth2/token', {
|
||||
method: 'POST',
|
||||
body: new URLSearchParams({
|
||||
grant_type: 'authorization_code',
|
||||
client_id: discord.clientId,
|
||||
client_secret: discord.clientSecret,
|
||||
code: authCode,
|
||||
redirect_uri: discord.makeCallbackUrl()
|
||||
})
|
||||
});
|
||||
const authInfo = await discordFetch<DiscordUserResponse>('/oauth2/@me', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${tokens.access_token}`
|
||||
}
|
||||
});
|
||||
if (!authInfo.user) {
|
||||
return sendRedirect(event, '/account?discord_link_success=false'); // No identify scope
|
||||
}
|
||||
|
||||
const discordId = authInfo.user.id;
|
||||
const grpc = useApiGrpcWithToken(event, accessTokenCookie ?? '');
|
||||
await grpc.setDiscordConnectionData({
|
||||
id: discordId
|
||||
});
|
||||
|
||||
const userData = await grpc.getUserData({});
|
||||
const priceId = userData.connections?.stripe?.priceId;
|
||||
const stripe = useStripe(event);
|
||||
if (stripe && priceId) {
|
||||
if (priceId) {
|
||||
const price = await stripe.prices.retrieve(priceId);
|
||||
const product = await stripe.products.retrieve(price.product as string);
|
||||
const discordRoleId = product.metadata.discord_role_id;
|
||||
|
||||
if (discordRoleId) {
|
||||
await assignDiscordMemberSupporterRole(discord, discordId, discordRoleId);
|
||||
}
|
||||
if (product.metadata.beta === 'true') {
|
||||
await assignDiscordMemberTesterRole(discord, discordId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sendRedirect(event, '/account?discord_link_success=true');
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user