You've already forked AstralRinth
forked from didirus/AstralRinth
Migrate to Nuxt 3 (#933)
* Migrate to Nuxt 3 * Update vercel config * remove tsconfig comment * Changelog experiment + working proj pages * Fix package json * Prevent vercel complaining * fix deploy (hopefully) * Tag generator * Switch to yarn * Vercel pls 🙏 * Fix tag generation bug * Make (most) non-logged in pages work * fix base build * Linting + state * Eradicate axios, make most user pages work * Fix checkbox state being set incorrectly * Make most things work * Final stretch * Finish (most) things * Move to update model value * Fix modal text getting blurred from transforms (#964) * Adjust nav-link border radius when focused (#961) * Transition between animation states on TextLogo (#955) * Transition between animation states on TextLogo * Remove unused refs * Fixes from review * Disable tabbing to pagination arrows when disabled (#972) * Make position of the "no results" text on grid/gallery views consistent (fixes #963) (#965) * Fix position of the "no results" text on grid view * fix padding * Remove extra margin on main page, fixes #957 (#959) * Fix layout shift and placeholder line height (#973) * Fix a lot of issues * Fix more nuxt 3 issues * fix not all versions showing up (temp) * inline inter css file * More nuxt 3 fixes * [skip ci] broken- backup changes * Change modpack warnings to blue instead of red (#991) * Fix some hydration issues * Update nuxt * Fix some images not showing * Add pagination to versions page + fix lag * Make changelog page consistent with versions page * sync before merge * Delete old file * Fix actions failing * update branch * Fixes navbar transition animation. (#1012) * Fixes navbar transition animation. * Fixes Y-axis animation. Fixes mobile menu. Removes highlightjs prop. * Changes xss call to renderString. * Fixes renderString call. * Removes unnecessary styling. * Reverts mobile nav change. * Nuxt 3 Lazy Loading Search (#1022) * Uses lazyFetch for results. onSearchChange refreshes. Adds loading circle. * Removes console.log * Preserves old page when paging. * Diagnosing filtering bugs. * Fix single facet filtering * Implements useAuth in settings/account. * tiny ssr fix * Updating nuxt.config checklist. * Implements useAuth in revenue, moneitzation, and dashboard index pages. * Fixes setups. * Eliminates results when path changes. Adds animated logo. * Ensures loading animation renders on search page. --------- Co-authored-by: Jai A <jaiagr+gpg@pm.me> * Fix navigation issues * Square button fix (#1023) * Removes checklist from nuxt.config. * Modifies Nuxt CI to build after linting. * Fixes prettierignore file. * bug fixes * Update whitelist domains * Page improvements, fix CLS * Fix a lot of things * Fix project type redirect * Fix 404 errors * Fix user settings + hydration error * Final fixes * fix(creator-section): border radius on icons not aligning with bg (#1027) Co-authored-by: MagnusHJensen <magnus.holm.jensen@lego.dk> * Improvements to the mobile navbar (#984) * Transition between animation states on TextLogo * Remove unused refs * Fixes from review * Improvements to the mobile nav menu * fix avatar alt text * Nevermind, got confused for a moment * Tab bar, menu layout improvements * Highlight search icon when menu is open * Update layouts/default.vue Co-authored-by: Magnus Jensen <magnushjensen.mail@gmail.com> * Fix some issues * Use caret instead * Run prettier * Add create a project --------- Co-authored-by: Magnus Jensen <magnushjensen.mail@gmail.com> Co-authored-by: Geometrically <18202329+Geometrically@users.noreply.github.com> Co-authored-by: Jai A <jaiagr+gpg@pm.me> * Fix mobile menu issues * More issues * Fix lint --------- Co-authored-by: Kaeden Murphy <kmurphy@kaedenmurphy.dev> Co-authored-by: triphora <emmaffle@modrinth.com> Co-authored-by: Zach Baird <30800863+ZachBaird@users.noreply.github.com> Co-authored-by: stairman06 <36215135+stairman06@users.noreply.github.com> Co-authored-by: Zachary Baird <zdb1994@yahoo.com> Co-authored-by: Magnus Jensen <magnushjensen.mail@gmail.com> Co-authored-by: MagnusHJensen <magnus.holm.jensen@lego.dk>
This commit is contained in:
@@ -1,4 +1,3 @@
|
|||||||
# editorconfig.org
|
|
||||||
root = true
|
root = true
|
||||||
|
|
||||||
[*]
|
[*]
|
||||||
@@ -8,6 +7,7 @@ end_of_line = lf
|
|||||||
charset = utf-8
|
charset = utf-8
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
|
max_line_length = 100
|
||||||
|
|
||||||
[*.md]
|
[*.md]
|
||||||
trim_trailing_whitespace = false
|
trim_trailing_whitespace = false
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
node_modules/
|
|
||||||
jspm_packages/
|
|
||||||
22
.eslintrc.js
22
.eslintrc.js
@@ -1,22 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
root: true,
|
|
||||||
env: {
|
|
||||||
browser: true,
|
|
||||||
node: true,
|
|
||||||
},
|
|
||||||
parserOptions: {
|
|
||||||
parser: 'babel-eslint',
|
|
||||||
},
|
|
||||||
extends: [
|
|
||||||
'@nuxtjs',
|
|
||||||
'prettier',
|
|
||||||
'plugin:prettier/recommended',
|
|
||||||
'plugin:nuxt/recommended',
|
|
||||||
],
|
|
||||||
plugins: ['prettier'],
|
|
||||||
rules: {
|
|
||||||
'no-console': 'off',
|
|
||||||
'vue/no-v-html': 'off',
|
|
||||||
'import/no-named-as-default': 'off',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
27
.eslintrc.json
Normal file
27
.eslintrc.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"es2021": true,
|
||||||
|
"node": true
|
||||||
|
},
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:vue/vue3-recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"@nuxtjs/eslint-config-typescript",
|
||||||
|
"prettier"
|
||||||
|
],
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": "latest",
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"sourceType": "module"
|
||||||
|
},
|
||||||
|
"plugins": ["vue", "@typescript-eslint"],
|
||||||
|
"rules": {
|
||||||
|
"no-console": "off",
|
||||||
|
"vue/no-v-html": "off",
|
||||||
|
"comma-dangle": ["error", "only-multiline"],
|
||||||
|
"vue/multi-word-component-names": "off",
|
||||||
|
"import/no-named-as-default": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
2
.gitattributes
vendored
2
.gitattributes
vendored
@@ -1,2 +0,0 @@
|
|||||||
*.vue text eol=lf
|
|
||||||
|
|
||||||
23
.github/workflows/nuxt.yml
vendored
23
.github/workflows/nuxt.yml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: Nuxt CI
|
name: Build + Lint
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -15,18 +15,19 @@ jobs:
|
|||||||
- name: Use Node.js
|
- name: Use Node.js
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 16
|
node-version: 18.x
|
||||||
- name: Cache Node.js modules
|
- name: Get yarn cache
|
||||||
uses: actions/cache@v3
|
id: yarn-cache
|
||||||
|
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||||
|
- uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
# npm cache files are stored in `~/.npm` on Linux/macOS
|
path: ${{ steps.yarn-cache.outputs.dir }}
|
||||||
path: ~/.npm
|
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-node-
|
${{ runner.os }}-yarn-
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: yarn install --immutable --immutable-cache --check-cache
|
||||||
- name: Build Knossos
|
|
||||||
run: npm run build
|
|
||||||
- name: Run Lint
|
- name: Run Lint
|
||||||
run: npm run lint
|
run: npm run lint
|
||||||
|
- name: Build
|
||||||
|
run: npm run build
|
||||||
|
|||||||
29
.gitignore
vendored
29
.gitignore
vendored
@@ -1,3 +1,12 @@
|
|||||||
|
node_modules
|
||||||
|
*.log*
|
||||||
|
.nuxt
|
||||||
|
.nitro
|
||||||
|
.cache
|
||||||
|
.output
|
||||||
|
.env
|
||||||
|
dist
|
||||||
|
|
||||||
generated/
|
generated/
|
||||||
!.gitkeep
|
!.gitkeep
|
||||||
|
|
||||||
@@ -5,7 +14,6 @@ generated/
|
|||||||
### Node template
|
### Node template
|
||||||
# Logs
|
# Logs
|
||||||
/logs
|
/logs
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
@@ -39,7 +47,6 @@ bower_components
|
|||||||
build/Release
|
build/Release
|
||||||
|
|
||||||
# Dependency directories
|
# Dependency directories
|
||||||
node_modules/
|
|
||||||
jspm_packages/
|
jspm_packages/
|
||||||
|
|
||||||
# TypeScript v1 declaration files
|
# TypeScript v1 declaration files
|
||||||
@@ -60,24 +67,6 @@ typings/
|
|||||||
# Yarn Integrity file
|
# Yarn Integrity file
|
||||||
.yarn-integrity
|
.yarn-integrity
|
||||||
|
|
||||||
# dotenv environment variables file
|
|
||||||
.env
|
|
||||||
|
|
||||||
# parcel-bundler cache (https://parceljs.org/)
|
|
||||||
.cache
|
|
||||||
|
|
||||||
# next.js build output
|
|
||||||
.next
|
|
||||||
|
|
||||||
# nuxt.js build output
|
|
||||||
.nuxt
|
|
||||||
|
|
||||||
# Nuxt generate
|
|
||||||
dist
|
|
||||||
|
|
||||||
# vuepress build output
|
|
||||||
.vuepress/dist
|
|
||||||
|
|
||||||
# Serverless directories
|
# Serverless directories
|
||||||
.serverless
|
.serverless
|
||||||
|
|
||||||
|
|||||||
5
.idea/.gitignore
generated
vendored
Normal file
5
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
7
.idea/discord.xml
generated
Normal file
7
.idea/discord.xml
generated
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DiscordProjectSettings">
|
||||||
|
<option name="show" value="PROJECT_FILES" />
|
||||||
|
<option name="description" value="" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
12
.idea/knossos.iml
generated
Normal file
12
.idea/knossos.iml
generated
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="WEB_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/knossos.iml" filepath="$PROJECT_DIR$/.idea/knossos.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@@ -1,12 +1,25 @@
|
|||||||
|
node_modules
|
||||||
|
*.log*
|
||||||
|
.nuxt
|
||||||
|
.nitro
|
||||||
|
.cache
|
||||||
|
.output
|
||||||
|
.env
|
||||||
|
dist
|
||||||
|
*.md
|
||||||
|
|
||||||
|
generated/
|
||||||
|
!.gitkeep
|
||||||
|
|
||||||
# Created by .ignore support plugin (hsz.mobi)
|
# Created by .ignore support plugin (hsz.mobi)
|
||||||
### Node template
|
### Node template
|
||||||
# Logs
|
# Logs
|
||||||
/logs
|
/logs
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
|
||||||
|
|
||||||
# Runtime data
|
# Runtime data
|
||||||
pids
|
pids
|
||||||
*.pid
|
*.pid
|
||||||
@@ -35,7 +48,6 @@ bower_components
|
|||||||
build/Release
|
build/Release
|
||||||
|
|
||||||
# Dependency directories
|
# Dependency directories
|
||||||
node_modules/
|
|
||||||
jspm_packages/
|
jspm_packages/
|
||||||
|
|
||||||
# TypeScript v1 declaration files
|
# TypeScript v1 declaration files
|
||||||
@@ -56,24 +68,6 @@ typings/
|
|||||||
# Yarn Integrity file
|
# Yarn Integrity file
|
||||||
.yarn-integrity
|
.yarn-integrity
|
||||||
|
|
||||||
# dotenv environment variables file
|
|
||||||
.env
|
|
||||||
|
|
||||||
# parcel-bundler cache (https://parceljs.org/)
|
|
||||||
.cache
|
|
||||||
|
|
||||||
# next.js build output
|
|
||||||
.next
|
|
||||||
|
|
||||||
# nuxt.js build output
|
|
||||||
.nuxt
|
|
||||||
|
|
||||||
# Nuxt generate
|
|
||||||
dist
|
|
||||||
|
|
||||||
# vuepress build output
|
|
||||||
.vuepress/dist
|
|
||||||
|
|
||||||
# Serverless directories
|
# Serverless directories
|
||||||
.serverless
|
.serverless
|
||||||
|
|
||||||
@@ -89,4 +83,6 @@ sw.*
|
|||||||
# Vim swap files
|
# Vim swap files
|
||||||
*.swp
|
*.swp
|
||||||
|
|
||||||
Dockerfile
|
# pnpm files
|
||||||
|
pnpm-lock.yaml
|
||||||
|
/.npmrc
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"printWidth": 100,
|
||||||
"semi": false,
|
"semi": false,
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"endOfLine": "auto"
|
"endOfLine": "auto"
|
||||||
26
Dockerfile
26
Dockerfile
@@ -1,26 +0,0 @@
|
|||||||
# Dockerfile
|
|
||||||
FROM node:14.16.0-alpine
|
|
||||||
|
|
||||||
# update and install dependency
|
|
||||||
RUN apk update && apk upgrade
|
|
||||||
RUN apk add git
|
|
||||||
|
|
||||||
# create destination directory
|
|
||||||
RUN mkdir -p /usr/src/knossos
|
|
||||||
WORKDIR /usr/src/knossos
|
|
||||||
|
|
||||||
# copy the app, note .dockerignore
|
|
||||||
COPY . /usr/src/knossos/
|
|
||||||
RUN npm ci
|
|
||||||
|
|
||||||
ARG VERSION_ID=unknown
|
|
||||||
|
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
EXPOSE 3000
|
|
||||||
|
|
||||||
|
|
||||||
ENV NUXT_HOST=0.0.0.0
|
|
||||||
ENV NUXT_PORT=3000
|
|
||||||
|
|
||||||
ENTRYPOINT [ "npm", "start" ]
|
|
||||||
11
app.vue
Normal file
11
app.vue
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<template>
|
||||||
|
<NuxtLayout>
|
||||||
|
<ModrinthLoadingIndicator />
|
||||||
|
<Notifications />
|
||||||
|
<NuxtPage />
|
||||||
|
</NuxtLayout>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import ModrinthLoadingIndicator from '~/components/ui/modrinth-loading-indicator'
|
||||||
|
import Notifications from '~/components/ui/Notifications'
|
||||||
|
</script>
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
export default function (to, from, savedPosition) {
|
|
||||||
if (
|
|
||||||
from == null ||
|
|
||||||
(to.name.startsWith('type-id') && from.name.startsWith('type-id')) ||
|
|
||||||
to.name === from.name
|
|
||||||
) {
|
|
||||||
return savedPosition
|
|
||||||
} else {
|
|
||||||
return { x: 0, y: 0 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
131
assets/images/external/prism.svg
vendored
131
assets/images/external/prism.svg
vendored
@@ -1,91 +1,11 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
|
||||||
|
|
||||||
<svg
|
<svg
|
||||||
width="48"
|
|
||||||
height="48"
|
|
||||||
viewBox="0 0 12.7 12.7"
|
viewBox="0 0 12.7 12.7"
|
||||||
version="1.1"
|
version="1.1"
|
||||||
id="svg3606"
|
|
||||||
sodipodi:docname="org.prismlauncher.PrismLauncher.Source.svg"
|
|
||||||
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
>
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
<title id="title261">Prism Launcher Logo</title>
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
<defs id="defs3603" />
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
<g id="layer1">
|
||||||
<title
|
|
||||||
id="title261">Prism Launcher Logo</title>
|
|
||||||
<sodipodi:namedview
|
|
||||||
id="namedview3608"
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#000000"
|
|
||||||
borderopacity="0.25"
|
|
||||||
inkscape:showpageshadow="2"
|
|
||||||
inkscape:pageopacity="0.0"
|
|
||||||
inkscape:pagecheckerboard="0"
|
|
||||||
inkscape:deskcolor="#d1d1d1"
|
|
||||||
inkscape:document-units="px"
|
|
||||||
showgrid="false"
|
|
||||||
showguides="true"
|
|
||||||
inkscape:zoom="16"
|
|
||||||
inkscape:cx="14.9375"
|
|
||||||
inkscape:cy="13.9375"
|
|
||||||
inkscape:window-width="2560"
|
|
||||||
inkscape:window-height="1377"
|
|
||||||
inkscape:window-x="2552"
|
|
||||||
inkscape:window-y="-8"
|
|
||||||
inkscape:window-maximized="1"
|
|
||||||
inkscape:current-layer="layer1">
|
|
||||||
<sodipodi:guide
|
|
||||||
position="0.52916688,12.170833"
|
|
||||||
orientation="1,0"
|
|
||||||
id="guide4870"
|
|
||||||
inkscape:locked="false" />
|
|
||||||
<sodipodi:guide
|
|
||||||
position="12.170833,12.170833"
|
|
||||||
orientation="0,-1"
|
|
||||||
id="guide4872"
|
|
||||||
inkscape:locked="false" />
|
|
||||||
<sodipodi:guide
|
|
||||||
position="12.170833,0.5291669"
|
|
||||||
orientation="1,0"
|
|
||||||
id="guide4874"
|
|
||||||
inkscape:locked="false" />
|
|
||||||
<sodipodi:guide
|
|
||||||
position="0.52916688,0.5291666"
|
|
||||||
orientation="0,-1"
|
|
||||||
id="guide4876"
|
|
||||||
inkscape:locked="false" />
|
|
||||||
<sodipodi:guide
|
|
||||||
position="13.692187,21.332031"
|
|
||||||
orientation="0,-1"
|
|
||||||
id="guide6489"
|
|
||||||
inkscape:locked="false" />
|
|
||||||
<sodipodi:guide
|
|
||||||
position="6.3500002,12.170833"
|
|
||||||
orientation="1,0"
|
|
||||||
id="guide6491"
|
|
||||||
inkscape:locked="false" />
|
|
||||||
<sodipodi:guide
|
|
||||||
position="6.3500002,6.3499993"
|
|
||||||
orientation="-0.49999657,-0.86602738"
|
|
||||||
id="guide9375"
|
|
||||||
inkscape:locked="false" />
|
|
||||||
<sodipodi:guide
|
|
||||||
position="6.3500002,6.3499993"
|
|
||||||
orientation="-0.49999666,0.86602733"
|
|
||||||
id="guide9377"
|
|
||||||
inkscape:locked="false" />
|
|
||||||
</sodipodi:namedview>
|
|
||||||
<defs
|
|
||||||
id="defs3603" />
|
|
||||||
<g
|
|
||||||
inkscape:label="Layer 1"
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer1">
|
|
||||||
<g
|
<g
|
||||||
id="g531"
|
id="g531"
|
||||||
transform="matrix(0.1353646,0,0,0.1353646,15.301582,0.52916663)" />
|
transform="matrix(0.1353646,0,0,0.1353646,15.301582,0.52916663)" />
|
||||||
@@ -157,47 +77,4 @@
|
|||||||
</g>
|
</g>
|
||||||
</g>
|
</g>
|
||||||
</g>
|
</g>
|
||||||
<metadata
|
|
||||||
id="metadata259">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:title>Prism Launcher Logo</dc:title>
|
|
||||||
<dc:date>19/10/2022</dc:date>
|
|
||||||
<dc:creator>
|
|
||||||
<cc:Agent>
|
|
||||||
<dc:title>Prism Launcher</dc:title>
|
|
||||||
</cc:Agent>
|
|
||||||
</dc:creator>
|
|
||||||
<dc:contributor>
|
|
||||||
<cc:Agent>
|
|
||||||
<dc:title>AutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zeke</dc:title>
|
|
||||||
</cc:Agent>
|
|
||||||
</dc:contributor>
|
|
||||||
<dc:source>https://github.com/PrismLauncher/PrismLauncher</dc:source>
|
|
||||||
<dc:publisher>
|
|
||||||
<cc:Agent>
|
|
||||||
<dc:title>Prism Launcher</dc:title>
|
|
||||||
</cc:Agent>
|
|
||||||
</dc:publisher>
|
|
||||||
<cc:license
|
|
||||||
rdf:resource="http://creativecommons.org/licenses/by-sa/4.0/" />
|
|
||||||
</cc:Work>
|
|
||||||
<cc:License
|
|
||||||
rdf:about="http://creativecommons.org/licenses/by-sa/4.0/">
|
|
||||||
<cc:permits
|
|
||||||
rdf:resource="http://creativecommons.org/ns#Reproduction" />
|
|
||||||
<cc:permits
|
|
||||||
rdf:resource="http://creativecommons.org/ns#Distribution" />
|
|
||||||
<cc:requires
|
|
||||||
rdf:resource="http://creativecommons.org/ns#Notice" />
|
|
||||||
<cc:requires
|
|
||||||
rdf:resource="http://creativecommons.org/ns#Attribution" />
|
|
||||||
<cc:permits
|
|
||||||
rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
|
|
||||||
<cc:requires
|
|
||||||
rdf:resource="http://creativecommons.org/ns#ShareAlike" />
|
|
||||||
</cc:License>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 3.9 KiB |
@@ -71,9 +71,10 @@
|
|||||||
.multiselect__placeholder {
|
.multiselect__placeholder {
|
||||||
color: var(--color-button-text);
|
color: var(--color-button-text);
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
line-height: 20px;
|
line-height: 16px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,14 +187,10 @@
|
|||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.warning {
|
&:where(&.warning, &.information) {
|
||||||
border-left: 0.5rem solid var(--color-banner-side);
|
|
||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
background-color: var(--color-banner-bg);
|
|
||||||
color: var(--color-banner-text);
|
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
/* Uses active color to increase contrast */
|
/* Uses active color to increase contrast */
|
||||||
color: var(--color-link-active);
|
color: var(--color-link-active);
|
||||||
@@ -201,8 +198,20 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.warning {
|
||||||
|
border-left: 0.5rem solid var(--color-warning-banner-side);
|
||||||
|
background-color: var(--color-warning-banner-bg);
|
||||||
|
color: var(--color-warning-banner-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.information {
|
||||||
|
border-left: 0.5rem solid var(--color-info-banner-side);
|
||||||
|
background-color: var(--color-info-banner-bg);
|
||||||
|
color: var(--color-info-banner-text);
|
||||||
|
}
|
||||||
|
|
||||||
&.moderation-card {
|
&.moderation-card {
|
||||||
background-color: var(--color-banner-bg);
|
background-color: var(--color-warning-banner-bg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -427,11 +436,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .label:first-child :where(> :first-child, .label__title),
|
//> .label:first-child :where(> :first-child, .label__title),
|
||||||
> label:first-child :where(> :first-child, .label__title),
|
//> label:first-child :where(> :first-child, .label__title),
|
||||||
> .adjacent-input:first-child :where(> :first-child, .label__title) {
|
//> .adjacent-input:first-child :where(> :first-child, .label__title) {
|
||||||
margin-block-start: 0;
|
// margin-block-start: 0;
|
||||||
}
|
//}
|
||||||
}
|
}
|
||||||
|
|
||||||
.universal-card {
|
.universal-card {
|
||||||
@@ -734,104 +743,25 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip {
|
.v-popper--theme-tooltip {
|
||||||
display: block !important;
|
.v-popper__inner {
|
||||||
z-index: 10000;
|
background: var(--color-tooltip-bg) !important;
|
||||||
transition: opacity 0.15s ease-in-out, visibility 0.15s ease-in-out;
|
color: var(--color-tooltip-text) !important;
|
||||||
|
padding: 5px 10px 4px !important;
|
||||||
.tooltip-inner {
|
border-radius: var(--size-rounded-tooltip) !important;
|
||||||
background: var(--color-tooltip-bg);
|
box-shadow: var(--shadow-floating) !important;
|
||||||
color: var(--color-tooltip-text);
|
font-size: 0.9rem !important;
|
||||||
padding: 5px 10px 4px;
|
|
||||||
border-radius: var(--size-rounded-tooltip);
|
|
||||||
box-shadow: var(--shadow-floating);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-arrow {
|
.v-popper__arrow-outer,
|
||||||
width: 0;
|
.v-popper__arrow-inner {
|
||||||
height: 0;
|
border-color: var(--color-tooltip-bg) !important;
|
||||||
border-style: solid;
|
|
||||||
position: absolute;
|
|
||||||
margin: 5px;
|
|
||||||
border-color: var(--color-tooltip-bg);
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&[x-placement^='top'] {
|
|
||||||
margin-bottom: 5px;
|
|
||||||
|
|
||||||
.tooltip-arrow {
|
|
||||||
border-width: 5px 5px 0 5px;
|
|
||||||
border-left-color: transparent !important;
|
|
||||||
border-right-color: transparent !important;
|
|
||||||
border-bottom-color: transparent !important;
|
|
||||||
bottom: -5px;
|
|
||||||
left: calc(50% - 5px);
|
|
||||||
margin-top: 0;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&[x-placement^='bottom'] {
|
|
||||||
margin-top: 5px;
|
|
||||||
|
|
||||||
.tooltip-arrow {
|
|
||||||
border-width: 0 5px 5px 5px;
|
|
||||||
border-left-color: transparent !important;
|
|
||||||
border-right-color: transparent !important;
|
|
||||||
border-top-color: transparent !important;
|
|
||||||
top: -5px;
|
|
||||||
left: calc(50% - 5px);
|
|
||||||
margin-top: 0;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&[x-placement^='right'] {
|
|
||||||
margin-left: 5px;
|
|
||||||
|
|
||||||
.tooltip-arrow {
|
|
||||||
border-width: 5px 5px 5px 0;
|
|
||||||
border-left-color: transparent !important;
|
|
||||||
border-top-color: transparent !important;
|
|
||||||
border-bottom-color: transparent !important;
|
|
||||||
left: -5px;
|
|
||||||
top: calc(50% - 5px);
|
|
||||||
margin-left: 0;
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&[x-placement^='left'] {
|
|
||||||
margin-right: 5px;
|
|
||||||
|
|
||||||
.tooltip-arrow {
|
|
||||||
border-width: 5px 0 5px 5px;
|
|
||||||
border-top-color: transparent !important;
|
|
||||||
border-right-color: transparent !important;
|
|
||||||
border-bottom-color: transparent !important;
|
|
||||||
right: -5px;
|
|
||||||
top: calc(50% - 5px);
|
|
||||||
margin-left: 0;
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&[aria-hidden='true'] {
|
|
||||||
visibility: hidden;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&[aria-hidden='false'] {
|
|
||||||
visibility: visible;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-animation {
|
.button-animation {
|
||||||
transition: opacity 0.5s ease-in-out, filter 0.2s ease-in-out,
|
transition: opacity 0.5s ease-in-out, filter 0.2s ease-in-out, transform 0.05s ease-in-out,
|
||||||
transform 0.05s ease-in-out, outline 0.2s ease-in-out;
|
outline 0.2s ease-in-out;
|
||||||
|
|
||||||
&:active:not(&:disabled) {
|
&:active:not(&:disabled) {
|
||||||
transform: scale(0.95);
|
transform: scale(0.95);
|
||||||
@@ -901,8 +831,8 @@ tr.button-transparent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.button-within {
|
.button-within {
|
||||||
transition: opacity 0.5s ease-in-out, filter 0.2s ease-in-out,
|
transition: opacity 0.5s ease-in-out, filter 0.2s ease-in-out, transform 0.05s ease-in-out,
|
||||||
transform 0.05s ease-in-out, outline 0.2s ease-in-out;
|
outline 0.2s ease-in-out;
|
||||||
|
|
||||||
&:focus-visible:not(&.disabled),
|
&:focus-visible:not(&.disabled),
|
||||||
&:hover:not(&.disabled) {
|
&:hover:not(&.disabled) {
|
||||||
@@ -990,6 +920,7 @@ tr.button-transparent {
|
|||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
height: 2.25rem;
|
height: 2.25rem;
|
||||||
width: 2.25rem;
|
width: 2.25rem;
|
||||||
border-radius: var(--size-rounded-sm);
|
border-radius: var(--size-rounded-sm);
|
||||||
@@ -1135,9 +1066,10 @@ tr.button-transparent {
|
|||||||
.multiselect__placeholder {
|
.multiselect__placeholder {
|
||||||
color: var(--color-button-text);
|
color: var(--color-button-text);
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
line-height: 20px;
|
line-height: 16px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1313,76 +1245,27 @@ tr.button-transparent {
|
|||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.warning {
|
&:where(&.warning, &.information) {
|
||||||
border-left: 0.5rem solid var(--color-banner-side);
|
|
||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
background-color: var(--color-banner-bg);
|
|
||||||
color: var(--color-banner-text);
|
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
/* Uses active color to increase contrast */
|
/* Uses active color to increase contrast */
|
||||||
color: var(--color-link-active);
|
color: var(--color-link-active);
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.vue-notification {
|
&.warning {
|
||||||
background: var(--color-special-blue) !important;
|
border-left: 0.5rem solid var(--color-warning-banner-side);
|
||||||
border-left: 5px solid var(--color-special-blue) !important;
|
background-color: var(--color-warning-banner-bg);
|
||||||
color: var(--color-brand-inverted) !important;
|
color: var(--color-warning-banner-text);
|
||||||
|
|
||||||
&.success {
|
|
||||||
background: var(--color-special-green) !important;
|
|
||||||
border-left-color: var(--color-special-green) !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.warn {
|
&.information {
|
||||||
background: var(--color-special-orange) !important;
|
border-left: 0.5rem solid var(--color-info-banner-side);
|
||||||
border-left-color: var(--color-special-orange) !important;
|
background-color: var(--color-info-banner-bg);
|
||||||
}
|
color: var(--color-info-banner-text);
|
||||||
|
|
||||||
&.error {
|
|
||||||
background: var(--color-special-red) !important;
|
|
||||||
border-left-color: var(--color-special-red) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.vue-notification-group {
|
|
||||||
right: 25px !important;
|
|
||||||
bottom: 25px !important;
|
|
||||||
|
|
||||||
.vue-notification-wrapper {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
|
|
||||||
.vue-notification-template {
|
|
||||||
border-radius: var(--size-rounded-card);
|
|
||||||
margin: 0;
|
|
||||||
|
|
||||||
.notification-title {
|
|
||||||
font-size: var(--font-size-lg);
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-content {
|
|
||||||
font-size: var(--font-size-md);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 750px) {
|
|
||||||
transition: bottom 0.25s ease-in-out;
|
|
||||||
bottom: calc(var(--size-mobile-navbar-height) + 10px) !important;
|
|
||||||
|
|
||||||
&.browse-menu-open {
|
|
||||||
bottom: calc(var(--size-mobile-navbar-height-expanded) + 10px) !important;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1449,7 +1332,7 @@ h1 {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nuxt-link-exact-active,
|
.router-link-exact-active,
|
||||||
h1,
|
h1,
|
||||||
h2,
|
h2,
|
||||||
h3 {
|
h3 {
|
||||||
|
|||||||
@@ -67,9 +67,13 @@ html {
|
|||||||
--color-warning-bg: hsl(355, 70%, 88%);
|
--color-warning-bg: hsl(355, 70%, 88%);
|
||||||
--color-warning-text: hsl(342, 70%, 35%);
|
--color-warning-text: hsl(342, 70%, 35%);
|
||||||
|
|
||||||
--color-banner-text: hsl(0, 11%, 16%);
|
--color-warning-banner-text: hsl(0, 11%, 16%);
|
||||||
--color-banner-bg: hsl(0, 100%, 95%);
|
--color-warning-banner-bg: hsl(0, 100%, 95%);
|
||||||
--color-banner-side: hsl(357, 78%, 40%);
|
--color-warning-banner-side: hsl(357, 78%, 40%);
|
||||||
|
|
||||||
|
--color-info-banner-text: var(--color-text);
|
||||||
|
--color-info-banner-bg: var(--color-ad);
|
||||||
|
--color-info-banner-side: var(--color-special-blue);
|
||||||
|
|
||||||
--color-block-quote: var(--color-tooltip-bg);
|
--color-block-quote: var(--color-tooltip-bg);
|
||||||
--color-header-underline: var(--color-divider-dark);
|
--color-header-underline: var(--color-divider-dark);
|
||||||
@@ -86,9 +90,8 @@ html {
|
|||||||
--shadow-raised: 0.3px 0.5px 0.6px hsl(var(--shadow-color) / 0.15),
|
--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),
|
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);
|
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,
|
--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) 0px 0px 0px 0px, hsla(0, 0%, 0%, 0.1) 0px 4px 6px -1px,
|
hsla(0, 0%, 0%, 0.1) 0px 4px 6px -1px, hsla(0, 0%, 0%, 0.1) 0px 2px 4px -1px;
|
||||||
hsla(0, 0%, 0%, 0.1) 0px 2px 4px -1px;
|
|
||||||
|
|
||||||
--shadow-card: rgba(50, 50, 100, 0.1) 0px 2px 4px 0px;
|
--shadow-card: rgba(50, 50, 100, 0.1) 0px 2px 4px 0px;
|
||||||
|
|
||||||
@@ -112,11 +115,7 @@ html {
|
|||||||
rgba(66, 71, 97, 0.34) 100%
|
rgba(66, 71, 97, 0.34) 100%
|
||||||
);
|
);
|
||||||
--landing-border-color: rgba(129, 137, 175, 0.55);
|
--landing-border-color: rgba(129, 137, 175, 0.55);
|
||||||
--landing-creator-gradient: linear-gradient(
|
--landing-creator-gradient: linear-gradient(180deg, #f8f8f8 0%, #f8f8f8 63.19%);
|
||||||
180deg,
|
|
||||||
#f8f8f8 0%,
|
|
||||||
#f8f8f8 63.19%
|
|
||||||
);
|
|
||||||
|
|
||||||
--landing-blob-gradient: radial-gradient(
|
--landing-blob-gradient: radial-gradient(
|
||||||
50% 50% at 50% 50%,
|
50% 50% at 50% 50%,
|
||||||
@@ -196,9 +195,13 @@ html {
|
|||||||
--color-warning-bg: hsl(355, 70%, 20%);
|
--color-warning-bg: hsl(355, 70%, 20%);
|
||||||
--color-warning-text: hsl(342, 70%, 75%);
|
--color-warning-text: hsl(342, 70%, 75%);
|
||||||
|
|
||||||
--color-banner-text: hsl(0, 100%, 96%);
|
--color-warning-banner-text: hsl(0, 100%, 96%);
|
||||||
--color-banner-bg: hsl(356, 18%, 18%);
|
--color-warning-banner-bg: hsl(356, 18%, 18%);
|
||||||
--color-banner-side: hsl(357, 78%, 40%);
|
--color-warning-banner-side: hsl(357, 78%, 40%);
|
||||||
|
|
||||||
|
--color-info-banner-text: var(--color-text);
|
||||||
|
--color-info-banner-bg: var(--color-ad);
|
||||||
|
--color-info-banner-side: var(--color-special-blue);
|
||||||
|
|
||||||
--color-block-quote: var(--color-code-bg);
|
--color-block-quote: var(--color-code-bg);
|
||||||
--color-header-underline: var(--color-divider-dark);
|
--color-header-underline: var(--color-divider-dark);
|
||||||
@@ -213,18 +216,13 @@ html {
|
|||||||
|
|
||||||
--shadow-raised-lg: 0px 2px 4px hsla(221, 39%, 11%, 0.2);
|
--shadow-raised-lg: 0px 2px 4px hsla(221, 39%, 11%, 0.2);
|
||||||
--shadow-raised: 0px -2px 4px hsla(221, 39%, 11%, 0.1);
|
--shadow-raised: 0px -2px 4px hsla(221, 39%, 11%, 0.1);
|
||||||
--shadow-floating: hsla(0, 0%, 0%, 0) 0px 0px 0px 0px,
|
--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) 0px 0px 0px 0px, hsla(0, 0%, 0%, 0.1) 0px 4px 6px -1px,
|
hsla(0, 0%, 0%, 0.1) 0px 4px 6px -1px, rgba(0, 0, 0, 0.06) 0px 2px 4px -1px;
|
||||||
rgba(0, 0, 0, 0.06) 0px 2px 4px -1px;
|
|
||||||
|
|
||||||
--shadow-card: rgba(0, 0, 0, 0.25) 0px 2px 4px 0px;
|
--shadow-card: rgba(0, 0, 0, 0.25) 0px 2px 4px 0px;
|
||||||
|
|
||||||
--landing-maze-bg: url('https://cdn.modrinth.com/landing/landing.png');
|
--landing-maze-bg: url('https://cdn.modrinth.com/landing/landing.png');
|
||||||
--landing-maze-gradient-bg: linear-gradient(
|
--landing-maze-gradient-bg: linear-gradient(0deg, #31375f 0%, rgba(8, 14, 55, 0) 100%),
|
||||||
0deg,
|
|
||||||
#31375f 0%,
|
|
||||||
rgba(8, 14, 55, 0) 100%
|
|
||||||
),
|
|
||||||
url('https://cdn.modrinth.com/landing/landing-lower.png');
|
url('https://cdn.modrinth.com/landing/landing-lower.png');
|
||||||
--landing-maze-outer-bg: linear-gradient(180deg, #06060d 0%, #000000 100%);
|
--landing-maze-outer-bg: linear-gradient(180deg, #06060d 0%, #000000 100%);
|
||||||
|
|
||||||
@@ -251,8 +249,7 @@ html {
|
|||||||
rgba(44, 48, 79, 0.35) 0%,
|
rgba(44, 48, 79, 0.35) 0%,
|
||||||
rgba(32, 35, 50, 0.2695) 100%
|
rgba(32, 35, 50, 0.2695) 100%
|
||||||
);
|
);
|
||||||
--landing-blob-shadow: 2px 2px 12px rgba(0, 0, 0, 0.16),
|
--landing-blob-shadow: 2px 2px 12px rgba(0, 0, 0, 0.16), inset 2px 2px 64px rgba(57, 61, 94, 0.45);
|
||||||
inset 2px 2px 64px rgba(57, 61, 94, 0.45);
|
|
||||||
|
|
||||||
--landing-card-bg: rgba(59, 63, 85, 0.15);
|
--landing-card-bg: rgba(59, 63, 85, 0.15);
|
||||||
--landing-card-shadow: 2px 2px 12px rgba(0, 0, 0, 0.16);
|
--landing-card-shadow: 2px 2px 12px rgba(0, 0, 0, 0.16);
|
||||||
@@ -280,10 +277,9 @@ body {
|
|||||||
// Defaults
|
// Defaults
|
||||||
background-color: var(--color-bg);
|
background-color: var(--color-bg);
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
--font-standard: Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen,
|
--font-standard: Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Roboto,
|
||||||
Ubuntu, Roboto, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
||||||
--mono-font: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas,
|
--mono-font: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;
|
||||||
Liberation Mono, monospace;
|
|
||||||
font-family: var(--font-standard);
|
font-family: var(--font-standard);
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: var(--font-weight-regular);
|
font-weight: var(--font-weight-regular);
|
||||||
@@ -436,11 +432,12 @@ kbd {
|
|||||||
font-size: 0.85em !important;
|
font-size: 0.85em !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@import '~assets/styles/highlightjs.scss';
|
@import '~/assets/styles/highlightjs.scss';
|
||||||
@import '~assets/styles/layout.scss';
|
@import '~/assets/styles/layout.scss';
|
||||||
@import '~assets/styles/utils.scss';
|
@import '~/assets/styles/utils.scss';
|
||||||
@import '~assets/styles/components.scss';
|
@import '~/assets/styles/components.scss';
|
||||||
@import '~assets/styles/normalize.scss';
|
@import '~/assets/styles/normalize.scss';
|
||||||
|
@import '~/assets/styles/inter.scss';
|
||||||
|
|
||||||
button:focus-visible,
|
button:focus-visible,
|
||||||
a:focus-visible,
|
a:focus-visible,
|
||||||
|
|||||||
40
assets/styles/inter.scss
Normal file
40
assets/styles/inter.scss
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: inter;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('https://cdn-raw.modrinth.com/fonts/inter/Inter-Regular.woff2?v=3.19') format('woff2'),
|
||||||
|
url('https://cdn-raw.modrinth.com/fonts/inter/Inter-Regular.woff?v=3.19') format('woff');
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: inter;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('https://cdn-raw.modrinth.com/fonts/inter/Inter-Medium.woff2?v=3.19') format('woff2'),
|
||||||
|
url('https://cdn-raw.modrinth.com/fonts/inter/Inter-Medium.woff?v=3.19') format('woff');
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: inter;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('https://cdn-raw.modrinth.com/fonts/inter/Inter-SemiBold.woff2?v=3.19') format('woff2'),
|
||||||
|
url('https://cdn-raw.modrinth.com/fonts/inter/Inter-SemiBold.woff?v=3.19') format('woff');
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: inter;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('https://cdn-raw.modrinth.com/fonts/inter/Inter-Bold.woff2?v=3.19') format('woff2'),
|
||||||
|
url('https://cdn-raw.modrinth.com/fonts/inter/Inter-Bold.woff?v=3.19') format('woff');
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: inter;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 800;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('https://cdn-raw.modrinth.com/fonts/inter/Inter-ExtraBold.woff2?v=3.19') format('woff2'),
|
||||||
|
url('https://cdn-raw.modrinth.com/fonts/inter/Inter-ExtraBold.woff?v=3.19') format('woff');
|
||||||
|
}
|
||||||
@@ -47,6 +47,10 @@
|
|||||||
'info'
|
'info'
|
||||||
/ 100%;
|
/ 100%;
|
||||||
|
|
||||||
|
@media screen and (max-width: 1024px) {
|
||||||
|
margin-top: var(--spacing-card-md);
|
||||||
|
}
|
||||||
|
|
||||||
.normal-page__sidebar {
|
.normal-page__sidebar {
|
||||||
grid-area: sidebar;
|
grid-area: sidebar;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,51 +5,36 @@
|
|||||||
<div class="MYYLVTXBPUVWMLVBPVSDLHADDRYFBF-2">
|
<div class="MYYLVTXBPUVWMLVBPVSDLHADDRYFBF-2">
|
||||||
<a
|
<a
|
||||||
href="https://exaroton.com/?utm_source=modrinth&utm_medium=text&utm_campaign=host&utm_content=top"
|
href="https://exaroton.com/?utm_source=modrinth&utm_medium=text&utm_campaign=host&utm_content=top"
|
||||||
rel="noopener noreferrer nofollow sponsored"
|
rel="noopener nofollow sponsored"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<ColorScheme>
|
<LightIcon
|
||||||
<LightIcon
|
v-if="colorMode.value === 'light'"
|
||||||
v-if="$colorMode.value === 'light'"
|
class="MYYLVTXBPUVWMLVBPVSDLHADDRYFBF-3"
|
||||||
class="MYYLVTXBPUVWMLVBPVSDLHADDRYFBF-3"
|
/>
|
||||||
/>
|
<DarkIcon v-else class="MYYLVTXBPUVWMLVBPVSDLHADDRYFBF-3" />
|
||||||
<DarkIcon v-else class="MYYLVTXBPUVWMLVBPVSDLHADDRYFBF-3" />
|
|
||||||
</ColorScheme>
|
|
||||||
<span>
|
<span>
|
||||||
<span> Host your Minecraft server on </span>
|
<span> Host your Minecraft server on </span>
|
||||||
<strong>exaroton</strong>
|
<strong>exaroton</strong>
|
||||||
<span>
|
<span> - only pay while the server is running - billed per second. </span>
|
||||||
- only pay while the server is running - billed per second.
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="MYYLVTXBPUVWMLVBPVSDLHADDRYFBF-4">
|
<div class="MYYLVTXBPUVWMLVBPVSDLHADDRYFBF-4">
|
||||||
<a
|
<a rel="noopener sponsored" target="_blank" href="https://adrinth.com"> Ads via Adrinth </a>
|
||||||
rel="noopener noreferrer nofollow sponsored"
|
|
||||||
target="_blank"
|
|
||||||
href="https://adrinth.com"
|
|
||||||
>
|
|
||||||
Ads via Adrinth
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script setup>
|
||||||
import LightIcon from '~/assets/images/external/exaroton-light.svg?inline'
|
import LightIcon from '~/assets/images/external/exaroton-light.svg'
|
||||||
import DarkIcon from '~/assets/images/external/exaroton-dark.svg?inline'
|
import DarkIcon from '~/assets/images/external/exaroton-dark.svg'
|
||||||
|
|
||||||
export default {
|
const colorMode = useTheme()
|
||||||
components: {
|
|
||||||
LightIcon,
|
|
||||||
DarkIcon,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style>
|
||||||
.MYYLVTXBPUVWMLVBPVSDLHADDRYFBF {
|
.MYYLVTXBPUVWMLVBPVSDLHADDRYFBF {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-bottom: var(--spacing-card-md);
|
margin-bottom: var(--spacing-card-md);
|
||||||
@@ -5,17 +5,9 @@
|
|||||||
width="100%"
|
width="100%"
|
||||||
height="100%"
|
height="100%"
|
||||||
viewBox="0 0 590 591"
|
viewBox="0 0 590 591"
|
||||||
version="1.1"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
||||||
xml:space="preserve"
|
xml:space="preserve"
|
||||||
xmlns:serif="http://www.serif.com/"
|
style="fill-rule: evenodd; clip-rule: evenodd; stroke-linejoin: round; stroke-miterlimit: 2"
|
||||||
style="
|
|
||||||
fill-rule: evenodd;
|
|
||||||
clip-rule: evenodd;
|
|
||||||
stroke-linejoin: round;
|
|
||||||
stroke-miterlimit: 2;
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<g transform="matrix(1,0,0,1,652.392,-0.400578)">
|
<g transform="matrix(1,0,0,1,652.392,-0.400578)">
|
||||||
<g transform="matrix(4.16667,0,0,4.16667,-735.553,0)">
|
<g transform="matrix(4.16667,0,0,4.16667,-735.553,0)">
|
||||||
@@ -26,23 +18,16 @@
|
|||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
</g>
|
</g>
|
||||||
</g></svg
|
</g>
|
||||||
><svg
|
</svg>
|
||||||
|
<svg
|
||||||
class="rotate inner"
|
class="rotate inner"
|
||||||
width="100%"
|
width="100%"
|
||||||
height="100%"
|
height="100%"
|
||||||
viewBox="0 0 590 591"
|
viewBox="0 0 590 591"
|
||||||
version="1.1"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
||||||
xml:space="preserve"
|
xml:space="preserve"
|
||||||
xmlns:serif="http://www.serif.com/"
|
style="fill-rule: evenodd; clip-rule: evenodd; stroke-linejoin: round; stroke-miterlimit: 2"
|
||||||
style="
|
|
||||||
fill-rule: evenodd;
|
|
||||||
clip-rule: evenodd;
|
|
||||||
stroke-linejoin: round;
|
|
||||||
stroke-miterlimit: 2;
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<g transform="matrix(1,0,0,1,652.392,-0.400578)">
|
<g transform="matrix(1,0,0,1,652.392,-0.400578)">
|
||||||
<g transform="matrix(4.16667,0,0,4.16667,-735.553,0)">
|
<g transform="matrix(4.16667,0,0,4.16667,-735.553,0)">
|
||||||
@@ -59,17 +44,9 @@
|
|||||||
width="100%"
|
width="100%"
|
||||||
height="100%"
|
height="100%"
|
||||||
viewBox="0 0 590 591"
|
viewBox="0 0 590 591"
|
||||||
version="1.1"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
||||||
xml:space="preserve"
|
xml:space="preserve"
|
||||||
xmlns:serif="http://www.serif.com/"
|
style="fill-rule: evenodd; clip-rule: evenodd; stroke-linejoin: round; stroke-miterlimit: 2"
|
||||||
style="
|
|
||||||
fill-rule: evenodd;
|
|
||||||
clip-rule: evenodd;
|
|
||||||
stroke-linejoin: round;
|
|
||||||
stroke-miterlimit: 2;
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<g transform="matrix(1,0,0,1,652.392,-0.400578)">
|
<g transform="matrix(1,0,0,1,652.392,-0.400578)">
|
||||||
<g transform="matrix(4.16667,0,0,4.16667,-735.553,0)">
|
<g transform="matrix(4.16667,0,0,4.16667,-735.553,0)">
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
stroke-miterlimit="2"
|
stroke-miterlimit="2"
|
||||||
clip-rule="evenodd"
|
clip-rule="evenodd"
|
||||||
viewBox="0 0 3307 593"
|
viewBox="0 0 3307 593"
|
||||||
:class="{ animate: $nuxt.$loading ? $nuxt.$loading.show : false }"
|
:class="{ animate: loading }"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
fill-rule="nonzero"
|
fill-rule="nonzero"
|
||||||
@@ -30,6 +30,10 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
const loading = useLoading()
|
||||||
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.animate {
|
.animate {
|
||||||
.ring {
|
.ring {
|
||||||
|
|||||||
@@ -2,18 +2,14 @@
|
|||||||
<img
|
<img
|
||||||
v-if="src"
|
v-if="src"
|
||||||
ref="img"
|
ref="img"
|
||||||
:class="`avatar size-${size} ${circle ? 'circle' : ''} ${
|
:class="`avatar size-${size} ${circle ? 'circle' : ''} ${noShadow ? 'no-shadow' : ''}`"
|
||||||
noShadow ? 'no-shadow' : ''
|
|
||||||
}`"
|
|
||||||
:src="src"
|
:src="src"
|
||||||
:alt="alt"
|
:alt="alt"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
/>
|
/>
|
||||||
<svg
|
<svg
|
||||||
v-else
|
v-else
|
||||||
:class="`avatar size-${size} ${circle ? 'circle' : ''} ${
|
:class="`avatar size-${size} ${circle ? 'circle' : ''} ${noShadow ? 'no-shadow' : ''}`"
|
||||||
noShadow ? 'no-shadow' : ''
|
|
||||||
}`"
|
|
||||||
xml:space="preserve"
|
xml:space="preserve"
|
||||||
fill-rule="evenodd"
|
fill-rule="evenodd"
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
@@ -35,7 +31,6 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
name: 'Avatar',
|
|
||||||
props: {
|
props: {
|
||||||
src: {
|
src: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -68,10 +63,7 @@ export default {
|
|||||||
mounted() {
|
mounted() {
|
||||||
if (this.$refs.img && this.$refs.img.naturalWidth) {
|
if (this.$refs.img && this.$refs.img.naturalWidth) {
|
||||||
const isPixelated = () => {
|
const isPixelated = () => {
|
||||||
if (
|
if (this.$refs.img.naturalWidth < 96 && this.$refs.img.naturalWidth > 0) {
|
||||||
this.$refs.img.naturalWidth < 96 &&
|
|
||||||
this.$refs.img.naturalWidth > 0
|
|
||||||
) {
|
|
||||||
this.$refs.img.style.imageRendering = 'pixelated'
|
this.$refs.img.style.imageRendering = 'pixelated'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<span :class="'version-badge ' + color + ' type--' + type">
|
<span :class="'version-badge ' + color + ' type--' + type">
|
||||||
<template v-if="color">
|
<template v-if="color"> <span class="circle" /> {{ $capitalizeString(type) }} </template>
|
||||||
<span class="circle" /> {{ $capitalizeString(type) }}
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- User roles -->
|
<!-- User roles -->
|
||||||
<template v-else-if="type === 'admin'">
|
<template v-else-if="type === 'admin'"> <ModrinthIcon /> Modrinth Team </template>
|
||||||
<ModrinthIcon /> Modrinth Team
|
<template v-else-if="type === 'moderator'"> <ModeratorIcon /> Moderator </template>
|
||||||
</template>
|
|
||||||
<template v-else-if="type === 'moderator'">
|
|
||||||
<ModeratorIcon /> Moderator
|
|
||||||
</template>
|
|
||||||
<template v-else-if="type === 'creator'"><CreatorIcon /> Creator</template>
|
<template v-else-if="type === 'creator'"><CreatorIcon /> Creator</template>
|
||||||
|
|
||||||
<!-- Project statuses -->
|
<!-- Project statuses -->
|
||||||
@@ -18,45 +12,34 @@
|
|||||||
<template v-else-if="type === 'unlisted'"><EyeOffIcon /> Unlisted</template>
|
<template v-else-if="type === 'unlisted'"><EyeOffIcon /> Unlisted</template>
|
||||||
<template v-else-if="type === 'withheld'"><EyeOffIcon /> Withheld</template>
|
<template v-else-if="type === 'withheld'"><EyeOffIcon /> Withheld</template>
|
||||||
<template v-else-if="type === 'private'"><LockIcon /> Private</template>
|
<template v-else-if="type === 'private'"><LockIcon /> Private</template>
|
||||||
<template v-else-if="type === 'scheduled'">
|
<template v-else-if="type === 'scheduled'"> <CalendarIcon /> Scheduled </template>
|
||||||
<CalendarIcon /> Scheduled
|
|
||||||
</template>
|
|
||||||
<template v-else-if="type === 'draft'"><DraftIcon /> Draft</template>
|
<template v-else-if="type === 'draft'"><DraftIcon /> Draft</template>
|
||||||
<template v-else-if="type === 'archived'">
|
<template v-else-if="type === 'archived'"> <ArchiveIcon /> Archived </template>
|
||||||
<ArchiveIcon /> Archived
|
|
||||||
</template>
|
|
||||||
<template v-else-if="type === 'rejected'"><CrossIcon /> Rejected</template>
|
<template v-else-if="type === 'rejected'"><CrossIcon /> Rejected</template>
|
||||||
<template v-else-if="type === 'processing'">
|
<template v-else-if="type === 'processing'"> <ProcessingIcon /> Under review </template>
|
||||||
<ProcessingIcon /> Under review
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- Team members -->
|
<!-- Team members -->
|
||||||
<template v-else-if="type === 'accepted'"><CheckIcon /> Accepted</template>
|
<template v-else-if="type === 'accepted'"><CheckIcon /> Accepted</template>
|
||||||
<template v-else-if="type === 'pending'">
|
<template v-else-if="type === 'pending'"> <ProcessingIcon /> Pending </template>
|
||||||
<ProcessingIcon /> Pending
|
<template v-else> <span class="circle" /> {{ $capitalizeString(type) }} </template>
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<span class="circle" /> {{ $capitalizeString(type) }}
|
|
||||||
</template>
|
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import ModrinthIcon from '~/assets/images/logo.svg?inline'
|
import ModrinthIcon from '~/assets/images/logo.svg'
|
||||||
import ModeratorIcon from '~/assets/images/sidebar/admin.svg?inline'
|
import ModeratorIcon from '~/assets/images/sidebar/admin.svg'
|
||||||
import CreatorIcon from '~/assets/images/utils/box.svg?inline'
|
import CreatorIcon from '~/assets/images/utils/box.svg'
|
||||||
import ListIcon from '~/assets/images/utils/list.svg?inline'
|
import ListIcon from '~/assets/images/utils/list.svg'
|
||||||
import EyeOffIcon from '~/assets/images/utils/eye-off.svg?inline'
|
import EyeOffIcon from '~/assets/images/utils/eye-off.svg'
|
||||||
import DraftIcon from '~/assets/images/utils/file-text.svg?inline'
|
import DraftIcon from '~/assets/images/utils/file-text.svg'
|
||||||
import CrossIcon from '~/assets/images/utils/x.svg?inline'
|
import CrossIcon from '~/assets/images/utils/x.svg'
|
||||||
import ArchiveIcon from '~/assets/images/utils/archive.svg?inline'
|
import ArchiveIcon from '~/assets/images/utils/archive.svg'
|
||||||
import ProcessingIcon from '~/assets/images/utils/updated.svg?inline'
|
import ProcessingIcon from '~/assets/images/utils/updated.svg'
|
||||||
import CheckIcon from '~/assets/images/utils/check.svg?inline'
|
import CheckIcon from '~/assets/images/utils/check.svg'
|
||||||
import LockIcon from '~/assets/images/utils/lock.svg?inline'
|
import LockIcon from '~/assets/images/utils/lock.svg'
|
||||||
import CalendarIcon from '~/assets/images/utils/calendar.svg?inline'
|
import CalendarIcon from '~/assets/images/utils/calendar.svg'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Badge',
|
|
||||||
components: {
|
components: {
|
||||||
ModrinthIcon,
|
ModrinthIcon,
|
||||||
ListIcon,
|
ListIcon,
|
||||||
|
|||||||
@@ -9,25 +9,26 @@
|
|||||||
class="checkbox"
|
class="checkbox"
|
||||||
role="checkbox"
|
role="checkbox"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:class="{ checked: value, collapsing: collapsingToggleStyle }"
|
:class="{ checked: modelValue, collapsing: collapsingToggleStyle }"
|
||||||
:aria-label="description"
|
:aria-label="description ?? label"
|
||||||
:aria-checked="value"
|
:aria-checked="modelValue"
|
||||||
>
|
>
|
||||||
<CheckIcon v-if="value && !collapsingToggleStyle" aria-hidden="true" />
|
<CheckIcon v-if="modelValue && !collapsingToggleStyle" aria-hidden="true" />
|
||||||
<DropdownIcon v-else-if="collapsingToggleStyle" aria-hidden="true" />
|
<DropdownIcon v-else-if="collapsingToggleStyle" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
<!-- aria-hidden is set so screenreaders only use the <button>'s aria-label -->
|
<!-- aria-hidden is set so screenreaders only use the <button>'s aria-label -->
|
||||||
<p v-if="label" aria-hidden="true">{{ label }}</p>
|
<p v-if="label" aria-hidden="true">
|
||||||
|
{{ label }}
|
||||||
|
</p>
|
||||||
<slot v-else />
|
<slot v-else />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import CheckIcon from '~/assets/images/utils/check.svg?inline'
|
import CheckIcon from '~/assets/images/utils/check.svg'
|
||||||
import DropdownIcon from '~/assets/images/utils/dropdown.svg?inline'
|
import DropdownIcon from '~/assets/images/utils/dropdown.svg'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Checkbox',
|
|
||||||
components: {
|
components: {
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
DropdownIcon,
|
DropdownIcon,
|
||||||
@@ -43,9 +44,9 @@ export default {
|
|||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: null,
|
||||||
},
|
},
|
||||||
value: Boolean,
|
modelValue: Boolean,
|
||||||
clickEvent: {
|
clickEvent: {
|
||||||
type: Function,
|
type: Function,
|
||||||
default: () => {},
|
default: () => {},
|
||||||
@@ -55,10 +56,11 @@ export default {
|
|||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
emits: ['update:modelValue'],
|
||||||
methods: {
|
methods: {
|
||||||
toggle() {
|
toggle() {
|
||||||
if (!this.disabled) {
|
if (!this.disabled) {
|
||||||
this.$emit('input', !this.value)
|
this.$emit('update:modelValue', !this.modelValue)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -14,15 +14,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import CheckIcon from '~/assets/images/utils/check.svg?inline'
|
import CheckIcon from '~/assets/images/utils/check.svg'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Chips',
|
|
||||||
components: {
|
components: {
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
value: {
|
modelValue: {
|
||||||
required: true,
|
required: true,
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
@@ -39,13 +38,14 @@ export default {
|
|||||||
type: Function,
|
type: Function,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
emits: ['update:modelValue'],
|
||||||
computed: {
|
computed: {
|
||||||
selected: {
|
selected: {
|
||||||
get() {
|
get() {
|
||||||
return this.value
|
return this.modelValue
|
||||||
},
|
},
|
||||||
set(value) {
|
set(value) {
|
||||||
this.$emit('input', value)
|
this.$emit('update:modelValue', value)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<button
|
<button class="code" :class="{ copied }" title="Copy code to clipboard" @click="copyText">
|
||||||
class="code"
|
|
||||||
:class="{ copied }"
|
|
||||||
title="Copy code to clipboard"
|
|
||||||
@click="copyText"
|
|
||||||
>
|
|
||||||
{{ text }}
|
{{ text }}
|
||||||
<CheckIcon v-if="copied" />
|
<CheckIcon v-if="copied" />
|
||||||
<ClipboardCopyIcon v-else />
|
<ClipboardCopyIcon v-else />
|
||||||
@@ -12,11 +7,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import CheckIcon from '~/assets/images/utils/check.svg?inline'
|
import CheckIcon from '~/assets/images/utils/check.svg'
|
||||||
import ClipboardCopyIcon from '~/assets/images/utils/clipboard-copy.svg?inline'
|
import ClipboardCopyIcon from '~/assets/images/utils/clipboard-copy.svg'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'CopyCode',
|
|
||||||
components: {
|
components: {
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
ClipboardCopyIcon,
|
ClipboardCopyIcon,
|
||||||
@@ -54,8 +48,8 @@ export default {
|
|||||||
width: min-content;
|
width: min-content;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
user-select: text;
|
user-select: text;
|
||||||
transition: opacity 0.5s ease-in-out, filter 0.2s ease-in-out,
|
transition: opacity 0.5s ease-in-out, filter 0.2s ease-in-out, transform 0.05s ease-in-out,
|
||||||
transform 0.05s ease-in-out, outline 0.2s ease-in-out;
|
outline 0.2s ease-in-out;
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
width: 1em;
|
width: 1em;
|
||||||
|
|||||||
@@ -19,13 +19,13 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
name: 'DropArea',
|
|
||||||
props: {
|
props: {
|
||||||
accept: {
|
accept: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
emits: ['change'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
fileAllowed: false,
|
fileAllowed: false,
|
||||||
@@ -38,28 +38,25 @@ export default {
|
|||||||
allowDrag(event) {
|
allowDrag(event) {
|
||||||
const file = event.dataTransfer?.items[0]
|
const file = event.dataTransfer?.items[0]
|
||||||
|
|
||||||
console.log(file)
|
|
||||||
if (
|
if (
|
||||||
file &&
|
file &&
|
||||||
this.accept
|
this.accept
|
||||||
.split(',')
|
.split(',')
|
||||||
.reduce(
|
.reduce((acc, t) => acc || file.type.startsWith(t) || file.type === t || t === '*', false)
|
||||||
(acc, t) =>
|
|
||||||
acc || file.type.startsWith(t) || file.type === t || t === '*',
|
|
||||||
false
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
this.fileAllowed = true
|
this.fileAllowed = true
|
||||||
event.dataTransfer.dropEffect = 'copy'
|
event.dataTransfer.dropEffect = 'copy'
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|
||||||
if (this.$refs.drop_area)
|
if (this.$refs.drop_area) {
|
||||||
this.$refs.drop_area.style.visibility = 'visible'
|
this.$refs.drop_area.style.visibility = 'visible'
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.fileAllowed = false
|
this.fileAllowed = false
|
||||||
|
|
||||||
if (this.$refs.drop_area)
|
if (this.$refs.drop_area) {
|
||||||
this.$refs.drop_area.style.visibility = 'hidden'
|
this.$refs.drop_area.style.visibility = 'hidden'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -15,9 +15,7 @@
|
|||||||
<GlobeIcon aria-hidden="true" />
|
<GlobeIcon aria-hidden="true" />
|
||||||
Client or server
|
Client or server
|
||||||
</template>
|
</template>
|
||||||
<template
|
<template v-else-if="clientSide === 'required' && serverSide === 'required'">
|
||||||
v-else-if="clientSide === 'required' && serverSide === 'required'"
|
|
||||||
>
|
|
||||||
<GlobeIcon aria-hidden="true" />
|
<GlobeIcon aria-hidden="true" />
|
||||||
Client and server
|
Client and server
|
||||||
</template>
|
</template>
|
||||||
@@ -39,9 +37,7 @@
|
|||||||
<ServerIcon aria-hidden="true" />
|
<ServerIcon aria-hidden="true" />
|
||||||
Server
|
Server
|
||||||
</template>
|
</template>
|
||||||
<template
|
<template v-else-if="serverSide === 'unsupported' && clientSide === 'unsupported'">
|
||||||
v-else-if="serverSide === 'unsupported' && clientSide === 'unsupported'"
|
|
||||||
>
|
|
||||||
<GlobeIcon aria-hidden="true" />
|
<GlobeIcon aria-hidden="true" />
|
||||||
Unsupported
|
Unsupported
|
||||||
</template>
|
</template>
|
||||||
@@ -52,12 +48,11 @@
|
|||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import InfoIcon from '~/assets/images/utils/info.svg?inline'
|
import InfoIcon from '~/assets/images/utils/info.svg'
|
||||||
import ClientIcon from '~/assets/images/utils/client.svg?inline'
|
import ClientIcon from '~/assets/images/utils/client.svg'
|
||||||
import GlobeIcon from '~/assets/images/utils/globe.svg?inline'
|
import GlobeIcon from '~/assets/images/utils/globe.svg'
|
||||||
import ServerIcon from '~/assets/images/utils/server.svg?inline'
|
import ServerIcon from '~/assets/images/utils/server.svg'
|
||||||
export default {
|
export default {
|
||||||
name: 'EnvironmentIndicator',
|
|
||||||
components: {
|
components: {
|
||||||
InfoIcon,
|
InfoIcon,
|
||||||
ClientIcon,
|
ClientIcon,
|
||||||
|
|||||||
@@ -1,25 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<label
|
<label :class="{ 'long-style': longStyle }" @drop.prevent="handleDrop" @dragover.prevent>
|
||||||
:class="{ 'long-style': longStyle }"
|
<slot />
|
||||||
@drop.prevent="handleDrop"
|
|
||||||
@dragover.prevent
|
|
||||||
>
|
|
||||||
<slot></slot>
|
|
||||||
{{ prompt }}
|
{{ prompt }}
|
||||||
<input
|
<input type="file" :multiple="multiple" :accept="accept" @change="handleChange" />
|
||||||
type="file"
|
|
||||||
:multiple="multiple"
|
|
||||||
:accept="accept"
|
|
||||||
@change="handleChange"
|
|
||||||
/>
|
|
||||||
</label>
|
</label>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { fileIsValid } from '~/plugins/fileUtils'
|
import { fileIsValid } from '~/helpers/fileUtils'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FileInput',
|
|
||||||
components: {},
|
components: {},
|
||||||
props: {
|
props: {
|
||||||
prompt: {
|
prompt: {
|
||||||
@@ -54,6 +44,7 @@ export default {
|
|||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
emits: ['change'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
files: [],
|
files: [],
|
||||||
@@ -61,12 +52,12 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
addFiles(files, shouldNotReset) {
|
addFiles(files, shouldNotReset) {
|
||||||
if (!shouldNotReset || this.shouldAlwaysReset) this.files = files
|
if (!shouldNotReset || this.shouldAlwaysReset) {
|
||||||
|
this.files = files
|
||||||
|
}
|
||||||
|
|
||||||
const validationOptions = { maxSize: this.maxSize, alertOnInvalid: true }
|
const validationOptions = { maxSize: this.maxSize, alertOnInvalid: true }
|
||||||
this.files = [...this.files].filter((file) =>
|
this.files = [...this.files].filter((file) => fileIsValid(file, validationOptions))
|
||||||
fileIsValid(file, validationOptions)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (this.files.length > 0) {
|
if (this.files.length > 0) {
|
||||||
this.$emit('change', this.files)
|
this.$emit('change', this.files)
|
||||||
|
|||||||
@@ -8,25 +8,26 @@
|
|||||||
class="modal-overlay"
|
class="modal-overlay"
|
||||||
@click="hide"
|
@click="hide"
|
||||||
/>
|
/>
|
||||||
<div class="modal-body" :class="{ shown: shown }">
|
<div class="modal-container" :class="{ shown }">
|
||||||
<div v-if="header" class="header">
|
<div class="modal-body">
|
||||||
<h1>{{ header }}</h1>
|
<div v-if="header" class="header">
|
||||||
<button class="iconified-button icon-only transparent" @click="hide">
|
<h1>{{ header }}</h1>
|
||||||
<CrossIcon />
|
<button class="iconified-button icon-only transparent" @click="hide">
|
||||||
</button>
|
<CrossIcon />
|
||||||
</div>
|
</button>
|
||||||
<div class="content">
|
</div>
|
||||||
<slot></slot>
|
<div class="content">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import CrossIcon from '~/assets/images/utils/x.svg?inline'
|
import CrossIcon from '~/assets/images/utils/x.svg'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Modal',
|
|
||||||
components: {
|
components: {
|
||||||
CrossIcon,
|
CrossIcon,
|
||||||
},
|
},
|
||||||
@@ -61,7 +62,6 @@ export default {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
z-index: 20;
|
z-index: 20;
|
||||||
|
|
||||||
transition: all 0.3s ease-in-out;
|
transition: all 0.3s ease-in-out;
|
||||||
|
|
||||||
&.shown {
|
&.shown {
|
||||||
@@ -76,46 +76,61 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-body {
|
.modal-container {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
left: 50%;
|
top: 0;
|
||||||
transform: translate(-50%, -50%);
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
z-index: 21;
|
z-index: 21;
|
||||||
box-shadow: var(--shadow-raised), var(--shadow-inset);
|
visibility: hidden;
|
||||||
border-radius: var(--size-rounded-lg);
|
pointer-events: none;
|
||||||
max-height: calc(100% - 2 * var(--spacing-card-bg));
|
|
||||||
overflow-y: auto;
|
|
||||||
width: 600px;
|
|
||||||
|
|
||||||
.header {
|
&.shown {
|
||||||
display: flex;
|
visibility: visible;
|
||||||
justify-content: space-between;
|
.modal-body {
|
||||||
align-items: center;
|
opacity: 1;
|
||||||
background-color: var(--color-bg);
|
visibility: visible;
|
||||||
padding: var(--spacing-card-md) var(--spacing-card-lg);
|
transform: translateY(0);
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.modal-body {
|
||||||
background-color: var(--color-raised-bg);
|
position: fixed;
|
||||||
}
|
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;
|
||||||
|
pointer-events: auto;
|
||||||
|
|
||||||
top: calc(100% + 400px);
|
.header {
|
||||||
visibility: hidden;
|
display: flex;
|
||||||
opacity: 0;
|
justify-content: space-between;
|
||||||
transition: all 0.25s ease-in-out;
|
align-items: center;
|
||||||
|
background-color: var(--color-bg);
|
||||||
|
padding: var(--spacing-card-md) var(--spacing-card-lg);
|
||||||
|
|
||||||
&.shown {
|
h1 {
|
||||||
opacity: 1;
|
font-size: 1.25rem;
|
||||||
visibility: visible;
|
}
|
||||||
top: 50%;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 650px) {
|
.content {
|
||||||
width: calc(100% - 2 * var(--spacing-card-bg));
|
background-color: var(--color-raised-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
transform: translateY(50vh);
|
||||||
|
visibility: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.25s ease-in-out;
|
||||||
|
|
||||||
|
@media screen and (max-width: 650px) {
|
||||||
|
width: calc(100% - 2 * var(--spacing-card-bg));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<Modal ref="modal" :header="title">
|
<Modal ref="modal" :header="title">
|
||||||
<div class="modal-delete">
|
<div class="modal-delete">
|
||||||
<div class="markdown-body" v-html="$xss($md.render(description))"></div>
|
<div class="markdown-body" v-html="renderString(description)" />
|
||||||
<label v-if="hasToType" for="confirmation" class="confirmation-label">
|
<label v-if="hasToType" for="confirmation" class="confirmation-label">
|
||||||
<span>
|
<span>
|
||||||
<strong>To verify, type</strong>
|
<strong>To verify, type</strong>
|
||||||
@@ -24,11 +24,7 @@
|
|||||||
<CrossIcon />
|
<CrossIcon />
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button class="iconified-button danger-button" :disabled="action_disabled" @click="proceed">
|
||||||
class="iconified-button danger-button"
|
|
||||||
:disabled="action_disabled"
|
|
||||||
@click="proceed"
|
|
||||||
>
|
|
||||||
<TrashIcon />
|
<TrashIcon />
|
||||||
{{ proceedLabel }}
|
{{ proceedLabel }}
|
||||||
</button>
|
</button>
|
||||||
@@ -38,12 +34,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import CrossIcon from '~/assets/images/utils/x.svg?inline'
|
import CrossIcon from '~/assets/images/utils/x.svg'
|
||||||
import TrashIcon from '~/assets/images/utils/trash.svg?inline'
|
import TrashIcon from '~/assets/images/utils/trash.svg'
|
||||||
import Modal from '~/components/ui/Modal'
|
import Modal from '~/components/ui/Modal'
|
||||||
|
import { renderString } from '~/helpers/parse'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ModalConfirm',
|
|
||||||
components: {
|
components: {
|
||||||
CrossIcon,
|
CrossIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
@@ -73,6 +69,7 @@ export default {
|
|||||||
default: 'Proceed',
|
default: 'Proceed',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
emits: ['proceed'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
action_disabled: this.hasToType,
|
action_disabled: this.hasToType,
|
||||||
@@ -80,6 +77,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
renderString,
|
||||||
cancel() {
|
cancel() {
|
||||||
this.$refs.modal.hide()
|
this.$refs.modal.hide()
|
||||||
},
|
},
|
||||||
@@ -90,8 +88,7 @@ export default {
|
|||||||
type() {
|
type() {
|
||||||
if (this.hasToType) {
|
if (this.hasToType) {
|
||||||
this.action_disabled =
|
this.action_disabled =
|
||||||
this.confirmation_typed.toLowerCase() !==
|
this.confirmation_typed.toLowerCase() !== this.confirmationText.toLowerCase()
|
||||||
this.confirmationText.toLowerCase()
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
show() {
|
show() {
|
||||||
|
|||||||
@@ -2,15 +2,10 @@
|
|||||||
<Modal ref="modal" header="Create a project">
|
<Modal ref="modal" header="Create a project">
|
||||||
<div class="modal-creation universal-labels">
|
<div class="modal-creation universal-labels">
|
||||||
<div class="markdown-body">
|
<div class="markdown-body">
|
||||||
<p>
|
<p>New projects are created as drafts and can be found under your profile page.</p>
|
||||||
New projects are created as drafts and can be found under your profile
|
|
||||||
page.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<label for="project-type">
|
<label for="project-type">
|
||||||
<span class="label__title"
|
<span class="label__title">Project type<span class="required">*</span></span>
|
||||||
>Project type<span class="required">*</span></span
|
|
||||||
>
|
|
||||||
</label>
|
</label>
|
||||||
<Chips
|
<Chips
|
||||||
id="project-type"
|
id="project-type"
|
||||||
@@ -34,9 +29,7 @@
|
|||||||
</label>
|
</label>
|
||||||
<div class="text-input-wrapper">
|
<div class="text-input-wrapper">
|
||||||
<div class="text-input-wrapper__before">
|
<div class="text-input-wrapper__before">
|
||||||
https://modrinth.com/{{
|
https://modrinth.com/{{ getProjectType() ? getProjectType().id : '???' }}/
|
||||||
getProjectType() ? getProjectType().id : '???'
|
|
||||||
}}/
|
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
id="slug"
|
id="slug"
|
||||||
@@ -50,16 +43,11 @@
|
|||||||
<label for="additional-information">
|
<label for="additional-information">
|
||||||
<span class="label__title">Summary<span class="required">*</span></span>
|
<span class="label__title">Summary<span class="required">*</span></span>
|
||||||
<span class="label__description"
|
<span class="label__description"
|
||||||
>This appears in search and on the sidebar of your project's
|
>This appears in search and on the sidebar of your project's page.</span
|
||||||
page.</span
|
|
||||||
>
|
>
|
||||||
</label>
|
</label>
|
||||||
<div class="textarea-wrapper">
|
<div class="textarea-wrapper">
|
||||||
<textarea
|
<textarea id="additional-information" v-model="description" maxlength="256" />
|
||||||
id="additional-information"
|
|
||||||
v-model="description"
|
|
||||||
maxlength="256"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="push-right input-group">
|
<div class="push-right input-group">
|
||||||
<button class="iconified-button" @click="cancel">
|
<button class="iconified-button" @click="cancel">
|
||||||
@@ -76,13 +64,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import CrossIcon from '~/assets/images/utils/x.svg?inline'
|
import CrossIcon from '~/assets/images/utils/x.svg'
|
||||||
import CheckIcon from '~/assets/images/utils/right-arrow.svg?inline'
|
import CheckIcon from '~/assets/images/utils/right-arrow.svg'
|
||||||
import Modal from '~/components/ui/Modal'
|
import Modal from '~/components/ui/Modal'
|
||||||
import Chips from '~/components/ui/Chips'
|
import Chips from '~/components/ui/Chips'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ModalCreation',
|
|
||||||
components: {
|
components: {
|
||||||
Chips,
|
Chips,
|
||||||
CrossIcon,
|
CrossIcon,
|
||||||
@@ -144,7 +131,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async createProject() {
|
async createProject() {
|
||||||
this.$nuxt.$loading.start()
|
startLoading()
|
||||||
|
|
||||||
const projectType = this.getProjectType()
|
const projectType = this.getProjectType()
|
||||||
|
|
||||||
@@ -175,12 +162,11 @@ export default {
|
|||||||
)
|
)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.$axios({
|
await useBaseFetch('project', {
|
||||||
url: 'project',
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: formData,
|
body: formData,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'multipart/form-data',
|
'Content-Disposition': formData,
|
||||||
Authorization: this.$auth.token,
|
Authorization: this.$auth.token,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -191,6 +177,8 @@ export default {
|
|||||||
params: {
|
params: {
|
||||||
type: projectType.id,
|
type: projectType.id,
|
||||||
id: this.slug,
|
id: this.slug,
|
||||||
|
},
|
||||||
|
state: {
|
||||||
overrideProjectType: projectType.id,
|
overrideProjectType: projectType.id,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -198,11 +186,11 @@ export default {
|
|||||||
this.$notify({
|
this.$notify({
|
||||||
group: 'main',
|
group: 'main',
|
||||||
title: 'An error occurred',
|
title: 'An error occurred',
|
||||||
text: err.response.data.description,
|
text: err.data.description,
|
||||||
type: 'error',
|
type: 'error',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
this.$nuxt.$loading.finish()
|
stopLoading()
|
||||||
},
|
},
|
||||||
show() {
|
show() {
|
||||||
this.projectType = this.$tag.projectTypes[0].display
|
this.projectType = this.$tag.projectTypes[0].display
|
||||||
|
|||||||
@@ -2,27 +2,18 @@
|
|||||||
<Modal ref="modal" header="Project moderation">
|
<Modal ref="modal" header="Project moderation">
|
||||||
<div v-if="project !== null" class="moderation-modal universal-body">
|
<div v-if="project !== null" class="moderation-modal universal-body">
|
||||||
<p>
|
<p>
|
||||||
A moderation message is optional, but it can be used to communicate
|
A moderation message is optional, but it can be used to communicate problems with a
|
||||||
problems with a project's team members. The body is also optional and
|
project's team members. The body is also optional and supports markdown formatting!
|
||||||
supports markdown formatting!
|
|
||||||
</p>
|
</p>
|
||||||
<div v-if="status" class="status">
|
<div v-if="status" class="status">
|
||||||
<span>New project status: </span>
|
<span>New project status: </span>
|
||||||
<Badge :type="status" />
|
<Badge :type="status" />
|
||||||
</div>
|
</div>
|
||||||
<h3>Message title</h3>
|
<h3>Message title</h3>
|
||||||
<input
|
<input v-model="moderationMessage" type="text" placeholder="Enter the message..." />
|
||||||
v-model="moderationMessage"
|
|
||||||
type="text"
|
|
||||||
placeholder="Enter the message..."
|
|
||||||
/>
|
|
||||||
<h3>Message body</h3>
|
<h3>Message body</h3>
|
||||||
<div class="textarea-wrapper">
|
<div class="textarea-wrapper">
|
||||||
<Chips
|
<Chips v-model="bodyViewMode" class="separator" :items="['source', 'preview']" />
|
||||||
v-model="bodyViewMode"
|
|
||||||
class="separator"
|
|
||||||
:items="['source', 'preview']"
|
|
||||||
/>
|
|
||||||
<textarea
|
<textarea
|
||||||
v-if="bodyViewMode === 'source'"
|
v-if="bodyViewMode === 'source'"
|
||||||
id="body"
|
id="body"
|
||||||
@@ -34,20 +25,17 @@
|
|||||||
: 'You must add a title before you add a body.'
|
: 'You must add a title before you add a body.'
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<div
|
<div v-else class="markdown-body preview" v-html="renderString(moderationMessageBody)" />
|
||||||
v-else
|
|
||||||
v-highlightjs
|
|
||||||
class="markdown-body preview"
|
|
||||||
v-html="$xss($md.render(moderationMessageBody))"
|
|
||||||
></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="push-right input-group">
|
<div class="push-right input-group">
|
||||||
<button
|
<button
|
||||||
v-if="moderationMessage || moderationMessageBody"
|
v-if="moderationMessage || moderationMessageBody"
|
||||||
class="iconified-button"
|
class="iconified-button"
|
||||||
@click="
|
@click="
|
||||||
moderationMessage = ''
|
() => {
|
||||||
moderationMessageBody = ''
|
moderationMessage = ''
|
||||||
|
moderationMessageBody = ''
|
||||||
|
}
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<TrashIcon />
|
<TrashIcon />
|
||||||
@@ -67,15 +55,15 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import TrashIcon from '~/assets/images/utils/trash.svg?inline'
|
import TrashIcon from '~/assets/images/utils/trash.svg'
|
||||||
import CrossIcon from '~/assets/images/utils/x.svg?inline'
|
import CrossIcon from '~/assets/images/utils/x.svg'
|
||||||
import Modal from '~/components/ui/Modal'
|
import Modal from '~/components/ui/Modal'
|
||||||
import Chips from '~/components/ui/Chips'
|
import Chips from '~/components/ui/Chips'
|
||||||
import Badge from '~/components/ui/Badge'
|
import Badge from '~/components/ui/Badge'
|
||||||
import CheckIcon from '~/assets/images/utils/check.svg?inline'
|
import CheckIcon from '~/assets/images/utils/check.svg'
|
||||||
|
import { renderString } from '~/helpers/parse'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ModalModeration',
|
|
||||||
components: {
|
components: {
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
CrossIcon,
|
CrossIcon,
|
||||||
@@ -102,9 +90,7 @@ export default {
|
|||||||
return {
|
return {
|
||||||
bodyViewMode: 'source',
|
bodyViewMode: 'source',
|
||||||
moderationMessage:
|
moderationMessage:
|
||||||
this.project && this.project.moderation_message
|
this.project && this.project.moderation_message ? this.project.moderation_message : '',
|
||||||
? this.project.moderation_message
|
|
||||||
: '',
|
|
||||||
moderationMessageBody:
|
moderationMessageBody:
|
||||||
this.project && this.project.moderation_message_body
|
this.project && this.project.moderation_message_body
|
||||||
? this.project.moderation_message_body
|
? this.project.moderation_message_body
|
||||||
@@ -112,26 +98,23 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
renderString,
|
||||||
async saveProject() {
|
async saveProject() {
|
||||||
this.$nuxt.$loading.start()
|
startLoading()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = {
|
const data = {
|
||||||
moderation_message: this.moderationMessage
|
moderation_message: this.moderationMessage ? this.moderationMessage : null,
|
||||||
? this.moderationMessage
|
moderation_message_body: this.moderationMessageBody ? this.moderationMessageBody : null,
|
||||||
: null,
|
|
||||||
moderation_message_body: this.moderationMessageBody
|
|
||||||
? this.moderationMessageBody
|
|
||||||
: null,
|
|
||||||
}
|
}
|
||||||
if (this.status) {
|
if (this.status) {
|
||||||
data.status = this.status
|
data.status = this.status
|
||||||
}
|
}
|
||||||
await this.$axios.patch(
|
await useBaseFetch(`project/${this.project.id}`, {
|
||||||
`project/${this.project.id}`,
|
method: 'PATCH',
|
||||||
data,
|
body: data,
|
||||||
this.$defaultHeaders()
|
...this.$defaultHeaders(),
|
||||||
)
|
})
|
||||||
|
|
||||||
this.$refs.modal.hide()
|
this.$refs.modal.hide()
|
||||||
if (this.onClose !== null) {
|
if (this.onClose !== null) {
|
||||||
@@ -141,25 +124,21 @@ export default {
|
|||||||
this.$notify({
|
this.$notify({
|
||||||
group: 'main',
|
group: 'main',
|
||||||
title: 'An error occurred',
|
title: 'An error occurred',
|
||||||
text: err.response ? err.response.data.description : err,
|
text: err.data ? err.data.description : err,
|
||||||
type: 'error',
|
type: 'error',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$nuxt.$loading.finish()
|
stopLoading()
|
||||||
},
|
},
|
||||||
show() {
|
show() {
|
||||||
this.$refs.modal.show()
|
this.$refs.modal.show()
|
||||||
this.moderationMessage =
|
this.moderationMessage =
|
||||||
this.project &&
|
this.project && this.project.moderator_message && this.project.moderator_message.message
|
||||||
this.project.moderator_message &&
|
|
||||||
this.project.moderator_message.message
|
|
||||||
? this.project.moderator_message.message
|
? this.project.moderator_message.message
|
||||||
: ''
|
: ''
|
||||||
this.moderationMessageBody =
|
this.moderationMessageBody =
|
||||||
this.project &&
|
this.project && this.project.moderator_message && this.project.moderator_message.body
|
||||||
this.project.moderator_message &&
|
|
||||||
this.project.moderator_message.body
|
|
||||||
? this.project.moderator_message.body
|
? this.project.moderator_message.body
|
||||||
: ''
|
: ''
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,18 +3,16 @@
|
|||||||
<div class="modal-report legacy-label-styles">
|
<div class="modal-report legacy-label-styles">
|
||||||
<div class="markdown-body">
|
<div class="markdown-body">
|
||||||
<p>
|
<p>
|
||||||
Modding should be safe for everyone, so we take abuse and malicious
|
Modding should be safe for everyone, so we take abuse and malicious intent seriously at
|
||||||
intent seriously at Modrinth. We want to hear about harmful content on
|
Modrinth. We want to hear about harmful content on the site that violates our
|
||||||
the site that violates our
|
<nuxt-link to="/legal/terms"> ToS </nuxt-link> and
|
||||||
<nuxt-link to="/legal/terms">ToS</nuxt-link> and
|
<nuxt-link to="/legal/rules"> Rules </nuxt-link>. Rest assured, we’ll keep your
|
||||||
<nuxt-link to="/legal/rules">Rules</nuxt-link>. Rest assured, we’ll
|
identifying information private.
|
||||||
keep your identifying information private.
|
|
||||||
</p>
|
</p>
|
||||||
<p v-if="itemType === 'project' || itemType === 'version'">
|
<p v-if="itemType === 'project' || itemType === 'version'">
|
||||||
Please <strong>do not</strong> use this to report bugs with the
|
Please <strong>do not</strong> use this to report bugs with the project itself. This form
|
||||||
project itself. This form is only for submitting a report to Modrinth
|
is only for submitting a report to Modrinth staff. If the project has an Issues link or a
|
||||||
staff. If the project has an Issues link or a Discord invite, consider
|
Discord invite, consider reporting it there.
|
||||||
reporting it there.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<label class="report-label" for="report-type">
|
<label class="report-label" for="report-type">
|
||||||
@@ -25,10 +23,8 @@
|
|||||||
<multiselect
|
<multiselect
|
||||||
id="report-type"
|
id="report-type"
|
||||||
v-model="reportType"
|
v-model="reportType"
|
||||||
:options="$store.state.tag.reportTypes"
|
:options="$tag.reportTypes"
|
||||||
:custom-label="
|
:custom-label="(value) => value.charAt(0).toUpperCase() + value.slice(1)"
|
||||||
(value) => value.charAt(0).toUpperCase() + value.slice(1)
|
|
||||||
"
|
|
||||||
:multiple="false"
|
:multiple="false"
|
||||||
:searchable="false"
|
:searchable="false"
|
||||||
:show-no-results="false"
|
:show-no-results="false"
|
||||||
@@ -37,26 +33,14 @@
|
|||||||
/>
|
/>
|
||||||
<label class="report-label" for="additional-information">
|
<label class="report-label" for="additional-information">
|
||||||
<strong>Additional information</strong>
|
<strong>Additional information</strong>
|
||||||
<span>
|
<span> Include links and images if possible. Markdown formatting is supported. </span>
|
||||||
Include links and images if possible. Markdown formatting is
|
|
||||||
supported.
|
|
||||||
</span>
|
|
||||||
</label>
|
</label>
|
||||||
<div class="textarea-wrapper">
|
<div class="textarea-wrapper">
|
||||||
<Chips
|
<Chips v-model="bodyViewType" class="separator" :items="['source', 'preview']" />
|
||||||
v-model="bodyViewType"
|
|
||||||
class="separator"
|
|
||||||
:items="['source', 'preview']"
|
|
||||||
/>
|
|
||||||
<div v-if="bodyViewType === 'source'" class="textarea-wrapper">
|
<div v-if="bodyViewType === 'source'" class="textarea-wrapper">
|
||||||
<textarea id="body" v-model="body" spellcheck="true" />
|
<textarea id="body" v-model="body" spellcheck="true" />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div v-else class="preview" v-html="renderString(body)" />
|
||||||
v-else
|
|
||||||
v-highlightjs
|
|
||||||
class="preview"
|
|
||||||
v-html="$xss($md.render(body))"
|
|
||||||
></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="button-group">
|
<div class="button-group">
|
||||||
<button class="iconified-button" @click="cancel">
|
<button class="iconified-button" @click="cancel">
|
||||||
@@ -74,13 +58,13 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Multiselect from 'vue-multiselect'
|
import Multiselect from 'vue-multiselect'
|
||||||
import CrossIcon from '~/assets/images/utils/x.svg?inline'
|
import CrossIcon from '~/assets/images/utils/x.svg'
|
||||||
import CheckIcon from '~/assets/images/utils/check.svg?inline'
|
import CheckIcon from '~/assets/images/utils/check.svg'
|
||||||
import Modal from '~/components/ui/Modal'
|
import Modal from '~/components/ui/Modal'
|
||||||
import Chips from '~/components/ui/Chips'
|
import Chips from '~/components/ui/Chips'
|
||||||
|
import { renderString } from '~/helpers/parse'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ModalReport',
|
|
||||||
components: {
|
components: {
|
||||||
Chips,
|
Chips,
|
||||||
CrossIcon,
|
CrossIcon,
|
||||||
@@ -106,6 +90,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
renderString,
|
||||||
cancel() {
|
cancel() {
|
||||||
this.reportType = ''
|
this.reportType = ''
|
||||||
this.body = ''
|
this.body = ''
|
||||||
@@ -114,7 +99,7 @@ export default {
|
|||||||
this.$refs.modal.hide()
|
this.$refs.modal.hide()
|
||||||
},
|
},
|
||||||
async submitReport() {
|
async submitReport() {
|
||||||
this.$nuxt.$loading.start()
|
startLoading()
|
||||||
try {
|
try {
|
||||||
const data = {
|
const data = {
|
||||||
report_type: this.reportType,
|
report_type: this.reportType,
|
||||||
@@ -122,18 +107,22 @@ export default {
|
|||||||
item_type: this.itemType,
|
item_type: this.itemType,
|
||||||
body: this.body,
|
body: this.body,
|
||||||
}
|
}
|
||||||
await this.$axios.post('report', data, this.$defaultHeaders())
|
await useBaseFetch('report', {
|
||||||
|
method: 'POST',
|
||||||
|
body: data,
|
||||||
|
...this.$defaultHeaders(),
|
||||||
|
})
|
||||||
|
|
||||||
this.$refs.modal.hide()
|
this.$refs.modal.hide()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.$notify({
|
this.$notify({
|
||||||
group: 'main',
|
group: 'main',
|
||||||
title: 'An error occurred',
|
title: 'An error occurred',
|
||||||
text: err.response.data.description,
|
text: err.data.description,
|
||||||
type: 'error',
|
type: 'error',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
this.$nuxt.$loading.finish()
|
stopLoading()
|
||||||
},
|
},
|
||||||
show() {
|
show() {
|
||||||
this.$refs.modal.show()
|
this.$refs.modal.show()
|
||||||
|
|||||||
@@ -2,9 +2,8 @@
|
|||||||
<Modal ref="modal" :header="'Transfer to ' + $formatWallet(wallet)">
|
<Modal ref="modal" :header="'Transfer to ' + $formatWallet(wallet)">
|
||||||
<div class="modal-transfer">
|
<div class="modal-transfer">
|
||||||
<span
|
<span
|
||||||
>You are initiating a transfer of your revenue from Modrinth's Creator
|
>You are initiating a transfer of your revenue from Modrinth's Creator Monetization Program.
|
||||||
Monetization Program. How much of your
|
How much of your <strong>{{ $formatMoney(balance) }}</strong> balance would you like to
|
||||||
<strong>{{ $formatMoney(balance) }}</strong> balance would you like to
|
|
||||||
transfer?</span
|
transfer?</span
|
||||||
>
|
>
|
||||||
<div class="confirmation-input">
|
<div class="confirmation-input">
|
||||||
@@ -19,40 +18,30 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="confirm-text">
|
<div class="confirm-text">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
v-if="
|
v-if="isValidInput() && parseInput() >= minWithdraw && parseInput() <= balance"
|
||||||
isValidInput() &&
|
|
||||||
parseInput() >= minWithdraw &&
|
|
||||||
parseInput() <= balance
|
|
||||||
"
|
|
||||||
v-model="consentedFee"
|
v-model="consentedFee"
|
||||||
|
description="Consent to fee"
|
||||||
>
|
>
|
||||||
<template v-if="wallet === 'venmo'"
|
<template v-if="wallet === 'venmo'">
|
||||||
>I acknowledge that $0.25 will be deducted from the amount I receive
|
I acknowledge that $0.25 will be deducted from the amount I receive to cover
|
||||||
to cover {{ $formatWallet(wallet) }} processing fees.</template
|
{{ $formatWallet(wallet) }} processing fees.
|
||||||
>
|
</template>
|
||||||
<template v-else
|
<template v-else>
|
||||||
>I acknowledge that an estimated
|
I acknowledge that an estimated
|
||||||
{{ $formatMoney(calcProcessingFees()) }} will be deducted from the
|
{{ $formatMoney(calcProcessingFees()) }} will be deducted from the amount I receive to
|
||||||
amount I receive to cover {{ $formatWallet(wallet) }} processing
|
cover {{ $formatWallet(wallet) }} processing fees and that any excess will be returned
|
||||||
fees and that any excess will be returned to my Modrinth
|
to my Modrinth balance.
|
||||||
balance.</template
|
</template>
|
||||||
>
|
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
v-if="
|
v-if="isValidInput() && parseInput() >= minWithdraw && parseInput() <= balance"
|
||||||
isValidInput() &&
|
|
||||||
parseInput() >= minWithdraw &&
|
|
||||||
parseInput() <= balance
|
|
||||||
"
|
|
||||||
v-model="consentedAccount"
|
v-model="consentedAccount"
|
||||||
|
description="Confirm transfer"
|
||||||
>
|
>
|
||||||
I confirm that I an initiating a transfer to the following
|
I confirm that I an initiating a transfer to the following
|
||||||
{{ $formatWallet(wallet) }} account: {{ account }}
|
{{ $formatWallet(wallet) }} account: {{ account }}
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
<span
|
<span v-else-if="validInput && parseInput() < minWithdraw" class="invalid">
|
||||||
v-else-if="validInput && parseInput() < minWithdraw"
|
|
||||||
class="invalid"
|
|
||||||
>
|
|
||||||
The amount must be at least {{ $formatMoney(minWithdraw) }}</span
|
The amount must be at least {{ $formatMoney(minWithdraw) }}</span
|
||||||
>
|
>
|
||||||
<span v-else-if="validInput && parseInput() > balance" class="invalid">
|
<span v-else-if="validInput && parseInput() > balance" class="invalid">
|
||||||
@@ -84,14 +73,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import CrossIcon from '~/assets/images/utils/x.svg?inline'
|
import CrossIcon from '~/assets/images/utils/x.svg'
|
||||||
import TransferIcon from '~/assets/images/utils/transfer.svg?inline'
|
import TransferIcon from '~/assets/images/utils/transfer.svg'
|
||||||
import SettingsIcon from '~/assets/images/utils/settings.svg?inline'
|
import SettingsIcon from '~/assets/images/utils/settings.svg'
|
||||||
import Modal from '~/components/ui/Modal'
|
import Modal from '~/components/ui/Modal'
|
||||||
import Checkbox from '~/components/ui/Checkbox'
|
import Checkbox from '~/components/ui/Checkbox'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ModalTransfer',
|
|
||||||
components: {
|
components: {
|
||||||
Checkbox,
|
Checkbox,
|
||||||
CrossIcon,
|
CrossIcon,
|
||||||
@@ -138,29 +126,27 @@ export default {
|
|||||||
this.$refs.modal.hide()
|
this.$refs.modal.hide()
|
||||||
},
|
},
|
||||||
async proceed() {
|
async proceed() {
|
||||||
this.$nuxt.$loading.start()
|
startLoading()
|
||||||
try {
|
try {
|
||||||
await this.$axios.post(
|
await useBaseFetch(`user/${this.$auth.user.id}/payouts`, {
|
||||||
`user/${this.$auth.user.id}/payouts`,
|
method: 'POST',
|
||||||
{
|
body: {
|
||||||
amount: Number(this.amount.replace('$', '')),
|
amount: Number(this.amount.replace('$', '')),
|
||||||
},
|
},
|
||||||
this.$defaultHeaders()
|
...this.$defaultHeaders(),
|
||||||
)
|
|
||||||
await this.$store.dispatch('auth/fetchUser', {
|
|
||||||
token: this.$auth.token,
|
|
||||||
})
|
})
|
||||||
|
await useAuth(this.$auth.token)
|
||||||
|
|
||||||
this.$refs.modal.hide()
|
this.$refs.modal.hide()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.$notify({
|
this.$notify({
|
||||||
group: 'main',
|
group: 'main',
|
||||||
title: 'An error occurred',
|
title: 'An error occurred',
|
||||||
text: err.response.data.description,
|
text: err.data.description,
|
||||||
type: 'error',
|
type: 'error',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
this.$nuxt.$loading.finish()
|
stopLoading()
|
||||||
},
|
},
|
||||||
show() {
|
show() {
|
||||||
this.$refs.modal.show()
|
this.$refs.modal.show()
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<nav class="navigation" :class="{ 'use-animation': useAnimation }">
|
<nav class="navigation">
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
v-for="(link, index) in filteredLinks"
|
v-for="(link, index) in filteredLinks"
|
||||||
v-show="link.shown === undefined ? true : link.shown"
|
v-show="link.shown === undefined ? true : link.shown"
|
||||||
@@ -7,32 +7,24 @@
|
|||||||
ref="linkElements"
|
ref="linkElements"
|
||||||
:to="query ? (link.href ? `?${query}=${link.href}` : '?') : link.href"
|
:to="query ? (link.href ? `?${query}=${link.href}` : '?') : link.href"
|
||||||
class="nav-link button-animation"
|
class="nav-link button-animation"
|
||||||
:class="{ 'is-active': index === activeIndex }"
|
|
||||||
>
|
>
|
||||||
<span>{{ link.label }}</span>
|
<span>{{ link.label }}</span>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="nav-indicator"
|
class="nav-indicator"
|
||||||
:style="`visibility: ${
|
:style="{
|
||||||
useAnimation && activeIndex !== -1 ? 'visible' : 'hidden'
|
left: positionToMoveX,
|
||||||
}; left: ${indicator.left}px; right: ${indicator.right}px;
|
top: positionToMoveY,
|
||||||
top: ${indicator.top}px; transition: left 350ms ${
|
width: sliderWidth,
|
||||||
indicator.direction === 'left'
|
opacity: activeIndex === -1 ? 0 : 1,
|
||||||
? 'cubic-bezier(1,0,.3,1) -140ms'
|
}"
|
||||||
: 'cubic-bezier(.75,-0.01,.24,.99) -40ms'
|
aria-hidden="true"
|
||||||
},right 350ms ${
|
></div>
|
||||||
indicator.direction === 'right'
|
|
||||||
? 'cubic-bezier(1,0,.3,1) -140ms'
|
|
||||||
: 'cubic-bezier(.75,-0.01,.24,.99) -40ms'
|
|
||||||
}, top 100ms ease-in-out`"
|
|
||||||
/>
|
|
||||||
</nav>
|
</nav>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
name: 'NavRow',
|
|
||||||
props: {
|
props: {
|
||||||
links: {
|
links: {
|
||||||
default: () => [],
|
default: () => [],
|
||||||
@@ -45,21 +37,26 @@ export default {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
useAnimation: false,
|
sliderPositionX: 0,
|
||||||
oldIndex: -1,
|
sliderPositionY: 18,
|
||||||
|
selectedElementWidth: 0,
|
||||||
activeIndex: -1,
|
activeIndex: -1,
|
||||||
indicator: {
|
oldIndex: -1,
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
top: 22,
|
|
||||||
direction: 'right',
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
filteredLinks() {
|
filteredLinks() {
|
||||||
return this.links.filter((x) => (x.shown === undefined ? true : x.shown))
|
return this.links.filter((x) => (x.shown === undefined ? true : x.shown))
|
||||||
},
|
},
|
||||||
|
positionToMoveX() {
|
||||||
|
return `${this.sliderPositionX}px`
|
||||||
|
},
|
||||||
|
positionToMoveY() {
|
||||||
|
return `${this.sliderPositionY}px`
|
||||||
|
},
|
||||||
|
sliderWidth() {
|
||||||
|
return `${this.selectedElementWidth}px`
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
'$route.path': {
|
'$route.path': {
|
||||||
@@ -74,53 +71,34 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
window.addEventListener('resize', this.pickLink)
|
||||||
this.pickLink()
|
this.pickLink()
|
||||||
},
|
},
|
||||||
|
unmounted() {
|
||||||
|
window.removeEventListener('resize', this.pickLink)
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
pickLink() {
|
pickLink() {
|
||||||
if (this.oldIndex === -1) {
|
|
||||||
this.useAnimation = false
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
this.useAnimation = true
|
|
||||||
}, 300)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.activeIndex = this.query
|
this.activeIndex = this.query
|
||||||
? this.filteredLinks.findIndex(
|
? this.filteredLinks.findIndex(
|
||||||
(x) =>
|
(x) => (x.href === '' ? undefined : x.href) === this.$route.path[this.query]
|
||||||
(x.href === '' ? undefined : x.href) ===
|
|
||||||
this.$route.query[this.query]
|
|
||||||
)
|
|
||||||
: this.filteredLinks.findIndex(
|
|
||||||
(x) => x.href === decodeURIComponent(this.$route.path)
|
|
||||||
)
|
)
|
||||||
|
: this.filteredLinks.findIndex((x) => x.href === decodeURIComponent(this.$route.path))
|
||||||
|
|
||||||
if (this.activeIndex !== -1) {
|
if (this.activeIndex !== -1) {
|
||||||
this.startAnimation()
|
this.startAnimation()
|
||||||
} else {
|
} else {
|
||||||
this.oldIndex = -1
|
this.oldIndex = -1
|
||||||
|
this.sliderPositionX = 0
|
||||||
|
this.selectedElementWidth = 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
startAnimation() {
|
startAnimation() {
|
||||||
if (this.$refs.linkElements[this.activeIndex]) {
|
const el = this.$refs.linkElements[this.activeIndex].$el
|
||||||
this.indicator.direction =
|
|
||||||
this.activeIndex < this.oldIndex ? 'left' : 'right'
|
|
||||||
|
|
||||||
this.indicator.left =
|
this.sliderPositionX = el.offsetLeft
|
||||||
this.$refs.linkElements[this.activeIndex].$el.offsetLeft
|
this.sliderPositionY = el.offsetTop + el.offsetHeight
|
||||||
this.indicator.right =
|
this.selectedElementWidth = el.offsetWidth
|
||||||
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
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -141,19 +119,6 @@ export default {
|
|||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
position: relative;
|
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 {
|
&:hover {
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
|
|
||||||
@@ -166,7 +131,7 @@ export default {
|
|||||||
opacity: 0.2;
|
opacity: 0.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.is-active {
|
&.router-link-exact-active {
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
@@ -186,11 +151,12 @@ export default {
|
|||||||
.nav-indicator {
|
.nav-indicator {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
height: 0.25rem;
|
height: 0.25rem;
|
||||||
|
bottom: -5px;
|
||||||
|
left: 0;
|
||||||
|
width: 3rem;
|
||||||
|
transition: all ease-in-out 0.2s;
|
||||||
border-radius: var(--size-rounded-max);
|
border-radius: var(--size-rounded-max);
|
||||||
background-color: var(--color-brand);
|
background-color: var(--color-brand);
|
||||||
transition-property: left, right, top;
|
|
||||||
transition-duration: 350ms;
|
|
||||||
visibility: hidden;
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion) {
|
@media (prefers-reduced-motion) {
|
||||||
transition: none !important;
|
transition: none !important;
|
||||||
|
|||||||
@@ -7,9 +7,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {}
|
||||||
name: 'NavStack',
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -23,10 +23,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import ChevronRightIcon from '~/assets/images/utils/chevron-right.svg?inline'
|
import ChevronRightIcon from '~/assets/images/utils/chevron-right.svg'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'NavStackItem',
|
|
||||||
components: {
|
components: {
|
||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
},
|
},
|
||||||
@@ -88,7 +87,13 @@ export default {
|
|||||||
background-color: var(--background-color);
|
background-color: var(--background-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.nuxt-link-exact-active {
|
&:focus-visible {
|
||||||
|
.nav-content {
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.router-link-exact-active {
|
||||||
.nav-content {
|
.nav-content {
|
||||||
color: var(--color-button-text-active);
|
color: var(--color-button-text-active);
|
||||||
background-color: var(--color-button-bg);
|
background-color: var(--color-button-bg);
|
||||||
|
|||||||
107
components/ui/Notifications.vue
Normal file
107
components/ui/Notifications.vue
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
<template>
|
||||||
|
<div class="vue-notification-group">
|
||||||
|
<transition-group name="notifs">
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in notifications"
|
||||||
|
:key="item.id"
|
||||||
|
class="vue-notification-wrapper"
|
||||||
|
@click="notifications.splice(index, 1)"
|
||||||
|
@mouseenter="stopTimer(item)"
|
||||||
|
@mouseleave="setNotificationTimer(item)"
|
||||||
|
>
|
||||||
|
<div class="vue-notification-template vue-notification" :class="{ [item.type]: true }">
|
||||||
|
<div class="notification-title" v-html="item.title"></div>
|
||||||
|
<div class="notification-content" v-html="item.text"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition-group>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
const notifications = useNotifications()
|
||||||
|
|
||||||
|
function stopTimer(notif) {
|
||||||
|
clearTimeout(notif.timer)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.vue-notification {
|
||||||
|
background: var(--color-special-blue) !important;
|
||||||
|
border-left: 5px solid var(--color-special-blue) !important;
|
||||||
|
color: var(--color-brand-inverted) !important;
|
||||||
|
|
||||||
|
box-sizing: border-box;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 10px;
|
||||||
|
margin: 0 5px 5px;
|
||||||
|
|
||||||
|
&.success {
|
||||||
|
background: var(--color-special-green) !important;
|
||||||
|
border-left-color: var(--color-special-green) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.warn {
|
||||||
|
background: var(--color-special-orange) !important;
|
||||||
|
border-left-color: var(--color-special-orange) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
background: var(--color-special-red) !important;
|
||||||
|
border-left-color: var(--color-special-red) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-notification-group {
|
||||||
|
position: fixed;
|
||||||
|
right: 25px;
|
||||||
|
bottom: 25px;
|
||||||
|
z-index: 99999999;
|
||||||
|
width: 300px;
|
||||||
|
|
||||||
|
.vue-notification-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
|
||||||
|
.vue-notification-template {
|
||||||
|
border-radius: var(--size-rounded-card);
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
.notification-title {
|
||||||
|
font-size: var(--font-size-lg);
|
||||||
|
margin-right: auto;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-content {
|
||||||
|
margin-right: auto;
|
||||||
|
font-size: var(--font-size-md);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 750px) {
|
||||||
|
transition: bottom 0.25s ease-in-out;
|
||||||
|
bottom: calc(var(--size-mobile-navbar-height) + 10px) !important;
|
||||||
|
|
||||||
|
&.browse-menu-open {
|
||||||
|
bottom: calc(var(--size-mobile-navbar-height-expanded) + 10px) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.notifs-enter-active,
|
||||||
|
.notifs-leave-active,
|
||||||
|
.notifs-move {
|
||||||
|
transition: all 0.5s;
|
||||||
|
}
|
||||||
|
.notifs-enter-from,
|
||||||
|
.notifs-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
<div v-if="count > 1" class="columns paginates">
|
<div v-if="count > 1" class="columns paginates">
|
||||||
<a
|
<a
|
||||||
:class="{ disabled: page === 1 }"
|
:class="{ disabled: page === 1 }"
|
||||||
|
:tabindex="page === 1 ? -1 : 0"
|
||||||
class="left-arrow paginate has-icon"
|
class="left-arrow paginate has-icon"
|
||||||
aria-label="Previous Page"
|
aria-label="Previous Page"
|
||||||
:href="linkFunction(page - 1)"
|
:href="linkFunction(page - 1)"
|
||||||
@@ -38,12 +39,11 @@
|
|||||||
:class="{
|
:class="{
|
||||||
disabled: page === pages[pages.length - 1],
|
disabled: page === pages[pages.length - 1],
|
||||||
}"
|
}"
|
||||||
|
:tabindex="page === pages[pages.length - 1] ? -1 : 0"
|
||||||
class="right-arrow paginate has-icon"
|
class="right-arrow paginate has-icon"
|
||||||
aria-label="Next Page"
|
aria-label="Next Page"
|
||||||
:href="linkFunction(page + 1)"
|
:href="linkFunction(page + 1)"
|
||||||
@click.prevent="
|
@click.prevent="page !== pages[pages.length - 1] ? switchPage(page + 1) : null"
|
||||||
page !== pages[pages.length - 1] ? switchPage(page + 1) : null
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<RightArrowIcon />
|
<RightArrowIcon />
|
||||||
</a>
|
</a>
|
||||||
@@ -51,12 +51,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import GapIcon from '~/assets/images/utils/gap.svg?inline'
|
import GapIcon from '~/assets/images/utils/gap.svg'
|
||||||
import LeftArrowIcon from '~/assets/images/utils/left-arrow.svg?inline'
|
import LeftArrowIcon from '~/assets/images/utils/left-arrow.svg'
|
||||||
import RightArrowIcon from '~/assets/images/utils/right-arrow.svg?inline'
|
import RightArrowIcon from '~/assets/images/utils/right-arrow.svg'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Pagination',
|
|
||||||
components: {
|
components: {
|
||||||
GapIcon,
|
GapIcon,
|
||||||
LeftArrowIcon,
|
LeftArrowIcon,
|
||||||
@@ -78,6 +77,7 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
emits: ['switch-page'],
|
||||||
computed: {
|
computed: {
|
||||||
pages() {
|
pages() {
|
||||||
let pages = []
|
let pages = []
|
||||||
@@ -94,15 +94,7 @@ export default {
|
|||||||
this.count,
|
this.count,
|
||||||
]
|
]
|
||||||
} else if (this.page > 4) {
|
} else if (this.page > 4) {
|
||||||
pages = [
|
pages = [1, '-', this.page - 1, this.page, this.page + 1, '-', this.count]
|
||||||
1,
|
|
||||||
'-',
|
|
||||||
this.page - 1,
|
|
||||||
this.page,
|
|
||||||
this.page + 1,
|
|
||||||
'-',
|
|
||||||
this.count,
|
|
||||||
]
|
|
||||||
} else {
|
} else {
|
||||||
pages = [1, 2, 3, 4, 5, '-', this.count]
|
pages = [1, 2, 3, 4, 5, '-', this.count]
|
||||||
}
|
}
|
||||||
@@ -131,8 +123,8 @@ a {
|
|||||||
border-radius: 2rem;
|
border-radius: 2rem;
|
||||||
background: var(--color-raised-bg);
|
background: var(--color-raised-bg);
|
||||||
|
|
||||||
transition: opacity 0.5s ease-in-out, filter 0.2s ease-in-out,
|
transition: opacity 0.5s ease-in-out, filter 0.2s ease-in-out, transform 0.05s ease-in-out,
|
||||||
transform 0.05s ease-in-out, outline 0.2s ease-in-out;
|
outline 0.2s ease-in-out;
|
||||||
|
|
||||||
&.page-number.current {
|
&.page-number.current {
|
||||||
background: var(--color-brand);
|
background: var(--color-brand);
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<article
|
<article class="project-card base-card padding-bg" :aria-label="name" role="listitem">
|
||||||
class="project-card base-card padding-bg"
|
|
||||||
:aria-label="name"
|
|
||||||
role="listitem"
|
|
||||||
>
|
|
||||||
<nuxt-link
|
<nuxt-link
|
||||||
|
:title="name"
|
||||||
class="icon"
|
class="icon"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
:to="`/${$getProjectTypeForUrl(type, categories)}/${id}`"
|
:to="`/${$getProjectTypeForUrl(type, categories)}/${id}`"
|
||||||
@@ -18,7 +15,7 @@
|
|||||||
:to="`/${$getProjectTypeForUrl(type, categories)}/${id}`"
|
:to="`/${$getProjectTypeForUrl(type, categories)}/${id}`"
|
||||||
:style="color ? `background-color: ${toColor};` : ''"
|
:style="color ? `background-color: ${toColor};` : ''"
|
||||||
>
|
>
|
||||||
<img v-if="featuredImage" :src="featuredImage" alt="gallery image" />
|
<img v-if="featuredImage" :src="featuredImage" alt="gallery image" loading="lazy" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<nuxt-link :to="`/${$getProjectTypeForUrl(type, categories)}/${id}`">
|
<nuxt-link :to="`/${$getProjectTypeForUrl(type, categories)}/${id}`">
|
||||||
@@ -28,24 +25,18 @@
|
|||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
<p v-if="author" class="author">
|
<p v-if="author" class="author">
|
||||||
by
|
by
|
||||||
<nuxt-link class="title-link" :to="'/user/' + author"
|
<nuxt-link class="title-link" :to="'/user/' + author">
|
||||||
>{{ author }}
|
{{ author }}
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</p>
|
</p>
|
||||||
<Badge
|
<Badge v-if="status && status !== 'approved'" :type="status" class="status" />
|
||||||
v-if="status && status !== 'approved'"
|
|
||||||
:type="status"
|
|
||||||
class="status"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<p class="description">
|
<p class="description">
|
||||||
{{ description }}
|
{{ description }}
|
||||||
</p>
|
</p>
|
||||||
<Categories
|
<Categories
|
||||||
:categories="
|
:categories="
|
||||||
categories.filter(
|
categories.filter((x) => !hideLoaders || !$tag.loaders.find((y) => y.name === x))
|
||||||
(x) => !hideLoaders || !$tag.loaders.find((y) => y.name === x)
|
|
||||||
)
|
|
||||||
"
|
"
|
||||||
:type="type"
|
:type="type"
|
||||||
class="tags"
|
class="tags"
|
||||||
@@ -64,18 +55,14 @@
|
|||||||
<DownloadIcon aria-hidden="true" />
|
<DownloadIcon aria-hidden="true" />
|
||||||
<p>
|
<p>
|
||||||
<strong>{{ $formatNumber(downloads) }}</strong
|
<strong>{{ $formatNumber(downloads) }}</strong
|
||||||
><span class="stat-label">
|
><span class="stat-label"> download<span v-if="downloads !== '1'">s</span></span>
|
||||||
download<span v-if="downloads !== '1'">s</span></span
|
|
||||||
>
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="follows" class="stat">
|
<div v-if="follows" class="stat">
|
||||||
<HeartIcon aria-hidden="true" />
|
<HeartIcon aria-hidden="true" />
|
||||||
<p>
|
<p>
|
||||||
<strong>{{ $formatNumber(follows) }}</strong
|
<strong>{{ $formatNumber(follows) }}</strong
|
||||||
><span class="stat-label">
|
><span class="stat-label"> follower<span v-if="follows !== '1'">s</span></span>
|
||||||
follower<span v-if="follows !== '1'">s</span></span
|
|
||||||
>
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
@@ -87,8 +74,7 @@
|
|||||||
class="stat date"
|
class="stat date"
|
||||||
>
|
>
|
||||||
<EditIcon aria-hidden="true" />
|
<EditIcon aria-hidden="true" />
|
||||||
<span class="date-label">Updated </span
|
<span class="date-label">Updated </span>{{ fromNow(updatedAt) }}
|
||||||
>{{ $dayjs(updatedAt).fromNow() }}
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
@@ -96,8 +82,7 @@
|
|||||||
class="stat date"
|
class="stat date"
|
||||||
>
|
>
|
||||||
<CalendarIcon aria-hidden="true" />
|
<CalendarIcon aria-hidden="true" />
|
||||||
<span class="date-label">Published </span
|
<span class="date-label">Published </span>{{ fromNow(createdAt) }}
|
||||||
>{{ $dayjs(createdAt).fromNow() }}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
@@ -108,14 +93,13 @@ import Categories from '~/components/ui/search/Categories'
|
|||||||
import Badge from '~/components/ui/Badge'
|
import Badge from '~/components/ui/Badge'
|
||||||
import EnvironmentIndicator from '~/components/ui/EnvironmentIndicator'
|
import EnvironmentIndicator from '~/components/ui/EnvironmentIndicator'
|
||||||
|
|
||||||
import CalendarIcon from '~/assets/images/utils/calendar.svg?inline'
|
import CalendarIcon from '~/assets/images/utils/calendar.svg'
|
||||||
import EditIcon from '~/assets/images/utils/updated.svg?inline'
|
import EditIcon from '~/assets/images/utils/updated.svg'
|
||||||
import DownloadIcon from '~/assets/images/utils/download.svg?inline'
|
import DownloadIcon from '~/assets/images/utils/download.svg'
|
||||||
import HeartIcon from '~/assets/images/utils/heart.svg?inline'
|
import HeartIcon from '~/assets/images/utils/heart.svg'
|
||||||
import Avatar from '~/components/ui/Avatar'
|
import Avatar from '~/components/ui/Avatar'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ProjectCard',
|
|
||||||
components: {
|
components: {
|
||||||
EnvironmentIndicator,
|
EnvironmentIndicator,
|
||||||
Avatar,
|
Avatar,
|
||||||
|
|||||||
@@ -4,8 +4,7 @@
|
|||||||
$auth.user &&
|
$auth.user &&
|
||||||
currentMember &&
|
currentMember &&
|
||||||
nags.filter((x) => x.condition).length > 0 &&
|
nags.filter((x) => x.condition).length > 0 &&
|
||||||
(project.status === 'draft' ||
|
(project.status === 'draft' || $tag.rejectedStatuses.includes(project.status))
|
||||||
$tag.rejectedStatuses.includes(project.status))
|
|
||||||
"
|
"
|
||||||
class="author-actions universal-card"
|
class="author-actions universal-card"
|
||||||
>
|
>
|
||||||
@@ -19,6 +18,7 @@
|
|||||||
v-for="nag in nags"
|
v-for="nag in nags"
|
||||||
:key="`checklist-${nag.id}`"
|
:key="`checklist-${nag.id}`"
|
||||||
v-tooltip="nag.title"
|
v-tooltip="nag.title"
|
||||||
|
:aria-label="nag.title"
|
||||||
class="circle"
|
class="circle"
|
||||||
:class="'circle ' + (!nag.condition ? 'done ' : '') + nag.status"
|
:class="'circle ' + (!nag.condition ? 'done ' : '') + nag.status"
|
||||||
>
|
>
|
||||||
@@ -41,25 +41,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!collapsed" class="grid-display width-16">
|
<div v-if="!collapsed" class="grid-display width-16">
|
||||||
<div
|
<div v-for="nag in nags.filter((x) => x.condition)" :key="nag.id" class="grid-display__item">
|
||||||
v-for="nag in nags.filter((x) => x.condition)"
|
|
||||||
:key="nag.id"
|
|
||||||
class="grid-display__item"
|
|
||||||
>
|
|
||||||
<span class="label">
|
<span class="label">
|
||||||
<RequiredIcon
|
<RequiredIcon
|
||||||
v-if="nag.status === 'required'"
|
v-if="nag.status === 'required'"
|
||||||
v-tooltip="'Required'"
|
v-tooltip="'Required'"
|
||||||
|
aria-label="Required"
|
||||||
:class="nag.status"
|
:class="nag.status"
|
||||||
/>
|
/>
|
||||||
<SuggestionIcon
|
<SuggestionIcon
|
||||||
v-else-if="nag.status === 'suggestion'"
|
v-else-if="nag.status === 'suggestion'"
|
||||||
v-tooltip="'Suggestion'"
|
v-tooltip="'Suggestion'"
|
||||||
|
aria-label="Suggestion"
|
||||||
:class="nag.status"
|
:class="nag.status"
|
||||||
/>
|
/>
|
||||||
<ModerationIcon
|
<ModerationIcon
|
||||||
v-else-if="nag.status === 'review'"
|
v-else-if="nag.status === 'review'"
|
||||||
v-tooltip="'Review'"
|
v-tooltip="'Review'"
|
||||||
|
aria-label="Review"
|
||||||
:class="nag.status"
|
:class="nag.status"
|
||||||
/>{{ nag.title }}</span
|
/>{{ nag.title }}</span
|
||||||
>
|
>
|
||||||
@@ -71,6 +70,7 @@
|
|||||||
$tag.rejectedStatuses.includes(project.status)
|
$tag.rejectedStatuses.includes(project.status)
|
||||||
"
|
"
|
||||||
v-model="acknowledgedMessage"
|
v-model="acknowledgedMessage"
|
||||||
|
description="Acknowledge staff message in sidebar"
|
||||||
>
|
>
|
||||||
I acknowledge that I have addressed the staff's message on the sidebar
|
I acknowledge that I have addressed the staff's message on the sidebar
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
@@ -78,15 +78,12 @@
|
|||||||
v-if="nag.link"
|
v-if="nag.link"
|
||||||
:class="{ invisible: nag.link.hide }"
|
:class="{ invisible: nag.link.hide }"
|
||||||
class="goto-link"
|
class="goto-link"
|
||||||
:to="`/${project.project_type}/${
|
:to="`/${project.project_type}/${project.slug ? project.slug : project.id}/${
|
||||||
project.slug ? project.slug : project.id
|
nag.link.path
|
||||||
}/${nag.link.path}`"
|
}`"
|
||||||
>
|
>
|
||||||
{{ nag.link.title }}
|
{{ nag.link.title }}
|
||||||
<ChevronRightIcon
|
<ChevronRightIcon class="featured-header-chevron" aria-hidden="true" />
|
||||||
class="featured-header-chevron"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<button
|
<button
|
||||||
v-else-if="nag.action"
|
v-else-if="nag.action"
|
||||||
@@ -103,17 +100,16 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import ChevronRightIcon from '~/assets/images/utils/chevron-right.svg?inline'
|
import ChevronRightIcon from '~/assets/images/utils/chevron-right.svg'
|
||||||
import DropdownIcon from '~/assets/images/utils/dropdown.svg?inline'
|
import DropdownIcon from '~/assets/images/utils/dropdown.svg'
|
||||||
import CheckIcon from '~/assets/images/utils/check.svg?inline'
|
import CheckIcon from '~/assets/images/utils/check.svg'
|
||||||
import RequiredIcon from '~/assets/images/utils/asterisk.svg?inline'
|
import RequiredIcon from '~/assets/images/utils/asterisk.svg'
|
||||||
import SuggestionIcon from '~/assets/images/utils/lightbulb.svg?inline'
|
import SuggestionIcon from '~/assets/images/utils/lightbulb.svg'
|
||||||
import ModerationIcon from '~/assets/images/sidebar/admin.svg?inline'
|
import ModerationIcon from '~/assets/images/sidebar/admin.svg'
|
||||||
import SendIcon from '~/assets/images/utils/send.svg?inline'
|
import SendIcon from '~/assets/images/utils/send.svg'
|
||||||
import Checkbox from '~/components/ui/Checkbox'
|
import Checkbox from '~/components/ui/Checkbox'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ProjectPublishingChecklist',
|
|
||||||
components: {
|
components: {
|
||||||
Checkbox,
|
Checkbox,
|
||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
@@ -131,7 +127,9 @@ export default {
|
|||||||
},
|
},
|
||||||
versions: {
|
versions: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true,
|
default() {
|
||||||
|
return []
|
||||||
|
},
|
||||||
},
|
},
|
||||||
currentMember: {
|
currentMember: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -189,8 +187,7 @@ export default {
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
condition:
|
condition:
|
||||||
this.project.body === '' ||
|
this.project.body === '' || this.project.body.startsWith('# Placeholder description'),
|
||||||
this.project.body.startsWith('# Placeholder description'),
|
|
||||||
title: 'Add a description',
|
title: 'Add a description',
|
||||||
id: 'add-description',
|
id: 'add-description',
|
||||||
description:
|
description:
|
||||||
@@ -219,8 +216,7 @@ export default {
|
|||||||
condition: !this.featuredGalleryImage,
|
condition: !this.featuredGalleryImage,
|
||||||
title: 'Feature a gallery image',
|
title: 'Feature a gallery image',
|
||||||
id: 'feature-gallery-image',
|
id: 'feature-gallery-image',
|
||||||
description:
|
description: 'Featured gallery images may be the first impression of many users.',
|
||||||
'Featured gallery images may be the first impression for many users.',
|
|
||||||
status: 'suggestion',
|
status: 'suggestion',
|
||||||
link: {
|
link: {
|
||||||
path: 'gallery',
|
path: 'gallery',
|
||||||
@@ -232,8 +228,7 @@ export default {
|
|||||||
condition: this.versions.length < 1,
|
condition: this.versions.length < 1,
|
||||||
title: 'Upload a version',
|
title: 'Upload a version',
|
||||||
id: 'upload-version',
|
id: 'upload-version',
|
||||||
description:
|
description: 'At least one version is required for a project to be submitted for review.',
|
||||||
'At least one version is required for a project to be submitted for review.',
|
|
||||||
status: 'required',
|
status: 'required',
|
||||||
link: {
|
link: {
|
||||||
path: 'versions',
|
path: 'versions',
|
||||||
@@ -279,8 +274,7 @@ export default {
|
|||||||
this.project.project_type === 'shader' ||
|
this.project.project_type === 'shader' ||
|
||||||
this.project.project_type === 'datapack',
|
this.project.project_type === 'datapack',
|
||||||
condition:
|
condition:
|
||||||
this.project.client_side === 'unknown' ||
|
this.project.client_side === 'unknown' || this.project.server_side === 'unknown',
|
||||||
this.project.server_side === 'unknown',
|
|
||||||
title: 'Select supported environments',
|
title: 'Select supported environments',
|
||||||
id: 'select-environments',
|
id: 'select-environments',
|
||||||
description: `Select if the ${this.$formatProjectType(
|
description: `Select if the ${this.$formatProjectType(
|
||||||
@@ -320,8 +314,7 @@ export default {
|
|||||||
onClick: this.submitForReview,
|
onClick: this.submitForReview,
|
||||||
title: 'Submit for review',
|
title: 'Submit for review',
|
||||||
disabled: () =>
|
disabled: () =>
|
||||||
this.nags.filter((x) => x.condition && x.status === 'required')
|
this.nags.filter((x) => x.condition && x.status === 'required').length > 0,
|
||||||
.length > 0,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -339,8 +332,7 @@ export default {
|
|||||||
title: 'Resubmit for review',
|
title: 'Resubmit for review',
|
||||||
disabled: () =>
|
disabled: () =>
|
||||||
!this.acknowledgedMessage ||
|
!this.acknowledgedMessage ||
|
||||||
this.nags.filter((x) => x.condition && x.status === 'required')
|
this.nags.filter((x) => x.condition && x.status === 'required').length > 0,
|
||||||
.length > 0,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -380,8 +372,7 @@ export default {
|
|||||||
async submitForReview() {
|
async submitForReview() {
|
||||||
if (
|
if (
|
||||||
!this.acknowledgedMessage ||
|
!this.acknowledgedMessage ||
|
||||||
this.nags.filter((x) => x.condition && x.status === 'required')
|
this.nags.filter((x) => x.condition && x.status === 'required').length === 0
|
||||||
.length === 0
|
|
||||||
) {
|
) {
|
||||||
await this.setProcessing()
|
await this.setProcessing()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="getValidLoaders().length > 1 || getValidVersions().length > 1"
|
v-if="
|
||||||
|
loaderFilters.length > 1 || gameVersionFilters.length > 1 || versionTypeFilters.length > 1
|
||||||
|
"
|
||||||
class="card search-controls"
|
class="card search-controls"
|
||||||
>
|
>
|
||||||
<Multiselect
|
<Multiselect
|
||||||
v-if="getValidLoaders().length > 1"
|
v-if="loaderFilters.length > 1"
|
||||||
v-model="selectedLoaders"
|
v-model="selectedLoaders"
|
||||||
:options="getValidLoaders()"
|
:options="loaderFilters"
|
||||||
:custom-label="(value) => value.charAt(0).toUpperCase() + value.slice(1)"
|
:custom-label="(value) => value.charAt(0).toUpperCase() + value.slice(1)"
|
||||||
:multiple="true"
|
:multiple="true"
|
||||||
:searchable="false"
|
:searchable="false"
|
||||||
@@ -15,19 +17,16 @@
|
|||||||
:clear-search-on-select="false"
|
:clear-search-on-select="false"
|
||||||
:show-labels="false"
|
:show-labels="false"
|
||||||
:allow-empty="true"
|
:allow-empty="true"
|
||||||
:disabled="getValidLoaders().length === 1"
|
|
||||||
placeholder="Filter loader..."
|
placeholder="Filter loader..."
|
||||||
@input="updateVersionFilters()"
|
@update:model-value="updateVersionFilters()"
|
||||||
></Multiselect>
|
/>
|
||||||
<Multiselect
|
<Multiselect
|
||||||
v-if="getValidVersions().length > 1"
|
v-if="gameVersionFilters.length > 1"
|
||||||
v-model="selectedGameVersions"
|
v-model="selectedGameVersions"
|
||||||
:options="
|
:options="
|
||||||
showSnapshots
|
includeSnapshots
|
||||||
? getValidVersions().map((x) => x.version)
|
? gameVersionFilters.map((x) => x.version)
|
||||||
: getValidVersions()
|
: gameVersionFilters.filter((it) => it.version_type === 'release').map((x) => x.version)
|
||||||
.filter((it) => it.version_type === 'release')
|
|
||||||
.map((x) => x.version)
|
|
||||||
"
|
"
|
||||||
:multiple="true"
|
:multiple="true"
|
||||||
:searchable="true"
|
:searchable="true"
|
||||||
@@ -37,12 +36,12 @@
|
|||||||
:hide-selected="true"
|
:hide-selected="true"
|
||||||
:selectable="() => selectedGameVersions.length <= 6"
|
:selectable="() => selectedGameVersions.length <= 6"
|
||||||
placeholder="Filter versions..."
|
placeholder="Filter versions..."
|
||||||
@input="updateVersionFilters()"
|
@update:model-value="updateVersionFilters()"
|
||||||
></Multiselect>
|
/>
|
||||||
<Multiselect
|
<Multiselect
|
||||||
v-if="getValidChannels().length > 1"
|
v-if="versionTypeFilters.length > 1"
|
||||||
v-model="selectedChannels"
|
v-model="selectedVersionTypes"
|
||||||
:options="getValidChannels()"
|
:options="versionTypeFilters"
|
||||||
:custom-label="(x) => $capitalizeString(x)"
|
:custom-label="(x) => $capitalizeString(x)"
|
||||||
:multiple="true"
|
:multiple="true"
|
||||||
:searchable="false"
|
:searchable="false"
|
||||||
@@ -52,29 +51,30 @@
|
|||||||
:show-labels="false"
|
:show-labels="false"
|
||||||
:allow-empty="true"
|
:allow-empty="true"
|
||||||
placeholder="Filter channels..."
|
placeholder="Filter channels..."
|
||||||
@input="updateVersionFilters()"
|
@update:model-value="updateVersionFilters()"
|
||||||
></Multiselect>
|
/>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
v-if="
|
v-if="
|
||||||
getValidVersions().length > 1 &&
|
gameVersionFilters.length > 1 &&
|
||||||
getValidVersions().some((v) => v.version_type !== 'release')
|
gameVersionFilters.some((v) => v.version_type !== 'release')
|
||||||
"
|
"
|
||||||
v-model="showSnapshots"
|
v-model="includeSnapshots"
|
||||||
label="Include snapshots"
|
label="Include snapshots"
|
||||||
description="Include snapshots"
|
description="Include snapshots"
|
||||||
:border="false"
|
:border="false"
|
||||||
@input="updateQuery"
|
@update:model-value="updateQuery"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
title="Clear filters"
|
title="Clear filters"
|
||||||
:disabled="
|
:disabled="selectedLoaders.length === 0 && selectedGameVersions.length === 0"
|
||||||
selectedLoaders.length === 0 && selectedGameVersions.length === 0
|
|
||||||
"
|
|
||||||
class="iconified-button"
|
class="iconified-button"
|
||||||
@click="
|
@click="
|
||||||
selectedLoaders = []
|
() => {
|
||||||
selectedGameVersions = []
|
selectedLoaders = []
|
||||||
updateVersionFilters()
|
selectedGameVersions = []
|
||||||
|
selectedVersionTypes = []
|
||||||
|
updateVersionFilters()
|
||||||
|
}
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<ClearIcon />
|
<ClearIcon />
|
||||||
@@ -83,125 +83,81 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import Multiselect from 'vue-multiselect'
|
import Multiselect from 'vue-multiselect'
|
||||||
import Checkbox from '~/components/ui/Checkbox'
|
import Checkbox from '~/components/ui/Checkbox'
|
||||||
import ClearIcon from '~/assets/images/utils/clear.svg?inline'
|
import ClearIcon from '~/assets/images/utils/clear.svg'
|
||||||
export default {
|
|
||||||
name: 'VersionFilterControl',
|
|
||||||
components: {
|
|
||||||
Multiselect,
|
|
||||||
Checkbox,
|
|
||||||
ClearIcon,
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
versions: {
|
|
||||||
type: Array,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
query: '',
|
|
||||||
showSnapshots: false,
|
|
||||||
cachedValidChannels: null,
|
|
||||||
cachedValidVersions: null,
|
|
||||||
cachedValidLoaders: null,
|
|
||||||
selectedGameVersions: [],
|
|
||||||
selectedLoaders: [],
|
|
||||||
selectedChannels: [],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
fetch() {
|
|
||||||
this.selectedLoaders = this.$route.query.l?.split(',') || []
|
|
||||||
this.selectedGameVersions = this.$route.query.g?.split(',') || []
|
|
||||||
this.selectedChannels = this.$route.query.c?.split(',') || []
|
|
||||||
this.showSnapshots = this.$route.query.s === 'true'
|
|
||||||
this.updateVersionFilters()
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
getValidChannels() {
|
|
||||||
if (!this.cachedValidChannels) {
|
|
||||||
this.cachedValidChannels = ['release', 'beta', 'alpha'].filter(
|
|
||||||
(channel) =>
|
|
||||||
this.versions.some((projVer) => projVer.version_type === channel)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return this.cachedValidChannels
|
|
||||||
},
|
|
||||||
getValidVersions() {
|
|
||||||
if (!this.cachedValidVersions) {
|
|
||||||
this.cachedValidVersions = this.$tag.gameVersions.filter((gameVer) =>
|
|
||||||
this.versions.some((projVer) =>
|
|
||||||
projVer.game_versions.includes(gameVer.version)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return this.cachedValidVersions
|
|
||||||
},
|
|
||||||
getValidLoaders() {
|
|
||||||
if (!this.cachedValidLoaders) {
|
|
||||||
const temp = new Set()
|
|
||||||
for (const version of this.versions) {
|
|
||||||
version.loaders.forEach((v) => {
|
|
||||||
temp.add(v)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
this.cachedValidLoaders = Array.from(temp)
|
|
||||||
this.cachedValidLoaders.sort()
|
|
||||||
}
|
|
||||||
return this.cachedValidLoaders
|
|
||||||
},
|
|
||||||
async updateVersionFilters() {
|
|
||||||
this.selectedChannels = this.selectedChannels.filter((channel) =>
|
|
||||||
this.getValidChannels().includes(channel)
|
|
||||||
)
|
|
||||||
this.selectedLoaders = this.selectedLoaders.filter((loader) =>
|
|
||||||
this.getValidLoaders().includes(loader)
|
|
||||||
)
|
|
||||||
this.selectedGameVersions = this.selectedGameVersions.filter((version) =>
|
|
||||||
this.getValidVersions().some(
|
|
||||||
(validVersion) => validVersion.version === version
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
const temp = this.versions.filter(
|
const emit = defineEmits(['updateVersions'])
|
||||||
(projectVersion) =>
|
const props = defineProps({
|
||||||
(this.selectedGameVersions.length === 0 ||
|
versions: {
|
||||||
this.selectedGameVersions.some((gameVersion) =>
|
type: Array,
|
||||||
projectVersion.game_versions.includes(gameVersion)
|
default() {
|
||||||
)) &&
|
return []
|
||||||
(this.selectedLoaders.length === 0 ||
|
|
||||||
this.selectedLoaders.some((loader) =>
|
|
||||||
projectVersion.loaders.includes(loader)
|
|
||||||
)) &&
|
|
||||||
(this.selectedChannels.length === 0 ||
|
|
||||||
this.selectedChannels.includes(projectVersion.version_type))
|
|
||||||
)
|
|
||||||
await this.updateQuery()
|
|
||||||
this.$emit('updateVersions', temp)
|
|
||||||
},
|
|
||||||
async updateQuery() {
|
|
||||||
await this.$router.replace({
|
|
||||||
query: {
|
|
||||||
...this.$route.query,
|
|
||||||
l:
|
|
||||||
this.selectedLoaders.length === 0
|
|
||||||
? undefined
|
|
||||||
: this.selectedLoaders.join(','),
|
|
||||||
g:
|
|
||||||
this.selectedGameVersions.length === 0
|
|
||||||
? undefined
|
|
||||||
: this.selectedGameVersions.join(','),
|
|
||||||
c:
|
|
||||||
this.selectedChannels.length === 0
|
|
||||||
? undefined
|
|
||||||
: this.selectedChannels.join(','),
|
|
||||||
s: this.showSnapshots ? true : undefined,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = useNuxtApp()
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
const tempLoaders = new Set()
|
||||||
|
let tempVersions = new Set()
|
||||||
|
const tempReleaseChannels = new Set()
|
||||||
|
|
||||||
|
for (const version of props.versions) {
|
||||||
|
for (const loader of version.loaders) {
|
||||||
|
tempLoaders.add(loader)
|
||||||
|
}
|
||||||
|
for (const gameVersion of version.game_versions) {
|
||||||
|
tempVersions.add(gameVersion)
|
||||||
|
}
|
||||||
|
tempReleaseChannels.add(version.version_type)
|
||||||
|
}
|
||||||
|
|
||||||
|
tempVersions = Array.from(tempVersions)
|
||||||
|
|
||||||
|
const loaderFilters = shallowRef(Array.from(tempLoaders))
|
||||||
|
const gameVersionFilters = shallowRef(
|
||||||
|
data.$tag.gameVersions.filter((gameVer) => tempVersions.includes(gameVer.version))
|
||||||
|
)
|
||||||
|
const versionTypeFilters = shallowRef(Array.from(tempReleaseChannels))
|
||||||
|
const includeSnapshots = ref(route.query.s === 'true')
|
||||||
|
|
||||||
|
const selectedGameVersions = shallowRef(route.query.g ?? [])
|
||||||
|
const selectedLoaders = shallowRef(route.query.l ?? [])
|
||||||
|
const selectedVersionTypes = shallowRef(route.query.c ?? [])
|
||||||
|
|
||||||
|
async function updateVersionFilters() {
|
||||||
|
const temp = props.versions.filter(
|
||||||
|
(projectVersion) =>
|
||||||
|
(selectedGameVersions.value.length === 0 ||
|
||||||
|
selectedGameVersions.value.some((gameVersion) =>
|
||||||
|
projectVersion.game_versions.includes(gameVersion)
|
||||||
|
)) &&
|
||||||
|
(selectedLoaders.value.length === 0 ||
|
||||||
|
selectedLoaders.value.some((loader) => projectVersion.loaders.includes(loader))) &&
|
||||||
|
(selectedVersionTypes.value.length === 0 ||
|
||||||
|
selectedVersionTypes.value.includes(projectVersion.version_type))
|
||||||
|
)
|
||||||
|
|
||||||
|
await updateQuery()
|
||||||
|
emit('updateVersions', temp)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateQuery() {
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
await router.replace({
|
||||||
|
query: {
|
||||||
|
...route.query,
|
||||||
|
l: selectedLoaders.value.length === 0 ? undefined : selectedLoaders.value,
|
||||||
|
g: selectedGameVersions.value.length === 0 ? undefined : selectedGameVersions.value,
|
||||||
|
c: selectedVersionTypes.value.length === 0 ? undefined : selectedVersionTypes.value,
|
||||||
|
s: includeSnapshots.value ? true : undefined,
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -219,25 +175,4 @@ export default {
|
|||||||
min-width: fit-content;
|
min-width: fit-content;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.circle-button {
|
|
||||||
display: flex;
|
|
||||||
max-width: 2rem;
|
|
||||||
padding: 0.5rem;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
142
components/ui/modrinth-loading-indicator.ts
Normal file
142
components/ui/modrinth-loading-indicator.ts
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
import { computed, defineComponent, h, onBeforeUnmount, ref, watch } from 'vue'
|
||||||
|
import { useNuxtApp } from '#app'
|
||||||
|
import { startLoading, stopLoading } from '#imports'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'ModrinthLoadingIndicator',
|
||||||
|
props: {
|
||||||
|
throttle: {
|
||||||
|
type: Number,
|
||||||
|
default: 50,
|
||||||
|
},
|
||||||
|
duration: {
|
||||||
|
type: Number,
|
||||||
|
default: 500,
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
type: Number,
|
||||||
|
default: 3,
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
type: [String, Boolean],
|
||||||
|
default:
|
||||||
|
'repeating-linear-gradient(to right, var(--color-brand-green) 0%, var(--landing-green-label) 100%)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props, { slots }) {
|
||||||
|
const indicator = useLoadingIndicator({
|
||||||
|
duration: props.duration,
|
||||||
|
throttle: props.throttle,
|
||||||
|
})
|
||||||
|
|
||||||
|
const nuxtApp = useNuxtApp()
|
||||||
|
nuxtApp.hook('page:start', () => {
|
||||||
|
startLoading()
|
||||||
|
indicator.start()
|
||||||
|
})
|
||||||
|
nuxtApp.hook('page:finish', () => {
|
||||||
|
stopLoading()
|
||||||
|
indicator.finish()
|
||||||
|
})
|
||||||
|
onBeforeUnmount(() => indicator.clear)
|
||||||
|
|
||||||
|
const loading = useLoading()
|
||||||
|
|
||||||
|
watch(loading, (newValue, _oldValue) => {
|
||||||
|
if (newValue) {
|
||||||
|
indicator.start()
|
||||||
|
} else {
|
||||||
|
indicator.finish()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return () =>
|
||||||
|
h(
|
||||||
|
'div',
|
||||||
|
{
|
||||||
|
class: 'nuxt-loading-indicator',
|
||||||
|
style: {
|
||||||
|
position: 'fixed',
|
||||||
|
top: 0,
|
||||||
|
right: 0,
|
||||||
|
left: 0,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
width: `${indicator.progress.value}%`,
|
||||||
|
height: `${props.height}px`,
|
||||||
|
opacity: indicator.isLoading.value ? 1 : 0,
|
||||||
|
background: props.color || undefined,
|
||||||
|
backgroundSize: `${(100 / indicator.progress.value) * 100}% auto`,
|
||||||
|
transition: 'width 0.1s, height 0.4s, opacity 0.4s',
|
||||||
|
zIndex: 999999,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
slots
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
function useLoadingIndicator(opts: { duration: number; throttle: number }) {
|
||||||
|
const progress = ref(0)
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const step = computed(() => 10000 / opts.duration)
|
||||||
|
|
||||||
|
let _timer: any = null
|
||||||
|
let _throttle: any = null
|
||||||
|
|
||||||
|
function start() {
|
||||||
|
clear()
|
||||||
|
progress.value = 0
|
||||||
|
if (opts.throttle && process.client) {
|
||||||
|
_throttle = setTimeout(() => {
|
||||||
|
isLoading.value = true
|
||||||
|
_startTimer()
|
||||||
|
}, opts.throttle)
|
||||||
|
} else {
|
||||||
|
isLoading.value = true
|
||||||
|
_startTimer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function finish() {
|
||||||
|
progress.value = 100
|
||||||
|
_hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
function clear() {
|
||||||
|
clearInterval(_timer)
|
||||||
|
clearTimeout(_throttle)
|
||||||
|
_timer = null
|
||||||
|
_throttle = null
|
||||||
|
}
|
||||||
|
|
||||||
|
function _increase(num: number) {
|
||||||
|
progress.value = Math.min(100, progress.value + num)
|
||||||
|
}
|
||||||
|
|
||||||
|
function _hide() {
|
||||||
|
clear()
|
||||||
|
if (process.client) {
|
||||||
|
setTimeout(() => {
|
||||||
|
isLoading.value = false
|
||||||
|
setTimeout(() => {
|
||||||
|
progress.value = 0
|
||||||
|
}, 400)
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _startTimer() {
|
||||||
|
if (process.client) {
|
||||||
|
_timer = setInterval(() => {
|
||||||
|
_increase(step.value)
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
progress,
|
||||||
|
isLoading,
|
||||||
|
start,
|
||||||
|
finish,
|
||||||
|
clear,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,7 +11,6 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
name: 'Categories',
|
|
||||||
props: {
|
props: {
|
||||||
categories: {
|
categories: {
|
||||||
type: Array,
|
type: Array,
|
||||||
@@ -30,8 +29,7 @@ export default {
|
|||||||
.concat(this.$tag.loaders)
|
.concat(this.$tag.loaders)
|
||||||
.filter(
|
.filter(
|
||||||
(x) =>
|
(x) =>
|
||||||
this.categories.includes(x.name) &&
|
this.categories.includes(x.name) && (!x.project_type || x.project_type === this.type)
|
||||||
(!x.project_type || x.project_type === this.type)
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -44,7 +42,7 @@ export default {
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
||||||
span ::v-deep {
|
:deep(span) {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
class="filter"
|
class="filter"
|
||||||
:value="activeFilters.includes(facetName)"
|
:model-value="activeFilters.includes(facetName)"
|
||||||
:description="displayName"
|
:description="displayName"
|
||||||
@input="toggle()"
|
@update:model-value="toggle()"
|
||||||
>
|
>
|
||||||
<div class="filter-text">
|
<div class="filter-text">
|
||||||
<div v-if="icon" aria-hidden="true" class="icon" v-html="icon"></div>
|
<div v-if="icon" aria-hidden="true" class="icon" v-html="icon" />
|
||||||
<div v-else class="icon"><slot /></div>
|
<div v-else class="icon">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
<span aria-hidden="true"> {{ displayName }}</span>
|
<span aria-hidden="true"> {{ displayName }}</span>
|
||||||
</div>
|
</div>
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
@@ -17,7 +19,6 @@
|
|||||||
import Checkbox from '~/components/ui/Checkbox'
|
import Checkbox from '~/components/ui/Checkbox'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'SearchFilter',
|
|
||||||
components: {
|
components: {
|
||||||
Checkbox,
|
Checkbox,
|
||||||
},
|
},
|
||||||
@@ -41,6 +42,7 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
emits: ['toggle'],
|
||||||
methods: {
|
methods: {
|
||||||
toggle() {
|
toggle() {
|
||||||
this.$emit('toggle', this.facetName)
|
this.$emit('toggle', this.facetName)
|
||||||
@@ -50,10 +52,10 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.filter ::v-deep {
|
.filter {
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
|
|
||||||
.filter-text {
|
:deep(.filter-text) {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
|
|||||||
63
composables/auth.js
Normal file
63
composables/auth.js
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
export const useAuth = async (oldToken = null) => {
|
||||||
|
const auth = useState('auth', () => ({
|
||||||
|
user: null,
|
||||||
|
token: '',
|
||||||
|
headers: {},
|
||||||
|
}))
|
||||||
|
|
||||||
|
if (!auth.value.user || oldToken) {
|
||||||
|
auth.value = await initAuth(oldToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
return auth
|
||||||
|
}
|
||||||
|
|
||||||
|
export const initAuth = async (oldToken = null) => {
|
||||||
|
const auth = {
|
||||||
|
user: null,
|
||||||
|
token: '',
|
||||||
|
headers: {},
|
||||||
|
}
|
||||||
|
const route = useRoute()
|
||||||
|
const authCookie = useCookie('auth-token', {
|
||||||
|
secure: true,
|
||||||
|
sameSite: 'Strict',
|
||||||
|
httpOnly: false,
|
||||||
|
expires: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
|
||||||
|
path: '/',
|
||||||
|
})
|
||||||
|
|
||||||
|
if (oldToken) {
|
||||||
|
authCookie.value = oldToken
|
||||||
|
}
|
||||||
|
|
||||||
|
if (route.query.code) {
|
||||||
|
authCookie.value = route.query.code
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authCookie.value) {
|
||||||
|
auth.token = authCookie.value
|
||||||
|
try {
|
||||||
|
auth.user = await useBaseFetch('user', {
|
||||||
|
headers: {
|
||||||
|
Authorization: auth.token,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
auth.headers = {
|
||||||
|
headers: {
|
||||||
|
Authorization: auth.token,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return auth
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getAuthUrl = () => {
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
return `${config.public.apiBaseUrl}auth/init?url=${config.public.siteUrl}${route.fullPath}`
|
||||||
|
}
|
||||||
46
composables/cosmetics.js
Normal file
46
composables/cosmetics.js
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
export const useCosmetics = () =>
|
||||||
|
useState('cosmetics', () => {
|
||||||
|
const cosmetics = useCookie('cosmetics', {
|
||||||
|
maxAge: 60 * 60 * 24 * 365 * 10,
|
||||||
|
sameSite: 'Strict',
|
||||||
|
secure: true,
|
||||||
|
httpOnly: false,
|
||||||
|
path: '/',
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!cosmetics.value) {
|
||||||
|
cosmetics.value = {
|
||||||
|
searchLayout: false,
|
||||||
|
projectLayout: false,
|
||||||
|
modpacksAlphaNotice: true,
|
||||||
|
advancedRendering: true,
|
||||||
|
externalLinksNewTab: true,
|
||||||
|
notUsingBlockers: false,
|
||||||
|
searchDisplayMode: {
|
||||||
|
mod: 'list',
|
||||||
|
plugin: 'list',
|
||||||
|
resourcepack: 'gallery',
|
||||||
|
modpack: 'list',
|
||||||
|
shader: 'gallery',
|
||||||
|
datapack: 'list',
|
||||||
|
user: 'list',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cosmetics.value
|
||||||
|
})
|
||||||
|
|
||||||
|
export const saveCosmetics = () => {
|
||||||
|
const cosmetics = useCosmetics()
|
||||||
|
|
||||||
|
const cosmeticsCookie = useCookie('cosmetics', {
|
||||||
|
maxAge: 60 * 60 * 24 * 365 * 10,
|
||||||
|
sameSite: 'Strict',
|
||||||
|
secure: true,
|
||||||
|
httpOnly: false,
|
||||||
|
path: '/',
|
||||||
|
})
|
||||||
|
|
||||||
|
cosmeticsCookie.value = cosmetics.value
|
||||||
|
}
|
||||||
18
composables/date.js
Normal file
18
composables/date.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import dayjs from 'dayjs'
|
||||||
|
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-named-as-default-member
|
||||||
|
dayjs.extend(relativeTime)
|
||||||
|
|
||||||
|
export const useCurrentDate = () => useState('currentDate', () => Date.now())
|
||||||
|
|
||||||
|
export const updateCurrentDate = () => {
|
||||||
|
const currentDate = useCurrentDate()
|
||||||
|
|
||||||
|
currentDate.value = Date.now()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fromNow = (date) => {
|
||||||
|
const currentDate = useCurrentDate()
|
||||||
|
return dayjs(date).from(currentDate.value)
|
||||||
|
}
|
||||||
14
composables/fetch.js
Normal file
14
composables/fetch.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
export const useBaseFetch = async (url, options = {}) => {
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const base = process.server ? config.apiBaseUrl : config.public.apiBaseUrl
|
||||||
|
|
||||||
|
if (options.headers && process.server) {
|
||||||
|
options.headers['x-ratelimit-key'] = config.rateLimitKey
|
||||||
|
} else if (process.server) {
|
||||||
|
options.headers = {
|
||||||
|
'x-ratelimit-key': config.rateLimitKey,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return await $fetch(`${base}${url}`, options)
|
||||||
|
}
|
||||||
13
composables/loading.js
Normal file
13
composables/loading.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
export const useLoading = () => useState('loading', () => false)
|
||||||
|
|
||||||
|
export const startLoading = () => {
|
||||||
|
const loading = useLoading()
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
export const stopLoading = () => {
|
||||||
|
const loading = useLoading()
|
||||||
|
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
34
composables/notifs.js
Normal file
34
composables/notifs.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
export const useNotifications = () => useState('notifications', () => [])
|
||||||
|
|
||||||
|
export const addNotification = (notification) => {
|
||||||
|
const notifications = useNotifications()
|
||||||
|
|
||||||
|
const existingNotif = notifications.value.find(
|
||||||
|
(x) =>
|
||||||
|
x.text === notification.text && x.title === notification.title && x.type === notification.type
|
||||||
|
)
|
||||||
|
if (existingNotif) {
|
||||||
|
setNotificationTimer(existingNotif)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
notification.id = new Date()
|
||||||
|
|
||||||
|
setNotificationTimer(notification)
|
||||||
|
notifications.value.push(notification)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setNotificationTimer = (notification) => {
|
||||||
|
if (!notification) return
|
||||||
|
|
||||||
|
const notifications = useNotifications()
|
||||||
|
|
||||||
|
if (notification.timer) {
|
||||||
|
clearTimeout(notification.timer)
|
||||||
|
}
|
||||||
|
|
||||||
|
notification.timer = setTimeout(() => {
|
||||||
|
notifications.value.splice(notifications.value.indexOf(notification), 1)
|
||||||
|
}, 30000)
|
||||||
|
}
|
||||||
62
composables/tag.js
Normal file
62
composables/tag.js
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import tags from '~/generated/state.json'
|
||||||
|
|
||||||
|
export const useTags = () =>
|
||||||
|
useState('tags', () => ({
|
||||||
|
categories: tags.categories,
|
||||||
|
loaders: tags.loaders,
|
||||||
|
gameVersions: tags.gameVersions,
|
||||||
|
donationPlatforms: tags.donationPlatforms,
|
||||||
|
reportTypes: tags.reportTypes,
|
||||||
|
projectTypes: [
|
||||||
|
{
|
||||||
|
actual: 'mod',
|
||||||
|
id: 'mod',
|
||||||
|
display: 'mod',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
actual: 'mod',
|
||||||
|
id: 'plugin',
|
||||||
|
display: 'plugin',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
actual: 'mod',
|
||||||
|
id: 'datapack',
|
||||||
|
display: 'data pack',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
actual: 'shader',
|
||||||
|
id: 'shader',
|
||||||
|
display: 'shader',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
actual: 'resourcepack',
|
||||||
|
id: 'resourcepack',
|
||||||
|
display: 'resource pack',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
actual: 'modpack',
|
||||||
|
id: 'modpack',
|
||||||
|
display: 'modpack',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
loaderData: {
|
||||||
|
pluginLoaders: ['bukkit', 'spigot', 'paper', 'purpur', 'sponge'],
|
||||||
|
pluginPlatformLoaders: ['bungeecord', 'waterfall', 'velocity'],
|
||||||
|
allPluginLoaders: [
|
||||||
|
'bukkit',
|
||||||
|
'spigot',
|
||||||
|
'paper',
|
||||||
|
'purpur',
|
||||||
|
'sponge',
|
||||||
|
'bungeecord',
|
||||||
|
'waterfall',
|
||||||
|
'velocity',
|
||||||
|
],
|
||||||
|
dataPackLoaders: ['datapack'],
|
||||||
|
modLoaders: ['forge', 'fabric', 'quilt', 'liteloader', 'modloader', 'rift'],
|
||||||
|
},
|
||||||
|
projectViewModes: ['list', 'grid', 'gallery'],
|
||||||
|
approvedStatuses: ['approved', 'archived', 'unlisted', 'private'],
|
||||||
|
rejectedStatuses: ['rejected', 'withheld'],
|
||||||
|
staffRoles: ['moderator', 'admin'],
|
||||||
|
}))
|
||||||
55
composables/theme.js
Normal file
55
composables/theme.js
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
export const useTheme = () =>
|
||||||
|
useState('theme', () => {
|
||||||
|
const colorMode = useCookie('color-mode', {
|
||||||
|
maxAge: 60 * 60 * 24 * 365 * 10,
|
||||||
|
sameSite: 'Strict',
|
||||||
|
secure: true,
|
||||||
|
httpOnly: false,
|
||||||
|
path: '/',
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!colorMode.value) {
|
||||||
|
colorMode.value = {
|
||||||
|
value: 'dark',
|
||||||
|
preference: 'system',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (colorMode.value.preference !== 'system') {
|
||||||
|
colorMode.value.value = colorMode.value.preference
|
||||||
|
}
|
||||||
|
|
||||||
|
return colorMode.value
|
||||||
|
})
|
||||||
|
|
||||||
|
export const updateTheme = (value, updatePreference = false) => {
|
||||||
|
const theme = useTheme()
|
||||||
|
|
||||||
|
const themeCookie = useCookie('color-mode', {
|
||||||
|
maxAge: 60 * 60 * 24 * 365 * 10,
|
||||||
|
sameSite: 'Strict',
|
||||||
|
secure: true,
|
||||||
|
httpOnly: false,
|
||||||
|
path: '/',
|
||||||
|
})
|
||||||
|
|
||||||
|
if (value === 'system') {
|
||||||
|
theme.value.preference = 'system'
|
||||||
|
|
||||||
|
const colorSchemeQueryList = window.matchMedia('(prefers-color-scheme: light)')
|
||||||
|
if (colorSchemeQueryList.matches) {
|
||||||
|
theme.value.value = 'light'
|
||||||
|
} else {
|
||||||
|
theme.value.value = 'dark'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
theme.value.value = value
|
||||||
|
if (updatePreference) theme.value.preference = value
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.client) {
|
||||||
|
document.documentElement.className = `${theme.value.value}-mode`
|
||||||
|
}
|
||||||
|
|
||||||
|
themeCookie.value = theme.value
|
||||||
|
}
|
||||||
112
composables/user.js
Normal file
112
composables/user.js
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
export const useUser = async (force = false) => {
|
||||||
|
const user = useState('user', () => {})
|
||||||
|
|
||||||
|
if (!user.value || force || (user.value && Date.now() - user.value.lastUpdated > 300000)) {
|
||||||
|
user.value = await initUser()
|
||||||
|
}
|
||||||
|
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
export const initUser = async () => {
|
||||||
|
const auth = (await useAuth()).value
|
||||||
|
|
||||||
|
const user = {
|
||||||
|
notifications: [],
|
||||||
|
follows: [],
|
||||||
|
projects: [],
|
||||||
|
lastUpdated: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auth.user && auth.user.id) {
|
||||||
|
try {
|
||||||
|
const [notifications, follows, projects] = await Promise.all([
|
||||||
|
useBaseFetch(`user/${auth.user.id}/notifications`, auth.headers),
|
||||||
|
useBaseFetch(`user/${auth.user.id}/follows`, auth.headers),
|
||||||
|
useBaseFetch(`user/${auth.user.id}/projects`, auth.headers),
|
||||||
|
])
|
||||||
|
|
||||||
|
user.notifications = notifications
|
||||||
|
user.follows = follows
|
||||||
|
user.projects = projects
|
||||||
|
user.lastUpdated = Date.now()
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
export const initUserNotifs = async () => {
|
||||||
|
const auth = (await useAuth()).value
|
||||||
|
const user = (await useUser()).value
|
||||||
|
|
||||||
|
if (auth.user && auth.user.id) {
|
||||||
|
try {
|
||||||
|
user.notifications = await useBaseFetch(`user/${auth.user.id}/notifications`, auth.headers)
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const initUserFollows = async () => {
|
||||||
|
const auth = (await useAuth()).value
|
||||||
|
const user = (await useUser()).value
|
||||||
|
|
||||||
|
if (auth.user && auth.user.id) {
|
||||||
|
try {
|
||||||
|
user.follows = await useBaseFetch(`user/${auth.user.id}/follows`, auth.headers)
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const initUserProjects = async () => {
|
||||||
|
const auth = (await useAuth()).value
|
||||||
|
const user = (await useUser()).value
|
||||||
|
|
||||||
|
if (auth.user && auth.user.id) {
|
||||||
|
try {
|
||||||
|
user.projects = await useBaseFetch(`user/${auth.user.id}/projects`, auth.headers)
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const userFollowProject = async (project) => {
|
||||||
|
const auth = (await useAuth()).value
|
||||||
|
const user = (await useUser()).value
|
||||||
|
|
||||||
|
user.follows = user.follows.concat(project)
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
useBaseFetch(`project/${project.id}/follow`, {
|
||||||
|
method: 'POST',
|
||||||
|
...auth.headers,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const userUnfollowProject = async (project) => {
|
||||||
|
const auth = (await useAuth()).value
|
||||||
|
const user = (await useUser()).value
|
||||||
|
|
||||||
|
user.follows = user.follows.filter((x) => x.id !== project.id)
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
useBaseFetch(`project/${project.id}/follow`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
...auth.headers,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const userDeleteNotification = async (id) => {
|
||||||
|
const user = (await useUser()).value
|
||||||
|
|
||||||
|
user.notifications = user.notifications.filter((x) => x.id !== id)
|
||||||
|
}
|
||||||
@@ -1,33 +1,34 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="main">
|
<NuxtLayout>
|
||||||
<div class="error">
|
<div class="main">
|
||||||
<Logo404 v-if="error.statusCode === 404" />
|
<div class="error">
|
||||||
<h1 v-else>An error occurred!</h1>
|
<Logo404 v-if="error.statusCode === '404'" />
|
||||||
<p>{{ error.message }}</p>
|
<h1 v-else>An error occurred!</h1>
|
||||||
<div class="button-group">
|
<p>{{ error.message }}</p>
|
||||||
<nuxt-link to="/" class="iconified-button raised-button brand-button">
|
<div class="button-group">
|
||||||
Go home
|
<nuxt-link to="/" class="iconified-button raised-button brand-button">
|
||||||
</nuxt-link>
|
Go home
|
||||||
<a
|
</nuxt-link>
|
||||||
href="https://discord.gg/EUHuJHt"
|
<a
|
||||||
class="iconified-button raised-button"
|
href="https://discord.gg/EUHuJHt"
|
||||||
rel="noopener noreferrer nofollow"
|
class="iconified-button raised-button"
|
||||||
>
|
rel="noopener"
|
||||||
Get help on Discord
|
>
|
||||||
</a>
|
Get help on Discord
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</NuxtLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Logo404 from '~/assets/images/404.svg?inline'
|
import Logo404 from './assets/images/404.svg'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
Logo404,
|
Logo404,
|
||||||
},
|
},
|
||||||
layout: 'home',
|
|
||||||
props: {
|
props: {
|
||||||
error: {
|
error: {
|
||||||
type: Object,
|
type: Object,
|
||||||
32
helpers/fileUtils.js
Normal file
32
helpers/fileUtils.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { formatBytes } from '~/plugins/shorthands'
|
||||||
|
|
||||||
|
export const fileIsValid = (file, validationOptions) => {
|
||||||
|
const { maxSize, alertOnInvalid } = validationOptions
|
||||||
|
if (maxSize !== null && maxSize !== undefined && file.size > maxSize) {
|
||||||
|
if (alertOnInvalid) {
|
||||||
|
alert(`File ${file.name} is too big! Must be less than ${formatBytes(maxSize)}`)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
export const acceptFileFromProjectType = (projectType) => {
|
||||||
|
switch (projectType) {
|
||||||
|
case 'mod':
|
||||||
|
return '.jar,.zip,.litemod,application/java-archive,application/x-java-archive,application/zip'
|
||||||
|
case 'plugin':
|
||||||
|
return '.jar,.zip,application/java-archive,application/x-java-archive,application/zip'
|
||||||
|
case 'resourcepack':
|
||||||
|
return '.zip,application/zip'
|
||||||
|
case 'shader':
|
||||||
|
return '.zip,application/zip'
|
||||||
|
case 'datapack':
|
||||||
|
return '.zip,application/zip'
|
||||||
|
case 'modpack':
|
||||||
|
return '.mrpack,application/x-modrinth-modpack+zip,application/zip'
|
||||||
|
default:
|
||||||
|
return '*'
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import Vue from 'vue'
|
|
||||||
import hljs from 'highlight.js/lib/core'
|
import hljs from 'highlight.js/lib/core'
|
||||||
// Scripting
|
// Scripting
|
||||||
import javascript from 'highlight.js/lib/languages/javascript'
|
import javascript from 'highlight.js/lib/languages/javascript'
|
||||||
@@ -16,6 +15,7 @@ import ini from 'highlight.js/lib/languages/ini'
|
|||||||
import yaml from 'highlight.js/lib/languages/yaml'
|
import yaml from 'highlight.js/lib/languages/yaml'
|
||||||
import xml from 'highlight.js/lib/languages/xml'
|
import xml from 'highlight.js/lib/languages/xml'
|
||||||
import properties from 'highlight.js/lib/languages/properties'
|
import properties from 'highlight.js/lib/languages/properties'
|
||||||
|
import { md, configuredXss } from '~/helpers/parse'
|
||||||
|
|
||||||
/* REGISTRATION */
|
/* REGISTRATION */
|
||||||
// Scripting
|
// Scripting
|
||||||
@@ -37,38 +37,27 @@ hljs.registerLanguage('properties', properties)
|
|||||||
|
|
||||||
/* ALIASES */
|
/* ALIASES */
|
||||||
// Scripting
|
// Scripting
|
||||||
hljs.registerAliases(['js'], 'javascript')
|
hljs.registerAliases(['js'], { languageName: 'javascript' })
|
||||||
hljs.registerAliases(['py'], 'python')
|
hljs.registerAliases(['py'], { languageName: 'python' })
|
||||||
// Coding
|
// Coding
|
||||||
hljs.registerAliases(['kt'], 'kotlin')
|
hljs.registerAliases(['kt'], { languageName: 'kotlin' })
|
||||||
// Configs
|
// Configs
|
||||||
hljs.registerAliases(['json5'], 'json')
|
hljs.registerAliases(['json5'], { languageName: 'json' })
|
||||||
hljs.registerAliases(['toml'], 'ini')
|
hljs.registerAliases(['toml'], { languageName: 'ini' })
|
||||||
hljs.registerAliases(['yml'], 'yaml')
|
hljs.registerAliases(['yml'], { languageName: 'yaml' })
|
||||||
hljs.registerAliases(['html', 'htm', 'xhtml', 'mcui', 'fxml'], 'xml')
|
hljs.registerAliases(['html', 'htm', 'xhtml', 'mcui', 'fxml'], { languageName: 'xml' })
|
||||||
|
|
||||||
Vue.directive('highlightjs', {
|
export const renderHighlightedString = (string) =>
|
||||||
deep: true,
|
configuredXss.process(
|
||||||
bind(el, binding) {
|
md({
|
||||||
// on first bind, highlight all targets
|
highlight: function (str, lang) {
|
||||||
const targets = el.querySelectorAll('pre > code')
|
if (lang && hljs.getLanguage(lang)) {
|
||||||
targets.forEach((target) => {
|
try {
|
||||||
// if a value is directly assigned to the directive, use this
|
return hljs.highlight(str, { language: lang }).value
|
||||||
// instead of the element content.
|
} catch (__) {}
|
||||||
if (binding.value) {
|
}
|
||||||
target.textContent = binding.value
|
|
||||||
}
|
return ''
|
||||||
hljs.highlightBlock(target)
|
},
|
||||||
})
|
}).render(string)
|
||||||
},
|
)
|
||||||
componentUpdated(el, binding) {
|
|
||||||
// after an update, re-fill the content and then highlight
|
|
||||||
const targets = el.querySelectorAll('pre > code')
|
|
||||||
targets.forEach((target) => {
|
|
||||||
if (binding.value) {
|
|
||||||
target.textContent = binding.value
|
|
||||||
hljs.highlightBlock(target)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
305
helpers/infer.js
Normal file
305
helpers/infer.js
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
import TOML from 'toml'
|
||||||
|
import JSZip from 'jszip'
|
||||||
|
import yaml from 'js-yaml'
|
||||||
|
|
||||||
|
export const inferVersionInfo = async function (rawFile, project, gameVersions) {
|
||||||
|
function versionType(number) {
|
||||||
|
if (number.includes('alpha')) {
|
||||||
|
return 'alpha'
|
||||||
|
} else if (
|
||||||
|
number.includes('beta') ||
|
||||||
|
number.match(/[^A-z](rc)[^A-z]/) || // includes `rc`
|
||||||
|
number.match(/[^A-z](pre)[^A-z]/) // includes `pre`
|
||||||
|
) {
|
||||||
|
return 'beta'
|
||||||
|
} else {
|
||||||
|
return 'release'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: This func does not handle accurate semver parsing. We should eventually
|
||||||
|
function gameVersionRange(gameVersionString, gameVersions) {
|
||||||
|
if (!gameVersionString) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
// Truncate characters after `-` & `+`
|
||||||
|
const gameString = gameVersionString.replace(/-|\+.*$/g, '')
|
||||||
|
|
||||||
|
let prefix = ''
|
||||||
|
if (gameString.includes('~')) {
|
||||||
|
// Include minor versions
|
||||||
|
// ~1.2.3 -> 1.2
|
||||||
|
prefix = gameString.replace('~', '').split('.').slice(0, 2).join('.')
|
||||||
|
} else if (gameString.includes('>=')) {
|
||||||
|
// Include minor versions
|
||||||
|
// >=1.2.3 -> 1.2
|
||||||
|
prefix = gameString.replace('>=', '').split('.').slice(0, 2).join('.')
|
||||||
|
} else if (gameString.includes('^')) {
|
||||||
|
// Include major versions
|
||||||
|
// ^1.2.3 -> 1
|
||||||
|
prefix = gameString.replace('^', '').split('.')[0]
|
||||||
|
} else if (gameString.includes('x')) {
|
||||||
|
// Include versions that match `x.x.x`
|
||||||
|
// 1.2.x -> 1.2
|
||||||
|
prefix = gameString.replace(/\.x$/, '')
|
||||||
|
} else {
|
||||||
|
// Include exact version
|
||||||
|
// 1.2.3 -> 1.2.3
|
||||||
|
prefix = gameString
|
||||||
|
}
|
||||||
|
|
||||||
|
const simplified = gameVersions
|
||||||
|
.filter((it) => it.version_type === 'release')
|
||||||
|
.map((it) => it.version)
|
||||||
|
return simplified.filter((version) => version.startsWith(prefix))
|
||||||
|
}
|
||||||
|
|
||||||
|
const inferFunctions = {
|
||||||
|
// Forge 1.13+
|
||||||
|
'META-INF/mods.toml': async (file, zip) => {
|
||||||
|
const metadata = TOML.parse(file)
|
||||||
|
|
||||||
|
// TODO: Parse minecraft version ranges, handle if version is set to value from manifest
|
||||||
|
if (metadata.mods && metadata.mods.length > 0) {
|
||||||
|
let versionNum = metadata.mods[0].version
|
||||||
|
|
||||||
|
// ${file.jarVersion} -> Implementation-Version from manifest
|
||||||
|
const manifestFile = zip.file('META-INF/MANIFEST.MF')
|
||||||
|
if (
|
||||||
|
// eslint-disable-next-line no-template-curly-in-string
|
||||||
|
metadata.mods[0].version.includes('${file.jarVersion}') &&
|
||||||
|
manifestFile !== null
|
||||||
|
) {
|
||||||
|
const manifestText = await manifestFile.async('text')
|
||||||
|
const regex = /Implementation-Version: (.*)$/m
|
||||||
|
const match = manifestText.match(regex)
|
||||||
|
if (match) {
|
||||||
|
// eslint-disable-next-line no-template-curly-in-string
|
||||||
|
versionNum = versionNum.replace('${file.jarVersion}', match[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: `${project.title} ${versionNum}`,
|
||||||
|
version_number: versionNum,
|
||||||
|
version_type: versionType(versionNum),
|
||||||
|
loaders: ['forge'],
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Old Forge
|
||||||
|
'mcmod.info': (file) => {
|
||||||
|
const metadata = JSON.parse(file)
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: metadata.version ? `${project.title} ${metadata.version}` : '',
|
||||||
|
version_number: metadata.version,
|
||||||
|
version_type: versionType(metadata.version),
|
||||||
|
loaders: ['forge'],
|
||||||
|
game_versions: gameVersions
|
||||||
|
.filter((x) => x.version.startsWith(metadata.mcversion) && x.version_type === 'release')
|
||||||
|
.map((x) => x.version),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Fabric
|
||||||
|
'fabric.mod.json': (file) => {
|
||||||
|
const metadata = JSON.parse(file)
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: `${project.title} ${metadata.version}`,
|
||||||
|
version_number: metadata.version,
|
||||||
|
loaders: ['fabric'],
|
||||||
|
version_type: versionType(metadata.version),
|
||||||
|
game_versions: metadata.depends
|
||||||
|
? gameVersionRange(metadata.depends.minecraft, gameVersions)
|
||||||
|
: [],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Quilt
|
||||||
|
'quilt.mod.json': (file) => {
|
||||||
|
const metadata = JSON.parse(file)
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: `${project.title} ${metadata.quilt_loader.version}`,
|
||||||
|
version_number: metadata.quilt_loader.version,
|
||||||
|
loaders: ['quilt'],
|
||||||
|
version_type: versionType(metadata.quilt_loader.version),
|
||||||
|
game_versions: metadata.quilt_loader.depends
|
||||||
|
? gameVersionRange(
|
||||||
|
metadata.quilt_loader.depends.find((x) => x.id === 'minecraft')
|
||||||
|
? metadata.quilt_loader.depends.find((x) => x.id === 'minecraft').versions
|
||||||
|
: [],
|
||||||
|
gameVersions
|
||||||
|
)
|
||||||
|
: [],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Bukkit + Other Forks
|
||||||
|
'plugin.yml': (file) => {
|
||||||
|
const metadata = yaml.load(file)
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: `${project.title} ${metadata.version}`,
|
||||||
|
version_number: metadata.version,
|
||||||
|
version_type: versionType(metadata.version),
|
||||||
|
// We don't know which fork of Bukkit users are using
|
||||||
|
loaders: [],
|
||||||
|
game_versions: gameVersions
|
||||||
|
.filter(
|
||||||
|
(x) => x.version.startsWith(metadata['api-version']) && x.version_type === 'release'
|
||||||
|
)
|
||||||
|
.map((x) => x.version),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Bungeecord + Waterfall
|
||||||
|
'bungee.yml': (file) => {
|
||||||
|
const metadata = yaml.load(file)
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: `${project.title} ${metadata.version}`,
|
||||||
|
version_number: metadata.version,
|
||||||
|
version_type: versionType(metadata.version),
|
||||||
|
loaders: ['bungeecord'],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Modpacks
|
||||||
|
'modrinth.index.json': (file) => {
|
||||||
|
const metadata = JSON.parse(file)
|
||||||
|
|
||||||
|
const loaders = []
|
||||||
|
if ('forge' in metadata.dependencies) {
|
||||||
|
loaders.push('forge')
|
||||||
|
}
|
||||||
|
if ('fabric-loader' in metadata.dependencies) {
|
||||||
|
loaders.push('fabric')
|
||||||
|
}
|
||||||
|
if ('quilt-loader' in metadata.dependencies) {
|
||||||
|
loaders.push('quilt')
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: `${project.title} ${metadata.versionId}`,
|
||||||
|
version_number: metadata.versionId,
|
||||||
|
version_type: versionType(metadata.versionId),
|
||||||
|
loaders,
|
||||||
|
game_versions: gameVersions
|
||||||
|
.filter((x) => x.version === metadata.dependencies.minecraft)
|
||||||
|
.map((x) => x.version),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Resource Packs + Data Packs
|
||||||
|
'pack.mcmeta': (file) => {
|
||||||
|
const metadata = JSON.parse(file)
|
||||||
|
|
||||||
|
function getRange(versionA, versionB) {
|
||||||
|
const startingIndex = gameVersions.findIndex((x) => x.version === versionA)
|
||||||
|
const endingIndex = gameVersions.findIndex((x) => x.version === versionB)
|
||||||
|
|
||||||
|
const final = []
|
||||||
|
const filterOnlyRelease = gameVersions[startingIndex].version_type === 'release'
|
||||||
|
|
||||||
|
for (let i = startingIndex; i >= endingIndex; i--) {
|
||||||
|
if (gameVersions[i].version_type === 'release' || !filterOnlyRelease) {
|
||||||
|
final.push(gameVersions[i].version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return final
|
||||||
|
}
|
||||||
|
|
||||||
|
const loaders = []
|
||||||
|
let newGameVersions = []
|
||||||
|
|
||||||
|
if (project.actualProjectType === 'mod') {
|
||||||
|
loaders.push('datapack')
|
||||||
|
|
||||||
|
switch (metadata.pack.pack_format) {
|
||||||
|
case 4:
|
||||||
|
newGameVersions = getRange('1.13', '1.14.4')
|
||||||
|
break
|
||||||
|
case 5:
|
||||||
|
newGameVersions = getRange('1.15', '1.16.1')
|
||||||
|
break
|
||||||
|
case 6:
|
||||||
|
newGameVersions = getRange('1.16.2', '1.16.5')
|
||||||
|
break
|
||||||
|
case 7:
|
||||||
|
newGameVersions = getRange('1.17', '1.17.1')
|
||||||
|
break
|
||||||
|
case 8:
|
||||||
|
newGameVersions = getRange('1.18', '1.18.1')
|
||||||
|
break
|
||||||
|
case 9:
|
||||||
|
newGameVersions.push('1.18.2')
|
||||||
|
break
|
||||||
|
case 10:
|
||||||
|
newGameVersions = getRange('1.19', '1.19.3')
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (project.actualProjectType === 'resourcepack') {
|
||||||
|
loaders.push('minecraft')
|
||||||
|
|
||||||
|
switch (metadata.pack.pack_format) {
|
||||||
|
case 1:
|
||||||
|
newGameVersions = getRange('1.6.1', '1.8.9')
|
||||||
|
break
|
||||||
|
case 2:
|
||||||
|
newGameVersions = getRange('1.9', '1.10.2')
|
||||||
|
break
|
||||||
|
case 3:
|
||||||
|
newGameVersions = getRange('1.11', '1.12.2')
|
||||||
|
break
|
||||||
|
case 4:
|
||||||
|
newGameVersions = getRange('1.13', '1.14.4')
|
||||||
|
break
|
||||||
|
case 5:
|
||||||
|
newGameVersions = getRange('1.15', '1.16.1')
|
||||||
|
break
|
||||||
|
case 6:
|
||||||
|
newGameVersions = getRange('1.16.2', '1.16.5')
|
||||||
|
break
|
||||||
|
case 7:
|
||||||
|
newGameVersions = getRange('1.17', '1.17.1')
|
||||||
|
break
|
||||||
|
case 8:
|
||||||
|
newGameVersions = getRange('1.18', '1.18.2')
|
||||||
|
break
|
||||||
|
case 9:
|
||||||
|
newGameVersions = getRange('1.19', '1.19.2')
|
||||||
|
break
|
||||||
|
case 11:
|
||||||
|
newGameVersions = getRange('22w42a', '22w44a')
|
||||||
|
break
|
||||||
|
case 12:
|
||||||
|
newGameVersions.push('1.19.3')
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
loaders,
|
||||||
|
game_versions: newGameVersions,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const zipReader = new JSZip()
|
||||||
|
|
||||||
|
const zip = await zipReader.loadAsync(rawFile)
|
||||||
|
|
||||||
|
for (const fileName in inferFunctions) {
|
||||||
|
const file = zip.file(fileName)
|
||||||
|
|
||||||
|
if (file !== null) {
|
||||||
|
const text = await file.async('text')
|
||||||
|
return inferFunctions[fileName](text, zip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
198
helpers/package.js
Normal file
198
helpers/package.js
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
import JSZip from 'jszip'
|
||||||
|
import TOML from 'toml'
|
||||||
|
|
||||||
|
export const createDataPackVersion = async function (
|
||||||
|
project,
|
||||||
|
version,
|
||||||
|
primaryFile,
|
||||||
|
members,
|
||||||
|
allGameVersions,
|
||||||
|
loaders
|
||||||
|
) {
|
||||||
|
// force version to start with number, as required by FML
|
||||||
|
const newVersionNumber = version.version_number.match(/^\d/)
|
||||||
|
? version.version_number
|
||||||
|
: `1-${version.version_number}`
|
||||||
|
|
||||||
|
const targetStartingDigitsRegex = /^(\d+)(\D+)$/g
|
||||||
|
const newSlug = `${project.slug
|
||||||
|
.replace('-', '_')
|
||||||
|
.replace(/\W/g, '')
|
||||||
|
.replace(targetStartingDigitsRegex, '$2')
|
||||||
|
.replace(/^(\d+)$/g, project.id.replace(targetStartingDigitsRegex, '$2'))
|
||||||
|
.substring(0, 63)}_mr`
|
||||||
|
|
||||||
|
const iconPath = `${project.slug}_pack.png`
|
||||||
|
|
||||||
|
const fabricModJson = {
|
||||||
|
schemaVersion: 1,
|
||||||
|
id: newSlug,
|
||||||
|
version: newVersionNumber,
|
||||||
|
name: project.title,
|
||||||
|
description: project.description,
|
||||||
|
authors: members.map((x) => x.name),
|
||||||
|
contact: {
|
||||||
|
homepage: `${process.env.domain}/${project.project_type}/${project.slug ?? project.id}`,
|
||||||
|
},
|
||||||
|
license: project.license.id,
|
||||||
|
icon: iconPath,
|
||||||
|
environment: '*',
|
||||||
|
depends: {
|
||||||
|
'fabric-resource-loader-v0': '*',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const quiltModJson = {
|
||||||
|
schema_version: 1,
|
||||||
|
quilt_loader: {
|
||||||
|
group: 'com.modrinth',
|
||||||
|
id: newSlug,
|
||||||
|
version: newVersionNumber,
|
||||||
|
metadata: {
|
||||||
|
name: project.title,
|
||||||
|
description: project.description,
|
||||||
|
contributors: members.reduce(
|
||||||
|
(acc, x) => ({
|
||||||
|
...acc,
|
||||||
|
[x.name]: x.role,
|
||||||
|
}),
|
||||||
|
{}
|
||||||
|
),
|
||||||
|
contact: {
|
||||||
|
homepage: `${process.env.domain}/${project.project_type}/${project.slug ?? project.id}`,
|
||||||
|
},
|
||||||
|
icon: iconPath,
|
||||||
|
},
|
||||||
|
intermediate_mappings: 'net.fabricmc:intermediary',
|
||||||
|
depends: [
|
||||||
|
{
|
||||||
|
id: 'quilt_resource_loader',
|
||||||
|
versions: '*',
|
||||||
|
unless: 'fabric-resource-loader-v0',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const cutoffIndex = allGameVersions.findIndex((x) => x.version === '1.18.2')
|
||||||
|
|
||||||
|
let maximumIndex = Number.MIN_VALUE
|
||||||
|
for (const val of version.game_versions) {
|
||||||
|
const index = allGameVersions.findIndex((x) => x.version === val)
|
||||||
|
if (index > maximumIndex) {
|
||||||
|
maximumIndex = index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newForge = maximumIndex < cutoffIndex
|
||||||
|
|
||||||
|
const forgeModsToml = {
|
||||||
|
modLoader: newForge ? 'lowcodefml' : 'javafml',
|
||||||
|
loaderVersion: newForge ? '[40,)' : '[25,)',
|
||||||
|
license: project.license.id,
|
||||||
|
showAsResourcePack: false,
|
||||||
|
mods: [
|
||||||
|
{
|
||||||
|
modId: newSlug,
|
||||||
|
version: newVersionNumber,
|
||||||
|
displayName: project.title,
|
||||||
|
description: project.description,
|
||||||
|
logoFile: iconPath,
|
||||||
|
updateJSONURL: `${getAuthUrl().replace('/v2/', '')}/updates/${
|
||||||
|
project.id
|
||||||
|
}/forge_updates.json`,
|
||||||
|
credits: 'Generated by Modrinth',
|
||||||
|
authors: members.map((x) => x.name).join(', '),
|
||||||
|
displayURL: `${process.env.domain}/${project.project_type}/${project.slug ?? project.id}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
if (project.source_url) {
|
||||||
|
quiltModJson.quilt_loader.metadata.contact.sources = project.source_url
|
||||||
|
fabricModJson.contact.sources = project.source_url
|
||||||
|
}
|
||||||
|
|
||||||
|
if (project.issues_url) {
|
||||||
|
quiltModJson.quilt_loader.metadata.contact.issues = project.issues_url
|
||||||
|
fabricModJson.contact.issues = project.issues_url
|
||||||
|
forgeModsToml.issueTrackerURL = project.issues_url
|
||||||
|
}
|
||||||
|
|
||||||
|
const primaryFileData = await (await fetch(primaryFile.url)).blob()
|
||||||
|
|
||||||
|
const primaryZipReader = new JSZip()
|
||||||
|
await primaryZipReader.loadAsync(primaryFileData)
|
||||||
|
|
||||||
|
if (loaders.includes('fabric')) {
|
||||||
|
primaryZipReader.file('fabric.mod.json', JSON.stringify(fabricModJson))
|
||||||
|
}
|
||||||
|
if (loaders.includes('quilt')) {
|
||||||
|
primaryZipReader.file('quilt.mod.json', JSON.stringify(quiltModJson))
|
||||||
|
}
|
||||||
|
if (loaders.includes('forge')) {
|
||||||
|
primaryZipReader.file('META-INF/mods.toml', TOML.stringify(forgeModsToml))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!newForge && loaders.includes('forge')) {
|
||||||
|
const classFile = new Uint8Array(
|
||||||
|
await (
|
||||||
|
await fetch('https://cdn.modrinth.com/wrapper/ModrinthWrapperRestiched.class')
|
||||||
|
).arrayBuffer()
|
||||||
|
)
|
||||||
|
|
||||||
|
let binary = ''
|
||||||
|
for (let i = 0; i < classFile.byteLength; i++) {
|
||||||
|
binary += String.fromCharCode(classFile[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
let sanitizedId = project.id
|
||||||
|
|
||||||
|
if (project.id.match(/^(\d+)/g)) {
|
||||||
|
sanitizedId = '_' + sanitizedId
|
||||||
|
}
|
||||||
|
|
||||||
|
sanitizedId = sanitizedId.substring(0, 8)
|
||||||
|
|
||||||
|
binary = binary
|
||||||
|
.replace(
|
||||||
|
String.fromCharCode(32) + 'needs1to1be1changed1modrinth1mod',
|
||||||
|
String.fromCharCode(newSlug.length) + newSlug
|
||||||
|
)
|
||||||
|
.replace('/wrappera/', `/${sanitizedId}/`)
|
||||||
|
|
||||||
|
const newArr = []
|
||||||
|
for (let i = 0; i < binary.length; i++) {
|
||||||
|
newArr.push(binary.charCodeAt(i))
|
||||||
|
}
|
||||||
|
|
||||||
|
primaryZipReader.file(
|
||||||
|
`com/modrinth/${sanitizedId}/ModrinthWrapper.class`,
|
||||||
|
new Uint8Array(newArr)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const resourcePack = version.files.find((x) => x.file_type === 'required-resource-pack')
|
||||||
|
|
||||||
|
const resourcePackData = resourcePack ? await (await fetch(resourcePack.url)).blob() : null
|
||||||
|
|
||||||
|
if (resourcePackData) {
|
||||||
|
const resourcePackReader = new JSZip()
|
||||||
|
await resourcePackReader.loadAsync(resourcePackData)
|
||||||
|
|
||||||
|
for (const [path, file] of Object.entries(resourcePackReader.files)) {
|
||||||
|
if (!primaryZipReader.file(path) && !path.includes('.mcassetsroot')) {
|
||||||
|
primaryZipReader.file(path, await file.async('uint8array'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (primaryZipReader.file('pack.png')) {
|
||||||
|
primaryZipReader.file(iconPath, await primaryZipReader.file('pack.png').async('uint8array'))
|
||||||
|
}
|
||||||
|
|
||||||
|
return await primaryZipReader.generateAsync({
|
||||||
|
type: 'blob',
|
||||||
|
mimeType: 'application/java-archive',
|
||||||
|
})
|
||||||
|
}
|
||||||
131
helpers/parse.js
Normal file
131
helpers/parse.js
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import MarkdownIt from 'markdown-it'
|
||||||
|
import xss from 'xss'
|
||||||
|
|
||||||
|
export const configuredXss = new xss.FilterXSS({
|
||||||
|
whiteList: {
|
||||||
|
...xss.whiteList,
|
||||||
|
summary: [],
|
||||||
|
h1: ['id'],
|
||||||
|
h2: ['id'],
|
||||||
|
h3: ['id'],
|
||||||
|
h4: ['id'],
|
||||||
|
h5: ['id'],
|
||||||
|
h6: ['id'],
|
||||||
|
kbd: ['id'],
|
||||||
|
input: ['checked', 'disabled', 'type'],
|
||||||
|
iframe: ['width', 'height', 'allowfullscreen', 'frameborder', 'start', 'end'],
|
||||||
|
img: [...xss.whiteList.img, 'style'],
|
||||||
|
a: [...xss.whiteList.a, 'rel'],
|
||||||
|
},
|
||||||
|
css: {
|
||||||
|
whiteList: {
|
||||||
|
'image-rendering': /^pixelated$/,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onIgnoreTagAttr: (tag, name, value) => {
|
||||||
|
// Allow iframes from acceptable sources
|
||||||
|
if (tag === 'iframe' && name === 'src') {
|
||||||
|
const allowedSources = [
|
||||||
|
{
|
||||||
|
regex:
|
||||||
|
/^https?:\/\/(www\.)?youtube(-nocookie)?\.com\/embed\/[a-zA-Z0-9_-]{11}(\?&autoplay=[0-1]{1})?$/,
|
||||||
|
remove: ['&autoplay=1'], // Prevents autoplay
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const source of allowedSources) {
|
||||||
|
if (source.regex.test(value)) {
|
||||||
|
for (const remove of source.remove) {
|
||||||
|
value = value.replace(remove, '')
|
||||||
|
}
|
||||||
|
return name + '="' + xss.escapeAttrValue(value) + '"'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For Highlight.JS
|
||||||
|
if (
|
||||||
|
name === 'class' &&
|
||||||
|
['pre', 'code', 'span'].includes(tag) &&
|
||||||
|
(value.startsWith('hljs-') || value.startsWith('language-'))
|
||||||
|
) {
|
||||||
|
return name + '="' + xss.escapeAttrValue(value) + '"'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const md = (options = {}) => {
|
||||||
|
const md = new MarkdownIt('default', {
|
||||||
|
html: true,
|
||||||
|
linkify: true,
|
||||||
|
breaks: false,
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
|
||||||
|
const defaultLinkOpenRenderer =
|
||||||
|
md.renderer.rules.link_open ||
|
||||||
|
function (tokens, idx, options, _env, self) {
|
||||||
|
return self.renderToken(tokens, idx, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
md.renderer.rules.link_open = function (tokens, idx, options, env, self) {
|
||||||
|
const token = tokens[idx]
|
||||||
|
const index = token.attrIndex('href')
|
||||||
|
|
||||||
|
if (index !== -1) {
|
||||||
|
const href = token.attrs[index][1]
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = new URL(href)
|
||||||
|
const allowedHostnames = ['modrinth.com']
|
||||||
|
|
||||||
|
if (allowedHostnames.includes(url.hostname)) {
|
||||||
|
return defaultLinkOpenRenderer(tokens, idx, options, env, self)
|
||||||
|
}
|
||||||
|
} catch (err) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
tokens[idx].attrSet('rel', 'noopener nofollow ugc')
|
||||||
|
|
||||||
|
return defaultLinkOpenRenderer(tokens, idx, options, env, self)
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultImageRenderer =
|
||||||
|
md.renderer.rules.image ||
|
||||||
|
function (tokens, idx, options, _env, self) {
|
||||||
|
return self.renderToken(tokens, idx, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
md.renderer.rules.image = function (tokens, idx, options, env, self) {
|
||||||
|
const token = tokens[idx]
|
||||||
|
const index = token.attrIndex('src')
|
||||||
|
|
||||||
|
if (index !== -1) {
|
||||||
|
const src = token.attrs[index][1]
|
||||||
|
|
||||||
|
const url = new URL(src)
|
||||||
|
try {
|
||||||
|
const allowedHostnames = [
|
||||||
|
'i.imgur.com',
|
||||||
|
'cdn-raw.modrinth.com',
|
||||||
|
'cdn.modrinth.com',
|
||||||
|
'staging-cdn-raw.modrinth.com',
|
||||||
|
'staging-cdn.modrinth.com',
|
||||||
|
'raw.githubusercontent.com',
|
||||||
|
'img.shields.io',
|
||||||
|
]
|
||||||
|
|
||||||
|
if (allowedHostnames.includes(url.hostname)) {
|
||||||
|
return defaultImageRenderer(tokens, idx, options, env, self)
|
||||||
|
}
|
||||||
|
} catch (err) {}
|
||||||
|
token.attrs[index][1] = `//wsrv.nl/?url=${encodeURIComponent(src)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultImageRenderer(tokens, idx, options, env, self)
|
||||||
|
}
|
||||||
|
|
||||||
|
return md
|
||||||
|
}
|
||||||
|
|
||||||
|
export const renderString = (string) => configuredXss.process(md().render(string))
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,24 +0,0 @@
|
|||||||
export default function (context) {
|
|
||||||
if (!process.client) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
context.from &&
|
|
||||||
context.route &&
|
|
||||||
context.from.path === context.route.path
|
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
context.$axios
|
|
||||||
.post(`${context.$config.analytics.base_url}view`, {
|
|
||||||
url: process.env.domain + context.route.fullPath,
|
|
||||||
})
|
|
||||||
.then(() => {})
|
|
||||||
.catch((e) => {
|
|
||||||
console.error('An error occurred while registering the visit: ', e)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,73 +1,7 @@
|
|||||||
export default async function (context) {
|
export default defineNuxtRouteMiddleware(async () => {
|
||||||
if (!context.from) {
|
const auth = await useAuth()
|
||||||
if (context.app.$cookies.get('auth-token-reset')) {
|
|
||||||
// Only remove the cookie related to the auth, instead of removing everything
|
|
||||||
context.app.$cookies.remove('auth-token')
|
|
||||||
context.app.$cookies.remove('auth-token-reset')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (context.route.query.code) {
|
if (!auth.value.user) {
|
||||||
const date = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) // 30 days
|
return navigateTo(getAuthUrl(), { external: true })
|
||||||
context.app.$cookies.set('auth-token', context.route.query.code, {
|
|
||||||
secure: true,
|
|
||||||
sameSite: 'Strict',
|
|
||||||
httpOnly: true,
|
|
||||||
expires: date,
|
|
||||||
path: '/',
|
|
||||||
})
|
|
||||||
|
|
||||||
await context.store.dispatch('auth/fetchUser', {
|
|
||||||
token: context.route.query.code,
|
|
||||||
})
|
|
||||||
} else if (context.app.$cookies.get('auth-token')) {
|
|
||||||
const cookie = context.app.$cookies.get('auth-token')
|
|
||||||
|
|
||||||
await context.store.dispatch('auth/fetchUser', { token: cookie })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
// Disable middleware if options: { auth: false } is set on the route
|
|
||||||
if (routeOption(context.route, 'auth', false)) return
|
|
||||||
|
|
||||||
// Disable middleware if no route was matched to allow 404/error page
|
|
||||||
if (!getMatchedComponents(context.route, []).length) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!context.$auth.user) {
|
|
||||||
return context.redirect(
|
|
||||||
`${process.env.authURLBase}auth/init?url=${process.env.domain}${context.route.fullPath}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function routeOption(route, key, value) {
|
|
||||||
return route.matched.some((m) => {
|
|
||||||
if (process.client) {
|
|
||||||
// Client
|
|
||||||
return Object.values(m.components).some(
|
|
||||||
(component) => component.options && component.options[key] === value
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// SSR
|
|
||||||
return Object.values(m.components).some((component) =>
|
|
||||||
Object.values(component._Ctor).some(
|
|
||||||
(ctor) => ctor.options && ctor.options[key] === value
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMatchedComponents(route, matches) {
|
|
||||||
return [].concat(
|
|
||||||
...[],
|
|
||||||
...route.matched.map((m, index) => {
|
|
||||||
return Object.keys(m.components).map((key) => {
|
|
||||||
matches.push(index)
|
|
||||||
return m.components[key]
|
|
||||||
})
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
632
nuxt.config.js
632
nuxt.config.js
@@ -1,414 +1,288 @@
|
|||||||
import { promises as fs } from 'fs'
|
import { promises as fs } from 'fs'
|
||||||
import { sortRoutes } from '@nuxt/utils'
|
import svgLoader from 'vite-svg-loader'
|
||||||
import axios from 'axios'
|
import eslintPlugin from 'vite-plugin-eslint'
|
||||||
|
import { resolve } from 'pathe'
|
||||||
|
import { defineNuxtConfig } from 'nuxt/config'
|
||||||
|
import { $fetch } from 'ofetch'
|
||||||
|
|
||||||
const STAGING_API_URL = 'https://staging-api.modrinth.com/v2/'
|
const STAGING_API_URL = 'https://staging-api.modrinth.com/v2/'
|
||||||
const STAGING_ARIADNE_URL = 'https://staging-ariadne.modrinth.com/v1/'
|
const STAGING_ARIADNE_URL = 'https://staging-ariadne.modrinth.com/v1/'
|
||||||
|
|
||||||
export default {
|
export default defineNuxtConfig({
|
||||||
/*
|
app: {
|
||||||
** Nuxt target
|
head: {
|
||||||
** See https://nuxtjs.org/api/configuration-target
|
htmlAttrs: {
|
||||||
*/
|
lang: 'en',
|
||||||
target: 'server',
|
},
|
||||||
/*
|
title: 'Modrinth',
|
||||||
** Headers of the page
|
meta: [
|
||||||
** See https://nuxtjs.org/api/configuration-head
|
{
|
||||||
*/
|
name: 'description',
|
||||||
head: {
|
content:
|
||||||
htmlAttrs: {
|
'Download Minecraft mods, plugins, datapacks, shaders, resourcepacks, and modpacks on Modrinth. Discover and publish projects on Modrinth with a modern, easy to use interface and API.',
|
||||||
lang: 'en',
|
},
|
||||||
|
{
|
||||||
|
name: 'publisher',
|
||||||
|
content: 'Rinth, Inc.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'og:title',
|
||||||
|
content: 'Modrinth',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'apple-mobile-web-app-title',
|
||||||
|
content: 'Modrinth',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'theme-color',
|
||||||
|
content: '#1bd96a',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'color-scheme',
|
||||||
|
content: 'dark light',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'og:site_name',
|
||||||
|
content: 'Modrinth',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'og:description',
|
||||||
|
content: 'An open source modding platform',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'og:type',
|
||||||
|
content: 'website',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'og:url',
|
||||||
|
content: 'https://modrinth.com',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'og:image',
|
||||||
|
content: 'https://cdn.modrinth.com/modrinth-new.png?',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'twitter:card',
|
||||||
|
content: 'summary',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'twitter:site',
|
||||||
|
content: '@modrinth',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
link: [
|
||||||
|
{
|
||||||
|
rel: 'preload',
|
||||||
|
href: 'https://cdn-raw.modrinth.com/fonts/inter/Inter-Regular.woff2?v=3.19',
|
||||||
|
as: 'font',
|
||||||
|
type: 'font/woff2',
|
||||||
|
crossorigin: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rel: 'preload',
|
||||||
|
href: 'https://cdn-raw.modrinth.com/fonts/inter/Inter-Medium.woff2?v=3.19',
|
||||||
|
as: 'font',
|
||||||
|
type: 'font/woff2',
|
||||||
|
crossorigin: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rel: 'preload',
|
||||||
|
href: 'https://cdn-raw.modrinth.com/fonts/inter/Inter-SemiBold.woff2?v=3.19',
|
||||||
|
as: 'font',
|
||||||
|
type: 'font/woff2',
|
||||||
|
crossorigin: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rel: 'preload',
|
||||||
|
href: 'https://cdn-raw.modrinth.com/fonts/inter/Inter-Bold.woff2?v=3.19',
|
||||||
|
as: 'font',
|
||||||
|
type: 'font/woff2',
|
||||||
|
crossorigin: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rel: 'preload',
|
||||||
|
href: 'https://cdn-raw.modrinth.com/fonts/inter/Inter-ExtraBold.woff2?v=3.19',
|
||||||
|
as: 'font',
|
||||||
|
type: 'font/woff2',
|
||||||
|
crossorigin: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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: 'search',
|
||||||
|
type: 'application/opensearchdescription+xml',
|
||||||
|
href: '/opensearch.xml',
|
||||||
|
title: 'Modrinth mods',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
title: 'Modrinth',
|
},
|
||||||
meta: [
|
vite: {
|
||||||
{
|
plugins: [
|
||||||
charset: 'utf-8',
|
svgLoader({
|
||||||
},
|
svgoConfig: {
|
||||||
{
|
plugins: [
|
||||||
name: 'viewport',
|
{
|
||||||
content: 'width=device-width, initial-scale=1',
|
name: 'preset-default',
|
||||||
},
|
params: {
|
||||||
{
|
overrides: {
|
||||||
hid: 'description',
|
removeViewBox: false,
|
||||||
name: 'description',
|
},
|
||||||
content:
|
},
|
||||||
'Download Minecraft mods, plugins, datapacks, shaders, resourcepacks, and modpacks on Modrinth. Discover and publish projects on Modrinth with a modern, easy to use interface and API.',
|
},
|
||||||
},
|
],
|
||||||
{
|
},
|
||||||
hid: 'publisher',
|
}),
|
||||||
name: 'publisher',
|
eslintPlugin(),
|
||||||
content: 'Rinth, Inc.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hid: 'og:title',
|
|
||||||
name: 'og:title',
|
|
||||||
content: 'Modrinth',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hid: 'apple-mobile-web-app-title',
|
|
||||||
name: 'apple-mobile-web-app-title',
|
|
||||||
content: 'Modrinth',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hid: 'theme-color',
|
|
||||||
name: 'theme-color',
|
|
||||||
content: '#1bd96a',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hid: 'color-scheme',
|
|
||||||
name: 'color-scheme',
|
|
||||||
content: 'light dark',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hid: 'og:site_name',
|
|
||||||
name: 'og:site_name',
|
|
||||||
content: 'Modrinth',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hid: 'og:description',
|
|
||||||
name: 'og:description',
|
|
||||||
content: 'An open source modding platform',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hid: 'og:type',
|
|
||||||
name: 'og:type',
|
|
||||||
content: 'website',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hid: 'og:url',
|
|
||||||
name: 'og:url',
|
|
||||||
content: 'https://modrinth.com',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hid: 'og:image',
|
|
||||||
name: 'og:image',
|
|
||||||
content: 'https://cdn.modrinth.com/modrinth-new.png?',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hid: 'twitter:card',
|
|
||||||
name: 'twitter:card',
|
|
||||||
content: 'summary',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hid: 'twitter:site',
|
|
||||||
name: 'twitter:site',
|
|
||||||
content: '@modrinth',
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
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',
|
|
||||||
href: 'https://cdn-raw.modrinth.com/fonts/inter/inter.css',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
rel: 'search',
|
|
||||||
type: 'application/opensearchdescription+xml',
|
|
||||||
href: '/opensearch.xml',
|
|
||||||
title: 'Modrinth mods',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
|
|
||||||
vue: {
|
|
||||||
config: {
|
|
||||||
devtools: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
router: {
|
|
||||||
extendRoutes(routes, resolve) {
|
|
||||||
routes.splice(
|
|
||||||
routes.findIndex((x) => x.name === 'search'),
|
|
||||||
1
|
|
||||||
)
|
|
||||||
|
|
||||||
routes.push({
|
|
||||||
path: '/search',
|
|
||||||
component: resolve(__dirname, 'pages/search.vue'),
|
|
||||||
name: 'search',
|
|
||||||
chunkName: 'pages/search',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: '/mods',
|
|
||||||
component: resolve(__dirname, 'pages/search/mods.vue'),
|
|
||||||
name: 'mods',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/modpacks',
|
|
||||||
component: resolve(__dirname, 'pages/search/modpacks.vue'),
|
|
||||||
name: 'modpacks',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/plugins',
|
|
||||||
component: resolve(__dirname, 'pages/search/plugins.vue'),
|
|
||||||
name: 'plugins',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/resourcepacks',
|
|
||||||
component: resolve(__dirname, 'pages/search/resourcepacks.vue'),
|
|
||||||
name: 'resourcepacks',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/shaders',
|
|
||||||
component: resolve(__dirname, 'pages/search/shaders.vue'),
|
|
||||||
name: 'shaders',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/datapacks',
|
|
||||||
component: resolve(__dirname, 'pages/search/datapacks.vue'),
|
|
||||||
name: 'datapacks',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
sortRoutes(routes)
|
|
||||||
},
|
|
||||||
middleware: ['auth', 'analytics'],
|
|
||||||
},
|
|
||||||
/*
|
|
||||||
** Global CSS
|
|
||||||
*/
|
|
||||||
css: ['~assets/styles/global.scss'],
|
|
||||||
/*
|
|
||||||
** Plugins to load before mounting the App
|
|
||||||
** https://nuxtjs.org/guide/plugins
|
|
||||||
*/
|
|
||||||
plugins: [
|
|
||||||
'~/plugins/vue-tooltip.js',
|
|
||||||
'~/plugins/vue-notification.js',
|
|
||||||
'~/plugins/xss.js',
|
|
||||||
'~/plugins/vue-syntax.js',
|
|
||||||
'~/plugins/shorthands.js',
|
|
||||||
'~/plugins/markdown.js',
|
|
||||||
],
|
|
||||||
/*
|
|
||||||
** Auto import components
|
|
||||||
** See https://nuxtjs.org/api/configuration-components
|
|
||||||
*/
|
|
||||||
components: true,
|
|
||||||
/*
|
|
||||||
** Nuxt.js dev-modules
|
|
||||||
*/
|
|
||||||
buildModules: [
|
|
||||||
// Doc: https://github.com/nuxt-community/eslint-module
|
|
||||||
'@nuxtjs/eslint-module',
|
|
||||||
'@nuxtjs/svg',
|
|
||||||
'@nuxtjs/color-mode',
|
|
||||||
],
|
|
||||||
/*
|
|
||||||
** Nuxt.js modules
|
|
||||||
*/
|
|
||||||
modules: [
|
|
||||||
// Doc: https://axios.nuxtjs.org/usage
|
|
||||||
'@nuxtjs/dayjs',
|
|
||||||
'@nuxtjs/axios',
|
|
||||||
'@nuxtjs/style-resources',
|
|
||||||
'cookie-universal-nuxt',
|
|
||||||
],
|
|
||||||
ads: {
|
|
||||||
// Module options
|
|
||||||
ghostMode: true,
|
|
||||||
geoEdgeId: '',
|
|
||||||
},
|
|
||||||
robots: {
|
|
||||||
Sitemap: 'https://modrinth.com/sitemap.xml',
|
|
||||||
},
|
|
||||||
/*
|
|
||||||
** Axios module configuration
|
|
||||||
** See https://axios.nuxtjs.org/options
|
|
||||||
*/
|
|
||||||
axios: {
|
|
||||||
baseURL: getApiUrl(),
|
|
||||||
headers: {
|
|
||||||
common: {
|
|
||||||
Accept: 'application/json',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
dayjs: {
|
dayjs: {
|
||||||
locales: ['en'],
|
locales: ['en'],
|
||||||
defaultLocale: 'en',
|
defaultLocale: 'en',
|
||||||
plugins: ['relativeTime'],
|
plugins: ['relativeTime'],
|
||||||
},
|
},
|
||||||
/*
|
|
||||||
** Build configuration
|
|
||||||
** See https://nuxtjs.org/api/configuration-build/
|
|
||||||
*/
|
|
||||||
build: {
|
|
||||||
transpile: ['vue-tooltip', 'vue-notification'],
|
|
||||||
html: {
|
|
||||||
minify: {
|
|
||||||
collapseWhitespace: true, // as @dario30186 mentioned
|
|
||||||
removeComments: true, // 👈 add this line
|
|
||||||
},
|
|
||||||
},
|
|
||||||
babel: {
|
|
||||||
plugins: [
|
|
||||||
[
|
|
||||||
'@babel/plugin-proposal-private-methods',
|
|
||||||
{
|
|
||||||
loose: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
loading: {
|
|
||||||
color: '#1bd96a',
|
|
||||||
height: '2px',
|
|
||||||
},
|
|
||||||
env: {
|
|
||||||
owner: process.env.VERCEL_GIT_REPO_OWNER || 'modrinth',
|
|
||||||
slug: process.env.VERCEL_GIT_REPO_SLUG || 'knossos',
|
|
||||||
branch: process.env.VERCEL_GIT_COMMIT_REF || 'master',
|
|
||||||
hash: process.env.VERCEL_GIT_COMMIT_SHA || 'unknown',
|
|
||||||
domain: getDomain(),
|
|
||||||
authURLBase: getApiUrl(),
|
|
||||||
},
|
|
||||||
publicRuntimeConfig: {
|
|
||||||
axios: {
|
|
||||||
browserBaseURL: process.env.BROWSER_BASE_URL,
|
|
||||||
},
|
|
||||||
ads: {
|
|
||||||
ethicalAds: process.env.ETHICAL_ADS,
|
|
||||||
},
|
|
||||||
analytics: {
|
|
||||||
base_url: process.env.BROWSER_ARIADNE_URL || STAGING_ARIADNE_URL,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
privateRuntimeConfig: {
|
|
||||||
axios: {
|
|
||||||
baseURL: process.env.BASE_URL,
|
|
||||||
headers: {
|
|
||||||
common: {
|
|
||||||
'x-ratelimit-key': process.env.RATE_LIMIT_IGNORE_KEY || '',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
hooks: {
|
hooks: {
|
||||||
build: {
|
async 'build:before'() {
|
||||||
async before(nuxt, buildOptions) {
|
// 30 minutes
|
||||||
// 30 minutes
|
const TTL = 30 * 60 * 1000
|
||||||
const TTL = 30 * 60 * 1000
|
|
||||||
|
|
||||||
let state = {}
|
let state = {}
|
||||||
try {
|
try {
|
||||||
state = JSON.parse(
|
state = JSON.parse(await fs.readFile('./generated/state.json', 'utf8'))
|
||||||
await fs.readFile('./generated/state.json', 'utf8')
|
} catch {
|
||||||
)
|
// File doesn't exist, create folder
|
||||||
} catch {
|
await fs.mkdir('./generated', { recursive: true })
|
||||||
// File doesn't exist, create folder
|
}
|
||||||
await fs.mkdir('./generated', { recursive: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
const API_URL = getApiUrl()
|
const API_URL = getApiUrl()
|
||||||
|
|
||||||
if (
|
if (
|
||||||
// Skip regeneration if within TTL...
|
// Skip regeneration if within TTL...
|
||||||
state.lastGenerated &&
|
state.lastGenerated &&
|
||||||
new Date(state.lastGenerated).getTime() + TTL >
|
new Date(state.lastGenerated).getTime() + TTL > new Date().getTime() &&
|
||||||
new Date().getTime() &&
|
// ...but only if the API URL is the same
|
||||||
// ...but only if the API URL is the same
|
state.apiUrl &&
|
||||||
state.apiUrl &&
|
state.apiUrl === API_URL
|
||||||
state.apiUrl === API_URL
|
) {
|
||||||
) {
|
return
|
||||||
return
|
}
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Generating tags...')
|
state.lastGenerated = new Date().toISOString()
|
||||||
|
|
||||||
state.lastGenerated = new Date().toISOString()
|
state.apiUrl = API_URL
|
||||||
|
|
||||||
state.apiUrl = API_URL
|
const headers = {
|
||||||
|
headers: {
|
||||||
|
'user-agent': 'Knossos generator (support@modrinth.com)',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
const headers = {
|
const [categories, loaders, gameVersions, donationPlatforms, reportTypes] = await Promise.all(
|
||||||
headers: {
|
[
|
||||||
'user-agent': `Knossos generator (admin@modrinth.com)`,
|
$fetch(`${API_URL}tag/category`, headers),
|
||||||
},
|
$fetch(`${API_URL}tag/loader`, headers),
|
||||||
}
|
$fetch(`${API_URL}tag/game_version`, headers),
|
||||||
|
$fetch(`${API_URL}tag/donation_platform`, headers),
|
||||||
|
$fetch(`${API_URL}tag/report_type`, headers),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
const [
|
state.categories = categories
|
||||||
categories,
|
state.loaders = loaders
|
||||||
loaders,
|
state.gameVersions = gameVersions
|
||||||
gameVersions,
|
state.donationPlatforms = donationPlatforms
|
||||||
donationPlatforms,
|
state.reportTypes = reportTypes
|
||||||
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/donation_platform`, headers),
|
|
||||||
axios.get(`${API_URL}tag/report_type`, headers),
|
|
||||||
])
|
|
||||||
).map((it) => it.data)
|
|
||||||
|
|
||||||
state.categories = categories
|
await fs.writeFile('./generated/state.json', JSON.stringify(state))
|
||||||
state.loaders = loaders
|
|
||||||
state.gameVersions = gameVersions
|
|
||||||
state.donationPlatforms = donationPlatforms
|
|
||||||
state.reportTypes = reportTypes
|
|
||||||
|
|
||||||
await fs.writeFile('./generated/state.json', JSON.stringify(state))
|
console.log('Tags generated!')
|
||||||
|
|
||||||
console.log('Tags generated!')
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
render: {
|
'pages:extend'(routes) {
|
||||||
routeDone(url, result, context) {
|
routes.splice(
|
||||||
setTimeout(() => {
|
routes.findIndex((x) => x.name === 'search-searchProjectType'),
|
||||||
axios
|
1
|
||||||
.post(
|
)
|
||||||
`${process.env.ARIADNE_URL || STAGING_ARIADNE_URL}view`,
|
|
||||||
{
|
routes.push({
|
||||||
url: getDomain() + url,
|
name: 'search-mods',
|
||||||
ip:
|
path: '/mods',
|
||||||
context.req.headers['cf-connecting-ip'] ??
|
file: resolve(__dirname, 'pages/search/[searchProjectType].vue'),
|
||||||
context.req.headers['x-real-ip'] ??
|
children: [],
|
||||||
context.req.connection.remoteAddress,
|
})
|
||||||
headers: context.req.headers,
|
routes.push({
|
||||||
},
|
name: 'search-modpacks',
|
||||||
{
|
path: '/modpacks',
|
||||||
headers: {
|
file: resolve(__dirname, 'pages/search/[searchProjectType].vue'),
|
||||||
'Modrinth-Admin': process.env.ARIADNE_ADMIN_KEY || 'feedbeef',
|
children: [],
|
||||||
},
|
})
|
||||||
}
|
routes.push({
|
||||||
)
|
name: 'search-plugins',
|
||||||
.then(() => {})
|
path: '/plugins',
|
||||||
.catch((e) => {
|
file: resolve(__dirname, 'pages/search/[searchProjectType].vue'),
|
||||||
console.error(
|
children: [],
|
||||||
'An error occurred while registering the visit: ',
|
})
|
||||||
e.response ? e.response.data : e
|
routes.push({
|
||||||
)
|
name: 'search-resourcepacks',
|
||||||
})
|
path: '/resourcepacks',
|
||||||
})
|
file: resolve(__dirname, 'pages/search/[searchProjectType].vue'),
|
||||||
},
|
children: [],
|
||||||
|
})
|
||||||
|
routes.push({
|
||||||
|
name: 'search-shaders',
|
||||||
|
path: '/shaders',
|
||||||
|
file: resolve(__dirname, 'pages/search/[searchProjectType].vue'),
|
||||||
|
children: [],
|
||||||
|
})
|
||||||
|
routes.push({
|
||||||
|
name: 'search-datapacks',
|
||||||
|
path: '/datapacks',
|
||||||
|
file: resolve(__dirname, 'pages/search/[searchProjectType].vue'),
|
||||||
|
children: [],
|
||||||
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
runtimeConfig: {
|
||||||
|
apiBaseUrl: process.env.BASE_URL ?? getApiUrl(),
|
||||||
|
ariadneBaseUrl: process.env.ARIADNE_URL ?? getAriadneUrl(),
|
||||||
|
ariadneAdminKey: process.env.ARIADNE_ADMIN_KEY,
|
||||||
|
rateLimitKey: process.env.RATE_LIMIT_IGNORE_KEY,
|
||||||
|
public: {
|
||||||
|
apiBaseUrl: getApiUrl(),
|
||||||
|
ariadneBaseUrl: getAriadneUrl(),
|
||||||
|
siteUrl: getDomain(),
|
||||||
|
|
||||||
|
owner: process.env.VERCEL_GIT_REPO_OWNER || 'modrinth',
|
||||||
|
slug: process.env.VERCEL_GIT_REPO_SLUG || 'knossos',
|
||||||
|
branch: process.env.VERCEL_GIT_COMMIT_REF || 'master',
|
||||||
|
hash: process.env.VERCEL_GIT_COMMIT_SHA || 'unknown',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
function getApiUrl() {
|
function getApiUrl() {
|
||||||
return process.env.BROWSER_BASE_URL ?? STAGING_API_URL
|
return process.env.BROWSER_BASE_URL ?? STAGING_API_URL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getAriadneUrl() {
|
||||||
|
return process.env.BROWSER_ARIADNE_URL ?? STAGING_ARIADNE_URL
|
||||||
|
}
|
||||||
|
|
||||||
function getDomain() {
|
function getDomain() {
|
||||||
if (process.env.NODE_ENV === 'production') {
|
if (process.env.NODE_ENV === 'production') {
|
||||||
if (process.env.SITE_URL) {
|
if (process.env.SITE_URL) {
|
||||||
|
|||||||
29410
package-lock.json
generated
29410
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
69
package.json
69
package.json
@@ -1,51 +1,38 @@
|
|||||||
{
|
{
|
||||||
"name": "knossos",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "nuxt",
|
"build": "nuxi build",
|
||||||
"build": "nuxt build",
|
"dev": "nuxi dev",
|
||||||
"start": "nuxt start",
|
"generate": "nuxi generate",
|
||||||
"export": "nuxt export",
|
"preview": "nuxi preview",
|
||||||
"serve": "nuxt serve",
|
"postinstall": "nuxi prepare",
|
||||||
"lint:js": "eslint --ext .js,.vue --ignore-path .eslintignore .",
|
"lint:js": "eslint --ext .js,.vue,.ts,.jsx,.tsx,.html,.vue .",
|
||||||
"lint": "npm run lint:js",
|
"lint": "npm run lint:js && prettier --check .",
|
||||||
"fix": "eslint --fix --ext .js,.vue --ignore-path .eslintignore ."
|
"fix": "eslint --fix --ext .js,.vue,.ts,.jsx,.tsx,.html,.vue ."
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@nuxtjs/eslint-config-typescript": "^12.0.0",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.50.0",
|
||||||
|
"@typescript-eslint/parser": "^5.50.0",
|
||||||
|
"eslint": "^8.33.0",
|
||||||
|
"eslint-config-prettier": "^8.6.0",
|
||||||
|
"eslint-plugin-vue": "^9.9.0",
|
||||||
|
"nuxt": "^3.2.3",
|
||||||
|
"prettier": "^2.8.3",
|
||||||
|
"sass": "^1.58.0",
|
||||||
|
"typescript": "^4.9.5",
|
||||||
|
"vite-plugin-eslint": "^1.8.1",
|
||||||
|
"vite-svg-loader": "^4.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@iarna/toml": "^2.2.5",
|
"dayjs": "^1.11.7",
|
||||||
"@nuxtjs/axios": "^5.13.1",
|
"floating-vue": "^2.0.0-beta.20",
|
||||||
"@nuxtjs/dayjs": "^1.2.0",
|
"highlight.js": "^11.7.0",
|
||||||
"@nuxtjs/style-resources": "^1.0.0",
|
|
||||||
"cookie-universal-nuxt": "^2.1.5",
|
|
||||||
"core-js": "^3.9.1",
|
|
||||||
"highlight.js": "^10.3.2",
|
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
"markdown-it": "^13.0.1",
|
"markdown-it": "^13.0.1",
|
||||||
"nuxt": "^2.15.3",
|
"toml": "^3.0.0",
|
||||||
"sass": "^1.32.12",
|
"vue-multiselect": "^3.0.0-alpha.2",
|
||||||
"v-tooltip": "^2.0.3",
|
"xss": "^1.0.14"
|
||||||
"vue-click-outside": "^1.1.0",
|
|
||||||
"vue-highlightjs": "^1.3.3",
|
|
||||||
"vue-multiselect": "^2.1.6",
|
|
||||||
"vue-notification": "^1.3.20",
|
|
||||||
"xss": "^1.0.8"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@nuxtjs/color-mode": "^2.1.1",
|
|
||||||
"@nuxtjs/eslint-config": "^6.0.0",
|
|
||||||
"@nuxtjs/eslint-module": "^3.0.2",
|
|
||||||
"@nuxtjs/svg": "^0.1.12",
|
|
||||||
"babel-eslint": "^10.1.0",
|
|
||||||
"eslint": "^7.22.0",
|
|
||||||
"eslint-config-prettier": "^8.1.0",
|
|
||||||
"eslint-plugin-nuxt": "^2.0.0",
|
|
||||||
"eslint-plugin-prettier": "^3.3.1",
|
|
||||||
"prettier": "^2.2.1",
|
|
||||||
"sass-loader": "^10.1.1"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"fsevents": "^2.3.2"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
235
pages/[type]/[id]/changelog.vue
Normal file
235
pages/[type]/[id]/changelog.vue
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
<template>
|
||||||
|
<div class="content">
|
||||||
|
<Head>
|
||||||
|
<Title> {{ project.title }} - Changelog </Title>
|
||||||
|
<Meta name="og:title" :content="`${props.project.title} - Changelog`" />
|
||||||
|
<Meta name="description" :content="metaDescription" />
|
||||||
|
<Meta name="apple-mobile-web-app-title" :content="`${props.project.title} - Changelog`" />
|
||||||
|
<Meta name="og:description" :content="metaDescription" />
|
||||||
|
</Head>
|
||||||
|
<VersionFilterControl
|
||||||
|
:versions="props.versions"
|
||||||
|
@update-versions="
|
||||||
|
(v) => {
|
||||||
|
filteredVersions = v
|
||||||
|
switchPage(1)
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<Pagination
|
||||||
|
:page="currentPage"
|
||||||
|
:count="Math.ceil(filteredVersions.length / 20)"
|
||||||
|
class="pagination-before"
|
||||||
|
:link-function="(page) => `?page=${page}`"
|
||||||
|
@switch-page="switchPage"
|
||||||
|
/>
|
||||||
|
<div class="card">
|
||||||
|
<div
|
||||||
|
v-for="version in filteredVersions.slice((currentPage - 1) * 20, currentPage * 20)"
|
||||||
|
:key="version.id"
|
||||||
|
class="changelog-item"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
:class="`changelog-bar ${version.version_type} ${version.duplicate ? 'duplicate' : ''}`"
|
||||||
|
/>
|
||||||
|
<div class="version-wrapper">
|
||||||
|
<div class="version-header">
|
||||||
|
<div class="version-header-text">
|
||||||
|
<h2 class="name">
|
||||||
|
<nuxt-link
|
||||||
|
:to="`/${props.project.project_type}/${
|
||||||
|
props.project.slug ? props.project.slug : props.project.id
|
||||||
|
}/version/${encodeURI(version.displayUrlEnding)}`"
|
||||||
|
>
|
||||||
|
{{ version.name }}
|
||||||
|
</nuxt-link>
|
||||||
|
</h2>
|
||||||
|
<span v-if="version.author">
|
||||||
|
by
|
||||||
|
<nuxt-link class="text-link" :to="'/user/' + version.author.user.username">{{
|
||||||
|
version.author.user.username
|
||||||
|
}}</nuxt-link>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
on
|
||||||
|
{{ $dayjs(version.date_published).format('MMM D, YYYY') }}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
:href="version.primaryFile.url"
|
||||||
|
class="iconified-button download"
|
||||||
|
:title="`Download ${version.name}`"
|
||||||
|
>
|
||||||
|
<DownloadIcon aria-hidden="true" />
|
||||||
|
Download
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="version.changelog && !version.duplicate"
|
||||||
|
class="markdown-body"
|
||||||
|
v-html="renderHighlightedString(version.changelog)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Pagination
|
||||||
|
:page="currentPage"
|
||||||
|
:count="Math.ceil(filteredVersions.length / 20)"
|
||||||
|
class="pagination-before"
|
||||||
|
:link-function="(page) => `?page=${page}`"
|
||||||
|
@switch-page="switchPage"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import DownloadIcon from '~/assets/images/utils/download.svg'
|
||||||
|
import { renderHighlightedString } from '~/helpers/highlight'
|
||||||
|
import VersionFilterControl from '~/components/ui/VersionFilterControl'
|
||||||
|
import Pagination from '~/components/ui/Pagination'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
project: {
|
||||||
|
type: Object,
|
||||||
|
default() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
versions: {
|
||||||
|
type: Array,
|
||||||
|
default() {
|
||||||
|
return []
|
||||||
|
},
|
||||||
|
},
|
||||||
|
members: {
|
||||||
|
type: Array,
|
||||||
|
default() {
|
||||||
|
return []
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const metaDescription = computed(
|
||||||
|
() => `View the changelog of ${props.project.title}'s ${props.versions.length} versions.`
|
||||||
|
)
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const currentPage = ref(Number(route.query.p ?? 1))
|
||||||
|
const filteredVersions = shallowRef(props.versions)
|
||||||
|
|
||||||
|
async function switchPage(page) {
|
||||||
|
currentPage.value = page
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
await router.replace({
|
||||||
|
query: {
|
||||||
|
...route.query,
|
||||||
|
p: currentPage.value !== 1 ? currentPage.value : undefined,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.changelog-item {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
position: relative;
|
||||||
|
padding-left: 1.8rem;
|
||||||
|
|
||||||
|
.changelog-bar {
|
||||||
|
--color: var(--color-special-green);
|
||||||
|
|
||||||
|
&.alpha {
|
||||||
|
--color: var(--color-special-red);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.release {
|
||||||
|
--color: var(--color-special-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.beta {
|
||||||
|
--color: var(--color-special-orange);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
min-height: 0.75rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-header-text {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: var(--font-size-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
h2,
|
||||||
|
span {
|
||||||
|
padding-right: 0.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.download {
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
@media screen and (min-width: 800px) {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body {
|
||||||
|
margin: 0.5rem 0.5rem 0 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,5 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
|
<Head>
|
||||||
|
<Title> {{ project.title }} - Gallery </Title>
|
||||||
|
<Meta name="og:title" :content="`${project.title} - Gallery`" />
|
||||||
|
<Meta name="description" :content="metaDescription" />
|
||||||
|
<Meta name="apple-mobile-web-app-title" :content="`${project.title} - Gallery`" />
|
||||||
|
<Meta name="og:description" :contcent="metaDescription" />
|
||||||
|
</Head>
|
||||||
<Modal
|
<Modal
|
||||||
v-if="$auth.user && currentMember"
|
v-if="$auth.user && currentMember"
|
||||||
ref="modal_edit_item"
|
ref="modal_edit_item"
|
||||||
@@ -71,8 +78,8 @@
|
|||||||
<label for="gallery-image-featured">
|
<label for="gallery-image-featured">
|
||||||
<span class="label__title">Featured</span>
|
<span class="label__title">Featured</span>
|
||||||
<span class="label__description">
|
<span class="label__description">
|
||||||
A featured gallery image shows up in search and your project card.
|
A featured gallery image shows up in search and your project card. Only one gallery
|
||||||
Only one gallery image can be featured.
|
image can be featured.
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<button
|
<button
|
||||||
@@ -94,10 +101,7 @@
|
|||||||
Unfeature image
|
Unfeature image
|
||||||
</button>
|
</button>
|
||||||
<div class="button-group">
|
<div class="button-group">
|
||||||
<button
|
<button class="iconified-button" @click="$refs.modal_edit_item.hide()">
|
||||||
class="iconified-button"
|
|
||||||
@click="$refs.modal_edit_item.hide()"
|
|
||||||
>
|
|
||||||
<CrossIcon />
|
<CrossIcon />
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
@@ -145,11 +149,7 @@
|
|||||||
? expandedGalleryItem.url
|
? expandedGalleryItem.url
|
||||||
: 'https://cdn.modrinth.com/placeholder-banner.svg'
|
: 'https://cdn.modrinth.com/placeholder-banner.svg'
|
||||||
"
|
"
|
||||||
:alt="
|
:alt="expandedGalleryItem.title ? expandedGalleryItem.title : 'gallery-image'"
|
||||||
expandedGalleryItem.title
|
|
||||||
? expandedGalleryItem.title
|
|
||||||
: 'gallery-image'
|
|
||||||
"
|
|
||||||
@click.stop=""
|
@click.stop=""
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -164,10 +164,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<button
|
<button class="close circle-button" @click="expandedGalleryItem = null">
|
||||||
class="close circle-button"
|
|
||||||
@click="expandedGalleryItem = null"
|
|
||||||
>
|
|
||||||
<CrossIcon aria-hidden="true" />
|
<CrossIcon aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
<a
|
<a
|
||||||
@@ -220,25 +217,21 @@
|
|||||||
<DropArea :accept="acceptFileTypes" @change="handleFiles" />
|
<DropArea :accept="acceptFileTypes" @change="handleFiles" />
|
||||||
</div>
|
</div>
|
||||||
<div class="items">
|
<div class="items">
|
||||||
<div
|
<div v-for="(item, index) in project.gallery" :key="index" class="card gallery-item">
|
||||||
v-for="(item, index) in project.gallery"
|
|
||||||
:key="index"
|
|
||||||
class="card gallery-item"
|
|
||||||
>
|
|
||||||
<a class="gallery-thumbnail" @click="expandImage(item, index)">
|
<a class="gallery-thumbnail" @click="expandImage(item, index)">
|
||||||
<img
|
<img
|
||||||
:src="
|
:src="item.url ? item.url : 'https://cdn.modrinth.com/placeholder-banner.svg'"
|
||||||
item.url
|
|
||||||
? item.url
|
|
||||||
: 'https://cdn.modrinth.com/placeholder-banner.svg'
|
|
||||||
"
|
|
||||||
:alt="item.title ? item.title : 'gallery-image'"
|
:alt="item.title ? item.title : 'gallery-image'"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
<div class="gallery-body">
|
<div class="gallery-body">
|
||||||
<div class="gallery-info">
|
<div class="gallery-info">
|
||||||
<h2 v-if="item.title">{{ item.title }}</h2>
|
<h2 v-if="item.title">
|
||||||
<p v-if="item.description">{{ item.description }}</p>
|
{{ item.title }}
|
||||||
|
</h2>
|
||||||
|
<p v-if="item.description">
|
||||||
|
{{ item.description }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="gallery-bottom">
|
<div class="gallery-bottom">
|
||||||
@@ -250,13 +243,15 @@
|
|||||||
<button
|
<button
|
||||||
class="iconified-button"
|
class="iconified-button"
|
||||||
@click="
|
@click="
|
||||||
resetEdit()
|
() => {
|
||||||
editIndex = index
|
resetEdit()
|
||||||
editTitle = item.title
|
editIndex = index
|
||||||
editDescription = item.description
|
editTitle = item.title
|
||||||
editFeatured = item.featured
|
editDescription = item.description
|
||||||
editOrder = item.ordering
|
editFeatured = item.featured
|
||||||
$refs.modal_edit_item.show()
|
editOrder = item.ordering
|
||||||
|
$refs.modal_edit_item.show()
|
||||||
|
}
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<EditIcon />
|
<EditIcon />
|
||||||
@@ -265,8 +260,10 @@
|
|||||||
<button
|
<button
|
||||||
class="iconified-button"
|
class="iconified-button"
|
||||||
@click="
|
@click="
|
||||||
deleteIndex = index
|
() => {
|
||||||
$refs.modal_confirm.show()
|
deleteIndex = index
|
||||||
|
$refs.modal_confirm.show()
|
||||||
|
}
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<TrashIcon />
|
<TrashIcon />
|
||||||
@@ -280,29 +277,29 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import PlusIcon from '~/assets/images/utils/plus.svg?inline'
|
import PlusIcon from '~/assets/images/utils/plus.svg'
|
||||||
import CalendarIcon from '~/assets/images/utils/calendar.svg?inline'
|
import CalendarIcon from '~/assets/images/utils/calendar.svg'
|
||||||
import TrashIcon from '~/assets/images/utils/trash.svg?inline'
|
import TrashIcon from '~/assets/images/utils/trash.svg'
|
||||||
import CrossIcon from '~/assets/images/utils/x.svg?inline'
|
import CrossIcon from '~/assets/images/utils/x.svg'
|
||||||
import RightArrowIcon from '~/assets/images/utils/right-arrow.svg?inline'
|
import RightArrowIcon from '~/assets/images/utils/right-arrow.svg'
|
||||||
import LeftArrowIcon from '~/assets/images/utils/left-arrow.svg?inline'
|
import LeftArrowIcon from '~/assets/images/utils/left-arrow.svg'
|
||||||
import EditIcon from '~/assets/images/utils/edit.svg?inline'
|
import EditIcon from '~/assets/images/utils/edit.svg'
|
||||||
import SaveIcon from '~/assets/images/utils/save.svg?inline'
|
import SaveIcon from '~/assets/images/utils/save.svg'
|
||||||
import ExternalIcon from '~/assets/images/utils/external.svg?inline'
|
import ExternalIcon from '~/assets/images/utils/external.svg'
|
||||||
import ExpandIcon from '~/assets/images/utils/expand.svg?inline'
|
import ExpandIcon from '~/assets/images/utils/expand.svg'
|
||||||
import ContractIcon from '~/assets/images/utils/contract.svg?inline'
|
import ContractIcon from '~/assets/images/utils/contract.svg'
|
||||||
import StarIcon from '~/assets/images/utils/star.svg?inline'
|
import StarIcon from '~/assets/images/utils/star.svg'
|
||||||
import UploadIcon from '~/assets/images/utils/upload.svg?inline'
|
import UploadIcon from '~/assets/images/utils/upload.svg'
|
||||||
import InfoIcon from '~/assets/images/utils/info.svg?inline'
|
import InfoIcon from '~/assets/images/utils/info.svg'
|
||||||
import ImageIcon from '~/assets/images/utils/image.svg?inline'
|
import ImageIcon from '~/assets/images/utils/image.svg'
|
||||||
import TransferIcon from '~/assets/images/utils/transfer.svg?inline'
|
import TransferIcon from '~/assets/images/utils/transfer.svg'
|
||||||
|
|
||||||
import FileInput from '~/components/ui/FileInput'
|
import FileInput from '~/components/ui/FileInput'
|
||||||
import DropArea from '~/components/ui/DropArea'
|
import DropArea from '~/components/ui/DropArea'
|
||||||
import ModalConfirm from '~/components/ui/ModalConfirm'
|
import ModalConfirm from '~/components/ui/ModalConfirm'
|
||||||
import Modal from '~/components/ui/Modal'
|
import Modal from '~/components/ui/Modal'
|
||||||
|
|
||||||
export default {
|
export default defineNuxtComponent({
|
||||||
components: {
|
components: {
|
||||||
CalendarIcon,
|
CalendarIcon,
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
@@ -325,7 +322,6 @@ export default {
|
|||||||
FileInput,
|
FileInput,
|
||||||
DropArea,
|
DropArea,
|
||||||
},
|
},
|
||||||
auth: false,
|
|
||||||
props: {
|
props: {
|
||||||
project: {
|
project: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -356,36 +352,8 @@ export default {
|
|||||||
editFile: null,
|
editFile: null,
|
||||||
previewImage: null,
|
previewImage: null,
|
||||||
shouldPreventActions: false,
|
shouldPreventActions: false,
|
||||||
}
|
|
||||||
},
|
|
||||||
head() {
|
|
||||||
const title = `${this.project.title} - Gallery`
|
|
||||||
const description = `View ${this.project.gallery.length} images of ${this.project.title} on Modrinth.`
|
|
||||||
|
|
||||||
return {
|
metaDescription: `View ${this.project.gallery.length} images of ${this.project.title} on Modrinth.`,
|
||||||
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: {
|
computed: {
|
||||||
@@ -456,24 +424,30 @@ export default {
|
|||||||
},
|
},
|
||||||
async createGalleryItem() {
|
async createGalleryItem() {
|
||||||
this.shouldPreventActions = true
|
this.shouldPreventActions = true
|
||||||
this.$nuxt.$loading.start()
|
startLoading()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let url = `project/${this.project.id}/gallery?ext=${
|
let url = `project/${this.project.id}/gallery?ext=${
|
||||||
this.editFile
|
this.editFile
|
||||||
? this.editFile.type.split('/')[
|
? this.editFile.type.split('/')[this.editFile.type.split('/').length - 1]
|
||||||
this.editFile.type.split('/').length - 1
|
|
||||||
]
|
|
||||||
: null
|
: null
|
||||||
}&featured=${this.editFeatured}`
|
}&featured=${this.editFeatured}`
|
||||||
|
|
||||||
if (this.editTitle)
|
if (this.editTitle) {
|
||||||
url += `&title=${encodeURIComponent(this.editTitle)}`
|
url += `&title=${encodeURIComponent(this.editTitle)}`
|
||||||
if (this.editDescription)
|
}
|
||||||
|
if (this.editDescription) {
|
||||||
url += `&description=${encodeURIComponent(this.editDescription)}`
|
url += `&description=${encodeURIComponent(this.editDescription)}`
|
||||||
if (this.editOrder) url += `&ordering=${this.editOrder}`
|
}
|
||||||
|
if (this.editOrder) {
|
||||||
|
url += `&ordering=${this.editOrder}`
|
||||||
|
}
|
||||||
|
|
||||||
await this.$axios.post(url, this.editFile, this.$defaultHeaders())
|
await useBaseFetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
body: this.editFile,
|
||||||
|
...this.$defaultHeaders(),
|
||||||
|
})
|
||||||
await this.updateProject()
|
await this.updateProject()
|
||||||
|
|
||||||
this.$refs.modal_edit_item.hide()
|
this.$refs.modal_edit_item.hide()
|
||||||
@@ -481,30 +455,37 @@ export default {
|
|||||||
this.$notify({
|
this.$notify({
|
||||||
group: 'main',
|
group: 'main',
|
||||||
title: 'An error occurred',
|
title: 'An error occurred',
|
||||||
text: err.response ? err.response.data.description : err,
|
text: err.data ? err.data.description : err,
|
||||||
type: 'error',
|
type: 'error',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$nuxt.$loading.finish()
|
stopLoading()
|
||||||
this.shouldPreventActions = false
|
this.shouldPreventActions = false
|
||||||
},
|
},
|
||||||
async editGalleryItem() {
|
async editGalleryItem() {
|
||||||
this.shouldPreventActions = true
|
this.shouldPreventActions = true
|
||||||
this.$nuxt.$loading.start()
|
startLoading()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let url = `project/${this.project.id}/gallery?url=${encodeURIComponent(
|
let url = `project/${this.project.id}/gallery?url=${encodeURIComponent(
|
||||||
this.project.gallery[this.editIndex].url
|
this.project.gallery[this.editIndex].url
|
||||||
)}&featured=${this.editFeatured}`
|
)}&featured=${this.editFeatured}`
|
||||||
|
|
||||||
if (this.editTitle)
|
if (this.editTitle) {
|
||||||
url += `&title=${encodeURIComponent(this.editTitle)}`
|
url += `&title=${encodeURIComponent(this.editTitle)}`
|
||||||
if (this.editDescription)
|
}
|
||||||
|
if (this.editDescription) {
|
||||||
url += `&description=${encodeURIComponent(this.editDescription)}`
|
url += `&description=${encodeURIComponent(this.editDescription)}`
|
||||||
if (this.editOrder) url += `&ordering=${this.editOrder}`
|
}
|
||||||
|
if (this.editOrder) {
|
||||||
|
url += `&ordering=${this.editOrder}`
|
||||||
|
}
|
||||||
|
|
||||||
await this.$axios.patch(url, {}, this.$defaultHeaders())
|
await useBaseFetch(url, {
|
||||||
|
method: 'PATCH',
|
||||||
|
...this.$defaultHeaders(),
|
||||||
|
})
|
||||||
|
|
||||||
await this.updateProject()
|
await this.updateProject()
|
||||||
this.$refs.modal_edit_item.hide()
|
this.$refs.modal_edit_item.hide()
|
||||||
@@ -512,23 +493,26 @@ export default {
|
|||||||
this.$notify({
|
this.$notify({
|
||||||
group: 'main',
|
group: 'main',
|
||||||
title: 'An error occurred',
|
title: 'An error occurred',
|
||||||
text: err.response ? err.response.data.description : err,
|
text: err.data ? err.data.description : err,
|
||||||
type: 'error',
|
type: 'error',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$nuxt.$loading.finish()
|
stopLoading()
|
||||||
this.shouldPreventActions = false
|
this.shouldPreventActions = false
|
||||||
},
|
},
|
||||||
async deleteGalleryImage() {
|
async deleteGalleryImage() {
|
||||||
this.$nuxt.$loading.start()
|
startLoading()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.$axios.delete(
|
await useBaseFetch(
|
||||||
`project/${this.project.id}/gallery?url=${encodeURIComponent(
|
`project/${this.project.id}/gallery?url=${encodeURIComponent(
|
||||||
this.project.gallery[this.deleteIndex].url
|
this.project.gallery[this.deleteIndex].url
|
||||||
)}`,
|
)}`,
|
||||||
this.$defaultHeaders()
|
{
|
||||||
|
method: 'DELETE',
|
||||||
|
...this.$defaultHeaders(),
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
await this.updateProject()
|
await this.updateProject()
|
||||||
@@ -536,19 +520,25 @@ export default {
|
|||||||
this.$notify({
|
this.$notify({
|
||||||
group: 'main',
|
group: 'main',
|
||||||
title: 'An error occurred',
|
title: 'An error occurred',
|
||||||
text: err.response ? err.response.data.description : err,
|
text: err.data ? err.data.description : err,
|
||||||
type: 'error',
|
type: 'error',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$nuxt.$loading.finish()
|
stopLoading()
|
||||||
},
|
},
|
||||||
async updateProject() {
|
async updateProject() {
|
||||||
await this.$parent.resetProject()
|
const project = await useBaseFetch(`project/${this.project.id}`, this.$defaultHeaders())
|
||||||
|
|
||||||
|
project.actualProjectType = JSON.parse(JSON.stringify(project.project_type))
|
||||||
|
|
||||||
|
project.project_type = this.$getProjectTypeForUrl(project.project_type, project.loaders)
|
||||||
|
|
||||||
|
this.$emit('update:project', project)
|
||||||
this.resetEdit()
|
this.resetEdit()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@@ -756,8 +746,7 @@ export default {
|
|||||||
|
|
||||||
.gallery-bottom {
|
.gallery-bottom {
|
||||||
width: calc(100% - 2 * var(--spacing-card-md));
|
width: calc(100% - 2 * var(--spacing-card-md));
|
||||||
padding: 0 var(--spacing-card-md) var(--spacing-card-sm)
|
padding: 0 var(--spacing-card-md) var(--spacing-card-sm) var(--spacing-card-md);
|
||||||
var(--spacing-card-md);
|
|
||||||
|
|
||||||
.gallery-created {
|
.gallery-created {
|
||||||
display: flex;
|
display: flex;
|
||||||
21
pages/[type]/[id]/index.vue
Normal file
21
pages/[type]/[id]/index.vue
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<template>
|
||||||
|
<div class="markdown-body card" v-html="renderHighlightedString(project.body)" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { renderHighlightedString } from '~/helpers/highlight'
|
||||||
|
|
||||||
|
export default defineNuxtComponent({
|
||||||
|
props: {
|
||||||
|
project: {
|
||||||
|
type: Object,
|
||||||
|
default() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: { renderHighlightedString },
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
||||||
@@ -4,22 +4,19 @@
|
|||||||
<label for="project-description">
|
<label for="project-description">
|
||||||
<span class="label__title size-card-header">Description</span>
|
<span class="label__title size-card-header">Description</span>
|
||||||
<span class="label__description">
|
<span class="label__description">
|
||||||
You can type an extended description of your mod here. This editor
|
You can type an extended description of your mod here. This editor supports
|
||||||
supports
|
|
||||||
<a
|
<a
|
||||||
class="text-link"
|
class="text-link"
|
||||||
href="https://guides.github.com/features/mastering-markdown/"
|
href="https://guides.github.com/features/mastering-markdown/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener"
|
||||||
>Markdown</a
|
>Markdown</a
|
||||||
>. HTML can also be used inside your description, not including
|
>. HTML can also be used inside your description, not including styles, scripts, and
|
||||||
styles, scripts, and iframes (though YouTube iframes are allowed).
|
iframes (though YouTube iframes are allowed).
|
||||||
<span class="label__subdescription">
|
<span class="label__subdescription">
|
||||||
The description must clearly and honestly describe the purpose and
|
The description must clearly and honestly describe the purpose and function of the
|
||||||
function of the project. See section 2.1 of the
|
project. See section 2.1 of the
|
||||||
<nuxt-link to="/legal/rules" class="text-link" target="_blank"
|
<nuxt-link to="/legal/rules" class="text-link" target="_blank">Content Rules</nuxt-link>
|
||||||
>Content Rules</nuxt-link
|
|
||||||
>
|
|
||||||
for the full requirements.
|
for the full requirements.
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -34,12 +31,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="bodyViewMode === 'preview'"
|
v-else-if="bodyViewMode === 'preview'"
|
||||||
v-highlightjs
|
|
||||||
class="markdown-body"
|
class="markdown-body"
|
||||||
v-html="
|
v-html="description ? renderHighlightedString(description) : 'No body specified.'"
|
||||||
description ? $xss($md.render(description)) : 'No body specified.'
|
/>
|
||||||
"
|
|
||||||
></div>
|
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -57,10 +51,10 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Chips from '~/components/ui/Chips'
|
import Chips from '~/components/ui/Chips'
|
||||||
|
import SaveIcon from '~/assets/images/utils/save.svg'
|
||||||
|
import { renderHighlightedString } from '~/helpers/highlight'
|
||||||
|
|
||||||
import SaveIcon from '~/assets/images/utils/save.svg?inline'
|
export default defineNuxtComponent({
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
components: {
|
||||||
Chips,
|
Chips,
|
||||||
SaveIcon,
|
SaveIcon,
|
||||||
@@ -100,13 +94,10 @@ export default {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
description: '',
|
description: this.project.body,
|
||||||
bodyViewMode: 'source',
|
bodyViewMode: 'source',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fetch() {
|
|
||||||
this.description = this.project.body
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
patchData() {
|
patchData() {
|
||||||
const data = {}
|
const data = {}
|
||||||
@@ -125,13 +116,14 @@ export default {
|
|||||||
this.EDIT_BODY = 1 << 3
|
this.EDIT_BODY = 1 << 3
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
renderHighlightedString,
|
||||||
saveChanges() {
|
saveChanges() {
|
||||||
if (this.hasChanges) {
|
if (this.hasChanges) {
|
||||||
this.patchProject(this.patchData)
|
this.patchProject(this.patchData)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
})
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.resizable-textarea-wrapper textarea {
|
.resizable-textarea-wrapper textarea {
|
||||||
@@ -20,9 +20,7 @@
|
|||||||
</label>
|
</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<Avatar
|
<Avatar
|
||||||
:src="
|
:src="deletedIcon ? null : previewImage ? previewImage : project.icon_url"
|
||||||
deletedIcon ? null : previewImage ? previewImage : project.icon_url
|
|
||||||
"
|
|
||||||
:alt="project.title"
|
:alt="project.title"
|
||||||
size="md"
|
size="md"
|
||||||
class="project__icon"
|
class="project__icon"
|
||||||
@@ -86,7 +84,7 @@
|
|||||||
v-model="summary"
|
v-model="summary"
|
||||||
maxlength="256"
|
maxlength="256"
|
||||||
:disabled="!hasPermission"
|
:disabled="!hasPermission"
|
||||||
></textarea>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<template
|
<template
|
||||||
v-if="
|
v-if="
|
||||||
@@ -101,9 +99,9 @@
|
|||||||
<span class="label__title">Client-side</span>
|
<span class="label__title">Client-side</span>
|
||||||
<span class="label__description">
|
<span class="label__description">
|
||||||
Select based on if the
|
Select based on if the
|
||||||
{{ $formatProjectType(project.project_type).toLowerCase() }} has
|
{{ $formatProjectType(project.project_type).toLowerCase() }} has functionality on the
|
||||||
functionality on the client side. Just because a mod works in
|
client side. Just because a mod works in Singleplayer doesn't mean it has actual
|
||||||
Singleplayer doesn't mean it has actual client-side functionality.
|
client-side functionality.
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<Multiselect
|
<Multiselect
|
||||||
@@ -111,9 +109,7 @@
|
|||||||
v-model="clientSide"
|
v-model="clientSide"
|
||||||
placeholder="Select one"
|
placeholder="Select one"
|
||||||
:options="sideTypes"
|
:options="sideTypes"
|
||||||
:custom-label="
|
:custom-label="(value) => value.charAt(0).toUpperCase() + value.slice(1)"
|
||||||
(value) => value.charAt(0).toUpperCase() + value.slice(1)
|
|
||||||
"
|
|
||||||
:searchable="false"
|
:searchable="false"
|
||||||
:close-on-select="true"
|
:close-on-select="true"
|
||||||
:show-labels="false"
|
:show-labels="false"
|
||||||
@@ -126,9 +122,9 @@
|
|||||||
<span class="label__title">Server-side</span>
|
<span class="label__title">Server-side</span>
|
||||||
<span class="label__description">
|
<span class="label__description">
|
||||||
Select based on if the
|
Select based on if the
|
||||||
{{ $formatProjectType(project.project_type).toLowerCase() }} has
|
{{ $formatProjectType(project.project_type).toLowerCase() }} has functionality on the
|
||||||
functionality on the <strong>logical</strong> server. Remember
|
<strong>logical</strong> server. Remember that Singleplayer contains an integrated
|
||||||
that Singleplayer contains an integrated server.
|
server.
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<Multiselect
|
<Multiselect
|
||||||
@@ -136,9 +132,7 @@
|
|||||||
v-model="serverSide"
|
v-model="serverSide"
|
||||||
placeholder="Select one"
|
placeholder="Select one"
|
||||||
:options="sideTypes"
|
:options="sideTypes"
|
||||||
:custom-label="
|
:custom-label="(value) => value.charAt(0).toUpperCase() + value.slice(1)"
|
||||||
(value) => value.charAt(0).toUpperCase() + value.slice(1)
|
|
||||||
"
|
|
||||||
:searchable="false"
|
:searchable="false"
|
||||||
:close-on-select="true"
|
:close-on-select="true"
|
||||||
:show-labels="false"
|
:show-labels="false"
|
||||||
@@ -151,10 +145,9 @@
|
|||||||
<label for="project-visibility">
|
<label for="project-visibility">
|
||||||
<span class="label__title">Visibility</span>
|
<span class="label__title">Visibility</span>
|
||||||
<span class="label__description">
|
<span class="label__description">
|
||||||
Set the visibility of your project. Listed and archived projects are
|
Set the visibility of your project. Listed and archived projects are visible in search.
|
||||||
visible in search. Unlisted projects are published, but not visible
|
Unlisted projects are published, but not visible in search or on user profiles. Private
|
||||||
in search or on user profiles. Private projects are only accessible
|
projects are only accessible by members of the project.
|
||||||
by members of the project.
|
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<Multiselect
|
<Multiselect
|
||||||
@@ -190,8 +183,8 @@
|
|||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
Removes your project from Modrinth's servers and search. Clicking on
|
Removes your project from Modrinth's servers and search. Clicking on this will delete your
|
||||||
this will delete your project, so be extra careful!
|
project, so be extra careful!
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -212,11 +205,11 @@ import Avatar from '~/components/ui/Avatar'
|
|||||||
import ModalConfirm from '~/components/ui/ModalConfirm'
|
import ModalConfirm from '~/components/ui/ModalConfirm'
|
||||||
import FileInput from '~/components/ui/FileInput'
|
import FileInput from '~/components/ui/FileInput'
|
||||||
|
|
||||||
import UploadIcon from '~/assets/images/utils/upload.svg?inline'
|
import UploadIcon from '~/assets/images/utils/upload.svg'
|
||||||
import SaveIcon from '~/assets/images/utils/save.svg?inline'
|
import SaveIcon from '~/assets/images/utils/save.svg'
|
||||||
import TrashIcon from '~/assets/images/utils/trash.svg?inline'
|
import TrashIcon from '~/assets/images/utils/trash.svg'
|
||||||
|
|
||||||
export default {
|
export default defineNuxtComponent({
|
||||||
components: {
|
components: {
|
||||||
Avatar,
|
Avatar,
|
||||||
ModalConfirm,
|
ModalConfirm,
|
||||||
@@ -281,27 +274,19 @@ export default {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
name: '',
|
name: this.project.title,
|
||||||
slug: '',
|
slug: this.project.slug,
|
||||||
summary: '',
|
summary: this.project.description,
|
||||||
icon: null,
|
icon: null,
|
||||||
previewImage: null,
|
previewImage: null,
|
||||||
clientSide: '',
|
clientSide: this.project.client_side,
|
||||||
serverSide: '',
|
serverSide: this.project.server_side,
|
||||||
deletedIcon: false,
|
deletedIcon: false,
|
||||||
visibility: '',
|
visibility: this.$tag.approvedStatuses.includes(this.project.status)
|
||||||
|
? this.project.status
|
||||||
|
: this.project.requested_status,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fetch() {
|
|
||||||
this.name = this.project.title
|
|
||||||
this.slug = this.project.slug
|
|
||||||
this.summary = this.project.description
|
|
||||||
this.clientSide = this.project.client_side
|
|
||||||
this.serverSide = this.project.server_side
|
|
||||||
this.visibility = this.$tag.approvedStatuses.includes(this.project.status)
|
|
||||||
? this.project.status
|
|
||||||
: this.project.requested_status
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
hasPermission() {
|
hasPermission() {
|
||||||
const EDIT_DETAILS = 1 << 2
|
const EDIT_DETAILS = 1 << 2
|
||||||
@@ -309,9 +294,7 @@ export default {
|
|||||||
},
|
},
|
||||||
hasDeletePermission() {
|
hasDeletePermission() {
|
||||||
const DELETE_PROJECT = 1 << 7
|
const DELETE_PROJECT = 1 << 7
|
||||||
return (
|
return (this.currentMember.permissions & DELETE_PROJECT) === DELETE_PROJECT
|
||||||
(this.currentMember.permissions & DELETE_PROJECT) === DELETE_PROJECT
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
sideTypes() {
|
sideTypes() {
|
||||||
return ['required', 'optional', 'unsupported']
|
return ['required', 'optional', 'unsupported']
|
||||||
@@ -345,9 +328,7 @@ export default {
|
|||||||
return data
|
return data
|
||||||
},
|
},
|
||||||
hasChanges() {
|
hasChanges() {
|
||||||
return (
|
return Object.keys(this.patchData).length > 0 || this.deletedIcon || this.icon
|
||||||
Object.keys(this.patchData).length > 0 || this.deletedIcon || this.icon
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -374,12 +355,12 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async deleteProject() {
|
async deleteProject() {
|
||||||
await this.$axios.delete(
|
await useBaseFetch(`project/${this.project.id}`, {
|
||||||
`project/${this.project.id}`,
|
method: 'DELETE',
|
||||||
this.$defaultHeaders()
|
...this.$defaultHeaders(),
|
||||||
)
|
})
|
||||||
await this.$store.dispatch('user/fetchProjects')
|
await initUserProjects()
|
||||||
await this.$router.push(`/dashboard/projects`)
|
await this.$router.push('/dashboard/projects')
|
||||||
this.$notify({
|
this.$notify({
|
||||||
group: 'main',
|
group: 'main',
|
||||||
title: 'Project deleted',
|
title: 'Project deleted',
|
||||||
@@ -393,10 +374,10 @@ export default {
|
|||||||
this.previewImage = null
|
this.previewImage = null
|
||||||
},
|
},
|
||||||
async deleteIcon() {
|
async deleteIcon() {
|
||||||
await this.$axios.delete(
|
await useBaseFetch(`project/${this.project.id}/icon`, {
|
||||||
`project/${this.project.id}/icon`,
|
method: 'DELETE',
|
||||||
this.$defaultHeaders()
|
...this.$defaultHeaders(),
|
||||||
)
|
})
|
||||||
await this.updateIcon()
|
await this.updateIcon()
|
||||||
this.$notify({
|
this.$notify({
|
||||||
group: 'main',
|
group: 'main',
|
||||||
@@ -406,7 +387,7 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
})
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.summary-input {
|
.summary-input {
|
||||||
@@ -6,34 +6,24 @@
|
|||||||
<span class="label__title size-card-header">License</span>
|
<span class="label__title size-card-header">License</span>
|
||||||
<span class="label__description">
|
<span class="label__description">
|
||||||
It is very important to choose a proper license for your
|
It is very important to choose a proper license for your
|
||||||
{{ $formatProjectType(project.project_type).toLowerCase() }}. You
|
{{ $formatProjectType(project.project_type).toLowerCase() }}. You may choose one from
|
||||||
may choose one from our list or provide a custom license. You may
|
our list or provide a custom license. You may also provide a custom URL to your chosen
|
||||||
also provide a custom URL to your chosen license; otherwise, the
|
license; otherwise, the license text will be displayed.
|
||||||
license text will be displayed.
|
<span v-if="license && license.friendly === 'Custom'" class="label__subdescription">
|
||||||
<span
|
|
||||||
v-if="license && license.friendly === 'Custom'"
|
|
||||||
class="label__subdescription"
|
|
||||||
>
|
|
||||||
Enter a valid
|
Enter a valid
|
||||||
<a
|
<a href="https://spdx.org/licenses/" target="_blank" rel="noopener" class="text-link">
|
||||||
href="https://spdx.org/licenses/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
class="text-link"
|
|
||||||
>
|
|
||||||
SPDX license identifier</a
|
SPDX license identifier</a
|
||||||
>
|
>
|
||||||
in the marked area. If your license does not have a SPDX
|
in the marked area. If your license does not have a SPDX identifier (for example, if
|
||||||
identifier (for example, if you created the license yourself or if
|
you created the license yourself or if the license is Minecraft-specific), simply
|
||||||
the license is Minecraft-specific), simply check the box and enter
|
check the box and enter the name of the license instead.
|
||||||
the name of the license instead.
|
|
||||||
</span>
|
</span>
|
||||||
<span class="label__subdescription">
|
<span class="label__subdescription">
|
||||||
Confused? See our
|
Confused? See our
|
||||||
<a
|
<a
|
||||||
href="https://blog.modrinth.com/licensing-guide/"
|
href="https://blog.modrinth.com/licensing-guide/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener"
|
||||||
class="text-link"
|
class="text-link"
|
||||||
>
|
>
|
||||||
licensing guide</a
|
licensing guide</a
|
||||||
@@ -62,6 +52,7 @@
|
|||||||
v-if="license.requiresOnlyOrLater"
|
v-if="license.requiresOnlyOrLater"
|
||||||
v-model="allowOrLater"
|
v-model="allowOrLater"
|
||||||
:disabled="!hasPermission"
|
:disabled="!hasPermission"
|
||||||
|
description="Allow later editions of this license"
|
||||||
>
|
>
|
||||||
Allow later editions of this license
|
Allow later editions of this license
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
@@ -69,6 +60,7 @@
|
|||||||
v-if="license.friendly === 'Custom'"
|
v-if="license.friendly === 'Custom'"
|
||||||
v-model="nonSpdxLicense"
|
v-model="nonSpdxLicense"
|
||||||
:disabled="!hasPermission"
|
:disabled="!hasPermission"
|
||||||
|
description="License does not have a SPDX identifier"
|
||||||
>
|
>
|
||||||
License does not have a SPDX identifier
|
License does not have a SPDX identifier
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
@@ -110,9 +102,9 @@
|
|||||||
<script>
|
<script>
|
||||||
import Multiselect from 'vue-multiselect'
|
import Multiselect from 'vue-multiselect'
|
||||||
import Checkbox from '~/components/ui/Checkbox'
|
import Checkbox from '~/components/ui/Checkbox'
|
||||||
import SaveIcon from '~/assets/images/utils/save.svg?inline'
|
import SaveIcon from '~/assets/images/utils/save.svg'
|
||||||
|
|
||||||
export default {
|
export default defineNuxtComponent({
|
||||||
components: {
|
components: {
|
||||||
Multiselect,
|
Multiselect,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
@@ -154,28 +146,110 @@ export default {
|
|||||||
showKnownErrors: false,
|
showKnownErrors: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fetch() {
|
async setup(props) {
|
||||||
this.licenseUrl = this.project.license.url
|
const defaultLicenses = shallowRef([
|
||||||
|
{ friendly: 'Custom', short: '' },
|
||||||
|
{
|
||||||
|
friendly: 'All Rights Reserved/No License',
|
||||||
|
short: 'All-Rights-Reserved',
|
||||||
|
},
|
||||||
|
{ friendly: 'Apache License 2.0', short: 'Apache-2.0' },
|
||||||
|
{
|
||||||
|
friendly: 'BSD 2-Clause "Simplified" License',
|
||||||
|
short: 'BSD-2-Clause',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
friendly: 'BSD 3-Clause "New" or "Revised" License',
|
||||||
|
short: 'BSD-3-Clause',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
friendly: 'CC Zero (Public Domain equivalent)',
|
||||||
|
short: 'CC0-1.0',
|
||||||
|
},
|
||||||
|
{ friendly: 'CC-BY 4.0', short: 'CC-BY-4.0' },
|
||||||
|
{
|
||||||
|
friendly: 'CC-BY-SA 4.0',
|
||||||
|
short: 'CC-BY-SA-4.0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
friendly: 'CC-BY-NC 4.0',
|
||||||
|
short: 'CC-BY-NC-4.0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
friendly: 'CC-BY-NC-SA 4.0',
|
||||||
|
short: 'CC-BY-NC-SA-4.0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
friendly: 'CC-BY-ND 4.0',
|
||||||
|
short: 'CC-BY-ND-4.0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
friendly: 'CC-BY-NC-ND 4.0',
|
||||||
|
short: 'CC-BY-NC-ND-4.0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
friendly: 'GNU Affero General Public License v3',
|
||||||
|
short: 'AGPL-3.0',
|
||||||
|
requiresOnlyOrLater: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
friendly: 'GNU Lesser General Public License v2.1',
|
||||||
|
short: 'LGPL-2.1',
|
||||||
|
requiresOnlyOrLater: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
friendly: 'GNU Lesser General Public License v3',
|
||||||
|
short: 'LGPL-3.0',
|
||||||
|
requiresOnlyOrLater: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
friendly: 'GNU General Public License v2',
|
||||||
|
short: 'GPL-2.0',
|
||||||
|
requiresOnlyOrLater: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
friendly: 'GNU General Public License v3',
|
||||||
|
short: 'GPL-3.0',
|
||||||
|
requiresOnlyOrLater: true,
|
||||||
|
},
|
||||||
|
{ friendly: 'ISC License', short: 'ISC' },
|
||||||
|
{ friendly: 'MIT License', short: 'MIT' },
|
||||||
|
{ friendly: 'Mozilla Public License 2.0', short: 'MPL-2.0' },
|
||||||
|
{ friendly: 'zlib License', short: 'Zlib' },
|
||||||
|
])
|
||||||
|
|
||||||
const licenseId = this.project.license.id
|
const licenseUrl = ref(props.project.license.url)
|
||||||
|
|
||||||
|
const licenseId = props.project.license.id
|
||||||
const trimmedLicenseId = licenseId
|
const trimmedLicenseId = licenseId
|
||||||
.replaceAll('-only', '')
|
.replaceAll('-only', '')
|
||||||
.replaceAll('-or-later', '')
|
.replaceAll('-or-later', '')
|
||||||
.replaceAll('LicenseRef-', '')
|
.replaceAll('LicenseRef-', '')
|
||||||
this.license = this.defaultLicenses.find(
|
|
||||||
(x) => x.short === trimmedLicenseId
|
const license = ref(
|
||||||
) ?? {
|
defaultLicenses.value.find((x) => x.short === trimmedLicenseId) ?? {
|
||||||
friendly: 'Custom',
|
friendly: 'Custom',
|
||||||
short: licenseId.replaceAll('LicenseRef-', ''),
|
short: licenseId.replaceAll('LicenseRef-', ''),
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
|
||||||
if (licenseId === 'LicenseRef-Unknown') {
|
if (licenseId === 'LicenseRef-Unknown') {
|
||||||
this.license = {
|
license.value = {
|
||||||
friendly: 'Unknown',
|
friendly: 'Unknown',
|
||||||
short: licenseId.replaceAll('LicenseRef-', ''),
|
short: licenseId.replaceAll('LicenseRef-', ''),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.allowOrLater = licenseId.includes('-or-later')
|
|
||||||
this.nonSpdxLicense = licenseId.includes('LicenseRef-')
|
const allowOrLater = computed(() => props.project.license.id.includes('-or-later'))
|
||||||
|
const nonSpdxLicense = computed(() => props.project.license.id.includes('LicenseRef-'))
|
||||||
|
|
||||||
|
return {
|
||||||
|
defaultLicenses,
|
||||||
|
licenseUrl,
|
||||||
|
license,
|
||||||
|
allowOrLater,
|
||||||
|
nonSpdxLicense,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
hasPermission() {
|
hasPermission() {
|
||||||
@@ -188,87 +262,18 @@ export default {
|
|||||||
(this.nonSpdxLicense && this.license.friendly === 'Custom') ||
|
(this.nonSpdxLicense && this.license.friendly === 'Custom') ||
|
||||||
this.license.short === 'All-Rights-Reserved' ||
|
this.license.short === 'All-Rights-Reserved' ||
|
||||||
this.license.short === 'Unknown'
|
this.license.short === 'Unknown'
|
||||||
)
|
) {
|
||||||
id += 'LicenseRef-'
|
id += 'LicenseRef-'
|
||||||
|
}
|
||||||
id += this.license.short
|
id += this.license.short
|
||||||
if (this.license.requiresOnlyOrLater)
|
if (this.license.requiresOnlyOrLater) {
|
||||||
id += this.allowOrLater ? '-or-later' : '-only'
|
id += this.allowOrLater ? '-or-later' : '-only'
|
||||||
if (this.nonSpdxLicense && this.license.friendly === 'Custom')
|
}
|
||||||
|
if (this.nonSpdxLicense && this.license.friendly === 'Custom') {
|
||||||
id = id.replaceAll(' ', '-')
|
id = id.replaceAll(' ', '-')
|
||||||
|
}
|
||||||
return id
|
return id
|
||||||
},
|
},
|
||||||
defaultLicenses() {
|
|
||||||
return [
|
|
||||||
{ friendly: 'Custom', short: '' },
|
|
||||||
{
|
|
||||||
friendly: 'All Rights Reserved/No License',
|
|
||||||
short: 'All-Rights-Reserved',
|
|
||||||
},
|
|
||||||
{ friendly: 'Apache License 2.0', short: 'Apache-2.0' },
|
|
||||||
{
|
|
||||||
friendly: 'BSD 2-Clause "Simplified" License',
|
|
||||||
short: 'BSD-2-Clause',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
friendly: 'BSD 3-Clause "New" or "Revised" License',
|
|
||||||
short: 'BSD-3-Clause',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
friendly: 'CC Zero (Public Domain equivalent)',
|
|
||||||
short: 'CC0-1.0',
|
|
||||||
},
|
|
||||||
{ friendly: 'CC-BY 4.0', short: 'CC-BY-4.0' },
|
|
||||||
{
|
|
||||||
friendly: 'CC-BY-SA 4.0',
|
|
||||||
short: 'CC-BY-SA-4.0',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
friendly: 'CC-BY-NC 4.0',
|
|
||||||
short: 'CC-BY-NC-4.0',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
friendly: 'CC-BY-NC-SA 4.0',
|
|
||||||
short: 'CC-BY-NC-SA-4.0',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
friendly: 'CC-BY-ND 4.0',
|
|
||||||
short: 'CC-BY-ND-4.0',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
friendly: 'CC-BY-NC-ND 4.0',
|
|
||||||
short: 'CC-BY-NC-ND-4.0',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
friendly: 'GNU Affero General Public License v3',
|
|
||||||
short: 'AGPL-3.0',
|
|
||||||
requiresOnlyOrLater: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
friendly: 'GNU Lesser General Public License v2.1',
|
|
||||||
short: 'LGPL-2.1',
|
|
||||||
requiresOnlyOrLater: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
friendly: 'GNU Lesser General Public License v3',
|
|
||||||
short: 'LGPL-3.0',
|
|
||||||
requiresOnlyOrLater: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
friendly: 'GNU General Public License v2',
|
|
||||||
short: 'GPL-2.0',
|
|
||||||
requiresOnlyOrLater: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
friendly: 'GNU General Public License v3',
|
|
||||||
short: 'GPL-3.0',
|
|
||||||
requiresOnlyOrLater: true,
|
|
||||||
},
|
|
||||||
{ friendly: 'ISC License', short: 'ISC' },
|
|
||||||
{ friendly: 'MIT License', short: 'MIT' },
|
|
||||||
{ friendly: 'Mozilla Public License 2.0', short: 'MPL-2.0' },
|
|
||||||
{ friendly: 'zlib License', short: 'Zlib' },
|
|
||||||
]
|
|
||||||
},
|
|
||||||
patchData() {
|
patchData() {
|
||||||
const data = {}
|
const data = {}
|
||||||
|
|
||||||
@@ -292,6 +297,6 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
})
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped></style>
|
<style lang="scss" scoped></style>
|
||||||
@@ -9,8 +9,7 @@
|
|||||||
>
|
>
|
||||||
<span class="label__title">Issue tracker</span>
|
<span class="label__title">Issue tracker</span>
|
||||||
<span class="label__description">
|
<span class="label__description">
|
||||||
A place for users to report bugs, issues, and concerns about your
|
A place for users to report bugs, issues, and concerns about your project.
|
||||||
project.
|
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -48,8 +47,7 @@
|
|||||||
>
|
>
|
||||||
<span class="label__title">Wiki page</span>
|
<span class="label__title">Wiki page</span>
|
||||||
<span class="label__description">
|
<span class="label__description">
|
||||||
A page containing information, documentation, and help for the
|
A page containing information, documentation, and help for the project.
|
||||||
project.
|
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -62,14 +60,9 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="adjacent-input">
|
<div class="adjacent-input">
|
||||||
<label
|
<label id="project-discord-invite" title="An invitation link to your Discord server.">
|
||||||
id="project-discord-invite"
|
|
||||||
title="An invitation link to your Discord server."
|
|
||||||
>
|
|
||||||
<span class="label__title">Discord invite</span>
|
<span class="label__title">Discord invite</span>
|
||||||
<span class="label__description">
|
<span class="label__description"> An invitation link to your Discord server. </span>
|
||||||
An invitation link to your Discord server.
|
|
||||||
</span>
|
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="project-discord-invite"
|
id="project-discord-invite"
|
||||||
@@ -100,7 +93,7 @@
|
|||||||
:close-on-select="true"
|
:close-on-select="true"
|
||||||
:show-labels="false"
|
:show-labels="false"
|
||||||
:disabled="!hasPermission"
|
:disabled="!hasPermission"
|
||||||
@input="updateDonationLinks"
|
@update:model-value="updateDonationLinks"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
v-model="donationLink.url"
|
v-model="donationLink.url"
|
||||||
@@ -128,9 +121,9 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Multiselect from 'vue-multiselect'
|
import Multiselect from 'vue-multiselect'
|
||||||
import SaveIcon from '~/assets/images/utils/save.svg?inline'
|
import SaveIcon from '~/assets/images/utils/save.svg'
|
||||||
|
|
||||||
export default {
|
export default defineNuxtComponent({
|
||||||
components: {
|
components: {
|
||||||
Multiselect,
|
Multiselect,
|
||||||
SaveIcon,
|
SaveIcon,
|
||||||
@@ -163,23 +156,22 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
|
const donationLinks = JSON.parse(JSON.stringify(this.project.donation_urls))
|
||||||
|
donationLinks.push({
|
||||||
|
id: null,
|
||||||
|
platform: null,
|
||||||
|
url: null,
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
issuesUrl: '',
|
issuesUrl: this.project.issues_url,
|
||||||
sourceUrl: '',
|
sourceUrl: this.project.source_url,
|
||||||
wikiUrl: '',
|
wikiUrl: this.project.wiki_url,
|
||||||
discordUrl: '',
|
discordUrl: this.project.discord_url,
|
||||||
|
|
||||||
donationLinks: [],
|
donationLinks,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fetch() {
|
|
||||||
this.issuesUrl = this.project.issues_url
|
|
||||||
this.sourceUrl = this.project.source_url
|
|
||||||
this.wikiUrl = this.project.wiki_url
|
|
||||||
this.discordUrl = this.project.discord_url
|
|
||||||
|
|
||||||
this.resetDonationLinks()
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
hasPermission() {
|
hasPermission() {
|
||||||
const EDIT_DETAILS = 1 << 2
|
const EDIT_DETAILS = 1 << 2
|
||||||
@@ -198,13 +190,10 @@ export default {
|
|||||||
data.wiki_url = this.wikiUrl === '' ? null : this.wikiUrl.trim()
|
data.wiki_url = this.wikiUrl === '' ? null : this.wikiUrl.trim()
|
||||||
}
|
}
|
||||||
if (this.checkDifference(this.discordUrl, this.project.discord_url)) {
|
if (this.checkDifference(this.discordUrl, this.project.discord_url)) {
|
||||||
data.discord_url =
|
data.discord_url = this.discordUrl === '' ? null : this.discordUrl.trim()
|
||||||
this.discordUrl === '' ? null : this.discordUrl.trim()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const donationLinks = this.donationLinks.filter(
|
const donationLinks = this.donationLinks.filter((link) => link.url && link.platform)
|
||||||
(link) => link.url && link.platform
|
|
||||||
)
|
|
||||||
donationLinks.forEach((link) => {
|
donationLinks.forEach((link) => {
|
||||||
link.id = this.$tag.donationPlatforms.find(
|
link.id = this.$tag.donationPlatforms.find(
|
||||||
(platform) => platform.name === link.platform
|
(platform) => platform.name === link.platform
|
||||||
@@ -230,7 +219,12 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
async saveChanges() {
|
async saveChanges() {
|
||||||
if (this.patchData && (await this.patchProject(this.patchData))) {
|
if (this.patchData && (await this.patchProject(this.patchData))) {
|
||||||
this.resetDonationLinks()
|
this.donationLinks = JSON.parse(JSON.stringify(this.project.donation_urls))
|
||||||
|
this.donationLinks.push({
|
||||||
|
id: null,
|
||||||
|
platform: null,
|
||||||
|
url: null,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updateDonationLinks() {
|
updateDonationLinks() {
|
||||||
@@ -258,16 +252,6 @@ export default {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
resetDonationLinks() {
|
|
||||||
this.donationLinks = JSON.parse(
|
|
||||||
JSON.stringify(this.project.donation_urls)
|
|
||||||
)
|
|
||||||
this.donationLinks.push({
|
|
||||||
id: null,
|
|
||||||
platform: null,
|
|
||||||
url: null,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
checkDifference(newLink, existingLink) {
|
checkDifference(newLink, existingLink) {
|
||||||
if (newLink === '' && existingLink !== null) {
|
if (newLink === '' && existingLink !== null) {
|
||||||
return true
|
return true
|
||||||
@@ -278,7 +262,7 @@ export default {
|
|||||||
return newLink !== existingLink
|
return newLink !== existingLink
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
})
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.donation-link-group {
|
.donation-link-group {
|
||||||
@@ -9,20 +9,15 @@
|
|||||||
<span class="label">
|
<span class="label">
|
||||||
<span class="label__title">Invite a member</span>
|
<span class="label__title">Invite a member</span>
|
||||||
<span class="label__description">
|
<span class="label__description">
|
||||||
Enter the Modrinth username of the person you'd like to invite to be a
|
Enter the Modrinth username of the person you'd like to invite to be a member of this
|
||||||
member of this project.
|
project.
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<div
|
<div
|
||||||
v-if="(currentMember.permissions & MANAGE_INVITES) === MANAGE_INVITES"
|
v-if="(currentMember.permissions & MANAGE_INVITES) === MANAGE_INVITES"
|
||||||
class="input-group"
|
class="input-group"
|
||||||
>
|
>
|
||||||
<input
|
<input id="username" v-model="currentUsername" type="text" placeholder="Username" />
|
||||||
id="username"
|
|
||||||
v-model="currentUsername"
|
|
||||||
type="text"
|
|
||||||
placeholder="Username"
|
|
||||||
/>
|
|
||||||
<label for="username" class="hidden">Username</label>
|
<label for="username" class="hidden">Username</label>
|
||||||
<button class="iconified-button brand-button" @click="inviteTeamMember">
|
<button class="iconified-button brand-button" @click="inviteTeamMember">
|
||||||
<UserPlusIcon />
|
<UserPlusIcon />
|
||||||
@@ -38,12 +33,7 @@
|
|||||||
>
|
>
|
||||||
<div class="member-header">
|
<div class="member-header">
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<Avatar
|
<Avatar :src="member.avatar_url" :alt="member.username" size="sm" circle />
|
||||||
:src="member.avatar_url"
|
|
||||||
:alt="member.username"
|
|
||||||
size="sm"
|
|
||||||
circle
|
|
||||||
/>
|
|
||||||
<div class="text">
|
<div class="text">
|
||||||
<nuxt-link :to="'/user/' + member.user.username" class="name">
|
<nuxt-link :to="'/user/' + member.user.username" class="name">
|
||||||
<p>{{ member.name }}</p>
|
<p>{{ member.name }}</p>
|
||||||
@@ -59,9 +49,7 @@
|
|||||||
@click="
|
@click="
|
||||||
openTeamMembers.indexOf(member.user.id) === -1
|
openTeamMembers.indexOf(member.user.id) === -1
|
||||||
? openTeamMembers.push(member.user.id)
|
? openTeamMembers.push(member.user.id)
|
||||||
: (openTeamMembers = openTeamMembers.filter(
|
: (openTeamMembers = openTeamMembers.filter((it) => it !== member.user.id))
|
||||||
(it) => it !== member.user.id
|
|
||||||
))
|
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<DropdownIcon />
|
<DropdownIcon />
|
||||||
@@ -81,37 +69,27 @@
|
|||||||
v-model="allTeamMembers[index].role"
|
v-model="allTeamMembers[index].role"
|
||||||
type="text"
|
type="text"
|
||||||
:class="{ 'known-error': member.role === 'Owner' }"
|
:class="{ 'known-error': member.role === 'Owner' }"
|
||||||
:disabled="
|
:disabled="(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER"
|
||||||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER
|
|
||||||
"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="adjacent-input">
|
<div class="adjacent-input">
|
||||||
<label
|
<label :for="`member-${allTeamMembers[index].user.username}-monetization-weight`">
|
||||||
:for="`member-${allTeamMembers[index].user.username}-monetization-weight`"
|
|
||||||
>
|
|
||||||
<span class="label__title">Monetization weight</span>
|
<span class="label__title">Monetization weight</span>
|
||||||
<span class="label__description">
|
<span class="label__description">
|
||||||
Relative to all other members' monetization weights, this
|
Relative to all other members' monetization weights, this determines what portion of
|
||||||
determines what portion of this project's revenue goes to this
|
this project's revenue goes to this member.
|
||||||
member.
|
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
:id="`member-${allTeamMembers[index].user.username}-monetization-weight`"
|
:id="`member-${allTeamMembers[index].user.username}-monetization-weight`"
|
||||||
v-model="allTeamMembers[index].payouts_split"
|
v-model="allTeamMembers[index].payouts_split"
|
||||||
type="number"
|
type="number"
|
||||||
:disabled="
|
:disabled="(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER"
|
||||||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER
|
|
||||||
"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p
|
<p v-if="member.role === 'Owner' && member.oldRole !== 'Owner'" class="known-errors">
|
||||||
v-if="member.role === 'Owner' && member.oldRole !== 'Owner'"
|
A project can only have one 'Owner'. Use the 'Transfer ownership' button below if you no
|
||||||
class="known-errors"
|
longer wish to be owner.
|
||||||
>
|
|
||||||
A project can only have one 'Owner'. Use the 'Transfer ownership'
|
|
||||||
button below if you no longer wish to be owner.
|
|
||||||
</p>
|
</p>
|
||||||
<template v-if="member.oldRole !== 'Owner'">
|
<template v-if="member.oldRole !== 'Owner'">
|
||||||
<span class="label">
|
<span class="label">
|
||||||
@@ -119,102 +97,98 @@
|
|||||||
</span>
|
</span>
|
||||||
<div class="permissions">
|
<div class="permissions">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
:value="(member.permissions & UPLOAD_VERSION) === UPLOAD_VERSION"
|
:model-value="(member.permissions & UPLOAD_VERSION) === UPLOAD_VERSION"
|
||||||
:disabled="
|
:disabled="
|
||||||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
||||||
(currentMember.permissions & UPLOAD_VERSION) !== UPLOAD_VERSION
|
(currentMember.permissions & UPLOAD_VERSION) !== UPLOAD_VERSION
|
||||||
"
|
"
|
||||||
label="Upload version"
|
label="Upload version"
|
||||||
@input="allTeamMembers[index].permissions ^= UPLOAD_VERSION"
|
@update:model-value="allTeamMembers[index].permissions ^= UPLOAD_VERSION"
|
||||||
/>
|
/>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
:value="(member.permissions & DELETE_VERSION) === DELETE_VERSION"
|
:model-value="(member.permissions & DELETE_VERSION) === DELETE_VERSION"
|
||||||
:disabled="
|
:disabled="
|
||||||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
||||||
(currentMember.permissions & DELETE_VERSION) !== DELETE_VERSION
|
(currentMember.permissions & DELETE_VERSION) !== DELETE_VERSION
|
||||||
"
|
"
|
||||||
label="Delete version"
|
label="Delete version"
|
||||||
@input="allTeamMembers[index].permissions ^= DELETE_VERSION"
|
@update:model-value="allTeamMembers[index].permissions ^= DELETE_VERSION"
|
||||||
/>
|
/>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
:value="(member.permissions & EDIT_DETAILS) === EDIT_DETAILS"
|
:model-value="(member.permissions & EDIT_DETAILS) === EDIT_DETAILS"
|
||||||
:disabled="
|
:disabled="
|
||||||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
||||||
(currentMember.permissions & EDIT_DETAILS) !== EDIT_DETAILS
|
(currentMember.permissions & EDIT_DETAILS) !== EDIT_DETAILS
|
||||||
"
|
"
|
||||||
label="Edit details"
|
label="Edit details"
|
||||||
@input="allTeamMembers[index].permissions ^= EDIT_DETAILS"
|
@update:model-value="allTeamMembers[index].permissions ^= EDIT_DETAILS"
|
||||||
/>
|
/>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
:value="(member.permissions & EDIT_BODY) === EDIT_BODY"
|
:model-value="(member.permissions & EDIT_BODY) === EDIT_BODY"
|
||||||
:disabled="
|
:disabled="
|
||||||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
||||||
(currentMember.permissions & EDIT_BODY) !== EDIT_BODY
|
(currentMember.permissions & EDIT_BODY) !== EDIT_BODY
|
||||||
"
|
"
|
||||||
label="Edit body"
|
label="Edit body"
|
||||||
@input="allTeamMembers[index].permissions ^= EDIT_BODY"
|
@update:model-value="allTeamMembers[index].permissions ^= EDIT_BODY"
|
||||||
/>
|
/>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
:value="(member.permissions & MANAGE_INVITES) === MANAGE_INVITES"
|
:model-value="(member.permissions & MANAGE_INVITES) === MANAGE_INVITES"
|
||||||
:disabled="
|
:disabled="
|
||||||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
||||||
(currentMember.permissions & MANAGE_INVITES) !== MANAGE_INVITES
|
(currentMember.permissions & MANAGE_INVITES) !== MANAGE_INVITES
|
||||||
"
|
"
|
||||||
label="Manage invites"
|
label="Manage invites"
|
||||||
@input="allTeamMembers[index].permissions ^= MANAGE_INVITES"
|
@update:model-value="allTeamMembers[index].permissions ^= MANAGE_INVITES"
|
||||||
/>
|
/>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
:value="(member.permissions & REMOVE_MEMBER) === REMOVE_MEMBER"
|
:model-value="(member.permissions & REMOVE_MEMBER) === REMOVE_MEMBER"
|
||||||
:disabled="
|
:disabled="
|
||||||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
||||||
(currentMember.permissions & REMOVE_MEMBER) !== REMOVE_MEMBER
|
(currentMember.permissions & REMOVE_MEMBER) !== REMOVE_MEMBER
|
||||||
"
|
"
|
||||||
label="Remove member"
|
label="Remove member"
|
||||||
@input="allTeamMembers[index].permissions ^= REMOVE_MEMBER"
|
@update:model-value="allTeamMembers[index].permissions ^= REMOVE_MEMBER"
|
||||||
/>
|
/>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
:value="(member.permissions & EDIT_MEMBER) === EDIT_MEMBER"
|
:model-value="(member.permissions & EDIT_MEMBER) === EDIT_MEMBER"
|
||||||
:disabled="
|
:disabled="(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER"
|
||||||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER
|
|
||||||
"
|
|
||||||
label="Edit member"
|
label="Edit member"
|
||||||
@input="allTeamMembers[index].permissions ^= EDIT_MEMBER"
|
@update:model-value="allTeamMembers[index].permissions ^= EDIT_MEMBER"
|
||||||
/>
|
/>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
:value="(member.permissions & DELETE_PROJECT) === DELETE_PROJECT"
|
:model-value="(member.permissions & DELETE_PROJECT) === DELETE_PROJECT"
|
||||||
:disabled="
|
:disabled="
|
||||||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
||||||
(currentMember.permissions & DELETE_PROJECT) !== DELETE_PROJECT
|
(currentMember.permissions & DELETE_PROJECT) !== DELETE_PROJECT
|
||||||
"
|
"
|
||||||
label="Delete project"
|
label="Delete project"
|
||||||
@input="allTeamMembers[index].permissions ^= DELETE_PROJECT"
|
@update:model-value="allTeamMembers[index].permissions ^= DELETE_PROJECT"
|
||||||
/>
|
/>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
:value="(member.permissions & VIEW_ANALYTICS) === VIEW_ANALYTICS"
|
:model-value="(member.permissions & VIEW_ANALYTICS) === VIEW_ANALYTICS"
|
||||||
:disabled="
|
:disabled="
|
||||||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
||||||
(currentMember.permissions & VIEW_ANALYTICS) !== VIEW_ANALYTICS
|
(currentMember.permissions & VIEW_ANALYTICS) !== VIEW_ANALYTICS
|
||||||
"
|
"
|
||||||
label="View analytics"
|
label="View analytics"
|
||||||
@input="allTeamMembers[index].permissions ^= VIEW_ANALYTICS"
|
@update:model-value="allTeamMembers[index].permissions ^= VIEW_ANALYTICS"
|
||||||
/>
|
/>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
:value="(member.permissions & VIEW_PAYOUTS) === VIEW_PAYOUTS"
|
:model-value="(member.permissions & VIEW_PAYOUTS) === VIEW_PAYOUTS"
|
||||||
:disabled="
|
:disabled="
|
||||||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
||||||
(currentMember.permissions & VIEW_PAYOUTS) !== VIEW_PAYOUTS
|
(currentMember.permissions & VIEW_PAYOUTS) !== VIEW_PAYOUTS
|
||||||
"
|
"
|
||||||
label="View revenue"
|
label="View revenue"
|
||||||
@input="allTeamMembers[index].permissions ^= VIEW_PAYOUTS"
|
@update:model-value="allTeamMembers[index].permissions ^= VIEW_PAYOUTS"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<button
|
<button
|
||||||
class="iconified-button brand-button"
|
class="iconified-button brand-button"
|
||||||
:disabled="
|
:disabled="(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER"
|
||||||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER
|
|
||||||
"
|
|
||||||
@click="updateTeamMember(index)"
|
@click="updateTeamMember(index)"
|
||||||
>
|
>
|
||||||
<SaveIcon />
|
<SaveIcon />
|
||||||
@@ -223,20 +197,14 @@
|
|||||||
<button
|
<button
|
||||||
v-if="member.oldRole !== 'Owner'"
|
v-if="member.oldRole !== 'Owner'"
|
||||||
class="iconified-button danger-button"
|
class="iconified-button danger-button"
|
||||||
:disabled="
|
:disabled="(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER"
|
||||||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER
|
|
||||||
"
|
|
||||||
@click="removeTeamMember(index)"
|
@click="removeTeamMember(index)"
|
||||||
>
|
>
|
||||||
<UserRemoveIcon />
|
<UserRemoveIcon />
|
||||||
Remove member
|
Remove member
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="
|
v-if="member.oldRole !== 'Owner' && currentMember.role === 'Owner' && member.accepted"
|
||||||
member.oldRole !== 'Owner' &&
|
|
||||||
currentMember.role === 'Owner' &&
|
|
||||||
member.accepted
|
|
||||||
"
|
|
||||||
class="iconified-button"
|
class="iconified-button"
|
||||||
@click="transferOwnership(index)"
|
@click="transferOwnership(index)"
|
||||||
>
|
>
|
||||||
@@ -253,14 +221,14 @@
|
|||||||
import Checkbox from '~/components/ui/Checkbox'
|
import Checkbox from '~/components/ui/Checkbox'
|
||||||
import Badge from '~/components/ui/Badge'
|
import Badge from '~/components/ui/Badge'
|
||||||
|
|
||||||
import DropdownIcon from '~/assets/images/utils/dropdown.svg?inline'
|
import DropdownIcon from '~/assets/images/utils/dropdown.svg'
|
||||||
import SaveIcon from '~/assets/images/utils/save.svg?inline'
|
import SaveIcon from '~/assets/images/utils/save.svg'
|
||||||
import TransferIcon from '~/assets/images/utils/transfer.svg?inline'
|
import TransferIcon from '~/assets/images/utils/transfer.svg'
|
||||||
import UserPlusIcon from '~/assets/images/utils/user-plus.svg?inline'
|
import UserPlusIcon from '~/assets/images/utils/user-plus.svg'
|
||||||
import UserRemoveIcon from '~/assets/images/utils/user-x.svg?inline'
|
import UserRemoveIcon from '~/assets/images/utils/user-x.svg'
|
||||||
import Avatar from '~/components/ui/Avatar'
|
import Avatar from '~/components/ui/Avatar'
|
||||||
|
|
||||||
export default {
|
export default defineNuxtComponent({
|
||||||
components: {
|
components: {
|
||||||
Avatar,
|
Avatar,
|
||||||
DropdownIcon,
|
DropdownIcon,
|
||||||
@@ -295,14 +263,12 @@ export default {
|
|||||||
return {
|
return {
|
||||||
currentUsername: '',
|
currentUsername: '',
|
||||||
openTeamMembers: [],
|
openTeamMembers: [],
|
||||||
allTeamMembers: [],
|
allTeamMembers: this.allMembers.map((x) => {
|
||||||
|
x.oldRole = x.role
|
||||||
|
return x
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fetch() {
|
|
||||||
this.allTeamMembers = this.allMembers
|
|
||||||
|
|
||||||
this.allTeamMembers.forEach((x) => (x.oldRole = x.role))
|
|
||||||
},
|
|
||||||
created() {
|
created() {
|
||||||
this.UPLOAD_VERSION = 1 << 0
|
this.UPLOAD_VERSION = 1 << 0
|
||||||
this.DELETE_VERSION = 1 << 1
|
this.DELETE_VERSION = 1 << 1
|
||||||
@@ -317,55 +283,57 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async inviteTeamMember() {
|
async inviteTeamMember() {
|
||||||
this.$nuxt.$loading.start()
|
startLoading()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const user = (await this.$axios.get(`user/${this.currentUsername}`))
|
const user = await useBaseFetch(`user/${this.currentUsername}`)
|
||||||
.data
|
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
user_id: user.id.trim(),
|
user_id: user.id.trim(),
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.$axios.post(
|
await useBaseFetch(`team/${this.project.team}/members`, {
|
||||||
`team/${this.project.team}/members`,
|
method: 'POST',
|
||||||
data,
|
body: data,
|
||||||
this.$defaultHeaders()
|
...this.$defaultHeaders(),
|
||||||
)
|
})
|
||||||
await this.updateMembers()
|
await this.updateMembers()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.$notify({
|
this.$notify({
|
||||||
group: 'main',
|
group: 'main',
|
||||||
title: 'An error occurred',
|
title: 'An error occurred',
|
||||||
text: err.response.data.description,
|
text: err.data.description,
|
||||||
type: 'error',
|
type: 'error',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$nuxt.$loading.finish()
|
stopLoading()
|
||||||
},
|
},
|
||||||
async removeTeamMember(index) {
|
async removeTeamMember(index) {
|
||||||
this.$nuxt.$loading.start()
|
startLoading()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.$axios.delete(
|
await useBaseFetch(
|
||||||
`team/${this.project.team}/members/${this.allTeamMembers[index].user.id}`,
|
`team/${this.project.team}/members/${this.allTeamMembers[index].user.id}`,
|
||||||
this.$defaultHeaders()
|
{
|
||||||
|
method: 'DELETE',
|
||||||
|
...this.$defaultHeaders(),
|
||||||
|
}
|
||||||
)
|
)
|
||||||
await this.updateMembers()
|
await this.updateMembers()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.$notify({
|
this.$notify({
|
||||||
group: 'main',
|
group: 'main',
|
||||||
title: 'An error occurred',
|
title: 'An error occurred',
|
||||||
text: err.response.data.description,
|
text: err.data.description,
|
||||||
type: 'error',
|
type: 'error',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$nuxt.$loading.finish()
|
stopLoading()
|
||||||
},
|
},
|
||||||
async updateTeamMember(index) {
|
async updateTeamMember(index) {
|
||||||
this.$nuxt.$loading.start()
|
startLoading()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data =
|
const data =
|
||||||
@@ -379,59 +347,59 @@ export default {
|
|||||||
payouts_split: this.allTeamMembers[index].payouts_split,
|
payouts_split: this.allTeamMembers[index].payouts_split,
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.$axios.patch(
|
await useBaseFetch(
|
||||||
`team/${this.project.team}/members/${this.allTeamMembers[index].user.id}`,
|
`team/${this.project.team}/members/${this.allTeamMembers[index].user.id}`,
|
||||||
data,
|
{
|
||||||
this.$defaultHeaders()
|
method: 'PATCH',
|
||||||
|
body: data,
|
||||||
|
...this.$defaultHeaders(),
|
||||||
|
}
|
||||||
)
|
)
|
||||||
await this.updateMembers()
|
await this.updateMembers()
|
||||||
this.$notify({
|
this.$notify({
|
||||||
group: 'main',
|
group: 'main',
|
||||||
title: 'Member(s) updated',
|
title: 'Member(s) updated',
|
||||||
text: `Your project's member(s) has been updated.`,
|
text: "Your project's member(s) has been updated.",
|
||||||
type: 'success',
|
type: 'success',
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.$notify({
|
this.$notify({
|
||||||
group: 'main',
|
group: 'main',
|
||||||
title: 'An error occurred',
|
title: 'An error occurred',
|
||||||
text: err.response.data.description,
|
text: err.data.description,
|
||||||
type: 'error',
|
type: 'error',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$nuxt.$loading.finish()
|
stopLoading()
|
||||||
},
|
},
|
||||||
async transferOwnership(index) {
|
async transferOwnership(index) {
|
||||||
this.$nuxt.$loading.start()
|
startLoading()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.$axios.patch(
|
await useBaseFetch(`team/${this.project.team}/owner`, {
|
||||||
`team/${this.project.team}/owner`,
|
method: 'PATCH',
|
||||||
{
|
body: {
|
||||||
user_id: this.allTeamMembers[index].user.id,
|
user_id: this.allTeamMembers[index].user.id,
|
||||||
},
|
},
|
||||||
this.$defaultHeaders()
|
...this.$defaultHeaders(),
|
||||||
)
|
})
|
||||||
await this.updateMembers()
|
await this.updateMembers()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.$notify({
|
this.$notify({
|
||||||
group: 'main',
|
group: 'main',
|
||||||
title: 'An error occurred',
|
title: 'An error occurred',
|
||||||
text: err.response.data.description,
|
text: err.data.description,
|
||||||
type: 'error',
|
type: 'error',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$nuxt.$loading.finish()
|
stopLoading()
|
||||||
},
|
},
|
||||||
async updateMembers() {
|
async updateMembers() {
|
||||||
this.allTeamMembers = (
|
this.allTeamMembers = (
|
||||||
await this.$axios.get(
|
await useBaseFetch(`team/${this.project.team}/members`, this.$defaultHeaders())
|
||||||
`team/${this.project.team}/members`,
|
).map((it) => ({
|
||||||
this.$defaultHeaders()
|
|
||||||
)
|
|
||||||
).data.map((it) => ({
|
|
||||||
avatar_url: it.user.avatar_url,
|
avatar_url: it.user.avatar_url,
|
||||||
name: it.user.username,
|
name: it.user.username,
|
||||||
oldRole: it.role,
|
oldRole: it.role,
|
||||||
@@ -439,7 +407,7 @@ export default {
|
|||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@@ -8,15 +8,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
Accurate tagging is important to help people find your
|
Accurate tagging is important to help people find your
|
||||||
{{ $formatProjectType(project.project_type).toLowerCase() }}. Make sure
|
{{ $formatProjectType(project.project_type).toLowerCase() }}. Make sure to select all tags
|
||||||
to select all tags that apply.
|
that apply.
|
||||||
</p>
|
</p>
|
||||||
<template v-for="header in Object.keys(categoryLists)">
|
<template v-for="header in Object.keys(categoryLists)" :key="`categories-${header}`">
|
||||||
<div :key="`categories-${header}`" class="label">
|
<div class="label">
|
||||||
<h4>
|
<h4>
|
||||||
<span class="label__title">{{
|
<span class="label__title">{{ $formatCategoryHeader(header) }}</span>
|
||||||
$formatCategoryHeader(header)
|
|
||||||
}}</span>
|
|
||||||
</h4>
|
</h4>
|
||||||
<span class="label__description">
|
<span class="label__description">
|
||||||
<template v-if="header === 'categories'">
|
<template v-if="header === 'categories'">
|
||||||
@@ -25,8 +23,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<template v-else-if="header === 'features'">
|
<template v-else-if="header === 'features'">
|
||||||
Select all of the features that your
|
Select all of the features that your
|
||||||
{{ $formatProjectType(project.project_type).toLowerCase() }} makes
|
{{ $formatProjectType(project.project_type).toLowerCase() }} makes use of.
|
||||||
use of.
|
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="header === 'resolutions'">
|
<template v-else-if="header === 'resolutions'">
|
||||||
Select the resolution(s) of textures in your
|
Select the resolution(s) of textures in your
|
||||||
@@ -34,21 +31,20 @@
|
|||||||
</template>
|
</template>
|
||||||
<template v-else-if="header === 'performance impact'">
|
<template v-else-if="header === 'performance impact'">
|
||||||
Select the realistic performance impact of your
|
Select the realistic performance impact of your
|
||||||
{{ $formatProjectType(project.project_type).toLowerCase() }}.
|
{{ $formatProjectType(project.project_type).toLowerCase() }}. Select multiple if the
|
||||||
Select multiple if the
|
{{ $formatProjectType(project.project_type).toLowerCase() }} is configurable to
|
||||||
{{ $formatProjectType(project.project_type).toLowerCase() }} is
|
different levels of performance impact.
|
||||||
configurable to different levels of performance impact.
|
|
||||||
</template>
|
</template>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div :key="`categories-${header}-list`" class="category-list input-div">
|
<div class="category-list input-div">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
v-for="category in categoryLists[header]"
|
v-for="category in categoryLists[header]"
|
||||||
:key="`category-${header}-${category.name}`"
|
:key="`category-${header}-${category.name}`"
|
||||||
:value="selectedTags.includes(category)"
|
:model-value="selectedTags.includes(category)"
|
||||||
:description="$formatCategory(category.name)"
|
:description="$formatCategory(category.name)"
|
||||||
class="category-selector"
|
class="category-selector"
|
||||||
@input="toggleCategory(category)"
|
@update:model-value="toggleCategory(category)"
|
||||||
>
|
>
|
||||||
<div class="category-selector__label">
|
<div class="category-selector__label">
|
||||||
<div
|
<div
|
||||||
@@ -56,10 +52,8 @@
|
|||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
class="icon"
|
class="icon"
|
||||||
v-html="category.icon"
|
v-html="category.icon"
|
||||||
></div>
|
/>
|
||||||
<span aria-hidden="true">
|
<span aria-hidden="true"> {{ $formatCategory(category.name) }}</span>
|
||||||
{{ $formatCategory(category.name) }}</span
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
</div>
|
</div>
|
||||||
@@ -69,8 +63,8 @@
|
|||||||
<span class="label__title"><StarIcon /> Featured tags</span>
|
<span class="label__title"><StarIcon /> Featured tags</span>
|
||||||
</h4>
|
</h4>
|
||||||
<span class="label__description">
|
<span class="label__description">
|
||||||
You can feature up to 3 of your most relevant tags. Other tags may be
|
You can feature up to 3 of your most relevant tags. Other tags may be promoted to featured
|
||||||
promoted to featured if you do not select all 3.
|
if you do not select all 3.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="selectedTags.length < 1">
|
<p v-if="selectedTags.length < 1">
|
||||||
@@ -81,12 +75,10 @@
|
|||||||
v-for="category in selectedTags"
|
v-for="category in selectedTags"
|
||||||
:key="`featured-category-${category.name}`"
|
:key="`featured-category-${category.name}`"
|
||||||
class="category-selector"
|
class="category-selector"
|
||||||
:value="featuredTags.includes(category)"
|
:model-value="featuredTags.includes(category)"
|
||||||
:description="$formatCategory(category.name)"
|
:description="$formatCategory(category.name)"
|
||||||
:disabled="
|
:disabled="featuredTags.length >= 3 && !featuredTags.includes(category)"
|
||||||
featuredTags.length >= 3 && !featuredTags.includes(category)
|
@update:model-value="toggleFeaturedCategory(category)"
|
||||||
"
|
|
||||||
@input="toggleFeaturedCategory(category)"
|
|
||||||
>
|
>
|
||||||
<div class="category-selector__label">
|
<div class="category-selector__label">
|
||||||
<div
|
<div
|
||||||
@@ -94,10 +86,8 @@
|
|||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
class="icon"
|
class="icon"
|
||||||
v-html="category.icon"
|
v-html="category.icon"
|
||||||
></div>
|
/>
|
||||||
<span aria-hidden="true">
|
<span aria-hidden="true"> {{ $formatCategory(category.name) }}</span>
|
||||||
{{ $formatCategory(category.name) }}</span
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
</div>
|
</div>
|
||||||
@@ -118,10 +108,10 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Checkbox from '~/components/ui/Checkbox'
|
import Checkbox from '~/components/ui/Checkbox'
|
||||||
import StarIcon from '~/assets/images/utils/star.svg?inline'
|
import StarIcon from '~/assets/images/utils/star.svg'
|
||||||
import SaveIcon from '~/assets/images/utils/save.svg?inline'
|
import SaveIcon from '~/assets/images/utils/save.svg'
|
||||||
|
|
||||||
export default {
|
export default defineNuxtComponent({
|
||||||
components: {
|
components: {
|
||||||
Checkbox,
|
Checkbox,
|
||||||
SaveIcon,
|
SaveIcon,
|
||||||
@@ -162,23 +152,19 @@ export default {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
selectedTags: [],
|
selectedTags: this.$sortedCategories.filter(
|
||||||
featuredTags: [],
|
(x) =>
|
||||||
|
x.project_type === this.project.actualProjectType &&
|
||||||
|
(this.project.categories.includes(x.name) ||
|
||||||
|
this.project.additional_categories.includes(x.name))
|
||||||
|
),
|
||||||
|
featuredTags: this.$sortedCategories.filter(
|
||||||
|
(x) =>
|
||||||
|
x.project_type === this.project.actualProjectType &&
|
||||||
|
this.project.categories.includes(x.name)
|
||||||
|
),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fetch() {
|
|
||||||
this.selectedTags = this.$sortedCategories.filter(
|
|
||||||
(x) =>
|
|
||||||
x.project_type === this.project.actualProjectType &&
|
|
||||||
(this.project.categories.includes(x.name) ||
|
|
||||||
this.project.additional_categories.includes(x.name))
|
|
||||||
)
|
|
||||||
this.featuredTags = this.$sortedCategories.filter(
|
|
||||||
(x) =>
|
|
||||||
x.project_type === this.project.actualProjectType &&
|
|
||||||
this.project.categories.includes(x.name)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
categoryLists() {
|
categoryLists() {
|
||||||
const lists = {}
|
const lists = {}
|
||||||
@@ -197,19 +183,11 @@ export default {
|
|||||||
const data = {}
|
const data = {}
|
||||||
// Promote selected categories to featured if there are less than 3 featured
|
// Promote selected categories to featured if there are less than 3 featured
|
||||||
const newFeaturedTags = this.featuredTags.slice()
|
const newFeaturedTags = this.featuredTags.slice()
|
||||||
if (
|
if (newFeaturedTags.length < 1 && this.selectedTags.length > newFeaturedTags.length) {
|
||||||
newFeaturedTags.length < 1 &&
|
const nonFeaturedCategories = this.selectedTags.filter((x) => !newFeaturedTags.includes(x))
|
||||||
this.selectedTags.length > newFeaturedTags.length
|
|
||||||
) {
|
|
||||||
const nonFeaturedCategories = this.selectedTags.filter(
|
|
||||||
(x) => !newFeaturedTags.includes(x)
|
|
||||||
)
|
|
||||||
|
|
||||||
nonFeaturedCategories
|
nonFeaturedCategories
|
||||||
.slice(
|
.slice(0, Math.min(nonFeaturedCategories.length, 3 - newFeaturedTags.length))
|
||||||
0,
|
|
||||||
Math.min(nonFeaturedCategories.length, 3 - newFeaturedTags.length)
|
|
||||||
)
|
|
||||||
.forEach((x) => newFeaturedTags.push(x))
|
.forEach((x) => newFeaturedTags.push(x))
|
||||||
}
|
}
|
||||||
// Convert selected and featured categories to backend-usable arrays
|
// Convert selected and featured categories to backend-usable arrays
|
||||||
@@ -226,11 +204,8 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
additionalCategories.length !==
|
additionalCategories.length !== this.project.additional_categories.length ||
|
||||||
this.project.additional_categories.length ||
|
additionalCategories.some((value) => !this.project.additional_categories.includes(value))
|
||||||
additionalCategories.some(
|
|
||||||
(value) => !this.project.additional_categories.includes(value)
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
data.additional_categories = additionalCategories
|
data.additional_categories = additionalCategories
|
||||||
}
|
}
|
||||||
@@ -265,7 +240,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
})
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.label__title {
|
.label__title {
|
||||||
@@ -281,7 +256,7 @@ export default {
|
|||||||
column-gap: var(--spacing-card-lg);
|
column-gap: var(--spacing-card-lg);
|
||||||
margin-bottom: var(--spacing-card-md);
|
margin-bottom: var(--spacing-card-md);
|
||||||
|
|
||||||
.category-selector ::v-deep {
|
:deep(.category-selector) {
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
.category-selector__label {
|
.category-selector__label {
|
||||||
display: flex;
|
display: flex;
|
||||||
File diff suppressed because it is too large
Load Diff
8
pages/[type]/[id]/version/[version]/edit.vue
Normal file
8
pages/[type]/[id]/version/[version]/edit.vue
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<template>
|
||||||
|
<div />
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
definePageMeta({
|
||||||
|
middleware: 'auth',
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -1,5 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
|
<Head>
|
||||||
|
<Title> {{ project.title }} - Versions </Title>
|
||||||
|
<Meta name="og:title" :content="`${project.title} - Versions`" />
|
||||||
|
<Meta name="description" :content="metaDescription" />
|
||||||
|
<Meta name="apple-mobile-web-app-title" :content="`${project.title} - Versions`" />
|
||||||
|
<Meta name="og:description" :content="metaDescription" />
|
||||||
|
</Head>
|
||||||
<div v-if="currentMember" class="card header-buttons">
|
<div v-if="currentMember" class="card header-buttons">
|
||||||
<FileInput
|
<FileInput
|
||||||
:max-size="524288000"
|
:max-size="524288000"
|
||||||
@@ -13,25 +20,33 @@
|
|||||||
<span class="indicator">
|
<span class="indicator">
|
||||||
<InfoIcon /> Click to choose a file or drag one onto this page
|
<InfoIcon /> Click to choose a file or drag one onto this page
|
||||||
</span>
|
</span>
|
||||||
<DropArea
|
<DropArea :accept="acceptFileFromProjectType(project.project_type)" @change="handleFiles" />
|
||||||
:accept="acceptFileFromProjectType(project.project_type)"
|
|
||||||
@change="handleFiles"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<VersionFilterControl
|
<VersionFilterControl
|
||||||
class="card"
|
:versions="props.versions"
|
||||||
:versions="versions"
|
@update-versions="
|
||||||
@updateVersions="updateVersions"
|
(v) => {
|
||||||
|
filteredVersions = v
|
||||||
|
switchPage(1)
|
||||||
|
}
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
<div v-if="versions.length > 0" class="universal-card all-versions">
|
<Pagination
|
||||||
|
:page="currentPage"
|
||||||
|
:count="Math.ceil(filteredVersions.length / 20)"
|
||||||
|
class="pagination-before"
|
||||||
|
:link-function="(page) => `?page=${page}`"
|
||||||
|
@switch-page="switchPage"
|
||||||
|
/>
|
||||||
|
<div v-if="filteredVersions.length > 0" class="universal-card all-versions">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<div></div>
|
<div />
|
||||||
<div>Version</div>
|
<div>Version</div>
|
||||||
<div>Supports</div>
|
<div>Supports</div>
|
||||||
<div>Stats</div>
|
<div>Stats</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-for="version in filteredVersions"
|
v-for="version in filteredVersions.slice((currentPage - 1) * 20, currentPage * 20)"
|
||||||
:key="version.id"
|
:key="version.id"
|
||||||
class="version-button button-transparent"
|
class="version-button button-transparent"
|
||||||
@click="
|
@click="
|
||||||
@@ -44,15 +59,12 @@
|
|||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
v-tooltip="
|
v-tooltip="
|
||||||
$parent.findPrimary(version).filename +
|
version.primaryFile.filename + ' (' + $formatBytes(version.primaryFile.size) + ')'
|
||||||
' (' +
|
|
||||||
$formatBytes($parent.findPrimary(version).size) +
|
|
||||||
')'
|
|
||||||
"
|
"
|
||||||
:href="$parent.findPrimary(version).url"
|
:href="version.primaryFile.url"
|
||||||
class="download-button square-button brand-button"
|
class="download-button square-button brand-button"
|
||||||
:class="version.version_type"
|
:class="version.version_type"
|
||||||
:title="`Download ${version.name}`"
|
:aria-label="`Download ${version.name}`"
|
||||||
@click.stop="(event) => event.stopPropagation()"
|
@click.stop="(event) => event.stopPropagation()"
|
||||||
>
|
>
|
||||||
<DownloadIcon aria-hidden="true" />
|
<DownloadIcon aria-hidden="true" />
|
||||||
@@ -64,24 +76,11 @@
|
|||||||
class="version__title"
|
class="version__title"
|
||||||
>
|
>
|
||||||
{{ version.name }}
|
{{ version.name }}
|
||||||
<FeaturedIcon v-if="featuredVersionIds.includes(version.id)" />
|
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
<div class="version__metadata">
|
<div class="version__metadata">
|
||||||
<VersionBadge
|
<VersionBadge v-if="version.version_type === 'release'" type="release" color="green" />
|
||||||
v-if="version.version_type === 'release'"
|
<VersionBadge v-else-if="version.version_type === 'beta'" type="beta" color="orange" />
|
||||||
type="release"
|
<VersionBadge v-else-if="version.version_type === 'alpha'" type="alpha" color="red" />
|
||||||
color="green"
|
|
||||||
/>
|
|
||||||
<VersionBadge
|
|
||||||
v-else-if="version.version_type === 'beta'"
|
|
||||||
type="beta"
|
|
||||||
color="orange"
|
|
||||||
/>
|
|
||||||
<VersionBadge
|
|
||||||
v-else-if="version.version_type === 'alpha'"
|
|
||||||
type="alpha"
|
|
||||||
color="red"
|
|
||||||
/>
|
|
||||||
<span class="divider" />
|
<span class="divider" />
|
||||||
<span class="version_number">{{ version.version_number }}</span>
|
<span class="version_number">{{ version.version_number }}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -98,130 +97,99 @@
|
|||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
Published on
|
Published on
|
||||||
<strong>{{
|
<strong>{{ $dayjs(version.date_published).format('MMM D, YYYY') }}</strong>
|
||||||
$dayjs(version.date_published).format('MMM D, YYYY')
|
|
||||||
}}</strong>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Pagination
|
||||||
|
:page="currentPage"
|
||||||
|
:count="Math.ceil(filteredVersions.length / 20)"
|
||||||
|
class="pagination-before"
|
||||||
|
:link-function="(page) => `?page=${page}`"
|
||||||
|
@switch-page="switchPage"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script setup>
|
||||||
import { acceptFileFromProjectType } from '~/plugins/fileUtils'
|
import { acceptFileFromProjectType } from '~/helpers/fileUtils'
|
||||||
import DownloadIcon from '~/assets/images/utils/download.svg?inline'
|
import DownloadIcon from '~/assets/images/utils/download.svg'
|
||||||
import UploadIcon from '~/assets/images/utils/upload.svg?inline'
|
import UploadIcon from '~/assets/images/utils/upload.svg'
|
||||||
import InfoIcon from '~/assets/images/utils/info.svg?inline'
|
import InfoIcon from '~/assets/images/utils/info.svg'
|
||||||
import FeaturedIcon from '~/assets/images/utils/star.svg?inline'
|
|
||||||
import VersionBadge from '~/components/ui/Badge'
|
import VersionBadge from '~/components/ui/Badge'
|
||||||
import FileInput from '~/components/ui/FileInput'
|
import FileInput from '~/components/ui/FileInput'
|
||||||
|
import DropArea from '~/components/ui/DropArea'
|
||||||
|
import Pagination from '~/components/ui/Pagination'
|
||||||
import VersionFilterControl from '~/components/ui/VersionFilterControl'
|
import VersionFilterControl from '~/components/ui/VersionFilterControl'
|
||||||
import DropArea from '~/components/ui/DropArea.vue'
|
|
||||||
|
|
||||||
export default {
|
const props = defineProps({
|
||||||
components: {
|
project: {
|
||||||
DropArea,
|
type: Object,
|
||||||
DownloadIcon,
|
default() {
|
||||||
UploadIcon,
|
return {}
|
||||||
InfoIcon,
|
|
||||||
FeaturedIcon,
|
|
||||||
VersionBadge,
|
|
||||||
VersionFilterControl,
|
|
||||||
FileInput,
|
|
||||||
},
|
|
||||||
auth: false,
|
|
||||||
props: {
|
|
||||||
project: {
|
|
||||||
type: Object,
|
|
||||||
default() {
|
|
||||||
return {}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
versions: {
|
|
||||||
type: Array,
|
|
||||||
default() {
|
|
||||||
return []
|
|
||||||
},
|
|
||||||
},
|
|
||||||
featuredVersions: {
|
|
||||||
type: Array,
|
|
||||||
default() {
|
|
||||||
return []
|
|
||||||
},
|
|
||||||
},
|
|
||||||
currentMember: {
|
|
||||||
type: Object,
|
|
||||||
default() {
|
|
||||||
return null
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
data() {
|
versions: {
|
||||||
return {
|
type: Array,
|
||||||
filteredVersions: this.versions,
|
default() {
|
||||||
}
|
return []
|
||||||
|
},
|
||||||
},
|
},
|
||||||
fetch() {
|
members: {
|
||||||
if (this.$route.query.page)
|
type: Array,
|
||||||
this.currentPage = parseInt(this.$route.query.page)
|
default() {
|
||||||
|
return []
|
||||||
|
},
|
||||||
},
|
},
|
||||||
head() {
|
currentMember: {
|
||||||
const title = `${this.project.title} - Versions`
|
type: Object,
|
||||||
const description = `Download and browse ${this.versions.length} ${
|
default() {
|
||||||
this.project.title
|
return {}
|
||||||
} 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 {
|
const data = useNuxtApp()
|
||||||
title,
|
const metaDescription = computed(
|
||||||
meta: [
|
() =>
|
||||||
{
|
`Download and browse ${props.versions.length} ${
|
||||||
hid: 'og:title',
|
props.project.title
|
||||||
name: 'og:title',
|
} versions. ${data.$formatNumber(props.project.downloads)} total downloads. Last updated ${data
|
||||||
content: title,
|
.$dayjs(props.project.updated)
|
||||||
},
|
.format('MMM D, YYYY')}.`
|
||||||
{
|
)
|
||||||
hid: 'apple-mobile-web-app-title',
|
|
||||||
name: 'apple-mobile-web-app-title',
|
const route = useRoute()
|
||||||
content: title,
|
const currentPage = ref(Number(route.query.p ?? 1))
|
||||||
},
|
const filteredVersions = shallowRef(props.versions)
|
||||||
{
|
|
||||||
hid: 'og:description',
|
async function switchPage(page) {
|
||||||
name: 'og:description',
|
currentPage.value = page
|
||||||
content: description,
|
|
||||||
},
|
const router = useRouter()
|
||||||
{
|
const route = useRoute()
|
||||||
hid: 'description',
|
|
||||||
name: 'description',
|
await router.replace({
|
||||||
content: description,
|
query: {
|
||||||
},
|
...route.query,
|
||||||
],
|
p: currentPage.value !== 1 ? currentPage.value : undefined,
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
featuredVersionIds() {
|
|
||||||
return this.featuredVersions.map((x) => x.id)
|
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
methods: {
|
}
|
||||||
acceptFileFromProjectType,
|
|
||||||
updateVersions(updatedVersions) {
|
async function handleFiles(files) {
|
||||||
this.filteredVersions = updatedVersions
|
const router = useRouter()
|
||||||
|
await router.push({
|
||||||
|
name: 'type-id-version-version',
|
||||||
|
params: {
|
||||||
|
type: props.project.project_type,
|
||||||
|
id: props.project.slug ? props.project.slug : props.project.id,
|
||||||
|
version: 'create',
|
||||||
},
|
},
|
||||||
async handleFiles(files) {
|
state: {
|
||||||
await this.$router.push({
|
newPrimaryFile: files[0],
|
||||||
name: 'type-id-version-create',
|
|
||||||
params: {
|
|
||||||
type: this.project.project_type,
|
|
||||||
id: this.project.slug ? this.project.slug : this.project.id,
|
|
||||||
newPrimaryFile: files[0],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -246,7 +214,7 @@ export default {
|
|||||||
.header {
|
.header {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template: 'download title supports stats';
|
grid-template: 'download title supports stats';
|
||||||
grid-template-columns: calc(2.25rem + var(--spacing-card-sm)) 1fr 1fr 1fr;
|
grid-template-columns: calc(2.25rem + var(--spacing-card-sm)) 1.25fr 1fr 1fr;
|
||||||
color: var(--color-text-dark);
|
color: var(--color-text-dark);
|
||||||
font-size: var(--font-size-md);
|
font-size: var(--font-size-md);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@@ -278,7 +246,7 @@ export default {
|
|||||||
'download title supports stats'
|
'download title supports stats'
|
||||||
'download metadata supports stats'
|
'download metadata supports stats'
|
||||||
'download dummy supports stats';
|
'download dummy supports stats';
|
||||||
grid-template-columns: calc(2.25rem + var(--spacing-card-sm)) 1fr 1fr 1fr;
|
grid-template-columns: calc(2.25rem + var(--spacing-card-sm)) 1.25fr 1fr 1fr;
|
||||||
column-gap: var(--spacing-card-sm);
|
column-gap: var(--spacing-card-sm);
|
||||||
justify-content: left;
|
justify-content: left;
|
||||||
padding: var(--spacing-card-md);
|
padding: var(--spacing-card-md);
|
||||||
@@ -354,13 +322,17 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-create {
|
.search-controls {
|
||||||
padding: var(--spacing-card-bg);
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
.input-group {
|
gap: var(--spacing-card-md);
|
||||||
width: fit-content;
|
align-items: center;
|
||||||
margin-left: auto;
|
flex-wrap: wrap;
|
||||||
margin-top: 1.5rem;
|
.multiselect {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.checkbox-outer {
|
||||||
|
min-width: fit-content;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,266 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="content">
|
|
||||||
<VersionFilterControl
|
|
||||||
class="card"
|
|
||||||
:versions="versions"
|
|
||||||
@updateVersions="updateVersions"
|
|
||||||
/>
|
|
||||||
<div class="card">
|
|
||||||
<div
|
|
||||||
v-for="version in filteredVersions"
|
|
||||||
:key="version.id"
|
|
||||||
class="changelog-item"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
: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>
|
|
||||||
</template>
|
|
||||||
<script>
|
|
||||||
import DownloadIcon from '~/assets/images/utils/download.svg?inline'
|
|
||||||
import VersionFilterControl from '~/components/ui/VersionFilterControl'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
VersionFilterControl,
|
|
||||||
DownloadIcon,
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
project: {
|
|
||||||
type: Object,
|
|
||||||
default() {
|
|
||||||
return {}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
versions: {
|
|
||||||
type: Array,
|
|
||||||
default() {
|
|
||||||
return []
|
|
||||||
},
|
|
||||||
},
|
|
||||||
members: {
|
|
||||||
type: Array,
|
|
||||||
default() {
|
|
||||||
return []
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
filteredVersions: this.$calculateDuplicates(this.versions),
|
|
||||||
currentPage: 1,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
fetch() {
|
|
||||||
if (this.$route.query.page) {
|
|
||||||
this.currentPage = parseInt(this.$route.query.page)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
async switchPage(page, toTop) {
|
|
||||||
this.currentPage = page
|
|
||||||
await 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 = this.$calculateDuplicates(updatedVersions)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
auth: false,
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.changelog-item {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
position: relative;
|
|
||||||
padding-left: 1.8rem;
|
|
||||||
|
|
||||||
.changelog-bar {
|
|
||||||
--color: var(--color-special-green);
|
|
||||||
|
|
||||||
&.alpha {
|
|
||||||
--color: var(--color-special-red);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.release {
|
|
||||||
--color: var(--color-special-green);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.beta {
|
|
||||||
--color: var(--color-special-orange);
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
min-height: 0.75rem;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: inline-block;
|
|
||||||
margin-right: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.version-header-text {
|
|
||||||
display: flex;
|
|
||||||
align-items: baseline;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: var(--font-size-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
h2,
|
|
||||||
span {
|
|
||||||
padding-right: 0.25rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.download {
|
|
||||||
display: none;
|
|
||||||
|
|
||||||
@media screen and (min-width: 800px) {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-body {
|
|
||||||
margin: 0.5rem 0.5rem 0 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div
|
|
||||||
v-highlightjs
|
|
||||||
class="markdown-body card"
|
|
||||||
v-html="$xss($md.render(project.body))"
|
|
||||||
></div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
auth: false,
|
|
||||||
props: {
|
|
||||||
project: {
|
|
||||||
type: Object,
|
|
||||||
default() {
|
|
||||||
return {}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped></style>
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div></div>
|
|
||||||
</template>
|
|
||||||
<script>
|
|
||||||
export default {}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped></style>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div></div>
|
|
||||||
</template>
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
auth: false,
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped></style>
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div></div>
|
|
||||||
</template>
|
|
||||||
<script>
|
|
||||||
export default {}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped></style>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div></div>
|
|
||||||
</template>
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
auth: false,
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped></style>
|
|
||||||
@@ -13,49 +13,27 @@
|
|||||||
<!-- <NavStackItem link="/dashboard/analytics" label="Analytics">-->
|
<!-- <NavStackItem link="/dashboard/analytics" label="Analytics">-->
|
||||||
<!-- <ChartIcon />-->
|
<!-- <ChartIcon />-->
|
||||||
<!-- </NavStackItem>-->
|
<!-- </NavStackItem>-->
|
||||||
<NavStackItem
|
<NavStackItem link="/dashboard/revenue" label="Revenue">
|
||||||
v-if="hasMonetization()"
|
|
||||||
link="/dashboard/revenue"
|
|
||||||
label="Revenue"
|
|
||||||
>
|
|
||||||
<CurrencyIcon />
|
<CurrencyIcon />
|
||||||
</NavStackItem>
|
</NavStackItem>
|
||||||
</NavStack>
|
</NavStack>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
<div class="normal-page__content">
|
<div class="normal-page__content">
|
||||||
<NuxtChild />
|
<NuxtPage />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
<script setup>
|
||||||
<script>
|
|
||||||
import NavStack from '~/components/ui/NavStack'
|
import NavStack from '~/components/ui/NavStack'
|
||||||
import NavStackItem from '~/components/ui/NavStackItem'
|
import NavStackItem from '~/components/ui/NavStackItem'
|
||||||
|
|
||||||
import DashboardIcon from '~/assets/images/utils/dashboard.svg?inline'
|
import DashboardIcon from '~/assets/images/utils/dashboard.svg'
|
||||||
// import ChartIcon from '~/assets/images/utils/chart.svg?inline'
|
import CurrencyIcon from '~/assets/images/utils/currency.svg'
|
||||||
import CurrencyIcon from '~/assets/images/utils/currency.svg?inline'
|
import ListIcon from '~/assets/images/utils/list.svg'
|
||||||
import ListIcon from '~/assets/images/utils/list.svg?inline'
|
|
||||||
|
|
||||||
const monetization = true
|
definePageMeta({
|
||||||
|
middleware: 'auth',
|
||||||
export default {
|
})
|
||||||
name: 'Dashboard',
|
|
||||||
components: {
|
|
||||||
NavStack,
|
|
||||||
NavStackItem,
|
|
||||||
DashboardIcon,
|
|
||||||
// ChartIcon,
|
|
||||||
CurrencyIcon,
|
|
||||||
ListIcon,
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
hasMonetization() {
|
|
||||||
return monetization
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped></style>
|
<style lang="scss" scoped></style>
|
||||||
|
|||||||
@@ -3,24 +3,16 @@
|
|||||||
<section class="universal-card">
|
<section class="universal-card">
|
||||||
<h2>Analytics</h2>
|
<h2>Analytics</h2>
|
||||||
<p>You found a secret!</p>
|
<p>You found a secret!</p>
|
||||||
<nuxt-link to="/frog" class="goto-link"
|
<nuxt-link to="/frog" class="goto-link"> Click here for fancy graphs! </nuxt-link>
|
||||||
>Click here for fancy graphs!</nuxt-link
|
|
||||||
>
|
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default defineNuxtComponent({
|
||||||
components: {},
|
|
||||||
data() {
|
|
||||||
return {}
|
|
||||||
},
|
|
||||||
fetch() {},
|
|
||||||
head: {
|
head: {
|
||||||
title: 'Analytics - Modrinth',
|
title: 'Analytics - Modrinth',
|
||||||
},
|
},
|
||||||
methods: {},
|
})
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped></style>
|
<style lang="scss" scoped></style>
|
||||||
|
|||||||
@@ -6,11 +6,7 @@
|
|||||||
<div class="grid-display__item">
|
<div class="grid-display__item">
|
||||||
<div class="label">Total downloads</div>
|
<div class="label">Total downloads</div>
|
||||||
<div class="value">
|
<div class="value">
|
||||||
{{
|
{{ $formatNumber(user.projects.reduce((agg, x) => agg + x.downloads, 0)) }}
|
||||||
$formatNumber(
|
|
||||||
$user.projects.reduce((agg, x) => agg + x.downloads, 0)
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
>from
|
>from
|
||||||
@@ -27,11 +23,7 @@
|
|||||||
<div class="grid-display__item">
|
<div class="grid-display__item">
|
||||||
<div class="label">Total followers</div>
|
<div class="label">Total followers</div>
|
||||||
<div class="value">
|
<div class="value">
|
||||||
{{
|
{{ $formatNumber(user.projects.reduce((agg, x) => agg + x.followers, 0)) }}
|
||||||
$formatNumber(
|
|
||||||
$user.projects.reduce((agg, x) => agg + x.followers, 0)
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</div>
|
</div>
|
||||||
<span>
|
<span>
|
||||||
<span
|
<span
|
||||||
@@ -49,7 +41,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="grid-display__item">
|
<div class="grid-display__item">
|
||||||
<div class="label">Total revenue</div>
|
<div class="label">Total revenue</div>
|
||||||
<div class="value">{{ $formatMoney(payouts.all_time) }}</div>
|
<div class="value">
|
||||||
|
{{ $formatMoney(payouts.all_time) }}
|
||||||
|
</div>
|
||||||
<span>{{ $formatMoney(payouts.last_month) }} this month</span>
|
<span>{{ $formatMoney(payouts.last_month) }} this month</span>
|
||||||
<!-- <NuxtLink class="goto-link" to="/dashboard/analytics"-->
|
<!-- <NuxtLink class="goto-link" to="/dashboard/analytics"-->
|
||||||
<!-- >View breakdown-->
|
<!-- >View breakdown-->
|
||||||
@@ -61,17 +55,16 @@
|
|||||||
<div class="grid-display__item">
|
<div class="grid-display__item">
|
||||||
<div class="label">Current balance</div>
|
<div class="label">Current balance</div>
|
||||||
<div class="value">
|
<div class="value">
|
||||||
{{ $formatMoney($auth.user.payout_data.balance) }}
|
{{ $formatMoney(auth.user.payout_data.balance) }}
|
||||||
</div>
|
</div>
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
v-if="$auth.user.payout_data.balance >= minWithdraw"
|
v-if="auth.user.payout_data.balance >= minWithdraw"
|
||||||
class="goto-link"
|
class="goto-link"
|
||||||
to="/dashboard/revenue"
|
to="/dashboard/revenue"
|
||||||
>Withdraw earnings
|
>
|
||||||
<ChevronRightIcon
|
Withdraw earnings
|
||||||
class="featured-header-chevron"
|
<ChevronRightIcon class="featured-header-chevron" aria-hidden="true" />
|
||||||
aria-hidden="true"
|
</NuxtLink>
|
||||||
/></NuxtLink>
|
|
||||||
<span v-else>${{ minWithdraw }} is the withdraw minimum</span>
|
<span v-else>${{ minWithdraw }} is the withdraw minimum</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -79,55 +72,37 @@
|
|||||||
<section class="universal-card more-soon">
|
<section class="universal-card more-soon">
|
||||||
<h2>More coming soon!</h2>
|
<h2>More coming soon!</h2>
|
||||||
<p>
|
<p>
|
||||||
Stay tuned for more metrics and analytics (pretty graphs, anyone? 👀)
|
Stay tuned for more metrics and analytics (pretty graphs, anyone? 👀) coming to the creators
|
||||||
coming to the creators dashboard soon!
|
dashboard soon!
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import ChevronRightIcon from '~/assets/images/utils/chevron-right.svg'
|
||||||
|
|
||||||
<script>
|
useHead({
|
||||||
import ChevronRightIcon from '~/assets/images/utils/chevron-right.svg?inline'
|
title: 'Creator dashboard - Modrinth',
|
||||||
|
})
|
||||||
|
|
||||||
export default {
|
const auth = await useAuth()
|
||||||
components: { ChevronRightIcon },
|
const app = useNuxtApp()
|
||||||
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
|
const [raw] = await Promise.all([
|
||||||
payouts.last_month = Math.floor(payouts.last_month * 100) / 100
|
useBaseFetch(`user/${auth.value.user.id}/payouts`, app.$defaultHeaders()),
|
||||||
|
])
|
||||||
|
const user = await useUser()
|
||||||
|
|
||||||
return {
|
raw.all_time = Math.floor(raw.all_time * 100) / 100
|
||||||
payouts,
|
raw.last_month = Math.floor(raw.last_month * 100) / 100
|
||||||
}
|
|
||||||
},
|
const payouts = ref(raw)
|
||||||
data() {
|
const minWithdraw = ref(0.26)
|
||||||
return {
|
|
||||||
minWithdraw: 0.26,
|
const downloadsProjectCount = computed(
|
||||||
}
|
() => user.value.projects.filter((project) => project.downloads > 0).length
|
||||||
},
|
)
|
||||||
fetch() {},
|
const followersProjectCount = computed(
|
||||||
head: {
|
() => user.value.projects.filter((project) => project.followers > 0).length
|
||||||
title: 'Creator dashboard - Modrinth',
|
)
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
downloadsProjectCount() {
|
|
||||||
return this.$user.projects.filter((project) => project.downloads > 0)
|
|
||||||
.length
|
|
||||||
},
|
|
||||||
followersProjectCount() {
|
|
||||||
return this.$user.projects.filter((project) => project.followers > 0)
|
|
||||||
.length
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {},
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped></style>
|
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
<Modal ref="editLinksModal" header="Edit links">
|
<Modal ref="editLinksModal" header="Edit links">
|
||||||
<div class="universal-modal links-modal">
|
<div class="universal-modal links-modal">
|
||||||
<p>
|
<p>
|
||||||
Any links you specify below will be overwritten on each of the
|
Any links you specify below will be overwritten on each of the selected projects. Any you
|
||||||
selected projects. Any you leave blank will be ignored. You can clear
|
leave blank will be ignored. You can clear a link from all selected projects using the
|
||||||
a link from all selected projects using the trash can button.
|
trash can button.
|
||||||
</p>
|
</p>
|
||||||
<section class="links">
|
<section class="links">
|
||||||
<label
|
<label
|
||||||
@@ -21,14 +21,13 @@
|
|||||||
:disabled="editLinks.issues.clear"
|
:disabled="editLinks.issues.clear"
|
||||||
type="url"
|
type="url"
|
||||||
:placeholder="
|
:placeholder="
|
||||||
editLinks.issues.clear
|
editLinks.issues.clear ? 'Existing link will be cleared' : 'Enter a valid URL'
|
||||||
? 'Existing link will be cleared'
|
|
||||||
: 'Enter a valid URL'
|
|
||||||
"
|
"
|
||||||
maxlength="2048"
|
maxlength="2048"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
v-tooltip="'Clear link'"
|
v-tooltip="'Clear link'"
|
||||||
|
aria-label="Clear link"
|
||||||
class="square-button label-button"
|
class="square-button label-button"
|
||||||
:data-active="editLinks.issues.clear"
|
:data-active="editLinks.issues.clear"
|
||||||
@click="editLinks.issues.clear = !editLinks.issues.clear"
|
@click="editLinks.issues.clear = !editLinks.issues.clear"
|
||||||
@@ -50,13 +49,12 @@
|
|||||||
type="url"
|
type="url"
|
||||||
maxlength="2048"
|
maxlength="2048"
|
||||||
:placeholder="
|
:placeholder="
|
||||||
editLinks.source.clear
|
editLinks.source.clear ? 'Existing link will be cleared' : 'Enter a valid URL'
|
||||||
? 'Existing link will be cleared'
|
|
||||||
: 'Enter a valid URL'
|
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
v-tooltip="'Clear link'"
|
v-tooltip="'Clear link'"
|
||||||
|
aria-label="Clear link"
|
||||||
class="square-button label-button"
|
class="square-button label-button"
|
||||||
:data-active="editLinks.source.clear"
|
:data-active="editLinks.source.clear"
|
||||||
@click="editLinks.source.clear = !editLinks.source.clear"
|
@click="editLinks.source.clear = !editLinks.source.clear"
|
||||||
@@ -78,13 +76,12 @@
|
|||||||
type="url"
|
type="url"
|
||||||
maxlength="2048"
|
maxlength="2048"
|
||||||
:placeholder="
|
:placeholder="
|
||||||
editLinks.wiki.clear
|
editLinks.wiki.clear ? 'Existing link will be cleared' : 'Enter a valid URL'
|
||||||
? 'Existing link will be cleared'
|
|
||||||
: 'Enter a valid URL'
|
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
v-tooltip="'Clear link'"
|
v-tooltip="'Clear link'"
|
||||||
|
aria-label="Clear link"
|
||||||
class="square-button label-button"
|
class="square-button label-button"
|
||||||
:data-active="editLinks.wiki.clear"
|
:data-active="editLinks.wiki.clear"
|
||||||
@click="editLinks.wiki.clear = !editLinks.wiki.clear"
|
@click="editLinks.wiki.clear = !editLinks.wiki.clear"
|
||||||
@@ -92,10 +89,7 @@
|
|||||||
<TrashIcon />
|
<TrashIcon />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<label
|
<label for="discord-invite-input" title="An invitation link to your Discord server.">
|
||||||
for="discord-invite-input"
|
|
||||||
title="An invitation link to your Discord server."
|
|
||||||
>
|
|
||||||
<span class="label__title">Discord invite</span>
|
<span class="label__title">Discord invite</span>
|
||||||
</label>
|
</label>
|
||||||
<div class="input-group shrink-first">
|
<div class="input-group shrink-first">
|
||||||
@@ -113,6 +107,7 @@
|
|||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
v-tooltip="'Clear link'"
|
v-tooltip="'Clear link'"
|
||||||
|
aria-label="Clear link"
|
||||||
class="square-button label-button"
|
class="square-button label-button"
|
||||||
:data-active="editLinks.discord.clear"
|
:data-active="editLinks.discord.clear"
|
||||||
@click="editLinks.discord.clear = !editLinks.discord.clear"
|
@click="editLinks.discord.clear = !editLinks.discord.clear"
|
||||||
@@ -154,10 +149,7 @@
|
|||||||
<CrossIcon />
|
<CrossIcon />
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button class="iconified-button brand-button" @click="bulkEditLinks()">
|
||||||
class="iconified-button brand-button"
|
|
||||||
@click="bulkEditLinks()"
|
|
||||||
>
|
|
||||||
<SaveIcon />
|
<SaveIcon />
|
||||||
Save changes
|
Save changes
|
||||||
</button>
|
</button>
|
||||||
@@ -169,10 +161,7 @@
|
|||||||
<div class="header__row">
|
<div class="header__row">
|
||||||
<h2 class="header__title">Projects</h2>
|
<h2 class="header__title">Projects</h2>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<button
|
<button class="iconified-button brand-button" @click="$refs.modal_creation.show()">
|
||||||
class="iconified-button brand-button"
|
|
||||||
@click="$refs.modal_creation.show()"
|
|
||||||
>
|
|
||||||
<PlusIcon />
|
<PlusIcon />
|
||||||
Create a project
|
Create a project
|
||||||
</button>
|
</button>
|
||||||
@@ -203,8 +192,8 @@
|
|||||||
:close-on-select="true"
|
:close-on-select="true"
|
||||||
:show-labels="false"
|
:show-labels="false"
|
||||||
:allow-empty="false"
|
:allow-empty="false"
|
||||||
@input="updateSort()"
|
@update:model-value="projects = updateSort(projects, sortBy)"
|
||||||
></Multiselect>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -212,8 +201,8 @@
|
|||||||
<div class="grid-table__row grid-table__header">
|
<div class="grid-table__row grid-table__header">
|
||||||
<div>
|
<div>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
:value="selectedProjects === projects"
|
:model-value="selectedProjects === projects"
|
||||||
@input="
|
@update:model-value="
|
||||||
selectedProjects === projects
|
selectedProjects === projects
|
||||||
? (selectedProjects = [])
|
? (selectedProjects = [])
|
||||||
: (selectedProjects = projects)
|
: (selectedProjects = projects)
|
||||||
@@ -225,33 +214,22 @@
|
|||||||
<div>ID</div>
|
<div>ID</div>
|
||||||
<div>Type</div>
|
<div>Type</div>
|
||||||
<div>Status</div>
|
<div>Status</div>
|
||||||
<div></div>
|
<div />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div v-for="project in projects" :key="`project-${project.id}`" class="grid-table__row">
|
||||||
v-for="project in projects"
|
|
||||||
:key="`project-${project.id}`"
|
|
||||||
class="grid-table__row"
|
|
||||||
>
|
|
||||||
<div>
|
<div>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
:disabled="
|
:disabled="(project.permissions & EDIT_DETAILS) === EDIT_DETAILS"
|
||||||
(project.permissions & EDIT_DETAILS) === EDIT_DETAILS
|
:model-value="selectedProjects.includes(project)"
|
||||||
"
|
@update:model-value="
|
||||||
:value="selectedProjects.includes(project)"
|
|
||||||
@input="
|
|
||||||
selectedProjects.includes(project)
|
selectedProjects.includes(project)
|
||||||
? (selectedProjects = selectedProjects.filter(
|
? (selectedProjects = selectedProjects.filter((it) => it !== project))
|
||||||
(it) => it !== project
|
|
||||||
))
|
|
||||||
: selectedProjects.push(project)
|
: selectedProjects.push(project)
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<nuxt-link
|
<nuxt-link tabindex="-1" :to="`/${project.project_type}/${project.slug}`">
|
||||||
tabindex="-1"
|
|
||||||
:to="`/${project.project_type}/${project.slug}`"
|
|
||||||
>
|
|
||||||
<Avatar
|
<Avatar
|
||||||
:src="project.icon_url"
|
:src="project.icon_url"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
@@ -265,9 +243,6 @@
|
|||||||
<span class="project-title">
|
<span class="project-title">
|
||||||
<IssuesIcon
|
<IssuesIcon
|
||||||
v-if="project.moderator_message"
|
v-if="project.moderator_message"
|
||||||
v-tooltip="
|
|
||||||
'Project has a message from the moderators. View the project to see more.'
|
|
||||||
"
|
|
||||||
aria-label="Project has a message from the moderators. View the project to see more."
|
aria-label="Project has a message from the moderators. View the project to see more."
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -285,15 +260,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{{ $formatProjectType(project.project_type) }}
|
{{ $formatProjectType($getProjectTypeForUrl(project.project_type, project.loaders)) }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Badge
|
<Badge v-if="project.status" :type="project.status" class="status" />
|
||||||
v-if="project.status"
|
|
||||||
:type="project.status"
|
|
||||||
class="status"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -317,20 +288,19 @@ import Multiselect from 'vue-multiselect'
|
|||||||
import Badge from '~/components/ui/Badge.vue'
|
import Badge from '~/components/ui/Badge.vue'
|
||||||
import Checkbox from '~/components/ui/Checkbox.vue'
|
import Checkbox from '~/components/ui/Checkbox.vue'
|
||||||
import Modal from '~/components/ui/Modal.vue'
|
import Modal from '~/components/ui/Modal.vue'
|
||||||
// import ModalConfirm from '~/components/ui/ModalConfirm.vue'
|
|
||||||
import Avatar from '~/components/ui/Avatar.vue'
|
import Avatar from '~/components/ui/Avatar.vue'
|
||||||
import ModalCreation from '~/components/ui/ModalCreation.vue'
|
import ModalCreation from '~/components/ui/ModalCreation.vue'
|
||||||
import CopyCode from '~/components/ui/CopyCode.vue'
|
import CopyCode from '~/components/ui/CopyCode.vue'
|
||||||
|
|
||||||
import SettingsIcon from '~/assets/images/utils/settings.svg?inline'
|
import SettingsIcon from '~/assets/images/utils/settings.svg'
|
||||||
import TrashIcon from '~/assets/images/utils/trash.svg?inline'
|
import TrashIcon from '~/assets/images/utils/trash.svg'
|
||||||
import IssuesIcon from '~/assets/images/utils/issues.svg?inline'
|
import IssuesIcon from '~/assets/images/utils/issues.svg'
|
||||||
import PlusIcon from '~/assets/images/utils/plus.svg?inline'
|
import PlusIcon from '~/assets/images/utils/plus.svg'
|
||||||
import CrossIcon from '~/assets/images/utils/x.svg?inline'
|
import CrossIcon from '~/assets/images/utils/x.svg'
|
||||||
import EditIcon from '~/assets/images/utils/edit.svg?inline'
|
import EditIcon from '~/assets/images/utils/edit.svg'
|
||||||
import SaveIcon from '~/assets/images/utils/save.svg?inline'
|
import SaveIcon from '~/assets/images/utils/save.svg'
|
||||||
|
|
||||||
export default {
|
export default defineNuxtComponent({
|
||||||
components: {
|
components: {
|
||||||
Avatar,
|
Avatar,
|
||||||
Badge,
|
Badge,
|
||||||
@@ -343,14 +313,21 @@ export default {
|
|||||||
EditIcon,
|
EditIcon,
|
||||||
SaveIcon,
|
SaveIcon,
|
||||||
Modal,
|
Modal,
|
||||||
// ModalConfirm,
|
|
||||||
ModalCreation,
|
ModalCreation,
|
||||||
Multiselect,
|
Multiselect,
|
||||||
CopyCode,
|
CopyCode,
|
||||||
},
|
},
|
||||||
|
async setup() {
|
||||||
|
const user = await useUser()
|
||||||
|
if (process.client) {
|
||||||
|
await initUserProjects()
|
||||||
|
}
|
||||||
|
|
||||||
|
return { user: ref(user) }
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
projects: [],
|
projects: this.updateSort(this.user.projects, 'Name'),
|
||||||
versions: [],
|
versions: [],
|
||||||
selectedProjects: [],
|
selectedProjects: [],
|
||||||
sortBy: 'Name',
|
sortBy: 'Name',
|
||||||
@@ -375,10 +352,6 @@ export default {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fetch() {
|
|
||||||
this.projects = this.$user.projects
|
|
||||||
this.updateSort()
|
|
||||||
},
|
|
||||||
head: {
|
head: {
|
||||||
title: 'Projects - Modrinth',
|
title: 'Projects - Modrinth',
|
||||||
},
|
},
|
||||||
@@ -392,12 +365,11 @@ export default {
|
|||||||
this.EDIT_MEMBER = 1 << 6
|
this.EDIT_MEMBER = 1 << 6
|
||||||
this.DELETE_PROJECT = 1 << 7
|
this.DELETE_PROJECT = 1 << 7
|
||||||
},
|
},
|
||||||
mounted() {},
|
|
||||||
methods: {
|
methods: {
|
||||||
updateSort() {
|
updateSort(projects, sort) {
|
||||||
switch (this.sortBy) {
|
switch (sort) {
|
||||||
case 'Name':
|
case 'Name':
|
||||||
this.projects = this.projects.slice().sort((a, b) => {
|
return projects.slice().sort((a, b) => {
|
||||||
if (a.title < b.title) {
|
if (a.title < b.title) {
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
@@ -406,9 +378,8 @@ export default {
|
|||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
})
|
})
|
||||||
break
|
|
||||||
case 'Status':
|
case 'Status':
|
||||||
this.projects = this.projects.slice().sort((a, b) => {
|
return projects.slice().sort((a, b) => {
|
||||||
if (a.status < b.status) {
|
if (a.status < b.status) {
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
@@ -417,9 +388,8 @@ export default {
|
|||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
})
|
})
|
||||||
break
|
|
||||||
case 'Type':
|
case 'Type':
|
||||||
this.projects = this.projects.slice().sort((a, b) => {
|
return projects.slice().sort((a, b) => {
|
||||||
if (a.project_type < b.project_type) {
|
if (a.project_type < b.project_type) {
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
@@ -428,7 +398,6 @@ export default {
|
|||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
})
|
})
|
||||||
break
|
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -437,13 +406,11 @@ export default {
|
|||||||
try {
|
try {
|
||||||
const baseData = {
|
const baseData = {
|
||||||
issues_url:
|
issues_url:
|
||||||
!this.editLinks.issues.clear &&
|
!this.editLinks.issues.clear && this.editLinks.issues.val.trim() !== ''
|
||||||
this.editLinks.issues.val.trim() !== ''
|
|
||||||
? this.editLinks.issues.val
|
? this.editLinks.issues.val
|
||||||
: null,
|
: null,
|
||||||
source_url:
|
source_url:
|
||||||
!this.editLinks.source.clear &&
|
!this.editLinks.source.clear && this.editLinks.source.val.trim() !== ''
|
||||||
this.editLinks.source.val.trim() !== ''
|
|
||||||
? this.editLinks.source.val
|
? this.editLinks.source.val
|
||||||
: null,
|
: null,
|
||||||
wiki_url:
|
wiki_url:
|
||||||
@@ -451,18 +418,18 @@ export default {
|
|||||||
? this.editLinks.wiki.val
|
? this.editLinks.wiki.val
|
||||||
: null,
|
: null,
|
||||||
discord_url:
|
discord_url:
|
||||||
!this.editLinks.discord.clear &&
|
!this.editLinks.discord.clear && this.editLinks.discord.val.trim() !== ''
|
||||||
this.editLinks.discord.val.trim() !== ''
|
|
||||||
? this.editLinks.discord.val
|
? this.editLinks.discord.val
|
||||||
: null,
|
: null,
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.$axios.patch(
|
await useBaseFetch(
|
||||||
`projects?ids=${JSON.stringify(
|
`projects?ids=${JSON.stringify(this.selectedProjects.map((x) => x.id))}`,
|
||||||
this.selectedProjects.map((x) => x.id)
|
{
|
||||||
)}`,
|
method: 'PATCH',
|
||||||
baseData,
|
body: baseData,
|
||||||
this.$defaultHeaders()
|
...this.$defaultHeaders(),
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
this.$refs.editLinksModal.hide()
|
this.$refs.editLinksModal.hide()
|
||||||
@@ -483,7 +450,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
})
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.grid-table {
|
.grid-table {
|
||||||
|
|||||||
@@ -3,47 +3,42 @@
|
|||||||
<ModalTransfer
|
<ModalTransfer
|
||||||
v-if="enrolled"
|
v-if="enrolled"
|
||||||
ref="modal_transfer"
|
ref="modal_transfer"
|
||||||
:wallet="$auth.user.payout_data.payout_wallet"
|
:wallet="auth.user.payout_data.payout_wallet"
|
||||||
:account-type="$auth.user.payout_data.payout_wallet_type"
|
:account-type="auth.user.payout_data.payout_wallet_type"
|
||||||
:account="$auth.user.payout_data.payout_address"
|
:account="auth.user.payout_data.payout_address"
|
||||||
:balance="$auth.user.payout_data.balance"
|
:balance="auth.user.payout_data.balance"
|
||||||
:min-withdraw="minWithdraw"
|
:min-withdraw="minWithdraw"
|
||||||
/>
|
/>
|
||||||
<section class="universal-card">
|
<section class="universal-card">
|
||||||
<h2>Withdraw</h2>
|
<h2>Withdraw</h2>
|
||||||
<div v-if="$auth.user.payout_data.balance >= minWithdraw">
|
<div v-if="auth.user.payout_data.balance >= minWithdraw">
|
||||||
<p>
|
<p>
|
||||||
You have
|
You have
|
||||||
<strong>{{ $formatMoney($auth.user.payout_data.balance) }}</strong>
|
<strong>{{ $formatMoney(auth.user.payout_data.balance) }}</strong>
|
||||||
available to withdraw.
|
available to withdraw.
|
||||||
<span v-if="!enrolled"
|
<span v-if="!enrolled"
|
||||||
>Enroll in the Creator Monetization Program to withdraw your
|
>Enroll in the Creator Monetization Program to withdraw your revenue.</span
|
||||||
revenue.</span
|
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div v-if="enrolled" class="input-group">
|
<div v-if="enrolled" class="input-group">
|
||||||
<button
|
<button class="iconified-button brand-button" @click="$refs.modal_transfer.show()">
|
||||||
class="iconified-button brand-button"
|
|
||||||
@click="$refs.modal_transfer.show()"
|
|
||||||
>
|
|
||||||
<TransferIcon /> Transfer to
|
<TransferIcon /> Transfer to
|
||||||
{{ $formatWallet($auth.user.payout_data.payout_wallet) }}
|
{{ $formatWallet(auth.user.payout_data.payout_wallet) }}
|
||||||
</button>
|
</button>
|
||||||
<NuxtLink class="iconified-button" to="/settings/monetization">
|
<NuxtLink class="iconified-button" to="/settings/monetization">
|
||||||
<SettingsIcon /> Monetization settings
|
<SettingsIcon /> Monetization settings
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p v-else-if="$auth.user.payout_data.balance > 0">
|
<p v-else-if="auth.user.payout_data.balance > 0">
|
||||||
You have made
|
You have made
|
||||||
<strong>{{ $formatMoney($auth.user.payout_data.balance) }}</strong
|
<strong>{{ $formatMoney(auth.user.payout_data.balance) }}</strong
|
||||||
>, however you have not yet met the minimum of ${{ minWithdraw }} to
|
>, however you have not yet met the minimum of ${{ minWithdraw }} to withdraw.
|
||||||
withdraw.
|
|
||||||
</p>
|
</p>
|
||||||
<p v-else>
|
<p v-else>
|
||||||
You have made
|
You have made
|
||||||
<strong>{{ $formatMoney($auth.user.payout_data.balance) }}</strong
|
<strong>{{ $formatMoney(auth.user.payout_data.balance) }}</strong
|
||||||
>, which is under the minimum of ${{ minWithdraw }} to withdraw.
|
>, which is under the minimum of ${{ minWithdraw }} to withdraw.
|
||||||
</p>
|
</p>
|
||||||
<div v-if="!enrolled">
|
<div v-if="!enrolled">
|
||||||
@@ -55,9 +50,9 @@
|
|||||||
<section class="universal-card">
|
<section class="universal-card">
|
||||||
<h2>Processing fees</h2>
|
<h2>Processing fees</h2>
|
||||||
<p>
|
<p>
|
||||||
To avoid paying unnecessary fee deductions, you may want to wait to
|
To avoid paying unnecessary fee deductions, you may want to wait to transfer your money out
|
||||||
transfer your money out after it accumulates for a bit rather than
|
after it accumulates for a bit rather than transferring as soon as you reach the minimum of
|
||||||
transferring as soon as you reach the minimum of ${{ minWithdraw }}.
|
${{ minWithdraw }}.
|
||||||
</p>
|
</p>
|
||||||
<h3>PayPal</h3>
|
<h3>PayPal</h3>
|
||||||
<ul>
|
<ul>
|
||||||
@@ -67,55 +62,57 @@
|
|||||||
fee per transaction.
|
fee per transaction.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
In the rest of the world, PayPal charges a <strong>2%</strong> (up to
|
In the rest of the world, PayPal charges a <strong>2%</strong> (up to $20) fee per
|
||||||
$20) fee per transaction.
|
transaction.
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p>
|
<p>
|
||||||
Modrinth will deduct <strong>2%</strong> for the fee (minimum of $0.25
|
Modrinth will deduct <strong>2%</strong> for the fee (minimum of $0.25 and maximum of $20)
|
||||||
and maximum of $20) from <strong>all transfers</strong> and if the fee
|
from <strong>all transfers</strong> and if the fee PayPal charges is less than the amount we
|
||||||
PayPal charges is less than the amount we deducted, the difference will
|
deducted, the difference will be added back to your Modrinth balance. This happens as
|
||||||
be added back to your Modrinth balance. This happens as Modrinth cannot
|
Modrinth cannot determine if a transaction will be in the United States or international or
|
||||||
determine if a transaction will be in the United States or international
|
not until after the transaction has been made.
|
||||||
or not until after the transaction has been made.
|
|
||||||
</p>
|
</p>
|
||||||
<h3>Venmo (United States only)</h3>
|
<h3>Venmo (United States only)</h3>
|
||||||
<p>
|
<p>
|
||||||
Venmo will charge a $0.25 processing fee per transaction, which will be
|
Venmo will charge a $0.25 processing fee per transaction, which will be deducted from the
|
||||||
deducted from the amount you choose to transfer.
|
amount you choose to transfer.
|
||||||
</p>
|
</p>
|
||||||
<h2>Currency conversions</h2>
|
<h2>Currency conversions</h2>
|
||||||
<p>
|
<p>
|
||||||
All revenue generated by Modrinth is in United States dollars. Any
|
All revenue generated by Modrinth is in United States dollars. Any conversions to your local
|
||||||
conversions to your local currency will happen at withdrawal and is not
|
currency will happen at withdrawal and is not handled by Modrinth. Modrinth cannot guarantee
|
||||||
handled by Modrinth. Modrinth cannot guarantee any exchange rate, so
|
any exchange rate, so only USD is displayed in the creator dashboard.
|
||||||
only USD is displayed in the creator dashboard.
|
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import TransferIcon from '~/assets/images/utils/transfer.svg?inline'
|
import TransferIcon from '~/assets/images/utils/transfer.svg'
|
||||||
import SettingsIcon from '~/assets/images/utils/settings.svg?inline'
|
import SettingsIcon from '~/assets/images/utils/settings.svg'
|
||||||
import ModalTransfer from '~/components/ui/ModalTransfer'
|
import ModalTransfer from '~/components/ui/ModalTransfer'
|
||||||
|
|
||||||
export default {
|
export default defineNuxtComponent({
|
||||||
components: { TransferIcon, SettingsIcon, ModalTransfer },
|
components: { TransferIcon, SettingsIcon, ModalTransfer },
|
||||||
|
async setup() {
|
||||||
|
const auth = await useAuth()
|
||||||
|
|
||||||
|
return { auth }
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
minWithdraw: 0.26,
|
minWithdraw: 0.26,
|
||||||
enrolled:
|
enrolled:
|
||||||
this.$auth.user.payout_data.payout_wallet &&
|
this.auth.user.payout_data.payout_wallet &&
|
||||||
this.$auth.user.payout_data.payout_wallet_type &&
|
this.auth.user.payout_data.payout_wallet_type &&
|
||||||
this.$auth.user.payout_data.payout_address,
|
this.auth.user.payout_data.payout_address,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
head: {
|
head: {
|
||||||
title: 'Revenue - Modrinth',
|
title: 'Revenue - Modrinth',
|
||||||
},
|
},
|
||||||
methods: {},
|
methods: {},
|
||||||
}
|
})
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
strong {
|
strong {
|
||||||
|
|||||||
@@ -9,12 +9,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
auth: false,
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.card {
|
.card {
|
||||||
width: calc(100% - 2 * var(--spacing-card-md));
|
width: calc(100% - 2 * var(--spacing-card-md));
|
||||||
|
|||||||
278
pages/index.vue
278
pages/index.vue
File diff suppressed because one or more lines are too long
@@ -20,7 +20,7 @@
|
|||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
<div class="normal-page__content">
|
<div class="normal-page__content">
|
||||||
<NuxtChild class="universal-card" />
|
<NuxtPage class="universal-card" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -29,13 +29,12 @@
|
|||||||
import NavStack from '~/components/ui/NavStack'
|
import NavStack from '~/components/ui/NavStack'
|
||||||
import NavStackItem from '~/components/ui/NavStackItem'
|
import NavStackItem from '~/components/ui/NavStackItem'
|
||||||
|
|
||||||
import TermsIcon from '~/assets/images/utils/heart-handshake.svg?inline'
|
import TermsIcon from '~/assets/images/utils/heart-handshake.svg'
|
||||||
import PrivacyIcon from '~/assets/images/utils/lock.svg?inline'
|
import PrivacyIcon from '~/assets/images/utils/lock.svg'
|
||||||
import RulesIcon from '~/assets/images/sidebar/admin.svg?inline'
|
import RulesIcon from '~/assets/images/sidebar/admin.svg'
|
||||||
import ShieldIcon from '~/assets/images/utils/shield.svg?inline'
|
import ShieldIcon from '~/assets/images/utils/shield.svg'
|
||||||
|
|
||||||
export default {
|
export default defineNuxtComponent({
|
||||||
name: 'Settings',
|
|
||||||
components: {
|
components: {
|
||||||
NavStack,
|
NavStack,
|
||||||
NavStackItem,
|
NavStackItem,
|
||||||
@@ -44,11 +43,11 @@ export default {
|
|||||||
RulesIcon,
|
RulesIcon,
|
||||||
ShieldIcon,
|
ShieldIcon,
|
||||||
},
|
},
|
||||||
}
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.normal-page__content ::v-deep a {
|
.normal-page__content :deep(a) {
|
||||||
color: var(--color-link);
|
color: var(--color-link);
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
|
|
||||||
|
|||||||
@@ -7,31 +7,27 @@
|
|||||||
<h2>Foreword</h2>
|
<h2>Foreword</h2>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
The following document was created as required by several laws, including
|
The following document was created as required by several laws, including but not limited to:
|
||||||
but not limited to:
|
|
||||||
</p>
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
the California Consumer Privacy Act (CA CCPA), more information about
|
the California Consumer Privacy Act (CA CCPA), more information about which can be found on
|
||||||
which can be found on
|
|
||||||
<a href="https://oag.ca.gov/privacy/ccpa">oag.ca.gov</a>
|
<a href="https://oag.ca.gov/privacy/ccpa">oag.ca.gov</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
the European Union General Data Protection Regulation (EU GDPR), more
|
the European Union General Data Protection Regulation (EU GDPR), more information about
|
||||||
information about which can be found on
|
which can be found on
|
||||||
<a href="https://gdpr.eu/">gdpr.eu</a>
|
<a href="https://gdpr.eu/">gdpr.eu</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<a href="https://modrinth.com">Modrinth</a> is part of Rinth, Inc. ("us",
|
<a href="https://modrinth.com">Modrinth</a> is part of Rinth, Inc. ("us", "we", "our"). This
|
||||||
"we", "our"). This privacy policy explains how we collect data, process
|
privacy policy explains how we collect data, process it, and your rights relative to your
|
||||||
it, and your rights relative to your data.
|
data.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>Rinth, Inc. is the data controller for data collected through Modrinth.</p>
|
||||||
Rinth, Inc. is the data controller for data collected through Modrinth.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2>What data do we collect?</h2>
|
<h2>What data do we collect?</h2>
|
||||||
|
|
||||||
@@ -45,14 +41,12 @@
|
|||||||
<li>Your GitHub ID</li>
|
<li>Your GitHub ID</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p>
|
<p>
|
||||||
This data is used to identify you and display your profile. It will be
|
This data is used to identify you and display your profile. It will be linked to your
|
||||||
linked to your projects.
|
projects.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h3>View data and download data</h3>
|
<h3>View data and download data</h3>
|
||||||
<p>
|
<p>When you view a project page or download a file from Modrinth, we collect:</p>
|
||||||
When you view a project page or download a file from Modrinth, we collect:
|
|
||||||
</p>
|
|
||||||
<ul>
|
<ul>
|
||||||
<li>Your IP address</li>
|
<li>Your IP address</li>
|
||||||
<li>Your user ID (if applicable)</li>
|
<li>Your user ID (if applicable)</li>
|
||||||
@@ -60,10 +54,7 @@
|
|||||||
<li>Your country</li>
|
<li>Your country</li>
|
||||||
<li>Some additional metadata about your connection (HTTP headers)</li>
|
<li>Some additional metadata about your connection (HTTP headers)</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p>
|
<p>This data is used to monitor automated access to our service and deliver statistics.</p>
|
||||||
This data is used to monitor automated access to our service and deliver
|
|
||||||
statistics.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h3>Creator Monetization Program data</h3>
|
<h3>Creator Monetization Program data</h3>
|
||||||
<p>
|
<p>
|
||||||
@@ -77,44 +68,37 @@
|
|||||||
<li>Your PayPal email address (if applicable)</li>
|
<li>Your PayPal email address (if applicable)</li>
|
||||||
<li>Your Venmo username (if applicable)</li>
|
<li>Your Venmo username (if applicable)</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p>
|
<p>This data is used to carry out the CMP. It will be linked to your transactions.</p>
|
||||||
This data is used to carry out the CMP. It will be linked to your
|
|
||||||
transactions.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2>Data retention</h2>
|
<h2>Data retention</h2>
|
||||||
<p>
|
<p>
|
||||||
View data and download data are anonymized 24 months after being recorded.
|
View data and download data are anonymized 24 months after being recorded. All personal
|
||||||
All personal information will be removed from those records during
|
information will be removed from those records during anonymization.<br />
|
||||||
anonymization.<br />
|
Data is retained indefinitely. We do not delete any data unless you request it.
|
||||||
Data is retained indefinitely. We do not delete any data unless you
|
|
||||||
request it.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>Third-party services</h2>
|
<h2>Third-party services</h2>
|
||||||
<p>
|
<p>
|
||||||
We use some third-party services to make Modrinth run. Please refer to
|
We use some third-party services to make Modrinth run. Please refer to each of their privacy
|
||||||
each of their privacy policies for more information:
|
policies for more information:
|
||||||
</p>
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<a href="https://www.cloudflare.com/en-gb/gdpr/introduction/">
|
<a href="https://www.cloudflare.com/en-gb/gdpr/introduction/"> Cloudflare </a>
|
||||||
Cloudflare
|
|
||||||
</a>
|
|
||||||
</li>
|
</li>
|
||||||
<li><a href="https://sentry.io/trust/privacy/">Sentry</a></li>
|
<li><a href="https://sentry.io/trust/privacy/">Sentry</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
<p>
|
<p>
|
||||||
Data that we specifically collect isn't shared with any other third party.
|
Data that we specifically collect isn't shared with any other third party. We do not sell any
|
||||||
We do not sell any data.
|
data.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>Data Governance</h2>
|
<h2>Data Governance</h2>
|
||||||
<p>
|
<p>
|
||||||
Database access is limited to the minimum amount of Rinth, Inc. employees
|
Database access is limited to the minimum amount of Rinth, Inc. employees required to run the
|
||||||
required to run the service.<br />
|
service.<br />
|
||||||
Data is stored in a jurisdiction that is part of the European Economic
|
Data is stored in a jurisdiction that is part of the European Economic Area (EEA), encrypted
|
||||||
Area (EEA), encrypted both in storage and in transit.
|
both in storage and in transit.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>Marketing and advertising</h2>
|
<h2>Marketing and advertising</h2>
|
||||||
@@ -124,107 +108,94 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>Cookies</h2>
|
<h2>Cookies</h2>
|
||||||
|
<p>We use cookies to log you into your account and save your cosmetic preferences.</p>
|
||||||
<p>
|
<p>
|
||||||
We use cookies to log you into your account and save your cosmetic
|
Cookies are text files placed on your computer to collect standard Internet information. For
|
||||||
preferences.
|
more information, please visit
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Cookies are text files placed on your computer to collect standard
|
|
||||||
Internet information. For more information, please visit
|
|
||||||
<a href="https://allaboutcookies.org/">allaboutcookies.org</a>.
|
<a href="https://allaboutcookies.org/">allaboutcookies.org</a>.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
You can set your browser not to accept cookies, and the above website
|
You can set your browser not to accept cookies, and the above website tells you how to remove
|
||||||
tells you how to remove cookies from your browser. However, in a few
|
cookies from your browser. However, in a few cases, some of our website features may not
|
||||||
cases, some of our website features may not function as a result.
|
function as a result.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>
|
<h2>Access, rectification, erasure, restriction, portability, and objection</h2>
|
||||||
Access, rectification, erasure, restriction, portability, and objection
|
|
||||||
</h2>
|
|
||||||
<p>Every user is entitled to the following:</p>
|
<p>Every user is entitled to the following:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<strong>The right to access</strong> – You have the right to request
|
<strong>The right to access</strong> – You have the right to request copies of your personal
|
||||||
copies of your personal data. We may charge you a small fee for this
|
data. We may charge you a small fee for this service.
|
||||||
service.
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong>The right to rectification</strong> – You have the right to
|
<strong>The right to rectification</strong> – You have the right to request that we correct
|
||||||
request that we correct any information you believe is inaccurate. You
|
any information you believe is inaccurate. You also have the right to request us to complete
|
||||||
also have the right to request us to complete the information you
|
the information you believe is incomplete.
|
||||||
believe is incomplete.
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong>The right to erasure</strong> – You have the right to request
|
<strong>The right to erasure</strong> – You have the right to request that we erase your
|
||||||
that we erase your personal data, under certain conditions.
|
personal data, under certain conditions.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong>The right to restrict processing</strong> – You have the right
|
<strong>The right to restrict processing</strong> – You have the right to request that we
|
||||||
to request that we restrict the processing of your personal data, under
|
restrict the processing of your personal data, under certain conditions.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>The right to data portability</strong> – You have the right to request that we
|
||||||
|
transfer the data that we have collected to another organization, or directly to you, under
|
||||||
certain conditions.
|
certain conditions.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong>The right to data portability</strong> – You have the right to
|
<strong>The right to object to processing</strong> – You have the right to object to our
|
||||||
request that we transfer the data that we have collected to another
|
processing of your personal data, under certain conditions.
|
||||||
organization, or directly to you, under certain conditions.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>The right to object to processing</strong> – You have the right
|
|
||||||
to object to our processing of your personal data, under certain
|
|
||||||
conditions.
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p>
|
<p>
|
||||||
If you would like to exercise those rights, contact us at
|
If you would like to exercise those rights, contact us at
|
||||||
<a href="mailto:gdpr@modrinth.com">gdpr@modrinth.com</a>. We may ask you
|
<a href="mailto:gdpr@modrinth.com">gdpr@modrinth.com</a>. We may ask you to verify your
|
||||||
to verify your identity before proceeding and will respond to your request
|
identity before proceeding and will respond to your request within 30 days as required by law,
|
||||||
within 30 days as required by law, or notify you of an extended reply
|
or notify you of an extended reply time.
|
||||||
time.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>Children's Information</h2>
|
<h2>Children's Information</h2>
|
||||||
<p>
|
<p>
|
||||||
Another part of our priority is adding protection for children while using
|
Another part of our priority is adding protection for children while using the Internet. We
|
||||||
the Internet. We encourage parents and guardians to observe, participate
|
encourage parents and guardians to observe, participate in, and/or monitor and guide their
|
||||||
in, and/or monitor and guide their online activity.
|
online activity.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Modrinth does not knowingly collect any Personal Identifiable Information
|
Modrinth does not knowingly collect any Personal Identifiable Information from children under
|
||||||
from children under the age of 13. If you think that your child provided
|
the age of 13. If you think that your child provided this kind of information on our website,
|
||||||
this kind of information on our website, we strongly encourage you to
|
we strongly encourage you to contact us immediately and we will do our best efforts to
|
||||||
contact us immediately and we will do our best efforts to promptly remove
|
promptly remove such information from our records.
|
||||||
such information from our records.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>Online Privacy Policy Only</h2>
|
<h2>Online Privacy Policy Only</h2>
|
||||||
<p>
|
<p>
|
||||||
This Privacy Policy applies only to our online activities and is valid for
|
This Privacy Policy applies only to our online activities and is valid for visitors to our
|
||||||
visitors to our website with regards to the information that they shared
|
website with regards to the information that they shared and/or collect in Modrinth. This
|
||||||
and/or collect in Modrinth. This policy is not applicable to any
|
policy is not applicable to any information collected offline or via channels other than this
|
||||||
information collected offline or via channels other than this website.
|
website.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>Consent</h2>
|
<h2>Consent</h2>
|
||||||
<p>
|
<p>
|
||||||
By using our website, you hereby consent to our Privacy Policy and agree
|
By using our website, you hereby consent to our Privacy Policy and agree to its Terms and
|
||||||
to its Terms and Conditions.
|
Conditions.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>Changes to the Privacy Policy</h2>
|
<h2>Changes to the Privacy Policy</h2>
|
||||||
<p>
|
<p>
|
||||||
We keep this privacy policy under regular review and place any updates on
|
We keep this privacy policy under regular review and place any updates on this web page. If we
|
||||||
this web page. If we do this, we will post the changes on this page and
|
do this, we will post the changes on this page and update the "Last edited" date at the top of
|
||||||
update the "Last edited" date at the top of this page, after which such
|
this page, after which such changes will become effective immediately. We will make an effort
|
||||||
changes will become effective immediately. We will make an effort to keep
|
to keep users updated on any such changes, but because most changes do not affect how we
|
||||||
users updated on any such changes, but because most changes do not affect
|
process existing data, a notice will not be sent for all changes.
|
||||||
how we process existing data, a notice will not be sent for all changes.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>Contact</h2>
|
<h2>Contact</h2>
|
||||||
<p>
|
<p>
|
||||||
If you have any questions about this privacy policy or how we process your
|
If you have any questions about this privacy policy or how we process your data, contact us at
|
||||||
data, contact us at
|
|
||||||
<a href="mailto:gdpr@modrinth.com">gdpr@modrinth.com</a> or write us at:
|
<a href="mailto:gdpr@modrinth.com">gdpr@modrinth.com</a> or write us at:
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
@@ -236,8 +207,8 @@
|
|||||||
|
|
||||||
<h3>How to contact the appropriate authority</h3>
|
<h3>How to contact the appropriate authority</h3>
|
||||||
<p>
|
<p>
|
||||||
Should you wish to fill a complaint or if you feel like we haven't
|
Should you wish to fill a complaint or if you feel like we haven't addressed your concerns or
|
||||||
addressed your concerns or request, you may contact the
|
request, you may contact the
|
||||||
<a href="https://ico.org.uk/">Information Commissioner's Office</a>
|
<a href="https://ico.org.uk/">Information Commissioner's Office</a>
|
||||||
using their online form or by writing at:
|
using their online form or by writing at:
|
||||||
</p>
|
</p>
|
||||||
@@ -251,15 +222,14 @@
|
|||||||
United Kingdom
|
United Kingdom
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
You do not need to be a citizen of the United Kingdom to use this method
|
You do not need to be a citizen of the United Kingdom to use this method of lodging
|
||||||
of lodging complaints.
|
complaints.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default defineNuxtComponent({
|
||||||
auth: false,
|
|
||||||
head: {
|
head: {
|
||||||
title: 'Privacy - Modrinth',
|
title: 'Privacy - Modrinth',
|
||||||
meta: [
|
meta: [
|
||||||
@@ -282,11 +252,11 @@ export default {
|
|||||||
{
|
{
|
||||||
hid: 'og:url',
|
hid: 'og:url',
|
||||||
name: 'og:url',
|
name: 'og:url',
|
||||||
content: `https://modrinth.com/legal/privacy`,
|
content: 'https://modrinth.com/legal/privacy',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
}
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped></style>
|
<style lang="scss" scoped></style>
|
||||||
|
|||||||
@@ -4,165 +4,138 @@
|
|||||||
|
|
||||||
<p>
|
<p>
|
||||||
In order to facilitate Modrinth's
|
In order to facilitate Modrinth's
|
||||||
<nuxt-link to="/legal/terms">Terms and Conditions</nuxt-link>, all Content
|
<nuxt-link to="/legal/terms"> Terms and Conditions </nuxt-link>, all Content must obey the
|
||||||
must obey the following Rules. For more information on what exactly
|
following Rules. For more information on what exactly Content is, please refer to the Content
|
||||||
Content is, please refer to the Content section of the Terms.
|
section of the Terms.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Please note that these are general rules and will not be enforced "to the
|
Please note that these are general rules and will not be enforced "to the letter". We reserve
|
||||||
letter". We reserve the right to modify and/or remove any file, project,
|
the right to modify and/or remove any file, project, or other Content uploaded to our platform
|
||||||
or other Content uploaded to our platform for any reason. We reserve the
|
for any reason. We reserve the right to introduce new rules at any time, which may or may not
|
||||||
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.
|
||||||
retroactively apply to already uploaded Content at the discretion of our
|
|
||||||
moderators.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
If you find any violations of these Rules on our website, it is your
|
If you find any violations of these Rules on our website, it is your responsibility to report
|
||||||
responsibility to report it. You may use the Report button on any project,
|
it. You may use the Report button on any project, version, or user page, or you may email us
|
||||||
version, or user page, or you may email us at
|
at
|
||||||
<a href="mailto:support@modrinth.com">support@modrinth.com</a>.
|
<a href="mailto:support@modrinth.com">support@modrinth.com</a>.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2 id="malicious-content">1. Malicious Content</h2>
|
<h2 id="malicious-content">1. Malicious Content</h2>
|
||||||
|
|
||||||
<p>
|
<p>Content cannot contain or download malware, which we define as anything that is designed:</p>
|
||||||
Content cannot contain or download malware, which we define as anything
|
|
||||||
that is designed:
|
|
||||||
</p>
|
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
to upload any data to a remote server (i.e. one that the user does not
|
to upload any data to a remote server (i.e. one that the user does not directly choose to
|
||||||
directly choose to connect to in-game) without clear disclosure
|
connect to in-game) without clear disclosure
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
to disrupt, damage, or otherwise cause harm or damage to an individual,
|
to disrupt, damage, or otherwise cause harm or damage to an individual, computer, or network
|
||||||
computer, or network
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</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>
|
<p>
|
||||||
Content, especially projects, must make a clear and honest attempt to
|
Content, especially projects, must make a clear and honest attempt to describe their purpose
|
||||||
describe their purpose on the page(s) where it may be found.
|
on the page(s) where it may be found.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Content must not make or share intentionally wrong or misleading claims.
|
Content must not make or share intentionally wrong or misleading claims. This includes but is
|
||||||
This includes but is not limited to claims regarding the Content itself,
|
not limited to claims regarding the Content itself, claims regarding other Content, and claims
|
||||||
claims regarding other Content, and claims not relating to Content on
|
not relating to Content on Modrinth.
|
||||||
Modrinth.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h3 id="general-expectations">2.1. General expectations</h3>
|
<h3 id="general-expectations">2.1. General expectations</h3>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Projects in particular must attempt to describe the following three things
|
Projects in particular must attempt to describe the following three things within their
|
||||||
within their description:
|
description:
|
||||||
</p>
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>what a project specifically does or adds</li>
|
<li>what a project specifically does or adds</li>
|
||||||
<li>why someone should want to download the project</li>
|
<li>why someone should want to download the project</li>
|
||||||
<li>
|
<li>any other critical information the user must know before downloading</li>
|
||||||
any other critical information the user must know before downloading
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Project descriptions must also be accessible. For the most part, this
|
Project descriptions must also be accessible. For the most part, this means that descriptions
|
||||||
means that descriptions cannot mostly consist of text within images, and
|
cannot mostly consist of text within images, and necessary information cannot be obscured.
|
||||||
necessary information cannot be obscured.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Projects which don't meet of these expectations may be removed from search
|
Projects which don't meet of these expectations may be removed from search rather than removed
|
||||||
rather than removed from the platform altogether, at the moderators'
|
from the platform altogether, at the moderators' discretion.
|
||||||
discretion.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2 id="cheats-and-hacks">3. Cheats and Hacks</h2>
|
<h2 id="cheats-and-hacks">3. Cheats and Hacks</h2>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Projects cannot contain or download "cheats", which we define as a
|
Projects cannot contain or download "cheats", which we define as a client-side modification
|
||||||
client-side modification that:
|
that:
|
||||||
</p>
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>is advertised as a "cheat", "hack", or "hacked client"</li>
|
<li>is advertised as a "cheat", "hack", or "hacked client"</li>
|
||||||
<li>
|
<li>
|
||||||
gives an unfair advantage in a multiplayer setting over other players
|
gives an unfair advantage in a multiplayer setting over other players that do not have a
|
||||||
that do not have a comparable modification and does not provide a
|
comparable modification and does not provide a server-side opt-out
|
||||||
server-side opt-out
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
contains any of the following functions without requiring a server-side
|
contains any of the following functions without requiring a server-side opt-in:
|
||||||
opt-in:
|
|
||||||
<ul>
|
<ul>
|
||||||
<li>X-ray or the ability to see through opaque blocks</li>
|
<li>X-ray or the ability to see through opaque blocks</li>
|
||||||
<li>aim bot or aim assist</li>
|
<li>aim bot or aim assist</li>
|
||||||
<li>flight, speed, or other movement modifications</li>
|
<li>flight, speed, or other movement modifications</li>
|
||||||
<li>automatic PvP</li>
|
<li>automatic PvP</li>
|
||||||
<li>
|
<li>
|
||||||
active client-side hiding of third party modifications that have
|
active client-side hiding of third party modifications that have server-side opt-outs
|
||||||
server-side opt-outs
|
|
||||||
</li>
|
</li>
|
||||||
<li>item duplication</li>
|
<li>item duplication</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h2 id="copyright-and-legality-of-content">
|
<h2 id="copyright-and-legality-of-content">4. Copyright and legality of Content</h2>
|
||||||
4. Copyright and legality of Content
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
You must own or have the necessary licenses, rights, consents, and
|
You must own or have the necessary licenses, rights, consents, and permissions to store,
|
||||||
permissions to store, share, or distribute the Content that is uploaded
|
share, or distribute the Content that is uploaded under your Modrinth account.
|
||||||
under your Modrinth account.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Content may not be directly "reuploaded" from another platform without the
|
Content may not be directly "reuploaded" from another platform without the permission of the
|
||||||
permission of the author or copyright holder, even with the appropriate
|
author or copyright holder, even with the appropriate licensing or other rights. This
|
||||||
licensing or other rights. This restriction does not apply to content
|
restriction does not apply to content within modpacks or to so called "forks" - that is,
|
||||||
within modpacks or to so called "forks" - that is, modified copies of a
|
modified copies of a project which have diverged substantially enough from the original
|
||||||
project which have diverged substantially enough from the original
|
|
||||||
project, at the discretion of Modrinth's moderators.
|
project, at the discretion of Modrinth's moderators.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>Content must not infringe upon anyone's rights or intellectual property.</p>
|
||||||
Content must not infringe upon anyone's rights or intellectual property.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Content must abide by the laws which govern Rinth, Inc., i.e. those of the
|
Content must abide by the laws which govern Rinth, Inc., i.e. those of the United States and
|
||||||
United States and of the State of Delaware.
|
of the State of Delaware.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2 id="prohibited-content">5. Prohibited Content</h2>
|
<h2 id="prohibited-content">5. Prohibited Content</h2>
|
||||||
|
|
||||||
<p>
|
<p>Content on Modrinth is meant to be appropriate for audiences 13 years of age and above.</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>
|
<p>This means that the following Content is not allowed:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Content containing sexual or explicit material</li>
|
<li>Content containing sexual or explicit material</li>
|
||||||
<li>Content promoting or sharing harmful or hateful behavior</li>
|
<li>Content promoting or sharing harmful or hateful behavior</li>
|
||||||
<li>
|
<li>Content themed around or containing real-life drugs or illicit substances</li>
|
||||||
Content themed around or containing real-life drugs or illicit
|
|
||||||
substances
|
|
||||||
</li>
|
|
||||||
<li>Content with an excessive amount of profane language</li>
|
<li>Content with an excessive amount of profane language</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default defineNuxtComponent({
|
||||||
auth: false,
|
|
||||||
head: {
|
head: {
|
||||||
title: 'Rules - Modrinth',
|
title: 'Rules - Modrinth',
|
||||||
meta: [
|
meta: [
|
||||||
@@ -185,11 +158,11 @@ export default {
|
|||||||
{
|
{
|
||||||
hid: 'og:url',
|
hid: 'og:url',
|
||||||
name: 'og:url',
|
name: 'og:url',
|
||||||
content: `https://modrinth.com/legal/rules`,
|
content: 'https://modrinth.com/legal/rules',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
}
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped></style>
|
<style lang="scss" scoped></style>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user