Rewrite Parity (#647)
* Rewrite Parity * Update SEO, fix modals, add dashes to changelog * Edit create version title * Cache tags, SEO for search/partial noscript support, notifications fix * Deploy? * Fix vercel config * Fix it again * Finish user editing * Remove broken docker build * Switch reports to modals * Update project card * Navbar line animation in most places * Add chips * Move to navlink query params * remove autogen file * Add copy code * Fix webkit text box outlines, port report modal * Update error page * Switch to avatar component * Make keyboard nav work * Fix team member spacing * improve project ID display (#676) * Bug fixes * Update OG site title * More fixes * Design tweaks * Fix card wrapping on mobile * Darken light theme color a little * Sidebar navigation for settings, notifications, and moderation * Change follow icon from a heart to a bell * Revert "Change follow icon from a heart to a bell" This reverts commit e30b46ec5d93c57df847be88eba123c7419dd03b. * Change follows icon in settings * AaaaUUUUUUUGghghhhhhhhh * Project sidebar transparent button animations * Update file input button styling and change icon remove button text * Fix environments filter condition being inverted * Remove -> revert * Improve readability of warning banners on light mode * Fix mobile menu button colors * Clean up notifications page more * Creator dashboard and monetization work * Add processing fees declarations and acknowledgement box * Beta badges * Downgrade Nuxt Vercel Builder * Update the style of button groups to be more consistent * More button consistency * Remove desktop navbar on mobile * Update home page progress indicators * Fix page jumping (Thanks @stairman06) * Make checkbox checked style consistent with other selection indicators * More home page updates * Properly reset NavRows * Move filters menu on mobile * Stylized checkbox updated to match active styling * Filters icon * Respect prefers-reduced-motion * Add most backend payouts changes (untested) * Finish tested payouts code * Allow monetization unenrolling * No longer use brand color for active highlights on standard nav elements * More consistent button group on project page * Rounded tables * Fix some things (#716) * Team member fixes + re-add changelog/versions stuff * Remove dummy data * The great CSS refactor * Remove commented out css * Give modals the legacy label styles and update profile edit labels * Fix active chip size * Remove shadow from selected chip * Require email set for CMP * Update styles of notifications to universal-card * Equivalent exchange, trading some jank for some less bad jank * Fix all gallery buttons being missing when there is only 1 image * Update project creation modal * Make beta badge less bright * Beta badge heading styling * Update withdraw processing fees info * Remove redundant label * be * Fix inverted logic * 2% is 0.02 * Add toggle to turn off alpha modpacks banner * Why warning button? * Add more footer links (#719) * Add more footer links * Move twitter * Make items on user pages less comically large and move ad above navigation * Bump text down a little on home page * Update favicon colors * Remove task list package and change default description to use bullet points * I don't remember why I made this important but let's not * Ah, yes * this doesn't actually need to be important * Align items in input groups * Adjust some spacings and clear creation modal on opening * Versions now clickable * Add link to edit page to default description * Improve monetization information text * Make wrapped text inputs not shrink * Make chips work better * smol margin on clear mod message button * Allow non-authenticated users to access settings * Remove settings anchors * Fix versions page button style on firefox * Add advanced rendering toggle * Update slug input and icon card in project edit page * Legal sidebar * h1 at beginning of description no longer has top margin * Use universal card for legal pages * Update email addresses on legal pages * Update various page titles and descriptions for consistency * Various fixes and consolidation to API URL retrieval Prevents a bug where it's possible to generate the tags under one API, switch the API, and still have tags leftover from the old API Also finally fixes staging URL being jank * Make the theme button show regardless of login state Also remove the change theme from the user dropdown because it's very redundant with the several other ways of changing theme * Make mobile profile dropdown ordering consistent with desktop * Change the base url back * Revert "Change the base url back" This reverts commit c1da89fddb83776b39f626eab33c8dc67f8a75e4. * constantize * Tiny fixes (#722) * Box-shadow chip outlines * Show settings when signed out * mods -> projects * space * Beta badge border * Slug input overflow fix, scrollable * 🙈 it will all be okay 🙊 this is just temporary 🙉 😭😭 forgive me * Fix minor bugs * fix moderation page * More fixes * Temp fix for download button * BEGONE TABLES * Fix download button Co-authored-by: Ryan Cao <70191398+ryanccn@users.noreply.github.com> Co-authored-by: Prospector <prospectordev@gmail.com> Co-authored-by: stairman06 <36215135+stairman06@users.noreply.github.com> Co-authored-by: triphora <emmaffle@modrinth.com>
56
.github/workflows/docker-compile.yml
vendored
@ -1,56 +0,0 @@
|
||||
name: docker-build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
-
|
||||
name: Docker meta
|
||||
id: docker_meta
|
||||
uses: crazy-max/ghaction-docker-meta@v1
|
||||
with:
|
||||
images: ghcr.io/modrinth/knossos
|
||||
-
|
||||
name: Get branch name
|
||||
id: vars
|
||||
run: echo ::set-output name=short_ref::${GITHUB_REF#refs/*/}
|
||||
-
|
||||
name: Get short SHA
|
||||
id: slug
|
||||
run: echo "::set-output name=sha8::$(echo ${GITHUB_SHA} | cut -c1-8)"
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
-
|
||||
name: Login to GHCR
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.docker_meta.outputs.tags }}
|
||||
labels: ${{ steps.docker_meta.outputs.labels }}
|
||||
build-args: |
|
||||
VERSION_ID=${{ steps.vars.outputs.short_ref }}-${{ steps.slug.outputs.sha8 }}
|
||||
14
.github/workflows/regenerate.yml
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
name: Regenerate tags
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '*/30 * * * *'
|
||||
|
||||
jobs:
|
||||
regenerate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Re-deploy site
|
||||
run: |
|
||||
curl -X POST \
|
||||
${{ secrets.VERCEL_DEPLOY_URL }}
|
||||
3
.gitignore
vendored
@ -1,3 +1,6 @@
|
||||
generated/
|
||||
!.gitkeep
|
||||
|
||||
# Created by .ignore support plugin (hsz.mobi)
|
||||
### Node template
|
||||
# Logs
|
||||
|
||||
@ -9,6 +9,7 @@ assets/images/text-logo-white.svg <br />
|
||||
static/favicon.ico <br />
|
||||
components/ui/search/LogoAnimated.vue <br />
|
||||
assets/images/landing.svg <br />
|
||||
assets/images/404.svg <br />
|
||||
|
||||
## Logo Licenses
|
||||
All rights reserved. ©2020-2022 Rinth, Inc.
|
||||
|
||||
74
assets/images/404.svg
Normal file
@ -0,0 +1,74 @@
|
||||
<svg width="100%" height="100%" viewBox="0 0 3247 1234" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.5;">
|
||||
<g transform="matrix(1,0,0,1,-149.406,-1192.33)">
|
||||
<g transform="matrix(1,0,0,1,0,-0.0649902)">
|
||||
<g transform="matrix(1,0,0,1,-190.112,0.0649902)">
|
||||
<g transform="matrix(1549.76,0,0,1549.76,285.034,2355.46)">
|
||||
<path d="M0.386,-0L0.526,-0L0.526,-0.129L0.619,-0.129L0.619,-0.247L0.526,-0.247L0.526,-0.705L0.318,-0.705L0.035,-0.252L0.035,-0.129L0.386,-0.129L0.386,-0ZM0.164,-0.243L0.164,-0.247L0.385,-0.598L0.389,-0.598L0.389,-0.243L0.164,-0.243Z" style="fill-rule:nonzero;" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,0,-0.0649902)">
|
||||
<g transform="matrix(1,0,0,1,2151.95,0.0649902)">
|
||||
<g transform="matrix(1549.76,0,0,1549.76,285.034,2355.46)">
|
||||
<path d="M0.386,-0L0.526,-0L0.526,-0.129L0.619,-0.129L0.619,-0.247L0.526,-0.247L0.526,-0.705L0.318,-0.705L0.035,-0.252L0.035,-0.129L0.386,-0.129L0.386,-0ZM0.164,-0.243L0.164,-0.247L0.385,-0.598L0.389,-0.598L0.389,-0.243L0.164,-0.243Z" style="fill-rule:nonzero;" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,0,-0.0649902)">
|
||||
<g transform="matrix(1.04827,0,0,1.04827,1374.32,1499.56)">
|
||||
<path d="M309.082,335.571L283.216,266.381L336.966,211.169L404.9,196.531L424.57,220.74L393.254,252.46L365.941,261.052L346.425,281.11L355.987,307.719L375.387,328.306L402.745,321.031L422.216,299.648L464.729,286.185L477.395,314.677L433.529,368.46L360.02,391.735L309.082,335.571Z" />
|
||||
</g>
|
||||
<g transform="matrix(1.6241,0,0,1.6241,3325.29,75.5995)">
|
||||
<path d="M-799.272,1141.68C-827.119,1200.8 -887.255,1241.76 -956.882,1241.76C-1048.32,1241.76 -1123.39,1171.12 -1130.46,1081.49" style="fill:none;stroke:currentcolor;stroke-width:33.35px;" />
|
||||
</g>
|
||||
<g transform="matrix(1.6241,0,0,1.6241,3325.29,75.5995)">
|
||||
<path d="M-1129.95,1048.4C-1120.36,961.34 -1046.48,893.522 -956.882,893.522C-860.784,893.522 -782.765,971.541 -782.765,1067.64C-782.765,1082.32 -784.586,1096.58 -788.016,1110.21" style="fill:none;stroke:currentcolor;stroke-width:33.35px;" />
|
||||
</g>
|
||||
<g transform="matrix(-0.578174,2.15777,-2.15777,-0.578174,3522.29,4490.98)">
|
||||
<path d="M-799.272,1141.68C-827.119,1200.8 -887.255,1241.76 -956.882,1241.76C-1048.32,1241.76 -1123.39,1171.12 -1130.46,1081.49" style="fill:none;stroke:currentcolor;stroke-width:24.25px;" />
|
||||
</g>
|
||||
<g transform="matrix(-0.578174,2.15777,-2.15777,-0.578174,3522.29,4490.98)">
|
||||
<path d="M-1129.95,1048.4C-1120.36,961.34 -1046.48,893.522 -956.882,893.522C-860.784,893.522 -782.765,971.541 -782.765,1067.64C-782.765,1082.32 -784.586,1096.58 -788.016,1110.21" style="fill:none;stroke:currentcolor;stroke-width:24.25px;" />
|
||||
</g>
|
||||
<g transform="matrix(2.06597,1.89638,-1.89638,2.06597,5772.64,1417.93)">
|
||||
<path d="M-799.272,1141.68C-827.119,1200.8 -887.255,1241.76 -956.882,1241.76C-1048.32,1241.76 -1123.39,1171.12 -1130.46,1081.49" style="fill:none;stroke:currentcolor;stroke-width:19.32px;" />
|
||||
</g>
|
||||
<g transform="matrix(2.06597,1.89638,-1.89638,2.06597,5772.64,1417.93)">
|
||||
<path d="M-1129.95,1048.4C-1120.36,961.34 -1046.48,893.522 -956.882,893.522C-860.784,893.522 -782.765,971.541 -782.765,1067.64C-782.765,1082.32 -784.586,1096.58 -788.016,1110.21" style="fill:none;stroke:currentcolor;stroke-width:19.32px;" />
|
||||
</g>
|
||||
<g transform="matrix(-3.26901,0.875928,-0.875928,-3.26901,-420.338,6137.6)">
|
||||
<path d="M-799.272,1141.68C-827.119,1200.8 -887.255,1241.76 -956.882,1241.76C-1048.32,1241.76 -1123.39,1171.12 -1130.46,1081.49" style="fill:none;stroke:currentcolor;stroke-width:16.01px;" />
|
||||
</g>
|
||||
<g transform="matrix(-3.26901,0.875928,-0.875928,-3.26901,-420.338,6137.6)">
|
||||
<path d="M-1129.95,1048.4C-1120.36,961.34 -1046.48,893.522 -956.882,893.522C-860.784,893.522 -782.765,971.541 -782.765,1067.64C-782.765,1082.32 -784.586,1096.58 -788.016,1110.21" style="fill:none;stroke:currentcolor;stroke-width:16.01px;" />
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,2728.09,741.915)">
|
||||
<path d="M-904.543,901.539C-833.986,923.78 -782.765,989.775 -782.765,1067.64C-782.765,1137.58 -824.09,1197.94 -883.633,1225.62" style="fill:none;stroke:currentcolor;stroke-width:54.17px;" />
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,2728.09,741.915)">
|
||||
<path d="M-937.34,1240.67C-943.756,1241.39 -950.276,1241.76 -956.882,1241.76C-1052.98,1241.76 -1131,1163.74 -1131,1067.64C-1131,971.541 -1052.98,893.522 -956.882,893.522" style="fill:none;stroke:currentcolor;stroke-width:54.17px;" />
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,2728.09,741.915)">
|
||||
<path d="M-904.543,901.539C-833.986,923.78 -782.765,989.775 -782.765,1067.64C-782.765,1137.58 -824.09,1197.94 -883.633,1225.62" style="fill:none;stroke:currentcolor;stroke-width:54.17px;" />
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,2728.09,741.915)">
|
||||
<path d="M-937.34,1240.67C-943.756,1241.39 -950.276,1241.76 -956.882,1241.76C-1052.98,1241.76 -1131,1163.74 -1131,1067.64C-1131,971.541 -1052.98,893.522 -956.882,893.522" style="fill:none;stroke:currentcolor;stroke-width:54.17px;" />
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,2730.59,729.536)">
|
||||
<path d="M-791.26,1034.59L-686.335,1006.23" style="fill:none;stroke:currentcolor;stroke-width:54.17px;" />
|
||||
</g>
|
||||
<g transform="matrix(0.730214,-0.361316,0.535157,0.830095,1995.93,464.536)">
|
||||
<path d="M-791.26,1034.59L-686.335,1006.23" style="fill:none;stroke:currentcolor;stroke-width:56.77px;" />
|
||||
</g>
|
||||
<g transform="matrix(0.588836,0.702962,-0.731171,0.673626,3318.16,1883.42)">
|
||||
<path d="M-791.26,1034.59L-686.335,1006.23" style="fill:none;stroke:currentcolor;stroke-width:55.35px;" />
|
||||
</g>
|
||||
<g transform="matrix(0.260851,-0.884338,0.965379,0.238953,979.167,372.085)">
|
||||
<path d="M-791.26,1034.59L-686.335,1006.23" style="fill:none;stroke:currentcolor;stroke-width:55.28px;" />
|
||||
</g>
|
||||
<g transform="matrix(1.94443,-0.569179,-0.569179,1.34303,4301.69,-217.31)">
|
||||
<path d="M-1202.21,1224.92L-999.087,1102.51" style="fill:none;stroke:currentcolor;stroke-width:48.54px;" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.3 KiB |
7
assets/images/utils/bell-ring.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path>
|
||||
<path d="M13.73 21a2 2 0 0 1-3.46 0"></path>
|
||||
<path d="M2 8c0-2.2.7-4.3 2-6"></path>
|
||||
<path d="M22 8a10 10 0 0 0-2-6"></path>
|
||||
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 383 B |
5
assets/images/utils/bell.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path>
|
||||
<path d="M13.73 21a2 2 0 0 1-3.46 0"></path>
|
||||
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 300 B |
7
assets/images/utils/chart.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M3 3v18h18"></path>
|
||||
<path d="M18 17V9"></path>
|
||||
<path d="M13 17V5"></path>
|
||||
<path d="M8 17v-3"></path>
|
||||
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 307 B |
1
assets/images/utils/clipboard-copy.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3" /></svg>
|
||||
|
After Width: | Height: | Size: 299 B |
5
assets/images/utils/currency.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="12" y1="2" x2="12" y2="22"></line>
|
||||
<path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"></path>
|
||||
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 306 B |
7
assets/images/utils/dashboard.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="3" y="3" width="7" height="9"></rect>
|
||||
<rect x="14" y="3" width="7" height="5"></rect>
|
||||
<rect x="14" y="12" width="7" height="9"></rect>
|
||||
<rect x="3" y="16" width="7" height="5"></rect>
|
||||
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 389 B |
1
assets/images/utils/filter.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-filter"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"></polygon></svg>
|
||||
|
After Width: | Height: | Size: 290 B |
7
assets/images/utils/heart-handshake.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M20.42 4.58a5.4 5.4 0 0 0-7.65 0l-.77.78-.77-.78a5.4 5.4 0 0 0-7.65 0C1.46 6.7 1.33 10.28 4 13l8 8 8-8c2.67-2.72 2.54-6.3.42-8.42z"></path>
|
||||
<path d="M12 5.36 8.87 8.5a2.13 2.13 0 0 0 0 3h0a2.13 2.13 0 0 0 3 0l2.26-2.21a3 3 0 0 1 4.22 0l2.4 2.4"></path>
|
||||
<path d="m18 15-2-2"></path>
|
||||
<path d="m15 18-2-2"></path>
|
||||
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 517 B |
9
assets/images/utils/list.svg
Normal file
@ -0,0 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="3" y="14" width="7" height="7"></rect>
|
||||
<rect x="3" y="3" width="7" height="7"></rect>
|
||||
<line x1="14" y1="4" x2="21" y2="4"></line>
|
||||
<line x1="14" y1="9" x2="21" y2="9"></line>
|
||||
<line x1="14" y1="15" x2="21" y2="15"></line>
|
||||
<line x1="14" y1="20" x2="21" y2="20"></line>
|
||||
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 476 B |
1
assets/images/utils/lock.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-lock"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg>
|
||||
|
After Width: | Height: | Size: 321 B |
6
assets/images/utils/paintbrush.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M18.37 2.63 14 7l-1.59-1.59a2 2 0 0 0-2.82 0L8 7l9 9 1.59-1.59a2 2 0 0 0 0-2.82L17 10l4.37-4.37a2.12 2.12 0 1 0-3-3Z"></path>
|
||||
<path d="M9 8c-2 3-4 3.5-7 4l8 10c2-1 6-5 6-7"></path>
|
||||
<path d="M14.5 17.5 4.5 15"></path>
|
||||
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 421 B |
1
assets/images/utils/slash.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-slash"><circle cx="12" cy="12" r="10"></circle><line x1="4.93" y1="4.93" x2="19.07" y2="19.07"></line></svg>
|
||||
|
After Width: | Height: | Size: 312 B |
7
assets/images/utils/transfer.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="17 11 21 7 17 3"></polyline>
|
||||
<line x1="21" y1="7" x2="9" y2="7"></line>
|
||||
<polyline points="7 21 3 17 7 13"></polyline>
|
||||
<line x1="15" y1="17" x2="3" y2="17"></line>
|
||||
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 378 B |
5
assets/images/utils/undo.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M3 7v6h6"></path>
|
||||
<path d="M21 17a9 9 0 0 0-9-9 9 9 0 0 0-6 2.3L3 13"></path>
|
||||
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 280 B |
@ -11,7 +11,7 @@ html {
|
||||
--color-text-inactive: hsl(215, 14%, 34%);
|
||||
--color-text-dark: #1a202c;
|
||||
--color-heading: #2c313d;
|
||||
--color-bg: hsl(220, 13%, 91%);
|
||||
--color-bg: #e5e7eb;
|
||||
--color-raised-bg: #ffffff;
|
||||
--color-divider: hsl(220, 13%, 91%);
|
||||
--color-divider-dark: #c8cdd3;
|
||||
@ -19,32 +19,25 @@ html {
|
||||
--color-text-inverted: var(--color-bg);
|
||||
--color-bg-inverted: var(--color-text);
|
||||
|
||||
--color-brand: #30b27b;
|
||||
--color-brand-hover: #1e9565;
|
||||
--color-brand-active: #177955;
|
||||
--color-brand: #00af5c;
|
||||
--color-brand-highlight: rgba(0, 175, 92, 0.25);
|
||||
--color-brand-shadow: rgba(0, 175, 92, 0.7);
|
||||
--color-brand-inverted: #ffffff;
|
||||
|
||||
--tab-underline-hovered: #e2e8f0;
|
||||
|
||||
--color-button-bg: #e0e0e5;
|
||||
--color-button-bg: hsl(220, 13%, 91%);
|
||||
--color-button-text: var(--color-text-dark);
|
||||
--color-button-bg-hover: #d9dce0;
|
||||
--color-button-text-hover: #1b1e24;
|
||||
--color-button-bg-active: #c3c6cb;
|
||||
--color-button-text-active: var(--color-button-text-hover);
|
||||
--color-button-bg-disabled: #cacdd2;
|
||||
--color-button-text-disabled: #9da3ac;
|
||||
--color-transparent-button-bg-hover: var(--color-button-bg);
|
||||
--color-transparent-button-text-hover: var(--color-text-dark);
|
||||
--color-transparent-button-bg-active: var(--color-button-bg-hover);
|
||||
--color-transparent-button-text-active: var(--color-text-dark);
|
||||
|
||||
--color-toggle-handle: var(--color-icon);
|
||||
|
||||
--color-dropdown-bg: var(--color-button-bg);
|
||||
--color-dropdown-text: var(--color-button-text);
|
||||
|
||||
--color-category-bg: var(--color-bg);
|
||||
--color-category-text: var(--color-text-dark);
|
||||
|
||||
--color-tooltip-bg: var(--color-text);
|
||||
--color-tooltip-text: var(--color-bg);
|
||||
|
||||
@ -56,14 +49,10 @@ html {
|
||||
--color-ad: #d6e6f9;
|
||||
--color-ad-raised: #b1c8e4;
|
||||
|
||||
--shadow-dropdown: 3px 3px 14px hsla(0, 0%, 0%, 0.15);
|
||||
--shadow-dropdown-strong: 3px 3px 10px hsla(0, 0%, 0%, 0.3);
|
||||
--shadow-tooltip: 0.2rem 0.2rem 10px rgba(0, 0, 0, 0.15);
|
||||
|
||||
--color-grey-link: var(--color-text);
|
||||
--color-grey-link-hover: var(--color-heading);
|
||||
--color-grey-link-active: var(--color-text-dark);
|
||||
--color-link: #2089ff;
|
||||
--color-link: #0d60bb;
|
||||
--color-link-hover: #1a76e7;
|
||||
--color-link-active: #146fd7;
|
||||
|
||||
@ -76,11 +65,11 @@ html {
|
||||
--color-badge-yellow-text: #755920;
|
||||
--color-badge-yellow-bg: #f7bb43;
|
||||
|
||||
--color-warning-text: hsl(358, 57%, 20%);
|
||||
--color-warning-bg: hsl(358, 57%, 80%);
|
||||
--color-warning-bg: hsl(355, 70%, 88%);
|
||||
--color-warning-text: hsl(342, 70%, 35%);
|
||||
|
||||
--color-banner-text: hsl(0, 11%, 16%);
|
||||
--color-banner-bg: hsl(356, 59%, 77%);
|
||||
--color-banner-bg: hsl(0, 100%, 95%);
|
||||
--color-banner-side: hsl(357, 78%, 40%);
|
||||
|
||||
--color-block-quote: var(--color-tooltip-bg);
|
||||
@ -90,12 +79,18 @@ html {
|
||||
--color-table-border: #dfe2e5;
|
||||
--color-table-alternate-row: #f6f8fa;
|
||||
|
||||
--shadow-card: 0px 2px 4px 0px hsla(221, 39%, 11%, 0.1),
|
||||
inset 0px -2px 2px 0px hsla(221, 39%, 11%, 0.05);
|
||||
--shadow-image-md: 0px 2px 4px 0px hsla(221, 39%, 11%, 0.2),
|
||||
inset 0px -2px 2px 0px hsla(221, 39%, 11%, 0.1);
|
||||
--shadow-inset-lg: inset 0px -2px 2px hsla(221, 39%, 11%, 0.1);
|
||||
--shadow-inset: inset 0px -2px 2px hsla(221, 39%, 11%, 0.05);
|
||||
--shadow-inset-sm: inset 0px -1px 2px hsla(221, 39%, 11%, 0.15);
|
||||
|
||||
--color-card-link-bg: rgba(0, 0, 0, 7%);
|
||||
--shadow-raised-lg: 0px 2px 4px hsla(221, 39%, 11%, 0.2);
|
||||
--shadow-raised: 0.3px 0.5px 0.6px hsl(var(--shadow-color) / 0.15),
|
||||
1px 2px 2.2px -1.7px hsl(var(--shadow-color) / 0.12),
|
||||
4.4px 8.8px 9.7px -3.4px hsl(var(--shadow-color) / 0.09);
|
||||
--shadow-floating: hsla(0, 0%, 0%, 0) 0px 0px 0px 0px, hsla(0, 0%, 0%, 0) 0px 0px 0px 0px,
|
||||
hsla(0, 0%, 0%, 0.1) 0px 4px 6px -1px, hsla(0, 0%, 0%, 0.1) 0px 2px 4px -1px;
|
||||
|
||||
--shadow-card: rgba(50, 50, 100, 0.1) 0px 2px 4px 0px;
|
||||
}
|
||||
|
||||
.dark-mode {
|
||||
@ -113,33 +108,26 @@ html {
|
||||
--color-bg-inverted: var(--color-text);
|
||||
|
||||
--color-brand: #1bd96a;
|
||||
--color-brand-hover: #2de391;
|
||||
--color-brand-active: #55f5ae;
|
||||
--color-brand-highlight: rgba(27, 217, 106, 0.25);
|
||||
--color-brand-shadow: rgba(27, 217, 106, 0.7);
|
||||
--color-brand-inverted: #000;
|
||||
|
||||
--tab-underline-hovered: #414146;
|
||||
|
||||
--color-button-bg: #3e434b;
|
||||
--color-button-bg: hsl(222, 13%, 30%);
|
||||
--color-button-text: var(--color-text);
|
||||
--color-button-bg-hover: #494f58;
|
||||
--color-button-text-hover: #ffffff;
|
||||
--color-button-bg-active: #616570;
|
||||
--color-button-text-active: var(--color-button-text-hover);
|
||||
--color-button-bg-disabled: #3a434d;
|
||||
--color-button-text-disabled: #555d65;
|
||||
--color-transparent-button-bg-hover: var(--color-button-bg);
|
||||
--color-transparent-button-text-hover: var(--color-text-dark);
|
||||
--color-transparent-button-bg-active: var(--color-button-bg-hover);
|
||||
--color-transparent-button-text-active: var(--color-text-dark);
|
||||
|
||||
--color-toggle-handle: var(--color-button-text);
|
||||
|
||||
--color-dropdown-bg: var(--color-button-bg);
|
||||
--color-dropdown-text: var(--color-button-text);
|
||||
|
||||
--color-category-bg: var(--color-button-bg);
|
||||
--color-category-text: var(--color-text-dark);
|
||||
|
||||
--color-tooltip-bg: var(--color-text);
|
||||
--color-tooltip-text: var(--color-bg);
|
||||
--color-tooltip-bg: var(--color-button-bg);
|
||||
--color-tooltip-text: var(--color-text);
|
||||
|
||||
--color-code-bg: var(--color-button-bg);
|
||||
--color-code-text: var(--color-text-dark);
|
||||
@ -149,10 +137,6 @@ html {
|
||||
--color-ad: #1f324a;
|
||||
--color-ad-raised: #2e4057;
|
||||
|
||||
--shadow-dropdown: 3px 3px 14px hsla(0, 0%, 0%, 0.15);
|
||||
--shadow-dropdown-strong: 3px 3px 20px hsla(0, 0%, 0%, 0.15);
|
||||
--shadow-tooltip: 0.2rem 0.2rem 10px rgba(0, 0, 0, 0.15);
|
||||
|
||||
--color-link: #74b6f3;
|
||||
--color-link-hover: #92c0f5;
|
||||
--color-link-active: #b5d5fd;
|
||||
@ -166,8 +150,8 @@ html {
|
||||
--color-badge-yellow-text: #dba22d;
|
||||
--color-badge-yellow-bg: #f7bb43;
|
||||
|
||||
--color-warning-text: hsl(358, 57%, 85%);
|
||||
--color-warning-bg: hsl(358, 57%, 25%);
|
||||
--color-warning-bg: hsl(355, 70%, 20%);
|
||||
--color-warning-text: hsl(342, 70%, 75%);
|
||||
|
||||
--color-banner-text: hsl(0, 100%, 96%);
|
||||
--color-banner-bg: hsl(356, 18%, 18%);
|
||||
@ -180,7 +164,16 @@ html {
|
||||
--color-table-border: #4f5864;
|
||||
--color-table-alternate-row: #262a30;
|
||||
|
||||
--color-card-link-bg: rgb(255, 255, 255, 15%);
|
||||
--shadow-inset-lg: inset 0px -2px 2px hsla(221, 39%, 11%, 0.1);
|
||||
--shadow-inset: inset 0px -2px 2px hsla(221, 39%, 11%, 0.05);
|
||||
--shadow-inset-sm: inset 0px -1px 1px hsla(221, 39%, 11%, 0.25);
|
||||
|
||||
--shadow-raised-lg: 0px 2px 4px hsla(221, 39%, 11%, 0.2);
|
||||
--shadow-raised: 0px -2px 4px hsla(221, 39%, 11%, 0.1);
|
||||
--shadow-floating: hsla(0, 0%, 0%, 0) 0px 0px 0px 0px, hsla(0, 0%, 0%, 0) 0px 0px 0px 0px,
|
||||
hsla(0, 0%, 0%, 0.1) 0px 4px 6px -1px, rgba(0, 0, 0, 0.06) 0px 2px 4px -1px;
|
||||
|
||||
--shadow-card: rgba(0, 0, 0, 0.25) 0px 2px 4px 0px;
|
||||
}
|
||||
|
||||
.oled-mode {
|
||||
@ -193,15 +186,21 @@ html {
|
||||
--color-button-bg-active: #3c3c40;
|
||||
}
|
||||
|
||||
.midas-mode {
|
||||
@extend .dark-mode;
|
||||
--color-brand: #fac413;
|
||||
--color-brand-shadow: rgba(250, 196, 19, 0.7);
|
||||
}
|
||||
|
||||
body {
|
||||
// Defaults
|
||||
background-color: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
--font-standard: Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen,
|
||||
Ubuntu, Roboto, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
||||
--font-standard: Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Roboto, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
||||
--mono-font: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;
|
||||
font-family: var(--font-standard);
|
||||
font-size: 16px;
|
||||
font-weight: var(--font-weight-medium);
|
||||
font-weight: var(--font-weight-regular);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
@ -219,12 +218,13 @@ body {
|
||||
|
||||
--size-navbar-height: 3.5rem;
|
||||
--size-mobile-navbar-height: 3.5rem;
|
||||
--size-mobile-navbar-height-expanded: 10rem;
|
||||
--size-mobile-navbar-height-expanded: 11.75rem;
|
||||
|
||||
--spacing-card-lg: 1.5rem;
|
||||
--spacing-card-bg: 1rem;
|
||||
--spacing-card-md: 0.75rem;
|
||||
--spacing-card-sm: 0.5rem;
|
||||
--spacing-card-xs: 0.25rem;
|
||||
|
||||
// Font Sizes
|
||||
--font-size-xxs: 0.625rem; //10px
|
||||
@ -247,14 +247,18 @@ body {
|
||||
--font-weight-heading: var(--font-weight-extrabold);
|
||||
--font-weight-title: var(--font-weight-extrabold);
|
||||
|
||||
@media screen and (min-width: 501px) {
|
||||
@media screen and (min-width: 318px) {
|
||||
--size-mobile-navbar-height-expanded: 9.25rem;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 517px) {
|
||||
--size-mobile-navbar-height-expanded: 7rem;
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
height: 1em;
|
||||
width: 1em;
|
||||
}
|
||||
|
||||
a {
|
||||
@ -262,6 +266,10 @@ a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: var(--color-text-dark);
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
@ -269,19 +277,17 @@ h2 {
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.25rem;
|
||||
margin-block: var(--spacing-card-md) var(--spacing-card-sm);
|
||||
color: var(--color-text-dark);
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
@extend .button;
|
||||
}
|
||||
|
||||
input {
|
||||
border-radius: 2rem;
|
||||
border-radius: var(--size-rounded-sm);
|
||||
box-sizing: border-box;
|
||||
border: 2px solid transparent;
|
||||
// safari iOS rounds inputs by default
|
||||
// set the appearance to none to prevent this
|
||||
appearance: none !important;
|
||||
}
|
||||
|
||||
pre {
|
||||
@ -293,17 +299,15 @@ textarea {
|
||||
background: var(--color-button-bg);
|
||||
color: var(--color-text);
|
||||
padding: 0.5rem 1rem;
|
||||
border: 2px solid transparent;
|
||||
font-weight: var(--font-weight-medium);
|
||||
border: none;
|
||||
outline: none;
|
||||
box-shadow: var(--shadow-inset-sm), 0 0 0 0 transparent;
|
||||
transition: box-shadow 0.1s ease-in-out;
|
||||
min-height: 40px;
|
||||
|
||||
&:hover:not([disabled]):not(:focus) {
|
||||
background: var(--color-button-bg-hover);
|
||||
color: var(--color-button-text-hover);
|
||||
border-color: var(--color-divider-dark);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none !important;
|
||||
border-color: var(--color-brand);
|
||||
&:focus, &:focus-visible {
|
||||
box-shadow: inset 0 0 0 transparent, 0 0 0 .25rem var(--color-brand-shadow);
|
||||
color: var(--color-button-text-active);
|
||||
}
|
||||
|
||||
@ -313,26 +317,21 @@ textarea {
|
||||
pointer-events: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&:focus::placeholder {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: var(--color-button-text);
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0.5rem 0;
|
||||
//outline: none; Bad for accessibility
|
||||
color: var(--color-button-text);
|
||||
background-color: var(--color-button-bg);
|
||||
button, input[type=button] {
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
border-radius: var(--size-rounded-control);
|
||||
|
||||
&:focus-visible,
|
||||
&:hover {
|
||||
background-color: var(--color-button-bg-hover);
|
||||
color: var(--color-button-text-hover);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: var(--color-button-bg-active);
|
||||
color: var(--color-button-text-active);
|
||||
}
|
||||
outline: none;
|
||||
}
|
||||
|
||||
kbd {
|
||||
@ -345,10 +344,13 @@ kbd {
|
||||
font-size: 0.85em !important;
|
||||
}
|
||||
|
||||
// @import "vue-select/src/scss/vue-select.scss";
|
||||
|
||||
@import '~assets/styles/highlightjs.scss';
|
||||
@import '~assets/styles/layout.scss';
|
||||
@import '~assets/styles/utils.scss';
|
||||
@import '~assets/styles/components.scss';
|
||||
@import '~assets/styles/normalize.scss';
|
||||
|
||||
button:focus-visible, a:focus-visible, [tabindex="0"]:focus-visible {
|
||||
outline: .25rem solid #ea80ff;
|
||||
border-radius: .25rem;
|
||||
}
|
||||
|
||||
@ -43,6 +43,10 @@
|
||||
margin: 0 0.75rem;
|
||||
}
|
||||
|
||||
.site-header {
|
||||
margin: 0 0.75rem;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.normal-page {
|
||||
flex-direction: row;
|
||||
@ -56,10 +60,16 @@
|
||||
}
|
||||
|
||||
.normal-page__sidebar {
|
||||
min-width: 20rem;
|
||||
width: 20rem;
|
||||
}
|
||||
|
||||
.normal-page__content {
|
||||
width: 60rem;
|
||||
max-width: 60rem;
|
||||
}
|
||||
|
||||
.site-header {
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
2
assets/styles/normalize.scss
vendored
@ -165,7 +165,7 @@ textarea {
|
||||
font-family: inherit; /* 1 */
|
||||
font-size: 100%; /* 1 */
|
||||
line-height: 1.15; /* 1 */
|
||||
margin: 2px; /* 2 */
|
||||
margin: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
.hidden {
|
||||
display: none;
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.w-100 {
|
||||
|
||||
111
components/ui/Avatar.vue
Normal file
@ -0,0 +1,111 @@
|
||||
<template>
|
||||
<img
|
||||
v-if="src"
|
||||
ref="img"
|
||||
:class="`avatar size-${size} ${circle ? 'circle' : ''}`"
|
||||
:src="src"
|
||||
:alt="alt"
|
||||
/>
|
||||
<svg
|
||||
v-else
|
||||
:class="`avatar size-${size} ${circle ? 'circle' : ''}`"
|
||||
xml:space="preserve"
|
||||
fill-rule="evenodd"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-miterlimit="1.5"
|
||||
clip-rule="evenodd"
|
||||
viewBox="0 0 104 104"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path fill="none" d="M0 0h103.4v103.4H0z" />
|
||||
<path
|
||||
fill="none"
|
||||
stroke="#9a9a9a"
|
||||
stroke-width="5"
|
||||
d="M51.7 92.5V51.7L16.4 31.3l35.3 20.4L87 31.3 51.7 11 16.4 31.3v40.8l35.3 20.4L87 72V31.3L51.7 11"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Avatar',
|
||||
props: {
|
||||
src: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
alt: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'sm',
|
||||
validator(value) {
|
||||
return ['xs', 'sm', 'md', 'lg'].includes(value)
|
||||
},
|
||||
},
|
||||
circle: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (this.$refs.img && this.$refs.img.naturalWidth) {
|
||||
const isPixelated = () => {
|
||||
if (
|
||||
this.$refs.img.naturalWidth < 96 &&
|
||||
this.$refs.img.naturalWidth > 0
|
||||
) {
|
||||
this.$refs.img.style.imageRendering = 'pixelated'
|
||||
}
|
||||
}
|
||||
|
||||
if (this.$refs.img.naturalWidth) {
|
||||
isPixelated()
|
||||
} else {
|
||||
this.$refs.img.onload = isPixelated
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.avatar {
|
||||
border-radius: var(--size-rounded-icon);
|
||||
box-shadow: var(--shadow-inset-lg), var(--shadow-card);
|
||||
height: var(--size);
|
||||
width: var(--size);
|
||||
background-color: var(--color-button-bg);
|
||||
object-fit: contain;
|
||||
|
||||
&.size-xs {
|
||||
--size: 2.5rem;
|
||||
box-shadow: var(--shadow-inset), var(--shadow-card);
|
||||
border-radius: var(--size-rounded-sm);
|
||||
}
|
||||
|
||||
&.size-sm {
|
||||
--size: 3rem;
|
||||
box-shadow: var(--shadow-inset), var(--shadow-card);
|
||||
border-radius: var(--size-rounded-sm);
|
||||
}
|
||||
|
||||
&.size-md {
|
||||
--size: 6rem;
|
||||
border-radius: var(--size-rounded-lg);
|
||||
}
|
||||
|
||||
&.size-lg {
|
||||
--size: 9rem;
|
||||
border-radius: var(--size-rounded-lg);
|
||||
}
|
||||
|
||||
&.circle {
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -6,7 +6,7 @@
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'VersionBadge',
|
||||
name: 'Badge',
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
class="checkbox-outer"
|
||||
class="checkbox-outer button-within"
|
||||
:class="{ disabled }"
|
||||
role="presentation"
|
||||
@click="toggle"
|
||||
@ -71,58 +71,14 @@ export default {
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
|
||||
button {
|
||||
cursor: not-allowed;
|
||||
|
||||
&:active,
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: var(--color-button-bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
user-select: none;
|
||||
padding: 0.2rem 0rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&:focus-visible,
|
||||
&:hover {
|
||||
color: var(--color-heading);
|
||||
|
||||
.checkbox.collapsing svg {
|
||||
color: var(--color-heading);
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: var(--color-button-bg-hover);
|
||||
|
||||
&.checked {
|
||||
background-color: var(--color-brand-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
color: var(--color-text-dark);
|
||||
|
||||
.checkbox.collapsing svg {
|
||||
color: var(--color-text-dark);
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: var(--color-button-bg-active);
|
||||
|
||||
&.checked {
|
||||
background-color: var(--color-brand-active);
|
||||
}
|
||||
}
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
@ -132,18 +88,23 @@ export default {
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
min-width: 1rem;
|
||||
min-height: 1rem;
|
||||
|
||||
padding: 0;
|
||||
margin: 0 0.5rem 0 0;
|
||||
|
||||
color: var(--color-button-text);
|
||||
background-color: var(--color-button-bg);
|
||||
border-radius: var(--size-rounded-control);
|
||||
box-shadow: var(--shadow-inset-sm), 0 0 0 0 transparent;
|
||||
|
||||
&.checked {
|
||||
background-color: var(--color-brand);
|
||||
}
|
||||
|
||||
svg {
|
||||
color: var(--color-text-inverted);
|
||||
color: var(--color-brand-inverted);
|
||||
stroke-width: 0.2rem;
|
||||
height: 0.8rem;
|
||||
width: 0.8rem;
|
||||
@ -152,6 +113,7 @@ export default {
|
||||
|
||||
&.collapsing {
|
||||
background-color: transparent !important;
|
||||
box-shadow: none;
|
||||
|
||||
svg {
|
||||
color: inherit;
|
||||
@ -166,5 +128,10 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
box-shadow: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
95
components/ui/Chips.vue
Normal file
@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<div class="chips">
|
||||
<button
|
||||
v-for="item in items"
|
||||
:key="item"
|
||||
class="iconified-button"
|
||||
:class="{ selected: selected === item }"
|
||||
@click="toggleItem(item)"
|
||||
>
|
||||
<CheckIcon v-if="selected === item" />
|
||||
<span>{{ formatLabel(item) }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CheckIcon from '~/assets/images/utils/check.svg?inline'
|
||||
|
||||
export default {
|
||||
name: 'Chips',
|
||||
components: {
|
||||
CheckIcon,
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
required: true,
|
||||
type: String,
|
||||
},
|
||||
items: {
|
||||
required: true,
|
||||
type: Array,
|
||||
},
|
||||
neverEmpty: {
|
||||
default: true,
|
||||
type: Boolean,
|
||||
},
|
||||
formatLabel: {
|
||||
default: (x) => x,
|
||||
type: Function,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
selected: {
|
||||
get() {
|
||||
return this.value
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('input', value)
|
||||
},
|
||||
},
|
||||
},
|
||||
created() {
|
||||
if (this.items.length > 0 && this.neverEmpty) {
|
||||
this.selected = this.items[0]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleItem(item) {
|
||||
if (this.selected === item && !this.neverEmpty) {
|
||||
this.selected = null
|
||||
} else {
|
||||
this.selected = item
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.chips {
|
||||
display: flex;
|
||||
grid-gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.iconified-button {
|
||||
text-transform: capitalize;
|
||||
|
||||
svg {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 0.25rem solid #ea80ff;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.selected {
|
||||
color: var(--color-button-text-active);
|
||||
background-color: var(--color-brand-highlight);
|
||||
box-shadow: inset 0 0 0 transparent, 0 0 0 2px var(--color-brand);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,157 +0,0 @@
|
||||
<template>
|
||||
<Popup :show-popup="display">
|
||||
<div class="popup-delete">
|
||||
<span class="title">{{ title }}</span>
|
||||
<span class="description">
|
||||
{{ description }}
|
||||
</span>
|
||||
<label v-if="hasToType" for="confirmation" class="confirmation-label">
|
||||
<span>
|
||||
To confirm your action, please type
|
||||
<span class="confirmation-text">{{ confirmationText }}</span>
|
||||
to continue
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
v-if="hasToType"
|
||||
id="confirmation"
|
||||
v-model="confirmation_typed"
|
||||
type="text"
|
||||
placeholder="Type the input needed to continue"
|
||||
@input="type"
|
||||
/>
|
||||
<div class="actions">
|
||||
<button class="button" @click="cancel">Cancel</button>
|
||||
<button
|
||||
class="button warn-button"
|
||||
:disabled="action_disabled"
|
||||
@click="proceed"
|
||||
>
|
||||
{{ proceedLabel }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Popup>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Popup from '~/components/ui/Popup'
|
||||
|
||||
export default {
|
||||
name: 'ConfirmPopup',
|
||||
components: {
|
||||
Popup,
|
||||
},
|
||||
props: {
|
||||
confirmationText: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
hasToType: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: 'No title defined',
|
||||
required: true,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: 'No description defined',
|
||||
required: true,
|
||||
},
|
||||
proceedLabel: {
|
||||
type: String,
|
||||
default: 'Proceed',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
action_disabled: this.hasToType,
|
||||
confirmation_typed: '',
|
||||
display: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
cancel() {
|
||||
this.display = false
|
||||
},
|
||||
proceed() {
|
||||
this.display = false
|
||||
this.$emit('proceed')
|
||||
},
|
||||
type() {
|
||||
if (this.hasToType) {
|
||||
this.action_disabled =
|
||||
this.confirmation_typed.toLowerCase() !==
|
||||
this.confirmationText.toLowerCase()
|
||||
}
|
||||
},
|
||||
show() {
|
||||
this.display = true
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.popup-delete {
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@media screen and (min-width: 900px) {
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1024px) {
|
||||
max-width: 40vw;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.25rem;
|
||||
align-self: stretch;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.description {
|
||||
word-wrap: break-word;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.confirmation-label {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.confirmation-text {
|
||||
font-weight: bold;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-top: 1.5rem;
|
||||
|
||||
button {
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
margin: 0.75rem 1rem;
|
||||
padding: 0.75rem 0;
|
||||
}
|
||||
|
||||
.warn-button {
|
||||
transition: background-color 1s, color 1s;
|
||||
color: var(--color-brand-inverted);
|
||||
background-color: var(--color-badge-red-bg);
|
||||
|
||||
&:disabled {
|
||||
background-color: var(--color-button-bg);
|
||||
color: var(--color-button-text-disabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
74
components/ui/CopyCode.vue
Normal file
@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<button
|
||||
class="code"
|
||||
:class="{ copied }"
|
||||
title="Copy code to clipboard"
|
||||
@click="copyText"
|
||||
>
|
||||
{{ text }}
|
||||
<CheckIcon v-if="copied" />
|
||||
<ClipboardCopyIcon v-else />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CheckIcon from '~/assets/images/utils/check.svg?inline'
|
||||
import ClipboardCopyIcon from '~/assets/images/utils/clipboard-copy.svg?inline'
|
||||
|
||||
export default {
|
||||
name: 'CopyCode',
|
||||
components: {
|
||||
CheckIcon,
|
||||
ClipboardCopyIcon,
|
||||
},
|
||||
props: {
|
||||
text: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
copied: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async copyText() {
|
||||
await navigator.clipboard.writeText(this.text)
|
||||
this.copied = true
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.code {
|
||||
color: var(--color-text);
|
||||
display: flex;
|
||||
grid-gap: 0.5rem;
|
||||
font-family: var(--mono-font);
|
||||
font-size: var(--font-size-sm);
|
||||
margin: 0;
|
||||
padding: 0.25rem 0.5rem;
|
||||
background-color: var(--color-code-bg);
|
||||
width: min-content;
|
||||
border-radius: 10px;
|
||||
user-select: text;
|
||||
transition: opacity 0.5s ease-in-out, filter 0.2s ease-in-out,
|
||||
transform 0.05s ease-in-out, outline 0.2s ease-in-out;
|
||||
|
||||
svg {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
filter: brightness(0.85);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
filter: brightness(0.8);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,15 +1,17 @@
|
||||
<template>
|
||||
<div class="columns">
|
||||
<label class="button" @drop.prevent="addFile" @dragover.prevent>
|
||||
<span>
|
||||
<UploadIcon v-if="showIcon" />
|
||||
{{ prompt }}
|
||||
</span>
|
||||
<label
|
||||
class="iconified-button"
|
||||
@drop.prevent="handleDrop"
|
||||
@dragover.prevent
|
||||
>
|
||||
<UploadIcon v-if="showIcon" />
|
||||
{{ prompt }}
|
||||
<input
|
||||
type="file"
|
||||
:multiple="multiple"
|
||||
:accept="accept"
|
||||
@change="onChange"
|
||||
@change="handleChange"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
@ -20,7 +22,7 @@ import { fileIsValid } from '~/plugins/fileUtils'
|
||||
import UploadIcon from '~/assets/images/utils/upload.svg?inline'
|
||||
|
||||
export default {
|
||||
name: 'SmartFileInput',
|
||||
name: 'FileInput',
|
||||
components: {
|
||||
UploadIcon,
|
||||
},
|
||||
@ -48,6 +50,10 @@ export default {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
shouldAlwaysReset: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@ -55,8 +61,8 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onChange(files, shouldNotReset) {
|
||||
if (!shouldNotReset) this.files = files.target.files
|
||||
addFiles(files, shouldNotReset) {
|
||||
if (!shouldNotReset || this.shouldAlwaysReset) this.files = files
|
||||
|
||||
const validationOptions = { maxSize: this.maxSize, alertOnInvalid: true }
|
||||
this.files = [...this.files].filter((file) =>
|
||||
@ -67,19 +73,11 @@ export default {
|
||||
this.$emit('change', this.files)
|
||||
}
|
||||
},
|
||||
addFile(e) {
|
||||
const droppedFiles = e.dataTransfer.files
|
||||
|
||||
if (!this.multiple) this.files = []
|
||||
|
||||
if (!droppedFiles) return
|
||||
;[...droppedFiles].forEach((f) => {
|
||||
this.files.push(f)
|
||||
})
|
||||
|
||||
if (!this.multiple && this.files.length > 0) this.files = [this.files[0]]
|
||||
|
||||
if (this.files.length > 0) this.onChange(null, true)
|
||||
handleDrop(e) {
|
||||
this.addFiles(e.dataTransfer.files)
|
||||
},
|
||||
handleChange(e) {
|
||||
this.addFiles(e.target.files)
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -87,26 +85,12 @@ export default {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
label {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding: var(--spacing-card-sm) var(--spacing-card-md);
|
||||
}
|
||||
|
||||
span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
gap: 0.5rem;
|
||||
border: 2px dashed var(--color-divider-dark);
|
||||
border-radius: var(--size-rounded-control);
|
||||
padding: var(--spacing-card-md) var(--spacing-card-lg);
|
||||
flex-direction: unset;
|
||||
margin-bottom: 0;
|
||||
max-height: unset;
|
||||
|
||||
svg {
|
||||
height: 1.25rem;
|
||||
height: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
121
components/ui/Modal.vue
Normal file
@ -0,0 +1,121 @@
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
:class="{
|
||||
shown: shown,
|
||||
noblur: !$orElse($store.state.cosmetics.advancedRendering, true),
|
||||
}"
|
||||
class="modal-overlay"
|
||||
@click="hide"
|
||||
/>
|
||||
<div class="modal-body" :class="{ shown: shown }">
|
||||
<div v-if="header" class="header">
|
||||
<h1>{{ header }}</h1>
|
||||
<button class="iconified-button icon-only transparent" @click="hide">
|
||||
<CrossIcon />
|
||||
</button>
|
||||
</div>
|
||||
<div class="content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CrossIcon from '~/assets/images/utils/x.svg?inline'
|
||||
|
||||
export default {
|
||||
name: 'Modal',
|
||||
components: {
|
||||
CrossIcon,
|
||||
},
|
||||
props: {
|
||||
header: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
shown: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
show() {
|
||||
this.shown = true
|
||||
},
|
||||
hide() {
|
||||
this.shown = false
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.modal-overlay {
|
||||
visibility: hidden;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 20;
|
||||
|
||||
transition: all 0.3s ease-in-out;
|
||||
|
||||
&.shown {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
background: hsla(0, 0%, 0%, 0.5);
|
||||
backdrop-filter: blur(3px);
|
||||
}
|
||||
|
||||
&.noblur {
|
||||
backdrop-filter: none;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 21;
|
||||
box-shadow: var(--shadow-raised), var(--shadow-inset);
|
||||
border-radius: var(--size-rounded-lg);
|
||||
max-height: calc(100% - 2 * var(--spacing-card-bg));
|
||||
overflow-y: auto;
|
||||
width: 600px;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background-color: var(--color-bg);
|
||||
padding: var(--spacing-card-md) var(--spacing-card-lg);
|
||||
|
||||
h1 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
background-color: var(--color-raised-bg);
|
||||
}
|
||||
|
||||
top: calc(100% + 400px);
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
transition: all 0.25s ease-in-out;
|
||||
|
||||
&.shown {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
top: 50%;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 650px) {
|
||||
width: calc(100% - 2 * var(--spacing-card-bg));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
134
components/ui/ModalConfirm.vue
Normal file
@ -0,0 +1,134 @@
|
||||
<template>
|
||||
<Modal ref="modal" :header="title">
|
||||
<div class="modal-delete">
|
||||
<div class="markdown-body" v-html="$xss($md.render(description))"></div>
|
||||
<label v-if="hasToType" for="confirmation" class="confirmation-label">
|
||||
<span>
|
||||
<strong>To verify, type</strong>
|
||||
<em class="confirmation-text">{{ confirmationText }}</em>
|
||||
<strong>below:</strong>
|
||||
</span>
|
||||
</label>
|
||||
<div class="confirmation-input">
|
||||
<input
|
||||
v-if="hasToType"
|
||||
id="confirmation"
|
||||
v-model="confirmation_typed"
|
||||
type="text"
|
||||
placeholder="Type here..."
|
||||
@input="type"
|
||||
/>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<button class="iconified-button" @click="cancel">
|
||||
<CrossIcon />
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
class="iconified-button danger-button"
|
||||
:disabled="action_disabled"
|
||||
@click="proceed"
|
||||
>
|
||||
<TrashIcon />
|
||||
{{ proceedLabel }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CrossIcon from '~/assets/images/utils/x.svg?inline'
|
||||
import TrashIcon from '~/assets/images/utils/trash.svg?inline'
|
||||
import Modal from '~/components/ui/Modal'
|
||||
|
||||
export default {
|
||||
name: 'ModalConfirm',
|
||||
components: {
|
||||
CrossIcon,
|
||||
TrashIcon,
|
||||
Modal,
|
||||
},
|
||||
props: {
|
||||
confirmationText: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
hasToType: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: 'No title defined',
|
||||
required: true,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: 'No description defined',
|
||||
required: true,
|
||||
},
|
||||
proceedLabel: {
|
||||
type: String,
|
||||
default: 'Proceed',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
action_disabled: this.hasToType,
|
||||
confirmation_typed: '',
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
cancel() {
|
||||
this.$refs.modal.hide()
|
||||
},
|
||||
proceed() {
|
||||
this.$refs.modal.hide()
|
||||
this.$emit('proceed')
|
||||
},
|
||||
type() {
|
||||
if (this.hasToType) {
|
||||
this.action_disabled =
|
||||
this.confirmation_typed.toLowerCase() !==
|
||||
this.confirmationText.toLowerCase()
|
||||
}
|
||||
},
|
||||
show() {
|
||||
this.$refs.modal.show()
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.modal-delete {
|
||||
padding: var(--spacing-card-bg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.markdown-body {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.confirmation-label {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.confirmation-text {
|
||||
padding-right: 0.25ch;
|
||||
}
|
||||
|
||||
.confirmation-input {
|
||||
input {
|
||||
width: 20rem;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.button-group {
|
||||
margin-left: auto;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
240
components/ui/ModalCreation.vue
Normal file
@ -0,0 +1,240 @@
|
||||
<template>
|
||||
<Modal ref="modal" header="Create a project">
|
||||
<div class="modal-creation universal-labels">
|
||||
<div class="markdown-body">
|
||||
<p>
|
||||
New projects are created as drafts and can be found under your profile
|
||||
page.
|
||||
</p>
|
||||
</div>
|
||||
<label for="project-type">
|
||||
<span class="label__title"
|
||||
>Project type<span class="required">*</span></span
|
||||
>
|
||||
</label>
|
||||
<Chips
|
||||
id="project-type"
|
||||
v-model="projectType"
|
||||
:items="$tag.projectTypes.map((x) => x.display)"
|
||||
/>
|
||||
<label for="name">
|
||||
<span class="label__title">Name<span class="required">*</span></span>
|
||||
</label>
|
||||
<input
|
||||
id="name"
|
||||
v-model="name"
|
||||
type="text"
|
||||
maxlength="64"
|
||||
placeholder="Enter project name..."
|
||||
autocomplete="off"
|
||||
@input="updatedName()"
|
||||
/>
|
||||
<label for="slug">
|
||||
<span class="label__title">URL<span class="required">*</span></span>
|
||||
</label>
|
||||
<div class="text-input-wrapper">
|
||||
<div class="text-input-wrapper__before">
|
||||
https://modrinth.com/{{
|
||||
getProjectType() ? getProjectType().id : '???'
|
||||
}}/
|
||||
</div>
|
||||
<input
|
||||
id="slug"
|
||||
v-model="slug"
|
||||
type="text"
|
||||
maxlength="64"
|
||||
autocomplete="off"
|
||||
@input="manualSlug = true"
|
||||
/>
|
||||
</div>
|
||||
<label for="additional-information">
|
||||
<span class="label__title">Summary<span class="required">*</span></span>
|
||||
<span class="label__description"
|
||||
>This appears in search and on the sidebar of your project's
|
||||
page.</span
|
||||
>
|
||||
</label>
|
||||
<div class="textarea-wrapper">
|
||||
<textarea
|
||||
id="additional-information"
|
||||
v-model="description"
|
||||
maxlength="256"
|
||||
/>
|
||||
</div>
|
||||
<div class="push-right input-group">
|
||||
<button class="iconified-button" @click="cancel">
|
||||
<CrossIcon />
|
||||
Cancel
|
||||
</button>
|
||||
<button class="iconified-button brand-button" @click="createProject">
|
||||
<CheckIcon />
|
||||
Continue
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CrossIcon from '~/assets/images/utils/x.svg?inline'
|
||||
import CheckIcon from '~/assets/images/utils/right-arrow.svg?inline'
|
||||
import Modal from '~/components/ui/Modal'
|
||||
import Chips from '~/components/ui/Chips'
|
||||
|
||||
export default {
|
||||
name: 'ModalCreation',
|
||||
components: {
|
||||
Chips,
|
||||
CrossIcon,
|
||||
CheckIcon,
|
||||
Modal,
|
||||
},
|
||||
props: {
|
||||
itemType: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
itemId: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
projectType: this.$tag.projectTypes[0].display,
|
||||
name: '',
|
||||
slug: '',
|
||||
description: '',
|
||||
manualSlug: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
cancel() {
|
||||
this.$refs.modal.hide()
|
||||
},
|
||||
getProjectType() {
|
||||
return this.$tag.projectTypes.find((x) => this.projectType === x.display)
|
||||
},
|
||||
async createProject() {
|
||||
this.$nuxt.$loading.start()
|
||||
|
||||
const projectType = this.getProjectType()
|
||||
|
||||
const formData = new FormData()
|
||||
|
||||
formData.append(
|
||||
'data',
|
||||
JSON.stringify({
|
||||
title: this.name,
|
||||
project_type: projectType.actual,
|
||||
slug: this.slug,
|
||||
description: this.description,
|
||||
body: `# Placeholder description
|
||||
This is your new ${projectType.display}, ${
|
||||
this.name
|
||||
}. A checklist below is provided to help prepare for release.
|
||||
|
||||
### Before submitting for review
|
||||
- Upload at least one version
|
||||
- [Edit project description](https://modrinth.com/${this.getProjectType().id}/${
|
||||
this.slug
|
||||
}/edit)
|
||||
- Update metadata
|
||||
- Select license
|
||||
- Set up environments
|
||||
- Choose categories
|
||||
- Add source, wiki, Discord and donation links (optional)
|
||||
- Add images to gallery (optional)
|
||||
- Invite project team members (optional)
|
||||
|
||||
> Submissions are normally reviewed within 24 hours, but may take up to 48 hours
|
||||
|
||||
Questions? [Join the Modrinth Discord for support!](https://discord.gg/EUHuJHt)`,
|
||||
initial_versions: [],
|
||||
team_members: [
|
||||
{
|
||||
user_id: this.$auth.user.id,
|
||||
name: this.$auth.user.username,
|
||||
role: 'Owner',
|
||||
},
|
||||
],
|
||||
categories: [],
|
||||
client_side: 'unknown',
|
||||
server_side: 'unknown',
|
||||
license_id: this.$tag.licenses.map((it) => it.short).includes('arr')
|
||||
? 'arr'
|
||||
: this.$tag.licenses[0].short,
|
||||
is_draft: true,
|
||||
})
|
||||
)
|
||||
|
||||
console.log(formData)
|
||||
|
||||
try {
|
||||
await this.$axios({
|
||||
url: 'project',
|
||||
method: 'POST',
|
||||
data: formData,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
Authorization: this.$auth.token,
|
||||
},
|
||||
})
|
||||
|
||||
this.$refs.modal.hide()
|
||||
await this.$router.replace(`/${projectType.display}/${this.slug}`)
|
||||
} catch (err) {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.response.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
this.$nuxt.$loading.finish()
|
||||
},
|
||||
show() {
|
||||
this.projectType = this.$tag.projectTypes[0].display
|
||||
this.name = ''
|
||||
this.slug = ''
|
||||
this.description = ''
|
||||
this.manualSlug = false
|
||||
this.$refs.modal.show()
|
||||
},
|
||||
updatedName() {
|
||||
if (!this.manualSlug) {
|
||||
this.slug = this.name.toLowerCase().replaceAll(' ', '-')
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.modal-creation {
|
||||
padding: var(--spacing-card-bg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.markdown-body {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 20rem;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.text-input-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 5rem;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
margin-top: var(--spacing-card-md);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
179
components/ui/ModalReport.vue
Normal file
@ -0,0 +1,179 @@
|
||||
<template>
|
||||
<Modal ref="modal" :header="`Report ${itemType}`">
|
||||
<div class="modal-report legacy-label-styles">
|
||||
<div class="markdown-body">
|
||||
<p>
|
||||
Modding should be safe for everyone, so we take abuse and malicious
|
||||
intent seriously at Modrinth. We want to hear about harmful content on
|
||||
the site that violates our
|
||||
<nuxt-link to="/legal/terms">ToS</nuxt-link> and
|
||||
<nuxt-link to="/legal/rules">Rules</nuxt-link>. Rest assured, we’ll
|
||||
keep your identifying information private.
|
||||
</p>
|
||||
</div>
|
||||
<label class="report-label" for="report-type">
|
||||
<span>
|
||||
<strong>Reason</strong>
|
||||
</span>
|
||||
</label>
|
||||
<multiselect
|
||||
id="report-type"
|
||||
v-model="reportType"
|
||||
:options="$store.state.tag.reportTypes"
|
||||
:custom-label="
|
||||
(value) => value.charAt(0).toUpperCase() + value.slice(1)
|
||||
"
|
||||
:multiple="false"
|
||||
:searchable="false"
|
||||
:show-no-results="false"
|
||||
:show-labels="false"
|
||||
placeholder="Choose report type"
|
||||
/>
|
||||
<label class="report-label" for="additional-information">
|
||||
<strong>Additional information</strong>
|
||||
<span>
|
||||
Include links and images if possible. Markdown formatting is
|
||||
supported.
|
||||
</span>
|
||||
</label>
|
||||
<div class="textarea-wrapper">
|
||||
<Chips
|
||||
v-model="bodyViewType"
|
||||
class="separator"
|
||||
:items="['source', 'preview']"
|
||||
/>
|
||||
<div v-if="bodyViewType === 'source'" class="textarea-wrapper">
|
||||
<textarea id="body" v-model="body" spellcheck="true" />
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
v-highlightjs
|
||||
class="preview"
|
||||
v-html="$xss($md.render(body))"
|
||||
></div>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<button class="iconified-button" @click="cancel">
|
||||
<CrossIcon />
|
||||
Cancel
|
||||
</button>
|
||||
<button class="iconified-button brand-button" @click="submitReport">
|
||||
<CheckIcon />
|
||||
Report
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Multiselect from 'vue-multiselect'
|
||||
import CrossIcon from '~/assets/images/utils/x.svg?inline'
|
||||
import CheckIcon from '~/assets/images/utils/check.svg?inline'
|
||||
import Modal from '~/components/ui/Modal'
|
||||
import Chips from '~/components/ui/Chips'
|
||||
|
||||
export default {
|
||||
name: 'ModalReport',
|
||||
components: {
|
||||
Chips,
|
||||
CrossIcon,
|
||||
CheckIcon,
|
||||
Modal,
|
||||
Multiselect,
|
||||
},
|
||||
props: {
|
||||
itemType: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
itemId: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
reportType: '',
|
||||
body: '',
|
||||
bodyViewType: 'source',
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
cancel() {
|
||||
this.reportType = ''
|
||||
this.body = ''
|
||||
this.bodyViewType = 'source'
|
||||
|
||||
this.$refs.modal.hide()
|
||||
},
|
||||
async submitReport() {
|
||||
this.$nuxt.$loading.start()
|
||||
try {
|
||||
const data = {
|
||||
report_type: this.reportType,
|
||||
item_id: this.itemId,
|
||||
item_type: this.itemType,
|
||||
body: this.body,
|
||||
}
|
||||
await this.$axios.post('report', data, this.$defaultHeaders())
|
||||
|
||||
this.$refs.modal.hide()
|
||||
} catch (err) {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.response.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
this.$nuxt.$loading.finish()
|
||||
},
|
||||
show() {
|
||||
this.$refs.modal.show()
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.modal-report {
|
||||
padding: var(--spacing-card-bg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.markdown-body {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.multiselect {
|
||||
max-width: 20rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.report-label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
margin-left: auto;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.textarea-wrapper {
|
||||
margin-top: 1rem;
|
||||
height: 12rem;
|
||||
|
||||
textarea {
|
||||
// here due to a bug in safari
|
||||
max-height: 9rem;
|
||||
}
|
||||
|
||||
.preview {
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
224
components/ui/ModalTransfer.vue
Normal file
@ -0,0 +1,224 @@
|
||||
<template>
|
||||
<Modal ref="modal" :header="'Transfer to ' + $formatWallet(wallet)">
|
||||
<div class="modal-transfer">
|
||||
<span
|
||||
>You are initiating a transfer of your revenue from Modrinth's Creator
|
||||
Monetization Program. How much of your
|
||||
<strong>${{ balance }}</strong> balance would you like to
|
||||
transfer?</span
|
||||
>
|
||||
<div class="confirmation-input">
|
||||
<input
|
||||
id="confirmation"
|
||||
v-model="amount"
|
||||
type="text"
|
||||
pattern="^\d*(\.\d{0,2})?$"
|
||||
autocomplete="off"
|
||||
placeholder="Amount to transfer..."
|
||||
/>
|
||||
</div>
|
||||
<div class="confirm-text">
|
||||
<Checkbox
|
||||
v-if="
|
||||
isValidInput() &&
|
||||
parseInput() >= minWithdraw &&
|
||||
parseInput() <= balance
|
||||
"
|
||||
v-model="consentedFee"
|
||||
>
|
||||
<template v-if="wallet === 'venmo'"
|
||||
>I acknowledge that $0.25 will be deducted from the amount I receive
|
||||
to cover {{ $formatWallet(wallet) }} processing fees.</template
|
||||
>
|
||||
<template v-else
|
||||
>I acknowledge that an estimated {{ calcProcessingFees() }} will be
|
||||
deducted from the amount I receive to cover
|
||||
{{ $formatWallet(wallet) }} processing fees and that any excess will
|
||||
be returned to my Modrinth balance.</template
|
||||
>
|
||||
</Checkbox>
|
||||
<Checkbox
|
||||
v-if="
|
||||
isValidInput() &&
|
||||
parseInput() >= minWithdraw &&
|
||||
parseInput() <= balance
|
||||
"
|
||||
v-model="consentedAccount"
|
||||
>
|
||||
I confirm that I an initiating a transfer to the following
|
||||
{{ $formatWallet(wallet) }} account: {{ account }}
|
||||
</Checkbox>
|
||||
<span
|
||||
v-else-if="validInput && parseInput() < minWithdraw"
|
||||
class="invalid"
|
||||
>
|
||||
The amount must be at least ${{ minWithdraw }}</span
|
||||
>
|
||||
<span v-else-if="validInput && parseInput() > balance" class="invalid">
|
||||
The amount must be no more than ${{ balance }}</span
|
||||
>
|
||||
<span v-else-if="amount.length > 0" class="invalid">
|
||||
{{ amount }} is not a valid amount</span
|
||||
>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<NuxtLink class="iconified-button" to="/settings/monetization">
|
||||
<SettingsIcon /> Monetization settings
|
||||
</NuxtLink>
|
||||
<button class="iconified-button" @click="cancel">
|
||||
<CrossIcon />
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
class="iconified-button brand-button"
|
||||
:disabled="!consentedFee || !consentedAccount"
|
||||
@click="proceed"
|
||||
>
|
||||
<TransferIcon />
|
||||
Transfer
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CrossIcon from '~/assets/images/utils/x.svg?inline'
|
||||
import TransferIcon from '~/assets/images/utils/transfer.svg?inline'
|
||||
import SettingsIcon from '~/assets/images/utils/settings.svg?inline'
|
||||
import Modal from '~/components/ui/Modal'
|
||||
import Checkbox from '~/components/ui/Checkbox'
|
||||
|
||||
export default {
|
||||
name: 'ModalTransfer',
|
||||
components: {
|
||||
Checkbox,
|
||||
CrossIcon,
|
||||
SettingsIcon,
|
||||
TransferIcon,
|
||||
Modal,
|
||||
},
|
||||
props: {
|
||||
wallet: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
accountType: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
account: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
balance: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
minWithdraw: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
consentedFee: false,
|
||||
consentedAccount: false,
|
||||
amount: '',
|
||||
validInput: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
cancel() {
|
||||
this.amount = ''
|
||||
this.consentedFee = false
|
||||
this.consentedAccount = false
|
||||
this.validInput = false
|
||||
this.$refs.modal.hide()
|
||||
},
|
||||
async proceed() {
|
||||
this.$nuxt.$loading.start()
|
||||
try {
|
||||
await this.$axios.post(
|
||||
`user/${this.$auth.user.id}/payouts`,
|
||||
{
|
||||
amount: Number(this.amount.replace('$', '')),
|
||||
},
|
||||
this.$defaultHeaders()
|
||||
)
|
||||
await this.$store.dispatch('auth/fetchUser', {
|
||||
token: this.$auth.token,
|
||||
})
|
||||
|
||||
this.$refs.modal.hide()
|
||||
} catch (err) {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.response.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
this.$nuxt.$loading.finish()
|
||||
},
|
||||
show() {
|
||||
this.$refs.modal.show()
|
||||
},
|
||||
isValidInput() {
|
||||
const regex = /^\$?(\d*(\.\d{2})?)$/gm
|
||||
this.validInput = regex.test(this.amount) && this.amount.length > 0
|
||||
return this.validInput
|
||||
},
|
||||
parseInput() {
|
||||
const regex = /^\$?(\d*(\.\d{2})?)$/gm
|
||||
const matches = regex.exec(this.amount)
|
||||
return parseFloat(matches[1])
|
||||
},
|
||||
calcProcessingFees() {
|
||||
if (this.wallet === 'venmo') {
|
||||
return 0.25
|
||||
} else {
|
||||
return Math.max(0.25, Math.min(this.parseInput() * 0.02, 20))
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.modal-transfer {
|
||||
padding: var(--spacing-card-bg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-card-sm);
|
||||
|
||||
.confirmation-input {
|
||||
input {
|
||||
width: 14rem;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.button-group {
|
||||
margin-left: auto;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
strong {
|
||||
color: var(--color-text-dark);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.invalid {
|
||||
color: var(--color-badge-red-bg);
|
||||
}
|
||||
|
||||
.confirm-text {
|
||||
margin-top: var(--spacing-card-sm);
|
||||
min-height: 6rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-card-sm);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
200
components/ui/NavRow.vue
Normal file
@ -0,0 +1,200 @@
|
||||
<template>
|
||||
<nav class="navigation" :class="{ 'use-animation': useAnimation }">
|
||||
<NuxtLink
|
||||
v-for="(link, index) in filteredLinks"
|
||||
v-show="link.shown === undefined ? true : link.shown"
|
||||
:key="index"
|
||||
ref="linkElements"
|
||||
:to="query ? (link.href ? `?${query}=${link.href}` : '?') : link.href"
|
||||
class="nav-link button-animation"
|
||||
:class="{ 'is-active': index === activeIndex }"
|
||||
>
|
||||
<span>{{ link.label }}</span>
|
||||
</NuxtLink>
|
||||
|
||||
<div
|
||||
class="nav-indicator"
|
||||
:style="`visibility: ${
|
||||
useAnimation && activeIndex !== -1 ? 'visible' : 'hidden'
|
||||
}; left: ${indicator.left}px; right: ${indicator.right}px;
|
||||
top: ${indicator.top}px; transition: left 350ms ${
|
||||
indicator.direction === 'left'
|
||||
? 'cubic-bezier(1,0,.3,1) -140ms'
|
||||
: 'cubic-bezier(.75,-0.01,.24,.99) -40ms'
|
||||
},right 350ms ${
|
||||
indicator.direction === 'right'
|
||||
? 'cubic-bezier(1,0,.3,1) -140ms'
|
||||
: 'cubic-bezier(.75,-0.01,.24,.99) -40ms'
|
||||
}, top 100ms ease-in-out`"
|
||||
/>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'NavRow',
|
||||
props: {
|
||||
links: {
|
||||
default: () => [],
|
||||
type: Array,
|
||||
},
|
||||
query: {
|
||||
default: null,
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
useAnimation: false,
|
||||
oldIndex: -1,
|
||||
activeIndex: -1,
|
||||
indicator: {
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 22,
|
||||
direction: 'right',
|
||||
},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filteredLinks() {
|
||||
return this.links.filter((x) => (x.shown === undefined ? true : x.shown))
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'$route.path': {
|
||||
handler() {
|
||||
this.pickLink()
|
||||
},
|
||||
},
|
||||
'$route.query': {
|
||||
handler() {
|
||||
if (this.query) this.pickLink()
|
||||
},
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.pickLink()
|
||||
},
|
||||
methods: {
|
||||
pickLink() {
|
||||
if (this.oldIndex === -1) {
|
||||
this.useAnimation = false
|
||||
|
||||
setTimeout(() => {
|
||||
this.useAnimation = true
|
||||
}, 300)
|
||||
}
|
||||
|
||||
this.activeIndex = this.query
|
||||
? this.filteredLinks.findIndex(
|
||||
(x) =>
|
||||
(x.href === '' ? undefined : x.href) ===
|
||||
this.$route.query[this.query]
|
||||
)
|
||||
: this.filteredLinks.findIndex(
|
||||
(x) => x.href === decodeURIComponent(this.$route.path)
|
||||
)
|
||||
|
||||
if (this.activeIndex !== -1) {
|
||||
this.startAnimation()
|
||||
} else {
|
||||
this.oldIndex = -1
|
||||
}
|
||||
},
|
||||
startAnimation() {
|
||||
if (this.$refs.linkElements[this.activeIndex]) {
|
||||
this.indicator.direction =
|
||||
this.activeIndex < this.oldIndex ? 'left' : 'right'
|
||||
|
||||
this.indicator.left =
|
||||
this.$refs.linkElements[this.activeIndex].$el.offsetLeft
|
||||
this.indicator.right =
|
||||
this.$refs.linkElements[this.activeIndex].$el.parentElement
|
||||
.offsetWidth -
|
||||
this.$refs.linkElements[this.activeIndex].$el.offsetLeft -
|
||||
this.$refs.linkElements[this.activeIndex].$el.offsetWidth
|
||||
this.indicator.top =
|
||||
this.$refs.linkElements[this.activeIndex].$el.offsetTop +
|
||||
this.$refs.linkElements[this.activeIndex].$el.offsetHeight +
|
||||
1
|
||||
}
|
||||
|
||||
this.oldIndex = this.activeIndex
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.navigation {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
grid-gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
position: relative;
|
||||
|
||||
.nav-link {
|
||||
text-transform: capitalize;
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--color-text);
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
bottom: -5px;
|
||||
width: 100%;
|
||||
border-radius: var(--size-rounded-max);
|
||||
height: 0.25rem;
|
||||
transition: opacity 0.1s ease-in-out;
|
||||
background-color: var(--color-brand);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--color-text);
|
||||
|
||||
&::after {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
&:active::after {
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
color: var(--color-text);
|
||||
|
||||
&::after {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.use-animation {
|
||||
.nav-link {
|
||||
&.is-active::after {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nav-indicator {
|
||||
position: absolute;
|
||||
height: 0.25rem;
|
||||
border-radius: var(--size-rounded-max);
|
||||
background-color: var(--color-brand);
|
||||
transition-property: left, right, top;
|
||||
transition-duration: 350ms;
|
||||
visibility: hidden;
|
||||
|
||||
@media (prefers-reduced-motion) {
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
20
components/ui/NavStack.vue
Normal file
@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<nav class="navigation">
|
||||
<slot />
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'NavStack',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.navigation {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
grid-gap: var(--spacing-card-xs);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
</style>
|
||||
64
components/ui/NavStackItem.vue
Normal file
@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<NuxtLink class="nav-link button-base" :to="link">
|
||||
<div class="nav-content">
|
||||
<slot />
|
||||
<span>{{ label }}</span>
|
||||
<span v-if="beta" class="beta-badge">BETA</span>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'NavStackItem',
|
||||
props: {
|
||||
link: {
|
||||
required: true,
|
||||
type: String,
|
||||
},
|
||||
label: {
|
||||
required: true,
|
||||
type: String,
|
||||
},
|
||||
beta: {
|
||||
default: false,
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.nav-link {
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--color-text);
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.25rem;
|
||||
box-shadow: none;
|
||||
|
||||
.nav-content {
|
||||
box-sizing: border-box;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: var(--size-rounded-sm);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
flex-grow: 1;
|
||||
background-color: var(--color-raised-bg);
|
||||
}
|
||||
|
||||
&.nuxt-link-exact-active {
|
||||
.nav-content {
|
||||
color: var(--color-button-text-active);
|
||||
background-color: var(--color-button-bg);
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.beta-badge {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,18 +1,19 @@
|
||||
<template>
|
||||
<div v-if="pages.length > 1" class="columns paginates">
|
||||
<button
|
||||
:class="{ disabled: currentPage === 1 }"
|
||||
<div v-if="count > 1" class="columns paginates">
|
||||
<a
|
||||
:class="{ disabled: page === 1 }"
|
||||
class="left-arrow paginate has-icon"
|
||||
aria-label="Previous Page"
|
||||
@click="currentPage !== 1 ? switchPage(currentPage - 1) : null"
|
||||
:href="linkFunction(page - 1)"
|
||||
@click.prevent="page !== 1 ? switchPage(page - 1) : null"
|
||||
>
|
||||
<LeftArrowIcon />
|
||||
</button>
|
||||
</a>
|
||||
<div
|
||||
v-for="(item, index) in pages"
|
||||
:key="'page-' + item + '-' + index"
|
||||
:class="{
|
||||
'page-number': currentPage !== item,
|
||||
'page-number': page !== item,
|
||||
shrink: item > 99,
|
||||
}"
|
||||
class="page-number-container"
|
||||
@ -20,32 +21,32 @@
|
||||
<div v-if="item === '-'" class="has-icon">
|
||||
<GapIcon />
|
||||
</div>
|
||||
<button
|
||||
<a
|
||||
v-else
|
||||
:class="{
|
||||
'page-number current': currentPage === item,
|
||||
'page-number current': page === item,
|
||||
shrink: item > 99,
|
||||
}"
|
||||
@click="currentPage !== item ? switchPage(item) : null"
|
||||
:href="linkFunction(item)"
|
||||
@click.prevent="page !== item ? switchPage(item) : null"
|
||||
>
|
||||
{{ item }}
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<button
|
||||
<a
|
||||
:class="{
|
||||
disabled: currentPage === pages[pages.length - 1],
|
||||
disabled: page === pages[pages.length - 1],
|
||||
}"
|
||||
class="right-arrow paginate has-icon"
|
||||
aria-label="Next Page"
|
||||
@click="
|
||||
currentPage !== pages[pages.length - 1]
|
||||
? switchPage(currentPage + 1)
|
||||
: null
|
||||
:href="linkFunction(page + 1)"
|
||||
@click.prevent="
|
||||
page !== pages[pages.length - 1] ? switchPage(page + 1) : null
|
||||
"
|
||||
>
|
||||
<RightArrowIcon />
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -62,17 +63,56 @@ export default {
|
||||
RightArrowIcon,
|
||||
},
|
||||
props: {
|
||||
currentPage: {
|
||||
page: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
pages: {
|
||||
type: Array,
|
||||
count: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
linkFunction: {
|
||||
type: Function,
|
||||
default() {
|
||||
return []
|
||||
return () => '/'
|
||||
},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
pages() {
|
||||
let pages = []
|
||||
|
||||
if (this.count > 4) {
|
||||
if (this.page + 3 >= this.count) {
|
||||
pages = [
|
||||
1,
|
||||
'-',
|
||||
this.count - 4,
|
||||
this.count - 3,
|
||||
this.count - 2,
|
||||
this.count - 1,
|
||||
this.count,
|
||||
]
|
||||
} else if (this.page > 4) {
|
||||
pages = [
|
||||
1,
|
||||
'-',
|
||||
this.page - 1,
|
||||
this.page,
|
||||
this.page + 1,
|
||||
'-',
|
||||
this.count,
|
||||
]
|
||||
} else {
|
||||
pages = [1, 2, 3, 4, 5, '-', this.count]
|
||||
}
|
||||
} else {
|
||||
pages = Array.from({ length: this.count }, (_, i) => i + 1)
|
||||
}
|
||||
|
||||
return pages
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
switchPage(newPage) {
|
||||
this.$emit('switch-page', newPage)
|
||||
@ -82,16 +122,18 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
button {
|
||||
box-shadow: var(--shadow-card);
|
||||
a {
|
||||
color: var(--color-button-text);
|
||||
box-shadow: var(--shadow-raised), var(--shadow-inset);
|
||||
|
||||
padding: 0;
|
||||
padding: 0.5rem 1rem;
|
||||
margin: 0;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border-radius: 2rem;
|
||||
background: var(--color-raised-bg);
|
||||
|
||||
transition: opacity 0.5s ease-in-out, filter 0.2s ease-in-out,
|
||||
transform 0.05s ease-in-out, outline 0.2s ease-in-out;
|
||||
|
||||
&.page-number.current {
|
||||
background: var(--color-brand);
|
||||
color: var(--color-brand-inverted);
|
||||
@ -100,40 +142,35 @@ button {
|
||||
|
||||
&.paginate.disabled {
|
||||
background-color: transparent;
|
||||
cursor: default;
|
||||
color: var(--color-button-text-disabled);
|
||||
box-shadow: inset 0 0 0 1px var(--color-button-bg-disabled);
|
||||
cursor: not-allowed;
|
||||
filter: grayscale(50%);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&:focus-visible,
|
||||
&:hover {
|
||||
background-color: var(--color-button-bg-hover);
|
||||
color: var(--color-button-text-hover);
|
||||
&:hover:not(&:disabled) {
|
||||
filter: brightness(0.85);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: var(--color-button-bg-active);
|
||||
color: var(--color-button-text-active);
|
||||
&:active:not(&:disabled) {
|
||||
transform: scale(0.95);
|
||||
filter: brightness(0.8);
|
||||
}
|
||||
}
|
||||
|
||||
.has-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 2em;
|
||||
svg {
|
||||
width: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.page-number-container,
|
||||
button,
|
||||
a,
|
||||
.has-icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 2em;
|
||||
width: 2em;
|
||||
}
|
||||
|
||||
.paginates {
|
||||
@ -143,16 +180,6 @@ button,
|
||||
.has-icon {
|
||||
margin: 0 0.3em;
|
||||
}
|
||||
font-size: 80%;
|
||||
@media screen and (min-width: 350px) {
|
||||
font-size: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.shrink {
|
||||
font-size: 0.9rem;
|
||||
height: 2.225em;
|
||||
width: 2.225em;
|
||||
}
|
||||
|
||||
.left-arrow {
|
||||
@ -162,4 +189,17 @@ button,
|
||||
.right-arrow {
|
||||
margin-right: auto !important;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 400px) {
|
||||
.paginates {
|
||||
font-size: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 530px) {
|
||||
a {
|
||||
width: 2.5rem;
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,48 +0,0 @@
|
||||
<template>
|
||||
<div v-if="showPopup">
|
||||
<div class="popup-overlay" />
|
||||
<div class="popup-body">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Popup',
|
||||
props: {
|
||||
showPopup: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.popup-overlay {
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--color-button-bg);
|
||||
border: none;
|
||||
opacity: 0.6;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.popup-body {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 11;
|
||||
box-shadow: 0 2px 3px 1px var(--color-button-bg);
|
||||
border-radius: var(--size-rounded-lg);
|
||||
max-height: 80%;
|
||||
overflow-y: auto;
|
||||
background-color: var(--color-raised-bg);
|
||||
}
|
||||
</style>
|
||||
@ -3,17 +3,8 @@
|
||||
<div class="columns">
|
||||
<div class="icon">
|
||||
<nuxt-link :to="`/${$getProjectTypeForUrl(type, categories)}/${id}`">
|
||||
<img
|
||||
:src="iconUrl || 'https://cdn.modrinth.com/placeholder.svg?inline'"
|
||||
:alt="name"
|
||||
loading="lazy"
|
||||
/>
|
||||
<Avatar :src="iconUrl" :alt="name" size="md" />
|
||||
</nuxt-link>
|
||||
<Categories
|
||||
:categories="categories"
|
||||
:type="type"
|
||||
class="left-categories"
|
||||
/>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="info">
|
||||
@ -103,47 +94,57 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-side">
|
||||
<div v-if="downloads" class="stat">
|
||||
<DownloadIcon aria-hidden="true" />
|
||||
<p>
|
||||
<strong>{{ $formatNumber(downloads) }}</strong> download<span
|
||||
v-if="downloads !== '1'"
|
||||
>s</span
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="follows" class="stat">
|
||||
<HeartIcon aria-hidden="true" />
|
||||
<p>
|
||||
<strong>{{ $formatNumber(follows) }}</strong> follower<span
|
||||
v-if="follows !== '1'"
|
||||
>s</span
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="status" class="status">
|
||||
<Badge
|
||||
v-if="status === 'approved'"
|
||||
color="green custom-circle"
|
||||
:type="status"
|
||||
/>
|
||||
<Badge
|
||||
v-else-if="status === 'processing' || status === 'archived'"
|
||||
color="yellow custom-circle"
|
||||
:type="status"
|
||||
/>
|
||||
<Badge
|
||||
v-else-if="status === 'rejected'"
|
||||
color="red custom-circle"
|
||||
:type="status"
|
||||
/>
|
||||
<Badge v-else color="gray custom-circle" :type="status" />
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-side">
|
||||
<div v-if="downloads" class="stat">
|
||||
<DownloadIcon aria-hidden="true" />
|
||||
<p>
|
||||
<strong>{{ $formatNumber(downloads) }}</strong> download<span
|
||||
v-if="downloads !== '1'"
|
||||
>s</span
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="follows" class="stat">
|
||||
<HeartIcon aria-hidden="true" />
|
||||
<p>
|
||||
<strong>{{ $formatNumber(follows) }}</strong> follower<span
|
||||
v-if="follows !== '1'"
|
||||
>s</span
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
<div class="mobile-dates">
|
||||
<div class="date">
|
||||
<CalendarIcon aria-hidden="true" />
|
||||
Created {{ $dayjs(createdAt).fromNow() }}
|
||||
</div>
|
||||
<div class="date">
|
||||
<EditIcon aria-hidden="true" />
|
||||
Updated {{ $dayjs(updatedAt).fromNow() }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="status" class="status">
|
||||
<Badge
|
||||
v-if="status === 'approved'"
|
||||
color="green custom-circle"
|
||||
:type="status"
|
||||
/>
|
||||
<Badge
|
||||
v-else-if="status === 'processing' || status === 'archived'"
|
||||
color="yellow custom-circle"
|
||||
:type="status"
|
||||
/>
|
||||
<Badge
|
||||
v-else-if="status === 'rejected'"
|
||||
color="red custom-circle"
|
||||
:type="status"
|
||||
/>
|
||||
<Badge v-else color="gray custom-circle" :type="status" />
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
@ -158,10 +159,12 @@ import CalendarIcon from '~/assets/images/utils/calendar.svg?inline'
|
||||
import EditIcon from '~/assets/images/utils/updated.svg?inline'
|
||||
import DownloadIcon from '~/assets/images/utils/download.svg?inline'
|
||||
import HeartIcon from '~/assets/images/utils/heart.svg?inline'
|
||||
import Avatar from '~/components/ui/Avatar'
|
||||
|
||||
export default {
|
||||
name: 'ProjectCard',
|
||||
components: {
|
||||
Avatar,
|
||||
Categories,
|
||||
Badge,
|
||||
InfoIcon,
|
||||
@ -263,6 +266,7 @@ export default {
|
||||
flex-direction: row;
|
||||
padding: var(--spacing-card-bg);
|
||||
width: calc(100% - 2 * var(--spacing-card-bg));
|
||||
overflow: hidden;
|
||||
|
||||
@media screen and (min-width: 1024px) {
|
||||
flex-direction: row;
|
||||
@ -270,19 +274,14 @@ export default {
|
||||
}
|
||||
|
||||
.icon {
|
||||
img {
|
||||
width: 6rem;
|
||||
height: 6rem;
|
||||
margin: 0 var(--spacing-card-md) var(--spacing-card-md) 0;
|
||||
border-radius: var(--size-rounded-icon);
|
||||
object-fit: contain;
|
||||
}
|
||||
margin: 0 var(--spacing-card-md) var(--spacing-card-md) 0;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
@ -301,11 +300,13 @@ export default {
|
||||
overflow-wrap: anywhere;
|
||||
color: var(--color-text-dark);
|
||||
font-size: var(--font-size-xl);
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.author {
|
||||
margin: auto 0 0 0;
|
||||
color: var(--color-text);
|
||||
line-break: anywhere;
|
||||
}
|
||||
}
|
||||
|
||||
@ -315,8 +316,7 @@ export default {
|
||||
font-weight: bolder;
|
||||
font-size: var(--font-size-sm);
|
||||
|
||||
margin-top: 0.125rem;
|
||||
margin-bottom: 0.5rem;
|
||||
margin: 0.125rem 0;
|
||||
|
||||
svg {
|
||||
width: auto;
|
||||
@ -352,94 +352,125 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right-side {
|
||||
min-width: 12rem;
|
||||
text-align: right;
|
||||
.right-side {
|
||||
min-width: fit-content;
|
||||
|
||||
.stat {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
.stat {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
svg {
|
||||
width: auto;
|
||||
height: 1.25rem;
|
||||
svg {
|
||||
width: auto;
|
||||
height: 1.25rem;
|
||||
|
||||
margin-left: auto;
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
font-size: var(--font-size-lg);
|
||||
}
|
||||
}
|
||||
margin-left: auto;
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.status {
|
||||
p {
|
||||
margin: 0;
|
||||
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
font-size: var(--font-size-lg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.status {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
button,
|
||||
a {
|
||||
margin-right: 0;
|
||||
margin-left: auto;
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-dates {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
flex-wrap: wrap;
|
||||
|
||||
.card-content {
|
||||
flex-direction: column;
|
||||
|
||||
.info {
|
||||
.top {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.dates {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right-side {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
|
||||
text-align: left;
|
||||
|
||||
.stat {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.stat svg {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
button,
|
||||
a {
|
||||
margin-right: 0;
|
||||
margin-left: auto;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.left-categories {
|
||||
display: none;
|
||||
}
|
||||
.buttons button,
|
||||
a {
|
||||
margin-left: unset;
|
||||
margin-right: unset;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 560px) {
|
||||
.card-content {
|
||||
flex-direction: column;
|
||||
margin-left: 0.75rem;
|
||||
.status {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.info {
|
||||
.dates {
|
||||
.date {
|
||||
margin-bottom: 0.5rem;
|
||||
.mobile-dates {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem 0.5rem;
|
||||
color: var(--color-icon);
|
||||
font-size: var(--font-size-nm);
|
||||
|
||||
.date {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: default;
|
||||
|
||||
svg {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right-side {
|
||||
padding-top: var(--spacing-card-sm);
|
||||
|
||||
text-align: left;
|
||||
|
||||
.stat svg {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.buttons button,
|
||||
a {
|
||||
margin-left: unset;
|
||||
margin-right: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.left-categories {
|
||||
display: flex;
|
||||
margin: 0 0 0.75rem 0;
|
||||
width: 7rem;
|
||||
}
|
||||
|
||||
.right-categories {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,116 +0,0 @@
|
||||
<template>
|
||||
<div class="columns">
|
||||
<label class="button" @drop.prevent="handleDrop" @dragover.prevent>
|
||||
<span>
|
||||
<UploadIcon v-if="showIcon" />
|
||||
{{ prompt }}
|
||||
</span>
|
||||
<input
|
||||
type="file"
|
||||
:multiple="multiple"
|
||||
:accept="accept"
|
||||
@change="handleChange"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { fileIsValid } from '~/plugins/fileUtils'
|
||||
import UploadIcon from '~/assets/images/utils/upload.svg?inline'
|
||||
|
||||
export default {
|
||||
name: 'StatelessFileInput',
|
||||
components: {
|
||||
UploadIcon,
|
||||
},
|
||||
props: {
|
||||
prompt: {
|
||||
type: String,
|
||||
default: 'Select file',
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
accept: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
/**
|
||||
* The max file size in bytes
|
||||
*/
|
||||
maxSize: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
showIcon: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onChange(addedFiles) {
|
||||
this.$emit('change', addedFiles)
|
||||
},
|
||||
/**
|
||||
* @param {FileList} filesToAdd
|
||||
*/
|
||||
addFiles(filesToAdd) {
|
||||
if (!filesToAdd) return
|
||||
|
||||
const validationOptions = { maxSize: this.maxSize, alertOnInvalid: true }
|
||||
const validFiles = [...filesToAdd].filter((file) =>
|
||||
fileIsValid(file, validationOptions)
|
||||
)
|
||||
|
||||
if (validFiles.length > 0) {
|
||||
this.onChange(this.multiple ? validFiles : [validFiles[0]])
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @param {DragEvent} e
|
||||
*/
|
||||
handleDrop(e) {
|
||||
this.addFiles(e.dataTransfer.files)
|
||||
},
|
||||
/**
|
||||
* @param {Event} e native file input event
|
||||
*/
|
||||
handleChange(e) {
|
||||
this.addFiles(e.target.files)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
label {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding: var(--spacing-card-sm) var(--spacing-card-md);
|
||||
margin-bottom: var(--spacing-card-sm);
|
||||
}
|
||||
|
||||
span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
gap: 0.5rem;
|
||||
border: 2px dashed var(--color-divider-dark);
|
||||
border-radius: var(--size-rounded-control);
|
||||
padding: var(--spacing-card-md) var(--spacing-card-lg);
|
||||
|
||||
svg {
|
||||
height: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
@ -1,57 +0,0 @@
|
||||
<template>
|
||||
<div v-if="items.length !== 1" class="styled-tabs">
|
||||
<button
|
||||
v-for="item in items"
|
||||
:key="item"
|
||||
class="tab"
|
||||
:class="{ selected: selected === item }"
|
||||
@click="toggleItem(item)"
|
||||
>
|
||||
<span>{{ item }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ThisOrThat',
|
||||
props: {
|
||||
items: {
|
||||
required: true,
|
||||
type: Array,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selected: '',
|
||||
}
|
||||
},
|
||||
created() {
|
||||
if (this.items.length > 0) {
|
||||
this.selected = this.items[0]
|
||||
this.$emit('input', this.selected)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleItem(item) {
|
||||
this.selected = item
|
||||
this.$emit('input', item)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
button {
|
||||
text-transform: capitalize;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
border-radius: 0;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
button span::first-letter {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
</style>
|
||||
@ -71,7 +71,6 @@
|
||||
import Multiselect from 'vue-multiselect'
|
||||
import Checkbox from '~/components/ui/Checkbox'
|
||||
import ClearIcon from '~/assets/images/utils/clear.svg?inline'
|
||||
|
||||
export default {
|
||||
name: 'VersionFilterControl',
|
||||
components: {
|
||||
@ -144,16 +143,13 @@ export default {
|
||||
gap: var(--spacing-card-md);
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.multiselect {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.checkbox-outer {
|
||||
min-width: fit-content;
|
||||
}
|
||||
}
|
||||
|
||||
.circle-button {
|
||||
display: flex;
|
||||
max-width: 2rem;
|
||||
@ -161,18 +157,15 @@ export default {
|
||||
background-color: var(--color-button-bg);
|
||||
border-radius: var(--size-rounded-max);
|
||||
box-shadow: inset 0px -1px 1px rgba(17, 24, 39, 0.1);
|
||||
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
background-color: var(--color-button-bg-hover);
|
||||
color: var(--color-button-text-hover);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: var(--color-button-bg-active);
|
||||
color: var(--color-button-text-active);
|
||||
}
|
||||
|
||||
svg {
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
|
||||
@ -49,11 +49,11 @@ export default {
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
color: var(--color-icon);
|
||||
margin-right: 1em;
|
||||
margin-right: var(--spacing-card-md);
|
||||
|
||||
svg {
|
||||
width: 1rem;
|
||||
margin-right: 0.125rem;
|
||||
margin-right: 0.2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
0
generated/.gitkeep
Normal file
@ -1,25 +1,38 @@
|
||||
<template>
|
||||
<div class="main">
|
||||
<div class="card">
|
||||
<NuxtLink to="/">
|
||||
<h2>{{ error.message }}</h2>
|
||||
<p>
|
||||
An error occurred! Click this text to go back home, and find your way
|
||||
back!
|
||||
</p>
|
||||
</NuxtLink>
|
||||
<div class="error">
|
||||
<Logo404 v-if="error.statusCode === 404" />
|
||||
<h1 v-else>An error occured!</h1>
|
||||
<p>{{ error.message }}</p>
|
||||
<div class="button-group">
|
||||
<nuxt-link to="/" class="iconified-button raised-button brand-button">
|
||||
Go home
|
||||
</nuxt-link>
|
||||
<a
|
||||
href="https://discord.gg/EUHuJHt"
|
||||
class="iconified-button raised-button"
|
||||
>
|
||||
Get help on Discord
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Logo404 from '~/assets/images/404.svg?inline'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Logo404,
|
||||
},
|
||||
layout: 'home',
|
||||
props: {
|
||||
error: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {
|
||||
statusCode: 1000,
|
||||
message: 'Unknown error',
|
||||
}
|
||||
},
|
||||
@ -30,7 +43,25 @@ export default {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.main {
|
||||
margin: var(--spacing-card-sm) auto;
|
||||
max-width: 800px;
|
||||
margin: var(--spacing-card-lg) auto;
|
||||
width: calc(100% - 4rem);
|
||||
|
||||
@media screen and (min-width: 800px) {
|
||||
width: 600px;
|
||||
}
|
||||
}
|
||||
|
||||
.error {
|
||||
h1 {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: var(--color-text);
|
||||
color: var(--color-text);
|
||||
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -3,7 +3,11 @@ export default function (context) {
|
||||
return
|
||||
}
|
||||
|
||||
if (context.from.path === context.route.path) {
|
||||
if (
|
||||
context.from &&
|
||||
context.route &&
|
||||
context.from.path === context.route.path
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
146
nuxt.config.js
@ -1,6 +1,10 @@
|
||||
import { promises as fs } from 'fs'
|
||||
import { sortRoutes } from '@nuxt/utils'
|
||||
import axios from 'axios'
|
||||
|
||||
const STAGING_API_URL = 'https://staging-api.modrinth.com/v2/'
|
||||
const STAGING_ARIADNE_URL = 'https://staging-ariadne.modrinth.com/v1/'
|
||||
|
||||
export default {
|
||||
/*
|
||||
** Nuxt target
|
||||
@ -15,7 +19,7 @@ export default {
|
||||
htmlAttrs: {
|
||||
lang: 'en',
|
||||
},
|
||||
title: 'Modrinth: Download and publish Minecraft Mods',
|
||||
title: 'Modrinth',
|
||||
meta: [
|
||||
{
|
||||
charset: 'utf-8',
|
||||
@ -28,14 +32,18 @@ export default {
|
||||
hid: 'description',
|
||||
name: 'description',
|
||||
content:
|
||||
'Download Minecraft Fabric and Forge mods on Modrinth. Discover and publish projects on Modrinth with a modern, easy to use interface and API.',
|
||||
'Download Minecraft mods, plugins, resource packs, and modpacks on Modrinth. Discover and publish projects on Modrinth with a modern, easy to use interface and API.',
|
||||
},
|
||||
|
||||
{
|
||||
hid: 'publisher',
|
||||
name: 'publisher',
|
||||
content: 'Rinth, Inc.',
|
||||
},
|
||||
{
|
||||
hid: 'og:title',
|
||||
name: 'og:title',
|
||||
content: 'Modrinth',
|
||||
},
|
||||
{
|
||||
hid: 'apple-mobile-web-app-title',
|
||||
name: 'apple-mobile-web-app-title',
|
||||
@ -44,14 +52,13 @@ export default {
|
||||
{
|
||||
hid: 'theme-color',
|
||||
name: 'theme-color',
|
||||
content: '#30b27b',
|
||||
content: '#1bd96a',
|
||||
},
|
||||
{
|
||||
hid: 'color-scheme',
|
||||
name: 'color-scheme',
|
||||
content: 'light dark',
|
||||
},
|
||||
|
||||
{
|
||||
hid: 'og:site_name',
|
||||
name: 'og:site_name',
|
||||
@ -62,11 +69,6 @@ export default {
|
||||
name: 'og:description',
|
||||
content: 'An open source modding platform',
|
||||
},
|
||||
{
|
||||
hid: 'og:title',
|
||||
name: 'og:title',
|
||||
content: 'Modrinth',
|
||||
},
|
||||
{
|
||||
hid: 'og:type',
|
||||
name: 'og:type',
|
||||
@ -75,12 +77,12 @@ export default {
|
||||
{
|
||||
hid: 'og:url',
|
||||
name: 'og:url',
|
||||
content: 'https://www.modrinth.com',
|
||||
content: 'https://modrinth.com',
|
||||
},
|
||||
{
|
||||
hid: 'og:image',
|
||||
name: 'og:image',
|
||||
content: 'https://cdn.modrinth.com/modrinth-new.png',
|
||||
content: 'https://cdn.modrinth.com/modrinth-new.png?',
|
||||
},
|
||||
{
|
||||
hid: 'twitter:card',
|
||||
@ -94,10 +96,23 @@ export default {
|
||||
},
|
||||
],
|
||||
link: [
|
||||
{
|
||||
rel: 'icon',
|
||||
type: 'image/x-icon',
|
||||
href: '/favicon-light.ico',
|
||||
media: '(prefers-color-scheme:no-preference)',
|
||||
},
|
||||
{
|
||||
rel: 'icon',
|
||||
type: 'image/x-icon',
|
||||
href: '/favicon.ico',
|
||||
media: '(prefers-color-scheme:dark)',
|
||||
},
|
||||
{
|
||||
rel: 'icon',
|
||||
type: 'image/x-icon',
|
||||
href: '/favicon-light.ico',
|
||||
media: '(prefers-color-scheme:light)',
|
||||
},
|
||||
{
|
||||
rel: 'stylesheet',
|
||||
@ -114,7 +129,7 @@ export default {
|
||||
|
||||
vue: {
|
||||
config: {
|
||||
devtools: false,
|
||||
devtools: true,
|
||||
},
|
||||
},
|
||||
router: {
|
||||
@ -194,7 +209,6 @@ export default {
|
||||
'@nuxtjs/dayjs',
|
||||
'@nuxtjs/axios',
|
||||
'@nuxtjs/robots',
|
||||
'@nuxtjs/sitemap',
|
||||
'@nuxtjs/style-resources',
|
||||
'@nuxtjs/markdownit',
|
||||
'cookie-universal-nuxt',
|
||||
@ -207,24 +221,12 @@ export default {
|
||||
robots: {
|
||||
Sitemap: 'https://modrinth.com/sitemap.xml',
|
||||
},
|
||||
sitemap: {
|
||||
exclude: [
|
||||
'/settings/**',
|
||||
'/settings',
|
||||
'/notifications',
|
||||
'/moderation',
|
||||
'/search',
|
||||
'/search/**',
|
||||
'/create/**',
|
||||
],
|
||||
routes: ['mods', 'modpacks', 'resourcepacks', 'plugins'],
|
||||
},
|
||||
/*
|
||||
** Axios module configuration
|
||||
** See https://axios.nuxtjs.org/options
|
||||
*/
|
||||
axios: {
|
||||
baseURL: 'https://staging-api.modrinth.com/v2/',
|
||||
baseURL: getApiUrl(),
|
||||
headers: {
|
||||
common: {
|
||||
Accept: 'application/json',
|
||||
@ -260,7 +262,6 @@ export default {
|
||||
},
|
||||
},
|
||||
markdownit: {
|
||||
runtime: true,
|
||||
preset: 'default',
|
||||
html: true,
|
||||
linkify: true,
|
||||
@ -276,8 +277,7 @@ export default {
|
||||
branch: process.env.VERCEL_GIT_COMMIT_REF || 'master',
|
||||
hash: process.env.VERCEL_GIT_COMMIT_SHA || 'unknown',
|
||||
domain: getDomain(),
|
||||
authURLBase:
|
||||
process.env.BROWSER_BASE_URL || 'https://staging-api.modrinth.com/v2/',
|
||||
authURLBase: getApiUrl(),
|
||||
},
|
||||
publicRuntimeConfig: {
|
||||
axios: {
|
||||
@ -287,9 +287,7 @@ export default {
|
||||
ethicalAds: process.env.ETHICAL_ADS,
|
||||
},
|
||||
analytics: {
|
||||
base_url:
|
||||
process.env.BROWSER_ARIADNE_URL ||
|
||||
'https://staging-ariadne.modrinth.com/v1/',
|
||||
base_url: process.env.BROWSER_ARIADNE_URL || STAGING_ARIADNE_URL,
|
||||
},
|
||||
},
|
||||
privateRuntimeConfig: {
|
||||
@ -303,15 +301,83 @@ export default {
|
||||
},
|
||||
},
|
||||
hooks: {
|
||||
build: {
|
||||
async before(nuxt, buildOptions) {
|
||||
// 30 minutes
|
||||
const TTL = 30 * 60 * 1000
|
||||
|
||||
let state = {}
|
||||
try {
|
||||
state = JSON.parse(
|
||||
await fs.readFile('./generated/state.json', 'utf8')
|
||||
)
|
||||
} catch {
|
||||
// File doesn't exist, create folder
|
||||
await fs.mkdir('./generated', { recursive: true })
|
||||
}
|
||||
|
||||
const API_URL = getApiUrl()
|
||||
|
||||
if (
|
||||
// Skip regeneration if within TTL...
|
||||
state.lastGenerated &&
|
||||
new Date(state.lastGenerated).getTime() + TTL >
|
||||
new Date().getTime() &&
|
||||
// ...but only if the API URL is the same
|
||||
state.apiUrl &&
|
||||
state.apiUrl === API_URL
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
console.log('Generating tags...')
|
||||
|
||||
state.lastGenerated = new Date().toISOString()
|
||||
|
||||
state.apiUrl = API_URL
|
||||
|
||||
const headers = {
|
||||
headers: {
|
||||
'user-agent': `Knossos generator (admin@modrinth.com)`,
|
||||
},
|
||||
}
|
||||
|
||||
const [
|
||||
categories,
|
||||
loaders,
|
||||
gameVersions,
|
||||
licenses,
|
||||
donationPlatforms,
|
||||
reportTypes,
|
||||
] = (
|
||||
await Promise.all([
|
||||
axios.get(`${API_URL}tag/category`, headers),
|
||||
axios.get(`${API_URL}tag/loader`, headers),
|
||||
axios.get(`${API_URL}tag/game_version`, headers),
|
||||
axios.get(`${API_URL}tag/license`, headers),
|
||||
axios.get(`${API_URL}tag/donation_platform`, headers),
|
||||
axios.get(`${API_URL}tag/report_type`, headers),
|
||||
])
|
||||
).map((it) => it.data)
|
||||
|
||||
state.categories = categories
|
||||
state.loaders = loaders
|
||||
state.gameVersions = gameVersions
|
||||
state.licenses = licenses
|
||||
state.donationPlatforms = donationPlatforms
|
||||
state.reportTypes = reportTypes
|
||||
|
||||
await fs.writeFile('./generated/state.json', JSON.stringify(state))
|
||||
|
||||
console.log('Tags generated!')
|
||||
},
|
||||
},
|
||||
render: {
|
||||
routeDone(url, result, context) {
|
||||
setTimeout(() => {
|
||||
axios
|
||||
.post(
|
||||
`${
|
||||
process.env.ARIADNE_URL ||
|
||||
'https://staging-ariadne.modrinth.com/v1/'
|
||||
}view`,
|
||||
`${process.env.ARIADNE_URL || STAGING_ARIADNE_URL}view`,
|
||||
{
|
||||
url: getDomain() + url,
|
||||
},
|
||||
@ -340,6 +406,10 @@ export default {
|
||||
},
|
||||
}
|
||||
|
||||
function getApiUrl() {
|
||||
return process.env.BROWSER_BASE_URL ?? STAGING_API_URL
|
||||
}
|
||||
|
||||
function getDomain() {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
if (process.env.SITE_URL) {
|
||||
@ -348,6 +418,8 @@ function getDomain() {
|
||||
return `https://${process.env.HEROKU_APP_NAME}.herokuapp.com`
|
||||
} else if (process.env.VERCEL_URL) {
|
||||
return `https://${process.env.VERCEL_URL}`
|
||||
} else if (getApiUrl() === STAGING_API_URL) {
|
||||
return 'https://staging.modrinth.com'
|
||||
} else {
|
||||
return 'https://modrinth.com'
|
||||
}
|
||||
|
||||
167
package-lock.json
generated
@ -10,10 +10,8 @@
|
||||
"dependencies": {
|
||||
"@nuxtjs/axios": "^5.13.1",
|
||||
"@nuxtjs/dayjs": "^1.2.0",
|
||||
"@nuxtjs/google-adsense": "^1.3.0",
|
||||
"@nuxtjs/markdownit": "^2.0.0",
|
||||
"@nuxtjs/robots": "^2.4.2",
|
||||
"@nuxtjs/sitemap": "^2.4.0",
|
||||
"@nuxtjs/style-resources": "^1.0.0",
|
||||
"cookie-universal-nuxt": "^2.1.5",
|
||||
"core-js": "^3.9.1",
|
||||
@ -2947,11 +2945,6 @@
|
||||
"integrity": "sha512-t2MZGLf1V2rV4VBZbWIaXKdX/mUcYW0n2znQZoADBkGGxYL8EWqCuCZBmJPJ/Yy9fofJkyuuSuo5GSwo0XdEgw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@nuxtjs/google-adsense": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@nuxtjs/google-adsense/-/google-adsense-1.4.0.tgz",
|
||||
"integrity": "sha512-C5PfE/IE/VhqhomZWNNbTmVIPjB2o+2iRXZA16QoPMA0n2cotfGqt1CkECTYPyFhN89beMa7ioOic9/+ESLPXg=="
|
||||
},
|
||||
"node_modules/@nuxtjs/markdownit": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@nuxtjs/markdownit/-/markdownit-2.0.0.tgz",
|
||||
@ -2990,26 +2983,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@nuxtjs/robots/-/robots-2.5.0.tgz",
|
||||
"integrity": "sha512-z1F3HXb05NiZga8Cuq6k5bbowfJOScPtbSOakip0nege+1aI9pGoajzap8eR5s1qwLXAk9Ts+NcgetoUn5lwrQ=="
|
||||
},
|
||||
"node_modules/@nuxtjs/sitemap": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@nuxtjs/sitemap/-/sitemap-2.4.0.tgz",
|
||||
"integrity": "sha512-TVgIYOtPp7KAfaUo76WRpGbO20j4D/xi/A7shFIGjARHs+FvfAWXNCtBT87dTwe/RoYzAsEKtijFFUTaSu5bUA==",
|
||||
"dependencies": {
|
||||
"async-cache": "^1.1.0",
|
||||
"consola": "^2.13.0",
|
||||
"etag": "^1.8.1",
|
||||
"fresh": "^0.5.2",
|
||||
"fs-extra": "^8.1.0",
|
||||
"is-https": "^2.0.2",
|
||||
"lodash.unionby": "^4.8.0",
|
||||
"minimatch": "^3.0.4",
|
||||
"sitemap": "^4.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.9.0",
|
||||
"npm": ">=5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nuxtjs/style-resources": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@nuxtjs/style-resources/-/style-resources-1.2.1.tgz",
|
||||
@ -3120,14 +3093,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.5.tgz",
|
||||
"integrity": "sha512-L28j2FcJfSZOnL1WBjDYp2vUHCeIFlyYI/53EwD/rKUBQ7MtUUfbQWiyKJGpcnv4/WgrhWsFKrcPstcAt/J0tQ=="
|
||||
},
|
||||
"node_modules/@types/sax": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/sax/-/sax-1.2.4.tgz",
|
||||
"integrity": "sha512-pSAff4IAxJjfAXUG6tFkO7dsSbTmf8CtUpfhhZ5VhkRpC4628tJhh3+V6H1E+/Gs9piSzYKT5yzHO5M4GG9jkw==",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/source-list-map": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz",
|
||||
@ -3968,15 +3933,6 @@
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz",
|
||||
"integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ=="
|
||||
},
|
||||
"node_modules/async-cache": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/async-cache/-/async-cache-1.1.0.tgz",
|
||||
"integrity": "sha512-YDQc4vBn5NFhY6g6HhVshyi3Fy9+SQ5ePnE7JLDJn1DoL+i7ER+vMwtTNOYk9leZkYMnOwpBCWqyLDPw8Aig8g==",
|
||||
"deprecated": "No longer maintained. Use [lru-cache](http://npm.im/lru-cache) version 7.6 or higher, and provide an asynchronous `fetchMethod` option.",
|
||||
"dependencies": {
|
||||
"lru-cache": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/async-each": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz",
|
||||
@ -8859,11 +8815,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-https": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/is-https/-/is-https-2.0.2.tgz",
|
||||
"integrity": "sha512-UfUCKVQH/6PQRCh5Qk9vNu4feLZiFmV/gr8DjbtJD0IrCRIDTA6E+d/AVFGPulI5tqK5W45fYbn1Nir1O99rFw=="
|
||||
},
|
||||
"node_modules/is-negative-zero": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz",
|
||||
@ -9348,11 +9299,6 @@
|
||||
"integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/lodash.unionby": {
|
||||
"version": "4.8.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.unionby/-/lodash.unionby-4.8.0.tgz",
|
||||
"integrity": "sha512-e60kn4GJIunNkw6v9MxRnUuLYI/Tyuanch7ozoCtk/1irJTYBj+qNTxr5B3qVflmJhwStJBv387Cb+9VOfABMg=="
|
||||
},
|
||||
"node_modules/lodash.uniq": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
|
||||
@ -13216,35 +13162,6 @@
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/sitemap": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/sitemap/-/sitemap-4.1.1.tgz",
|
||||
"integrity": "sha512-+8yd66IxyIFEMFkFpVoPuoPwBvdiL7Ap/HS5YD7igqO4phkyTPFIprCAE9NMHehAY5ZGN3MkAze4lDrOAX3sVQ==",
|
||||
"dependencies": {
|
||||
"@types/node": "^12.0.2",
|
||||
"@types/sax": "^1.2.0",
|
||||
"arg": "^4.1.1",
|
||||
"sax": "^1.2.4",
|
||||
"xmlbuilder": "^13.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"sitemap": "dist/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.9.0",
|
||||
"npm": ">=5.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sitemap/node_modules/@types/node": {
|
||||
"version": "12.20.55",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz",
|
||||
"integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ=="
|
||||
},
|
||||
"node_modules/sitemap/node_modules/arg": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
|
||||
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="
|
||||
},
|
||||
"node_modules/slash": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
|
||||
@ -16476,14 +16393,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xmlbuilder": {
|
||||
"version": "13.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-13.0.2.tgz",
|
||||
"integrity": "sha512-Eux0i2QdDYKbdbA6AM6xE4m6ZTZr4G4xF9kahI2ukSEMCzwce2eX9WlTI5J3s+NU7hpasFsr8hWIONae7LluAQ==",
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/xss": {
|
||||
"version": "1.0.13",
|
||||
"resolved": "https://registry.npmjs.org/xss/-/xss-1.0.13.tgz",
|
||||
@ -18788,11 +18697,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"@nuxtjs/google-adsense": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@nuxtjs/google-adsense/-/google-adsense-1.4.0.tgz",
|
||||
"integrity": "sha512-C5PfE/IE/VhqhomZWNNbTmVIPjB2o+2iRXZA16QoPMA0n2cotfGqt1CkECTYPyFhN89beMa7ioOic9/+ESLPXg=="
|
||||
},
|
||||
"@nuxtjs/markdownit": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@nuxtjs/markdownit/-/markdownit-2.0.0.tgz",
|
||||
@ -18833,22 +18737,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@nuxtjs/robots/-/robots-2.5.0.tgz",
|
||||
"integrity": "sha512-z1F3HXb05NiZga8Cuq6k5bbowfJOScPtbSOakip0nege+1aI9pGoajzap8eR5s1qwLXAk9Ts+NcgetoUn5lwrQ=="
|
||||
},
|
||||
"@nuxtjs/sitemap": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@nuxtjs/sitemap/-/sitemap-2.4.0.tgz",
|
||||
"integrity": "sha512-TVgIYOtPp7KAfaUo76WRpGbO20j4D/xi/A7shFIGjARHs+FvfAWXNCtBT87dTwe/RoYzAsEKtijFFUTaSu5bUA==",
|
||||
"requires": {
|
||||
"async-cache": "^1.1.0",
|
||||
"consola": "^2.13.0",
|
||||
"etag": "^1.8.1",
|
||||
"fresh": "^0.5.2",
|
||||
"fs-extra": "^8.1.0",
|
||||
"is-https": "^2.0.2",
|
||||
"lodash.unionby": "^4.8.0",
|
||||
"minimatch": "^3.0.4",
|
||||
"sitemap": "^4.1.1"
|
||||
}
|
||||
},
|
||||
"@nuxtjs/style-resources": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@nuxtjs/style-resources/-/style-resources-1.2.1.tgz",
|
||||
@ -18954,14 +18842,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.5.tgz",
|
||||
"integrity": "sha512-L28j2FcJfSZOnL1WBjDYp2vUHCeIFlyYI/53EwD/rKUBQ7MtUUfbQWiyKJGpcnv4/WgrhWsFKrcPstcAt/J0tQ=="
|
||||
},
|
||||
"@types/sax": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/sax/-/sax-1.2.4.tgz",
|
||||
"integrity": "sha512-pSAff4IAxJjfAXUG6tFkO7dsSbTmf8CtUpfhhZ5VhkRpC4628tJhh3+V6H1E+/Gs9piSzYKT5yzHO5M4GG9jkw==",
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/source-list-map": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz",
|
||||
@ -19632,14 +19512,6 @@
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz",
|
||||
"integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ=="
|
||||
},
|
||||
"async-cache": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/async-cache/-/async-cache-1.1.0.tgz",
|
||||
"integrity": "sha512-YDQc4vBn5NFhY6g6HhVshyi3Fy9+SQ5ePnE7JLDJn1DoL+i7ER+vMwtTNOYk9leZkYMnOwpBCWqyLDPw8Aig8g==",
|
||||
"requires": {
|
||||
"lru-cache": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"async-each": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz",
|
||||
@ -23332,11 +23204,6 @@
|
||||
"is-extglob": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"is-https": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/is-https/-/is-https-2.0.2.tgz",
|
||||
"integrity": "sha512-UfUCKVQH/6PQRCh5Qk9vNu4feLZiFmV/gr8DjbtJD0IrCRIDTA6E+d/AVFGPulI5tqK5W45fYbn1Nir1O99rFw=="
|
||||
},
|
||||
"is-negative-zero": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz",
|
||||
@ -23702,11 +23569,6 @@
|
||||
"integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==",
|
||||
"dev": true
|
||||
},
|
||||
"lodash.unionby": {
|
||||
"version": "4.8.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.unionby/-/lodash.unionby-4.8.0.tgz",
|
||||
"integrity": "sha512-e60kn4GJIunNkw6v9MxRnUuLYI/Tyuanch7ozoCtk/1irJTYBj+qNTxr5B3qVflmJhwStJBv387Cb+9VOfABMg=="
|
||||
},
|
||||
"lodash.uniq": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
|
||||
@ -26762,30 +26624,6 @@
|
||||
"totalist": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"sitemap": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/sitemap/-/sitemap-4.1.1.tgz",
|
||||
"integrity": "sha512-+8yd66IxyIFEMFkFpVoPuoPwBvdiL7Ap/HS5YD7igqO4phkyTPFIprCAE9NMHehAY5ZGN3MkAze4lDrOAX3sVQ==",
|
||||
"requires": {
|
||||
"@types/node": "^12.0.2",
|
||||
"@types/sax": "^1.2.0",
|
||||
"arg": "^4.1.1",
|
||||
"sax": "^1.2.4",
|
||||
"xmlbuilder": "^13.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/node": {
|
||||
"version": "12.20.55",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz",
|
||||
"integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ=="
|
||||
},
|
||||
"arg": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
|
||||
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"slash": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
|
||||
@ -29290,11 +29128,6 @@
|
||||
"integrity": "sha512-ri1Id1WinAX5Jqn9HejiGb8crfRio0Qgu8+MtL36rlTA6RLsMdWt1Az/19A2Qij6uSHUMphEFaTKa4WG+UNHNw==",
|
||||
"requires": {}
|
||||
},
|
||||
"xmlbuilder": {
|
||||
"version": "13.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-13.0.2.tgz",
|
||||
"integrity": "sha512-Eux0i2QdDYKbdbA6AM6xE4m6ZTZr4G4xF9kahI2ukSEMCzwce2eX9WlTI5J3s+NU7hpasFsr8hWIONae7LluAQ=="
|
||||
},
|
||||
"xss": {
|
||||
"version": "1.0.13",
|
||||
"resolved": "https://registry.npmjs.org/xss/-/xss-1.0.13.tgz",
|
||||
|
||||
@ -15,10 +15,8 @@
|
||||
"dependencies": {
|
||||
"@nuxtjs/axios": "^5.13.1",
|
||||
"@nuxtjs/dayjs": "^1.2.0",
|
||||
"@nuxtjs/google-adsense": "^1.3.0",
|
||||
"@nuxtjs/markdownit": "^2.0.0",
|
||||
"@nuxtjs/robots": "^2.4.2",
|
||||
"@nuxtjs/sitemap": "^2.4.0",
|
||||
"@nuxtjs/style-resources": "^1.0.0",
|
||||
"cookie-universal-nuxt": "^2.1.5",
|
||||
"core-js": "^3.9.1",
|
||||
|
||||
1528
pages/_type/_id.vue
@ -6,56 +6,63 @@
|
||||
@updateVersions="updateVersions"
|
||||
/>
|
||||
<div class="card">
|
||||
<div v-for="version in filteredVersions" :key="version.id">
|
||||
<div class="version-header">
|
||||
<span :class="'circle ' + version.version_type" />
|
||||
<div class="version-header-text">
|
||||
<h2 class="name title-link">
|
||||
<nuxt-link
|
||||
:to="`/${project.project_type}/${
|
||||
project.slug ? project.slug : project.id
|
||||
}/version/${encodeURI(version.displayUrlEnding)}`"
|
||||
>{{ version.name }}</nuxt-link
|
||||
>
|
||||
</h2>
|
||||
<span v-if="members.find((x) => x.user.id === version.author_id)">
|
||||
by
|
||||
<nuxt-link
|
||||
class="text-link"
|
||||
:to="
|
||||
'/user/' +
|
||||
members.find((x) => x.user.id === version.author_id).user
|
||||
.username
|
||||
"
|
||||
>{{
|
||||
members.find((x) => x.user.id === version.author_id).user
|
||||
.username
|
||||
}}</nuxt-link
|
||||
>
|
||||
</span>
|
||||
<span>
|
||||
on
|
||||
{{ $dayjs(version.date_published).format('MMM D, YYYY') }}</span
|
||||
>
|
||||
</div>
|
||||
<a
|
||||
:href="$parent.findPrimary(version).url"
|
||||
class="iconified-button download"
|
||||
:title="`Download ${version.name}`"
|
||||
>
|
||||
<DownloadIcon aria-hidden="true" />
|
||||
Download
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
v-for="version in filteredVersions"
|
||||
:key="version.id"
|
||||
class="changelog-item"
|
||||
>
|
||||
<div
|
||||
v-highlightjs
|
||||
:class="'markdown-body ' + version.version_type"
|
||||
v-html="
|
||||
version.changelog
|
||||
? $xss($md.render(version.changelog))
|
||||
: 'No changelog specified.'
|
||||
"
|
||||
/>
|
||||
:class="`changelog-bar ${version.version_type} ${
|
||||
version.duplicate ? 'duplicate' : ''
|
||||
}`"
|
||||
></div>
|
||||
<div class="version-wrapper">
|
||||
<div class="version-header">
|
||||
<div class="version-header-text">
|
||||
<h2 class="name">
|
||||
<nuxt-link
|
||||
:to="`/${project.project_type}/${
|
||||
project.slug ? project.slug : project.id
|
||||
}/version/${encodeURI(version.displayUrlEnding)}`"
|
||||
>{{ version.name }}</nuxt-link
|
||||
>
|
||||
</h2>
|
||||
<span v-if="members.find((x) => x.user.id === version.author_id)">
|
||||
by
|
||||
<nuxt-link
|
||||
class="text-link"
|
||||
:to="
|
||||
'/user/' +
|
||||
members.find((x) => x.user.id === version.author_id).user
|
||||
.username
|
||||
"
|
||||
>{{
|
||||
members.find((x) => x.user.id === version.author_id).user
|
||||
.username
|
||||
}}</nuxt-link
|
||||
>
|
||||
</span>
|
||||
<span>
|
||||
on
|
||||
{{ $dayjs(version.date_published).format('MMM D, YYYY') }}</span
|
||||
>
|
||||
</div>
|
||||
<a
|
||||
:href="$parent.findPrimary(version).url"
|
||||
class="iconified-button download"
|
||||
:title="`Download ${version.name}`"
|
||||
>
|
||||
<DownloadIcon aria-hidden="true" />
|
||||
Download
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
v-if="version.changelog && !version.duplicate"
|
||||
v-highlightjs
|
||||
class="markdown-body"
|
||||
v-html="$xss($md.render(version.changelog))"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -66,8 +73,8 @@ import VersionFilterControl from '~/components/ui/VersionFilterControl'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DownloadIcon,
|
||||
VersionFilterControl,
|
||||
DownloadIcon,
|
||||
},
|
||||
props: {
|
||||
project: {
|
||||
@ -92,21 +99,145 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
filteredVersions: this.versions,
|
||||
currentPage: 1,
|
||||
}
|
||||
},
|
||||
fetch() {
|
||||
if (this.$route.query.page)
|
||||
this.currentPage = parseInt(this.$route.query.page)
|
||||
|
||||
this.filteredVersions = this.versions.map((version, index) => {
|
||||
const nextVersion = this.versions[index + 1]
|
||||
if (
|
||||
nextVersion &&
|
||||
version.changelog &&
|
||||
nextVersion.changelog === version.changelog
|
||||
) {
|
||||
return { duplicate: true, ...version }
|
||||
} else {
|
||||
return { duplicate: false, ...version }
|
||||
}
|
||||
})
|
||||
},
|
||||
head() {
|
||||
const title = `${this.project.title} - Changelog`
|
||||
const description = `Explore the changelog of ${this.project.title}'s ${this.versions.length} versions.`
|
||||
|
||||
return {
|
||||
title,
|
||||
meta: [
|
||||
{
|
||||
hid: 'og:title',
|
||||
name: 'og:title',
|
||||
content: title,
|
||||
},
|
||||
{
|
||||
hid: 'apple-mobile-web-app-title',
|
||||
name: 'apple-mobile-web-app-title',
|
||||
content: title,
|
||||
},
|
||||
{
|
||||
hid: 'og:description',
|
||||
name: 'og:description',
|
||||
content: description,
|
||||
},
|
||||
{
|
||||
hid: 'description',
|
||||
name: 'description',
|
||||
content: description,
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
auth: false,
|
||||
methods: {
|
||||
switchPage(page, toTop) {
|
||||
this.currentPage = page
|
||||
this.$router.replace(this.getPageLink(page))
|
||||
|
||||
if (toTop) {
|
||||
setTimeout(() => window.scrollTo({ top: 0, behavior: 'smooth' }), 50)
|
||||
setTimeout(() => window.scrollTo({ top: 0, behavior: 'smooth' }), 50)
|
||||
}
|
||||
},
|
||||
getPageLink(page) {
|
||||
if (page === 1) {
|
||||
return this.$route.path
|
||||
} else {
|
||||
return `${this.$route.path}?page=${this.currentPage}`
|
||||
}
|
||||
},
|
||||
updateVersions(updatedVersions) {
|
||||
this.filteredVersions = updatedVersions
|
||||
},
|
||||
},
|
||||
auth: false,
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.changelog-item {
|
||||
display: flex;
|
||||
margin-bottom: 1rem;
|
||||
position: relative;
|
||||
padding-left: 1.8rem;
|
||||
|
||||
.changelog-bar {
|
||||
--color: var(--color-badge-green-bg);
|
||||
|
||||
&.alpha {
|
||||
--color: var(--color-badge-red-bg);
|
||||
}
|
||||
|
||||
&.release {
|
||||
--color: var(--color-badge-green-bg);
|
||||
}
|
||||
|
||||
&.beta {
|
||||
--color: var(--color-badge-yellow-bg);
|
||||
}
|
||||
|
||||
left: 0;
|
||||
top: 0.5rem;
|
||||
width: 0.2rem;
|
||||
min-width: 0.2rem;
|
||||
position: absolute;
|
||||
margin: 0 0.4rem;
|
||||
border-radius: var(--size-rounded-max);
|
||||
min-height: 100%;
|
||||
background-color: var(--color);
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -0.4rem;
|
||||
border-radius: var(--size-rounded-max);
|
||||
background-color: var(--color);
|
||||
}
|
||||
|
||||
&.duplicate {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
transparent,
|
||||
transparent 30%,
|
||||
var(--color) 30%,
|
||||
var(--color)
|
||||
);
|
||||
background-size: 100% 10px;
|
||||
}
|
||||
|
||||
&.duplicate {
|
||||
height: calc(100% + 1.5rem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.version-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 0.2rem;
|
||||
|
||||
.circle {
|
||||
min-width: 0.75rem;
|
||||
@ -114,24 +245,11 @@ export default {
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin-right: 0.25rem;
|
||||
|
||||
&.alpha {
|
||||
background-color: var(--color-badge-red-bg);
|
||||
}
|
||||
|
||||
&.release {
|
||||
background-color: var(--color-badge-green-bg);
|
||||
}
|
||||
|
||||
&.beta {
|
||||
background-color: var(--color-badge-yellow-bg);
|
||||
}
|
||||
}
|
||||
|
||||
.version-header-text {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
margin: 0 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
|
||||
h2 {
|
||||
@ -155,20 +273,6 @@ export default {
|
||||
}
|
||||
|
||||
.markdown-body {
|
||||
margin: 0.5rem 0.5rem 1rem calc(0.375rem - 1px);
|
||||
padding-left: 1.275rem;
|
||||
border-left: 2px solid var(--color-text);
|
||||
|
||||
&.alpha {
|
||||
border-left-color: var(--color-badge-red-bg);
|
||||
}
|
||||
|
||||
&.release {
|
||||
border-left-color: var(--color-badge-green-bg);
|
||||
}
|
||||
|
||||
&.beta {
|
||||
border-left-color: var(--color-badge-yellow-bg);
|
||||
}
|
||||
margin: 0.5rem 0.5rem 0 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,40 +1,42 @@
|
||||
<template>
|
||||
<div class="page-contents">
|
||||
<header class="card">
|
||||
<div class="columns">
|
||||
<h3 class="column-grow-1">Edit project</h3>
|
||||
<nuxt-link
|
||||
:to="`/${project.project_type}/${
|
||||
project.slug ? project.slug : project.id
|
||||
}/settings`"
|
||||
class="iconified-button column"
|
||||
>
|
||||
<CrossIcon />
|
||||
Cancel
|
||||
</nuxt-link>
|
||||
<button
|
||||
v-if="
|
||||
project.status === 'rejected' ||
|
||||
project.status === 'draft' ||
|
||||
project.status === 'unlisted'
|
||||
"
|
||||
title="Submit for review"
|
||||
class="iconified-button column"
|
||||
:disabled="!$nuxt.$loading"
|
||||
@click="saveProjectReview"
|
||||
>
|
||||
<CheckIcon />
|
||||
Submit for review
|
||||
</button>
|
||||
<button
|
||||
title="Save"
|
||||
class="iconified-button brand-button-colors column"
|
||||
:disabled="!$nuxt.$loading"
|
||||
@click="saveProjectNotForReview"
|
||||
>
|
||||
<SaveIcon />
|
||||
Save changes
|
||||
</button>
|
||||
<div class="page-contents legacy-label-styles">
|
||||
<header class="header-card">
|
||||
<div class="header__row">
|
||||
<h2 class="header__title">Edit project</h2>
|
||||
<div class="input-group">
|
||||
<nuxt-link
|
||||
:to="`/${project.project_type}/${
|
||||
project.slug ? project.slug : project.id
|
||||
}/settings`"
|
||||
class="iconified-button column"
|
||||
>
|
||||
<CrossIcon />
|
||||
Cancel
|
||||
</nuxt-link>
|
||||
<button
|
||||
v-if="
|
||||
project.status === 'rejected' ||
|
||||
project.status === 'draft' ||
|
||||
project.status === 'unlisted'
|
||||
"
|
||||
title="Submit for review"
|
||||
class="iconified-button column"
|
||||
:disabled="!$nuxt.$loading"
|
||||
@click="saveProjectReview"
|
||||
>
|
||||
<CheckIcon />
|
||||
Submit for review
|
||||
</button>
|
||||
<button
|
||||
title="Save"
|
||||
class="iconified-button brand-button column"
|
||||
:disabled="!$nuxt.$loading"
|
||||
@click="saveProjectNotForReview"
|
||||
>
|
||||
<SaveIcon />
|
||||
Save changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="showKnownErrors" class="known-errors">
|
||||
<ul>
|
||||
@ -43,7 +45,7 @@
|
||||
Your project must have a summary.
|
||||
</li>
|
||||
<li v-if="newProject.slug === ''">
|
||||
Your project must have a vanity URL.
|
||||
Your project cannot have an empty URL suffix.
|
||||
</li>
|
||||
<li v-if="!savingAsDraft && newProject.body === ''">
|
||||
Your project must have a body to submit for review.
|
||||
@ -74,6 +76,7 @@
|
||||
:class="{ 'known-error': newProject.title === '' && showKnownErrors }"
|
||||
type="text"
|
||||
placeholder="Enter the name"
|
||||
maxlength="64"
|
||||
:disabled="
|
||||
(currentMember.permissions & EDIT_DETAILS) !== EDIT_DETAILS
|
||||
"
|
||||
@ -94,6 +97,7 @@
|
||||
}"
|
||||
type="text"
|
||||
placeholder="Enter the summary"
|
||||
maxlength="256"
|
||||
:disabled="
|
||||
(currentMember.permissions & EDIT_DETAILS) !== EDIT_DETAILS
|
||||
"
|
||||
@ -164,45 +168,49 @@
|
||||
@input="setCategories"
|
||||
/>
|
||||
</label>
|
||||
<label class="vertical-input">
|
||||
<span>
|
||||
<h3>Vanity URL (slug)<span class="required">*</span></h3>
|
||||
<span class="slug-description"
|
||||
>https://modrinth.com/{{ project.project_type.toLowerCase() }}/{{
|
||||
newProject.slug ? newProject.slug : 'your-slug'
|
||||
}}
|
||||
</span>
|
||||
</span>
|
||||
<input
|
||||
id="name"
|
||||
v-model="newProject.slug"
|
||||
<div class="universal-labels">
|
||||
<label for="slug">
|
||||
<span class="label__title">URL<span class="required">*</span></span>
|
||||
</label>
|
||||
<div
|
||||
class="text-input-wrapper"
|
||||
:class="{ 'known-error': newProject.slug === '' && showKnownErrors }"
|
||||
type="text"
|
||||
placeholder="Enter the vanity URL"
|
||||
:disabled="
|
||||
(currentMember.permissions & EDIT_DETAILS) !== EDIT_DETAILS
|
||||
"
|
||||
/>
|
||||
</label>
|
||||
>
|
||||
<div class="text-input-wrapper__before">
|
||||
https://modrinth.com/{{ project.project_type.toLowerCase() }}/
|
||||
</div>
|
||||
<!-- this is a textarea so it is horizontally scrollable on mobile -->
|
||||
<textarea
|
||||
id="slug"
|
||||
v-model="newProject.slug"
|
||||
type="text"
|
||||
maxlength="64"
|
||||
autocorrect="off"
|
||||
autocomplete="off"
|
||||
autocapitalize="none"
|
||||
rows="1"
|
||||
:disabled="
|
||||
(currentMember.permissions & EDIT_DETAILS) !== EDIT_DETAILS
|
||||
"
|
||||
@input="manualSlug = true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="card project-icon">
|
||||
<h3>Icon</h3>
|
||||
<img
|
||||
:src="
|
||||
previewImage
|
||||
? previewImage
|
||||
: newProject.icon_url && !iconChanged
|
||||
? newProject.icon_url
|
||||
: 'https://cdn.modrinth.com/placeholder.svg'
|
||||
"
|
||||
<Avatar
|
||||
size="lg"
|
||||
class="avatar"
|
||||
:src="previewImage ? previewImage : newProject.icon_url"
|
||||
alt="preview-image"
|
||||
/>
|
||||
<SmartFileInput
|
||||
<FileInput
|
||||
:max-size="262144"
|
||||
:show-icon="false"
|
||||
:show-icon="true"
|
||||
accept="image/png,image/jpeg,image/gif,image/webp"
|
||||
class="choose-image"
|
||||
prompt="Choose image or drag it here"
|
||||
prompt="Choose image"
|
||||
:disabled="(currentMember.permissions & EDIT_DETAILS) !== EDIT_DETAILS"
|
||||
@change="showPreviewImage"
|
||||
/>
|
||||
@ -212,11 +220,11 @@
|
||||
@click="
|
||||
icon = null
|
||||
previewImage = null
|
||||
iconChanged = true
|
||||
iconChanged = false
|
||||
"
|
||||
>
|
||||
<TrashIcon />
|
||||
Reset
|
||||
<RevertIcon />
|
||||
Revert
|
||||
</button>
|
||||
</section>
|
||||
<section
|
||||
@ -289,7 +297,7 @@
|
||||
>. HTML can also be used inside your description, not including styles,
|
||||
scripts, and iframes (though YouTube iframes are allowed).
|
||||
</span>
|
||||
<ThisOrThat
|
||||
<Chips
|
||||
v-model="bodyViewMode"
|
||||
class="separator"
|
||||
:items="['source', 'preview']"
|
||||
@ -329,6 +337,7 @@
|
||||
v-model="newProject.issues_url"
|
||||
type="url"
|
||||
placeholder="Enter a valid URL"
|
||||
maxlength="2048"
|
||||
:disabled="
|
||||
(currentMember.permissions & EDIT_DETAILS) !== EDIT_DETAILS
|
||||
"
|
||||
@ -341,6 +350,7 @@
|
||||
<input
|
||||
v-model="newProject.source_url"
|
||||
type="url"
|
||||
maxlength="2048"
|
||||
placeholder="Enter a valid URL"
|
||||
/>
|
||||
</label>
|
||||
@ -351,17 +361,22 @@
|
||||
<input
|
||||
v-model="newProject.wiki_url"
|
||||
type="url"
|
||||
maxlength="2048"
|
||||
placeholder="Enter a valid URL"
|
||||
:disabled="
|
||||
(currentMember.permissions & EDIT_DETAILS) !== EDIT_DETAILS
|
||||
"
|
||||
/>
|
||||
</label>
|
||||
<label title="An invitation link to your Discord server.">
|
||||
<label
|
||||
class="no-margin"
|
||||
title="An invitation link to your Discord server."
|
||||
>
|
||||
<span>Discord invite</span>
|
||||
<input
|
||||
v-model="newProject.discord_url"
|
||||
type="url"
|
||||
maxlength="2048"
|
||||
placeholder="Enter a valid URL"
|
||||
:disabled="
|
||||
(currentMember.permissions & EDIT_DETAILS) !== EDIT_DETAILS
|
||||
@ -389,7 +404,7 @@
|
||||
>
|
||||
for more information.
|
||||
</span>
|
||||
<div class="input-group">
|
||||
<div class="legacy-input-group">
|
||||
<Multiselect
|
||||
v-model="license"
|
||||
placeholder="Choose license..."
|
||||
@ -407,7 +422,11 @@
|
||||
<input
|
||||
v-model="license_url"
|
||||
type="url"
|
||||
maxlength="2048"
|
||||
placeholder="License URL"
|
||||
:class="{
|
||||
'known-error': newProject.license_url === '' && showKnownErrors,
|
||||
}"
|
||||
:disabled="
|
||||
(currentMember.permissions & EDIT_DETAILS) !== EDIT_DETAILS
|
||||
"
|
||||
@ -415,21 +434,23 @@
|
||||
</div>
|
||||
</label>
|
||||
</section>
|
||||
<section class="card donations">
|
||||
<div class="title">
|
||||
<h3>Donation links</h3>
|
||||
<button
|
||||
title="Add a link"
|
||||
class="iconified-button"
|
||||
:disabled="false"
|
||||
@click="
|
||||
donationPlatforms.push({})
|
||||
donationLinks.push('')
|
||||
"
|
||||
>
|
||||
<PlusIcon />
|
||||
Add a link
|
||||
</button>
|
||||
<section class="header-card donations">
|
||||
<div class="header__row">
|
||||
<h3 class="header__title">Donation links</h3>
|
||||
<div class="input-group">
|
||||
<button
|
||||
title="Add a link"
|
||||
class="iconified-button"
|
||||
:disabled="false"
|
||||
@click="
|
||||
donationPlatforms.push({})
|
||||
donationLinks.push('')
|
||||
"
|
||||
>
|
||||
<PlusIcon />
|
||||
Add a link
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-for="(item, index) in donationPlatforms" :key="index">
|
||||
<label title="The donation link.">
|
||||
@ -438,6 +459,7 @@
|
||||
v-model="donationLinks[index]"
|
||||
type="url"
|
||||
placeholder="Enter a valid URL"
|
||||
class="donation-link-input"
|
||||
/>
|
||||
</label>
|
||||
<label title="The donation platform of the link.">
|
||||
@ -482,29 +504,24 @@ import CheckIcon from '~/assets/images/utils/check.svg?inline'
|
||||
import PlusIcon from '~/assets/images/utils/plus.svg?inline'
|
||||
import SaveIcon from '~/assets/images/utils/save.svg?inline'
|
||||
import TrashIcon from '~/assets/images/utils/trash.svg?inline'
|
||||
import RevertIcon from '~/assets/images/utils/undo.svg?inline'
|
||||
|
||||
import ThisOrThat from '~/components/ui/ThisOrThat'
|
||||
import SmartFileInput from '~/components/ui/SmartFileInput'
|
||||
import Chips from '~/components/ui/Chips'
|
||||
import FileInput from '~/components/ui/FileInput'
|
||||
import Avatar from '~/components/ui/Avatar'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SmartFileInput,
|
||||
ThisOrThat,
|
||||
Avatar,
|
||||
FileInput,
|
||||
Chips,
|
||||
Multiselect,
|
||||
CrossIcon,
|
||||
CheckIcon,
|
||||
PlusIcon,
|
||||
SaveIcon,
|
||||
TrashIcon,
|
||||
},
|
||||
beforeRouteLeave(to, from, next) {
|
||||
if (
|
||||
this.isEditing &&
|
||||
!window.confirm('Are you sure that you want to leave without saving?')
|
||||
) {
|
||||
return
|
||||
}
|
||||
next()
|
||||
RevertIcon,
|
||||
},
|
||||
props: {
|
||||
project: {
|
||||
@ -550,6 +567,7 @@ export default {
|
||||
|
||||
showKnownErrors: false,
|
||||
savingAsDraft: false,
|
||||
manualSlug: false,
|
||||
}
|
||||
},
|
||||
fetch() {
|
||||
@ -601,16 +619,6 @@ export default {
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
function preventLeave(e) {
|
||||
e.preventDefault()
|
||||
e.returnValue = ''
|
||||
}
|
||||
window.addEventListener('beforeunload', preventLeave)
|
||||
this.$once('hook:beforeDestroy', () => {
|
||||
window.removeEventListener('beforeunload', preventLeave)
|
||||
})
|
||||
},
|
||||
created() {
|
||||
this.UPLOAD_VERSION = 1 << 0
|
||||
this.DELETE_VERSION = 1 << 1
|
||||
@ -643,7 +651,7 @@ export default {
|
||||
const reviewConditions =
|
||||
this.newProject.body !== '' && this.newProject.versions.length > 0
|
||||
if (
|
||||
this.newProject.name !== '' &&
|
||||
this.newProject.title !== '' &&
|
||||
this.newProject.description !== '' &&
|
||||
this.newProject.slug !== '' &&
|
||||
this.license.short !== null &&
|
||||
@ -789,13 +797,13 @@ label {
|
||||
|
||||
input,
|
||||
.multiselect,
|
||||
.input-group {
|
||||
.legacy-input-group {
|
||||
flex: 3;
|
||||
height: fit-content;
|
||||
}
|
||||
}
|
||||
|
||||
.input-group {
|
||||
.legacy-input-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@ -849,24 +857,10 @@ label {
|
||||
|
||||
header {
|
||||
grid-area: header;
|
||||
padding: var(--spacing-card-md) var(--spacing-card-lg);
|
||||
|
||||
h3 {
|
||||
margin: auto 0;
|
||||
color: var(--color-text-dark);
|
||||
font-weight: var(--font-weight-extrabold);
|
||||
}
|
||||
|
||||
button {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
section.essentials {
|
||||
grid-area: essentials;
|
||||
label {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1024px) {
|
||||
input {
|
||||
@ -877,15 +871,17 @@ section.essentials {
|
||||
|
||||
section.project-icon {
|
||||
grid-area: project-icon;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--spacing-card-sm);
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
margin-bottom: 0.25rem;
|
||||
border-radius: var(--size-rounded-lg);
|
||||
.avatar {
|
||||
margin-bottom: var(--spacing-card-sm);
|
||||
}
|
||||
|
||||
.iconified-button {
|
||||
margin-top: 0.5rem;
|
||||
margin-top: var(--spacing-card-sm);
|
||||
}
|
||||
}
|
||||
|
||||
@ -901,10 +897,6 @@ section.game-sides {
|
||||
|
||||
.labeled-control {
|
||||
margin-left: var(--spacing-card-lg);
|
||||
|
||||
h3 {
|
||||
margin-bottom: var(--spacing-card-sm);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -959,10 +951,6 @@ section.donations {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
@ -973,7 +961,9 @@ section.donations {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.card {
|
||||
.card,
|
||||
.universal-card,
|
||||
.header-card {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@ -991,4 +981,35 @@ section.donations {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.legacy-input-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
* {
|
||||
margin-bottom: var(--spacing-card-sm);
|
||||
}
|
||||
}
|
||||
|
||||
.text-input-wrapper {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin-left: 0 !important;
|
||||
white-space: nowrap;
|
||||
overflow-x: auto;
|
||||
overflow-y: none;
|
||||
resize: none;
|
||||
min-height: 0;
|
||||
}
|
||||
margin-bottom: var(--spacing-card-md);
|
||||
}
|
||||
|
||||
.donation-link-input {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -32,7 +32,7 @@
|
||||
</p>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<div v-if="gallery.length > 1" class="buttons">
|
||||
<div class="buttons">
|
||||
<button
|
||||
class="close circle-button"
|
||||
@click="expandedGalleryItem = null"
|
||||
@ -54,10 +54,18 @@
|
||||
<ExpandIcon v-if="!zoomedIn" aria-hidden="true" />
|
||||
<ContractIcon v-else aria-hidden="true" />
|
||||
</button>
|
||||
<button class="previous circle-button" @click="previousImage()">
|
||||
<button
|
||||
v-if="gallery.length > 1"
|
||||
class="previous circle-button"
|
||||
@click="previousImage()"
|
||||
>
|
||||
<LeftArrowIcon aria-hidden="true" />
|
||||
</button>
|
||||
<button class="next circle-button" @click="nextImage()">
|
||||
<button
|
||||
v-if="gallery.length > 1"
|
||||
class="next circle-button"
|
||||
@click="nextImage()"
|
||||
>
|
||||
<RightArrowIcon aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
@ -66,6 +74,32 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="currentMember" class="card buttons header-buttons">
|
||||
<button
|
||||
class="iconified-button"
|
||||
:class="{
|
||||
'brand-button':
|
||||
newGalleryItems.length === 0 &&
|
||||
editGalleryIndexes.length === 0 &&
|
||||
deleteGalleryUrls.length === 0,
|
||||
}"
|
||||
@click="
|
||||
newGalleryItems.push({
|
||||
title: '',
|
||||
description: '',
|
||||
featured: false,
|
||||
url: '',
|
||||
})
|
||||
"
|
||||
>
|
||||
<PlusIcon />
|
||||
{{
|
||||
newGalleryItems.length === 0 &&
|
||||
editGalleryIndexes.length === 0 &&
|
||||
deleteGalleryUrls.length === 0
|
||||
? 'Add an image'
|
||||
: 'Add another image'
|
||||
}}
|
||||
</button>
|
||||
<button
|
||||
v-if="
|
||||
newGalleryItems.length > 0 ||
|
||||
@ -84,26 +118,12 @@
|
||||
editGalleryIndexes.length > 0 ||
|
||||
deleteGalleryUrls.length > 0
|
||||
"
|
||||
class="action brand-button-colors iconified-button"
|
||||
class="action brand-button iconified-button"
|
||||
@click="saveGallery"
|
||||
>
|
||||
<CheckIcon />
|
||||
Save changes
|
||||
</button>
|
||||
<button
|
||||
class="iconified-button"
|
||||
@click="
|
||||
newGalleryItems.push({
|
||||
title: '',
|
||||
description: '',
|
||||
featured: false,
|
||||
url: '',
|
||||
})
|
||||
"
|
||||
>
|
||||
<PlusIcon />
|
||||
Add an image
|
||||
</button>
|
||||
</div>
|
||||
<div class="items">
|
||||
<div
|
||||
@ -147,7 +167,7 @@
|
||||
<CalendarIcon />
|
||||
{{ $dayjs(item.created).format('MMMM D, YYYY') }}
|
||||
</div>
|
||||
<div v-if="currentMember" class="gallery-buttons">
|
||||
<div v-if="currentMember" class="gallery-buttons input-group">
|
||||
<button
|
||||
v-if="editGalleryIndexes.includes(index)"
|
||||
class="iconified-button"
|
||||
@ -213,7 +233,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="gallery-bottom">
|
||||
<SmartFileInput
|
||||
<FileInput
|
||||
:max-size="5242880"
|
||||
accept="image/png,image/jpeg,image/gif,image/webp,.png,.jpeg,.gif,.webp"
|
||||
prompt="Choose image or drag it here"
|
||||
@ -249,7 +269,7 @@ import ExternalIcon from '~/assets/images/utils/external.svg?inline'
|
||||
import ExpandIcon from '~/assets/images/utils/expand.svg?inline'
|
||||
import ContractIcon from '~/assets/images/utils/contract.svg?inline'
|
||||
|
||||
import SmartFileInput from '~/components/ui/SmartFileInput'
|
||||
import FileInput from '~/components/ui/FileInput'
|
||||
import Checkbox from '~/components/ui/Checkbox'
|
||||
|
||||
export default {
|
||||
@ -260,7 +280,7 @@ export default {
|
||||
EditIcon,
|
||||
TrashIcon,
|
||||
CheckIcon,
|
||||
SmartFileInput,
|
||||
FileInput,
|
||||
CrossIcon,
|
||||
RightArrowIcon,
|
||||
LeftArrowIcon,
|
||||
@ -302,6 +322,36 @@ export default {
|
||||
fetch() {
|
||||
this.gallery = JSON.parse(JSON.stringify(this.project.gallery))
|
||||
},
|
||||
head() {
|
||||
const title = `${this.project.title} - Gallery`
|
||||
const description = `View ${this.project.gallery.length} images of ${this.project.title} on Modrinth.`
|
||||
|
||||
return {
|
||||
title,
|
||||
meta: [
|
||||
{
|
||||
hid: 'og:title',
|
||||
name: 'og:title',
|
||||
content: title,
|
||||
},
|
||||
{
|
||||
hid: 'apple-mobile-web-app-title',
|
||||
name: 'apple-mobile-web-app-title',
|
||||
content: title,
|
||||
},
|
||||
{
|
||||
hid: 'og:description',
|
||||
name: 'og:description',
|
||||
content: description,
|
||||
},
|
||||
{
|
||||
hid: 'description',
|
||||
name: 'description',
|
||||
content: description,
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this._keyListener = function (e) {
|
||||
if (this.expandedGalleryItem) {
|
||||
@ -456,6 +506,7 @@ export default {
|
||||
line-height: 1;
|
||||
display: flex;
|
||||
max-width: 2rem;
|
||||
color: var(--color-button-text);
|
||||
background-color: var(--color-button-bg);
|
||||
border-radius: var(--size-rounded-max);
|
||||
margin: 0;
|
||||
@ -563,18 +614,6 @@ export default {
|
||||
|
||||
button {
|
||||
margin-right: 0.5rem;
|
||||
|
||||
&.brand-button-colors {
|
||||
background-color: var(--color-brand);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-brand-hover);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: var(--color-brand-active);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -22,6 +22,8 @@ export default {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.markdown-body {
|
||||
max-width: calc(100% - (2 * var(--spacing-card-lg)));
|
||||
max-width: calc(
|
||||
60rem - 2 * var(--spacing-card-lg) - 9px
|
||||
); // $2.50 to anyone who can figure out why the 9px is needed
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,103 +1,94 @@
|
||||
<template>
|
||||
<div>
|
||||
<ConfirmPopup
|
||||
ref="delete_popup"
|
||||
<ModalConfirm
|
||||
ref="modal_confirm"
|
||||
title="Are you sure you want to delete this project?"
|
||||
description="If you proceed, all versions and any attached data will be removed from our servers. This may break other projects, so be careful."
|
||||
:has-to-type="true"
|
||||
:confirmation-text="project.title"
|
||||
proceed-label="Delete project"
|
||||
proceed-label="Delete"
|
||||
@proceed="deleteProject"
|
||||
/>
|
||||
<div class="card">
|
||||
<h3>General</h3>
|
||||
</div>
|
||||
<section class="card main-settings">
|
||||
<label>
|
||||
<span>
|
||||
<h3>Edit project</h3>
|
||||
<span>
|
||||
This leads you to a page where you can edit your project.
|
||||
<div class="universal-card">
|
||||
<h2>General settings</h2>
|
||||
<div class="adjacent-input">
|
||||
<label>
|
||||
<span class="label__title">Edit project information</span>
|
||||
<span class="label__description">
|
||||
Edit your project's name, description, categories, and more.
|
||||
</span>
|
||||
</span>
|
||||
<div>
|
||||
<nuxt-link class="iconified-button" to="edit"
|
||||
><EditIcon />Edit</nuxt-link
|
||||
>
|
||||
</div>
|
||||
</label>
|
||||
<label>
|
||||
<span>
|
||||
<h3>Create a version</h3>
|
||||
<span>
|
||||
This leads to a page where you can create a version for your
|
||||
project.
|
||||
</span>
|
||||
</span>
|
||||
<div>
|
||||
<nuxt-link
|
||||
class="iconified-button"
|
||||
to="version/create"
|
||||
:disabled="
|
||||
(currentMember.permissions & UPLOAD_VERSION) !== UPLOAD_VERSION
|
||||
"
|
||||
><PlusIcon />Create a version</nuxt-link
|
||||
>
|
||||
</div>
|
||||
</label>
|
||||
<label>
|
||||
<span>
|
||||
<h3>Delete project</h3>
|
||||
<span>
|
||||
</label>
|
||||
<nuxt-link class="iconified-button" to="edit"
|
||||
><EditIcon />Edit</nuxt-link
|
||||
>
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<span class="label">
|
||||
<span class="label__title">Delete project</span>
|
||||
<span class="label__description">
|
||||
Removes your project from Modrinth's servers and search. Clicking on
|
||||
this will delete your project, so be extra careful!
|
||||
</span>
|
||||
</span>
|
||||
<div
|
||||
class="iconified-button"
|
||||
<button
|
||||
class="iconified-button danger-button"
|
||||
:disabled="
|
||||
(currentMember.permissions & DELETE_PROJECT) !== DELETE_PROJECT
|
||||
"
|
||||
@click="showPopup"
|
||||
@click="$refs.modal_confirm.show()"
|
||||
>
|
||||
<TrashIcon />Delete project
|
||||
</div>
|
||||
</label>
|
||||
</section>
|
||||
<div class="card columns team-invite">
|
||||
<h3>Team members</h3>
|
||||
<div
|
||||
v-if="(currentMember.permissions & MANAGE_INVITES) === MANAGE_INVITES"
|
||||
class="column"
|
||||
>
|
||||
<input
|
||||
id="username"
|
||||
v-model="currentUsername"
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
/>
|
||||
<label for="username" class="hidden">Username</label>
|
||||
<button
|
||||
class="iconified-button brand-button-colors column"
|
||||
@click="inviteTeamMember"
|
||||
>
|
||||
<PlusIcon />
|
||||
Invite
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="universal-card">
|
||||
<h2>Manage members</h2>
|
||||
<div class="adjacent-input">
|
||||
<span class="label">
|
||||
<span class="label__title">Invite a member</span>
|
||||
<span class="label__description">
|
||||
Enter the Modrinth username of the person you'd like to invite to be
|
||||
a member of this project.
|
||||
</span>
|
||||
</span>
|
||||
<div
|
||||
v-if="(currentMember.permissions & MANAGE_INVITES) === MANAGE_INVITES"
|
||||
class="input-group"
|
||||
>
|
||||
<input
|
||||
id="username"
|
||||
v-model="currentUsername"
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
/>
|
||||
<label for="username" class="hidden">Username</label>
|
||||
<button
|
||||
class="iconified-button brand-button"
|
||||
@click="inviteTeamMember"
|
||||
>
|
||||
<PlusIcon />
|
||||
Invite
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-for="(member, index) in allTeamMembers"
|
||||
:key="member.user.id"
|
||||
class="card member"
|
||||
class="universal-card member"
|
||||
:class="{ open: openTeamMembers.includes(member.user.id) }"
|
||||
>
|
||||
<div class="member-header">
|
||||
<div class="info">
|
||||
<img :src="member.avatar_url" :alt="member.name" />
|
||||
<Avatar
|
||||
:src="member.avatar_url"
|
||||
:alt="member.username"
|
||||
size="sm"
|
||||
circle
|
||||
/>
|
||||
<div class="text">
|
||||
<nuxt-link :to="'/user/' + member.user.username" class="name">
|
||||
<p class="title-link">{{ member.name }}</p>
|
||||
<p>{{ member.name }}</p>
|
||||
</nuxt-link>
|
||||
<p>{{ member.role }}</p>
|
||||
</div>
|
||||
@ -106,7 +97,6 @@
|
||||
<Badge v-if="member.accepted" type="accepted" color="green" />
|
||||
<Badge v-else type="pending" color="yellow" />
|
||||
<button
|
||||
v-if="member.role !== 'Owner'"
|
||||
class="dropdown-icon"
|
||||
@click="
|
||||
openTeamMembers.indexOf(member.user.id) === -1
|
||||
@ -121,98 +111,148 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="main-info">
|
||||
<label>
|
||||
Role:
|
||||
<input
|
||||
v-model="allTeamMembers[index].role"
|
||||
type="text"
|
||||
:class="{ 'known-error': member.role === 'Owner' }"
|
||||
:disabled="
|
||||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER
|
||||
"
|
||||
/>
|
||||
<div v-if="member.oldRole !== 'Owner'" class="adjacent-input">
|
||||
<label :for="`member-${allTeamMembers[index].user.username}-role`">
|
||||
<span class="label__title">Role</span>
|
||||
<span class="label__description">
|
||||
The title of the role that this member plays for this project.
|
||||
</span>
|
||||
</label>
|
||||
<ul v-if="member.role === 'Owner'" class="known-errors">
|
||||
<li>A project can only have one 'Owner'.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<h3>Permissions</h3>
|
||||
<div class="permissions">
|
||||
<Checkbox
|
||||
:value="(member.permissions & UPLOAD_VERSION) === UPLOAD_VERSION"
|
||||
:disabled="
|
||||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
||||
(currentMember.permissions & UPLOAD_VERSION) !== UPLOAD_VERSION
|
||||
"
|
||||
label="Upload version"
|
||||
@input="allTeamMembers[index].permissions ^= UPLOAD_VERSION"
|
||||
/>
|
||||
<Checkbox
|
||||
:value="(member.permissions & DELETE_VERSION) === DELETE_VERSION"
|
||||
:disabled="
|
||||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
||||
(currentMember.permissions & DELETE_VERSION) !== DELETE_VERSION
|
||||
"
|
||||
label="Delete version"
|
||||
@input="allTeamMembers[index].permissions ^= DELETE_VERSION"
|
||||
/>
|
||||
<Checkbox
|
||||
:value="(member.permissions & EDIT_DETAILS) === EDIT_DETAILS"
|
||||
:disabled="
|
||||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
||||
(currentMember.permissions & EDIT_DETAILS) !== EDIT_DETAILS
|
||||
"
|
||||
label="Edit details"
|
||||
@input="allTeamMembers[index].permissions ^= EDIT_DETAILS"
|
||||
/>
|
||||
<Checkbox
|
||||
:value="(member.permissions & EDIT_BODY) === EDIT_BODY"
|
||||
:disabled="
|
||||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
||||
(currentMember.permissions & EDIT_BODY) !== EDIT_BODY
|
||||
"
|
||||
label="Edit body"
|
||||
@input="allTeamMembers[index].permissions ^= EDIT_BODY"
|
||||
/>
|
||||
<Checkbox
|
||||
:value="(member.permissions & MANAGE_INVITES) === MANAGE_INVITES"
|
||||
:disabled="
|
||||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
||||
(currentMember.permissions & MANAGE_INVITES) !== MANAGE_INVITES
|
||||
"
|
||||
label="Manage invites"
|
||||
@input="allTeamMembers[index].permissions ^= MANAGE_INVITES"
|
||||
/>
|
||||
<Checkbox
|
||||
:value="(member.permissions & REMOVE_MEMBER) === REMOVE_MEMBER"
|
||||
:disabled="
|
||||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
||||
(currentMember.permissions & REMOVE_MEMBER) !== REMOVE_MEMBER
|
||||
"
|
||||
label="Remove member"
|
||||
@input="allTeamMembers[index].permissions ^= REMOVE_MEMBER"
|
||||
/>
|
||||
<Checkbox
|
||||
:value="(member.permissions & EDIT_MEMBER) === EDIT_MEMBER"
|
||||
<input
|
||||
:id="`member-${allTeamMembers[index].user.username}-role`"
|
||||
v-model="allTeamMembers[index].role"
|
||||
type="text"
|
||||
:class="{ 'known-error': member.role === 'Owner' }"
|
||||
:disabled="
|
||||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER
|
||||
"
|
||||
label="Edit member"
|
||||
@input="allTeamMembers[index].permissions ^= EDIT_MEMBER"
|
||||
/>
|
||||
<Checkbox
|
||||
:value="(member.permissions & DELETE_PROJECT) === DELETE_PROJECT"
|
||||
:disabled="
|
||||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
||||
(currentMember.permissions & DELETE_PROJECT) !== DELETE_PROJECT
|
||||
"
|
||||
label="Delete project"
|
||||
@input="allTeamMembers[index].permissions ^= DELETE_PROJECT"
|
||||
/>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<div class="adjacent-input">
|
||||
<label
|
||||
:for="`member-${allTeamMembers[index].user.username}-monetization-weight`"
|
||||
>
|
||||
<span class="label__title">Monetization weight</span>
|
||||
<span class="label__description">
|
||||
Relative to all other members' monetization weights, this
|
||||
determines what portion of this project's revenue goes to this
|
||||
member.
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
:id="`member-${allTeamMembers[index].user.username}-monetization-weight`"
|
||||
v-model="allTeamMembers[index].payouts_split"
|
||||
type="number"
|
||||
:disabled="
|
||||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<ul
|
||||
v-if="member.role === 'Owner' && member.oldRole !== 'Owner'"
|
||||
class="known-errors"
|
||||
>
|
||||
<li>A project can only have one 'Owner'.</li>
|
||||
</ul>
|
||||
<template v-if="member.oldRole !== 'Owner'">
|
||||
<span class="label">
|
||||
<span class="label__title">Permissions</span>
|
||||
</span>
|
||||
<div class="permissions">
|
||||
<Checkbox
|
||||
:value="(member.permissions & UPLOAD_VERSION) === UPLOAD_VERSION"
|
||||
:disabled="
|
||||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
||||
(currentMember.permissions & UPLOAD_VERSION) !== UPLOAD_VERSION
|
||||
"
|
||||
label="Upload version"
|
||||
@input="allTeamMembers[index].permissions ^= UPLOAD_VERSION"
|
||||
/>
|
||||
<Checkbox
|
||||
:value="(member.permissions & DELETE_VERSION) === DELETE_VERSION"
|
||||
:disabled="
|
||||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
||||
(currentMember.permissions & DELETE_VERSION) !== DELETE_VERSION
|
||||
"
|
||||
label="Delete version"
|
||||
@input="allTeamMembers[index].permissions ^= DELETE_VERSION"
|
||||
/>
|
||||
<Checkbox
|
||||
:value="(member.permissions & EDIT_DETAILS) === EDIT_DETAILS"
|
||||
:disabled="
|
||||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
||||
(currentMember.permissions & EDIT_DETAILS) !== EDIT_DETAILS
|
||||
"
|
||||
label="Edit details"
|
||||
@input="allTeamMembers[index].permissions ^= EDIT_DETAILS"
|
||||
/>
|
||||
<Checkbox
|
||||
:value="(member.permissions & EDIT_BODY) === EDIT_BODY"
|
||||
:disabled="
|
||||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
||||
(currentMember.permissions & EDIT_BODY) !== EDIT_BODY
|
||||
"
|
||||
label="Edit body"
|
||||
@input="allTeamMembers[index].permissions ^= EDIT_BODY"
|
||||
/>
|
||||
<Checkbox
|
||||
:value="(member.permissions & MANAGE_INVITES) === MANAGE_INVITES"
|
||||
:disabled="
|
||||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
||||
(currentMember.permissions & MANAGE_INVITES) !== MANAGE_INVITES
|
||||
"
|
||||
label="Manage invites"
|
||||
@input="allTeamMembers[index].permissions ^= MANAGE_INVITES"
|
||||
/>
|
||||
<Checkbox
|
||||
:value="(member.permissions & REMOVE_MEMBER) === REMOVE_MEMBER"
|
||||
:disabled="
|
||||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
||||
(currentMember.permissions & REMOVE_MEMBER) !== REMOVE_MEMBER
|
||||
"
|
||||
label="Remove member"
|
||||
@input="allTeamMembers[index].permissions ^= REMOVE_MEMBER"
|
||||
/>
|
||||
<Checkbox
|
||||
:value="(member.permissions & EDIT_MEMBER) === EDIT_MEMBER"
|
||||
:disabled="
|
||||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER
|
||||
"
|
||||
label="Edit member"
|
||||
@input="allTeamMembers[index].permissions ^= EDIT_MEMBER"
|
||||
/>
|
||||
<Checkbox
|
||||
:value="(member.permissions & DELETE_PROJECT) === DELETE_PROJECT"
|
||||
:disabled="
|
||||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
||||
(currentMember.permissions & DELETE_PROJECT) !== DELETE_PROJECT
|
||||
"
|
||||
label="Delete project"
|
||||
@input="allTeamMembers[index].permissions ^= DELETE_PROJECT"
|
||||
/>
|
||||
<Checkbox
|
||||
:value="(member.permissions & VIEW_ANALYTICS) === VIEW_ANALYTICS"
|
||||
:disabled="
|
||||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
||||
(currentMember.permissions & VIEW_ANALYTICS) !== VIEW_ANALYTICS
|
||||
"
|
||||
label="View analytics"
|
||||
@input="allTeamMembers[index].permissions ^= VIEW_ANALYTICS"
|
||||
/>
|
||||
<Checkbox
|
||||
:value="(member.permissions & VIEW_PAYOUTS) === VIEW_PAYOUTS"
|
||||
:disabled="
|
||||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
||||
(currentMember.permissions & VIEW_PAYOUTS) !== VIEW_PAYOUTS
|
||||
"
|
||||
label="View revenue"
|
||||
@input="allTeamMembers[index].permissions ^= VIEW_PAYOUTS"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<div class="button-group push-right">
|
||||
<button
|
||||
v-if="member.oldRole !== 'Owner'"
|
||||
class="iconified-button"
|
||||
:disabled="
|
||||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER
|
||||
@ -224,7 +264,7 @@
|
||||
</button>
|
||||
<button
|
||||
v-if="
|
||||
member.role !== 'Owner' &&
|
||||
member.oldRole !== 'Owner' &&
|
||||
currentMember.role === 'Owner' &&
|
||||
member.accepted
|
||||
"
|
||||
@ -235,10 +275,9 @@
|
||||
Transfer ownership
|
||||
</button>
|
||||
<button
|
||||
class="iconified-button brand-button-colors"
|
||||
class="iconified-button brand-button"
|
||||
:disabled="
|
||||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
||||
member.role === 'Owner'
|
||||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER
|
||||
"
|
||||
@click="updateTeamMember(index)"
|
||||
>
|
||||
@ -252,7 +291,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ConfirmPopup from '~/components/ui/ConfirmPopup'
|
||||
import ModalConfirm from '~/components/ui/ModalConfirm'
|
||||
import Checkbox from '~/components/ui/Checkbox'
|
||||
import Badge from '~/components/ui/Badge'
|
||||
|
||||
@ -262,11 +301,13 @@ import CheckIcon from '~/assets/images/utils/check.svg?inline'
|
||||
import EditIcon from '~/assets/images/utils/edit.svg?inline'
|
||||
import TrashIcon from '~/assets/images/utils/trash.svg?inline'
|
||||
import UserIcon from '~/assets/images/utils/user.svg?inline'
|
||||
import Avatar from '~/components/ui/Avatar'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Avatar,
|
||||
DropdownIcon,
|
||||
ConfirmPopup,
|
||||
ModalConfirm,
|
||||
Checkbox,
|
||||
Badge,
|
||||
PlusIcon,
|
||||
@ -304,6 +345,8 @@ export default {
|
||||
},
|
||||
fetch() {
|
||||
this.allTeamMembers = this.allMembers
|
||||
|
||||
this.allTeamMembers.forEach((x) => (x.oldRole = x.role))
|
||||
},
|
||||
created() {
|
||||
this.UPLOAD_VERSION = 1 << 0
|
||||
@ -314,6 +357,8 @@ export default {
|
||||
this.REMOVE_MEMBER = 1 << 5
|
||||
this.EDIT_MEMBER = 1 << 6
|
||||
this.DELETE_PROJECT = 1 << 7
|
||||
this.VIEW_ANALYTICS = 1 << 8
|
||||
this.VIEW_PAYOUTS = 1 << 9
|
||||
},
|
||||
methods: {
|
||||
async inviteTeamMember() {
|
||||
@ -368,10 +413,16 @@ export default {
|
||||
this.$nuxt.$loading.start()
|
||||
|
||||
try {
|
||||
const data = {
|
||||
permissions: this.allTeamMembers[index].permissions,
|
||||
role: this.allTeamMembers[index].role,
|
||||
}
|
||||
const data =
|
||||
this.allTeamMembers[index].oldRole !== 'Owner'
|
||||
? {
|
||||
permissions: this.allTeamMembers[index].permissions,
|
||||
role: this.allTeamMembers[index].role,
|
||||
payouts_split: this.allTeamMembers[index].payouts_split,
|
||||
}
|
||||
: {
|
||||
payouts_split: this.allTeamMembers[index].payouts_split,
|
||||
}
|
||||
|
||||
await this.$axios.patch(
|
||||
`team/${this.project.team}/members/${this.allTeamMembers[index].user.id}`,
|
||||
@ -413,14 +464,6 @@ export default {
|
||||
|
||||
this.$nuxt.$loading.finish()
|
||||
},
|
||||
showPopup() {
|
||||
if (
|
||||
(this.currentMember.permissions & this.DELETE_PROJECT) ===
|
||||
this.DELETE_PROJECT
|
||||
) {
|
||||
this.$refs.delete_popup.show()
|
||||
}
|
||||
},
|
||||
async deleteProject() {
|
||||
await this.$axios.delete(
|
||||
`project/${this.project.id}`,
|
||||
@ -444,6 +487,7 @@ export default {
|
||||
).data.map((it) => ({
|
||||
avatar_url: it.user.avatar_url,
|
||||
name: it.user.username,
|
||||
oldRole: it.role,
|
||||
...it,
|
||||
}))
|
||||
},
|
||||
@ -452,26 +496,12 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card {
|
||||
h3 {
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.member {
|
||||
margin-bottom: var(--spacing-card-md);
|
||||
|
||||
.member-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
.info {
|
||||
display: flex;
|
||||
img {
|
||||
border-radius: var(--size-rounded-icon);
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
}
|
||||
.text {
|
||||
margin: auto 0 auto 0.5rem;
|
||||
font-size: var(--font-size-sm);
|
||||
@ -499,33 +529,18 @@ export default {
|
||||
|
||||
.content {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
padding-top: var(--spacing-card-md);
|
||||
|
||||
.main-info {
|
||||
margin-bottom: var(--spacing-card-lg);
|
||||
|
||||
@media screen and (min-width: 1024px) {
|
||||
label {
|
||||
align-items: center;
|
||||
input {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.permissions {
|
||||
margin: 1rem 0;
|
||||
margin-bottom: var(--spacing-card-md);
|
||||
max-width: 45rem;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
|
||||
grid-gap: 0.5rem;
|
||||
|
||||
label {
|
||||
flex-direction: row;
|
||||
input {
|
||||
flex: none;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -536,90 +551,8 @@ export default {
|
||||
}
|
||||
}
|
||||
.content {
|
||||
display: unset;
|
||||
margin: var(--spacing-card-lg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input,
|
||||
button {
|
||||
&:disabled {
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
}
|
||||
|
||||
section {
|
||||
margin-bottom: var(--spacing-card-md);
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
|
||||
span {
|
||||
flex: 2;
|
||||
padding-right: var(--spacing-card-lg);
|
||||
}
|
||||
|
||||
div {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
input {
|
||||
flex: 3;
|
||||
height: fit-content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.team-invite {
|
||||
gap: 0.5rem;
|
||||
@media screen and (max-width: 1024px) {
|
||||
flex-direction: column;
|
||||
h3 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: auto auto auto 0;
|
||||
}
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
input {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 500px) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
input {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
|
||||
button {
|
||||
margin-right: 0.5rem;
|
||||
|
||||
&:first-child {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.main-settings span {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -22,26 +22,31 @@
|
||||
</ul>
|
||||
</div>
|
||||
<div class="content card">
|
||||
<ConfirmPopup
|
||||
ref="delete_version_popup"
|
||||
<ModalConfirm
|
||||
ref="modal_confirm"
|
||||
title="Are you sure you want to delete this version?"
|
||||
description="This will remove this version forever (like really forever)."
|
||||
:has-to-type="false"
|
||||
proceed-label="Delete version"
|
||||
proceed-label="Delete"
|
||||
@proceed="deleteVersion()"
|
||||
/>
|
||||
<ModalReport
|
||||
ref="modal_version_report"
|
||||
:item-id="version.id"
|
||||
item-type="version"
|
||||
/>
|
||||
<div class="columns">
|
||||
<nuxt-link
|
||||
v-if="mode === 'version'"
|
||||
class="iconified-button back-button"
|
||||
:to="`/${project.project_type}/${
|
||||
project.slug ? project.slug : project.id
|
||||
}/${
|
||||
$nuxt.context.from
|
||||
? $nuxt.context.from.name === 'type-id-changelog'
|
||||
? 'changelog'
|
||||
: 'versions'
|
||||
: 'versions'
|
||||
:to="`${
|
||||
$nuxt.context.from &&
|
||||
($nuxt.context.from.name === 'type-id-changelog' ||
|
||||
$nuxt.context.from.name === 'type-id-versions')
|
||||
? $nuxt.context.from.fullPath
|
||||
: `/${project.project_type}/${
|
||||
project.slug ? project.slug : project.id
|
||||
}/versions`
|
||||
}`"
|
||||
>
|
||||
<BackIcon aria-hidden="true" />
|
||||
@ -73,11 +78,12 @@
|
||||
class="full-width-input"
|
||||
type="text"
|
||||
placeholder="Enter an optional version name..."
|
||||
maxlength="64"
|
||||
/>
|
||||
<Checkbox v-model="version.featured" label="Featured" />
|
||||
<hr class="card-divider" />
|
||||
</div>
|
||||
<div v-if="mode === 'edit'" class="header-buttons buttons columns">
|
||||
<div v-if="mode === 'edit'" class="header-buttons button-group columns">
|
||||
<h3 class="column-grow-1">Edit version</h3>
|
||||
<nuxt-link
|
||||
v-if="$auth.user"
|
||||
@ -90,7 +96,7 @@
|
||||
Cancel
|
||||
</nuxt-link>
|
||||
<button
|
||||
class="iconified-button brand-button-colors"
|
||||
class="iconified-button brand-button"
|
||||
@click="saveEditedVersion"
|
||||
>
|
||||
<SaveIcon aria-hidden="true" />
|
||||
@ -99,7 +105,7 @@
|
||||
</div>
|
||||
<div
|
||||
v-else-if="mode === 'create'"
|
||||
class="header-buttons buttons columns"
|
||||
class="header-buttons button-group columns"
|
||||
>
|
||||
<h3 class="column-grow-1">Create version</h3>
|
||||
<nuxt-link
|
||||
@ -112,42 +118,36 @@
|
||||
<CrossIcon aria-hidden="true" />
|
||||
Cancel
|
||||
</nuxt-link>
|
||||
<button
|
||||
class="iconified-button brand-button-colors"
|
||||
@click="createVersion"
|
||||
>
|
||||
<button class="iconified-button brand-button" @click="createVersion">
|
||||
<CheckIcon aria-hidden="true" />
|
||||
Create
|
||||
</button>
|
||||
</div>
|
||||
<div v-else class="buttons">
|
||||
<div v-else class="button-group">
|
||||
<a
|
||||
v-if="primaryFile"
|
||||
v-tooltip="
|
||||
primaryFile.filename + ' (' + $formatBytes(primaryFile.size) + ')'
|
||||
"
|
||||
:href="primaryFile.url"
|
||||
class="bold-button iconified-button brand-button-colors"
|
||||
class="bold-button iconified-button brand-button"
|
||||
:title="`Download ${primaryFile.filename}`"
|
||||
>
|
||||
<DownloadIcon aria-hidden="true" />
|
||||
Download
|
||||
</a>
|
||||
<nuxt-link
|
||||
:to="`/create/report?id=${version.id}&t=version`"
|
||||
<button
|
||||
v-if="$auth.user"
|
||||
class="action iconified-button"
|
||||
@click="$refs.modal_version_report.show()"
|
||||
>
|
||||
<ReportIcon aria-hidden="true" />
|
||||
Report
|
||||
</nuxt-link>
|
||||
<button
|
||||
v-if="currentMember"
|
||||
class="action iconified-button"
|
||||
@click="$refs.delete_version_popup.show()"
|
||||
>
|
||||
<TrashIcon aria-hidden="true" />
|
||||
Delete
|
||||
</button>
|
||||
<a v-else class="action iconified-button" :href="authUrl">
|
||||
<ReportIcon aria-hidden="true" />
|
||||
Report
|
||||
</a>
|
||||
<nuxt-link
|
||||
v-if="currentMember"
|
||||
class="action iconified-button"
|
||||
@ -159,11 +159,20 @@
|
||||
<EditIcon aria-hidden="true" />
|
||||
Edit
|
||||
</nuxt-link>
|
||||
<button
|
||||
v-if="currentMember"
|
||||
class="action iconified-button danger-button"
|
||||
@click="$refs.modal_confirm.show()"
|
||||
>
|
||||
<TrashIcon aria-hidden="true" />
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
<section v-if="mode === 'edit' || mode === 'create'">
|
||||
<h3>Changelog</h3>
|
||||
<ThisOrThat
|
||||
<Chips
|
||||
v-model="changelogViewMode"
|
||||
class="separator"
|
||||
:items="['source', 'preview']"
|
||||
/>
|
||||
<div v-if="changelogViewMode === 'source'" class="textarea-wrapper">
|
||||
@ -289,6 +298,7 @@
|
||||
v-model="version.version_number"
|
||||
type="text"
|
||||
placeholder="Enter the version number..."
|
||||
maxlength="32"
|
||||
/>
|
||||
<p v-else class="value">{{ version.version_number }}</p>
|
||||
</div>
|
||||
@ -358,7 +368,7 @@
|
||||
</div>
|
||||
<div v-if="mode === 'version'" class="data">
|
||||
<p class="title">Version ID</p>
|
||||
<p class="value">{{ version.id }}</p>
|
||||
<p class="value"><CopyCode :text="version.id" /></p>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="card-divider" />
|
||||
@ -380,16 +390,10 @@
|
||||
:key="index"
|
||||
class="dependency"
|
||||
>
|
||||
<img
|
||||
class="icon"
|
||||
:src="
|
||||
dependency.project
|
||||
? dependency.project.icon_url
|
||||
? dependency.project.icon_url
|
||||
: 'https://cdn.modrinth.com/placeholder.svg?inline'
|
||||
: 'https://cdn.modrinth.com/placeholder.svg?inline'
|
||||
"
|
||||
<Avatar
|
||||
:src="dependency.project ? dependency.project.icon_url : null"
|
||||
alt="dependency-icon"
|
||||
size="sm"
|
||||
/>
|
||||
<div class="info">
|
||||
<nuxt-link
|
||||
@ -443,8 +447,9 @@
|
||||
class="edit-dependency"
|
||||
>
|
||||
<h4>Add dependency</h4>
|
||||
<ThisOrThat
|
||||
<Chips
|
||||
v-model="dependencyAddMode"
|
||||
class="separator"
|
||||
:items="['project', 'version']"
|
||||
/>
|
||||
<div class="edit-info">
|
||||
@ -582,9 +587,10 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<StatelessFileInput
|
||||
<FileInput
|
||||
v-if="mode === 'edit' || mode === 'create'"
|
||||
multiple
|
||||
should-always-reset
|
||||
class="choose-files"
|
||||
:accept="
|
||||
project.actualProjectType.toLowerCase() === 'modpack'
|
||||
@ -629,8 +635,7 @@
|
||||
</template>
|
||||
<script>
|
||||
import Multiselect from 'vue-multiselect'
|
||||
import ConfirmPopup from '~/components/ui/ConfirmPopup'
|
||||
import StatelessFileInput from '~/components/ui/StatelessFileInput'
|
||||
import FileInput from '~/components/ui/FileInput'
|
||||
|
||||
import InfoIcon from '~/assets/images/utils/info.svg?inline'
|
||||
import TrashIcon from '~/assets/images/utils/trash.svg?inline'
|
||||
@ -645,11 +650,20 @@ import StarIcon from '~/assets/images/utils/star.svg?inline'
|
||||
import CheckIcon from '~/assets/images/utils/check.svg?inline'
|
||||
import VersionBadge from '~/components/ui/Badge'
|
||||
import Checkbox from '~/components/ui/Checkbox'
|
||||
import ThisOrThat from '~/components/ui/ThisOrThat'
|
||||
import Chips from '~/components/ui/Chips'
|
||||
import ModalConfirm from '~/components/ui/ModalConfirm'
|
||||
import ModalReport from '~/components/ui/ModalReport'
|
||||
import CopyCode from '~/components/ui/CopyCode'
|
||||
import Avatar from '~/components/ui/Avatar'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ThisOrThat,
|
||||
Avatar,
|
||||
CopyCode,
|
||||
ModalConfirm,
|
||||
ModalReport,
|
||||
FileInput,
|
||||
Chips,
|
||||
Checkbox,
|
||||
VersionBadge,
|
||||
DownloadIcon,
|
||||
@ -657,29 +671,14 @@ export default {
|
||||
EditIcon,
|
||||
ReportIcon,
|
||||
BackIcon,
|
||||
ConfirmPopup,
|
||||
StarIcon,
|
||||
CheckIcon,
|
||||
Multiselect,
|
||||
SaveIcon,
|
||||
PlusIcon,
|
||||
CrossIcon,
|
||||
StatelessFileInput,
|
||||
InfoIcon,
|
||||
},
|
||||
beforeRouteLeave(to, from, next) {
|
||||
if (this.mode === 'create') {
|
||||
if (
|
||||
!window.confirm('Are you sure that you want to leave without saving?')
|
||||
) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
this.setVersion()
|
||||
|
||||
next()
|
||||
},
|
||||
props: {
|
||||
project: {
|
||||
type: Object,
|
||||
@ -738,8 +737,57 @@ export default {
|
||||
}
|
||||
},
|
||||
async fetch() {
|
||||
console.log(this.$nuxt.context.from)
|
||||
await this.setVersion()
|
||||
},
|
||||
head() {
|
||||
if (!this.version.game_versions) {
|
||||
return {}
|
||||
}
|
||||
const title = `${
|
||||
this.mode === 'create' ? 'Create Version' : this.version.name
|
||||
} - ${this.project.title}`
|
||||
const description = `Download ${this.project.title} ${
|
||||
this.version.version_number
|
||||
} on Modrinth. Supports ${this.$formatVersion(
|
||||
this.version.game_versions
|
||||
)} ${this.version.loaders
|
||||
.map((x) => x.charAt(0).toUpperCase() + x.slice(1))
|
||||
.join(' & ')}. Published on ${this.$dayjs(
|
||||
this.version.date_published
|
||||
).format('MMM D, YYYY')}. ${this.version.downloads} downloads.`
|
||||
|
||||
return {
|
||||
title,
|
||||
meta: [
|
||||
{
|
||||
hid: 'og:title',
|
||||
name: 'og:title',
|
||||
content: title,
|
||||
},
|
||||
{
|
||||
hid: 'apple-mobile-web-app-title',
|
||||
name: 'apple-mobile-web-app-title',
|
||||
content: title,
|
||||
},
|
||||
{
|
||||
hid: 'og:description',
|
||||
name: 'og:description',
|
||||
content: description,
|
||||
},
|
||||
{
|
||||
hid: 'description',
|
||||
name: 'description',
|
||||
content: description,
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
authUrl() {
|
||||
return `${process.env.authURLBase}auth/init?url=${process.env.domain}${this.$route.path}`
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'$route.path': {
|
||||
async handler() {
|
||||
@ -747,31 +795,15 @@ export default {
|
||||
},
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (this.mode === 'create') {
|
||||
function preventLeave(e) {
|
||||
e.preventDefault()
|
||||
e.returnValue = ''
|
||||
}
|
||||
window.addEventListener('beforeunload', preventLeave)
|
||||
this.$once('hook:beforeDestroy', () => {
|
||||
window.removeEventListener('beforeunload', preventLeave)
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
checkFields() {
|
||||
if (
|
||||
return !(
|
||||
this.version.version_number === '' ||
|
||||
this.version.game_versions.length === 0 ||
|
||||
(this.version.loaders.length === 0 &&
|
||||
this.project.project_type !== 'resourcepack') ||
|
||||
(this.newFiles.length === 0 && this.version.files.length === 0)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
)
|
||||
},
|
||||
reset() {
|
||||
this.changelogViewMode = 'source'
|
||||
@ -1112,8 +1144,12 @@ export default {
|
||||
section {
|
||||
margin: 1rem 0;
|
||||
|
||||
h3 {
|
||||
margin-bottom: 0.5rem;
|
||||
.separator {
|
||||
margin: var(--spacing-card-sm) 0;
|
||||
}
|
||||
|
||||
.choose-files {
|
||||
margin-bottom: var(--spacing-card-sm);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1126,10 +1162,6 @@ section {
|
||||
flex-wrap: wrap;
|
||||
row-gap: 0.5rem;
|
||||
|
||||
.bold-button {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1024px) {
|
||||
margin-left: auto;
|
||||
}
|
||||
@ -1186,19 +1218,12 @@ section {
|
||||
.dependency {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
|
||||
@media screen and (min-width: 800px) {
|
||||
flex-basis: 30%;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
margin-right: 0.5rem;
|
||||
border-radius: var(--size-rounded-xs);
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -1279,10 +1304,6 @@ section {
|
||||
margin-bottom: var(--spacing-card-sm);
|
||||
}
|
||||
|
||||
.styled-tabs {
|
||||
margin-bottom: var(--spacing-card-sm);
|
||||
}
|
||||
|
||||
.textarea-wrapper {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
|
||||
@ -1,10 +1,7 @@
|
||||
<template>
|
||||
<div class="content">
|
||||
<div v-if="currentMember" class="card header-buttons">
|
||||
<nuxt-link
|
||||
to="version/create"
|
||||
class="brand-button-colors iconified-button"
|
||||
>
|
||||
<nuxt-link to="version/create" class="brand-button iconified-button">
|
||||
<PlusIcon />
|
||||
Create a version
|
||||
</nuxt-link>
|
||||
@ -14,111 +11,79 @@
|
||||
:versions="versions"
|
||||
@updateVersions="updateVersions"
|
||||
/>
|
||||
<div v-if="versions.length > 0" class="card">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th role="presentation"></th>
|
||||
<th>Version</th>
|
||||
<th>Supports</th>
|
||||
<th>Stats</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="version in filteredVersions" :key="version.id">
|
||||
<td>
|
||||
<a
|
||||
v-tooltip="
|
||||
$parent.findPrimary(version).filename +
|
||||
' (' +
|
||||
$formatBytes($parent.findPrimary(version).size) +
|
||||
')'
|
||||
"
|
||||
:href="$parent.findPrimary(version).url"
|
||||
class="download-button"
|
||||
:class="version.version_type"
|
||||
:title="`Download ${version.name}`"
|
||||
>
|
||||
<DownloadIcon aria-hidden="true" />
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<div class="info">
|
||||
<div class="top title-link">
|
||||
<nuxt-link
|
||||
:to="`/${project.project_type}/${
|
||||
project.slug ? project.slug : project.id
|
||||
}/version/${encodeURI(version.displayUrlEnding)}`"
|
||||
>
|
||||
{{ version.name }}
|
||||
</nuxt-link>
|
||||
</div>
|
||||
<div class="bottom">
|
||||
<VersionBadge
|
||||
v-if="version.version_type === 'release'"
|
||||
type="release"
|
||||
color="green"
|
||||
/>
|
||||
<VersionBadge
|
||||
v-else-if="version.version_type === 'beta'"
|
||||
type="beta"
|
||||
color="yellow"
|
||||
/>
|
||||
<VersionBadge
|
||||
v-else-if="version.version_type === 'alpha'"
|
||||
type="alpha"
|
||||
color="red"
|
||||
/>
|
||||
<span class="divider" />
|
||||
<span class="version_number">{{
|
||||
version.version_number
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="mobile-info">
|
||||
<p>
|
||||
{{
|
||||
version.loaders
|
||||
.map((x) => $formatCategory(x))
|
||||
.join(', ') +
|
||||
' ' +
|
||||
$formatVersion(version.game_versions)
|
||||
}}
|
||||
</p>
|
||||
<p></p>
|
||||
<p>
|
||||
<strong>{{ $formatNumber(version.downloads) }}</strong>
|
||||
downloads
|
||||
</p>
|
||||
<p>
|
||||
Published on
|
||||
<strong>{{
|
||||
$dayjs(version.date_published).format('MMM D, YYYY')
|
||||
}}</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<p>
|
||||
{{ version.loaders.map((x) => $formatCategory(x)).join(', ') }}
|
||||
</p>
|
||||
<p>{{ $formatVersion(version.game_versions) }}</p>
|
||||
</td>
|
||||
<td>
|
||||
<p>
|
||||
<span>{{ $formatNumber(version.downloads) }}</span>
|
||||
downloads
|
||||
</p>
|
||||
<p>
|
||||
Published on
|
||||
<span>{{
|
||||
$dayjs(version.date_published).format('MMM D, YYYY')
|
||||
}}</span>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div v-if="versions.length > 0" class="universal-card all-versions">
|
||||
<div class="header">
|
||||
<div></div>
|
||||
<div>Version</div>
|
||||
<div>Supports</div>
|
||||
<div>Stats</div>
|
||||
</div>
|
||||
<div
|
||||
v-for="version in filteredVersions"
|
||||
:key="version.id + '-new'"
|
||||
class="version-button button-transparent"
|
||||
@click="
|
||||
$router.push(
|
||||
`/${project.project_type}/${
|
||||
project.slug ? project.slug : project.id
|
||||
}/version/${encodeURI(version.displayUrlEnding)}`
|
||||
)
|
||||
"
|
||||
>
|
||||
<a
|
||||
v-tooltip="
|
||||
$parent.findPrimary(version).filename +
|
||||
' (' +
|
||||
$formatBytes($parent.findPrimary(version).size) +
|
||||
')'
|
||||
"
|
||||
:href="$parent.findPrimary(version).url"
|
||||
class="download-button"
|
||||
:class="version.version_type"
|
||||
:title="`Download ${version.name}`"
|
||||
@click.stop="(event) => event.stopPropagation()"
|
||||
>
|
||||
<DownloadIcon aria-hidden="true" />
|
||||
</a>
|
||||
<span class="version__title">{{ version.name }}</span>
|
||||
<div class="version__metadata">
|
||||
<VersionBadge
|
||||
v-if="version.version_type === 'release'"
|
||||
type="release"
|
||||
color="green"
|
||||
/>
|
||||
<VersionBadge
|
||||
v-else-if="version.version_type === 'beta'"
|
||||
type="beta"
|
||||
color="yellow"
|
||||
/>
|
||||
<VersionBadge
|
||||
v-else-if="version.version_type === 'alpha'"
|
||||
type="alpha"
|
||||
color="red"
|
||||
/>
|
||||
<span class="divider" />
|
||||
<span class="version_number">{{ version.version_number }}</span>
|
||||
</div>
|
||||
<div class="version__supports">
|
||||
<span>
|
||||
{{ version.loaders.map((x) => $formatCategory(x)).join(', ') }}
|
||||
</span>
|
||||
<span>{{ $formatVersion(version.game_versions) }}</span>
|
||||
</div>
|
||||
<div class="version__stats">
|
||||
<span>
|
||||
<strong>{{ $formatNumber(version.downloads) }}</strong>
|
||||
downloads
|
||||
</span>
|
||||
<span>
|
||||
Published on
|
||||
<strong>{{
|
||||
$dayjs(version.date_published).format('MMM D, YYYY')
|
||||
}}</strong>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -161,6 +126,46 @@ export default {
|
||||
filteredVersions: this.versions,
|
||||
}
|
||||
},
|
||||
fetch() {
|
||||
if (this.$route.query.page)
|
||||
this.currentPage = parseInt(this.$route.query.page)
|
||||
},
|
||||
head() {
|
||||
const title = `${this.project.title} - Versions`
|
||||
const description = `Download and browse ${this.versions.length} ${
|
||||
this.project.title
|
||||
} versions. ${this.$formatNumber(
|
||||
this.project.downloads
|
||||
)} total downloads. Last updated ${this.$dayjs(
|
||||
this.versions[0] ? this.versions[0].date_published : null
|
||||
).format('MMM D, YYYY')}.`
|
||||
|
||||
return {
|
||||
title,
|
||||
meta: [
|
||||
{
|
||||
hid: 'og:title',
|
||||
name: 'og:title',
|
||||
content: title,
|
||||
},
|
||||
{
|
||||
hid: 'apple-mobile-web-app-title',
|
||||
name: 'apple-mobile-web-app-title',
|
||||
content: title,
|
||||
},
|
||||
{
|
||||
hid: 'og:description',
|
||||
name: 'og:description',
|
||||
content: description,
|
||||
},
|
||||
{
|
||||
hid: 'description',
|
||||
name: 'description',
|
||||
content: description,
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateVersions(updatedVersions) {
|
||||
this.filteredVersions = updatedVersions
|
||||
@ -170,89 +175,116 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
table {
|
||||
border-collapse: separate;
|
||||
border-spacing: 0 0.75rem;
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
font-size: var(--font-size-md);
|
||||
|
||||
&:nth-child(3),
|
||||
&:nth-child(4) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
tr {
|
||||
td:nth-child(2) {
|
||||
padding-right: 2rem;
|
||||
min-width: 16rem;
|
||||
.top {
|
||||
font-weight: bold;
|
||||
}
|
||||
.bottom {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
text-overflow: ellipsis;
|
||||
margin-top: 0.25rem;
|
||||
|
||||
.divider {
|
||||
width: 0.25rem;
|
||||
height: 0.25rem;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin: 0 0.25rem;
|
||||
background-color: var(--color-text);
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-info {
|
||||
p {
|
||||
margin: 0.25rem 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
td:nth-child(3) {
|
||||
display: none;
|
||||
width: 100%;
|
||||
p {
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
}
|
||||
td:nth-child(4) {
|
||||
display: none;
|
||||
min-width: 15rem;
|
||||
p {
|
||||
margin: 0.25rem 0;
|
||||
span {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1024px) {
|
||||
table {
|
||||
tr {
|
||||
th:nth-child(3),
|
||||
td:nth-child(3),
|
||||
th:nth-child(4),
|
||||
td:nth-child(4) {
|
||||
display: table-cell;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-info {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.header-buttons {
|
||||
display: flex;
|
||||
justify-content: right;
|
||||
}
|
||||
|
||||
.all-versions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.header {
|
||||
display: grid;
|
||||
grid-template: 'download title supports stats';
|
||||
grid-template-columns: calc(2.25rem + var(--spacing-card-sm)) 1fr 1fr 1fr;
|
||||
color: var(--color-text-dark);
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: bold;
|
||||
justify-content: left;
|
||||
margin-inline: var(--spacing-card-md);
|
||||
margin-bottom: var(--spacing-card-sm);
|
||||
column-gap: var(--spacing-card-sm);
|
||||
|
||||
div:first-child {
|
||||
grid-area: download;
|
||||
}
|
||||
|
||||
div:nth-child(2) {
|
||||
grid-area: title;
|
||||
}
|
||||
|
||||
div:nth-child(3) {
|
||||
grid-area: supports;
|
||||
}
|
||||
|
||||
div:nth-child(4) {
|
||||
grid-area: stats;
|
||||
}
|
||||
}
|
||||
|
||||
.version-button {
|
||||
display: grid;
|
||||
grid-template: 'download title supports stats' 'download metadata supports stats';
|
||||
grid-template-columns: calc(2.25rem + var(--spacing-card-sm)) 1fr 1fr 1fr;
|
||||
column-gap: var(--spacing-card-sm);
|
||||
justify-content: left;
|
||||
padding: var(--spacing-card-md);
|
||||
|
||||
.download-button {
|
||||
grid-area: download;
|
||||
}
|
||||
.version__title {
|
||||
grid-area: title;
|
||||
font-weight: bold;
|
||||
}
|
||||
.version__metadata {
|
||||
grid-area: metadata;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-card-xs);
|
||||
margin-top: var(--spacing-card-xs);
|
||||
}
|
||||
.version__supports {
|
||||
grid-area: supports;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-card-xs);
|
||||
}
|
||||
.version__stats {
|
||||
grid-area: stats;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-card-xs);
|
||||
}
|
||||
|
||||
&:active:not(&:disabled) {
|
||||
transform: scale(0.99) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1024px) {
|
||||
.all-versions {
|
||||
.header {
|
||||
grid-template: 'download title';
|
||||
grid-template-columns: calc(2.25rem + var(--spacing-card-sm)) 1fr;
|
||||
|
||||
div:nth-child(3) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
div:nth-child(4) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.version-button {
|
||||
grid-template: 'download title' 'download metadata' 'download supports' 'download stats';
|
||||
grid-template-columns: calc(2.25rem + var(--spacing-card-sm)) 1fr;
|
||||
row-gap: var(--spacing-card-xs);
|
||||
|
||||
.version__supports {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
column-gap: var(--spacing-card-xs);
|
||||
}
|
||||
.version__metadata {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -61,12 +61,6 @@ export default {
|
||||
head: {
|
||||
title: 'About - Modrinth',
|
||||
meta: [
|
||||
{
|
||||
hid: 'description',
|
||||
name: 'description',
|
||||
content:
|
||||
'View information about Modrinth, an open source modding platform here! Modrinth currently supports Minecraft, including the forge and fabric mod loaders.',
|
||||
},
|
||||
{
|
||||
hid: 'apple-mobile-web-app-title',
|
||||
name: 'apple-mobile-web-app-title',
|
||||
|
||||
@ -1,284 +0,0 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="page-contents">
|
||||
<header class="card columns">
|
||||
<h3 class="column-grow-1">File a report</h3>
|
||||
<button
|
||||
title="Create"
|
||||
class="brand-button-colors iconified-button column"
|
||||
:disabled="!$nuxt.$loading"
|
||||
@click="createReport"
|
||||
>
|
||||
<CheckIcon />
|
||||
Submit
|
||||
</button>
|
||||
</header>
|
||||
<section class="card info">
|
||||
<label>
|
||||
<span>
|
||||
<h3>Item ID</h3>
|
||||
<span>
|
||||
The ID of the item you are reporting. For example, the item ID of
|
||||
a project would be its project ID, found on the right side of that
|
||||
project's page under "Project ID".
|
||||
</span>
|
||||
</span>
|
||||
<input v-model="itemId" type="text" placeholder="Enter the item ID" />
|
||||
</label>
|
||||
<label>
|
||||
<span>
|
||||
<h3>Item type</h3>
|
||||
<span class="no-padding"
|
||||
>The type of the item that is being reported.</span
|
||||
>
|
||||
</span>
|
||||
<multiselect
|
||||
id="item-type"
|
||||
v-model="itemType"
|
||||
:options="['project', 'version', 'user']"
|
||||
:custom-label="
|
||||
(value) => value.charAt(0).toUpperCase() + value.slice(1)
|
||||
"
|
||||
:multiple="false"
|
||||
:searchable="false"
|
||||
:show-no-results="false"
|
||||
:show-labels="false"
|
||||
placeholder="Choose item type"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>
|
||||
<h3>Report type</h3>
|
||||
<span class="no-padding">
|
||||
The type of report. This is the category that this report falls
|
||||
under.
|
||||
</span>
|
||||
</span>
|
||||
<multiselect
|
||||
id="report-type"
|
||||
v-model="reportType"
|
||||
:options="reportTypes"
|
||||
:custom-label="
|
||||
(value) => value.charAt(0).toUpperCase() + value.slice(1)
|
||||
"
|
||||
:multiple="false"
|
||||
:searchable="false"
|
||||
:show-no-results="false"
|
||||
:show-labels="false"
|
||||
placeholder="Choose report type"
|
||||
/>
|
||||
</label>
|
||||
</section>
|
||||
<section class="card description">
|
||||
<h3>
|
||||
<label
|
||||
for="body"
|
||||
title="You can type the of the long form of your description here."
|
||||
>
|
||||
Body
|
||||
</label>
|
||||
</h3>
|
||||
<span>
|
||||
You can type the of the long form of your description here. This
|
||||
editor supports
|
||||
<a
|
||||
href="https://guides.github.com/features/mastering-markdown/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-link"
|
||||
>Markdown</a
|
||||
>.
|
||||
</span>
|
||||
<ThisOrThat
|
||||
v-model="bodyViewMode"
|
||||
class="separator"
|
||||
:items="['source', 'preview']"
|
||||
/>
|
||||
<div class="edit-wrapper">
|
||||
<div v-if="bodyViewMode === 'source'" class="textarea-wrapper">
|
||||
<textarea id="body" v-model="body" />
|
||||
</div>
|
||||
<div
|
||||
v-if="bodyViewMode === 'preview'"
|
||||
v-highlightjs
|
||||
class="markdown-body"
|
||||
v-html="body ? $xss($md.render(body)) : 'No body specified.'"
|
||||
></div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Multiselect from 'vue-multiselect'
|
||||
import ThisOrThat from '~/components/ui/ThisOrThat'
|
||||
|
||||
import CheckIcon from '~/assets/images/utils/check.svg?inline'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Multiselect,
|
||||
ThisOrThat,
|
||||
CheckIcon,
|
||||
},
|
||||
async asyncData(data) {
|
||||
const reportTypes = (await data.$axios.get(`tag/report_type`)).data
|
||||
|
||||
return {
|
||||
reportTypes,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
itemId: '',
|
||||
itemType: '',
|
||||
reportType: '',
|
||||
body: '',
|
||||
|
||||
bodyViewMode: 'source',
|
||||
|
||||
reportTypes: ['aaaa'],
|
||||
}
|
||||
},
|
||||
fetch() {
|
||||
if (this.$route.query.id) this.itemId = this.$route.query.id
|
||||
if (this.$route.query.t) this.itemType = this.$route.query.t
|
||||
},
|
||||
methods: {
|
||||
async createReport() {
|
||||
this.$nuxt.$loading.start()
|
||||
|
||||
try {
|
||||
const data = {
|
||||
report_type: this.reportType,
|
||||
item_id: this.itemId,
|
||||
item_type: this.itemType,
|
||||
body: this.body,
|
||||
}
|
||||
|
||||
await this.$axios.post('report', data, this.$defaultHeaders())
|
||||
|
||||
switch (this.itemType) {
|
||||
case 'version': {
|
||||
const version = (await this.$axios.get(`version/${this.itemId}`))
|
||||
.data
|
||||
const project = (
|
||||
await this.$axios.get(`project/${version.project_id}`)
|
||||
).data
|
||||
await this.$router.replace(
|
||||
`/${project.project_type}/${project.slug || project.id}/version/${
|
||||
this.itemId
|
||||
}`
|
||||
)
|
||||
break
|
||||
}
|
||||
case 'project': {
|
||||
const project = (await this.$axios.get(`project/${this.itemId}`))
|
||||
.data
|
||||
await this.$router.replace(
|
||||
`/${project.project_type}/${project.slug || project.id}`
|
||||
)
|
||||
break
|
||||
}
|
||||
default:
|
||||
await this.$router.replace(`/${this.itemType}/${this.itemId}`)
|
||||
}
|
||||
} catch (err) {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.response.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}
|
||||
this.$nuxt.$loading.finish()
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.title {
|
||||
* {
|
||||
display: inline;
|
||||
}
|
||||
.button {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.textarea-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
|
||||
textarea {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
resize: none;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.page-contents {
|
||||
display: grid;
|
||||
grid-template:
|
||||
'header header header' auto
|
||||
'info info info' auto
|
||||
'description description description' auto
|
||||
'footer footer footer' auto
|
||||
/ 4fr 1fr 4fr;
|
||||
column-gap: var(--spacing-card-md);
|
||||
row-gap: var(--spacing-card-md);
|
||||
}
|
||||
|
||||
header {
|
||||
grid-area: header;
|
||||
|
||||
h3 {
|
||||
margin: auto 0;
|
||||
color: var(--color-text-dark);
|
||||
font-weight: var(--font-weight-extrabold);
|
||||
}
|
||||
|
||||
button {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
section.info {
|
||||
grid-area: info;
|
||||
}
|
||||
|
||||
section.description {
|
||||
grid-area: description;
|
||||
|
||||
.separator {
|
||||
margin: var(--spacing-card-sm) 0;
|
||||
}
|
||||
|
||||
.edit-wrapper * {
|
||||
min-height: 10rem;
|
||||
max-height: 40rem;
|
||||
}
|
||||
|
||||
.markdown-body {
|
||||
overflow-y: auto;
|
||||
padding: 0 var(--spacing-card-sm);
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.card span {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
label {
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
61
pages/dashboard.vue
Normal file
@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<div class="normal-page">
|
||||
<div class="normal-page__sidebar">
|
||||
<aside class="universal-card">
|
||||
<h1>Dashboard<span class="beta-badge">BETA</span></h1>
|
||||
<NavStack>
|
||||
<NavStackItem link="/dashboard" label="Overview">
|
||||
<DashboardIcon />
|
||||
</NavStackItem>
|
||||
<!-- <NavStackItem link="/dashboard/projects" label="Projects">-->
|
||||
<!-- <ListIcon />-->
|
||||
<!-- </NavStackItem>-->
|
||||
<!-- <NavStackItem link="/dashboard/analytics" label="Analytics">-->
|
||||
<!-- <ChartIcon />-->
|
||||
<!-- </NavStackItem>-->
|
||||
<NavStackItem
|
||||
v-if="hasMonetization()"
|
||||
link="/dashboard/revenue"
|
||||
label="Revenue"
|
||||
>
|
||||
<CurrencyIcon />
|
||||
</NavStackItem>
|
||||
</NavStack>
|
||||
</aside>
|
||||
</div>
|
||||
<div class="normal-page__content">
|
||||
<NuxtChild />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import NavStack from '~/components/ui/NavStack'
|
||||
import NavStackItem from '~/components/ui/NavStackItem'
|
||||
|
||||
import DashboardIcon from '~/assets/images/utils/dashboard.svg?inline'
|
||||
// import ChartIcon from '~/assets/images/utils/chart.svg?inline'
|
||||
import CurrencyIcon from '~/assets/images/utils/currency.svg?inline'
|
||||
// import ListIcon from '~/assets/images/utils/list.svg?inline'
|
||||
|
||||
const monetization = true
|
||||
|
||||
export default {
|
||||
name: 'Dashboard',
|
||||
components: {
|
||||
NavStack,
|
||||
NavStackItem,
|
||||
DashboardIcon,
|
||||
// ChartIcon,
|
||||
CurrencyIcon,
|
||||
// ListIcon,
|
||||
},
|
||||
methods: {
|
||||
hasMonetization() {
|
||||
return monetization
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
22
pages/dashboard/analytics.vue
Normal file
@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<div>
|
||||
<section class="universal-card">
|
||||
<h2>Analytics</h2>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
components: {},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
fetch() {},
|
||||
head: {
|
||||
title: 'Analytics - Modrinth',
|
||||
},
|
||||
methods: {},
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
152
pages/dashboard/index.vue
Normal file
@ -0,0 +1,152 @@
|
||||
<template>
|
||||
<div>
|
||||
<section class="universal-card">
|
||||
<h2>Overview</h2>
|
||||
<div class="metrics">
|
||||
<div class="metric">
|
||||
<div class="label">Total downloads</div>
|
||||
<div class="value">
|
||||
{{
|
||||
$formatNumber(
|
||||
$user.projects.reduce((agg, x) => agg + x.downloads, 0)
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
<span
|
||||
>from {{ $user.projects.length }} project{{
|
||||
$user.projects.length === 1 ? '' : 's'
|
||||
}}</span
|
||||
>
|
||||
<!-- <NuxtLink class="goto-link" to="/dashboard/analytics"-->
|
||||
<!-- >View breakdown-->
|
||||
<!-- <ChevronRightIcon-->
|
||||
<!-- class="featured-header-chevron"-->
|
||||
<!-- aria-hidden="true"-->
|
||||
<!-- /></NuxtLink>-->
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="label">Total followers</div>
|
||||
<div class="value">
|
||||
{{
|
||||
$formatNumber(
|
||||
$user.projects.reduce((agg, x) => agg + x.followers, 0)
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
<span>
|
||||
<span
|
||||
>from {{ $user.projects.length }} project{{
|
||||
$user.projects.length === 1 ? '' : 's'
|
||||
}}</span
|
||||
></span
|
||||
>
|
||||
<!-- <NuxtLink class="goto-link" to="/dashboard/analytics"-->
|
||||
<!-- >View breakdown-->
|
||||
<!-- <ChevronRightIcon-->
|
||||
<!-- class="featured-header-chevron"-->
|
||||
<!-- aria-hidden="true"-->
|
||||
<!-- /></NuxtLink>-->
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="label">Total revenue</div>
|
||||
<div class="value">${{ $formatNumber(payouts.all_time) }}</div>
|
||||
<span>${{ $formatNumber(payouts.last_month) }} this month</span>
|
||||
<!-- <NuxtLink class="goto-link" to="/dashboard/analytics"-->
|
||||
<!-- >View breakdown-->
|
||||
<!-- <ChevronRightIcon-->
|
||||
<!-- class="featured-header-chevron"-->
|
||||
<!-- aria-hidden="true"-->
|
||||
<!-- /></NuxtLink>-->
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="label">Current balance</div>
|
||||
<div class="value">
|
||||
${{ $formatNumber($auth.user.payout_data.balance) }}
|
||||
</div>
|
||||
<NuxtLink
|
||||
v-if="$auth.user.payout_data.balance >= minWithdraw"
|
||||
class="goto-link"
|
||||
to="/dashboard/revenue"
|
||||
>Withdraw earnings
|
||||
<ChevronRightIcon
|
||||
class="featured-header-chevron"
|
||||
aria-hidden="true"
|
||||
/></NuxtLink>
|
||||
<span v-else>${{ minWithdraw }} is the withdraw minimum</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="universal-card more-soon">
|
||||
<h2>More coming soon!</h2>
|
||||
<p>
|
||||
Stay tuned for more metrics and analytics (pretty graphs, anyone? 👀)
|
||||
coming to the creators dashboard soon!
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ChevronRightIcon from '~/assets/images/utils/chevron-right.svg?inline'
|
||||
|
||||
export default {
|
||||
components: { ChevronRightIcon },
|
||||
async asyncData(data) {
|
||||
const [payouts] = (
|
||||
await Promise.all([
|
||||
data.$axios.get(
|
||||
`user/${data.$auth.user.id}/payouts`,
|
||||
data.$defaultHeaders()
|
||||
),
|
||||
])
|
||||
).map((it) => it.data)
|
||||
|
||||
payouts.all_time = Math.floor(payouts.all_time * 100) / 100
|
||||
payouts.last_month = Math.floor(payouts.last_month * 100) / 100
|
||||
|
||||
return {
|
||||
payouts,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
minWithdraw: 0.26,
|
||||
}
|
||||
},
|
||||
fetch() {},
|
||||
head: {
|
||||
title: 'Creator dashboard - Modrinth',
|
||||
},
|
||||
methods: {},
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.metrics {
|
||||
display: grid;
|
||||
grid-gap: var(--spacing-card-md);
|
||||
grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
|
||||
|
||||
.metric {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
background-color: var(--color-bg);
|
||||
border-radius: var(--size-rounded-card);
|
||||
padding: var(--spacing-card-lg);
|
||||
gap: var(--spacing-card-md);
|
||||
|
||||
.label {
|
||||
color: var(--color-heading);
|
||||
font-weight: bold;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: var(--color-text-dark);
|
||||
font-weight: bold;
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
22
pages/dashboard/projects.vue
Normal file
@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<div>
|
||||
<section class="universal-card">
|
||||
<h2>Projects</h2>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
components: {},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
fetch() {},
|
||||
head: {
|
||||
title: 'Projects - Modrinth',
|
||||
},
|
||||
methods: {},
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
131
pages/dashboard/revenue.vue
Normal file
@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<div>
|
||||
<ModalTransfer
|
||||
v-if="enrolled"
|
||||
ref="modal_transfer"
|
||||
:wallet="$auth.user.payout_data.payout_wallet"
|
||||
:account-type="$auth.user.payout_data.payout_wallet_type"
|
||||
:account="$auth.user.payout_data.payout_address"
|
||||
:balance="$auth.user.payout_data.balance"
|
||||
:min-withdraw="minWithdraw"
|
||||
/>
|
||||
<section class="universal-card">
|
||||
<h2>Withdraw</h2>
|
||||
<div v-if="$auth.user.payout_data.balance >= minWithdraw">
|
||||
<p>
|
||||
You have
|
||||
<strong>${{ $auth.user.payout_data.balance }}</strong>
|
||||
available to withdraw.
|
||||
<span v-if="!enrolled"
|
||||
>Enroll in the Creator Monetization Program to withdraw your
|
||||
revenue.</span
|
||||
>
|
||||
</p>
|
||||
|
||||
<div v-if="enrolled" class="buttons">
|
||||
<button
|
||||
class="iconified-button brand-button"
|
||||
@click="$refs.modal_transfer.show()"
|
||||
>
|
||||
<TransferIcon /> Transfer to
|
||||
{{ $formatWallet($auth.user.payout_data.payout_wallet) }}
|
||||
</button>
|
||||
<NuxtLink class="iconified-button" to="/settings/monetization">
|
||||
<SettingsIcon /> Monetization settings
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
<p v-else-if="$auth.user.payout_data.balance > 0">
|
||||
You have made
|
||||
<strong>${{ $auth.user.payout_data.balance }}</strong
|
||||
>, however you have not yet met the minimum of ${{ minWithdraw }} to
|
||||
withdraw.
|
||||
</p>
|
||||
<p v-else>
|
||||
You have made
|
||||
<strong>${{ $auth.user.payout_data.balance }}</strong
|
||||
>, which is under the minimum of ${{ minWithdraw }} to withdraw.
|
||||
</p>
|
||||
<div v-if="!enrolled">
|
||||
<NuxtLink class="iconified-button" to="/settings/monetization">
|
||||
<SettingsIcon /> Enroll in the Creator Monetization Program
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</section>
|
||||
<section class="universal-card">
|
||||
<h2>Processing fees</h2>
|
||||
<p>
|
||||
To avoid paying unnecessary fee deductions, you may want to wait to
|
||||
transfer your money out after it accumulates for a bit rather than
|
||||
transferring as soon as you reach the minimum of ${{ minWithdraw }}.
|
||||
</p>
|
||||
<h3>PayPal</h3>
|
||||
<ul>
|
||||
<li>
|
||||
In the <strong>United States</strong>, PayPal charges a flat
|
||||
<strong>$0.25</strong>
|
||||
fee per transaction.
|
||||
</li>
|
||||
<li>
|
||||
In the rest of the world, PayPal charges a <strong>2%</strong> (up to
|
||||
$20) fee per transaction.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
Modrinth will deduct <strong>2%</strong> for the fee (minimum of $0.25
|
||||
and maximum of $20) from <strong>all transfers</strong> and if the fee
|
||||
PayPal charges is less than the amount we deducted, the difference will
|
||||
be added back to your Modrinth balance. This happens as Modrinth cannot
|
||||
determine if a transaction will be in the United States or international
|
||||
or not until after the transaction has been made.
|
||||
</p>
|
||||
<h3>Venmo (United States only)</h3>
|
||||
<p>
|
||||
Venmo will charge a $0.25 processing fee per transaction, which will be
|
||||
deducted from the amount you choose to transfer.
|
||||
</p>
|
||||
<h2>Currency conversions</h2>
|
||||
<p>
|
||||
All revenue generated by Modrinth is in United States dollars. Any
|
||||
conversions to your local currency will happen at withdrawal and is not
|
||||
handled by Modrinth. Modrinth cannot guarantee any exchange rate, so
|
||||
only USD is displayed in the creator dashboard.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TransferIcon from '~/assets/images/utils/transfer.svg?inline'
|
||||
import SettingsIcon from '~/assets/images/utils/settings.svg?inline'
|
||||
import ModalTransfer from '~/components/ui/ModalTransfer'
|
||||
|
||||
export default {
|
||||
components: { TransferIcon, SettingsIcon, ModalTransfer },
|
||||
data() {
|
||||
return {
|
||||
minWithdraw: 0.26,
|
||||
enrolled:
|
||||
this.$auth.user.payout_data.payout_wallet &&
|
||||
this.$auth.user.payout_data.payout_wallet_type &&
|
||||
this.$auth.user.payout_data.payout_address,
|
||||
}
|
||||
},
|
||||
head: {
|
||||
title: 'Revenue - Modrinth',
|
||||
},
|
||||
methods: {},
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
strong {
|
||||
color: var(--color-text-dark);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--spacing-card-sm);
|
||||
}
|
||||
</style>
|
||||
@ -8,6 +8,7 @@
|
||||
alt="cover-image"
|
||||
/>
|
||||
<div class="text">
|
||||
<ModrinthLogo aria-hidden="true" class="modrinth-logo" />
|
||||
<h1>Discover, Play, and Create Minecraft content</h1>
|
||||
<h3>
|
||||
Find enjoyable, quality content through our
|
||||
@ -31,7 +32,7 @@
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
<button class="iconified-button brand-button-colors" type="submit">
|
||||
<button class="iconified-button brand-button" type="submit">
|
||||
<RightArrowIcon />
|
||||
Search
|
||||
</button>
|
||||
@ -76,10 +77,10 @@
|
||||
<h1>Constantly evolving and improving</h1>
|
||||
<p>
|
||||
We are always adding new features and working towards making
|
||||
Modrinth have the friendliest user experience possible. Right now,
|
||||
we're working on finishing modpacks and finally paying out
|
||||
creators on our site. If you have any more feature ideas, feel
|
||||
free to join our
|
||||
Modrinth have the best possible user experience. Right now, we're
|
||||
working on giving creators more analytics, adding new types of
|
||||
projects, our launcher, and much more! If you have any more
|
||||
feature ideas, feel free to join our
|
||||
<a href="https://discord.gg/EUHuJHt" target="_blank">Discord</a>!
|
||||
</p>
|
||||
<div class="features">
|
||||
@ -95,14 +96,6 @@
|
||||
<CheckIcon />
|
||||
<p>Real-time search</p>
|
||||
</div>
|
||||
<div class="feature completed">
|
||||
<CheckIcon />
|
||||
<p>Customizable project pages</p>
|
||||
</div>
|
||||
<div class="feature completed">
|
||||
<CheckIcon />
|
||||
<p>Robust team management</p>
|
||||
</div>
|
||||
<div class="feature completed">
|
||||
<CheckIcon />
|
||||
<p>
|
||||
@ -115,22 +108,30 @@
|
||||
<CheckIcon />
|
||||
<p>Dependency management</p>
|
||||
</div>
|
||||
<div class="feature in-progress">
|
||||
<div class="feature completed">
|
||||
<CheckIcon />
|
||||
<p>Modpacks</p>
|
||||
</div>
|
||||
<div class="feature in-progress">
|
||||
<InProgressIcon />
|
||||
<p>Creator payouts</p>
|
||||
</div>
|
||||
<div class="feature in-progress">
|
||||
<InProgressIcon />
|
||||
<p>More project types</p>
|
||||
<div class="feature completed">
|
||||
<CheckIcon />
|
||||
<p>Creator monetization</p>
|
||||
</div>
|
||||
<div class="feature in-progress">
|
||||
<InProgressIcon />
|
||||
<p>Creator analytics</p>
|
||||
</div>
|
||||
<div class="feature in-progress blurred">
|
||||
<InProgressIcon />
|
||||
<p>[Redacted]</p>
|
||||
</div>
|
||||
<div class="feature in-progress">
|
||||
<InProgressIcon />
|
||||
<p>More types of projects</p>
|
||||
</div>
|
||||
<div class="feature in-progress">
|
||||
<InProgressIcon />
|
||||
<p>Game launcher</p>
|
||||
</div>
|
||||
<div class="feature in-progress">
|
||||
<InProgressIcon />
|
||||
<p>In-house authentication</p>
|
||||
@ -166,7 +167,7 @@
|
||||
<span>14</span>
|
||||
<span>15</span>
|
||||
</div>
|
||||
<div class="text" v-highlightjs>
|
||||
<div v-highlightjs class="text">
|
||||
<span class="command">curl</span>
|
||||
<span>https://api.modrinth.com/v2/project/sodium</span>
|
||||
<pre><code class="language-json">
|
||||
@ -211,6 +212,7 @@ import InProgressIcon from '~/assets/images/utils/updated.svg?inline'
|
||||
import RightArrowIcon from '~/assets/images/utils/right-arrow.svg?inline'
|
||||
|
||||
import FeaturesIllustration from '~/assets/images/illustrations/features.svg?inline'
|
||||
import ModrinthLogo from '~/assets/images/text-logo.svg?inline'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -219,6 +221,7 @@ export default {
|
||||
CheckIcon,
|
||||
InProgressIcon,
|
||||
FeaturesIllustration,
|
||||
ModrinthLogo,
|
||||
},
|
||||
auth: false,
|
||||
data() {
|
||||
@ -240,6 +243,7 @@ export default {
|
||||
height: 40rem;
|
||||
object-fit: cover;
|
||||
object-position: 15% 12.5%;
|
||||
box-shadow: var(--shadow-card);
|
||||
|
||||
@media screen and (max-width: 1280px) {
|
||||
border-radius: 0;
|
||||
@ -248,13 +252,17 @@ export default {
|
||||
|
||||
.text {
|
||||
position: absolute;
|
||||
top: calc(5rem + var(--size-navbar-height));
|
||||
top: calc(8rem + var(--size-navbar-height));
|
||||
max-width: 30rem;
|
||||
text-shadow: 4px 4px 8px rgba(0, 0, 0, 0.3);
|
||||
padding-left: 6rem;
|
||||
|
||||
color: #fff;
|
||||
|
||||
.modrinth-logo {
|
||||
display: none;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #fff;
|
||||
margin: 1rem 0;
|
||||
@ -279,13 +287,23 @@ export default {
|
||||
gap: 0.25rem;
|
||||
box-sizing: border-box;
|
||||
|
||||
input {
|
||||
box-sizing: content-box;
|
||||
button {
|
||||
max-height: unset;
|
||||
min-height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 750px) {
|
||||
padding: 1rem;
|
||||
padding: 1rem 1.5rem;
|
||||
top: 5rem;
|
||||
|
||||
.modrinth-logo {
|
||||
--color-brand: currentColor;
|
||||
display: unset;
|
||||
width: 14rem;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -344,7 +362,15 @@ export default {
|
||||
|
||||
&.in-progress {
|
||||
svg {
|
||||
color: var(--color-badge-yellow-text);
|
||||
color: var(--color-badge-yellow-bg);
|
||||
}
|
||||
}
|
||||
|
||||
&.blurred {
|
||||
p {
|
||||
user-select: none;
|
||||
color: transparent;
|
||||
text-shadow: 0 0 12px var(--color-badge-yellow-bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
59
pages/legal.vue
Normal file
@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<div class="normal-page">
|
||||
<div class="normal-page__sidebar">
|
||||
<aside class="universal-card">
|
||||
<h1>Legal</h1>
|
||||
<NavStack>
|
||||
<NavStackItem link="/legal/terms" label="Terms and Conditions">
|
||||
<TermsIcon />
|
||||
</NavStackItem>
|
||||
<NavStackItem link="/legal/privacy" label="Privacy Policy">
|
||||
<PrivacyIcon />
|
||||
</NavStackItem>
|
||||
<NavStackItem link="/legal/rules" label="Content Rules">
|
||||
<RulesIcon />
|
||||
</NavStackItem>
|
||||
</NavStack>
|
||||
</aside>
|
||||
</div>
|
||||
<div class="normal-page__content">
|
||||
<NuxtChild class="universal-card" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import NavStack from '~/components/ui/NavStack'
|
||||
import NavStackItem from '~/components/ui/NavStackItem'
|
||||
|
||||
import TermsIcon from '~/assets/images/utils/heart-handshake.svg?inline'
|
||||
import PrivacyIcon from '~/assets/images/utils/lock.svg?inline'
|
||||
import RulesIcon from '~/assets/images/sidebar/admin.svg?inline'
|
||||
|
||||
export default {
|
||||
name: 'Settings',
|
||||
components: {
|
||||
NavStack,
|
||||
NavStackItem,
|
||||
TermsIcon,
|
||||
PrivacyIcon,
|
||||
RulesIcon,
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.normal-page__content ::v-deep a {
|
||||
color: var(--color-link);
|
||||
text-decoration: underline;
|
||||
|
||||
&:focus-visible,
|
||||
&:hover {
|
||||
color: var(--color-link-hover);
|
||||
}
|
||||
|
||||
&:active {
|
||||
color: var(--color-link-active);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,188 +1,183 @@
|
||||
<template>
|
||||
<div class="main">
|
||||
<div class="card">
|
||||
<h1>Privacy Policy</h1>
|
||||
<div>
|
||||
<h1>Privacy Policy</h1>
|
||||
|
||||
<p>
|
||||
At Modrinth, accessible from https://modrinth.com, one of our main
|
||||
priorities is the privacy of our visitors. This Privacy Policy document
|
||||
contains types of information that is collected and recorded by Modrinth
|
||||
and how we use it.
|
||||
</p>
|
||||
<p>
|
||||
At Modrinth, accessible from https://modrinth.com, one of our main
|
||||
priorities is the privacy of our visitors. This Privacy Policy document
|
||||
contains types of information that is collected and recorded by Modrinth
|
||||
and how we use it.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
If you have additional questions or require more information about our
|
||||
Privacy Policy, do not hesitate to contact us.
|
||||
</p>
|
||||
<p>
|
||||
If you have additional questions or require more information about our
|
||||
Privacy Policy, do not hesitate to contact us.
|
||||
</p>
|
||||
|
||||
<h2>General Data Protection Regulation (GDPR)</h2>
|
||||
<p>We (Modrinth) are a Data Controller of your information.</p>
|
||||
<h2>General Data Protection Regulation (GDPR)</h2>
|
||||
<p>We (Modrinth) are a Data Controller of your information.</p>
|
||||
|
||||
<p>
|
||||
Rinth, Inc. legal basis for collecting and using the personal
|
||||
information described in this Privacy Policy depends on the Personal
|
||||
Information we collect and the specific context in which we collect the
|
||||
information:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Rinth, Inc. needs to perform a contract with you</li>
|
||||
<li>You have given Rinth, Inc. permission to do so</li>
|
||||
<li>
|
||||
Processing your personal information is in Rinth, Inc. legitimate
|
||||
interests
|
||||
</li>
|
||||
<li>Rinth, Inc. needs to comply with the law</li>
|
||||
</ul>
|
||||
<p>
|
||||
Rinth, Inc. legal basis for collecting and using the personal information
|
||||
described in this Privacy Policy depends on the Personal Information we
|
||||
collect and the specific context in which we collect the information:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Rinth, Inc. needs to perform a contract with you</li>
|
||||
<li>You have given Rinth, Inc. permission to do so</li>
|
||||
<li>
|
||||
Processing your personal information is in Rinth, Inc. legitimate
|
||||
interests
|
||||
</li>
|
||||
<li>Rinth, Inc. needs to comply with the law</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
Rinth, Inc. will retain your personal information only for as long as is
|
||||
necessary for the purposes set out in this Privacy Policy. We will
|
||||
retain and use your information to the extent necessary to comply with
|
||||
our legal obligations, resolve disputes, and enforce our policies.
|
||||
</p>
|
||||
<p>
|
||||
Rinth, Inc. will retain your personal information only for as long as is
|
||||
necessary for the purposes set out in this Privacy Policy. We will retain
|
||||
and use your information to the extent necessary to comply with our legal
|
||||
obligations, resolve disputes, and enforce our policies.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
If you are a resident of the European Economic Area (EEA), you have
|
||||
certain data protection rights. If you wish to be informed what Personal
|
||||
Information we hold about you and if you want it to be removed from our
|
||||
systems, please contact us.
|
||||
</p>
|
||||
<p>
|
||||
If you are a resident of the European Economic Area (EEA), you have
|
||||
certain data protection rights. If you wish to be informed what Personal
|
||||
Information we hold about you and if you want it to be removed from our
|
||||
systems, please contact us by email at
|
||||
<a href="mailto:gdpr@modrinth.com">gdpr@modrinth.com</a>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
In certain circumstances, you have the following data protection rights:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
The right to access, update or to delete the information we have on
|
||||
you. (please email admin@modrinth.com for data requests)
|
||||
</li>
|
||||
<li>The right of rectification.</li>
|
||||
<li>The right to object.</li>
|
||||
<li>The right of restriction.</li>
|
||||
<li>The right to data portability</li>
|
||||
<li>The right to withdraw consent</li>
|
||||
</ul>
|
||||
<p>
|
||||
In certain circumstances, you have the following data protection rights:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
The right to access, update or to delete the information we have on you.
|
||||
</li>
|
||||
<li>The right of rectification.</li>
|
||||
<li>The right to object.</li>
|
||||
<li>The right of restriction.</li>
|
||||
<li>The right to data portability.</li>
|
||||
<li>The right to withdraw consent.</li>
|
||||
</ul>
|
||||
|
||||
<h2>Log Files</h2>
|
||||
<h2>Log Files</h2>
|
||||
|
||||
<p>
|
||||
Modrinth follows a standard procedure of using log files. These files
|
||||
log visitors when they visit websites. All hosting companies do this and
|
||||
a part of hosting services' analytics. The information collected by log
|
||||
files include internet protocol (IP) addresses, browser type, Internet
|
||||
Service Provider (ISP), date and time stamp, referring/exit pages, and
|
||||
possibly the number of clicks. These are not linked to any information
|
||||
that is personally identifiable. The purpose of the information is for
|
||||
analyzing trends, administering the site, tracking users' movement on
|
||||
the website, and gathering demographic information.
|
||||
</p>
|
||||
<p>
|
||||
Modrinth follows a standard procedure of using log files. These files log
|
||||
visitors when they visit websites. All hosting companies do this and a
|
||||
part of hosting services' analytics. The information collected by log
|
||||
files include internet protocol (IP) addresses, browser type, Internet
|
||||
Service Provider (ISP), date and time stamp, referring/exit pages, and
|
||||
possibly the number of clicks. These are not linked to any information
|
||||
that is personally identifiable. The purpose of the information is for
|
||||
analyzing trends, administering the site, tracking users' movement on the
|
||||
website, and gathering demographic information.
|
||||
</p>
|
||||
|
||||
<h2>Cookies and Web Beacons</h2>
|
||||
<h2>Cookies and Web Beacons</h2>
|
||||
|
||||
<p>
|
||||
Like any other website, Modrinth uses 'cookies'. These cookies are used
|
||||
to store information including visitors' preferences, and the pages on
|
||||
the website that the visitor accessed or visited. The information is
|
||||
used to optimize the users' experience by customizing our web page
|
||||
content based on visitors' browser type and/or other information.
|
||||
</p>
|
||||
<p>
|
||||
Like any other website, Modrinth uses 'cookies'. These cookies are used to
|
||||
store information including visitors' preferences, and the pages on the
|
||||
website that the visitor accessed or visited. The information is used to
|
||||
optimize the users' experience by customizing our web page content based
|
||||
on visitors' browser type and/or other information.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
For more general information on cookies, please read
|
||||
<a href="https://www.privacypolicies.com/blog/cookies/"
|
||||
>"What Are Cookies"</a
|
||||
>.
|
||||
</p>
|
||||
<p>
|
||||
For more general information on cookies, please read
|
||||
<a href="https://www.privacypolicies.com/blog/cookies/"
|
||||
>"What Are Cookies"</a
|
||||
>.
|
||||
</p>
|
||||
|
||||
<h2>Privacy Policies</h2>
|
||||
<h2>Privacy Policies</h2>
|
||||
|
||||
<P
|
||||
>You may consult this list to find the Privacy Policy for each of the
|
||||
advertising partners of Modrinth.</P
|
||||
<P
|
||||
>You may consult this list to find the Privacy Policy for each of the
|
||||
advertising partners of Modrinth.</P
|
||||
>
|
||||
|
||||
<p>
|
||||
Third-party ad servers or ad networks uses technologies like cookies,
|
||||
JavaScript, or Web Beacons that are used in their respective
|
||||
advertisements and links that appear on Modrinth, which are sent directly
|
||||
to users' browser. They automatically receive your IP address when this
|
||||
occurs. These technologies are used to measure the effectiveness of their
|
||||
advertising campaigns and/or to personalize the advertising content that
|
||||
you see on websites that you visit.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Note that Modrinth has no access to or control over these cookies that are
|
||||
used by third-party advertisers.
|
||||
</p>
|
||||
|
||||
<h2>Third Party Privacy Policies</h2>
|
||||
|
||||
<p>
|
||||
Modrinth's Privacy Policy does not apply to other advertisers or websites.
|
||||
Thus, we are advising you to consult the respective Privacy Policies of
|
||||
these third-party ad servers for more detailed information. It may include
|
||||
their practices and instructions about how to opt-out of certain options.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
You can choose to disable cookies through your individual browser options.
|
||||
To know more detailed information about cookie management with specific
|
||||
web browsers, it can be found at the browsers' respective websites.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
In addition, anytime you click on links (including advertising banners),
|
||||
which take you to third-party websites, you will be subject to the third
|
||||
parties’ privacy policies. While we support the protection of our
|
||||
customer’s privacy on the Internet, Modrinth expressly disclaims any and
|
||||
all liability for the actions of third parties, including but without
|
||||
limitation to actions relating to the use and/or disclosure of personal
|
||||
information by third parties.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Modrinth makes use of external ads providers. These include EthicalAds.
|
||||
You can read EthicalAds's privacy policy here and the data they collect:
|
||||
<a href="https://www.ethicalads.io/privacy-policy/"
|
||||
>https://www.ethicalads.io/privacy-policy/</a
|
||||
>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Third-party ad servers or ad networks uses technologies like cookies,
|
||||
JavaScript, or Web Beacons that are used in their respective
|
||||
advertisements and links that appear on Modrinth, which are sent
|
||||
directly to users' browser. They automatically receive your IP address
|
||||
when this occurs. These technologies are used to measure the
|
||||
effectiveness of their advertising campaigns and/or to personalize the
|
||||
advertising content that you see on websites that you visit.
|
||||
</p>
|
||||
<h2>Children's Information</h2>
|
||||
|
||||
<p>
|
||||
Note that Modrinth has no access to or control over these cookies that
|
||||
are used by third-party advertisers.
|
||||
</p>
|
||||
<p>
|
||||
Another part of our priority is adding protection for children while using
|
||||
the internet. We encourage parents and guardians to observe, participate
|
||||
in, and/or monitor and guide their online activity.
|
||||
</p>
|
||||
|
||||
<h2>Third Party Privacy Policies</h2>
|
||||
<p>
|
||||
Modrinth does not knowingly collect any Personal Identifiable Information
|
||||
from children under the age of 13. If you think that your child provided
|
||||
this kind of information on our website, we strongly encourage you to
|
||||
contact us immediately and we will do our best efforts to promptly remove
|
||||
such information from our records.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Modrinth's Privacy Policy does not apply to other advertisers or
|
||||
websites. Thus, we are advising you to consult the respective Privacy
|
||||
Policies of these third-party ad servers for more detailed information.
|
||||
It may include their practices and instructions about how to opt-out of
|
||||
certain options.
|
||||
</p>
|
||||
<h2>Online Privacy Policy Only</h2>
|
||||
|
||||
<p>
|
||||
You can choose to disable cookies through your individual browser
|
||||
options. To know more detailed information about cookie management with
|
||||
specific web browsers, it can be found at the browsers' respective
|
||||
websites.
|
||||
</p>
|
||||
<p>
|
||||
This Privacy Policy applies only to our online activities and is valid for
|
||||
visitors to our website with regards to the information that they shared
|
||||
and/or collect in Modrinth. This policy is not applicable to any
|
||||
information collected offline or via channels other than this website.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
In addition, anytime you click on links (including advertising banners),
|
||||
which take you to third-party websites, you will be subject to the third
|
||||
parties’ privacy policies. While we support the protection of our
|
||||
customer’s privacy on the Internet, Modrinth expressly disclaims any and
|
||||
all liability for the actions of third parties, including but without
|
||||
limitation to actions relating to the use and/or disclosure of personal
|
||||
information by third parties.
|
||||
</p>
|
||||
<h2>Consent</h2>
|
||||
|
||||
<p>
|
||||
Modrinth makes use of external ads providers. These include EthicalAds.
|
||||
You can read EthicalAds's privacy policy here and the data they collect:
|
||||
<a href="https://www.ethicalads.io/privacy-policy/"
|
||||
>https://www.ethicalads.io/privacy-policy/</a
|
||||
>
|
||||
</p>
|
||||
|
||||
<h2>Children's Information</h2>
|
||||
|
||||
<p>
|
||||
Another part of our priority is adding protection for children while
|
||||
using the internet. We encourage parents and guardians to observe,
|
||||
participate in, and/or monitor and guide their online activity.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Modrinth does not knowingly collect any Personal Identifiable
|
||||
Information from children under the age of 13. If you think that your
|
||||
child provided this kind of information on our website, we strongly
|
||||
encourage you to contact us immediately and we will do our best efforts
|
||||
to promptly remove such information from our records.
|
||||
</p>
|
||||
|
||||
<h2>Online Privacy Policy Only</h2>
|
||||
|
||||
<p>
|
||||
This Privacy Policy applies only to our online activities and is valid
|
||||
for visitors to our website with regards to the information that they
|
||||
shared and/or collect in Modrinth. This policy is not applicable to any
|
||||
information collected offline or via channels other than this website.
|
||||
</p>
|
||||
|
||||
<h2>Consent</h2>
|
||||
|
||||
<p>
|
||||
By using our website, you hereby consent to our Privacy Policy and agree
|
||||
to its Terms and Conditions.
|
||||
</p>
|
||||
</div>
|
||||
<p>
|
||||
By using our website, you hereby consent to our Privacy Policy and agree
|
||||
to its Terms and Conditions.
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -196,7 +191,7 @@ export default {
|
||||
hid: 'description',
|
||||
name: 'description',
|
||||
content:
|
||||
'The privacy policy of Modrinth, an open source modding platform. Modrinth currently supports Minecraft, including the forge and fabric mod loaders.',
|
||||
'The Privacy Policy of Modrinth, an open source modding platform focused on Minecraft.',
|
||||
},
|
||||
{
|
||||
hid: 'apple-mobile-web-app-title',
|
||||
@ -218,14 +213,4 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.main {
|
||||
margin: var(--spacing-card-sm) auto;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: underline;
|
||||
color: var(--color-link);
|
||||
}
|
||||
</style>
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@ -1,164 +1,161 @@
|
||||
<template>
|
||||
<div class="main">
|
||||
<div class="card">
|
||||
<h1>Content Rules</h1>
|
||||
<div>
|
||||
<h1>Content Rules</h1>
|
||||
|
||||
<p>
|
||||
In order to facilitate Modrinth's
|
||||
<nuxt-link to="/legal/terms">Terms and Conditions</nuxt-link>, all
|
||||
Content must obey the following Rules. For more information on what
|
||||
exactly Content is, please refer to the Content section of the Terms.
|
||||
</p>
|
||||
<p>
|
||||
In order to facilitate Modrinth's
|
||||
<nuxt-link to="/legal/terms">Terms and Conditions</nuxt-link>, all Content
|
||||
must obey the following Rules. For more information on what exactly
|
||||
Content is, please refer to the Content section of the Terms.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Please note that these are general rules and will not be enforced "to
|
||||
the letter". We reserve the right to modify and/or remove any file,
|
||||
project, or other Content uploaded to our platform for any reason. We
|
||||
reserve the right to introduce new rules at any time, which may or may
|
||||
not retroactively apply to already uploaded Content at the discretion of
|
||||
our moderators.
|
||||
</p>
|
||||
<p>
|
||||
Please note that these are general rules and will not be enforced "to the
|
||||
letter". We reserve the right to modify and/or remove any file, project,
|
||||
or other Content uploaded to our platform for any reason. We reserve the
|
||||
right to introduce new rules at any time, which may or may not
|
||||
retroactively apply to already uploaded Content at the discretion of our
|
||||
moderators.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
If you find any violations of these Rules on our website, it is your
|
||||
responsibility to report it. You may use the Report button on any
|
||||
project, version, or user page, or you may email us at
|
||||
admin@modrinth.com.
|
||||
</p>
|
||||
<p>
|
||||
If you find any violations of these Rules on our website, it is your
|
||||
responsibility to report it. You may use the Report button on any project,
|
||||
version, or user page, or you may email us at
|
||||
<a href="mailto:support@modrinth.com">support@modrinth.com</a>.
|
||||
</p>
|
||||
|
||||
<h2 id="malicious-content">1. Malicious Content</h2>
|
||||
<h2 id="malicious-content">1. Malicious Content</h2>
|
||||
|
||||
<p>
|
||||
Content cannot contain or download malware, which we define as anything
|
||||
that is designed:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
to upload any data to a remote server (i.e. one that the user does not
|
||||
directly choose to connect to in-game) without clear disclosure
|
||||
</li>
|
||||
<li>
|
||||
to disrupt, damage, or otherwise cause harm or damage to an
|
||||
individual, computer, or network
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
Content cannot contain or download malware, which we define as anything
|
||||
that is designed:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
to upload any data to a remote server (i.e. one that the user does not
|
||||
directly choose to connect to in-game) without clear disclosure
|
||||
</li>
|
||||
<li>
|
||||
to disrupt, damage, or otherwise cause harm or damage to an individual,
|
||||
computer, or network
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="clear-and-honest-function">2. Clear and honest function</h2>
|
||||
<h2 id="clear-and-honest-function">2. Clear and honest function</h2>
|
||||
|
||||
<p>
|
||||
Content, especially projects, must make a clear and honest attempt to
|
||||
describe their purpose on the page(s) where it may be found.
|
||||
</p>
|
||||
<p>
|
||||
Content, especially projects, must make a clear and honest attempt to
|
||||
describe their purpose on the page(s) where it may be found.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Content must not make or share intentionally wrong or misleading claims.
|
||||
This includes but is not limited to claims regarding the Content itself,
|
||||
claims regarding other Content, and claims not relating to Content on
|
||||
Modrinth.
|
||||
</p>
|
||||
<p>
|
||||
Content must not make or share intentionally wrong or misleading claims.
|
||||
This includes but is not limited to claims regarding the Content itself,
|
||||
claims regarding other Content, and claims not relating to Content on
|
||||
Modrinth.
|
||||
</p>
|
||||
|
||||
<h3 id="general-expectations">2.1. General expectations</h3>
|
||||
<h3 id="general-expectations">2.1. General expectations</h3>
|
||||
|
||||
<p>
|
||||
Projects in particular must attempt to describe the following three
|
||||
things within their description:
|
||||
</p>
|
||||
<ul>
|
||||
<li>what a project specifically does or adds</li>
|
||||
<li>why someone should want to download the project</li>
|
||||
<li>
|
||||
any other critical information the user must know before downloading
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
Projects in particular must attempt to describe the following three things
|
||||
within their description:
|
||||
</p>
|
||||
<ul>
|
||||
<li>what a project specifically does or adds</li>
|
||||
<li>why someone should want to download the project</li>
|
||||
<li>
|
||||
any other critical information the user must know before downloading
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
Project descriptions must also be accessible. For the most part, this
|
||||
means that descriptions cannot mostly consist of text within images, and
|
||||
necessary information cannot be obscured.
|
||||
</p>
|
||||
<p>
|
||||
Project descriptions must also be accessible. For the most part, this
|
||||
means that descriptions cannot mostly consist of text within images, and
|
||||
necessary information cannot be obscured.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Projects which don't meet of these expectations may be removed from
|
||||
search rather than removed from the platform altogether, at the
|
||||
moderators' discretion.
|
||||
</p>
|
||||
<p>
|
||||
Projects which don't meet of these expectations may be removed from search
|
||||
rather than removed from the platform altogether, at the moderators'
|
||||
discretion.
|
||||
</p>
|
||||
|
||||
<h2 id="cheats-and-hacks">3. Cheats and Hacks</h2>
|
||||
<h2 id="cheats-and-hacks">3. Cheats and Hacks</h2>
|
||||
|
||||
<p>
|
||||
Projects cannot contain or download "cheats", which we define as a
|
||||
client-side modification that:
|
||||
</p>
|
||||
<ul>
|
||||
<li>is advertised as a "cheat", "hack", or "hacked client"</li>
|
||||
<li>
|
||||
gives an unfair advantage in a multiplayer setting over other players
|
||||
that do not have a comparable modification and does not provide a
|
||||
server-side opt-out
|
||||
</li>
|
||||
<li>
|
||||
contains any of the following functions without requiring a
|
||||
server-side opt-in:
|
||||
<ul>
|
||||
<li>X-ray or the ability to see through opaque blocks</li>
|
||||
<li>aim bot or aim assist</li>
|
||||
<li>flight, speed, or other movement modifications</li>
|
||||
<li>automatic PvP</li>
|
||||
<li>
|
||||
active client-side hiding of third party modifications that have
|
||||
server-side opt-outs
|
||||
</li>
|
||||
<li>item duplication</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
Projects cannot contain or download "cheats", which we define as a
|
||||
client-side modification that:
|
||||
</p>
|
||||
<ul>
|
||||
<li>is advertised as a "cheat", "hack", or "hacked client"</li>
|
||||
<li>
|
||||
gives an unfair advantage in a multiplayer setting over other players
|
||||
that do not have a comparable modification and does not provide a
|
||||
server-side opt-out
|
||||
</li>
|
||||
<li>
|
||||
contains any of the following functions without requiring a server-side
|
||||
opt-in:
|
||||
<ul>
|
||||
<li>X-ray or the ability to see through opaque blocks</li>
|
||||
<li>aim bot or aim assist</li>
|
||||
<li>flight, speed, or other movement modifications</li>
|
||||
<li>automatic PvP</li>
|
||||
<li>
|
||||
active client-side hiding of third party modifications that have
|
||||
server-side opt-outs
|
||||
</li>
|
||||
<li>item duplication</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="copyright-and-legality-of-content">
|
||||
4. Copyright and legality of Content
|
||||
</h2>
|
||||
<h2 id="copyright-and-legality-of-content">
|
||||
4. Copyright and legality of Content
|
||||
</h2>
|
||||
|
||||
<p>
|
||||
You must own or have the necessary licenses, rights, consents, and
|
||||
permissions to store, share, or distribute the Content that is uploaded
|
||||
under your Modrinth account.
|
||||
</p>
|
||||
<p>
|
||||
You must own or have the necessary licenses, rights, consents, and
|
||||
permissions to store, share, or distribute the Content that is uploaded
|
||||
under your Modrinth account.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Content may not be directly "reuploaded" from another platform without
|
||||
the permission of the author or copyright holder, even with the
|
||||
appropriate licensing or other rights. This restriction does not apply
|
||||
to content within modpacks or to so called "forks" - that is, modified
|
||||
copies of a project which have diverged substantially enough from the
|
||||
original project, at the discretion of Modrinth's moderators.
|
||||
</p>
|
||||
<p>
|
||||
Content may not be directly "reuploaded" from another platform without the
|
||||
permission of the author or copyright holder, even with the appropriate
|
||||
licensing or other rights. This restriction does not apply to content
|
||||
within modpacks or to so called "forks" - that is, modified copies of a
|
||||
project which have diverged substantially enough from the original
|
||||
project, at the discretion of Modrinth's moderators.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Content must not infringe upon anyone's rights or intellectual property.
|
||||
</p>
|
||||
<p>
|
||||
Content must not infringe upon anyone's rights or intellectual property.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Content must abide by the laws which govern Rinth, Inc., i.e. those of
|
||||
the United States and of the State of Delaware.
|
||||
</p>
|
||||
<p>
|
||||
Content must abide by the laws which govern Rinth, Inc., i.e. those of the
|
||||
United States and of the State of Delaware.
|
||||
</p>
|
||||
|
||||
<h2 id="prohibited-content">5. Prohibited Content</h2>
|
||||
<h2 id="prohibited-content">5. Prohibited Content</h2>
|
||||
|
||||
<p>
|
||||
Content on Modrinth is meant to be appropriate for audiences 13 years of
|
||||
age and above.
|
||||
</p>
|
||||
<p>
|
||||
Content on Modrinth is meant to be appropriate for audiences 13 years of
|
||||
age and above.
|
||||
</p>
|
||||
|
||||
<p>This means that the following Content is not allowed:</p>
|
||||
<ul>
|
||||
<li>Content containing sexual or explicit material</li>
|
||||
<li>Content promoting or sharing harmful or hateful behavior</li>
|
||||
<li>
|
||||
Content with a focus on items such as alcohol, tobacco, and other
|
||||
drugs
|
||||
</li>
|
||||
<li>Content with an excessive amount of profane language</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p>This means that the following Content is not allowed:</p>
|
||||
<ul>
|
||||
<li>Content containing sexual or explicit material</li>
|
||||
<li>Content promoting or sharing harmful or hateful behavior</li>
|
||||
<li>
|
||||
Content with a focus on items such as alcohol, tobacco, and other drugs
|
||||
</li>
|
||||
<li>Content with an excessive amount of profane language</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -194,14 +191,4 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.main {
|
||||
margin: var(--spacing-card-sm) auto;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: underline;
|
||||
color: var(--color-link);
|
||||
}
|
||||
</style>
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@ -1,152 +1,149 @@
|
||||
<template>
|
||||
<div class="main">
|
||||
<div class="card">
|
||||
<h1>Terms and Conditions</h1>
|
||||
<div>
|
||||
<h1>Terms and Conditions</h1>
|
||||
|
||||
<h2>1. Terms</h2>
|
||||
<h2>1. Terms</h2>
|
||||
|
||||
<p>
|
||||
By accessing this Website, accessible from https://modrinth.com, you are
|
||||
agreeing to be bound by these Website Terms and Conditions of Use and
|
||||
agree that you are responsible for the agreement with any applicable
|
||||
local laws. If you disagree with any of these terms, you are prohibited
|
||||
from accessing this site. The materials contained in this Website are
|
||||
protected by copyright and trade mark law.
|
||||
</p>
|
||||
<p>
|
||||
By accessing this Website, accessible from https://modrinth.com, you are
|
||||
agreeing to be bound by these Website Terms and Conditions of Use and
|
||||
agree that you are responsible for the agreement with any applicable local
|
||||
laws. If you disagree with any of these terms, you are prohibited from
|
||||
accessing this site. The materials contained in this Website are protected
|
||||
by copyright and trade mark law.
|
||||
</p>
|
||||
|
||||
<h2>2. Use License</h2>
|
||||
<h2>2. Use License</h2>
|
||||
|
||||
<p>
|
||||
Permission is granted to temporarily download one copy of the materials
|
||||
on Rinth, Inc.'s Website for personal, non-commercial transitory viewing
|
||||
only. This is the grant of a license, not a transfer of title, and under
|
||||
this license you may not:
|
||||
</p>
|
||||
<p>
|
||||
Permission is granted to temporarily download one copy of the materials on
|
||||
Rinth, Inc.'s Website for personal, non-commercial transitory viewing
|
||||
only. This is the grant of a license, not a transfer of title, and under
|
||||
this license you may not:
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li>modify or copy the materials;</li>
|
||||
<li>
|
||||
use the materials for any commercial purpose or for any public
|
||||
display;
|
||||
</li>
|
||||
<li>
|
||||
attempt to reverse engineer any software contained on Rinth, Inc.'s
|
||||
Website;
|
||||
</li>
|
||||
<li>
|
||||
remove any copyright or other proprietary notations from the
|
||||
materials; or
|
||||
</li>
|
||||
<li>
|
||||
transferring the materials to another person or "mirror" the materials
|
||||
on any other server.
|
||||
</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li>modify or copy the materials;</li>
|
||||
<li>
|
||||
use the materials for any commercial purpose or for any public display;
|
||||
</li>
|
||||
<li>
|
||||
attempt to reverse engineer any software contained on Rinth, Inc.'s
|
||||
Website;
|
||||
</li>
|
||||
<li>
|
||||
remove any copyright or other proprietary notations from the materials;
|
||||
or
|
||||
</li>
|
||||
<li>
|
||||
transferring the materials to another person or "mirror" the materials
|
||||
on any other server.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
This will let Rinth, Inc. to terminate upon violations of any of these
|
||||
restrictions. Upon termination, your viewing right will also be
|
||||
terminated and you should destroy any downloaded materials in your
|
||||
possession whether it is printed or electronic format.
|
||||
</p>
|
||||
<p>
|
||||
This will let Rinth, Inc. to terminate upon violations of any of these
|
||||
restrictions. Upon termination, your viewing right will also be terminated
|
||||
and you should destroy any downloaded materials in your possession whether
|
||||
it is printed or electronic format.
|
||||
</p>
|
||||
|
||||
<h2>3. Disclaimer</h2>
|
||||
<h2>3. Disclaimer</h2>
|
||||
|
||||
<p>
|
||||
All the materials on Rinth, Inc.’s Website are provided "as is". Rinth,
|
||||
Inc. makes no warranties, may it be expressed or implied, therefore
|
||||
negates all other warranties. Furthermore, Rinth, Inc. does not make any
|
||||
representations concerning the accuracy or reliability of the use of the
|
||||
materials on its Website or otherwise relating to such materials or any
|
||||
sites linked to this Website.
|
||||
</p>
|
||||
<p>
|
||||
All the materials on Rinth, Inc.’s Website are provided "as is". Rinth,
|
||||
Inc. makes no warranties, may it be expressed or implied, therefore
|
||||
negates all other warranties. Furthermore, Rinth, Inc. does not make any
|
||||
representations concerning the accuracy or reliability of the use of the
|
||||
materials on its Website or otherwise relating to such materials or any
|
||||
sites linked to this Website.
|
||||
</p>
|
||||
|
||||
<h2>4. Limitations</h2>
|
||||
<h2>4. Limitations</h2>
|
||||
|
||||
<p>
|
||||
Rinth, Inc. or its suppliers will not be hold accountable for any
|
||||
damages that will arise with the use or inability to use the materials
|
||||
on Rinth, Inc.’s Website, even if Rinth, Inc. or an authorize
|
||||
representative of this Website has been notified, orally or written, of
|
||||
the possibility of such damage. Some jurisdiction does not allow
|
||||
limitations on implied warranties or limitations of liability for
|
||||
incidental damages, these limitations may not apply to you.
|
||||
</p>
|
||||
<p>
|
||||
Rinth, Inc. or its suppliers will not be hold accountable for any damages
|
||||
that will arise with the use or inability to use the materials on Rinth,
|
||||
Inc.’s Website, even if Rinth, Inc. or an authorize representative of this
|
||||
Website has been notified, orally or written, of the possibility of such
|
||||
damage. Some jurisdiction does not allow limitations on implied warranties
|
||||
or limitations of liability for incidental damages, these limitations may
|
||||
not apply to you.
|
||||
</p>
|
||||
|
||||
<h2>5. Revisions and Errata</h2>
|
||||
<h2>5. Revisions and Errata</h2>
|
||||
|
||||
<p>
|
||||
The materials appearing on Rinth, Inc.’s Website may include technical,
|
||||
typographical, or photographic errors. Rinth, Inc. will not promise that
|
||||
any of the materials in this Website are accurate, complete, or current.
|
||||
Rinth, Inc. may change the materials contained on its Website at any
|
||||
time without notice. Rinth, Inc. does not make any commitment to update
|
||||
the materials.
|
||||
</p>
|
||||
<p>
|
||||
The materials appearing on Rinth, Inc.’s Website may include technical,
|
||||
typographical, or photographic errors. Rinth, Inc. will not promise that
|
||||
any of the materials in this Website are accurate, complete, or current.
|
||||
Rinth, Inc. may change the materials contained on its Website at any time
|
||||
without notice. Rinth, Inc. does not make any commitment to update the
|
||||
materials.
|
||||
</p>
|
||||
|
||||
<h2>6. Links</h2>
|
||||
<h2>6. Links</h2>
|
||||
|
||||
<p>
|
||||
Rinth, Inc. has not reviewed all of the sites linked to its Website and
|
||||
is not responsible for the contents of any such linked site. The
|
||||
presence of any link does not imply endorsement by Rinth, Inc. of the
|
||||
site. The use of any linked website is at the user’s own risk.
|
||||
</p>
|
||||
<p>
|
||||
Rinth, Inc. has not reviewed all of the sites linked to its Website and is
|
||||
not responsible for the contents of any such linked site. The presence of
|
||||
any link does not imply endorsement by Rinth, Inc. of the site. The use of
|
||||
any linked website is at the user’s own risk.
|
||||
</p>
|
||||
|
||||
<h2>7. Site Terms of Use Modifications</h2>
|
||||
<h2>7. Site Terms of Use Modifications</h2>
|
||||
|
||||
<p>
|
||||
Rinth, Inc. may revise these Terms of Use for its Website at any time
|
||||
without prior notice. By using this Website, you are agreeing to be
|
||||
bound by the current version of these Terms and Conditions of Use.
|
||||
</p>
|
||||
<p>
|
||||
Rinth, Inc. may revise these Terms of Use for its Website at any time
|
||||
without prior notice. By using this Website, you are agreeing to be bound
|
||||
by the current version of these Terms and Conditions of Use.
|
||||
</p>
|
||||
|
||||
<h2>8. Your Privacy</h2>
|
||||
<h2>8. Your Privacy</h2>
|
||||
|
||||
<p>
|
||||
Please read our
|
||||
<nuxt-link to="/legal/privacy"> Privacy Policy</nuxt-link>.
|
||||
</p>
|
||||
<p>
|
||||
Please read our
|
||||
<nuxt-link to="/legal/privacy"> Privacy Policy</nuxt-link>.
|
||||
</p>
|
||||
|
||||
<h2>9. Governing Law</h2>
|
||||
<h2>9. Governing Law</h2>
|
||||
|
||||
<p>
|
||||
Any claim related to Rinth, Inc.'s Website shall be governed by the laws
|
||||
of us without regards to its conflict of law provisions.
|
||||
</p>
|
||||
<p>
|
||||
Any claim related to Rinth, Inc.'s Website shall be governed by the laws
|
||||
of us without regards to its conflict of law provisions.
|
||||
</p>
|
||||
|
||||
<h2>10. Content</h2>
|
||||
<h2>10. Content</h2>
|
||||
|
||||
<p>
|
||||
When you upload text, software, mods, scripts, graphics, photos, audio,
|
||||
videos, links, interactive features and other materials that may be
|
||||
viewed on or accessed through Modrinth, we refer to it as "Content".
|
||||
</p>
|
||||
<p>
|
||||
When you upload text, software, mods, scripts, graphics, photos, audio,
|
||||
videos, links, interactive features and other materials that may be viewed
|
||||
on or accessed through Modrinth, we refer to it as "Content".
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
You are responsible for all activity and Content that is uploaded
|
||||
under your Modrinth account.
|
||||
</li>
|
||||
<li>
|
||||
You retain all of your ownership rights to your Content. We do not
|
||||
claim any ownership in or to any of your Content.
|
||||
</li>
|
||||
<li>
|
||||
To enable us to provide the services of Modrinth, you hereby grant us
|
||||
a worldwide, non-exclusive, royalty-free, and unrestricted license to
|
||||
use, reproduce, distribute copies, prepare derivative works of, or
|
||||
display Content in connection with Modrinth in any medium and for any
|
||||
purpose (including commercial purposes).
|
||||
</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li>
|
||||
You are responsible for all activity and Content that is uploaded under
|
||||
your Modrinth account.
|
||||
</li>
|
||||
<li>
|
||||
You retain all of your ownership rights to your Content. We do not claim
|
||||
any ownership in or to any of your Content.
|
||||
</li>
|
||||
<li>
|
||||
To enable us to provide the services of Modrinth, you hereby grant us a
|
||||
worldwide, non-exclusive, royalty-free, and unrestricted license to use,
|
||||
reproduce, distribute copies, prepare derivative works of, or display
|
||||
Content in connection with Modrinth in any medium and for any purpose
|
||||
(including commercial purposes).
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
All Content on Modrinth must obey the
|
||||
<nuxt-link to="/legal/rules">Content Rules</nuxt-link>. Please be aware
|
||||
of these Rules before uploading any Content to Modrinth.
|
||||
</p>
|
||||
</div>
|
||||
<p>
|
||||
All Content on Modrinth must obey the
|
||||
<nuxt-link to="/legal/rules">Content Rules</nuxt-link>. Please be aware of
|
||||
these Rules before uploading any Content to Modrinth.
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -160,17 +157,17 @@ export default {
|
||||
hid: 'description',
|
||||
name: 'description',
|
||||
content:
|
||||
'The Terms of Service of Modrinth, an open source modding platform. Modrinth currently supports Minecraft, including the forge and fabric mod loaders.',
|
||||
'The Terms and Conditions of Modrinth, an open source modding platform focused on Minecraft.',
|
||||
},
|
||||
{
|
||||
hid: 'apple-mobile-web-app-title',
|
||||
name: 'apple-mobile-web-app-title',
|
||||
content: 'Terms of Service',
|
||||
content: 'Terms and Conditions',
|
||||
},
|
||||
{
|
||||
hid: 'og:title',
|
||||
name: 'og:title',
|
||||
content: 'Terms of Service',
|
||||
content: 'Terms and Conditions',
|
||||
},
|
||||
{
|
||||
hid: 'og:url',
|
||||
@ -182,14 +179,4 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.main {
|
||||
margin: var(--spacing-card-sm) auto;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: underline;
|
||||
color: var(--color-link);
|
||||
}
|
||||
</style>
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<Popup v-if="currentProject" :show-popup="true">
|
||||
<div class="moderation-popup">
|
||||
<h2>Moderation form</h2>
|
||||
<div>
|
||||
<Modal ref="modal" header="Moderation Form">
|
||||
<div v-if="currentProject !== null" class="moderation-modal">
|
||||
<p>
|
||||
Both of these fields are optional, but can be used to communicate
|
||||
problems with a project's team members. The body supports markdown
|
||||
@ -37,46 +36,63 @@
|
||||
placeholder="Enter the message..."
|
||||
/>
|
||||
<h3>Body</h3>
|
||||
<ThisOrThat v-model="bodyViewMode" :items="['source', 'preview']" />
|
||||
<div v-if="bodyViewMode === 'source'" class="textarea-wrapper">
|
||||
<div class="textarea-wrapper">
|
||||
<Chips
|
||||
v-model="bodyViewMode"
|
||||
class="separator"
|
||||
:items="['source', 'preview']"
|
||||
/>
|
||||
<textarea
|
||||
v-if="bodyViewMode === 'source'"
|
||||
id="body"
|
||||
v-model="currentProject.moderation_message_body"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
v-highlightjs
|
||||
class="markdown-body preview"
|
||||
v-html="$xss($md.render(currentProject.moderation_message_body))"
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
v-if="bodyViewMode === 'preview'"
|
||||
v-highlightjs
|
||||
class="markdown-body"
|
||||
v-html="$xss($md.render(currentProject.moderation_message_body))"
|
||||
></div>
|
||||
<div class="buttons">
|
||||
<button class="iconified-button" @click="currentProject = null">
|
||||
<button
|
||||
class="iconified-button"
|
||||
@click="
|
||||
$refs.modal.hide()
|
||||
currentProject = null
|
||||
"
|
||||
>
|
||||
<CrossIcon />
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
class="iconified-button brand-button-colors"
|
||||
@click="saveProject"
|
||||
>
|
||||
<button class="iconified-button brand-button" @click="saveProject">
|
||||
<CheckIcon />
|
||||
Confirm
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Popup>
|
||||
<div class="page-contents">
|
||||
<div class="content">
|
||||
<h1>Moderation</h1>
|
||||
<ThisOrThat
|
||||
v-model="selectedType"
|
||||
class="card"
|
||||
:items="moderationTypes"
|
||||
/>
|
||||
</Modal>
|
||||
<div class="normal-page">
|
||||
<div class="normal-page__sidebar">
|
||||
<aside class="universal-card">
|
||||
<h1>Moderation</h1>
|
||||
<NavStack>
|
||||
<NavStackItem link="" label="All"> </NavStackItem>
|
||||
<NavStackItem
|
||||
v-for="type in moderationTypes"
|
||||
:key="type"
|
||||
:link="'?type=' + type"
|
||||
:label="$formatProjectType(type) + 's'"
|
||||
>
|
||||
</NavStackItem>
|
||||
</NavStack>
|
||||
</aside>
|
||||
</div>
|
||||
<div class="normal-page__content">
|
||||
<div class="projects">
|
||||
<ProjectCard
|
||||
v-for="project in selectedType !== 'all'
|
||||
? projects.filter((x) => x.project_type === selectedType)
|
||||
v-for="project in $route.query.type !== undefined
|
||||
? projects.filter((x) => x.project_type === $route.query.type)
|
||||
: projects"
|
||||
:id="project.slug || project.id"
|
||||
:key="project.id"
|
||||
@ -115,49 +131,48 @@
|
||||
</ProjectCard>
|
||||
</div>
|
||||
<div
|
||||
v-if="selectedType === 'report' || selectedType === 'all'"
|
||||
v-if="
|
||||
$route.query.type === 'report' || $route.query.type === undefined
|
||||
"
|
||||
class="reports"
|
||||
>
|
||||
<div
|
||||
v-for="(report, index) in reports"
|
||||
:key="report.id"
|
||||
class="report card"
|
||||
v-for="(item, index) in reports"
|
||||
:key="index"
|
||||
class="card report"
|
||||
>
|
||||
<div class="header">
|
||||
<h5 class="title">
|
||||
Report for {{ report.item_type }}
|
||||
<nuxt-link
|
||||
:to="
|
||||
'/' +
|
||||
report.item_type +
|
||||
'/' +
|
||||
report.item_id.replace(/\W/g, '')
|
||||
"
|
||||
>{{ report.item_id }}
|
||||
</nuxt-link>
|
||||
</h5>
|
||||
<p
|
||||
<div class="info">
|
||||
<div class="title">
|
||||
<h3>
|
||||
{{ item.item_type }}
|
||||
<a :href="item.url">{{ item.item_id }}</a>
|
||||
</h3>
|
||||
reported by
|
||||
<a :href="`/user/${item.reporter}`">{{ item.reporter }}</a>
|
||||
</div>
|
||||
<div
|
||||
v-highlightjs
|
||||
class="markdown-body"
|
||||
v-html="$xss($md.render(item.body))"
|
||||
/>
|
||||
<Badge :type="`Marked as ${item.report_type}`" color="yellow" />
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="iconified-button" @click="deleteReport(index)">
|
||||
<TrashIcon /> Delete report
|
||||
</button>
|
||||
<span
|
||||
v-tooltip="
|
||||
$dayjs(report.created).format(
|
||||
$dayjs(item.created).format(
|
||||
'[Created at] YYYY-MM-DD [at] HH:mm A'
|
||||
)
|
||||
"
|
||||
class="date"
|
||||
class="stat"
|
||||
>
|
||||
Created {{ $dayjs(report.created).fromNow() }}
|
||||
</p>
|
||||
<button
|
||||
class="delete iconified-button"
|
||||
@click="deleteReport(index)"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
<CalendarIcon />
|
||||
Created {{ $dayjs(item.created).fromNow() }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-highlightjs
|
||||
class="markdown-body"
|
||||
v-html="$xss($md.render(report.body))"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="reports.length === 0 && projects.length === 0" class="error">
|
||||
@ -171,27 +186,35 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ThisOrThat from '~/components/ui/ThisOrThat'
|
||||
import Chips from '~/components/ui/Chips'
|
||||
import ProjectCard from '~/components/ui/ProjectCard'
|
||||
import Popup from '~/components/ui/Popup'
|
||||
import Modal from '~/components/ui/Modal'
|
||||
import Badge from '~/components/ui/Badge'
|
||||
|
||||
import CheckIcon from '~/assets/images/utils/check.svg?inline'
|
||||
import UnlistIcon from '~/assets/images/utils/eye-off.svg?inline'
|
||||
import CrossIcon from '~/assets/images/utils/x.svg?inline'
|
||||
import TrashIcon from '~/assets/images/utils/trash.svg?inline'
|
||||
import CalendarIcon from '~/assets/images/utils/calendar.svg?inline'
|
||||
import Security from '~/assets/images/illustrations/security.svg?inline'
|
||||
import NavStack from '~/components/ui/NavStack'
|
||||
import NavStackItem from '~/components/ui/NavStackItem'
|
||||
|
||||
export default {
|
||||
name: 'Moderation',
|
||||
components: {
|
||||
ThisOrThat,
|
||||
NavStack,
|
||||
NavStackItem,
|
||||
Chips,
|
||||
ProjectCard,
|
||||
CheckIcon,
|
||||
CrossIcon,
|
||||
UnlistIcon,
|
||||
Popup,
|
||||
Modal,
|
||||
Badge,
|
||||
Security,
|
||||
TrashIcon,
|
||||
CalendarIcon,
|
||||
},
|
||||
async asyncData(data) {
|
||||
const [projects, reports] = (
|
||||
@ -201,10 +224,76 @@ export default {
|
||||
])
|
||||
).map((it) => it.data)
|
||||
|
||||
const newReports = await Promise.all(
|
||||
reports.map(async (report) => {
|
||||
try {
|
||||
report.item_id = report.item_id?.replace
|
||||
? report.item_id.replace(/"/g, '')
|
||||
: report.item_id
|
||||
let url = ''
|
||||
|
||||
if (report.item_type === 'user') {
|
||||
const user = (
|
||||
await data.$axios.get(
|
||||
`user/${report.item_id}`,
|
||||
data.$defaultHeaders()
|
||||
)
|
||||
).data
|
||||
url = `/user/${user.username}`
|
||||
report.item_id = user.username
|
||||
} else if (report.item_type === 'project') {
|
||||
const project = (
|
||||
await data.$axios.get(
|
||||
`project/${report.item_id}`,
|
||||
data.$defaultHeaders()
|
||||
)
|
||||
).data
|
||||
report.item_id = project.slug || report.item_id
|
||||
url = `/${project.project_type}/${report.item_id}`
|
||||
} else if (report.item_type === 'version') {
|
||||
const version = (
|
||||
await data.$axios.get(
|
||||
`version/${report.item_id}`,
|
||||
data.$defaultHeaders()
|
||||
)
|
||||
).data
|
||||
const project = (
|
||||
await data.$axios.get(
|
||||
`project/${version.project_id}`,
|
||||
data.$defaultHeaders()
|
||||
)
|
||||
).data
|
||||
report.item_id = version.version_number || report.item_id
|
||||
url = `/${project.project_type}/${
|
||||
project.slug || project.id
|
||||
}/version/${report.item_id}`
|
||||
}
|
||||
|
||||
report.reporter = (
|
||||
await data.$axios.get(
|
||||
`user/${report.reporter}`,
|
||||
data.$defaultHeaders()
|
||||
)
|
||||
).data.username
|
||||
|
||||
return {
|
||||
...report,
|
||||
moderation_type: 'report',
|
||||
url,
|
||||
}
|
||||
} catch (err) {
|
||||
return {
|
||||
...report,
|
||||
url: 'error',
|
||||
moderation_type: 'report',
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
return {
|
||||
projects,
|
||||
reports,
|
||||
selectedType: 'all',
|
||||
reports: newReports,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@ -218,7 +307,7 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
moderationTypes() {
|
||||
const obj = { all: true }
|
||||
const obj = {}
|
||||
|
||||
for (const project of this.projects) {
|
||||
obj[project.project_type] = true
|
||||
@ -238,6 +327,7 @@ export default {
|
||||
project.newStatus = status
|
||||
|
||||
this.currentProject = project
|
||||
this.$refs.modal.show()
|
||||
},
|
||||
async saveProject() {
|
||||
this.$nuxt.$loading.start()
|
||||
@ -262,6 +352,7 @@ export default {
|
||||
1
|
||||
)
|
||||
this.currentProject = null
|
||||
this.$refs.modal.hide()
|
||||
} catch (err) {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
@ -299,7 +390,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.moderation-popup {
|
||||
.moderation-modal {
|
||||
width: auto;
|
||||
padding: var(--spacing-card-md) var(--spacing-card-lg);
|
||||
|
||||
@ -313,6 +404,19 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.textarea-wrapper {
|
||||
margin-top: 0.5rem;
|
||||
height: 15rem;
|
||||
|
||||
.preview {
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.separator {
|
||||
margin: var(--spacing-card-sm) 0;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
margin-top: 0.5rem;
|
||||
@ -325,28 +429,70 @@ export default {
|
||||
|
||||
h1 {
|
||||
color: var(--color-text-dark);
|
||||
margin: 0 0 1rem 1.5rem;
|
||||
}
|
||||
|
||||
.report {
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.title {
|
||||
font-size: var(--font-size-lg);
|
||||
margin: 0 0.5rem 0 0;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.25rem;
|
||||
flex-wrap: wrap;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
text-transform: capitalize;
|
||||
|
||||
a {
|
||||
text-transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
min-width: fit-content;
|
||||
|
||||
.iconified-button {
|
||||
margin-left: auto;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.stat {
|
||||
margin-top: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
grid-gap: 0.5rem;
|
||||
|
||||
svg {
|
||||
width: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1024px) {
|
||||
.page-contents {
|
||||
max-width: calc(1280px - 20rem) !important;
|
||||
max-width: 800px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,68 +1,93 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="page-contents">
|
||||
<div class="content">
|
||||
<div class="normal-page">
|
||||
<div class="normal-page__sidebar">
|
||||
<aside class="universal-card">
|
||||
<h1>Notifications</h1>
|
||||
|
||||
<div class="divider card">
|
||||
<ThisOrThat
|
||||
v-model="selectedNotificationType"
|
||||
:items="notificationTypes"
|
||||
/>
|
||||
<button class="iconified-button" @click="clearNotifications">
|
||||
<NavStack>
|
||||
<NavStackItem link="" label="All"> </NavStackItem>
|
||||
<NavStackItem
|
||||
v-for="type in notificationTypes"
|
||||
:key="type"
|
||||
:link="'?type=' + type"
|
||||
:label="NOTIFICATION_TYPES[type]"
|
||||
>
|
||||
</NavStackItem>
|
||||
</NavStack>
|
||||
<h3>Manage</h3>
|
||||
<div class="input-group">
|
||||
<NuxtLink class="iconified-button" to="/settings/follows">
|
||||
<SettingsIcon />
|
||||
Followed projects
|
||||
</NuxtLink>
|
||||
<button
|
||||
v-if="$user.notifications.length > 0"
|
||||
class="iconified-button danger-button"
|
||||
@click="clearNotifications"
|
||||
>
|
||||
<ClearIcon />
|
||||
Clear all
|
||||
</button>
|
||||
</div>
|
||||
<div class="notifications">
|
||||
<div
|
||||
v-for="notification in selectedNotificationType !== 'all'
|
||||
? $user.notifications.filter(
|
||||
(x) => x.type === NOTIFICATION_TYPES[selectedNotificationType]
|
||||
)
|
||||
: $user.notifications"
|
||||
:key="notification.id"
|
||||
class="card notification"
|
||||
>
|
||||
<div class="icon">
|
||||
<UpdateIcon v-if="notification.type === 'project_update'" />
|
||||
<UsersIcon v-else-if="notification.type === 'team_invite'" />
|
||||
</div>
|
||||
<div class="text">
|
||||
<nuxt-link :to="notification.link" class="top">
|
||||
</aside>
|
||||
</div>
|
||||
<div class="normal-page__content">
|
||||
<div class="notifications">
|
||||
<div
|
||||
v-for="notification in $route.query.type !== undefined
|
||||
? $user.notifications.filter((x) => x.type === $route.query.type)
|
||||
: $user.notifications"
|
||||
:key="notification.id"
|
||||
class="universal-card adjacent-input"
|
||||
>
|
||||
<div class="label">
|
||||
<span class="label__title">
|
||||
<nuxt-link :to="notification.link">
|
||||
<h3 v-html="$xss($md.render(notification.title))" />
|
||||
<span>
|
||||
Notified {{ $dayjs(notification.created).fromNow() }}</span
|
||||
>
|
||||
</nuxt-link>
|
||||
</span>
|
||||
<div class="label__description">
|
||||
<p>{{ notification.text }}</p>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<button
|
||||
v-for="(action, actionIndex) in notification.actions"
|
||||
:key="actionIndex"
|
||||
class="iconified-button"
|
||||
@click="
|
||||
performAction(notification, notificationIndex, actionIndex)
|
||||
<span
|
||||
v-tooltip="
|
||||
$dayjs(notification.created).format(
|
||||
'MMMM D, YYYY [at] h:mm:ss A'
|
||||
)
|
||||
"
|
||||
class="date"
|
||||
>
|
||||
{{ action.title }}
|
||||
</button>
|
||||
<button
|
||||
v-if="notification.actions.length === 0"
|
||||
class="iconified-button"
|
||||
@click="performAction(notification, notificationIndex, null)"
|
||||
<CalendarIcon />
|
||||
Received {{ $dayjs(notification.created).fromNow() }}</span
|
||||
>
|
||||
Dismiss
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="$user.notifications.length === 0" class="error">
|
||||
<UpToDate class="icon"></UpToDate>
|
||||
<br />
|
||||
<span class="text">You are up-to-date!</span>
|
||||
<div class="input-group">
|
||||
<button
|
||||
v-for="(action, actionIndex) in notification.actions"
|
||||
:key="actionIndex"
|
||||
class="iconified-button"
|
||||
:class="`action-button-${action.title
|
||||
.toLowerCase()
|
||||
.replaceAll(' ', '-')}`"
|
||||
@click="
|
||||
performAction(notification, notificationIndex, actionIndex)
|
||||
"
|
||||
>
|
||||
{{ action.title }}
|
||||
</button>
|
||||
<button
|
||||
v-if="notification.actions.length === 0"
|
||||
class="iconified-button"
|
||||
@click="performAction(notification, notificationIndex, null)"
|
||||
>
|
||||
Dismiss
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="$user.notifications.length === 0" class="error">
|
||||
<UpToDate class="icon"></UpToDate>
|
||||
<br />
|
||||
<span class="text">You are up-to-date!</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -70,31 +95,30 @@
|
||||
|
||||
<script>
|
||||
import ClearIcon from '~/assets/images/utils/clear.svg?inline'
|
||||
import UpdateIcon from '~/assets/images/utils/updated.svg?inline'
|
||||
import UsersIcon from '~/assets/images/utils/users.svg?inline'
|
||||
import SettingsIcon from '~/assets/images/utils/settings.svg?inline'
|
||||
import CalendarIcon from '~/assets/images/utils/calendar.svg?inline'
|
||||
import UpToDate from '~/assets/images/illustrations/up_to_date.svg?inline'
|
||||
import ThisOrThat from '~/components/ui/ThisOrThat'
|
||||
import NavStack from '~/components/ui/NavStack'
|
||||
import NavStackItem from '~/components/ui/NavStackItem'
|
||||
|
||||
const NOTIFICATION_TYPES = {
|
||||
'Team invites': 'team_invite',
|
||||
'Project updates': 'project_update',
|
||||
team_invite: 'Team invites',
|
||||
project_update: 'Project updates',
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'Notifications',
|
||||
components: {
|
||||
ThisOrThat,
|
||||
NavStack,
|
||||
NavStackItem,
|
||||
ClearIcon,
|
||||
UpdateIcon,
|
||||
UsersIcon,
|
||||
SettingsIcon,
|
||||
CalendarIcon,
|
||||
UpToDate,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedNotificationType: 'all',
|
||||
}
|
||||
},
|
||||
async fetch() {
|
||||
this.NOTIFICATION_TYPES = NOTIFICATION_TYPES
|
||||
|
||||
await this.$store.dispatch('user/fetchNotifications')
|
||||
},
|
||||
head: {
|
||||
@ -102,24 +126,17 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
notificationTypes() {
|
||||
const obj = { all: true }
|
||||
const obj = {}
|
||||
|
||||
for (const notification of this.$user.notifications.filter(
|
||||
(it) => it.type !== null
|
||||
)) {
|
||||
obj[
|
||||
Object.keys(NOTIFICATION_TYPES).find(
|
||||
(key) => NOTIFICATION_TYPES[key] === notification.type
|
||||
)
|
||||
] = true
|
||||
obj[notification.type] = true
|
||||
}
|
||||
|
||||
return Object.keys(obj)
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.NOTIFICATION_TYPES = NOTIFICATION_TYPES
|
||||
},
|
||||
methods: {
|
||||
async clearNotifications() {
|
||||
try {
|
||||
@ -176,97 +193,37 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
h1 {
|
||||
color: var(--color-text-dark);
|
||||
margin: 0 0 1rem 1.5rem;
|
||||
}
|
||||
|
||||
.divider {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
row-gap: 0.5rem;
|
||||
}
|
||||
|
||||
.notifications {
|
||||
.notification {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: var(--spacing-card-sm) var(--spacing-card-lg);
|
||||
|
||||
.icon {
|
||||
.label {
|
||||
.label__title {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: var(--spacing-card-sm);
|
||||
align-items: baseline;
|
||||
margin-block-start: 0;
|
||||
|
||||
svg {
|
||||
height: calc(3rem - var(--spacing-card-sm));
|
||||
width: auto;
|
||||
margin-right: 1rem;
|
||||
h3 ::v-deep {
|
||||
margin: 0;
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
.label__description {
|
||||
margin: 0;
|
||||
|
||||
.top {
|
||||
.date {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
flex-direction: column;
|
||||
|
||||
h3 ::v-deep {
|
||||
font-size: var(--font-size-lg);
|
||||
margin: 0 0.5rem 0 0;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
|
||||
strong {
|
||||
color: var(--color-brand);
|
||||
}
|
||||
}
|
||||
}
|
||||
align-items: center;
|
||||
gap: var(--spacing-card-xs);
|
||||
color: var(--color-heading);
|
||||
font-weight: 500;
|
||||
font-size: 1rem;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
p {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
margin-left: auto;
|
||||
text-align: right;
|
||||
|
||||
button {
|
||||
margin-left: auto;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1024px) {
|
||||
.page-contents {
|
||||
max-width: calc(1280px - 20rem) !important;
|
||||
}
|
||||
|
||||
.notifications {
|
||||
.notification {
|
||||
flex-wrap: nowrap;
|
||||
|
||||
.text {
|
||||
flex-direction: column;
|
||||
|
||||
.top {
|
||||
flex-direction: row;
|
||||
}
|
||||
margin-block: 0 var(--spacing-card-md);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
382
pages/search.vue
@ -1,20 +1,19 @@
|
||||
<template>
|
||||
<div
|
||||
:class="{
|
||||
'search-page': true,
|
||||
'normal-page': true,
|
||||
'alt-layout': $store.state.cosmetics.searchLayout,
|
||||
}"
|
||||
>
|
||||
<aside class="normal-page__sidebar" aria-label="Filters">
|
||||
<aside
|
||||
:class="{
|
||||
'normal-page__sidebar': true,
|
||||
open: sidebarMenuOpen,
|
||||
}"
|
||||
aria-label="Filters"
|
||||
>
|
||||
<section class="card filters-card" role="presentation">
|
||||
<button
|
||||
class="iconified-button sidebar-menu-close-button"
|
||||
@click="sidebarMenuOpen = !sidebarMenuOpen"
|
||||
>
|
||||
<EyeOffIcon v-if="sidebarMenuOpen" aria-hidden="true" />
|
||||
<EyeIcon v-else aria-hidden="true" />
|
||||
{{ sidebarMenuOpen ? 'Hide filters' : 'Show filters' }}
|
||||
</button>
|
||||
<div
|
||||
class="sidebar-menu"
|
||||
:class="{ 'sidebar-menu_open': sidebarMenuOpen }"
|
||||
@ -37,8 +36,9 @@
|
||||
<div v-for="(categories, header) in categoriesMap" :key="header">
|
||||
<h3
|
||||
v-if="
|
||||
categories.filter((x) => x.project_type === projectType)
|
||||
.length > 0
|
||||
categories.filter(
|
||||
(x) => x.project_type === projectType.actual
|
||||
).length > 0
|
||||
"
|
||||
class="sidebar-menu-heading"
|
||||
>
|
||||
@ -47,7 +47,7 @@
|
||||
|
||||
<SearchFilter
|
||||
v-for="category in categories
|
||||
.filter((x) => x.project_type === projectType)
|
||||
.filter((x) => x.project_type === projectType.actual)
|
||||
.sort((a, b) => {
|
||||
if (header === 'resolutions') {
|
||||
return (
|
||||
@ -68,13 +68,13 @@
|
||||
</div>
|
||||
</section>
|
||||
<section
|
||||
v-if="projectType !== 'resourcepack'"
|
||||
v-if="projectType.id !== 'resourcepack'"
|
||||
aria-label="Loader filters"
|
||||
>
|
||||
<h3
|
||||
v-if="
|
||||
$tag.loaders.filter((x) =>
|
||||
x.supported_project_types.includes(projectType)
|
||||
x.supported_project_types.includes(projectType.actual)
|
||||
).length > 0
|
||||
"
|
||||
class="sidebar-menu-heading"
|
||||
@ -84,8 +84,7 @@
|
||||
<SearchFilter
|
||||
v-for="loader in $tag.loaders.filter((x) => {
|
||||
if (
|
||||
projectType === 'mod' &&
|
||||
!isPlugins &&
|
||||
projectType.id === 'mod' &&
|
||||
!showAllLoaders &&
|
||||
x.name !== 'forge' &&
|
||||
x.name !== 'fabric' &&
|
||||
@ -94,13 +93,13 @@
|
||||
return false
|
||||
}
|
||||
|
||||
if (projectType === 'mod' && showAllLoaders) {
|
||||
if (projectType.id === 'mod' && showAllLoaders) {
|
||||
return $tag.loaderData.modLoaders.includes(x.name)
|
||||
}
|
||||
|
||||
return isPlugins
|
||||
return projectType.id === 'plugin'
|
||||
? $tag.loaderData.pluginLoaders.includes(x.name)
|
||||
: x.supported_project_types.includes(projectType)
|
||||
: x.supported_project_types.includes(projectType.actual)
|
||||
})"
|
||||
:key="loader.name"
|
||||
ref="loaderFilters"
|
||||
@ -111,7 +110,7 @@
|
||||
@toggle="toggleOrFacet"
|
||||
/>
|
||||
<Checkbox
|
||||
v-if="projectType === 'mod' && !isPlugins"
|
||||
v-if="projectType.id === 'mod'"
|
||||
v-model="showAllLoaders"
|
||||
:label="showAllLoaders ? 'Less' : 'More'"
|
||||
description="Show all loaders"
|
||||
@ -120,11 +119,14 @@
|
||||
:collapsing-toggle-style="true"
|
||||
/>
|
||||
</section>
|
||||
<section v-if="isPlugins" aria-label="Platform loader filters">
|
||||
<section
|
||||
v-if="projectType.id === 'plugin'"
|
||||
aria-label="Platform loader filters"
|
||||
>
|
||||
<h3
|
||||
v-if="
|
||||
$tag.loaders.filter((x) =>
|
||||
x.supported_project_types.includes(projectType)
|
||||
x.supported_project_types.includes(projectType.actual)
|
||||
).length > 0
|
||||
"
|
||||
class="sidebar-menu-heading"
|
||||
@ -145,7 +147,7 @@
|
||||
/>
|
||||
</section>
|
||||
<section
|
||||
v-if="projectType !== 'resourcepack' && !isPlugins"
|
||||
v-if="!['resourcepack', 'plugin'].includes(projectType.id)"
|
||||
aria-label="Environment filters"
|
||||
>
|
||||
<h3 class="sidebar-menu-heading">Environments</h3>
|
||||
@ -211,12 +213,16 @@
|
||||
</aside>
|
||||
<section class="normal-page__content">
|
||||
<div
|
||||
v-if="projectType === 'modpack'"
|
||||
v-if="
|
||||
projectType.id === 'modpack' &&
|
||||
$orElse($store.state.cosmetics.modpacksAlphaNotice, true)
|
||||
"
|
||||
class="card warning"
|
||||
aria-label="Warning"
|
||||
>
|
||||
Modpack support is currently in alpha, and you may encounter issues. Our
|
||||
documentation includes instructions on
|
||||
Modpack support is currently in alpha, and can only be created and
|
||||
installed through third party tools. Our documentation includes
|
||||
instructions on
|
||||
<a
|
||||
href="https://docs.modrinth.com/docs/modpacks/playing_modpacks/"
|
||||
target="_blank"
|
||||
@ -242,18 +248,28 @@
|
||||
ethical-ads-big
|
||||
/>
|
||||
<div class="card search-controls">
|
||||
<div class="iconified-input">
|
||||
<label class="hidden" for="search">Search</label>
|
||||
<SearchIcon aria-hidden="true" />
|
||||
<input
|
||||
id="search"
|
||||
v-model="query"
|
||||
type="search"
|
||||
name="search"
|
||||
placeholder="Search..."
|
||||
autocomplete="off"
|
||||
@input="onSearchChange(1)"
|
||||
/>
|
||||
<div class="search-filter-container">
|
||||
<button
|
||||
class="iconified-button sidebar-menu-close-button"
|
||||
:class="{ open: sidebarMenuOpen }"
|
||||
@click="sidebarMenuOpen = !sidebarMenuOpen"
|
||||
>
|
||||
<FilterIcon aria-hidden="true" />
|
||||
Filters...
|
||||
</button>
|
||||
<div class="iconified-input">
|
||||
<label class="hidden" for="search">Search</label>
|
||||
<SearchIcon aria-hidden="true" />
|
||||
<input
|
||||
id="search"
|
||||
v-model="query"
|
||||
type="search"
|
||||
name="search"
|
||||
:placeholder="`Search ${projectType.display}s...`"
|
||||
autocomplete="off"
|
||||
@input="onSearchChange(1)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sort-controls">
|
||||
<div class="labeled-control">
|
||||
@ -293,12 +309,14 @@
|
||||
</div>
|
||||
</div>
|
||||
<pagination
|
||||
:current-page="currentPage"
|
||||
:pages="pages"
|
||||
:page="currentPage"
|
||||
:count="pageCount"
|
||||
:link-function="(x) => getSearchUrl(x <= 1 ? 0 : (x - 1) * maxResults)"
|
||||
class="pagination-before"
|
||||
@switch-page="onSearchChange"
|
||||
></pagination>
|
||||
<div>
|
||||
<div v-if="$fetchState.pending" class="no-results">
|
||||
<div class="search-results-container">
|
||||
<div v-if="isLoading" class="no-results">
|
||||
<LogoAnimated aria-hidden="true" />
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
@ -327,8 +345,10 @@
|
||||
</div>
|
||||
</div>
|
||||
<pagination
|
||||
:current-page="currentPage"
|
||||
:pages="pages"
|
||||
:page="currentPage"
|
||||
:count="pageCount"
|
||||
:link-function="(x) => getSearchUrl(x <= 1 ? 0 : (x - 1) * maxResults)"
|
||||
class="pagination-after"
|
||||
@switch-page="onSearchChangeToTop"
|
||||
></pagination>
|
||||
</section>
|
||||
@ -348,8 +368,7 @@ import ServerSide from '~/assets/images/categories/server.svg?inline'
|
||||
|
||||
import SearchIcon from '~/assets/images/utils/search.svg?inline'
|
||||
import ClearIcon from '~/assets/images/utils/clear.svg?inline'
|
||||
import EyeIcon from '~/assets/images/utils/eye.svg?inline'
|
||||
import EyeOffIcon from '~/assets/images/utils/eye-off.svg?inline'
|
||||
import FilterIcon from '~/assets/images/utils/filter.svg?inline'
|
||||
|
||||
import Advertisement from '~/components/ads/Advertisement'
|
||||
|
||||
@ -366,8 +385,7 @@ export default {
|
||||
ServerSide,
|
||||
SearchIcon,
|
||||
ClearIcon,
|
||||
EyeIcon,
|
||||
EyeOffIcon,
|
||||
FilterIcon,
|
||||
LogoAnimated,
|
||||
},
|
||||
data() {
|
||||
@ -384,11 +402,10 @@ export default {
|
||||
facets: [],
|
||||
orFacets: [],
|
||||
results: null,
|
||||
pages: [],
|
||||
pageCount: 1,
|
||||
currentPage: 1,
|
||||
|
||||
projectType: 'mod',
|
||||
isPlugins: false,
|
||||
projectType: { id: 'mod', display: 'mod', actual: 'mod' },
|
||||
|
||||
sortTypes: [
|
||||
{ display: 'Relevance', name: 'relevance' },
|
||||
@ -405,6 +422,8 @@ export default {
|
||||
showAllLoaders: false,
|
||||
|
||||
skipLink: '#search-results',
|
||||
|
||||
isLoading: true,
|
||||
}
|
||||
},
|
||||
async fetch() {
|
||||
@ -453,17 +472,37 @@ export default {
|
||||
if (this.$route.query.o)
|
||||
this.currentPage = Math.ceil(this.$route.query.o / this.maxResults) + 1
|
||||
|
||||
this.projectType = this.$route.name.substring(
|
||||
0,
|
||||
this.$route.name.length - 1
|
||||
this.projectType = this.$tag.projectTypes.find(
|
||||
(x) => x.id === this.$route.name.substring(0, this.$route.name.length - 1)
|
||||
)
|
||||
|
||||
if (this.projectType === 'plugin') {
|
||||
this.projectType = 'mod'
|
||||
this.isPlugins = true
|
||||
}
|
||||
|
||||
await this.onSearchChange(this.currentPage)
|
||||
|
||||
this.isLoading = false
|
||||
},
|
||||
head() {
|
||||
const name = this.$route.name.substring(0, this.$route.name.length - 1)
|
||||
|
||||
return {
|
||||
title: `Search ${this.$formatProjectType(name)}s - Modrinth`,
|
||||
meta: [
|
||||
{
|
||||
hid: 'apple-mobile-web-app-title',
|
||||
name: 'apple-mobile-web-app-title',
|
||||
content: `Search ${this.$formatProjectType(name)}s`,
|
||||
},
|
||||
{
|
||||
hid: 'og:title',
|
||||
name: 'og:title',
|
||||
content: `Search ${this.$formatProjectType(name)}s`,
|
||||
},
|
||||
{
|
||||
hid: 'description',
|
||||
name: 'description',
|
||||
content: `Search and browse thousands of Minecraft ${name}s on Modrinth with instant, accurate search results. Our filters help you quickly find the best Minecraft ${name}s.\n`,
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
categoriesMap() {
|
||||
@ -494,20 +533,14 @@ export default {
|
||||
watch: {
|
||||
'$route.path': {
|
||||
async handler() {
|
||||
this.projectType = this.$route.name.substring(
|
||||
0,
|
||||
this.$route.name.length - 1
|
||||
this.isLoading = true
|
||||
this.projectType = this.$tag.projectTypes.find(
|
||||
(x) =>
|
||||
x.id === this.$route.name.substring(0, this.$route.name.length - 1)
|
||||
)
|
||||
|
||||
if (this.projectType === 'plugin') {
|
||||
this.projectType = 'mod'
|
||||
this.isPlugins = true
|
||||
} else {
|
||||
this.isPlugins = false
|
||||
}
|
||||
|
||||
this.results = null
|
||||
this.pages = []
|
||||
this.pageCount = 1
|
||||
this.currentPage = 1
|
||||
this.query = ''
|
||||
this.maxResults = 20
|
||||
@ -516,6 +549,8 @@ export default {
|
||||
this.sidebarMenuOpen = false
|
||||
|
||||
await this.clearFilters()
|
||||
|
||||
this.isLoading = false
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -596,6 +631,8 @@ export default {
|
||||
await this.onSearchChange(newPageNumber)
|
||||
},
|
||||
async onSearchChange(newPageNumber) {
|
||||
this.currentPage = newPageNumber
|
||||
|
||||
if (this.query === null) return
|
||||
|
||||
try {
|
||||
@ -623,13 +660,13 @@ export default {
|
||||
// loaders specifier
|
||||
if (this.orFacets.length > 0) {
|
||||
formattedFacets.push(this.orFacets)
|
||||
} else if (this.isPlugins) {
|
||||
} else if (this.projectType.id === 'plugin') {
|
||||
formattedFacets.push(
|
||||
this.$tag.loaderData.allPluginLoaders.map(
|
||||
(x) => `categories:'${encodeURIComponent(x)}'`
|
||||
)
|
||||
)
|
||||
} else if (this.projectType === 'mod') {
|
||||
} else if (this.projectType.id === 'mod') {
|
||||
formattedFacets.push(
|
||||
this.$tag.loaderData.modLoaders.map(
|
||||
(x) => `categories:'${encodeURIComponent(x)}'`
|
||||
@ -682,7 +719,7 @@ export default {
|
||||
}
|
||||
|
||||
if (this.projectType)
|
||||
formattedFacets.push([`project_type:${this.projectType}`])
|
||||
formattedFacets.push([`project_type:${this.projectType.actual}`])
|
||||
|
||||
params.push(`facets=${JSON.stringify(formattedFacets)}`)
|
||||
}
|
||||
@ -703,69 +740,10 @@ export default {
|
||||
const res = await this.$axios.get(url, this.$defaultHeaders())
|
||||
this.results = res.data.hits
|
||||
|
||||
const pageAmount = Math.ceil(res.data.total_hits / res.data.limit)
|
||||
|
||||
this.currentPage = newPageNumber
|
||||
if (pageAmount > 4) {
|
||||
if (this.currentPage + 3 >= pageAmount) {
|
||||
this.pages = [
|
||||
1,
|
||||
'-',
|
||||
pageAmount - 4,
|
||||
pageAmount - 3,
|
||||
pageAmount - 2,
|
||||
pageAmount - 1,
|
||||
pageAmount,
|
||||
]
|
||||
} else if (this.currentPage > 4) {
|
||||
this.pages = [
|
||||
1,
|
||||
'-',
|
||||
this.currentPage - 1,
|
||||
this.currentPage,
|
||||
this.currentPage + 1,
|
||||
'-',
|
||||
pageAmount,
|
||||
]
|
||||
} else {
|
||||
this.pages = [1, 2, 3, 4, 5, '-', pageAmount]
|
||||
}
|
||||
} else {
|
||||
this.pages = Array.from({ length: pageAmount }, (_, i) => i + 1)
|
||||
}
|
||||
this.pageCount = Math.ceil(res.data.total_hits / res.data.limit)
|
||||
|
||||
if (process.client) {
|
||||
const queryItems = []
|
||||
|
||||
if (this.query) queryItems.push(`q=${encodeURIComponent(this.query)}`)
|
||||
if (offset > 0) queryItems.push(`o=${offset}`)
|
||||
if (this.facets.length > 0)
|
||||
queryItems.push(`f=${encodeURIComponent(this.facets)}`)
|
||||
if (this.orFacets.length > 0)
|
||||
queryItems.push(`g=${encodeURIComponent(this.orFacets)}`)
|
||||
if (this.selectedVersions.length > 0)
|
||||
queryItems.push(`v=${encodeURIComponent(this.selectedVersions)}`)
|
||||
if (this.selectedLicenses.length > 0)
|
||||
queryItems.push(`l=${encodeURIComponent(this.selectedLicenses)}`)
|
||||
if (this.showSnapshots) url += `h=true`
|
||||
if (this.selectedEnvironments.length > 0)
|
||||
queryItems.push(
|
||||
`e=${encodeURIComponent(this.selectedEnvironments)}`
|
||||
)
|
||||
if (this.sortType.name !== 'relevance')
|
||||
queryItems.push(`s=${encodeURIComponent(this.sortType.name)}`)
|
||||
if (this.maxResults !== 20)
|
||||
queryItems.push(`m=${encodeURIComponent(this.maxResults)}`)
|
||||
|
||||
url = `${this.$route.path}`
|
||||
|
||||
if (queryItems.length > 0) {
|
||||
url += `?${queryItems[0]}`
|
||||
|
||||
for (let i = 1; i < queryItems.length; i++) {
|
||||
url += `&${queryItems[i]}`
|
||||
}
|
||||
}
|
||||
url = this.getSearchUrl(offset)
|
||||
|
||||
await this.$router.replace({ path: url })
|
||||
}
|
||||
@ -774,18 +752,91 @@ export default {
|
||||
console.error(err)
|
||||
}
|
||||
},
|
||||
getSearchUrl(offset) {
|
||||
const queryItems = []
|
||||
|
||||
if (this.query) queryItems.push(`q=${encodeURIComponent(this.query)}`)
|
||||
if (offset > 0) queryItems.push(`o=${offset}`)
|
||||
if (this.facets.length > 0)
|
||||
queryItems.push(`f=${encodeURIComponent(this.facets)}`)
|
||||
if (this.orFacets.length > 0)
|
||||
queryItems.push(`g=${encodeURIComponent(this.orFacets)}`)
|
||||
if (this.selectedVersions.length > 0)
|
||||
queryItems.push(`v=${encodeURIComponent(this.selectedVersions)}`)
|
||||
if (this.selectedLicenses.length > 0)
|
||||
queryItems.push(`l=${encodeURIComponent(this.selectedLicenses)}`)
|
||||
if (this.showSnapshots) queryItems.push(`h=true`)
|
||||
if (this.selectedEnvironments.length > 0)
|
||||
queryItems.push(`e=${encodeURIComponent(this.selectedEnvironments)}`)
|
||||
if (this.sortType.name !== 'relevance')
|
||||
queryItems.push(`s=${encodeURIComponent(this.sortType.name)}`)
|
||||
if (this.maxResults !== 20)
|
||||
queryItems.push(`m=${encodeURIComponent(this.maxResults)}`)
|
||||
|
||||
let url = `${this.$route.path}`
|
||||
|
||||
if (queryItems.length > 0) {
|
||||
url += `?${queryItems[0]}`
|
||||
|
||||
for (let i = 1; i < queryItems.length; i++) {
|
||||
url += `&${queryItems[i]}`
|
||||
}
|
||||
}
|
||||
|
||||
return url
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// Mobile-first CSS: search page is grid on mobile...
|
||||
.search-page {
|
||||
display: grid;
|
||||
grid-template-rows: auto;
|
||||
grid-template-columns: 100%;
|
||||
|
||||
// ...and flex on desktop
|
||||
@media screen and (min-width: 1024px) {
|
||||
display: flex;
|
||||
|
||||
// Note that the actual flex layout properties come from .normal-page
|
||||
}
|
||||
}
|
||||
|
||||
.normal-page__content {
|
||||
// Passthrough children as grid items on mobile
|
||||
display: contents;
|
||||
|
||||
@media screen and (min-width: 1024px) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
// Move the filters "sidebar" on mobile underneath the search card
|
||||
.normal-page__sidebar {
|
||||
grid-row: 3;
|
||||
|
||||
// Hide on mobile unless open
|
||||
@media screen and (max-width: 1024px) {
|
||||
display: none;
|
||||
|
||||
&.open {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filters-card {
|
||||
padding: var(--spacing-card-lg);
|
||||
padding: var(--spacing-card-md);
|
||||
|
||||
@media screen and (min-width: 1024px) {
|
||||
padding: var(--spacing-card-lg);
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-menu {
|
||||
display: none;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.sidebar-menu_open {
|
||||
@ -796,11 +847,47 @@ export default {
|
||||
margin: 1.5rem 0 0.5rem 0;
|
||||
}
|
||||
|
||||
// EthicalAds
|
||||
.content-wrapper {
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.search-controls {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--spacing-card-md);
|
||||
flex-wrap: wrap;
|
||||
padding: var(--spacing-card-md);
|
||||
grid-row: 2;
|
||||
|
||||
.search-filter-container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
|
||||
.sidebar-menu-close-button {
|
||||
max-height: none;
|
||||
// match height of the search field
|
||||
height: 40px;
|
||||
transition: box-shadow 0.1s ease-in-out;
|
||||
margin-right: var(--spacing-card-md);
|
||||
|
||||
&.open {
|
||||
color: var(--color-button-text-active);
|
||||
background-color: var(--color-brand-highlight);
|
||||
box-shadow: inset 0 0 0 transparent, 0 0 0 2px var(--color-brand);
|
||||
}
|
||||
}
|
||||
|
||||
.iconified-input {
|
||||
flex: 1;
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sort-controls {
|
||||
width: 100%;
|
||||
@ -822,15 +909,6 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.iconified-input {
|
||||
flex: 1;
|
||||
|
||||
input {
|
||||
min-width: 15rem;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-controls__sorting {
|
||||
@ -842,11 +920,23 @@ export default {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.pagination-before {
|
||||
grid-row: 4;
|
||||
}
|
||||
|
||||
.search-results-container {
|
||||
grid-row: 5;
|
||||
}
|
||||
|
||||
.pagination-after {
|
||||
grid-row: 6;
|
||||
}
|
||||
|
||||
.no-results {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 700px) {
|
||||
@media screen and (min-width: 750px) {
|
||||
.search-controls {
|
||||
flex-wrap: nowrap;
|
||||
flex-direction: row;
|
||||
|
||||
@ -5,26 +5,6 @@
|
||||
<script>
|
||||
export default {
|
||||
name: 'Modpacks',
|
||||
head: {
|
||||
title: 'Modpacks - Modrinth',
|
||||
meta: [
|
||||
{
|
||||
hid: 'apple-mobile-web-app-title',
|
||||
name: 'apple-mobile-web-app-title',
|
||||
content: 'Modpacks',
|
||||
},
|
||||
{
|
||||
hid: 'og:title',
|
||||
name: 'og:title',
|
||||
content: 'Modpacks',
|
||||
},
|
||||
{
|
||||
hid: 'og:url',
|
||||
name: 'og:url',
|
||||
content: `https://modrinth.com/modpacks`,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@ -5,26 +5,6 @@
|
||||
<script>
|
||||
export default {
|
||||
name: 'Mods',
|
||||
head: {
|
||||
title: 'Mods - Modrinth',
|
||||
meta: [
|
||||
{
|
||||
hid: 'apple-mobile-web-app-title',
|
||||
name: 'apple-mobile-web-app-title',
|
||||
content: 'Mods',
|
||||
},
|
||||
{
|
||||
hid: 'og:title',
|
||||
name: 'og:title',
|
||||
content: 'Mods',
|
||||
},
|
||||
{
|
||||
hid: 'og:url',
|
||||
name: 'og:url',
|
||||
content: `https://modrinth.com/mods`,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@ -5,26 +5,6 @@
|
||||
<script>
|
||||
export default {
|
||||
name: 'Plugins',
|
||||
head: {
|
||||
title: 'Plugins - Modrinth',
|
||||
meta: [
|
||||
{
|
||||
hid: 'apple-mobile-web-app-title',
|
||||
name: 'apple-mobile-web-app-title',
|
||||
content: 'Plugins - Modrinth',
|
||||
},
|
||||
{
|
||||
hid: 'og:title',
|
||||
name: 'og:title',
|
||||
content: 'Plugins - Modrinth',
|
||||
},
|
||||
{
|
||||
hid: 'og:url',
|
||||
name: 'og:url',
|
||||
content: `https://modrinth.com/plugins`,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@ -5,26 +5,6 @@
|
||||
<script>
|
||||
export default {
|
||||
name: 'ResourcePacks',
|
||||
head: {
|
||||
title: 'Resource packs - Modrinth',
|
||||
meta: [
|
||||
{
|
||||
hid: 'apple-mobile-web-app-title',
|
||||
name: 'apple-mobile-web-app-title',
|
||||
content: 'Resource packs - Modrinth',
|
||||
},
|
||||
{
|
||||
hid: 'og:title',
|
||||
name: 'og:title',
|
||||
content: 'Resource packs - Modrinth',
|
||||
},
|
||||
{
|
||||
hid: 'og:url',
|
||||
name: 'og:url',
|
||||
content: `https://modrinth.com/resourcepacks`,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@ -1,46 +1,57 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="page-contents">
|
||||
<div class="content">
|
||||
<h1 v-if="$auth.user">Settings for {{ $auth.user.username }}</h1>
|
||||
<h1 v-else>Settings</h1>
|
||||
<div class="card styled-tabs">
|
||||
<nuxt-link v-if="$auth.user" class="tab" to="/settings" exact
|
||||
><span>General</span></nuxt-link
|
||||
>
|
||||
<nuxt-link v-if="$auth.user" class="tab" to="/settings/follows">
|
||||
<span>Followed projects</span>
|
||||
</nuxt-link>
|
||||
<nuxt-link v-if="$auth.user" class="tab" to="/settings/security">
|
||||
<span>Security</span>
|
||||
</nuxt-link>
|
||||
</div>
|
||||
<NuxtChild />
|
||||
</div>
|
||||
<div class="normal-page">
|
||||
<div class="normal-page__sidebar">
|
||||
<aside class="universal-card">
|
||||
<h1>Settings</h1>
|
||||
<NavStack>
|
||||
<NavStackItem link="/settings" label="Appearance">
|
||||
<PaintbrushIcon />
|
||||
</NavStackItem>
|
||||
<template v-if="$auth.user">
|
||||
<h3>User settings</h3>
|
||||
<NavStackItem link="/settings/account" label="Account">
|
||||
<UserIcon />
|
||||
</NavStackItem>
|
||||
<NavStackItem link="/settings/follows" label="Followed projects">
|
||||
<HeartIcon />
|
||||
</NavStackItem>
|
||||
<NavStackItem
|
||||
link="/settings/monetization"
|
||||
label="Monetization"
|
||||
beta
|
||||
>
|
||||
<CurrencyIcon />
|
||||
</NavStackItem>
|
||||
</template>
|
||||
</NavStack>
|
||||
</aside>
|
||||
</div>
|
||||
<div class="normal-page__content">
|
||||
<NuxtChild />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import NavStack from '~/components/ui/NavStack'
|
||||
import NavStackItem from '~/components/ui/NavStackItem'
|
||||
|
||||
import PaintbrushIcon from '~/assets/images/utils/paintbrush.svg?inline'
|
||||
import UserIcon from '~/assets/images/utils/user.svg?inline'
|
||||
import HeartIcon from '~/assets/images/utils/heart.svg?inline'
|
||||
import CurrencyIcon from '~/assets/images/utils/currency.svg?inline'
|
||||
|
||||
export default {
|
||||
name: 'Settings',
|
||||
components: {
|
||||
NavStack,
|
||||
NavStackItem,
|
||||
PaintbrushIcon,
|
||||
UserIcon,
|
||||
HeartIcon,
|
||||
CurrencyIcon,
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-contents {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
h1 {
|
||||
color: var(--color-text-dark);
|
||||
margin: 0 0 0.5rem 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1024px) {
|
||||
.page-contents {
|
||||
max-width: 60rem !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
274
pages/settings/account.vue
Normal file
@ -0,0 +1,274 @@
|
||||
<template>
|
||||
<div>
|
||||
<ModalConfirm
|
||||
ref="modal_confirm"
|
||||
title="Are you sure you want to delete your account?"
|
||||
description="This will **immediately delete all of your user data and follows**. This will not delete your projects. Deleting your account cannot be reversed.<br><br>If you need help with your account, get support on the [Modrinth Discord](https://discord.gg/EUHuJHt)."
|
||||
proceed-label="Delete my account"
|
||||
:confirmation-text="$auth.user.username"
|
||||
:has-to-type="true"
|
||||
@proceed="deleteAccount"
|
||||
/>
|
||||
|
||||
<Modal ref="modal_revoke_token" header="Revoke your Modrinth token">
|
||||
<div class="modal-revoke-token markdown-body">
|
||||
<p>
|
||||
Revoking your Modrinth token can have unintended consequences. Please
|
||||
be aware that the following could break:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Any application that uses your token to access the API.</li>
|
||||
<li>
|
||||
Gradle - if Minotaur is given a incorrect token, your Gradle builds
|
||||
could fail.
|
||||
</li>
|
||||
<li>
|
||||
GitHub - if you use a GitHub action that uses the Modrinth API, it
|
||||
will cause errors.
|
||||
</li>
|
||||
</ul>
|
||||
<p>If you are willing to continue, complete the following steps:</p>
|
||||
<ol>
|
||||
<li>
|
||||
<a
|
||||
href="https://github.com/settings/connections/applications/3acffb2e808d16d4b226"
|
||||
target="_blank"
|
||||
>
|
||||
Head to the Modrinth Application page on GitHub.
|
||||
</a>
|
||||
Make sure to be logged into the GitHub account you used for
|
||||
Modrinth!
|
||||
</li>
|
||||
<li>
|
||||
Press the big red "Revoke Access" button next to the "Permissions"
|
||||
header.
|
||||
</li>
|
||||
</ol>
|
||||
<p>
|
||||
Once you have completed those steps, press the continue button below.
|
||||
</p>
|
||||
<p>
|
||||
<strong>
|
||||
This will log you out of Modrinth, however, when you log back in,
|
||||
your token will be regenerated.
|
||||
</strong>
|
||||
</p>
|
||||
<div class="button-group">
|
||||
<button
|
||||
class="iconified-button"
|
||||
@click="$refs.modal_revoke_token.hide()"
|
||||
>
|
||||
<CrossIcon />
|
||||
Cancel
|
||||
</button>
|
||||
<button class="iconified-button brand-button" @click="logout">
|
||||
<RightArrowIcon />
|
||||
Log out
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<section class="universal-card">
|
||||
<h2>User profile</h2>
|
||||
<p>Visit your user profile to edit your profile information.</p>
|
||||
<NuxtLink class="iconified-button" :to="`/user/${$auth.user.username}`">
|
||||
<UserIcon /> Visit your profile
|
||||
</NuxtLink>
|
||||
</section>
|
||||
|
||||
<section class="universal-card">
|
||||
<h2>Account information</h2>
|
||||
<p>Your account information is not displayed publicly.</p>
|
||||
<ul class="known-errors">
|
||||
<li v-if="hasMonetizationEnabled() && !email">
|
||||
You must have an email address set since you are enrolled in the
|
||||
Creator Monetization Program.
|
||||
</li>
|
||||
</ul>
|
||||
<label for="email-input"
|
||||
><span class="label__title">Email address</span>
|
||||
</label>
|
||||
<input
|
||||
id="email-input"
|
||||
v-model="email"
|
||||
maxlength="2048"
|
||||
type="email"
|
||||
:placeholder="`Enter your email address...`"
|
||||
/>
|
||||
<div class="button-group">
|
||||
<button
|
||||
type="button"
|
||||
class="iconified-button brand-button"
|
||||
:disabled="hasMonetizationEnabled() && !email"
|
||||
@click="saveChanges()"
|
||||
>
|
||||
<SaveIcon />
|
||||
Save changes
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="universal-card">
|
||||
<h2>Authorization token</h2>
|
||||
<p>
|
||||
Your authorization token can be used with the Modrinth API, the Minotaur
|
||||
Gradle plugin, and other applications that interact with Modrinth's API.
|
||||
Be sure to keep this secret!
|
||||
</p>
|
||||
<div class="input-group">
|
||||
<button
|
||||
type="button"
|
||||
class="iconified-button"
|
||||
value="Copy to clipboard"
|
||||
@click="copyToken"
|
||||
>
|
||||
<template v-if="copied">
|
||||
<CheckIcon />
|
||||
Copied token to clipboard
|
||||
</template>
|
||||
<template v-else><CopyIcon />Copy token to clipboard</template>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="iconified-button"
|
||||
@click="$refs.modal_revoke_token.show()"
|
||||
>
|
||||
<SlashIcon />
|
||||
Revoke token
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="delete-account" class="universal-card">
|
||||
<h2>Delete account</h2>
|
||||
<p>
|
||||
Once you delete your account, there is no going back. Deleting your
|
||||
account will remove all attached data, excluding projects, from our
|
||||
servers.
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
class="iconified-button danger-button"
|
||||
@click="$refs.modal_confirm.show()"
|
||||
>
|
||||
<TrashIcon />
|
||||
Delete account
|
||||
</button>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ModalConfirm from '~/components/ui/ModalConfirm'
|
||||
import Modal from '~/components/ui/Modal'
|
||||
|
||||
import CrossIcon from '~/assets/images/utils/x.svg?inline'
|
||||
import RightArrowIcon from '~/assets/images/utils/right-arrow.svg?inline'
|
||||
import CheckIcon from '~/assets/images/utils/check.svg?inline'
|
||||
import UserIcon from '~/assets/images/utils/user.svg?inline'
|
||||
import SaveIcon from '~/assets/images/utils/save.svg?inline'
|
||||
import CopyIcon from '~/assets/images/utils/clipboard-copy.svg?inline'
|
||||
import TrashIcon from '~/assets/images/utils/trash.svg?inline'
|
||||
import SlashIcon from '~/assets/images/utils/slash.svg?inline'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Modal,
|
||||
ModalConfirm,
|
||||
CrossIcon,
|
||||
RightArrowIcon,
|
||||
CheckIcon,
|
||||
SaveIcon,
|
||||
UserIcon,
|
||||
CopyIcon,
|
||||
TrashIcon,
|
||||
SlashIcon,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
copied: false,
|
||||
email: this.$auth.user.email,
|
||||
showKnownErrors: false,
|
||||
}
|
||||
},
|
||||
head: {
|
||||
title: 'Account settings - Modrinth',
|
||||
},
|
||||
methods: {
|
||||
async copyToken() {
|
||||
this.copied = true
|
||||
await navigator.clipboard.writeText(this.$auth.token)
|
||||
},
|
||||
async deleteAccount() {
|
||||
this.$nuxt.$loading.start()
|
||||
try {
|
||||
await this.$axios.delete(
|
||||
`user/${this.$auth.user.id}`,
|
||||
this.$defaultHeaders()
|
||||
)
|
||||
} catch (err) {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.response.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
this.$nuxt.$loading.finish()
|
||||
},
|
||||
logout() {
|
||||
this.$refs.modal_revoke_token.hide()
|
||||
this.$cookies.set('auth-token-reset', true)
|
||||
|
||||
window.location.href = `${this.$axios.defaults.baseURL}auth/init?url=${process.env.domain}`
|
||||
},
|
||||
hasMonetizationEnabled() {
|
||||
return (
|
||||
this.$auth.user.payout_data.payout_wallet &&
|
||||
this.$auth.user.payout_data.payout_wallet_type &&
|
||||
this.$auth.user.payout_data.payout_address
|
||||
)
|
||||
},
|
||||
async saveChanges() {
|
||||
if (this.hasMonetizationEnabled() && !this.email) {
|
||||
this.showKnownErrors = true
|
||||
return
|
||||
}
|
||||
this.$nuxt.$loading.start()
|
||||
try {
|
||||
const data = {
|
||||
email: this.email ? this.email : null,
|
||||
}
|
||||
|
||||
await this.$axios.patch(
|
||||
`user/${this.$auth.user.id}`,
|
||||
data,
|
||||
this.$defaultHeaders()
|
||||
)
|
||||
await this.$store.dispatch('auth/fetchUser', {
|
||||
token: this.$auth.token,
|
||||
})
|
||||
} catch (err) {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.response.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
this.$nuxt.$loading.finish()
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.modal-revoke-token {
|
||||
padding: var(--spacing-card-bg);
|
||||
|
||||
.button-group {
|
||||
width: fit-content;
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -28,7 +28,7 @@
|
||||
<FollowIllustration class="icon" />
|
||||
<br />
|
||||
<span class="text"
|
||||
>You don't have any followed mods. <br />
|
||||
>You don't have any followed projects. <br />
|
||||
Why don't you <nuxt-link class="link" to="/mods">search</nuxt-link> for
|
||||
new ones?</span
|
||||
>
|
||||
@ -51,7 +51,7 @@ export default {
|
||||
await this.$store.dispatch('user/fetchFollows')
|
||||
},
|
||||
head: {
|
||||
title: 'Followed Projects - Modrinth',
|
||||
title: 'Followed projects - Modrinth',
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -1,84 +1,16 @@
|
||||
<template>
|
||||
<div class="edit-page">
|
||||
<section class="card account-settings">
|
||||
<div class="header">
|
||||
<h2 class="title">Account settings</h2>
|
||||
<div class="controls">
|
||||
<button
|
||||
class="brand-button-colors iconified-button"
|
||||
title="Save account settings changes"
|
||||
@click="saveChanges()"
|
||||
<div>
|
||||
<section class="universal-card">
|
||||
<h2>Themes</h2>
|
||||
<div class="adjacent-input">
|
||||
<label for="theme-selector">
|
||||
<span class="label__title">Color theme</span>
|
||||
<span class="label__description"
|
||||
>Change the global site color theme.</span
|
||||
>
|
||||
<SaveIcon />
|
||||
Save changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="left-side">
|
||||
<h3>Profile picture</h3>
|
||||
<div class="profile-picture">
|
||||
<img :src="previewImage ? previewImage : $auth.user.avatar_url" />
|
||||
<div class="uploader">
|
||||
<SmartFileInput
|
||||
:show-icon="false"
|
||||
:max-size="2097152"
|
||||
accept="image/png,image/jpeg,image/gif,image/webp"
|
||||
class="choose-image"
|
||||
prompt="Choose image or drag it here"
|
||||
@change="showPreviewImage"
|
||||
/>
|
||||
<button
|
||||
class="iconified-button"
|
||||
@click="
|
||||
icon = null
|
||||
previewImage = null
|
||||
"
|
||||
>
|
||||
<TrashIcon />
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-side">
|
||||
<label>
|
||||
<span>
|
||||
<h3>Username</h3>
|
||||
<span>This must be unique.</span>
|
||||
</span>
|
||||
|
||||
<input
|
||||
v-model="username"
|
||||
type="text"
|
||||
placeholder="Enter your username"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>
|
||||
<h3>Email (optional)</h3>
|
||||
<span>This is kept private.</span>
|
||||
</span>
|
||||
<input v-model="email" type="email" placeholder="Enter your email" />
|
||||
</label>
|
||||
<label>
|
||||
<span>
|
||||
<h3>Bio</h3>
|
||||
<span>Describe yourself to other users!</span>
|
||||
</span>
|
||||
<input v-model="bio" type="text" placeholder="Enter your bio" />
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
<section class="card">
|
||||
<div class="header">
|
||||
<h2 class="title">Display settings</h2>
|
||||
</div>
|
||||
<label>
|
||||
<span>
|
||||
<h3>Theme</h3>
|
||||
<span>Change the global site theme.</span>
|
||||
</span>
|
||||
<Multiselect
|
||||
id="theme-selector"
|
||||
v-model="$colorMode.preference"
|
||||
:options="['system', 'light', 'dark', 'oled']"
|
||||
:custom-label="
|
||||
@ -92,78 +24,118 @@
|
||||
:show-labels="false"
|
||||
:allow-empty="false"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>
|
||||
<h3>Search sidebar on the right</h3>
|
||||
<span>
|
||||
Enabling this will put the search page's filters sidebar on the
|
||||
right side.
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="adjacent-input small">
|
||||
<label for="search-layout-toggle">
|
||||
<span class="label__title">Search sidebar on the right</span>
|
||||
<span class="label__description"
|
||||
>Enabling this will put the search page's filters sidebar on the
|
||||
right side.</span
|
||||
>
|
||||
</label>
|
||||
<input
|
||||
id="search-layout-toggle"
|
||||
v-model="searchLayout"
|
||||
class="switch stylized-toggle"
|
||||
type="checkbox"
|
||||
@change="changeLayout"
|
||||
@change="saveCosmeticSettings"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>
|
||||
<h3>Project sidebar on the right</h3>
|
||||
|
||||
<span>
|
||||
Enabling this will put the project pages' info sidebars on the right
|
||||
side.
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="adjacent-input small">
|
||||
<label for="project-layout-toggle">
|
||||
<span class="label__title">Project sidebar on the right</span>
|
||||
<span class="label__description"
|
||||
>Enabling this will put the project pages' info sidebars on the
|
||||
right side.</span
|
||||
>
|
||||
</label>
|
||||
<input
|
||||
id="project-layout-toggle"
|
||||
v-model="projectLayout"
|
||||
class="switch stylized-toggle"
|
||||
type="checkbox"
|
||||
@change="changeLayout"
|
||||
@change="saveCosmeticSettings"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
<section class="universal-card">
|
||||
<h2>Feature flags</h2>
|
||||
<div class="adjacent-input small">
|
||||
<label for="advanced-rendering">
|
||||
<span class="label__title">Advanced rendering</span>
|
||||
<span class="label__description"
|
||||
>Enables advanced rendering such as blur effects that may cause
|
||||
performance issues without hardware-accelerated rendering.</span
|
||||
>
|
||||
</label>
|
||||
<input
|
||||
id="advanced-rendering"
|
||||
v-model="advancedRendering"
|
||||
class="switch stylized-toggle"
|
||||
type="checkbox"
|
||||
@change="saveCosmeticSettings"
|
||||
/>
|
||||
</div>
|
||||
<div class="adjacent-input small">
|
||||
<label for="modpacks-alpha-notice">
|
||||
<span class="label__title">Modpacks alpha notice</span>
|
||||
<span class="label__description"
|
||||
>Shows a banner stating that modpacks are in alpha.</span
|
||||
>
|
||||
</label>
|
||||
<input
|
||||
id="modpacks-alpha-notice"
|
||||
v-model="modpacksAlphaNotice"
|
||||
class="switch stylized-toggle"
|
||||
type="checkbox"
|
||||
@change="saveCosmeticSettings"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Multiselect from 'vue-multiselect'
|
||||
import SmartFileInput from '~/components/ui/SmartFileInput'
|
||||
import TrashIcon from '~/assets/images/utils/trash.svg?inline'
|
||||
import SaveIcon from '~/assets/images/utils/save.svg?inline'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TrashIcon,
|
||||
SaveIcon,
|
||||
SmartFileInput,
|
||||
Multiselect,
|
||||
},
|
||||
asyncData(ctx) {
|
||||
return {
|
||||
username: ctx.$auth.user.username,
|
||||
email: ctx.$auth.user.email,
|
||||
bio: ctx.$auth.user.bio,
|
||||
}
|
||||
},
|
||||
auth: false,
|
||||
data() {
|
||||
return {
|
||||
icon: null,
|
||||
previewImage: null,
|
||||
searchLayout: false,
|
||||
projectLayout: false,
|
||||
modpacksAlphaNotice: true,
|
||||
advancedRendering: true,
|
||||
}
|
||||
},
|
||||
fetch() {
|
||||
this.searchLayout = this.$store.state.cosmetics.searchLayout
|
||||
this.projectLayout = this.$store.state.cosmetics.projectLayout
|
||||
this.searchLayout =
|
||||
this.$store.state.cosmetics.searchLayout ?? this.searchLayout
|
||||
this.projectLayout =
|
||||
this.$store.state.cosmetics.projectLayout ?? this.projectLayout
|
||||
this.modpacksAlphaNotice =
|
||||
this.$store.state.cosmetics.modpacksAlphaNotice ??
|
||||
this.modpacksAlphaNotice
|
||||
this.advancedRendering =
|
||||
this.$store.state.cosmetics.advancedRendering ?? this.advancedRendering
|
||||
},
|
||||
head: {
|
||||
title: 'Settings - Modrinth',
|
||||
title: 'Display settings - Modrinth',
|
||||
},
|
||||
methods: {
|
||||
async saveCosmeticSettings() {
|
||||
await this.$store.dispatch('cosmetics/save', {
|
||||
searchLayout: this.searchLayout,
|
||||
projectLayout: this.projectLayout,
|
||||
modpacksAlphaNotice: this.modpacksAlphaNotice,
|
||||
advancedRendering: this.advancedRendering,
|
||||
$cookies: this.$cookies,
|
||||
})
|
||||
},
|
||||
changeTheme() {
|
||||
const shift = event.shiftKey
|
||||
switch (this.$colorMode.preference) {
|
||||
@ -177,141 +149,7 @@ export default {
|
||||
this.$colorMode.preference = shift ? 'oled' : 'dark'
|
||||
}
|
||||
},
|
||||
showPreviewImage(files) {
|
||||
const reader = new FileReader()
|
||||
this.icon = files[0]
|
||||
reader.readAsDataURL(this.icon)
|
||||
|
||||
reader.onload = (event) => {
|
||||
this.previewImage = event.target.result
|
||||
}
|
||||
},
|
||||
sumDownloads() {
|
||||
let sum = 0
|
||||
|
||||
for (const projects of this.$user.projects) {
|
||||
sum += projects.downloads
|
||||
}
|
||||
|
||||
return this.$formatNumber(sum)
|
||||
},
|
||||
async changeLayout() {
|
||||
await this.$store.dispatch('cosmetics/save', {
|
||||
searchLayout: this.searchLayout,
|
||||
projectLayout: this.projectLayout,
|
||||
$cookies: this.$cookies,
|
||||
})
|
||||
},
|
||||
async saveChanges() {
|
||||
this.$nuxt.$loading.start()
|
||||
try {
|
||||
if (this.icon) {
|
||||
await this.$axios.patch(
|
||||
`user/${this.$auth.user.id}/icon?ext=${
|
||||
this.icon.type.split('/')[this.icon.type.split('/').length - 1]
|
||||
}`,
|
||||
this.icon,
|
||||
this.$defaultHeaders()
|
||||
)
|
||||
}
|
||||
|
||||
const data = {
|
||||
email: this.email,
|
||||
bio: this.bio,
|
||||
}
|
||||
|
||||
if (this.username !== this.$auth.user.username) {
|
||||
data.username = this.username
|
||||
}
|
||||
await this.$axios.patch(
|
||||
`user/${this.$auth.user.id}`,
|
||||
data,
|
||||
this.$defaultHeaders()
|
||||
)
|
||||
|
||||
await this.$store.dispatch('auth/fetchUser', {
|
||||
token: this.$auth.token,
|
||||
})
|
||||
} catch (err) {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.response.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
this.$nuxt.$loading.finish()
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.account-settings {
|
||||
display: grid;
|
||||
grid-template: 'header header' auto 'left-side left-side' auto 'right-side right-side' auto;
|
||||
|
||||
@media screen and (min-width: 1024px) {
|
||||
grid-template:
|
||||
'header header' auto
|
||||
'left-side right-side' auto;
|
||||
}
|
||||
|
||||
.left-side {
|
||||
grid-area: left-side;
|
||||
min-width: 20rem;
|
||||
|
||||
.profile-picture {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
|
||||
img {
|
||||
box-shadow: var(--shadow-card);
|
||||
border-radius: var(--size-rounded-md);
|
||||
width: 10rem;
|
||||
height: 10rem;
|
||||
object-fit: contain;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.uploader {
|
||||
text-align: center;
|
||||
.iconified-button {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.right-side {
|
||||
grid-area: right-side;
|
||||
margin-left: var(--spacing-card-lg);
|
||||
}
|
||||
}
|
||||
|
||||
.card span {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
label {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-bottom: 1rem;
|
||||
grid-area: header;
|
||||
|
||||
.title {
|
||||
flex-grow: 1;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
211
pages/settings/monetization.vue
Normal file
@ -0,0 +1,211 @@
|
||||
<template>
|
||||
<div>
|
||||
<section v-if="enrolled" class="universal-card">
|
||||
<h2>Revenue and metrics</h2>
|
||||
<p>View your revenue and metrics in the creator dashboard:</p>
|
||||
<NuxtLink class="iconified-button" to="/dashboard/revenue">
|
||||
<ChartIcon /> Visit creator dashboard
|
||||
</NuxtLink>
|
||||
</section>
|
||||
<section class="universal-card">
|
||||
<h2 class="title">Enrollment</h2>
|
||||
<template v-if="!enrolled && !$auth.user.email">
|
||||
<p v-if="!enrolled">
|
||||
You are not currently enrolled in Modrinth's Creator Monetization
|
||||
Program. In order to enroll, you must first add a valid email address
|
||||
to your account.
|
||||
</p>
|
||||
<NuxtLink class="iconified-button" to="/settings/account">
|
||||
<SettingsIcon /> Visit account settings
|
||||
</NuxtLink>
|
||||
</template>
|
||||
<template v-else-if="editing || !enrolled">
|
||||
<p v-if="!enrolled">
|
||||
You are not currently enrolled in Modrinth's Creator Monetization
|
||||
Program. Setup a method of receiving payments below to enable
|
||||
monetization.
|
||||
</p>
|
||||
<div class="enroll">
|
||||
<Chips
|
||||
v-model="selectedWallet"
|
||||
:starting-value="selectedWallet"
|
||||
:items="wallets"
|
||||
:format-label="$formatWallet"
|
||||
@input="onChangeWallet()"
|
||||
/>
|
||||
|
||||
<p>
|
||||
Enter the information for the
|
||||
{{ $formatWallet(selectedWallet) }} account you would like to
|
||||
receive your revenue from the Creator Monetization Program:
|
||||
</p>
|
||||
<div class="input-group">
|
||||
<Multiselect
|
||||
v-model="accountType"
|
||||
:options="getAccountTypes()"
|
||||
:custom-label="(value) => formatAccountType(value)"
|
||||
:searchable="false"
|
||||
:close-on-select="true"
|
||||
:show-labels="false"
|
||||
:allow-empty="false"
|
||||
/>
|
||||
|
||||
<label class="hidden" for="account-input"
|
||||
>{{ $formatWallet(selectedWallet) }}
|
||||
{{ formatAccountType(accountType).toLowerCase() }} input
|
||||
field</label
|
||||
>
|
||||
<input
|
||||
id="account-input"
|
||||
v-model="account"
|
||||
:placeholder="`Enter your ${$formatWallet(
|
||||
selectedWallet
|
||||
)} ${formatAccountType(accountType).toLowerCase()}...`"
|
||||
:type="accountType === 'email' ? 'email' : ''"
|
||||
/>
|
||||
<span v-if="accountType === 'phone'">
|
||||
Format: +18888888888 or +1-888-888-8888
|
||||
</span>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<button
|
||||
class="iconified-button brand-button"
|
||||
@click="updatePayoutData(false)"
|
||||
>
|
||||
<SaveIcon /> Save information
|
||||
</button>
|
||||
<button
|
||||
v-if="enrolled"
|
||||
class="iconified-button danger-button"
|
||||
@click="updatePayoutData(true)"
|
||||
>
|
||||
<TrashIcon /> Remove enrollment
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<p>
|
||||
You are currently enrolled in the Creator Monetization Program with a
|
||||
{{ $formatWallet(selectedWallet) }} account.
|
||||
</p>
|
||||
<button class="iconified-button brand-button" @click="editing = true">
|
||||
<EditIcon /> Edit information
|
||||
</button>
|
||||
</template>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Multiselect from 'vue-multiselect'
|
||||
import Chips from '~/components/ui/Chips'
|
||||
import SaveIcon from '~/assets/images/utils/save.svg?inline'
|
||||
import TrashIcon from '~/assets/images/utils/trash.svg?inline'
|
||||
import EditIcon from '~/assets/images/utils/edit.svg?inline'
|
||||
import ChartIcon from '~/assets/images/utils/chart.svg?inline'
|
||||
import SettingsIcon from '~/assets/images/utils/settings.svg?inline'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Multiselect,
|
||||
Chips,
|
||||
SaveIcon,
|
||||
TrashIcon,
|
||||
EditIcon,
|
||||
ChartIcon,
|
||||
SettingsIcon,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
editing: false,
|
||||
enrolled:
|
||||
this.$auth.user.payout_data.payout_wallet &&
|
||||
this.$auth.user.payout_data.payout_wallet_type &&
|
||||
this.$auth.user.payout_data.payout_address,
|
||||
wallets: ['paypal', 'venmo'],
|
||||
selectedWallet: this.$auth.user.payout_data.payout_wallet ?? 'paypal',
|
||||
accountType:
|
||||
this.$auth.user.payout_data.payout_wallet_type ??
|
||||
this.getAccountTypes()[0],
|
||||
account: this.$auth.user.payout_data.payout_address ?? '',
|
||||
}
|
||||
},
|
||||
head: {
|
||||
title: 'Monetization settings - Modrinth',
|
||||
},
|
||||
methods: {
|
||||
getAccountTypes() {
|
||||
const types = []
|
||||
if (this.selectedWallet === 'venmo') {
|
||||
types.push('user_handle')
|
||||
}
|
||||
types.push('email')
|
||||
types.push('phone')
|
||||
return types
|
||||
},
|
||||
formatAccountType(value) {
|
||||
switch (value) {
|
||||
case 'email':
|
||||
return 'Email address'
|
||||
case 'phone':
|
||||
return 'Phone number'
|
||||
case 'user_handle':
|
||||
return 'Username'
|
||||
default:
|
||||
return value.charAt(0).toUpperCase() + value.slice(1)
|
||||
}
|
||||
},
|
||||
onChangeWallet() {
|
||||
this.account = ''
|
||||
|
||||
// Set default account type for each wallet
|
||||
if (this.selectedWallet === 'paypal') {
|
||||
this.accountType = 'email'
|
||||
} else if (this.selectedWallet === 'venmo') {
|
||||
this.accountType = 'user_handle'
|
||||
}
|
||||
},
|
||||
async updatePayoutData(unenroll) {
|
||||
this.$nuxt.$loading.start()
|
||||
if (unenroll) {
|
||||
this.selectedWallet = 'paypal'
|
||||
this.accountType = this.getAccountTypes()[0]
|
||||
this.account = ''
|
||||
}
|
||||
try {
|
||||
const data = {
|
||||
payout_data: unenroll
|
||||
? null
|
||||
: {
|
||||
payout_wallet: this.selectedWallet,
|
||||
payout_wallet_type: this.accountType,
|
||||
payout_address: this.account,
|
||||
},
|
||||
}
|
||||
|
||||
await this.$axios.patch(
|
||||
`user/${this.$auth.user.id}`,
|
||||
data,
|
||||
this.$defaultHeaders()
|
||||
)
|
||||
await this.$store.dispatch('auth/fetchUser', {
|
||||
token: this.$auth.token,
|
||||
})
|
||||
|
||||
this.editing = false
|
||||
this.enrolled = !unenroll
|
||||
} catch (err) {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.response.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
this.$nuxt.$loading.finish()
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
@ -1,86 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<section class="card essentials pad-maker">
|
||||
<h3>Revoke your Modrinth token</h3>
|
||||
<p>
|
||||
Revoking your Modrinth token can have unintended consequences. Please be
|
||||
aware that the following could break:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Any application that uses your token to access the API.</li>
|
||||
<li>
|
||||
Gradle - if Minotaur is given a incorrect token, your Gradle builds
|
||||
could fail.
|
||||
</li>
|
||||
<li>
|
||||
GitHub - if you use a GitHub action that uses the Modrinth API, it
|
||||
will cause errors.
|
||||
</li>
|
||||
</ul>
|
||||
<p>If you are willing to continue, complete the following steps:</p>
|
||||
<ol>
|
||||
<li>
|
||||
<a
|
||||
href="https://github.com/settings/connections/applications/3acffb2e808d16d4b226"
|
||||
target="_blank"
|
||||
>
|
||||
Head to the Modrinth Application page on GitHub.
|
||||
</a>
|
||||
Make sure to be logged into the GitHub account you used for Modrinth!
|
||||
</li>
|
||||
<li>
|
||||
Press the big red "Revoke Access" button next to the "Permissions"
|
||||
header.
|
||||
</li>
|
||||
</ol>
|
||||
<p>
|
||||
Once you have completed those steps, press the continue button below.
|
||||
</p>
|
||||
<p>
|
||||
<strong>
|
||||
This will log you out of Modrinth, however, when you log back in, your
|
||||
token will be regenerated.
|
||||
</strong>
|
||||
</p>
|
||||
<button class="iconified-button brand-button-colors" @click="logout">
|
||||
<CheckIcon />
|
||||
Continue
|
||||
</button>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CheckIcon from '~/assets/images/utils/right-arrow.svg?inline'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CheckIcon,
|
||||
},
|
||||
head: {
|
||||
title: 'Revoke Token - Modrinth',
|
||||
},
|
||||
methods: {
|
||||
async logout() {
|
||||
this.$cookies.set('auth-token-reset', true)
|
||||
await this.$router.replace(
|
||||
`auth/init?url=${process.env.domain}${this.$route.fullPath}`
|
||||
)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
section {
|
||||
a {
|
||||
color: var(--color-link);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0.5rem 0.75rem;
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,130 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<ConfirmPopup
|
||||
ref="delete_popup"
|
||||
title="Are you sure you want to delete your account?"
|
||||
description="If you proceed, your user and all attached data will be removed from our
|
||||
servers. This cannot be reversed, so be careful!"
|
||||
proceed-label="Delete account"
|
||||
:confirmation-text="$auth.user.username"
|
||||
:has-to-type="true"
|
||||
@proceed="deleteAccount"
|
||||
/>
|
||||
|
||||
<section class="card">
|
||||
<div class="header">
|
||||
<h2 class="title">Security settings</h2>
|
||||
</div>
|
||||
<label>
|
||||
<span>
|
||||
<h3>Authorization token</h3>
|
||||
<span>
|
||||
Your authorization token can be used with the Modrinth API, the
|
||||
Minotaur Gradle plugin, and other applications that interact with
|
||||
Modrinth's API. Be sure to keep this secret!
|
||||
</span>
|
||||
</span>
|
||||
<input
|
||||
type="button"
|
||||
class="iconified-button"
|
||||
value="Copy to clipboard"
|
||||
@click="copyToken"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>
|
||||
<h3>Revoke your token</h3>
|
||||
<span
|
||||
>This will log you out of Modrinth, and you will have to log in
|
||||
again to access Modrinth with a new token.</span
|
||||
>
|
||||
</span>
|
||||
<input
|
||||
type="button"
|
||||
class="iconified-button"
|
||||
value="Revoke token"
|
||||
@click="$router.replace('/settings/revoke-token')"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>
|
||||
<h3>Delete your account</h3>
|
||||
<span
|
||||
>Clicking on this WILL delete your account. Do not click on this
|
||||
unless you want your account deleted. If you delete your account,
|
||||
all attached data, including projects, will be removed from our
|
||||
servers. This cannot be reversed, so be careful!</span
|
||||
>
|
||||
</span>
|
||||
<input
|
||||
value="Delete account"
|
||||
type="button"
|
||||
class="iconified-button"
|
||||
@click="$refs.delete_popup.show()"
|
||||
/>
|
||||
</label>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ConfirmPopup from '~/components/ui/ConfirmPopup'
|
||||
|
||||
export default {
|
||||
components: { ConfirmPopup },
|
||||
head: {
|
||||
title: 'Security - Modrinth',
|
||||
},
|
||||
methods: {
|
||||
async copyToken() {
|
||||
await navigator.clipboard.writeText(this.$auth.token)
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'Copied to clipboard.',
|
||||
text: 'Copied your Modrinth token to the clipboard.',
|
||||
type: 'success',
|
||||
})
|
||||
},
|
||||
async deleteAccount() {
|
||||
this.$nuxt.$loading.start()
|
||||
try {
|
||||
await this.$axios.delete(
|
||||
`user/${this.$auth.user.id}`,
|
||||
this.$defaultHeaders()
|
||||
)
|
||||
} catch (err) {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.response.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
this.$nuxt.$loading.finish()
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.card span {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-bottom: 1rem;
|
||||
grid-area: header;
|
||||
|
||||
.title {
|
||||
flex-grow: 1;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,130 +1,232 @@
|
||||
<template>
|
||||
<div class="normal-page">
|
||||
<div class="normal-page__sidebar">
|
||||
<aside class="card sidebar">
|
||||
<img
|
||||
class="sidebar__item profile-picture"
|
||||
:src="user.avatar_url"
|
||||
<div>
|
||||
<ModalCreation ref="modal_creation" />
|
||||
<ModalReport ref="modal_report" :item-id="user.id" item-type="user" />
|
||||
<div class="user-header-wrapper">
|
||||
<div class="user-header">
|
||||
<Avatar
|
||||
:src="previewImage ? previewImage : user.avatar_url"
|
||||
size="md"
|
||||
circle
|
||||
:alt="user.username"
|
||||
/>
|
||||
<h1 class="sidebar__item username">{{ user.username }}</h1>
|
||||
<div class="sidebar__item">
|
||||
<Badge v-if="user.role === 'admin'" type="admin" color="red" />
|
||||
<Badge
|
||||
v-else-if="user.role === 'moderator'"
|
||||
type="moderator"
|
||||
color="yellow"
|
||||
/>
|
||||
<Badge v-else type="developer" color="green" />
|
||||
</div>
|
||||
<hr class="card-divider" />
|
||||
<h3 class="sidebar__item">About me</h3>
|
||||
<span v-if="user.bio" class="sidebar__item bio">{{ user.bio }}</span>
|
||||
<a
|
||||
:href="githubUrl"
|
||||
target="_blank"
|
||||
class="sidebar__item report-button iconified-button"
|
||||
>
|
||||
<GitHubIcon aria-hidden="true" />
|
||||
View GitHub profile
|
||||
</a>
|
||||
<div class="sidebar__item stats-block">
|
||||
<div class="stats-block__item secondary-stat">
|
||||
<SunriseIcon class="secondary-stat__icon" aria-hidden="true" />
|
||||
<span
|
||||
v-tooltip="
|
||||
$dayjs(user.created).format('MMMM D, YYYY [at] h:mm:ss A')
|
||||
"
|
||||
class="secondary-stat__text date"
|
||||
>
|
||||
Joined {{ $dayjs(user.created).fromNow() }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="stats-block__item secondary-stat">
|
||||
<UserIcon class="secondary-stat__icon" aria-hidden="true" />
|
||||
<span class="secondary-stat__text">User ID: {{ user.id }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sidebar__item stats-block">
|
||||
<div class="stats-block__item primary-stat">
|
||||
<DownloadIcon class="primary-stat__icon" aria-hidden="true" />
|
||||
<div class="primary-stat__text">
|
||||
<span class="primary-stat__counter">{{ sumDownloads() }}</span>
|
||||
<span class="primary-stat__label">downloads</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template
|
||||
v-if="!$auth.user || ($auth.user && $auth.user.id !== user.id)"
|
||||
>
|
||||
<hr class="card-divider" />
|
||||
<nuxt-link
|
||||
:to="`/create/report?id=${user.id}&t=user`"
|
||||
class="sidebar__item report-button iconified-button"
|
||||
>
|
||||
<ReportIcon aria-hidden="true" />
|
||||
Report
|
||||
</nuxt-link>
|
||||
</template>
|
||||
</aside>
|
||||
</div>
|
||||
<div class="normal-page__content">
|
||||
<Advertisement />
|
||||
<nav class="card user-navigation">
|
||||
<ThisOrThat v-model="selectedProjectType" :items="projectTypes" />
|
||||
<nuxt-link
|
||||
v-if="$auth.user && $auth.user.id === user.id"
|
||||
to="/create/project"
|
||||
class="iconified-button brand-button-colors"
|
||||
>
|
||||
<PlusIcon />
|
||||
Create a project
|
||||
</nuxt-link>
|
||||
</nav>
|
||||
<div v-if="projects.length > 0">
|
||||
<ProjectCard
|
||||
v-for="project in selectedProjectType !== 'all'
|
||||
? projects.filter(
|
||||
(x) =>
|
||||
x.project_type === convertProjectType(selectedProjectType)
|
||||
)
|
||||
: projects"
|
||||
:id="project.slug || project.id"
|
||||
:key="project.id"
|
||||
:name="project.title"
|
||||
:description="project.description"
|
||||
:created-at="project.published"
|
||||
:updated-at="project.updated"
|
||||
:downloads="project.downloads.toString()"
|
||||
:follows="project.followers.toString()"
|
||||
:icon-url="project.icon_url"
|
||||
:categories="project.categories"
|
||||
:client-side="project.client_side"
|
||||
:server-side="project.server_side"
|
||||
:status="project.status"
|
||||
:type="project.project_type"
|
||||
>
|
||||
<nuxt-link
|
||||
v-if="$auth.user && $auth.user.id === user.id"
|
||||
class="iconified-button"
|
||||
:to="`/${project.project_type}/${
|
||||
project.slug ? project.slug : project.id
|
||||
}/settings`"
|
||||
>
|
||||
<SettingsIcon />
|
||||
Settings
|
||||
</nuxt-link>
|
||||
</ProjectCard>
|
||||
<h1 class="username">{{ user.username }}</h1>
|
||||
</div>
|
||||
<div v-else class="error">
|
||||
<UpToDate class="icon" /><br />
|
||||
<span v-if="$auth.user && $auth.user.id === user.id" class="text"
|
||||
>You don't have any projects.<br />
|
||||
Would you like to
|
||||
<nuxt-link class="link" to="/create/project">create one</nuxt-link
|
||||
>?</span
|
||||
>
|
||||
<span v-else class="text">This user has no projects!</span>
|
||||
</div>
|
||||
<div class="normal-page">
|
||||
<div class="normal-page__sidebar">
|
||||
<aside class="card sidebar">
|
||||
<h1 class="mobile-username">{{ user.username }}</h1>
|
||||
<div class="card__overlay">
|
||||
<FileInput
|
||||
v-if="isEditing"
|
||||
:max-size="262144"
|
||||
:show-icon="true"
|
||||
accept="image/png,image/jpeg,image/gif,image/webp"
|
||||
class="choose-image"
|
||||
prompt="Upload avatar"
|
||||
@change="showPreviewImage"
|
||||
/>
|
||||
<button
|
||||
v-else-if="$auth.user && $auth.user.id === user.id"
|
||||
class="iconified-button"
|
||||
@click="isEditing = true"
|
||||
>
|
||||
<EditIcon />
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
v-else-if="$auth.user"
|
||||
class="iconified-button"
|
||||
@click="$refs.modal_report.show()"
|
||||
>
|
||||
<ReportIcon aria-hidden="true" />
|
||||
Report
|
||||
</button>
|
||||
<a v-else class="iconified-button" :href="authUrl">
|
||||
<ReportIcon aria-hidden="true" />
|
||||
Report
|
||||
</a>
|
||||
</div>
|
||||
<template v-if="isEditing">
|
||||
<div class="inputs universal-labels">
|
||||
<label for="user-username"
|
||||
><span class="label__title">Username</span></label
|
||||
>
|
||||
<input
|
||||
id="user-username"
|
||||
v-model="user.username"
|
||||
maxlength="39"
|
||||
type="text"
|
||||
/>
|
||||
<label for="user-bio"
|
||||
><span class="label__title">Bio</span></label
|
||||
>
|
||||
<div class="textarea-wrapper">
|
||||
<textarea
|
||||
id="user-bio"
|
||||
v-model="user.bio"
|
||||
maxlength="160"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<button
|
||||
class="iconified-button"
|
||||
@click="
|
||||
isEditing = false
|
||||
user = JSON.parse(JSON.stringify($auth.user))
|
||||
previewImage = null
|
||||
icon = null
|
||||
"
|
||||
>
|
||||
<CrossIcon /> Cancel
|
||||
</button>
|
||||
<button
|
||||
class="iconified-button brand-button"
|
||||
@click="saveChanges"
|
||||
>
|
||||
<SaveIcon /> Save
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="sidebar__item">
|
||||
<Badge v-if="user.role === 'admin'" type="admin" color="red" />
|
||||
<Badge
|
||||
v-else-if="user.role === 'moderator'"
|
||||
type="moderator"
|
||||
color="yellow"
|
||||
/>
|
||||
<Badge v-else type="developer" color="green" />
|
||||
</div>
|
||||
<span v-if="user.bio" class="sidebar__item bio">{{
|
||||
user.bio
|
||||
}}</span>
|
||||
<hr class="card-divider" />
|
||||
<div class="primary-stat">
|
||||
<DownloadIcon class="primary-stat__icon" aria-hidden="true" />
|
||||
<div class="primary-stat__text">
|
||||
<span class="primary-stat__counter">{{ sumDownloads() }}</span>
|
||||
<span class="primary-stat__label">downloads</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="primary-stat">
|
||||
<HeartIcon class="primary-stat__icon" aria-hidden="true" />
|
||||
<div class="primary-stat__text">
|
||||
<span class="primary-stat__counter">{{ sumFollows() }}</span>
|
||||
<span class="primary-stat__label">followers of projects</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stats-block__item secondary-stat">
|
||||
<SunriseIcon class="secondary-stat__icon" aria-hidden="true" />
|
||||
<span
|
||||
v-tooltip="
|
||||
$dayjs(user.created).format('MMMM D, YYYY [at] h:mm:ss A')
|
||||
"
|
||||
class="secondary-stat__text date"
|
||||
>
|
||||
Joined {{ $dayjs(user.created).fromNow() }}
|
||||
</span>
|
||||
</div>
|
||||
<hr class="card-divider" />
|
||||
<div class="stats-block__item secondary-stat">
|
||||
<UserIcon class="secondary-stat__icon" aria-hidden="true" />
|
||||
<span class="secondary-stat__text">
|
||||
User ID: <CopyCode :text="user.id" />
|
||||
</span>
|
||||
</div>
|
||||
<a
|
||||
:href="githubUrl"
|
||||
target="_blank"
|
||||
class="sidebar__item github-button iconified-button"
|
||||
>
|
||||
<GitHubIcon aria-hidden="true" />
|
||||
View GitHub profile
|
||||
</a>
|
||||
</template>
|
||||
</aside>
|
||||
</div>
|
||||
<div class="normal-page__content">
|
||||
<Advertisement
|
||||
type="banner"
|
||||
small-screen="square"
|
||||
ethical-ads-small
|
||||
ethical-ads-big
|
||||
/>
|
||||
<nav class="card user-navigation">
|
||||
<NavRow
|
||||
query="type"
|
||||
:links="[
|
||||
{
|
||||
label: 'all',
|
||||
href: '',
|
||||
},
|
||||
...projectTypes.map((x) => {
|
||||
return {
|
||||
label: x === 'resourcepack' ? 'Resource Packs' : x + 's',
|
||||
href: x,
|
||||
}
|
||||
}),
|
||||
]"
|
||||
/>
|
||||
<button
|
||||
v-if="$auth.user && $auth.user.id === user.id"
|
||||
class="iconified-button brand-button"
|
||||
@click="$refs.modal_creation.show()"
|
||||
>
|
||||
<PlusIcon />
|
||||
Create a project
|
||||
</button>
|
||||
</nav>
|
||||
<div v-if="projects.length > 0">
|
||||
<ProjectCard
|
||||
v-for="project in $route.query.type !== undefined
|
||||
? projects.filter((x) => x.project_type === $route.query.type)
|
||||
: projects"
|
||||
:id="project.slug || project.id"
|
||||
:key="project.id"
|
||||
:name="project.title"
|
||||
:description="project.description"
|
||||
:created-at="project.published"
|
||||
:updated-at="project.updated"
|
||||
:downloads="project.downloads.toString()"
|
||||
:follows="project.followers.toString()"
|
||||
:icon-url="project.icon_url"
|
||||
:categories="project.categories"
|
||||
:client-side="project.client_side"
|
||||
:server-side="project.server_side"
|
||||
:status="
|
||||
$auth.user &&
|
||||
($auth.user.role === 'admin' || $auth.user.role === 'moderator')
|
||||
? project.status
|
||||
: null
|
||||
"
|
||||
:type="project.project_type"
|
||||
>
|
||||
<nuxt-link
|
||||
v-if="$auth.user && $auth.user.id === user.id"
|
||||
class="iconified-button"
|
||||
:to="`/${project.project_type}/${
|
||||
project.slug ? project.slug : project.id
|
||||
}/settings`"
|
||||
>
|
||||
<SettingsIcon />
|
||||
Settings
|
||||
</nuxt-link>
|
||||
</ProjectCard>
|
||||
</div>
|
||||
<div v-else class="error">
|
||||
<UpToDate class="icon" /><br />
|
||||
<span v-if="$auth.user && $auth.user.id === user.id" class="text">
|
||||
You don't have any projects.<br />
|
||||
Would you like to
|
||||
<a class="link" @click.prevent="$refs.modal_creation.show()">
|
||||
create one</a
|
||||
>?
|
||||
</span>
|
||||
<span v-else class="text">This user has no projects!</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -132,7 +234,6 @@
|
||||
|
||||
<script>
|
||||
import ProjectCard from '~/components/ui/ProjectCard'
|
||||
import ThisOrThat from '~/components/ui/ThisOrThat'
|
||||
import Badge from '~/components/ui/Badge'
|
||||
import Advertisement from '~/components/ads/Advertisement'
|
||||
|
||||
@ -144,10 +245,26 @@ import SettingsIcon from '~/assets/images/utils/settings.svg?inline'
|
||||
import PlusIcon from '~/assets/images/utils/plus.svg?inline'
|
||||
import UpToDate from '~/assets/images/illustrations/up_to_date.svg?inline'
|
||||
import UserIcon from '~/assets/images/utils/user.svg?inline'
|
||||
import EditIcon from '~/assets/images/utils/edit.svg?inline'
|
||||
import HeartIcon from '~/assets/images/utils/heart.svg?inline'
|
||||
import CrossIcon from '~/assets/images/utils/x.svg?inline'
|
||||
import SaveIcon from '~/assets/images/utils/save.svg?inline'
|
||||
import FileInput from '~/components/ui/FileInput'
|
||||
import ModalReport from '~/components/ui/ModalReport'
|
||||
import ModalCreation from '~/components/ui/ModalCreation'
|
||||
import NavRow from '~/components/ui/NavRow'
|
||||
import CopyCode from '~/components/ui/CopyCode'
|
||||
import Avatar from '~/components/ui/Avatar'
|
||||
|
||||
export default {
|
||||
auth: false,
|
||||
components: {
|
||||
Avatar,
|
||||
CopyCode,
|
||||
NavRow,
|
||||
ModalCreation,
|
||||
ModalReport,
|
||||
FileInput,
|
||||
ProjectCard,
|
||||
SunriseIcon,
|
||||
DownloadIcon,
|
||||
@ -156,10 +273,13 @@ export default {
|
||||
Badge,
|
||||
SettingsIcon,
|
||||
PlusIcon,
|
||||
ThisOrThat,
|
||||
UpToDate,
|
||||
UserIcon,
|
||||
EditIcon,
|
||||
Advertisement,
|
||||
HeartIcon,
|
||||
CrossIcon,
|
||||
SaveIcon,
|
||||
},
|
||||
async asyncData(data) {
|
||||
try {
|
||||
@ -173,6 +293,12 @@ export default {
|
||||
])
|
||||
).map((it) => it.data)
|
||||
|
||||
if (user.username !== data.params.id) {
|
||||
data.redirect(301, `/user/${user.username}`)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const [gitHubUser, versions] = (
|
||||
await Promise.all([
|
||||
data.$axios.get(`https://api.github.com/user/` + user.github_id),
|
||||
@ -213,7 +339,6 @@ export default {
|
||||
}
|
||||
|
||||
return {
|
||||
selectedProjectType: 'all',
|
||||
user,
|
||||
projects,
|
||||
githubUrl: gitHubUser.html_url,
|
||||
@ -225,6 +350,13 @@ export default {
|
||||
})
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isEditing: false,
|
||||
icon: null,
|
||||
previewImage: null,
|
||||
}
|
||||
},
|
||||
head() {
|
||||
return {
|
||||
title: this.user.username + ' - Modrinth',
|
||||
@ -247,19 +379,12 @@ export default {
|
||||
{
|
||||
hid: 'og:description',
|
||||
name: 'og:description',
|
||||
content: this.user.bio,
|
||||
content: `${this.user.bio} - Download ${this.user.username}'s projects on Modrinth`,
|
||||
},
|
||||
{
|
||||
hid: 'description',
|
||||
name: 'description',
|
||||
content:
|
||||
this.user.bio +
|
||||
' - View Minecraft mods on Modrinth today! Modrinth is a new and modern Minecraft modding platform.',
|
||||
},
|
||||
{
|
||||
hid: 'og:url',
|
||||
name: 'og:url',
|
||||
content: `https://modrinth.com/user/${this.user.id}`,
|
||||
content: `${this.user.bio} - Download ${this.user.username}'s projects on Modrinth`,
|
||||
},
|
||||
{
|
||||
hid: 'og:image',
|
||||
@ -271,28 +396,20 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
authUrl() {
|
||||
return `${process.env.authURLBase}auth/init?url=${process.env.domain}${this.$route.path}`
|
||||
},
|
||||
projectTypes() {
|
||||
const obj = { all: true }
|
||||
const obj = {}
|
||||
|
||||
for (const project of this.projects) {
|
||||
if (project.project_type === 'resourcepack') {
|
||||
obj['Resource Packs'] = true
|
||||
} else {
|
||||
obj[project.project_type + 's'] = true
|
||||
}
|
||||
obj[project.project_type] = true
|
||||
}
|
||||
|
||||
return Object.keys(obj)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
convertProjectType(name) {
|
||||
if (name === 'Resource Packs') {
|
||||
return 'resourcepack'
|
||||
} else {
|
||||
return name.slice(0, -1)
|
||||
}
|
||||
},
|
||||
sumDownloads() {
|
||||
let sum = 0
|
||||
|
||||
@ -302,11 +419,105 @@ export default {
|
||||
|
||||
return this.$formatNumber(sum)
|
||||
},
|
||||
sumFollows() {
|
||||
let sum = 0
|
||||
|
||||
for (const projects of this.projects) {
|
||||
sum += projects.followers
|
||||
}
|
||||
|
||||
return this.$formatNumber(sum)
|
||||
},
|
||||
showPreviewImage(files) {
|
||||
const reader = new FileReader()
|
||||
this.icon = files[0]
|
||||
reader.readAsDataURL(this.icon)
|
||||
reader.onload = (event) => {
|
||||
this.previewImage = event.target.result
|
||||
}
|
||||
},
|
||||
async saveChanges() {
|
||||
this.$nuxt.$loading.start()
|
||||
try {
|
||||
if (this.icon) {
|
||||
await this.$axios.patch(
|
||||
`user/${this.$auth.user.id}/icon?ext=${
|
||||
this.icon.type.split('/')[this.icon.type.split('/').length - 1]
|
||||
}`,
|
||||
this.icon,
|
||||
this.$defaultHeaders()
|
||||
)
|
||||
}
|
||||
|
||||
const data = {
|
||||
email: this.user.email,
|
||||
bio: this.user.bio,
|
||||
}
|
||||
if (this.user.username !== this.$auth.user.username) {
|
||||
data.username = this.user.username
|
||||
}
|
||||
|
||||
await this.$axios.patch(
|
||||
`user/${this.$auth.user.id}`,
|
||||
data,
|
||||
this.$defaultHeaders()
|
||||
)
|
||||
await this.$store.dispatch('auth/fetchUser', {
|
||||
token: this.$auth.token,
|
||||
})
|
||||
|
||||
this.isEditing = false
|
||||
} catch (err) {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.response.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
this.$nuxt.$loading.finish()
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
<style lang="scss" scoped>
|
||||
.user-header-wrapper {
|
||||
display: flex;
|
||||
margin: 0 auto -1.5rem;
|
||||
max-width: 80rem;
|
||||
|
||||
.user-header {
|
||||
position: relative;
|
||||
z-index: 4;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
padding: 0 1rem;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
|
||||
.username {
|
||||
display: none;
|
||||
font-size: 2rem;
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-username {
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 501px) {
|
||||
.mobile-username {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.user-header-wrapper .user-header .username {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.user-navigation {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
@ -314,6 +525,11 @@ export default {
|
||||
flex-wrap: wrap;
|
||||
row-gap: 0.5rem;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
padding-top: 2.5rem;
|
||||
}
|
||||
|
||||
.sidebar__item:not(:last-child) {
|
||||
margin: 0 0 0.75rem 0;
|
||||
}
|
||||
@ -328,22 +544,14 @@ export default {
|
||||
font-size: var(--font-size-xl);
|
||||
}
|
||||
|
||||
.report-button {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.bio {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.stats-block__item {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.secondary-stat {
|
||||
align-items: center;
|
||||
color: var(--color-text-secondary);
|
||||
display: flex;
|
||||
margin-bottom: 0.8rem;
|
||||
}
|
||||
|
||||
.secondary-stat__icon {
|
||||
@ -352,29 +560,58 @@ export default {
|
||||
}
|
||||
|
||||
.secondary-stat__text {
|
||||
margin-left: 0.25rem;
|
||||
margin-left: 0.4rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.primary-stat {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
margin-bottom: 0.6rem;
|
||||
}
|
||||
|
||||
.primary-stat__icon {
|
||||
height: 1.25rem;
|
||||
width: 1.25rem;
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
}
|
||||
|
||||
.primary-stat__text {
|
||||
margin-left: 0.25rem;
|
||||
margin-left: 0.4rem;
|
||||
}
|
||||
|
||||
.primary-stat__counter {
|
||||
font-size: var(--font-size-lg);
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.date {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.github-button {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.inputs {
|
||||
margin-bottom: 1rem;
|
||||
|
||||
input {
|
||||
margin-top: 0.5rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
label {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.button-group:first-child {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.textarea-wrapper {
|
||||
height: 10rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -19,7 +19,9 @@ export default (ctx, inject) => {
|
||||
inject('formatVersion', (versionsArray) =>
|
||||
formatVersions(versionsArray, ctx.store)
|
||||
)
|
||||
inject('orElse', (first, otherwise) => first ?? otherwise)
|
||||
inject('formatBytes', formatBytes)
|
||||
inject('formatWallet', formatWallet)
|
||||
inject('formatProjectType', formatProjectType)
|
||||
inject('formatCategory', formatCategory)
|
||||
inject('formatCategoryHeader', formatCategoryHeader)
|
||||
@ -192,9 +194,16 @@ export const formatBytes = (bytes, decimals = 2) => {
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]
|
||||
}
|
||||
|
||||
export const formatWallet = (name) => {
|
||||
if (name === 'paypal') {
|
||||
return 'PayPal'
|
||||
}
|
||||
return name.charAt(0).toUpperCase() + name.slice(1)
|
||||
}
|
||||
|
||||
export const formatProjectType = (name) => {
|
||||
if (name === 'resourcepack') {
|
||||
return 'resource pack'
|
||||
return 'Resource Pack'
|
||||
}
|
||||
return name.charAt(0).toUpperCase() + name.slice(1)
|
||||
}
|
||||
|
||||
@ -1 +0,0 @@
|
||||
google.com, pub-4615302805870170, DIRECT, f08c47fec0942fa0
|
||||
BIN
static/favicon-light.ico
Normal file
|
After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 24 KiB |
@ -30,6 +30,11 @@ export const actions = {
|
||||
})
|
||||
).data
|
||||
|
||||
if (user.payout_data && user.payout_data.balance) {
|
||||
user.payout_data.balance =
|
||||
Math.floor(user.payout_data.balance * 100) / 100
|
||||
}
|
||||
|
||||
commit('SET_USER', user)
|
||||
commit('SET_TOKEN', token)
|
||||
commit('SET_HEADERS', {
|
||||
|
||||
@ -9,6 +9,8 @@ const parameters = {
|
||||
export const state = () => ({
|
||||
searchLayout: false,
|
||||
projectLayout: false,
|
||||
modpacksAlphaNotice: true,
|
||||
advancedRendering: true,
|
||||
notUsingBlockers: false,
|
||||
})
|
||||
|
||||
@ -19,6 +21,12 @@ export const mutations = {
|
||||
SET_PROJECT_LAYOUT(state, projectLayout) {
|
||||
state.projectLayout = projectLayout
|
||||
},
|
||||
SET_MODPACKS_ALPHA_NOTICE(state, modpacksAlphaNotice) {
|
||||
state.modpacksAlphaNotice = modpacksAlphaNotice
|
||||
},
|
||||
SET_ADVANCED_RENDERING(state, advancedRendering) {
|
||||
state.advancedRendering = advancedRendering
|
||||
},
|
||||
SET_NOT_USING_BLOCKERS(state, notUsingBlockers) {
|
||||
state.notUsingBlockers = notUsingBlockers
|
||||
},
|
||||
@ -28,12 +36,27 @@ export const actions = {
|
||||
fetchCosmetics({ commit }, $cookies) {
|
||||
commit('SET_PROJECT_LAYOUT', $cookies.get('project-layout'))
|
||||
commit('SET_SEARCH_LAYOUT', $cookies.get('search-layout'))
|
||||
commit('SET_MODPACKS_ALPHA_NOTICE', $cookies.get('modpacks-alpha-notice'))
|
||||
commit('SET_ADVANCED_RENDERING', $cookies.get('advanced-rendering'))
|
||||
},
|
||||
save({ commit }, { projectLayout, searchLayout, $cookies }) {
|
||||
save(
|
||||
{ commit },
|
||||
{
|
||||
projectLayout,
|
||||
searchLayout,
|
||||
modpacksAlphaNotice,
|
||||
advancedRendering,
|
||||
$cookies,
|
||||
}
|
||||
) {
|
||||
commit('SET_PROJECT_LAYOUT', projectLayout)
|
||||
commit('SET_SEARCH_LAYOUT', searchLayout)
|
||||
commit('SET_MODPACKS_ALPHA_NOTICE', modpacksAlphaNotice)
|
||||
commit('SET_ADVANCED_RENDERING', advancedRendering)
|
||||
|
||||
$cookies.set('project-layout', projectLayout, parameters)
|
||||
$cookies.set('search-layout', searchLayout, parameters)
|
||||
$cookies.set('modpacks-alpha-notice', modpacksAlphaNotice, parameters)
|
||||
$cookies.set('advanced-rendering', advancedRendering, parameters)
|
||||
},
|
||||
}
|
||||
|
||||
81
store/tag.js
@ -1,9 +1,34 @@
|
||||
import tags from '~/generated/state.json'
|
||||
|
||||
export const state = () => ({
|
||||
categories: [],
|
||||
loaders: [],
|
||||
gameVersions: [],
|
||||
licenses: [],
|
||||
donationPlatforms: [],
|
||||
categories: tags.categories,
|
||||
loaders: tags.loaders,
|
||||
gameVersions: tags.gameVersions,
|
||||
licenses: tags.licenses,
|
||||
donationPlatforms: tags.donationPlatforms,
|
||||
reportTypes: tags.reportTypes,
|
||||
projectTypes: [
|
||||
{
|
||||
actual: 'mod',
|
||||
id: 'mod',
|
||||
display: 'mod',
|
||||
},
|
||||
{
|
||||
actual: 'mod',
|
||||
id: 'plugin',
|
||||
display: 'plugin',
|
||||
},
|
||||
{
|
||||
actual: 'resourcepack',
|
||||
id: 'resourcepack',
|
||||
display: 'resource pack',
|
||||
},
|
||||
{
|
||||
actual: 'modpack',
|
||||
id: 'modpack',
|
||||
display: 'modpack',
|
||||
},
|
||||
],
|
||||
loaderData: {
|
||||
pluginLoaders: ['bukkit', 'spigot', 'paper', 'purpur', 'sponge'],
|
||||
pluginPlatformLoaders: ['bungeecord', 'waterfall', 'velocity'],
|
||||
@ -20,49 +45,3 @@ export const state = () => ({
|
||||
modLoaders: ['forge', 'fabric', 'quilt', 'liteloader', 'modloader', 'rift'],
|
||||
},
|
||||
})
|
||||
|
||||
export const mutations = {
|
||||
SET_CATEGORIES(state, categories) {
|
||||
state.categories = categories
|
||||
},
|
||||
SET_LOADERS(state, loaders) {
|
||||
state.loaders = loaders
|
||||
},
|
||||
SET_GAME_VERSIONS(state, gameVersions) {
|
||||
state.gameVersions = gameVersions
|
||||
},
|
||||
SET_LICENSES(state, licenses) {
|
||||
state.licenses = licenses
|
||||
},
|
||||
SET_DONATION_PLATFORMS(state, donationPlatforms) {
|
||||
state.donationPlatforms = donationPlatforms
|
||||
},
|
||||
}
|
||||
|
||||
export const actions = {
|
||||
async fetchAllTags({ commit }) {
|
||||
const headers = {
|
||||
headers: {
|
||||
'x-ratelimit-key': process.server
|
||||
? process.env.RATE_LIMIT_IGNORE_KEY || ''
|
||||
: '',
|
||||
},
|
||||
}
|
||||
|
||||
const [categories, loaders, gameVersions, licenses, donationPlatforms] = (
|
||||
await Promise.all([
|
||||
this.$axios.get(`tag/category`, headers),
|
||||
this.$axios.get(`tag/loader`, headers),
|
||||
this.$axios.get(`tag/game_version`, headers),
|
||||
this.$axios.get(`tag/license`, headers),
|
||||
this.$axios.get(`tag/donation_platform`, headers),
|
||||
])
|
||||
).map((it) => it.data)
|
||||
|
||||
commit('SET_CATEGORIES', categories)
|
||||
commit('SET_LOADERS', loaders)
|
||||
commit('SET_GAME_VERSIONS', gameVersions)
|
||||
commit('SET_LICENSES', licenses)
|
||||
commit('SET_DONATION_PLATFORMS', donationPlatforms)
|
||||
},
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
"builds": [
|
||||
{
|
||||
"src": "nuxt.config.js",
|
||||
"use": "@nuxtjs/vercel-builder",
|
||||
"use": "@nuxtjs/vercel-builder@0.21.3",
|
||||
"config": {}
|
||||
}
|
||||
],
|
||||
|
||||