[WIP] Rework design (#34)

* WIP: Redesign the default layout

* Merge old & new default layouts

* Fix login logic; add proper user controls dropdown

* Fix latest version listing (#31) (#32)

Co-authored-by: Aeledfyr <45501007+Aeledfyr@users.noreply.github.com>

* First pass of design cleanup

* Improve ad integration and fix light theme

* Begin splitting up variables, change some styling to new mockup

* Continue redesign progress

* Work on some more pages

* Add missing dark theme variables for text

* Continue working on modularizing

* Continue progress, redo pagination

* Fix auth buttons in navbar layout

* Continue progress

* Continue progress more

* Redo ModResult

* Scope ModPage :irritater:

* Continue Dashboard

* Continue progress on Dashboard and cleanup

* Add missing variables for dark theme

* Small tweaks, cleanup, and continue mod page progress

* Fix user not being able to see hidden mods that they own

* Start reworking mod creation

* Continue revamp of mod creation page

* Yank v-html out

* Hotfix markdown rendering and some spacing issues

* Move legal; continue with mod creation; create reusable footer

* Create README.md

* Update README.md

* Update README.md

* Add in basic usage instructions

* Fix some stuff

* Continue with mod creation; fix some CSS errors

* Start user page

* Start transition to vue-select; fix a few bugs

* Continue mod creation page

* Finish mod pages

* Add very raw version editing

* Mod editing + creation

* Fixed versions that were in processing causing a 404 (#39)

Co-authored-by: Mikhail Oleynikov <falseresync@gmail.com>
Co-authored-by: Aeledfyr <45501007+Aeledfyr@users.noreply.github.com>
Co-authored-by: Jai A <jai.a@tuta.io>
Co-authored-by: MulverineX <mulverin3@gmail.com>
Co-authored-by: diabolical17 <calumproh28@gmail.com>
Co-authored-by: Geometrically <18202329+Geometrically@users.noreply.github.com>
This commit is contained in:
Prospector
2020-11-30 13:55:01 -08:00
committed by GitHub
parent e025df0824
commit 7b84d8c3d5
70 changed files with 10339 additions and 3284 deletions

View File

@@ -1,4 +1,5 @@
{
"semi": false,
"singleQuote": true
"singleQuote": true,
"endOfLine": "auto"
}

4
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,4 @@
{
"prettier.endOfLine": "lf",
"editor.formatOnSave": true
}

22
README.md Normal file
View File

@@ -0,0 +1,22 @@
![knossos banner](https://user-images.githubusercontent.com/12068027/100479893-d9b5a380-30ac-11eb-9db9-0c09d400f13f.png)
## Modrinth's center for its frontend service
## Build Setup
```bash
# install dependencies
$ npm install
# serve with hot reload at localhost:3000
$ npm run dev
# build for production and launch server
$ npm run build
$ npm run start
# generate static project
$ npm run generate
```
For detailed explanation on how things work, check out [Nuxt.js docs](https://nuxtjs.org).

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-external-link"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg>

After

Width:  |  Height:  |  Size: 388 B

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve">
<path d="M7.3,20.8c-0.7-0.8-5.2-6-0.6-10.5c3.8-3.8,4.4-6.9,3.1-8.5c0,0,9.7,4,4,13.2c-1,1.6-3.4,3.4-2.1,7.2L7.3,20.8L7.3,20.8z
M13.2,22c-0.2-0.8-1.1-2.5,1.7-5.8c1.5-1.8,2.1-4.4,2.2-5.4c0,0,5,2.9,0,9.9C17.2,20.7,13.2,22,13.2,22z"/>
</svg>

After

Width:  |  Height:  |  Size: 584 B

View File

@@ -0,0 +1,3 @@
<svg width="113" height="128" viewBox="0 0 113 128" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M24.6637 59.3337L32.1412 51.8562L44.2184 66.0338L29.8772 79.9532C28.7085 81.0876 28.3443 82.8173 28.9563 84.3266C29.5683 85.836 31.0344 86.8235 32.6631 86.8235H92.5028L67.5247 106.806C66.2594 107.818 65.7298 109.494 66.1835 111.049L69.9608 124H31.0805L36.7657 111.596C37.3659 110.286 37.2156 108.755 36.3721 107.587L20.215 85.2161C20.3794 84.9958 20.5226 84.7565 20.6408 84.5005C21.2947 83.0843 21.0658 81.4171 20.0542 80.2297L12.3066 71.1346L20.0395 63.9541C20.0812 63.9153 20.122 63.8758 20.1617 63.8356L24.6614 59.336C24.6622 59.3352 24.6629 59.3344 24.6637 59.3337ZM101.193 76.5647H89.4745C90.2162 73.609 92.1388 69.5598 96.679 64.1834C101.3 58.726 104.009 51.7383 105.519 46.2493C106.213 47.2422 106.872 48.4045 107.402 49.743C109.386 54.7518 109.993 63.3535 101.193 76.5647ZM77.9962 64.0567C75.9317 67.4679 74.1929 71.566 73.842 76.5647H62.454C60.2035 73.7571 56.5771 68.6022 54.7738 62.4556C52.6945 55.3677 53.0109 47.0396 61.1035 39.0164L61.104 39.0159C69.5087 30.6807 74.6146 22.7347 76.8078 15.6999C77.5821 13.2161 78.0043 10.7893 78.0555 8.48397C80.6738 10.3958 83.4898 12.8635 85.9054 15.9006C89.1604 19.9929 91.6386 25.049 92.0315 31.1916C92.424 37.3281 90.7617 44.957 85.0299 54.247C84.3475 55.3429 83.4533 56.4992 82.3222 57.962C82.2062 58.1119 82.0877 58.2651 81.9667 58.4218C80.7208 60.0353 79.2887 61.9211 77.9962 64.0567Z" stroke="currentColor" stroke-width="8" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg
viewBox="0 0 24 24"
fill="currentColor"
stroke="none"
>
<path class="st0" d="m20.2 17.6c-0.5-0.2-0.9 0-1.2 0.3-1.2 1.4-2.8 2.5-4.8 3-3.1 0.8-6.2 0-8.4-1.9l3.6-3.2 2.1 1.9 4-1.8 2.1-3.3-0.8-1.6-2.3 1-1 1.3-1.5 0.7-1.2-1-0.8-1.5 1-1.3 1.5-0.7 1.6-2-1.3-1.2-3.8 1.3-2.7 3.5 1.3 2.5-3.7 3.2-0.5-0.8c-0.1-0.2-0.2-0.5-0.3-0.8 0-0.1-0.1-0.2-0.1-0.2-0.1-0.3-0.5-0.6-0.9-0.6-0.5 0-1 0.4-1 1 0.1 0.2 0.1 0.4 0.2 0.5 2 5.3 7.8 8.3 13.4 6.8 2.4-0.6 4.3-1.9 5.8-3.7 0.5-0.4 0.3-1.2-0.3-1.4zm2.6-8.5c-1.7-5.9-7.8-9.4-13.8-7.9-3.8 1-6.6 3.8-7.8 7.1-0.2 0.7 0.3 1.3 0.9 1.3 0.4 0 0.8-0.3 0.9-0.7 1-2.8 3.3-5 6.4-5.9 4.7-1.2 9.5 1.3 11.1 5.6l0.5 1.8c0.1 0.7 0.1 1.4 0.1 2.1v0.1c0 0.4 0.3 0.8 0.7 0.9 0.6 0.2 1.3-0.2 1.3-0.9 0.1-1 0-2.3-0.3-3.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 789 B

View File

@@ -0,0 +1,10 @@
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10"></circle>
<line x1="14.31" y1="8" x2="20.05" y2="17.94"></line>
<line x1="9.69" y1="8" x2="21.17" y2="8"></line>
<line x1="7.38" y1="12" x2="13.12" y2="2.06"></line>
<line x1="9.69" y1="16" x2="3.95" y2="6.06"></line>
<line x1="14.31" y1="16" x2="2.83" y2="16"></line>
<line x1="16.62" y1="12" x2="10.88" y2="21.94"></line>
</svg>

After

Width:  |  Height:  |  Size: 552 B

View File

@@ -0,0 +1,6 @@
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg">
<line x1="18" y1="20" x2="18" y2="10"></line>
<line x1="12" y1="20" x2="12" y2="4"></line>
<line x1="6" y1="20" x2="6" y2="14"></line>
</svg>

After

Width:  |  Height:  |  Size: 316 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
</svg>

After

Width:  |  Height:  |  Size: 218 B

View File

@@ -0,0 +1,8 @@
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg">
<line x1="16.5" y1="9.4" x2="7.5" y2="4.21" />
<path
d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" />
<polyline points="3.27 6.96 12 12.01 20.73 6.96" />
<line x1="12" y1="22.08" x2="12" y2="12" />
</svg>

After

Width:  |  Height:  |  Size: 471 B

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="22 12 16 12 14 15 10 15 8 12 2 12"></polyline>
<path
d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z">
</path>
</svg>

After

Width:  |  Height:  |  Size: 380 B

View File

@@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 777 141.73">
<g>
<path d="M159.07,89.29A70.94,70.94,0,1,0,20,63.52H32A58.78,58.78,0,0,1,145.23,49.93l-11.66,3.12a46.54,46.54,0,0,0-29-26.52l-2.15,12.13a34.31,34.31,0,0,1,2.77,63.26l3.19,11.9a46.52,46.52,0,0,0,28.33-49l11.62-3.1A57.94,57.94,0,0,1,147.27,85Z" transform="translate(-19.79)" fill="#5da545" fill-rule="evenodd"/>
<path d="M108.92,139.3A70.93,70.93,0,0,1,19.79,76h12a59.48,59.48,0,0,0,1.78,9.91,58.73,58.73,0,0,0,3.63,9.91l10.68-6.41a46.58,46.58,0,0,1,44.72-65L90.43,36.54A34.38,34.38,0,0,0,57.36,79.75C57.67,80.88,58,82,58.43,83l13.66-8.19L68,63.93l12.9-13.25,16.31-3.51L101.9,53l-7.52,7.61-6.55,2.06-4.69,4.82,2.3,6.38s4.64,4.94,4.65,4.94l6.57-1.74,4.67-5.13,10.2-3.24,3,6.84L104.05,88.43,86.41,94l-7.92-8.81L64.7,93.48a34.44,34.44,0,0,0,28.72,11.59L96.61,117A46.6,46.6,0,0,1,54.13,99.83l-10.64,6.38a58.81,58.81,0,0,0,99.6-9.77l11.8,4.29A70.77,70.77,0,0,1,108.92,139.3Z" transform="translate(-19.79)" fill="#5da545"/>
</g>
<g>
<path d="M303,52.44q7.51,7.45,7.52,22.37v39H294.54v-37q0-8.92-4-13.45t-11.35-4.52q-8,0-12.87,5.29t-4.85,15.1v34.55H245.59v-37q0-8.92-4-13.45t-11.35-4.52q-8.16,0-12.94,5.22t-4.78,15.17v34.55H196.64V45.75h15.17v8.66a24.78,24.78,0,0,1,9.56-7A32.51,32.51,0,0,1,234.11,45a30.83,30.83,0,0,1,13.58,2.87,22,22,0,0,1,9.37,8.48A28.31,28.31,0,0,1,267.89,48a35.16,35.16,0,0,1,14.66-3Q295.43,45,303,52.44Z" transform="translate(-19.79)" fill="#fff"/>
<path d="M343.49,110.25a32.53,32.53,0,0,1-12.94-12.43,35,35,0,0,1-4.66-18,34.68,34.68,0,0,1,4.66-18,32.66,32.66,0,0,1,12.94-12.37,41.33,41.33,0,0,1,37.35,0,32.7,32.7,0,0,1,12.93,12.37,34.68,34.68,0,0,1,4.66,18,35,35,0,0,1-4.66,18,32.57,32.57,0,0,1-12.93,12.43,41.33,41.33,0,0,1-37.35,0Zm33.14-15q5.73-5.86,5.74-15.43t-5.74-15.42a19.46,19.46,0,0,0-14.53-5.87,19.25,19.25,0,0,0-14.47,5.87Q342,70.22,342,79.78t5.67,15.43a19.25,19.25,0,0,0,14.47,5.86A19.46,19.46,0,0,0,376.63,95.21Z" transform="translate(-19.79)" fill="#fff"/>
<path d="M480.39,19.23v94.59H465.1V105a24.75,24.75,0,0,1-9.76,7.27,33,33,0,0,1-12.81,2.42,35.84,35.84,0,0,1-17.65-4.33,31.06,31.06,0,0,1-12.3-12.31,36.71,36.71,0,0,1-4.47-18.29,36.4,36.4,0,0,1,4.47-18.23,31.27,31.27,0,0,1,12.3-12.24A35.94,35.94,0,0,1,442.53,45a32.36,32.36,0,0,1,12.37,2.3,24.89,24.89,0,0,1,9.56,6.88V19.23ZM454.77,98.46A18.92,18.92,0,0,0,462,91a22.87,22.87,0,0,0,2.67-11.22A22.87,22.87,0,0,0,462,68.56a18.89,18.89,0,0,0-7.27-7.45,21.65,21.65,0,0,0-20.65,0,18.89,18.89,0,0,0-7.27,7.45,22.87,22.87,0,0,0-2.67,11.22A22.87,22.87,0,0,0,426.85,91a18.92,18.92,0,0,0,7.27,7.46,21.73,21.73,0,0,0,20.65,0Z" transform="translate(-19.79)" fill="#fff"/>
<path d="M540.69,45V60.15a20.64,20.64,0,0,0-3.7-.38q-9.3,0-14.53,5.42T517.23,80.8v33H501.3V45.75h15.17v9.94Q523.35,45,540.69,45Z" transform="translate(-19.79)" fill="#fff"/>
<path d="M555,31.79A8.94,8.94,0,0,1,552,25,8.94,8.94,0,0,1,555,18.15a10.2,10.2,0,0,1,7.26-2.74A10.55,10.55,0,0,1,569.5,18a8.43,8.43,0,0,1,2.93,6.56,9.58,9.58,0,0,1-2.87,7.08,9.92,9.92,0,0,1-7.33,2.87A10.2,10.2,0,0,1,555,31.79Zm-.77,14h15.94v68.07H554.2Z" transform="translate(-19.79)" fill="#fff"/>
<path d="M650.32,52.5q7.77,7.53,7.77,22.31v39H642.16v-37q0-8.92-4.21-13.45t-12-4.52q-8.81,0-13.9,5.29T607,79.4v34.42H591V45.75h15.17v8.79a25,25,0,0,1,9.94-7.14A35.91,35.91,0,0,1,629.67,45Q642.54,45,650.32,52.5Z" transform="translate(-19.79)" fill="#fff"/>
<path d="M716.6,110.12a18.65,18.65,0,0,1-6.82,3.44,30.46,30.46,0,0,1-8.47,1.15q-11.22,0-17.34-5.86t-6.12-17.09V30.7h15.94V46.26H712V59H693.79V91.38c0,3.32.82,5.85,2.48,7.59a9.14,9.14,0,0,0,7,2.61,14,14,0,0,0,8.92-2.8Z" transform="translate(-19.79)" fill="#fff"/>
<path d="M789,52.5q7.77,7.53,7.78,22.31v39H780.85v-37q0-8.92-4.21-13.45t-12-4.52q-8.79,0-13.89,5.29t-5.1,15.23v34.42H729.73V19.23h15.94V53.65a25.82,25.82,0,0,1,9.75-6.44A36,36,0,0,1,768.36,45Q781.23,45,789,52.5Z" transform="translate(-19.79)" fill="#fff"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

5632
assets/images/text-logo.ai Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 777 141.73">
<g>
<path d="M159.07,89.29A70.94,70.94,0,1,0,20,63.52H32A58.78,58.78,0,0,1,145.23,49.93l-11.66,3.12a46.54,46.54,0,0,0-29-26.52l-2.15,12.13a34.31,34.31,0,0,1,2.77,63.26l3.19,11.9a46.52,46.52,0,0,0,28.33-49l11.62-3.1A57.94,57.94,0,0,1,147.27,85Z" transform="translate(-19.79)" fill="#5da545" fill-rule="evenodd"/>
<path d="M108.92,139.3A70.93,70.93,0,0,1,19.79,76h12a59.48,59.48,0,0,0,1.78,9.91,58.73,58.73,0,0,0,3.63,9.91l10.68-6.41a46.58,46.58,0,0,1,44.72-65L90.43,36.54A34.38,34.38,0,0,0,57.36,79.75C57.67,80.88,58,82,58.43,83l13.66-8.19L68,63.93l12.9-13.25,16.31-3.51L101.9,53l-7.52,7.61-6.55,2.06-4.69,4.82,2.3,6.38s4.64,4.94,4.65,4.94l6.57-1.74,4.67-5.13,10.2-3.24,3,6.84L104.05,88.43,86.41,94l-7.92-8.81L64.7,93.48a34.44,34.44,0,0,0,28.72,11.59L96.61,117A46.6,46.6,0,0,1,54.13,99.83l-10.64,6.38a58.81,58.81,0,0,0,99.6-9.77l11.8,4.29A70.77,70.77,0,0,1,108.92,139.3Z" transform="translate(-19.79)" fill="#5da545"/>
</g>
<g>
<path d="M303,52.44q7.51,7.45,7.52,22.37v39H294.54v-37q0-8.92-4-13.45t-11.35-4.52q-8,0-12.87,5.29t-4.85,15.1v34.55H245.59v-37q0-8.92-4-13.45t-11.35-4.52q-8.16,0-12.94,5.22t-4.78,15.17v34.55H196.64V45.75h15.17v8.66a24.78,24.78,0,0,1,9.56-7A32.51,32.51,0,0,1,234.11,45a30.83,30.83,0,0,1,13.58,2.87,22,22,0,0,1,9.37,8.48A28.31,28.31,0,0,1,267.89,48a35.16,35.16,0,0,1,14.66-3Q295.43,45,303,52.44Z" transform="translate(-19.79)" fill="#19202c"/>
<path d="M343.49,110.25a32.53,32.53,0,0,1-12.94-12.43,35,35,0,0,1-4.66-18,34.68,34.68,0,0,1,4.66-18,32.66,32.66,0,0,1,12.94-12.37,41.33,41.33,0,0,1,37.35,0,32.7,32.7,0,0,1,12.93,12.37,34.68,34.68,0,0,1,4.66,18,35,35,0,0,1-4.66,18,32.57,32.57,0,0,1-12.93,12.43,41.33,41.33,0,0,1-37.35,0Zm33.14-15q5.73-5.86,5.74-15.43t-5.74-15.42a19.46,19.46,0,0,0-14.53-5.87,19.25,19.25,0,0,0-14.47,5.87Q342,70.22,342,79.78t5.67,15.43a19.25,19.25,0,0,0,14.47,5.86A19.46,19.46,0,0,0,376.63,95.21Z" transform="translate(-19.79)" fill="#19202c"/>
<path d="M480.39,19.23v94.59H465.1V105a24.75,24.75,0,0,1-9.76,7.27,33,33,0,0,1-12.81,2.42,35.84,35.84,0,0,1-17.65-4.33,31.06,31.06,0,0,1-12.3-12.31,36.71,36.71,0,0,1-4.47-18.29,36.4,36.4,0,0,1,4.47-18.23,31.27,31.27,0,0,1,12.3-12.24A35.94,35.94,0,0,1,442.53,45a32.36,32.36,0,0,1,12.37,2.3,24.89,24.89,0,0,1,9.56,6.88V19.23ZM454.77,98.46A18.92,18.92,0,0,0,462,91a22.87,22.87,0,0,0,2.67-11.22A22.87,22.87,0,0,0,462,68.56a18.89,18.89,0,0,0-7.27-7.45,21.65,21.65,0,0,0-20.65,0,18.89,18.89,0,0,0-7.27,7.45,22.87,22.87,0,0,0-2.67,11.22A22.87,22.87,0,0,0,426.85,91a18.92,18.92,0,0,0,7.27,7.46,21.73,21.73,0,0,0,20.65,0Z" transform="translate(-19.79)" fill="#19202c"/>
<path d="M540.69,45V60.15a20.64,20.64,0,0,0-3.7-.38q-9.3,0-14.53,5.42T517.23,80.8v33H501.3V45.75h15.17v9.94Q523.35,45,540.69,45Z" transform="translate(-19.79)" fill="#19202c"/>
<path d="M555,31.79A8.94,8.94,0,0,1,552,25,8.94,8.94,0,0,1,555,18.15a10.2,10.2,0,0,1,7.26-2.74A10.55,10.55,0,0,1,569.5,18a8.43,8.43,0,0,1,2.93,6.56,9.58,9.58,0,0,1-2.87,7.08,9.92,9.92,0,0,1-7.33,2.87A10.2,10.2,0,0,1,555,31.79Zm-.77,14h15.94v68.07H554.2Z" transform="translate(-19.79)" fill="#19202c"/>
<path d="M650.32,52.5q7.77,7.53,7.77,22.31v39H642.16v-37q0-8.92-4.21-13.45t-12-4.52q-8.81,0-13.9,5.29T607,79.4v34.42H591V45.75h15.17v8.79a25,25,0,0,1,9.94-7.14A35.91,35.91,0,0,1,629.67,45Q642.54,45,650.32,52.5Z" transform="translate(-19.79)" fill="#19202c"/>
<path d="M716.6,110.12a18.65,18.65,0,0,1-6.82,3.44,30.46,30.46,0,0,1-8.47,1.15q-11.22,0-17.34-5.86t-6.12-17.09V30.7h15.94V46.26H712V59H693.79V91.38c0,3.32.82,5.85,2.48,7.59a9.14,9.14,0,0,0,7,2.61,14,14,0,0,0,8.92-2.8Z" transform="translate(-19.79)" fill="#19202c"/>
<path d="M789,52.5q7.77,7.53,7.78,22.31v39H780.85v-37q0-8.92-4.21-13.45t-12-4.52q-8.79,0-13.89,5.29t-5.1,15.23v34.42H729.73V19.23h15.94V53.65a25.82,25.82,0,0,1,9.75-6.44A36,36,0,0,1,768.36,45Q781.23,45,789,52.5Z" transform="translate(-19.79)" fill="#19202c"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -1,15 +1,3 @@
<svg viewBox="0 0 273 291" fill-rule="evenodd" stroke-linejoin="round">
<g transform="matrix(1,0,0,1,-3303.68,-2082.6)">
<g transform="matrix(0.564163,0,0,1.70346,1629.87,0)">
<g transform="matrix(29.6754,0,-5.39484e-14,10.5245,2970.52,1222.47)">
<g transform="matrix(1.4323,0,0,1.4323,-3.56656,-3.89073)">
<path d="M10.854,7.146C10.948,7.24 11.001,7.367 11.001,7.5C11.001,7.633 10.948,7.76 10.854,7.854L7.854,10.854C7.76,10.948 7.633,11.001 7.5,11.001C7.367,11.001 7.24,10.948 7.146,10.854L5.646,9.354C5.552,9.26 5.499,9.133 5.499,9C5.499,8.725 5.725,8.499 6,8.499C6.133,8.499 6.26,8.552 6.354,8.646L7.5,9.793L10.146,7.146C10.24,7.052 10.367,6.999 10.5,6.999C10.633,6.999 10.76,7.052 10.854,7.146Z"/>
</g>
<path d="M1.427,4L1.427,13.715C1.427,14.264 1.879,14.715 2.427,14.715L13.573,14.715C14.121,14.715 14.573,14.264 14.573,13.715L14.573,4L1.427,4ZM1.878,1C0.781,1 -0.122,1.903 -0.122,3L-0.122,14.191C-0.122,15.288 0.781,16.191 1.878,16.191L14.183,16.191C15.28,16.191 16.183,15.097 16.183,14L16.183,3C16.183,1.903 15.28,1 14.183,1L1.878,1Z"/>
<g transform="matrix(1.32086,0,-1.73365e-32,1.32086,-3.43823,0.00935866)">
<path d="M4.819,0C5.094,-0 5.319,0.226 5.319,0.5L5.319,1C5.319,1.274 5.094,1.5 4.819,1.5C4.545,1.5 4.319,1.274 4.319,1L4.319,0.5C4.319,0.226 4.545,0 4.819,0ZM12.5,0C12.774,-0 13,0.226 13,0.5L13,1C13,1.274 12.774,1.5 12.5,1.5C12.226,1.5 12,1.274 12,1L12,0.5C12,0.226 12.226,0 12.5,0Z"/>
</g>
</g>
</g>
</g>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 7V3M16 7V3M7 11H17M5 21H19C20.1046 21 21 20.1046 21 19V7C21 5.89543 20.1046 5 19 5H5C3.89543 5 3 5.89543 3 7V19C3 20.1046 3.89543 21 5 21Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 343 B

View File

@@ -1,5 +1,3 @@
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7 10 12 15 17 10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 16L4 17C4 18.6569 5.34315 20 7 20L17 20C18.6569 20 20 18.6569 20 17L20 16M16 12L12 16M12 16L8 12M12 16L12 4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 290 B

After

Width:  |  Height:  |  Size: 312 B

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 9L12 16L5 9" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 217 B

View File

@@ -1,15 +1,3 @@
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path
d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"
></path>
<path
d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"
></path>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11 5H6C4.89543 5 4 5.89543 4 7V18C4 19.1046 4.89543 20 6 20H17C18.1046 20 19 19.1046 19 18V13M17.5858 3.58579C18.3668 2.80474 19.6332 2.80474 20.4142 3.58579C21.1953 4.36683 21.1953 5.63316 20.4142 6.41421L11.8284 15H9L9 12.1716L17.5858 3.58579Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 313 B

After

Width:  |  Height:  |  Size: 448 B

View File

@@ -1 +1,6 @@
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
<polyline points="15 3 21 3 21 9"></polyline>
<line x1="10" y1="14" x2="21" y2="3"></line>
</svg>

Before

Width:  |  Height:  |  Size: 293 B

After

Width:  |  Height:  |  Size: 312 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="2" viewBox="0 0 14 2"><path d="M18,12H6" transform="translate(-5 -11)" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/></svg>

After

Width:  |  Height:  |  Size: 235 B

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg enable-background="new 0 0 24 24" version="1.1" viewBox="0 0 24 24" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor" class="st0" d="m12 1c-6.3 0-11.3 5-11.3 11.3 0 5 3.2 9.2 7.7 10.7 0.6 0.1 0.8-0.2 0.8-0.5v-1.9c-3.2 0.6-3.8-1.6-3.8-1.6-0.5-1.3-1.3-1.7-1.3-1.7-1-0.7 0.1-0.7 0.1-0.7 1.1 0.1 1.7 1.2 1.7 1.2 1 1.7 2.7 1.2 3.3 0.9 0.1-0.7 0.4-1.2 0.7-1.5-2.5-0.2-5.1-1.2-5.1-5.5 0-1.2 0.4-2.2 1.2-3-0.1-0.3-0.5-1.4 0.1-3 0 0 1-0.3 3.1 1.2 0.9-0.3 1.8-0.5 2.8-0.5s1.9 0.1 2.8 0.4c2.2-1.5 3.1-1.2 3.1-1.2 0.6 1.6 0.2 2.7 0.1 3 0.7 0.8 1.2 1.8 1.2 3 0 4.4-2.6 5.3-5.2 5.6 0.4 0.3 0.8 1 0.8 2.1v3.1c0 0.3 0.2 0.7 0.8 0.5 4.5-1.5 7.7-5.7 7.7-10.7 0-6.2-5-11.2-11.3-11.2z"/>
</svg>

After

Width:  |  Height:  |  Size: 752 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-menu">
<line x1="3" y1="12" x2="21" y2="12" />
<line x1="3" y1="6" x2="21" y2="6" />
<line x1="3" y1="18" x2="21" y2="18" />
</svg>

After

Width:  |  Height:  |  Size: 351 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="14.414" height="12.162" viewBox="0 0 14.414 12.162"><path d="M7.667,14.333,3,9.667m0,0L7.667,5M3,9.667H15" transform="translate(-1.586 -3.586)" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/></svg>

After

Width:  |  Height:  |  Size: 296 B

View File

@@ -0,0 +1,6 @@
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path>
<polyline points="16 17 21 12 16 7"></polyline>
<line x1="21" y1="12" x2="9" y2="12"></line>
</svg>

After

Width:  |  Height:  |  Size: 297 B

View File

@@ -0,0 +1,4 @@
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
</svg>

After

Width:  |  Height:  |  Size: 239 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="14.414" height="12.162" viewBox="0 0 14.414 12.162"><path d="M7.667,14.333,3,9.667m0,0L7.667,5M3,9.667H15" transform="translate(16 15.748) rotate(180)" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/></svg>

After

Width:  |  Height:  |  Size: 304 B

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21 21L15 15M17 10C17 13.866 13.866 17 10 17C6.13401 17 3 13.866 3 10C3 6.13401 6.13401 3 10 3C13.866 3 17 6.13401 17 10Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 323 B

View File

@@ -0,0 +1,12 @@
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="5"></circle>
<line x1="12" y1="1" x2="12" y2="3"></line>
<line x1="12" y1="21" x2="12" y2="23"></line>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
<line x1="1" y1="12" x2="3" y2="12"></line>
<line x1="21" y1="12" x2="23" y2="12"></line>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
</svg>

After

Width:  |  Height:  |  Size: 649 B

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7 7H7.01M7 3H12C12.5119 2.99999 13.0237 3.19525 13.4142 3.58579L20.4143 10.5858C21.1953 11.3668 21.1953 12.6332 20.4143 13.4142L13.4142 20.4142C12.6332 21.1953 11.3668 21.1953 10.5858 20.4142L3.58579 13.4142C3.19526 13.0237 3 12.5118 3 12V7C3 4.79086 4.79086 3 7 3Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 468 B

View File

@@ -0,0 +1,5 @@
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle>
</svg>

After

Width:  |  Height:  |  Size: 241 B

View File

@@ -0,0 +1,7 @@
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
<circle cx="9" cy="7" r="4"></circle>
<path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
</svg>

After

Width:  |  Height:  |  Size: 337 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>

After

Width:  |  Height:  |  Size: 337 B

View File

@@ -7,54 +7,47 @@
input {
padding-left: 2.5rem;
&:hover {
&+svg {
color: var(--color-grey-6);
}
}
&:focus {
&+svg {
color: var(--color-text);
}
}
}
svg {
color: var(--color-grey-5);
color: var(--color-icon);
margin-right: -2rem;
}
}
.badge {
max-height: 1rem;
border-radius: 1rem;
font-size: 0.8rem;
font-size: var(--font-size-xs);
font-weight: bold;
letter-spacing: 0.02rem;
padding: 0.25rem 0.5rem;
&.gray {
background-color: #c8c1c1;
color: #646161;
background-color: var(--color-badge-gray-bg);
color: var(--color-badge-gray-text);
}
&.red {
background-color: #fed7d7;
color: #9b2c2c;
background-color: var(--color-badge-red-bg);
color: var(--color-badge-red-text);
}
&.green {
background-color: #c6f6d5;
color: #276749;
background-color: var(--color-badge-green-bg);
color: var(--color-badge-green-text);
}
&.yellow {
background-color: #f6e8c6;
color: #675027;
background-color: var(--color-badge-yellow-bg);
color: var(--color-badge-yellow-text);
}
}
.text-link {
text-decoration: underline;
}
.required:after {
content: ' *';
color: red;
@@ -70,14 +63,14 @@
h1, h2 {
padding: 10px 0 5px;
border-bottom: 1px solid var(--color-grey-3);
border-bottom: 1px solid var(--color-header-underline);
}
blockquote {
margin: 15px 0;
padding: 0 1em;
color: var(--color-grey-5);
border-left: .25em solid var(--color-grey-3);
color: var(--color-text);
border-left: .25em solid var(--color-block-quote);
}
a {
@@ -90,8 +83,8 @@
pre {
padding: 15px 10px;
border-radius: var(--size-rounded-sm);
background-color: var(--color-grey-1);
border-radius: var(--size-rounded-control);
background-color: var(--color-code-bg);
code {
font-size: 80%;
@@ -101,10 +94,11 @@
}
code {
padding: .2em .4em;
font-size: 60%;
border-radius: var(--size-rounded-sm);
background-color: var(--color-grey-1)
padding: 0.2em 0.4em;
font-size: 80%;
border-radius: var(--size-rounded-control);
background-color: var(--color-code-bg);
color: var(--color-code-text);
}
hr {
@@ -118,9 +112,12 @@
z-index: 10000;
.tooltip-inner {
background: var(--color-grey-2);
color: var(--color-text);
background: var(--color-tooltip-bg);
color: var(--color-tooltip-text);
padding: 5px 10px 4px;
border-radius: var(--size-rounded-tooltip);
box-shadow: var(--shadow-tooltip);
font-size: 0.9rem;
}
.tooltip-arrow {
@@ -129,7 +126,7 @@
border-style: solid;
position: absolute;
margin: 5px;
border-color: var(--color-grey-2);
border-color: var(--color-tooltip-bg);
z-index: 1;
}
@@ -205,3 +202,174 @@
transition: opacity .15s;
}
}
.tabs {
display: flex;
padding: 0.5rem 1rem;
.filler {
flex-grow: 1;
}
}
.tabs a.tab {
user-select: none;
display: flex;
align-items: center;
padding: 0.5rem 0.25rem;
padding-bottom: 0.2rem;
margin: auto 0.5rem;
border-bottom: 3px solid transparent;
svg {
width: 1rem;
height: 1rem;
margin-right: 0.3rem;
}
&:hover,
&:focus {
border-bottom: 3px solid var(--color-brand-disabled);
color: var(--color-text-medium);
}
&.nuxt-link-exact-active {
border-bottom: 3px solid var(--color-brand);
color: var(--color-text-dark);
}
}
.sidebar {
.card {
padding: var(--spacing-card-md);
margin-bottom: var(--spacing-card-md);
@extend %card;
&.page-nav {
.tab {
padding: var(--spacing-card-sm);
display: flex;
align-items: center;
border-radius: var(--size-rounded-control);
margin-bottom: 0.5rem;
@extend %transparent-clickable;
&.last {
margin-bottom: 0;
}
svg {
color: var(--color-icon);
margin-right: 5px;
height: 1.25rem;
flex-shrink: 0;
}
&.nuxt-link-exact-active {
svg {
color: var(--color-brand-light);
}
}
}
}
}
@media screen and (min-width: 1024px) {
width: 300px;
}
}
.sidebar-l {
@extend .sidebar;
margin-right: var(--spacing-card-lg);
}
.sidebar-r {
@extend .sidebar;
margin-left: var(--spacing-card-lg);
}
.button {
margin: auto 0;
padding: 6px 20px;
border-radius: var(--size-rounded-control);
color: var(--color-button-text);
background-color: var(--color-button-bg);
font-weight: var(--font-weight-medium);
&:focus,
&:hover {
background-color: var(--color-button-bg-hover);
}
&:active {
background-color: var(--color-button-bg-active);
}
}
.transparent-button {
@extend %transparent-clickable;
margin: auto 0;
padding: 6px 20px;
text-decoration: underline;
}
.brand-button {
@extend .button;
color: var(--color-brand-inverted);
background-color: var(--color-brand);
&:focus,
&:hover {
background-color: var(--color-brand-hover);
color: var(--color-brand-inverted);
}
&:active {
background-color: var(--color-brand-active);
color: var(--color-brand-inverted);
}
}
.multiselect--above .multiselect__content-wrapper {
border-top: none !important;
}
.multiselect {
color: var(--color-text) !important;
input {
background: transparent;
}
.multiselect__tags {
background: var(--color-dropdown-bg);
border: none;
cursor: pointer;
&:active,
&:hover {
background: var(--color-button-bg-hover);
}
.multiselect__single {
background: transparent;
}
.multiselect__tag {
color: var(--color-text-dark);
background: transparent;
border: 2px solid var(--color-brand);
}
.multiselect__tag-icon {
background: transparent;
&:after {
color: var(--color-text-dark);
}
}
}
.multiselect__content-wrapper {
background: var(--color-dropdown-bg);
border: none;
.multiselect__element {
.multiselect__option--highlight {
background: var(--color-button-bg-active);
color: var(--color-text-dark);
}
.multiselect__option--selected {
background: var(--color-brand);
font-weight: bold;
color: var(--color-brand-inverted);
}
}
}
}

View File

@@ -1,63 +1,204 @@
.light-mode {
--color-text: #1a202c;
--color-bg: #ffffff;
--color-icon: #718096;
--color-text:#4A5568;
--color-text-medium:#2a303d;
--color-text-dark:#1A202C;
--color-heading: #2c313d;
--color-heading-light: #777e8d;
--color-bg: #edf2f7;
--color-raised-bg: #ffffff;
--color-divider: #eaecef;
--color-divider-dark: #c8cdd3;
--color-text-inverted: var(--color-bg);
--color-bg-inverted: var(--color-text);
--color-brand: #4d9227;
--color-brand: #5da545;
--color-brand-hover: #53923e;
--color-brand-active: #4b8138;
--color-brand-light: #6bac57;
--color-brand-inverted: #ffffff;
--color-brand-2: #5fa33b;
--color-brand-3: #c5ddb7;
--color-brand-disabled: #e2e8f0;
--color-grey-0: #f7fafc;
--color-grey-1: #edf2f7;
--color-grey-2: #e2e8f0;
--color-grey-3: #cbd5e0;
--color-grey-4: #a0aec0;
--color-grey-5: #718096;
--color-grey-6: #4a5568;
--color-grey-7: #2d3748;
--color-button-bg: var(--color-bg);
--color-button-text: var(--color-text-dark);
--color-button-bg-hover: #e0e7ee;
--color-button-text-hover: #1b1e24;
--color-button-bg-active: #d0d7df;
--color-button-text-active: var(--color-button-text-hover);
--color-button-bg-disabled: #eceef0;
--color-button-text-disabled: #9da3ac;
--color-transparent-button-bg-hover: var(--color-button-bg);
--color-transparent-button-text-hover: var(--color-text-dark);
--color-transparent-button-bg-active: var(--color-button-bg-hover);
--color-transparent-button-text-active: var(--color-text-dark);
--color-dropdown-bg: var(--color-button-bg);
--color-dropdown-text: var(--color-button-text);
--color-category-bg: var(--color-bg);
--color-category-text: var(--color-text-dark);
--color-tooltip-bg: var(--color-text);
--color-tooltip-text: var(--color-bg);
--color-code-bg: var(--color-bg);
--color-code-text: var(--color-text-dark);
--color-ad: #d6e6f9;
--shadow-dropdown: 3px 3px 14px hsla(0, 0%, 0%, 0.15);
--shadow-dropdown-strong: 3px 3px 10px hsla(0, 0%, 0%, 0.3);
--shadow-tooltip: 0.2rem 0.2rem 10px rgba(0, 0, 0, 0.15);
--color-link: #2089ff;
--color-badge-gray-text: #646161;
--color-badge-gray-bg: #c8c1c1;
--color-badge-red-text: #9b2c2c;
--color-badge-red-bg: #fed7d7;
--color-badge-green-text: #276749;
--color-badge-green-bg: #c6f6d5;
--color-badge-yellow-text: #675027;
--color-badge-yellow-bg: #f6e8c6;
--color-block-quote: var(--color-tooltip-bg);
--color-header-underline: var(--color-tooltip-text);
--color-hr: var(--color-text);
}
.dark-mode {
--color-text: #ccccc2;
--color-bg: #191917;
--color-icon: #acacac;
--color-text: #cecece;
--color-text-medium:#e4e4e4;
--color-text-dark:#ffffff;
--color-heading: #ffffff;
--color-heading-light: #8a8a8a;
--color-bg: #171719;
--color-raised-bg: #222224;
--color-divider: #49494d;
--color-divider-dark: #646468;
--color-text-inverted: var(--color-bg);
--color-bg-inverted: var(--color-text);
--color-brand: #4d9227;
--color-brand: #5da545;
--color-brand-hover: #6bac57;
--color-brand-active: #7fc46a;
--color-brand-light: #6bac57;
--color-brand-inverted: #ffffff;
--color-brand-2: #438121;
--color-brand-3: #344d26;
--color-brand-disabled: #414146;
--color-grey-7: #f7faf0;
--color-grey-6: #f7f2eb;
--color-grey-5: #ede9e4;
--color-grey-4: #b3b3b3;
--color-grey-3: #797b76;
--color-grey-2: #3b3937;
--color-grey-1: #3a3b38;
--color-grey-0: #1d1e1b;
--color-button-bg: #343438;
--color-button-text: var(--color-text);
--color-button-bg-hover: #3a3a3f;
--color-button-text-hover: #ffffff;
--color-button-bg-active: #49494e;
--color-button-text-active: var(--color-button-text-hover);
--color-button-text-disabled: #444444;
--color-transparent-button-bg-hover: var(--color-button-bg);
--color-transparent-button-text-hover: var(--color-text-dark);
--color-transparent-button-bg-active: var(--color-button-bg-hover);
--color-transparent-button-text-active: var(--color-text-dark);
--color-dropdown-bg: var(--color-button-bg);
--color-dropdown-text: var(--color-button-text);
--color-category-bg: var(--color-button-bg);
--color-category-text: var(--color-text-dark);
--color-tooltip-bg: var(--color-text);
--color-tooltip-text: var(--color-bg);
--color-code-bg: var(--color-button-bg);
--color-code-text: var(--color-text-dark);
--color-ad: #2c323a;
--shadow-dropdown: 3px 3px 14px hsla(0, 0%, 0%, 0.15);
--shadow-dropdown-strong: 3px 3px 20px hsla(0, 0%, 0%, 0.15);
--shadow-tooltip: 0.2rem 0.2rem 10px rgba(0, 0, 0, 0.15);
--color-link: #4793d9;
--color-badge-gray-bg: #646161;
--color-badge-gray-text: #c8c1c1;
--color-badge-red-bg: #9b2c2c;
--color-badge-red-text: #fed7d7;
--color-badge-green-bg: #276749;
--color-badge-green-text: #c6f6d5;
--color-badge-yellow-bg: #675027;
--color-badge-yellow-text: #f6e8c6;
--color-block-quote:var(--color-code-bg);
--color-header-underline: var(--color-tooltip-text);
--color-hr: var(--color-text);
}
body {
// Defaults
background-color: var(--color-bg);
color: var(--color-text);
font-family: Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen,
Ubuntu, Roboto, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
--font-standard: Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Roboto, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
font-family: var(--font-standard);
font-size: 16px;
font-weight: var(--font-weight-medium);
margin: 0;
padding: 0;
/**
* Sizes
*/
--size-rounded-sm: 0.25rem;
--size-rounded-md: 0.5rem;
--size-rounded-lg: 1rem;
overflow-y: overlay;
/**
* Defaults
*/
font-size: 18px;
// Rounding sizes
--size-rounded-xs: 0.5rem;
--size-rounded-sm: 0.75rem;
--size-rounded-md: 1rem;
--size-rounded-lg: 1.25rem;
--size-rounded-max: 999999999px;
--size-rounded-card: 0.5rem;
--size-rounded-icon: 0.5rem;
--size-rounded-control: 0.25rem;
--size-rounded-tooltip: 0.25rem;
--size-navbar-height: 4rem;
--spacing-card-lg: 1.5rem;
--spacing-card-md: 0.75rem;
--spacing-card-sm: 0.5rem;
// Font Sizes
--font-size-xxs: 0.625rem; //10px
--font-size-xs: 0.75rem; //12px
--font-size-sm: 0.875rem; //14px
--font-size-nm: 1rem; //16px
--font-size-md: 1.125rem; //18px
--font-size-lg: 1.25rem; //20px
--font-size-xl: 1.5rem; //24px
--font-size-2xl: 2rem; //32px
--font-size-3xl: 3rem; //48px
// Font Weights
--font-weight-regular: 400;
--font-weight-medium: 500;
--font-weight-bold: 700;
--font-weight-extrabold: 800;
--font-weight-text: var(--font-weight-medium);
--font-weight-heading: var(--font-weight-extrabold);
--font-weight-title: var(--font-weight-extrabold);
// Temporary
--color-grey-7: #ff0000;
--color-grey-6: #ff0000;
--color-grey-5: #ff0000;
--color-grey-4: #ff0000;
--color-grey-3: #ff0000;
--color-grey-2: #ff0000;
--color-grey-1: #ff0000;
--color-grey-0: #ff0000;
}
svg {
@@ -72,38 +213,91 @@ a {
h2 {
margin-top: 0;
margin-bottom: 1.25rem;
margin-bottom: 1rem;
color: var(--color-text-dark);
}
h3 {
margin-top: 1rem;
margin-bottom: 0.5rem;
margin-top: 0.5rem;
margin-bottom: 0;
color: var(--color-text-dark);
}
input {
background: var(--color-bg);
border: 2px solid var(--color-grey-3);
border-radius: var(--size-rounded-sm);
color: var(--color-grey-9);
font-size: 1rem;
button {
cursor: pointer;
@extend .button;
}
input,
textarea {
background: var(--color-button-bg);
border-radius: var(--size-rounded-control);
color: var(--color-text);
padding: 0.5rem 1rem;
width: 100%;
border: 2px solid transparent;
&:focus,
&:hover {
border-color: var(--color-grey-4);
background: var(--color-button-bg-hover);
color: var(--color-text);
outline: none;
&::placeholder {
color: var(--color-grey-7);
color: var(--color-text);
}
}
&:focus {
background: var(--color-raised-bg);
border-color: var(--color-divider-dark);
}
&::placeholder {
color: var(--color-grey-6);
color: var(--color-color-text);
}
}
.ea-content {
background: var(--color-ad) !important;
border-radius: var(--size-rounded-card) !important;
margin-left: 0 !important;
margin-right: 0 !important;
margin-bottom: 0 !important;
margin-top: 0 !important;
box-shadow: var(--shadow-faint) !important;
}
.ea-callout {
margin-top: 0.25rem !important;
margin-bottom: 0.25rem !important;
margin-left: 0 !important;
margin-right: 0 !important;
padding: 0 !important;
}
button {
padding: 0.5rem 0;
outline: none;
color: var(--color-button-text);
background-color: var(--color-button-bg);
border: none;
border-radius: var(--size-rounded-control);
&:focus,
&:hover {
background-color: var(--color-button-bg-hover);
color: var(--color-button-text-hover);
}
&:active {
background-color: var(--color-button-bg-active);
color: var(--color-button-text-active);
}
}
// @import "vue-select/src/scss/vue-select.scss";
@import "~assets/styles/highlightjs.scss";
@import "~assets/styles/layout.scss";
@import "~assets/styles/utils.scss";

View File

@@ -0,0 +1,87 @@
%card {
background: var(--color-raised-bg);
border-radius: var(--size-rounded-card);
}
%card-spaced-b {
@extend %card;
margin-bottom: var(--spacing-card-md);
}
%row {
display: flex;
flex-direction: row;
}
%column {
display: flex;
flex-direction: column;
}
%transparent-clickable {
border-radius: var(--size-rounded-control);
color: var(--color-text);
background-color: transparent;
&:focus,
&:hover,
&.selected,
&.nuxt-link-exact-active {
color: var(--color-transparent-button-text-hover);
background-color: var(--color-transparent-button-bg-hover);
}
&:active {
color: var(--color-transparent-button-text-active);
background-color: var(--color-transparent-button-bg-active);
}
}
%label {
color: var(--color-text);
font-weight: var(--font-weight-extrabold);
letter-spacing: 0.02rem;
margin: 0;
margin-bottom: 0.25em;
text-transform: uppercase;
}
%small-label {
@extend %label;
color: var(--color-text);
font-size: var(--font-size-xs);
letter-spacing: 0.02rem;
}
%large-label {
@extend %label;
color: var(--color-text);
font-size: var(--font-size-sm);
margin-bottom: 0.5em;
}
%stat {
margin-top: 0.5rem;
margin-right: 1rem;
@extend %row;
@media screen and (min-width: 900px) {
margin-top: 0;
}
svg {
margin: auto 0.5rem auto 0;
height: 1.5rem;
width: 1.5rem;
color: var(--color-icon);
}
.info {
margin: auto 0;
white-space: nowrap;
h4 {
@extend %small-label;
}
.value {
font-size: var(--font-size-sm);
margin: 0;
color: var(--color-text-dark);
}
}
}

View File

@@ -10,7 +10,7 @@
.rows {
display: flex;
flex-direction: row;
flex-direction: column;
@for $i from 1 through 4 {
.row-grow-#{$i} {
@@ -18,7 +18,20 @@
}
}
}
.w-100 {
width: 100%;
}
.page-container {
margin: var(--spacing-card-lg);
.page-contents {
display: flex;
flex-direction: row;
.content {
width: 100%;
}
@media screen and (min-width: 900px) {
}
@media screen and (min-width: 1024px) {
max-width: 1280px;
margin-left: auto;
margin-right: auto;
}
}
}

View File

@@ -1,7 +1,7 @@
.rounded-md {
border-radius: 0.5rem;
}
.hidden {
display: none;
}
.w-100 {
width: 100%;
}

132
components/Categories.vue Normal file
View File

@@ -0,0 +1,132 @@
<template>
<div class="categories">
<p v-if="categories.includes('fabric')">
<FabricLoader />
Fabric
</p>
<p v-if="categories.includes('forge')">
<ForgeLoader />
Forge
</p>
<p v-if="categories.includes('technology')">
<TechCategory />
Technology
</p>
<p v-if="categories.includes('adventure')">
<AdventureCategory />
Adventure
</p>
<p v-if="categories.includes('magic')">
<MagicCategory />
Magic
</p>
<p v-if="categories.includes('utility')">
<UtilityCategory />
Utility
</p>
<p v-if="categories.includes('decoration')">
<DecorationCategory />
Decoration
</p>
<p v-if="categories.includes('library')">
<LibraryCategory />
Library
</p>
<p v-if="categories.includes('cursed')">
<CursedCategory />
Cursed
</p>
<p v-if="categories.includes('worldgen')">
<WorldGenCategory />
Worldgen
</p>
<p v-if="categories.includes('storage')">
<StorageCategory />
Storage
</p>
<p v-if="categories.includes('food')">
<FoodCategory />
Food
</p>
<p v-if="categories.includes('equipment')">
<EquipmentCategory />
Equipment
</p>
<p v-if="categories.includes('misc')">
<MiscCategory />
Misc
</p>
</div>
</template>
<script>
import TechCategory from '~/assets/images/categories/tech.svg?inline'
import AdventureCategory from '~/assets/images/categories/adventure.svg?inline'
import CursedCategory from '~/assets/images/categories/cursed.svg?inline'
import DecorationCategory from '~/assets/images/categories/decoration.svg?inline'
import EquipmentCategory from '~/assets/images/categories/equipment.svg?inline'
import FoodCategory from '~/assets/images/categories/food.svg?inline'
import LibraryCategory from '~/assets/images/categories/library.svg?inline'
import MagicCategory from '~/assets/images/categories/magic.svg?inline'
import MiscCategory from '~/assets/images/categories/misc.svg?inline'
import StorageCategory from '~/assets/images/categories/storage.svg?inline'
import UtilityCategory from '~/assets/images/categories/utility.svg?inline'
import WorldGenCategory from '~/assets/images/categories/worldgen.svg?inline'
import ForgeLoader from '~/assets/images/categories/forge.svg?inline'
import FabricLoader from '~/assets/images/categories/fabric.svg?inline'
export default {
name: 'Categories',
components: {
TechCategory,
AdventureCategory,
CursedCategory,
DecorationCategory,
EquipmentCategory,
FoodCategory,
LibraryCategory,
MagicCategory,
MiscCategory,
StorageCategory,
UtilityCategory,
WorldGenCategory,
ForgeLoader,
FabricLoader,
},
props: {
categories: {
type: Array,
default() {
return []
},
},
},
}
</script>
<style lang="scss" scoped>
.categories {
@extend %row;
flex-wrap: wrap;
p {
display: flex;
align-items: center;
flex-direction: row;
background-color: var(--color-category-bg);
border-radius: var(--size-rounded-max);
color: var(--color-category-text);
margin-top: 0.25em;
margin-bottom: 0.25em;
margin-right: 0.5em;
padding: 0.4em 0.7em;
font-size: var(--font-size-sm);
height: 1em;
svg {
width: 15px;
margin-right: 5px;
}
}
}
</style>

View File

@@ -1,86 +1,53 @@
<template>
<div>
<slot></slot>
<label class="button">
<span>
{{ prompt }}
</span>
<input
:id="inputId"
class="file-input"
type="file"
:accept="inputAccept"
:multiple="inputMultiple"
@change="onChange"
:multiple="multiple"
:accept="accept"
@change="(files) => $emit('change', files)"
/>
<label :for="inputId">{{ text }}</label>
</div>
</label>
</template>
<script>
export default {
name: 'FileInput',
props: {
defaultText: {
prompt: {
type: String,
default: '',
default: 'Select file',
},
inputId: {
type: String,
default: '',
},
inputAccept: {
type: String,
default: '',
},
inputMultiple: {
multiple: {
type: Boolean,
default: true,
default: false,
},
},
data() {
return {
text: this.defaultText,
}
},
methods: {
onChange(files) {
const length = files.target.files.length
if (length === 0) {
this.text = this.defaultText
} else if (length === 1) {
this.text = '1 file selected'
} else if (length > 1) {
this.text = length + ' files selected'
}
this.$emit('change', files)
accept: {
type: String,
default: null,
},
},
}
</script>
<style lang="scss" scoped>
[type='file'] {
border: 0;
clip: rect(0, 0, 0, 0);
height: 1px;
overflow: hidden;
padding: 0;
position: absolute !important;
white-space: nowrap;
width: 1px;
label {
display: flex;
align-items: center;
justify-content: center;
text-align: center;
padding: var(--spacing-card-sm) var(--spacing-card-md);
}
+ label {
cursor: pointer;
border-radius: 5px;
color: var(--color-grey-5);
background-color: var(--color-grey-1);
padding: 10px 20px;
}
span {
border: 2px dashed var(--color-divider-dark);
border-radius: var(--size-rounded-control);
padding: var(--spacing-card-md) var(--spacing-card-lg);
}
&:focus + label,
+ label:hover,
&:focus + label {
background-color: var(--color-grey-2);
color: var(--color-text);
}
input {
display: none;
}
</style>

79
components/MFooter.vue Normal file
View File

@@ -0,0 +1,79 @@
<template>
<footer :class="{ centered }">
<span>
Modrinth is open source software. You may view the source code at
<a target="_blank" href="https://github.com/modrinth">our GitHub page</a>.
</span>
<ul>
<li>
<nuxt-link to="/legal/terms">Terms</nuxt-link>
</li>
<li>
<nuxt-link to="/legal/privacy">Privacy</nuxt-link>
</li>
<li>
<nuxt-link to="/about">About</nuxt-link>
</li>
</ul>
<ul>
<li>
<a target="_blank" href="https://blog.modrinth.com">Blog</a>
</li>
<li>
<a target="_blank" href="https://discord.gg/gFRbNQ2">Discord</a>
</li>
<li>
<a target="_blank" href="https://twitter.com/modrinth">Twitter</a>
</li>
</ul>
<span> © Guavy LLC </span>
</footer>
</template>
<script>
export default {
props: {
centered: {
type: Boolean,
default: false,
},
},
}
</script>
<style lang="scss" scoped>
.centered {
align-items: center;
}
footer {
padding: 2rem 0 5rem 0;
display: flex;
flex-direction: column;
ul {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-wrap: wrap;
li {
margin-bottom: var(--spacing-card-sm);
&:not(:last-child)::after {
content: '•';
padding: 0;
margin: 0 var(--spacing-card-sm);
}
}
}
& > *:not(:last-child) {
margin-bottom: var(--spacing-card-sm);
}
}
a {
text-decoration: underline;
}
</style>

View File

@@ -1,71 +1,134 @@
<template>
<div class="columns">
<div class="content column-grow-5">
<div class="mod-header columns">
<img
:src="
mod.icon_url
? mod.icon_url
: 'https://cdn.modrinth.com/placeholder.svg'
"
alt="mod-icon"
/>
<div class="mod-header-text">
<div class="columns title">
<h2>{{ mod.title }}</h2>
<nuxt-link
:to="'/user/' + members.find((x) => x.role === 'Owner').user_id"
>
<p>by {{ members.find((x) => x.role === 'Owner').name }}</p>
</nuxt-link>
<div class="page-container">
<div class="page-contents">
<div class="content">
<div class="header">
<div class="icon">
<img
:src="
mod.icon_url
? mod.icon_url
: 'https://cdn.modrinth.com/placeholder.svg'
"
alt="mod - icon"
/>
</div>
<p>{{ mod.description }}</p>
<div class="info">
<h2 class="title">{{ mod.title }}</h2>
<p class="description">
{{ mod.description }}
</p>
</div>
</div>
<client-only>
<EthicalAd type="text" />
</client-only>
<div class="mod-navigation">
<div class="tabs">
<nuxt-link :to="'/mod/' + mod.id" class="tab">
Description
</nuxt-link>
<nuxt-link :to="'/mod/' + mod.id + '/versions'" class="tab">
Versions
</nuxt-link>
<a v-if="mod.wiki_url" :href="mod.wiki_url" class="tab">
<ExternalIcon />
Wiki
</a>
<a
v-if="mod.issues_url"
:href="mod.issues_url"
target="_blank"
class="tab"
>
<ExternalIcon />
Issues
</a>
<a
v-if="mod.source_url"
:href="mod.source_url"
target="_blank"
class="tab"
>
<ExternalIcon />
Source
</a>
<nuxt-link
v-if="
this.$auth.loggedIn &&
members.find((x) => x.user_id === this.$auth.user.id)
"
:to="'/mod/' + mod.id + '/settings'"
class="tab"
>
Settings
</nuxt-link>
<div class="filler" />
</div>
</div>
<div class="mod-content">
<slot />
</div>
</div>
<div class="mod-navigation">
<nuxt-link :to="'/mod/' + mod.id">
<InfoIcon />
Description
</nuxt-link>
<nuxt-link :to="'/mod/' + mod.id + '/versions'">
<VersionIcon />
Versions
</nuxt-link>
<nuxt-link
v-if="
this.$auth.loggedIn &&
members.find((x) => x.user_id === this.$auth.user.id)
"
:to="'/mod/' + mod.id + '/settings'"
>
<SettingsIcon />
Settings
</nuxt-link>
<a v-if="mod.wiki_url" :href="mod.wiki_url">
<ExternalIcon />
Wiki
</a>
<a v-if="mod.issues_url" :href="mod.issues_url">
<ExternalIcon />
Issues
</a>
<a v-if="mod.source_url" :href="mod.source_url">
<ExternalIcon />
Source Code
</a>
<div class="filler" />
</div>
<slot />
</div>
<div>
<section class="mod-info">
<div class="mod-stats">
<h3>Info</h3>
<p>{{ mod.downloads }} Downloads</p>
<p>Created {{ $dayjs(mod.published).fromNow() }}</p>
<p>Updated {{ $dayjs(mod.updated).fromNow() }}</p>
<div class="mod-stats section">
<div class="stat">
<DownloadIcon />
<div class="info">
<h4>Downloads</h4>
<p class="value">{{ formatNumber(mod.downloads) }}</p>
</div>
</div>
<div class="stat">
<CalendarIcon />
<div class="info">
<h4>Created</h4>
<p
v-tooltip="
$dayjs(mod.published).format(
'[Created on] YYYY-MM-DD [at] HH:mm A'
)
"
class="value"
>
{{ $dayjs(mod.published).fromNow() }}
</p>
</div>
</div>
<div class="stat">
<TagIcon />
<div class="info">
<h4>Available For</h4>
<p class="value">
{{
versions[versions.length - 1]
? versions[versions.length - 1].game_versions[0]
? versions[versions.length - 1].game_versions[0]
: 'None'
: 'None'
}}
</p>
</div>
</div>
<div class="stat">
<EditIcon />
<div class="info">
<h4>Updated</h4>
<p
v-tooltip="
$dayjs(mod.updated).format(
'[Updated on] YYYY-MM-DD [at] HH:mm A'
)
"
class="value"
>
{{ $dayjs(mod.updated).fromNow() }}
</p>
</div>
</div>
<Categories :categories="mod.categories.concat(mod.loaders)" />
</div>
<div>
<div class="section">
<h3>Members</h3>
<div
v-for="member in members"
@@ -77,63 +140,75 @@
<nuxt-link :to="'/user/' + member.user_id">
<h4>{{ member.name }}</h4>
</nuxt-link>
<p>{{ member.role }}</p>
<h3>{{ member.role }}</h3>
</div>
</div>
</div>
<div v-if="versions.length > 0">
<div v-if="versions.length > 0" class="section">
<h3>Featured Versions</h3>
<div
v-for="version in versions"
:key="version.id"
class="featured-version columns"
class="featured-version"
>
<div class="version-info">
<div class="columns">
<h4 class="limit-text-width">
{{ version.name }}
</h4>
<p
<a
:href="findPrimary(version).url"
class="download"
@click.prevent="
downloadFile(
findPrimary(version).hashes.sha1,
findPrimary(version).url
)
"
>
<DownloadIcon />
</a>
<div class="info">
<div class="top">
<span
v-if="version.version_type === 'release'"
class="badge green"
>
Release
</p>
<p v-if="version.version_type === 'beta'" class="badge yellow">
</span>
<span
v-if="version.version_type === 'beta'"
class="badge yellow"
>
Beta
</p>
<p v-if="version.version_type === 'alpha'" class="badge red">
</span>
<span v-if="version.version_type === 'alpha'" class="badge red">
Alpha
</p>
</span>
<h4 class="title">
<nuxt-link :to="'/mod/' + mod.id + '/version/' + version.id">
{{ version.name }}
</nuxt-link>
</h4>
</div>
<div class="columns info-2">
<p class="version-number limit-text-width">
{{ version.version_number }}
</p>
<div class="bottom">
<span class="version-number limit-text-width">
{{ version.version_number }} ·
</span>
<FabricIcon
v-if="version.loaders.includes('fabric')"
stroke="#AC6C3A"
class="loader"
/>
<ForgeIcon
v-if="version.loaders.includes('forge')"
stroke="#8B81E6"
class="loader"
/>
<p
<span
v-if="version.game_versions.length > 0"
class="game-version limit-text-width"
>
{{ version.game_versions[0] }}
</p>
· {{ version.game_versions[0] }}
</span>
</div>
</div>
<nuxt-link :to="'/mod/' + mod.id + '/version/' + version.id">
<DownloadIcon />
</nuxt-link>
</div>
<client-only>
<EthicalAd type="image" />
</client-only>
</div>
<m-footer class="footer" />
</section>
</div>
</div>
@@ -142,11 +217,16 @@
<script>
import EthicalAd from '@/components/EthicalAd'
import Categories from '@/components/Categories'
import MFooter from '@/components/MFooter'
import axios from 'axios'
import CalendarIcon from '~/assets/images/utils/calendar.svg?inline'
import DownloadIcon from '~/assets/images/utils/download.svg?inline'
import EditIcon from '~/assets/images/utils/edit.svg?inline'
import TagIcon from '~/assets/images/utils/tag.svg?inline'
import ExternalIcon from '~/assets/images/utils/external.svg?inline'
import InfoIcon from '~/assets/images/utils/info.svg?inline'
import VersionIcon from '~/assets/images/utils/version.svg?inline'
import SettingsIcon from '~/assets/images/utils/settings.svg?inline'
import ForgeIcon from '~/assets/images/categories/forge.svg?inline'
import FabricIcon from '~/assets/images/categories/fabric.svg?inline'
@@ -154,14 +234,16 @@ import FabricIcon from '~/assets/images/categories/fabric.svg?inline'
export default {
name: 'ModPage',
components: {
MFooter,
Categories,
EthicalAd,
ExternalIcon,
InfoIcon,
VersionIcon,
SettingsIcon,
ForgeIcon,
FabricIcon,
DownloadIcon,
CalendarIcon,
EditIcon,
TagIcon,
},
props: {
mod: {
@@ -183,190 +265,168 @@ export default {
},
},
},
methods: {
formatNumber(x) {
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
},
findPrimary(version) {
let file = version.files.find((x) => x.primary)
if (!file) {
file = version.files[0]
}
if (!file) {
file = { url: `/mod/${this.mod.id}/version/${version.id}` }
}
return file
},
async downloadFile(hash, url) {
await axios.get(
`https://api.modrinth.com/api/v1/version_file/${hash}/download`
)
const elem = document.createElement('a')
elem.download = hash
elem.href = url
elem.click()
},
},
}
</script>
<style lang="scss">
.mod-header {
align-items: center;
img {
border-radius: var(--size-rounded-md);
width: 150px;
height: 150px;
object-fit: cover;
<style lang="scss" scoped>
.header {
@extend %row;
@extend %card-spaced-b;
width: 100%;
.icon {
margin: auto 0;
img {
width: 6rem;
height: 6rem;
margin: var(--spacing-card-md);
border-radius: var(--size-rounded-icon);
object-fit: contain;
}
}
.mod-header-text {
margin-left: 15px;
.info {
@extend %column;
.title {
align-items: end;
h2 {
margin: 0;
}
p {
align-self: flex-end;
margin: 0;
padding: 0 0 2px 5px;
}
margin: var(--spacing-card-md) var(--spacing-card-md) 0 0;
color: var(--color-text-dark);
font-size: var(--font-size-lg);
}
.description {
margin: var(--spacing-card-sm) var(--spacing-card-md) 0 0;
height: 100%;
color: var(--color-text-dark);
}
}
}
.mod-navigation {
display: flex;
margin-top: 20px;
a {
user-select: none;
display: flex;
align-items: center;
padding: 10px 20px;
border-bottom: 2px solid var(--color-grey-2);
svg {
margin-right: 10px;
}
&:hover,
&:focus {
border-bottom: 2px solid var(--color-grey-3);
}
&.nuxt-link-exact-active {
border-bottom: 2px solid var(--color-brand);
}
}
.filler {
flex-grow: 1;
border-bottom: 2px solid var(--color-grey-2);
}
@extend %card-spaced-b;
padding-bottom: 0.2rem;
}
.mod-info {
top: 1rem;
position: sticky;
min-width: 270px;
max-width: 270px;
margin: 1rem;
padding: 0 0.75rem 0 1rem;
overflow-y: auto;
background-color: var(--color-bg);
border: 1px solid var(--color-grey-2);
border-radius: var(--size-rounded-sm);
width: 30rem;
height: auto;
margin-left: var(--spacing-card-lg);
.section {
padding: var(--spacing-card-sm);
@extend %card-spaced-b;
margin-top: var(--spacing-card-lg);
}
h3 {
color: #718096;
font-size: 0.8rem;
letter-spacing: 0.02rem;
margin: 1.5rem 0 0.5rem 0;
text-transform: uppercase;
@extend %large-label;
}
.mod-stats {
display: flex;
flex-wrap: wrap;
margin-top: 0;
margin-left: 5px;
p {
color: var(--color-grey-4);
margin: 3px;
}
.stat {
width: 8.5rem;
margin: 0.75rem;
@extend %stat;
svg {
padding: 0.25rem;
border-radius: 50%;
background-color: var(--color-button-bg);
}
}
}
.team-member {
margin-left: 5px;
margin-bottom: 10px;
border: 1px solid var(--color-grey-1);
border-radius: var(--size-rounded-sm);
img {
border-radius: var(--size-rounded-sm);
border-top-right-radius: 0;
border-bottom-right-radius: 0;
border-radius: var(--size-rounded-icon);
height: 50px;
width: 50px;
}
.member-info {
max-width: 150px;
overflow: hidden;
margin: auto 0 auto 20px;
margin: auto 0 auto 0.5rem;
h4 {
font-weight: normal;
margin: 0;
}
p {
color: var(--color-grey-4);
font-weight: lighter;
font-size: 12pt;
margin: 0;
h3 {
margin-top: 0.1rem;
margin-bottom: 0;
}
}
}
.featured-version {
margin-left: 5px;
margin-bottom: 10px;
border: 1px solid var(--color-grey-1);
border-radius: var(--size-rounded-sm);
.version-info {
padding: 5px 10px;
h4 {
max-width: 120px;
font-weight: normal;
margin: 0 10px 0 0;
}
.badge {
margin: 0;
display: inline-block;
}
.info-2 {
overflow: hidden;
max-width: 180px;
align-items: center;
.version-number {
max-width: 80px;
}
.game-version {
max-width: 120px;
}
p {
color: var(--color-grey-4);
font-weight: lighter;
margin: 0 10px 0 0;
}
svg {
min-width: 24px;
min-height: 24px;
margin-right: 10px;
}
@extend %row;
padding-top: var(--spacing-card-sm);
padding-bottom: var(--spacing-card-sm);
.download {
display: flex;
align-items: center;
height: 2.25rem;
width: 2.25rem;
border-radius: 2rem;
background-color: var(--color-button-bg);
margin-right: var(--spacing-card-sm);
svg {
width: 1.25rem;
margin: auto;
}
}
a {
display: table-cell;
margin-left: auto;
width: 40px;
height: 60px;
background-color: var(--color-grey-1);
color: var(--color-grey-3);
svg {
margin-top: 15px;
height: 30px;
width: 40px;
.info {
@extend %column;
font-size: var(--font-size-xs);
.top {
@extend %row;
.badge {
font-size: var(--font-size-xs);
margin-right: var(--spacing-card-sm);
}
.title {
margin: auto 0;
}
}
&:hover,
&:focus {
background-color: var(--color-grey-3);
color: var(--color-grey-4);
.bottom {
margin-top: 0.25rem;
@extend %row;
.loader {
height: 1rem;
}
}
}
}

View File

@@ -1,464 +0,0 @@
<template>
<div class="result rows">
<img
class="result-icon"
:src="iconUrl ? iconUrl : 'https://cdn.modrinth.com/placeholder.svg'"
:alt="name"
/>
<div class="rows result-name-author">
<h2 class="mod-name">
<a :href="pageUrl">{{ name }}</a>
</h2>
<p v-if="author" class="author">
by <a :href="authorUrl">{{ author }}</a>
</p>
</div>
<p class="result-summary">
{{ description }}
</p>
<div class="column-grow-1 columns result-infos">
<div class="result-image columns">
<DownloadIcon stroke="#3cdb36" />
<p>{{ formatNumber(downloads) }}</p>
</div>
<div
v-tooltip="
$dayjs(createdAt).format('[Created on] YYYY-MM-DD [at] HH:mm A')
"
class="result-image columns"
>
<CalendarIcon fill="#099fef" />
<p>{{ $dayjs(createdAt).fromNow() }}</p>
</div>
<div
v-if="updatedAt"
v-tooltip="
$dayjs(updatedAt).format('[Updated on] YYYY-MM-DD [at] HH:mm A')
"
class="result-image columns"
>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="#e88d0d"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path
d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"
></path>
<path
d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"
></path>
</svg>
<p>{{ $dayjs(updatedAt).fromNow() }}</p>
</div>
<div class="result-image columns">
<svg
viewBox="0 0 24 24"
fill="none"
stroke="#e8200d"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path
d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"
></path>
<line x1="7" y1="7" x2="7.01" y2="7"></line>
</svg>
<p>{{ latestVersion }}</p>
</div>
<div class="loaders columns">
<FabricLoader v-if="categories.includes('fabric')" stroke="#AC6C3A" />
<ForgeLoader v-if="categories.includes('forge')" stroke="#8B81E6" />
</div>
</div>
<div class="categories">
<p v-if="categories.includes('technology')">
<TechCategory />
Technology
</p>
<p v-if="categories.includes('adventure')">
<AdventureCategory />
Adventure
</p>
<p v-if="categories.includes('magic')">
<MagicCategory />
Magic
</p>
<p v-if="categories.includes('utility')">
<UtilityCategory />
Utility
</p>
<p v-if="categories.includes('decoration')">
<DecorationCategory />
Decoration
</p>
<p v-if="categories.includes('library')">
<LibraryCategory />
Library
</p>
<p v-if="categories.includes('cursed')">
<CursedCategory />
Cursed
</p>
<p v-if="categories.includes('worldgen')">
<WorldGenCategory />
Worldgen
</p>
<p v-if="categories.includes('storage')">
<StorageCategory />
Storage
</p>
<p v-if="categories.includes('food')">
<FoodCategory />
Food
</p>
<p v-if="categories.includes('equipment')">
<EquipmentCategory />
Equipment
</p>
<p v-if="categories.includes('misc')">
<MiscCategory />
Misc
</p>
</div>
</div>
</template>
<script>
import CalendarIcon from '~/assets/images/utils/calendar.svg?inline'
import DownloadIcon from '~/assets/images/utils/download.svg?inline'
import TechCategory from '~/assets/images/categories/tech.svg?inline'
import AdventureCategory from '~/assets/images/categories/adventure.svg?inline'
import CursedCategory from '~/assets/images/categories/cursed.svg?inline'
import DecorationCategory from '~/assets/images/categories/decoration.svg?inline'
import EquipmentCategory from '~/assets/images/categories/equipment.svg?inline'
import FoodCategory from '~/assets/images/categories/food.svg?inline'
import LibraryCategory from '~/assets/images/categories/library.svg?inline'
import MagicCategory from '~/assets/images/categories/magic.svg?inline'
import MiscCategory from '~/assets/images/categories/misc.svg?inline'
import StorageCategory from '~/assets/images/categories/storage.svg?inline'
import UtilityCategory from '~/assets/images/categories/utility.svg?inline'
import WorldGenCategory from '~/assets/images/categories/worldgen.svg?inline'
import ForgeLoader from '~/assets/images/categories/forge.svg?inline'
import FabricLoader from '~/assets/images/categories/fabric.svg?inline'
export default {
name: 'ModResult',
components: {
TechCategory,
AdventureCategory,
CursedCategory,
DecorationCategory,
EquipmentCategory,
FoodCategory,
LibraryCategory,
MagicCategory,
MiscCategory,
StorageCategory,
UtilityCategory,
WorldGenCategory,
ForgeLoader,
FabricLoader,
CalendarIcon,
DownloadIcon,
},
props: {
id: {
type: String,
default: 'modrinth-0',
},
name: {
type: String,
default: 'Mod Name',
},
author: {
type: String,
default: null,
},
description: {
type: String,
default: 'A mod description',
},
pageUrl: {
type: String,
default: '#',
},
authorUrl: {
type: String,
default: '#',
},
iconUrl: {
type: String,
default: '#',
},
downloads: {
type: String,
default: '0',
},
createdAt: {
type: String,
default: '0000-00-00',
},
updatedAt: {
type: String,
default: null,
},
latestVersion: {
type: String,
default: 'None',
},
categories: {
type: Array,
default() {
return []
},
},
},
methods: {
formatNumber(x) {
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
},
},
}
</script>
<style lang="scss">
.results {
margin-top: 10px;
}
.result {
display: grid;
grid-template-columns: 80px auto;
grid-template-rows: auto auto auto 30px;
max-width: 100vw;
margin-bottom: 10px;
background: var(--color-bg);
box-shadow: 0 2px 3px 1px var(--color-grey-2);
// Columns are larger to accomodate larger screens
@media screen and (min-width: 1375px) {
grid-template-columns: 120px auto;
}
}
.result * {
object-fit: contain;
margin-bottom: 0;
margin-top: 0;
}
.result-icon {
width: 60px;
height: 60px;
margin: 5px !important;
border-radius: 0.5rem;
grid-row-start: 1;
grid-row-end: 4;
grid-column-start: 1;
@media screen and (min-width: 900px) {
margin: 10px 20px 10px 10px !important;
width: 70px;
height: 70px;
}
// Larger screen, larger icon
@media screen and (min-width: 1375px) {
width: 90px;
height: 90px;
}
}
.result-name-author {
display: block;
margin-top: 10px;
min-height: 38px;
}
.result-summary {
grid-row: 2;
grid-column: 2;
max-height: 150px;
font-size: 11pt;
margin: auto 0;
}
.mod-name {
align-self: flex-end;
font-size: 13pt;
}
.author {
margin-bottom: 2px !important;
align-self: flex-end;
font-size: 12pt;
}
.result-infos {
display: grid;
grid-template-columns: 115px 115px auto;
grid-template-rows: 20px 20px;
margin-top: 5px;
grid-column: 2;
align-items: flex-start;
align-self: flex-start;
.columns:nth-child(2) {
grid-column: 1;
}
.columns:nth-child(3) {
grid-column: 2;
grid-row: 1;
}
.result-image {
p {
font-size: 10pt;
}
svg {
width: 14px;
height: 14px;
}
}
}
.result-image svg {
width: 15px;
height: 15px;
align-self: center;
}
.result-image p {
margin-left: 5px;
margin-right: 15px;
font-size: 15px;
align-self: center;
}
.categories {
display: flex;
flex-direction: row;
grid-column: 1;
margin: 0 0 auto;
}
.categories p {
display: flex;
align-items: center;
flex-direction: row;
background-color: var(--color-grey-1);
color: var(--color-text);
margin: 0 5px;
padding: 2px;
font-size: 15px;
svg {
width: 15px;
margin-right: 5px;
}
}
.loaders {
align-items: center;
grid-column: 1;
grid-row: 3;
img {
width: 15px;
}
svg {
width: 20px;
margin-top: 2px;
margin-left: 5px;
}
}
// Larger tablet-sized screens
@media screen and (min-width: 900px) {
.result {
grid-template-columns: 90px auto;
grid-template-rows: auto auto 35px;
}
.result-infos {
display: flex;
align-items: center;
margin-top: 0;
.result-image {
p {
font-size: 15px;
}
svg {
width: 15px;
height: 15px;
}
}
}
.loaders {
svg {
width: 20px;
}
}
.categories {
margin: 0 0 10px 0;
}
.result-name-author {
display: flex;
margin-top: 0;
.author {
margin-left: 5px;
}
}
.mod-name {
font-size: 18pt;
}
.result-summary {
max-height: 100px;
font-size: 13pt;
}
}
// Larger screens
@media screen and (min-width: 1375px) {
.result {
margin: 5px 0;
grid-column: 1;
grid-template-columns: 110px auto;
}
.categories {
margin: 0 5px 10px auto;
grid-row: 3;
grid-column: 3;
}
.mod-name {
font-size: 20pt;
}
}
// Desktop
@media screen and (min-width: 1500px) {
.result-name-author {
display: flex;
min-height: 38px;
grid-column: 2;
}
}
</style>

View File

@@ -1,59 +1,57 @@
<template>
<div v-if="pages.length > 1" class="columns paginates">
<svg
:class="{
'disabled-paginate': currentPage === 1,
'active-paginate': currentPage !== 1,
}"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
<button
:class="{ disabled: currentPage === 1 }"
class="paginate has-icon"
@click="currentPage !== 1 ? switchPage(currentPage - 1) : null"
>
<polyline points="15 18 9 12 15 6"></polyline>
</svg>
<p
<LeftArrowIcon />
</button>
<div
v-for="(item, index) in pages"
:key="'page-' + item"
:class="{
'active-page-number': currentPage !== item,
'page-number': currentPage !== item,
}"
@click="currentPage !== item ? switchPage(item) : null"
class="page-number-container"
>
<span v-if="pages[index - 1] + 1 !== item && item !== 1">...</span>
<span :class="{ 'disabled-page-number': currentPage === item }">{{
item
}}</span>
</p>
<div v-if="pages[index - 1] + 1 !== item && item !== 1" class="has-icon">
<GapIcon />
</div>
<button
:class="{ 'page-number current': currentPage === item }"
@click="currentPage !== item ? switchPage(item) : null"
>
{{ item }}
</button>
</div>
<svg
:class="{
'disabled-paginate': currentPage === pages[pages.length - 1],
'active-paginate': currentPage !== pages[pages.length - 1],
}"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
<button
:class="{ disabled: currentPage === pages[pages.length - 1] }"
class="paginate has-icon"
@click="
currentPage !== pages[pages.length - 1]
? switchPage(currentPage + 1)
: null
"
>
<polyline points="9 18 15 12 9 6"></polyline>
</svg>
<RightArrowIcon />
</button>
</div>
</template>
<script>
import GapIcon from '~/assets/images/utils/gap.svg?inline'
import LeftArrowIcon from '~/assets/images/utils/left-arrow.svg?inline'
import RightArrowIcon from '~/assets/images/utils/right-arrow.svg?inline'
export default {
name: 'Pagination',
components: {
GapIcon,
LeftArrowIcon,
RightArrowIcon,
},
props: {
currentPage: {
type: Number,
@@ -74,32 +72,41 @@ export default {
}
</script>
<style scoped>
.paginates {
<style scoped lang="scss">
button {
min-width: 2rem;
padding: 0 0.5rem;
height: 2rem;
border-radius: 2rem;
background: transparent;
&.page-number.current {
background: var(--color-button-bg-hover);
color: var(--color-button-text-hover);
cursor: default;
}
&.paginate.disabled {
background: none;
color: var(--color-button-text-disabled);
cursor: default;
}
&:hover {
background: var(--color-button-bg-active);
color: var(--color-button-text-active);
}
}
.has-icon {
display: flex;
align-items: center;
padding: 0 0.5rem;
height: 2rem;
svg {
width: 1rem;
}
}
.paginates p {
margin-left: 5px;
margin-right: 5px;
}
.disabled-paginate {
cursor: default;
color: var(--color-grey-5);
}
.active-page-number,
.active-paginate {
user-select: none;
cursor: pointer;
}
.disabled-page-number {
user-select: none;
cursor: default;
padding: 2px 3px;
border-radius: 3px;
background-color: var(--color-grey-1);
.page-number-container {
display: flex;
max-height: 2rem;
}
</style>

View File

@@ -27,7 +27,7 @@ export default {
position: fixed;
width: 100%;
height: 100%;
background-color: var(--color-grey-3);
background-color: var(--color-button-bg);
border: none;
opacity: 0.6;
overflow-x: hidden;
@@ -39,7 +39,7 @@ export default {
left: 50%;
transform: translate(-50%, -50%);
z-index: 2;
box-shadow: 0 2px 3px 1px var(--color-grey-2);
box-shadow: 0 2px 3px 1px var(--color-button-bg);
padding: 5px 60px 5px 20px;
border-radius: 10px;
max-height: 80%;

275
components/ProjectCard.vue Normal file
View File

@@ -0,0 +1,275 @@
<template>
<div class="project-card">
<div class="icon">
<img
:src="iconUrl ? iconUrl : 'https://cdn.modrinth.com/placeholder.svg'"
:alt="name"
/>
</div>
<div class="info">
<div class="top">
<h2 class="title">
<nuxt-link v-if="isModrinth" :to="'/mod/' + id">{{ name }}</nuxt-link>
<a v-else :href="pageUrl">{{ name }}</a>
</h2>
<p v-if="author" class="author">
by <a :href="authorUrl">{{ author }}</a>
</p>
</div>
<p class="description">
{{ description }}
</p>
<div :class="{ vertical: editMode }" class="bottom">
<div class="stats">
<div v-if="status !== null" class="stat">
<div class="info">
<h4>Status</h4>
<span v-if="status === 'approved'" class="badge green">
Approved
</span>
<span v-if="status === 'rejected'" class="badge red">
Rejected
</span>
<span v-if="status === 'draft'" class="badge yellow">Draft</span>
<span v-if="status === 'processing'" class="badge yellow">
Processing
</span>
<span v-if="status === 'unlisted'" class="badge gray">
Unlisted
</span>
<span v-if="status === 'unknown'" class="badge gray">
Unknown
</span>
</div>
</div>
<div class="stat">
<DownloadIcon />
<div class="info">
<h4>Downloads</h4>
<p class="value">{{ formatNumber(downloads) }}</p>
</div>
</div>
<div class="stat">
<CalendarIcon />
<div class="info">
<h4>Created</h4>
<p
v-tooltip="
$dayjs(createdAt).format(
'[Created on] YYYY-MM-DD [at] HH:mm A'
)
"
class="value"
>
{{ $dayjs(createdAt).fromNow() }}
</p>
</div>
</div>
<div class="stat">
<EditIcon />
<div class="info">
<h4>Updated</h4>
<p
v-tooltip="
$dayjs(updatedAt).format(
'[Updated on] YYYY-MM-DD [at] HH:mm A'
)
"
class="value"
>
{{ $dayjs(updatedAt).fromNow() }}
</p>
</div>
</div>
<div v-if="latestVersion" class="stat">
<TagIcon />
<div class="info">
<h4>Available For</h4>
<p class="value">
{{ latestVersion }}
</p>
</div>
</div>
</div>
<Categories :categories="categories" />
</div>
</div>
<div v-if="editMode" class="buttons">
<nuxt-link class="transparent-button column" :to="'/mod/' + id + '/edit'">
Edit
</nuxt-link>
</div>
</div>
</template>
<script>
import Categories from '@/components/Categories'
import CalendarIcon from '~/assets/images/utils/calendar.svg?inline'
import DownloadIcon from '~/assets/images/utils/download.svg?inline'
import EditIcon from '~/assets/images/utils/edit.svg?inline'
import TagIcon from '~/assets/images/utils/tag.svg?inline'
export default {
name: 'ProjectCard',
components: {
Categories,
CalendarIcon,
DownloadIcon,
EditIcon,
TagIcon,
},
props: {
id: {
type: String,
default: 'modrinth-0',
},
name: {
type: String,
default: 'Mod Name',
},
author: {
type: String,
default: null,
},
description: {
type: String,
default: 'A mod description',
},
pageUrl: {
type: String,
default: '#',
},
authorUrl: {
type: String,
default: '#',
},
iconUrl: {
type: String,
default: '#',
},
downloads: {
type: String,
default: '0',
},
createdAt: {
type: String,
default: '0000-00-00',
},
updatedAt: {
type: String,
default: null,
},
latestVersion: {
type: String,
default: null,
},
categories: {
type: Array,
default() {
return []
},
},
editMode: {
type: Boolean,
default: false,
},
status: {
type: String,
default: null,
},
role: {
type: String,
default: null,
},
isModrinth: {
type: Boolean,
default: false,
},
},
methods: {
formatNumber(x) {
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
},
},
}
</script>
<style lang="scss" scoped>
.project-card {
@extend %row;
@extend %card-spaced-b;
width: 100%;
.icon {
margin: auto 0;
img {
width: 6rem;
height: 6rem;
margin: var(--spacing-card-md);
border-radius: var(--size-rounded-icon);
object-fit: contain;
}
}
.info {
@extend %column;
.top {
@extend %row;
flex-wrap: wrap;
margin-top: var(--spacing-card-md);
margin-right: var(--spacing-card-md);
.title {
margin: 0;
color: var(--color-text-dark);
font-size: var(--font-size-lg);
}
.author {
margin: auto 0 0 0.5rem;
color: var(--color-text);
}
}
.description {
margin: var(--spacing-card-sm) var(--spacing-card-md) 0 0;
height: 100%;
color: var(--color-text-dark);
}
.bottom {
@extend %column;
margin-top: var(--spacing-card-sm);
margin-right: var(--spacing-card-md);
margin-bottom: var(--spacing-card-md);
@media screen and (min-width: 1024px) {
flex-direction: row;
&.vertical {
flex-direction: column;
.categories {
margin-top: var(--spacing-card-sm);
}
}
}
.stats {
@extend %row;
flex-wrap: wrap;
@media screen and (min-width: 900px) {
flex-wrap: nowrap;
}
.stat {
@extend %stat;
}
}
.categories {
@media screen and (min-width: 1024px) {
flex-direction: row;
margin: auto 0;
}
}
}
}
.buttons {
@extend %column;
}
}
</style>

View File

@@ -1,7 +1,10 @@
<template>
<p
class="filter"
:class="{ 'filter-active': activeFilters.includes(facetName) }"
:class="{
'filter-active': activeFilters.includes(facetName),
cursed: displayName == 'FlameAnvil',
}"
@click="toggle"
>
<slot></slot>
@@ -41,30 +44,28 @@ export default {
display: flex;
align-items: center;
cursor: pointer;
padding: 2px 2px 2px 20px;
margin: 0 0 0 5px;
border-left: 4px solid var(--color-grey-3);
border-radius: 0 0.25rem 0.25rem 0;
color: var(--color-grey-5);
padding: 0.4rem 0.3rem;
margin: 3px 0 0 0.5rem;
font-size: 1rem;
letter-spacing: 0.02rem;
@extend %transparent-clickable;
@media screen and (min-width: 1024px) {
padding: 0.2rem 0.3rem;
}
svg {
color: var(--color-icon);
margin-right: 5px;
height: 1rem;
flex-shrink: 0;
}
&:hover,
&:focus {
background-color: var(--color-grey-1);
color: var(--color-text);
}
}
.filter-active {
background-color: var(--color-grey-1);
color: var(--color-text);
border-left: 4px solid var(--color-brand);
@extend %transparent-clickable.selected;
svg {
color: var(--color-brand-light);
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -10,10 +10,19 @@
<script>
export default {
props: ['error'],
props: {
error: {
type: Object,
default() {
return {
message: 'Unknown error',
}
},
},
},
layout: 'home',
created() {
console.log(this.error)
// console.log(this.error)
},
}
</script>

View File

@@ -1,144 +0,0 @@
<template>
<div>
<header class="columns">
<nuxt-link to="/">
<img class="logo" src="~/assets/images/logo.svg" alt="logo" />
</nuxt-link>
<div class="links">
<nuxt-link to="/">Home</nuxt-link>
<nuxt-link to="/mods">Mods</nuxt-link>
<nuxt-link to="/modpacks">Packs</nuxt-link>
<nuxt-link to="/about">About</nuxt-link>
<a href="https://discord.gg/gFRbNQ2">Discord</a>
</div>
</header>
<nuxt />
<footer>
<div class="logo-wrapper columns">
<img class="logo" src="~/assets/images/logo.svg" alt="logo" />
<p class="name">modrinth</p>
</div>
<p class="copyright">© Guavy LLC</p>
<div class="column">
<h4 class="pages">Pages</h4>
<nuxt-link to="/">Home</nuxt-link>
<nuxt-link to="/mods">Mods</nuxt-link>
<nuxt-link to="/modpacks">Packs</nuxt-link>
<nuxt-link to="/about">About</nuxt-link>
<nuxt-link to="/guides">Guides</nuxt-link>
</div>
<div class="column">
<h4 class="developers">Developers</h4>
<nuxt-link to="/documentation">Documentation</nuxt-link>
<a href="https://github.com/modrinth">Open Source</a>
<a href="https://github.com/modrinth/knossos/issues">Issues</a>
</div>
<div class="column legal-wrapper">
<h4 class="legal">Legal</h4>
<nuxt-link to="/tos">TOS</nuxt-link>
<nuxt-link to="/privacy">Privacy Policy</nuxt-link>
</div>
</footer>
</div>
</template>
<script>
export default {}
</script>
<style lang="scss" scoped>
header {
width: 100%;
.logo {
margin: 25px 50px;
height: 100px;
}
.links {
margin: auto 0;
a {
text-transform: uppercase;
font-weight: bold;
margin: 0 25px;
&:hover,
&:focus {
background-color: var(--color-grey-1);
color: var(--color-text);
}
&.nuxt-link-exact-active {
border-bottom: 3px var(--color-brand) solid;
}
}
}
}
footer {
justify-content: center;
margin-top: 250px;
margin-bottom: 100px;
display: grid;
column-gap: 50px;
grid-template-columns: 175px 120px 120px 175px;
grid-template-rows: 20px 20px 20px 20px 20px 20px 20px 20px 20px 20px 20px 20px;
.logo-wrapper {
grid-column-start: 1;
grid-row-start: 1;
.logo {
width: 30px;
}
.name {
place-self: center;
margin: 0;
font-size: 20px;
font-family: 'Montserrat Alternates', serif;
}
}
.copyright {
grid-column-start: 1;
grid-row-start: 10;
color: var(--color-grey-6);
}
h4 {
font-weight: 600;
margin: 0 0 20px;
grid-row-start: 1;
}
.column {
display: grid;
row-gap: 5px;
grid-template-rows: 40px 20px 20px 20px 20px 20px 20px 20px;
a {
&:hover,
&:focus {
color: var(--color-grey-5);
}
}
}
}
@media screen and (max-width: 550px) {
footer {
column-gap: 10px;
margin-left: 300px !important;
}
.legal-wrapper {
display: none !important;
}
}
@media screen and (max-width: 800px) {
footer {
margin-left: 200px;
}
.copyright,
.logo-wrapper {
display: none;
}
}
</style>

View File

@@ -20,7 +20,7 @@ export default {
hid: 'description',
name: 'description',
content:
'Modrinth is a mod distribution platform. Modrinth is modern, easy to use, and built for modders. Modrinth currently supports Minecraft, including the forge and fabric mod loaders.',
'Modrinth is a mod distribution platform. Modrinth is modern, easy to use, and built for modders. Modrinth currently supports Minecraft, including Forge and Fabric mod loaders.',
},
{ hid: 'publisher', name: 'publisher', content: 'Guavy LLC' },
@@ -53,12 +53,7 @@ export default {
{
rel: 'stylesheet',
href:
'https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap',
},
{
rel: 'stylesheet',
href:
'https://fonts.googleapis.com/css2?family=Montserrat+Alternates:wght@600&display=swap',
'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700;800&display=swap',
},
],
script: [
@@ -78,7 +73,7 @@ export default {
vue: {
config: {
productionTip: false,
devtools: false,
devtools: true,
},
},
@@ -96,6 +91,7 @@ export default {
plugins: [
'~/plugins/vue-tooltip.js',
'~/plugins/vue-notification.js',
'~/plugins/compiled-markdown-directive.js',
'~/plugins/vue-syntax.js',
],
/*
@@ -165,6 +161,9 @@ export default {
*/
build: {
transpile: ['vue-tooltip', 'vue-notification'],
styleResources: {
scss: './assets/styles/injected.scss',
}
},
loading: {
color: 'green',

5
package-lock.json generated
View File

@@ -12762,6 +12762,11 @@
"resolved": "https://registry.npmjs.org/vue/-/vue-2.6.12.tgz",
"integrity": "sha512-uhmLFETqPPNyuLLbsKz6ioJ4q7AZHzD8ZVFNATNyICSZouqP2Sz0rotWQC8UNBF6VGSCs5abnKJoStA6JbCbfg=="
},
"vue-click-outside": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/vue-click-outside/-/vue-click-outside-1.1.0.tgz",
"integrity": "sha512-pNyvAA9mRXJwPHlHJyjMb4IONSc7khS5lxGcMyE2EIKgNMAO279PWM9Hyq0d5J4FkiSRdmFLwnbjDd5UtPizHQ=="
},
"vue-client-only": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/vue-client-only/-/vue-client-only-2.0.0.tgz",

View File

@@ -21,6 +21,7 @@
"marked": "^1.2.0",
"nuxt": "^2.14.7",
"v-tooltip": "^2.0.3",
"vue-click-outside": "^1.1.0",
"vue-highlightjs": "^1.3.3",
"vue-multiselect": "^2.1.6",
"vue-notification": "^1.3.20",

View File

@@ -1,28 +1,35 @@
<template>
<div class="main">
<h1>About</h1>
<p>
Founded in 2020, Modrinth was created to provide modders with an open and
intuitive platform to publish their mods on.
</p>
<div class="container">
<h1>About</h1>
<p>
Founded in 2020, Modrinth was created to provide modders with an open
and intuitive platform to publish their mods on.
</p>
<p>
Our primary goal is to be as open as possible, with all our code being
Open Source, while giving back to the modding community as much as
possible.
</p>
<p>
Our primary goal is to be as open as possible, with all our code being
Open Source, while giving back to the modding community as much as
possible.
</p>
<p>
While we still are in early alpha, we hope we can soon be a major modding
platform for all modders :)
</p>
<p>
While we still are in early alpha, we hope we can soon be a major
modding platform for all modders :)
</p>
</div>
<m-footer class="footer" centered />
</div>
</template>
<script>
import MFooter from '@/components/MFooter'
export default {
components: {
MFooter,
},
auth: false,
layout: 'home',
head: {
title: 'About - Modrinth',
meta: [
@@ -54,7 +61,12 @@ export default {
<style lang="scss" scoped>
.main {
margin: 0 auto;
margin: var(--spacing-card-sm) auto;
max-width: 800px;
}
.container {
@extend %card;
padding: var(--spacing-card-sm) var(--spacing-card-lg);
}
</style>

View File

@@ -1,80 +1,80 @@
<template>
<div class="content">
<h2>My projects</h2>
<div class="section-header">
<h3>Mods</h3>
<nuxt-link class="create-button" to="/mod/create"
>Create a new mod</nuxt-link
>
<div class="page-container">
<div class="page-contents">
<div class="sidebar-l">
<div class="card page-nav">
<nuxt-link :to="'/dashboard/projects'" class="tab last">
<ModIcon />
My mods
</nuxt-link>
</div>
<client-only>
<EthicalAd type="image" />
</client-only>
</div>
<div class="content">
<div class="section-header columns">
<h3 class="column-grow-1">My mods</h3>
<nuxt-link class="brand-button column" to="/mod/create">
Create a mod
</nuxt-link>
</div>
<ModCard
v-for="mod in mods"
:id="mod.id"
:key="mod.id"
:author="mod.author"
:name="mod.title"
:description="mod.description"
:latest-version="mod.latest_version"
:created-at="mod.published"
:updated-at="mod.updated"
:downloads="mod.downloads.toString()"
:icon-url="mod.icon_url"
:author-url="mod.author_url"
:page-url="mod.page_url"
:categories="mod.categories"
:edit-mode="true"
:status="mod.status"
:is-modrinth="true"
/>
</div>
</div>
<table>
<thead>
<tr>
<th></th>
<th>Name</th>
<th>Role</th>
<th>Status</th>
<th>Downloads</th>
<th>Last updated</th>
</tr>
</thead>
<tbody>
<tr v-for="mod in mods" :key="mod.id">
<td>
<img class="rounded-md" :src="mod.icon_url" />
</td>
<td>
<nuxt-link :to="'/mod/' + mod.id">
{{ mod.title }}
</nuxt-link>
</td>
<td>Owner</td>
<td>
<span v-if="mod.status === 'approved'" class="badge green">
Approved
</span>
<span v-if="mod.status === 'rejected'" class="badge red">
Rejected
</span>
<span v-if="mod.status === 'draft'" class="badge yellow">
Draft
</span>
<span v-if="mod.status === 'processing'" class="badge yellow">
Processing
</span>
<span v-if="mod.status === 'unlisted'" class="badge gray">
Unlisted
</span>
<span v-if="mod.status === 'unknown'" class="badge gray">
Unknown
</span>
</td>
<td>{{ mod.downloads }}</td>
<td>{{ $dayjs(mod.published).format('YYYY-MM-DD') }}</td>
</tr>
</tbody>
</table>
<client-only>
<EthicalAd />
</client-only>
</div>
</template>
<script>
import axios from 'axios'
import EthicalAd from '@/components/EthicalAd'
import ModCard from '@/components/ProjectCard'
import ModIcon from '~/assets/images/sidebar/mod.svg?inline'
export default {
components: { EthicalAd },
components: {
EthicalAd,
ModCard,
ModIcon,
},
async fetch() {
const config = {
headers: {
Authorization: this.$auth.getToken('local'),
},
}
try {
let res = await axios.get(
`https://api.modrinth.com/api/v1/user/${this.$auth.user.id}/mods`
`https://api.modrinth.com/api/v1/user/${this.$auth.user.id}/mods`,
config
)
if (res.data) {
res = await axios.get(
`https://api.modrinth.com/api/v1/mods?ids=${JSON.stringify(res.data)}`
`https://api.modrinth.com/api/v1/mods?ids=${JSON.stringify(
res.data
)}`,
config
)
this.mods = res.data
}
@@ -90,29 +90,23 @@ export default {
<style lang="scss" scoped>
.section-header {
display: flex;
margin-bottom: 1rem;
& > * {
margin-right: 1rem;
@extend %card;
padding: var(--spacing-card-md) var(--spacing-card-lg);
margin-bottom: var(--spacing-card-md);
h3 {
margin: auto 0;
color: var(--color-text-dark);
font-weight: var(--font-weight-extrabold);
}
}
.create-button {
margin: auto 0;
padding: 4px 20px;
border-radius: 5px;
color: var(--color-grey-5);
background-color: var(--color-grey-1);
}
table {
background: var(--color-bg);
border-collapse: collapse;
border-radius: 0.5rem;
box-shadow: 0 2px 3px 1px var(--color-grey-2);
table-layout: fixed;
width: 100%;
margin-bottom: var(--spacing-card-md);
background: var(--color-raised-bg);
border-radius: var(--size-rounded-card);
* {
text-align: left;
@@ -122,7 +116,7 @@ table {
tr:first-child {
th,
td {
border-bottom: 1px solid var(--color-grey-2);
border-bottom: 1px solid var(--color-divider);
}
}
@@ -130,7 +124,7 @@ table {
td {
&:first-child {
text-align: center;
width: 5%;
width: 7%;
}
&:nth-child(2) {
@@ -140,7 +134,7 @@ table {
}
th {
color: #718096;
color: var(--color-heading);
font-size: 0.8rem;
letter-spacing: 0.02rem;
margin-bottom: 0.5rem;
@@ -150,7 +144,7 @@ table {
}
td {
padding: 0.25rem 1rem;
padding: 0.75rem;
img {
height: 3rem;
@@ -185,4 +179,8 @@ table {
}
}
}
.mod-name {
font-weight: bold;
}
</style>

View File

@@ -1,19 +0,0 @@
<template>
<div class="content">
<h2>Documentation</h2>
<p>Coming Soon!</p>
</div>
</template>
<script>
export default {
auth: false,
}
</script>
<style lang="scss" scoped>
.main {
margin: 0 auto;
max-width: 800px;
}
</style>

View File

@@ -2,7 +2,9 @@
<div>
<div class="main-hero columns">
<div class="left">
<h1 class="typewriter">{{ currentText }}</h1>
<h1 class="typewriter">
{{ currentText }}<span aria-hidden="true"></span>
</h1>
<h1>modding platform</h1>
</div>
<div class="right columns">
@@ -159,13 +161,18 @@ fetch('https://api.modrinth.com/api/v1/mod').then(res => res.json()).then(data =
</pre>
</div>
</div>
<m-footer class="footer" centered />
</div>
</template>
<script>
import MFooter from '@/components/MFooter'
export default {
components: {
MFooter,
},
auth: false,
layout: 'home',
data() {
return {
currentText: 'Open source',
@@ -208,6 +215,7 @@ export default {
}
.main-hero {
margin-top: 100px;
height: 600px;
.left {
@@ -217,12 +225,15 @@ export default {
.typewriter {
display: inline-block;
color: var(--color-brand);
border-right: 0.15em solid var(--color-brand);
animation: caret 1s steps(1) infinite;
@keyframes caret {
50% {
border-color: transparent;
span {
border-right: 0.15em solid var(--color-brand);
animation: caret 1s steps(1) infinite;
@keyframes caret {
50% {
border-color: transparent;
}
}
}
}
@@ -256,7 +267,7 @@ export default {
p {
line-height: 25px;
letter-spacing: 0.2px;
color: var(--color-grey-7);
color: var(--color-text);
span {
color: var(--color-brand);
@@ -276,7 +287,7 @@ export default {
}
.slanted-hero {
background: var(--color-grey-1);
background: var(--color-raised-bg);
height: 500px;
position: relative;
z-index: 1;
@@ -336,6 +347,10 @@ export default {
}
}
.footer {
margin-top: 150px;
}
@media screen and (max-width: 500px) {
.hero {
margin-top: 0 !important;

191
pages/legal/privacy.vue Normal file
View File

@@ -0,0 +1,191 @@
<template>
<div class="main">
<div class="container">
<h1>Privacy Policy</h1>
<p>
At Modrinth, accessible from https://modrinth.com, one of our main
priorities is the privacy of our visitors. This Privacy Policy document
contains types of information that is collected and recorded by Modrinth
and how we use it.
</p>
<p>
If you have additional questions or require more information about our
Privacy Policy, do not hesitate to contact us.
</p>
<h2>Log Files</h2>
<p>
Modrinth follows a standard procedure of using log files. These files
log visitors when they visit websites. All hosting companies do this and
a part of hosting services' analytics. The information collected by log
files include internet protocol (IP) addresses, browser type, Internet
Service Provider (ISP), date and time stamp, referring/exit pages, and
possibly the number of clicks. These are not linked to any information
that is personally identifiable. The purpose of the information is for
analyzing trends, administering the site, tracking users' movement on
the website, and gathering demographic information.
</p>
<h2>Cookies and Web Beacons</h2>
<p>
Like any other website, Modrinth uses 'cookies'. These cookies are used
to store information including visitors' preferences, and the pages on
the website that the visitor accessed or visited. The information is
used to optimize the users' experience by customizing our web page
content based on visitors' browser type and/or other information.
</p>
<p>
For more general information on cookies, please read
<a href="https://www.privacypolicies.com/blog/cookies/"
>"What Are Cookies"</a
>.
</p>
<h2>Our Advertising Partners</h2>
<p>
Some of advertisers on our site may use cookies and web beacons. Our
advertising partners are listed below. Each of our advertising partners
has their own Privacy Policy for their policies on user data. For easier
access, we hyperlinked to their Privacy Policies below.
</p>
<ul>
<li>
<p>EthicalAds</p>
<p>
<a href="https://www.ethicalads.io/privacy-policy/"
>https://www.ethicalads.io/privacy-policy/</a
>
</p>
</li>
</ul>
<h2>Privacy Policies</h2>
<P
>You may consult this list to find the Privacy Policy for each of the
advertising partners of Modrinth.</P
>
<p>
Third-party ad servers or ad networks uses technologies like cookies,
JavaScript, or Web Beacons that are used in their respective
advertisements and links that appear on Modrinth, which are sent
directly to users' browser. They automatically receive your IP address
when this occurs. These technologies are used to measure the
effectiveness of their advertising campaigns and/or to personalize the
advertising content that you see on websites that you visit.
</p>
<p>
Note that Modrinth has no access to or control over these cookies that
are used by third-party advertisers.
</p>
<h2>Third Party Privacy Policies</h2>
<p>
Modrinth's Privacy Policy does not apply to other advertisers or
websites. Thus, we are advising you to consult the respective Privacy
Policies of these third-party ad servers for more detailed information.
It may include their practices and instructions about how to opt-out of
certain options.
</p>
<p>
You can choose to disable cookies through your individual browser
options. To know more detailed information about cookie management with
specific web browsers, it can be found at the browsers' respective
websites. What Are Cookies?
</p>
<h2>Children's Information</h2>
<p>
Another part of our priority is adding protection for children while
using the internet. We encourage parents and guardians to observe,
participate in, and/or monitor and guide their online activity.
</p>
<p>
Modrinth does not knowingly collect any Personal Identifiable
Information from children under the age of 13. If you think that your
child provided this kind of information on our website, we strongly
encourage you to contact us immediately and we will do our best efforts
to promptly remove such information from our records.
</p>
<h2>Online Privacy Policy Only</h2>
<p>
This Privacy Policy applies only to our online activities and is valid
for visitors to our website with regards to the information that they
shared and/or collect in Modrinth. This policy is not applicable to any
information collected offline or via channels other than this website.
</p>
<h2>Consent</h2>
<p>
By using our website, you hereby consent to our Privacy Policy and agree
to its Terms and Conditions.
</p>
</div>
<m-footer class="footer" centered />
</div>
</template>
<script>
import MFooter from '@/components/MFooter'
export default {
components: {
MFooter,
},
auth: false,
head: {
title: 'Privacy - Modrinth',
meta: [
{
hid: 'description',
name: 'description',
content:
'The privacy policy of Modrinth, an open source modding platform. Modrinth currently supports Minecraft, including the forge and fabric mod loaders.',
},
{
hid: 'apple-mobile-web-app-title',
name: 'apple-mobile-web-app-title',
content: 'Privacy Policy',
},
{
hid: 'og:title',
name: 'og:title',
content: 'Privacy Policy',
},
{
hid: 'og:url',
name: 'og:url',
content: `https://modrinth.com/privacy`,
},
],
},
}
</script>
<style lang="scss" scoped>
.main {
margin: var(--spacing-card-sm) auto;
max-width: 800px;
}
.container {
@extend %card;
padding: var(--spacing-card-sm) var(--spacing-card-lg);
}
</style>

203
pages/legal/terms.vue Normal file
View File

@@ -0,0 +1,203 @@
<template>
<div class="main">
<div class="container">
<h1>Terms and Conditions</h1>
<h2>1. Terms</h2>
<p>
By accessing this Website, accessible from https://modrinth.com, you are
agreeing to be bound by these Website Terms and Conditions of Use and
agree that you are responsible for the agreement with any applicable
local laws. If you disagree with any of these terms, you are prohibited
from accessing this site. The materials contained in this Website are
protected by copyright and trade mark law.
</p>
<h2>2. Use License</h2>
<p>
Permission is granted to temporarily download one copy of the materials
on Guavy LLC's Website for personal, non-commercial transitory viewing
only. This is the grant of a license, not a transfer of title, and under
this license you may not:
</p>
<ul>
<li>modify or copy the materials;</li>
<li>
use the materials for any commercial purpose or for any public
display;
</li>
<li>
attempt to reverse engineer any software contained on Guavy LLC's
Website;
</li>
<li>
remove any copyright or other proprietary notations from the
materials; or
</li>
<li>
transferring the materials to another person or "mirror" the materials
on any other server.
</li>
</ul>
<p>
This will let Guavy LLC to terminate upon violations of any of these
restrictions. Upon termination, your viewing right will also be
terminated and you should destroy any downloaded materials in your
possession whether it is printed or electronic format.
</p>
<h2>3. Disclaimer</h2>
<p>
All the materials on Guavy LLCs Website are provided "as is". Guavy LLC
makes no warranties, may it be expressed or implied, therefore negates
all other warranties. Furthermore, Guavy LLC does not make any
representations concerning the accuracy or reliability of the use of the
materials on its Website or otherwise relating to such materials or any
sites linked to this Website.
</p>
<h2>4. Limitations</h2>
<p>
Guavy LLC or its suppliers will not be hold accountable for any damages
that will arise with the use or inability to use the materials on Guavy
LLCs Website, even if Guavy LLC or an authorize representative of this
Website has been notified, orally or written, of the possibility of such
damage. Some jurisdiction does not allow limitations on implied
warranties or limitations of liability for incidental damages, these
limitations may not apply to you.
</p>
<h2>5. Revisions and Errata</h2>
<p>
The materials appearing on Guavy LLCs Website may include technical,
typographical, or photographic errors. Guavy LLC will not promise that
any of the materials in this Website are accurate, complete, or current.
Guavy LLC may change the materials contained on its Website at any time
without notice. Guavy LLC does not make any commitment to update the
materials.
</p>
<h2>6. Links</h2>
<p>
Guavy LLC has not reviewed all of the sites linked to its Website and is
not responsible for the contents of any such linked site. The presence
of any link does not imply endorsement by Guavy LLC of the site. The use
of any linked website is at the users own risk.
</p>
<h2>7. Site Terms of Use Modifications</h2>
<p>
Guavy LLC may revise these Terms of Use for its Website at any time
without prior notice. By using this Website, you are agreeing to be
bound by the current version of these Terms and Conditions of Use.
</p>
<h2>8. Your Privacy</h2>
<p>
Please read our <nuxt-link to="/privacy"> Privacy Policy</nuxt-link>.
</p>
<h2>9. Governing Law</h2>
<p>
Any claim related to Guavy LLC's Website shall be governed by the laws
of us without regards to its conflict of law provisions.
</p>
<h2>10. Content</h2>
<p>
When you upload text, software, mods, scripts, graphics, photos, audio,
videos, links, interactive features and other materials that may be
viewed on, or accessed through Modrinth, we refer to it as “Content”.
</p>
<ul>
<li>
You must own or have the necessary licenses, rights, consents, and
permissions to store, share or distribute the Content that is uploaded
under your Modrinth account.
</li>
<li>
You are responsible for all activity and Content that is uploaded
under your Modrinth account.
</li>
<li>
You must not transmit any viruses, worms, malware, or any other code
of a destructive nature through Modrinth.
</li>
<li>
You retain all of your ownership rights to your Content. We do not
claim any ownership in or to any of your Content.
</li>
<li>
To enable us to provide the services of Modrinth, you hereby grant us
a worldwide, non-exclusive, royalty-free, and unrestricted license to
use, reproduce, distribute copies, prepare derivative works of, or
display Content in connection with Modrinth in any medium and for any
purpose (including commercial purposes), which is irrevocable.
</li>
</ul>
</div>
<m-footer class="footer" centered />
</div>
</template>
<script>
import MFooter from '@/components/MFooter'
export default {
components: {
MFooter,
},
auth: false,
head: {
title: 'Terms - Modrinth',
meta: [
{
hid: 'description',
name: 'description',
content:
'The Terms of Service of Modrinth, an open source modding platform. Modrinth currently supports Minecraft, including the forge and fabric mod loaders.',
},
{
hid: 'apple-mobile-web-app-title',
name: 'apple-mobile-web-app-title',
content: 'Terms of Service',
},
{
hid: 'og:title',
name: 'og:title',
content: 'Terms of Service',
},
{
hid: 'og:url',
name: 'og:url',
content: `https://modrinth.com/tos`,
},
],
},
}
</script>
<style lang="scss" scoped>
.main {
margin: var(--spacing-card-sm) auto;
max-width: 800px;
}
.container {
@extend %card;
padding: var(--spacing-card-sm) var(--spacing-card-lg);
}
</style>

571
pages/mod/_id/edit.vue Normal file
View File

@@ -0,0 +1,571 @@
<template>
<div class="page-container">
<div class="page-contents">
<header class="columns">
<h2 class="column-grow-1">Edit Mod</h2>
<button
title="Save"
class="brand-button column"
:disabled="!this.$nuxt.$loading"
@click="saveMod"
>
Save
</button>
</header>
<EthicalAd class="advert" />
<section class="essentials">
<h3>Name</h3>
<label>
<span>
Be creative. TechCraft v7 won't be searchable and won't be clicked
on
</span>
<input v-model="mod.title" type="text" placeholder="Enter the name" />
</label>
<h3>Summary</h3>
<label>
<span>
Give a quick description to your mod. It will appear in the search
</span>
<input
v-model="mod.description"
type="text"
placeholder="Enter the summary"
/>
</label>
<h3>Categories</h3>
<label>
<span>
Select up to 3 categories. They will help to find your mod
</span>
<multiselect
id="categories"
v-model="mod.categories"
:options="availableCategories"
:loading="availableCategories.length === 0"
:multiple="true"
:searchable="false"
:show-no-results="false"
:close-on-select="false"
:clear-on-select="false"
:show-labels="false"
:max="3"
:limit="6"
:hide-selected="true"
placeholder="Choose categories"
/>
</label>
<h3>Vanity URL (slug)</h3>
<label>
<span>
Set this to something pretty, so URLs to your mod are more readable
</span>
<input
id="name"
v-model="mod.slug"
type="text"
placeholder="Enter the vanity URL's last bit"
/>
</label>
</section>
<section class="mod-icon rows">
<h3>Icon</h3>
<div class="columns row-grow-1">
<div class="column-grow-1 rows">
<file-input
accept="image/png,image/jpeg,image/gif"
class="choose-image"
prompt="Choose image or drag it here"
@change="showPreviewImage"
/>
<ul class="row-grow-1">
<li>Must be a square</li>
<li>Minimum size is 100x100</li>
<li>Acceptable formats are PNG, JPEG and GIF</li>
</ul>
<button
class="transparent-button"
@click="
icon = null
previewImage = null
"
>
Reset icon
</button>
</div>
<img
:src="
mod.icon_url
? mod.icon_url
: 'https://cdn.modrinth.com/placeholder.svg'
"
alt="preview-image"
/>
</div>
</section>
<section class="game-sides">
<h3>Supported environments</h3>
<div class="columns">
<span>
Let others know if your mod is for clients, servers or universal.
For example, IC2 will be required + required, while OptiFine will be
required + no functionality
</span>
<div class="labeled-control">
<h3>Client</h3>
<Multiselect
v-model="clientSideType"
placeholder="Select one"
track-by="id"
label="label"
:options="sideTypes"
:searchable="false"
:close-on-select="true"
:show-labels="false"
:allow-empty="false"
/>
</div>
<div class="labeled-control">
<h3>Server</h3>
<Multiselect
v-model="serverSideType"
placeholder="Select one"
track-by="id"
label="label"
:options="sideTypes"
:searchable="false"
:close-on-select="true"
:show-labels="false"
:allow-empty="false"
/>
</div>
</div>
</section>
<section class="description">
<h3>
<label
for="body"
title="You can type the of the long form of your description here."
>
Description
</label>
</h3>
<span>
You can type the of the long form of your description here. This
editor supports markdown. You can find the syntax
<a
href="https://guides.github.com/features/mastering-markdown/"
target="_blank"
rel="noopener noreferrer"
>here</a
>.
</span>
<div class="columns">
<div class="textarea-wrapper">
<textarea id="body" v-model="body"></textarea>
</div>
<div v-compiled-markdown="body" class="markdown-body"></div>
</div>
</section>
<section class="extra-links">
<div class="title">
<h3>External links</h3>
</div>
<label
title="A place for users to report bugs, issues, and concerns about your mod."
>
<span>Issue tracker</span>
<input
v-model="mod.issues_url"
type="url"
placeholder="Enter a valid URL"
/>
</label>
<label title="A page/repository containing the source code">
<span>Source code</span>
<input
v-model="mod.source_url"
type="url"
placeholder="Enter a valid URL"
/>
</label>
<label
title="A page containing information, documentation, and help for the mod."
>
<span>Wiki page</span>
<input
v-model="mod.wiki_url"
type="url"
placeholder="Enter a valid URL"
/>
</label>
<label title="An inivitation link to your Discord server.">
<span>Discord invite</span>
<input
v-model="mod.wiki_url"
type="url"
placeholder="Enter a valid URL"
/>
</label>
</section>
<section class="license">
<div class="title">
<h3>License</h3>
</div>
<label>
<span>
It is really important to choose a proper license for your mod. You
may choose one from our list or provide a URL to your own license.
URL field will be filled automatically for provided licenses
</span>
<div class="input-group">
<Multiselect
v-model="mod.license"
placeholder="Select one"
track-by="short"
label="name"
:options="availableLicenses"
:searchable="true"
:close-on-select="true"
:show-labels="false"
/>
<input
v-model="mod.license.url"
type="url"
placeholder="License URL"
/>
</div>
</label>
</section>
<!--
<section class="donations">
<div class="title">
<h3>Donation links</h3>
<i> this section is optional</i>
</div>
</section>
-->
<m-footer class="footer" centered />
</div>
</div>
</template>
<script>
import axios from 'axios'
import Multiselect from 'vue-multiselect'
import MFooter from '@/components/MFooter'
import FileInput from '@/components/FileInput'
import EthicalAd from '@/components/EthicalAd'
export default {
components: {
MFooter,
FileInput,
EthicalAd,
Multiselect,
},
async asyncData(data) {
const [
mod,
availableCategories,
availableLoaders,
availableGameVersions,
availableLicenses,
// availableDonationPlatforms,
] = (
await Promise.all([
axios.get(`https://api.modrinth.com/api/v1/mod/${data.params.id}`),
axios.get(`https://api.modrinth.com/api/v1/tag/category`),
axios.get(`https://api.modrinth.com/api/v1/tag/loader`),
axios.get(`https://api.modrinth.com/api/v1/tag/game_version`),
axios.get(`https://api.modrinth.com/api/v1/tag/license`),
// axios.get(`https://api.modrinth.com/api/v1/tag/donation_platform`),
])
).map((it) => it.data)
mod.license = {
short: mod.license.id,
name: mod.license.name,
}
const res = await axios.get(mod.body_url)
return {
mod,
body: res.data,
clientSideType: {
label: mod.client_side,
id: mod.client_side,
},
serverSideType: {
label: mod.server_side,
id: mod.server_side,
},
availableCategories,
availableLoaders,
availableGameVersions,
availableLicenses,
// availableDonationPlatforms,
}
},
data() {
return {
previewImage: null,
compiledBody: '',
icon: null,
sideTypes: [
{ label: 'Required', id: 'required' },
{ label: 'No functionality', id: 'no-functionality' },
{ label: 'Unsupported', id: 'unsupported' },
],
}
},
watch: {
license(newValue, oldValue) {
if (newValue == null) {
this.license_url = ''
return
}
switch (newValue.short) {
case 'custom':
this.license_url = ''
break
default:
this.license_url = `https://cdn.modrinth.com/licenses/${newValue.short}.txt`
}
},
},
methods: {
async saveMod() {
this.$nuxt.$loading.start()
try {
await axios.patch(
`https://api.modrinth.com/api/v1/mod/${this.mod.id}`,
{
title: this.mod.title,
description: this.mod.description,
body: this.body,
categories: this.mod.categories,
issues_url: this.mod.issues_url,
source_url: this.mod.source_url,
wiki_url: this.mod.wiki_url,
license_url: this.mod.license_url,
discord_url: this.mod.discord_url,
license_id: this.mod.license.short,
client_side: this.clientSideType.id,
server_side: this.serverSideType.id,
slug: this.mod.mod_slug,
},
{
headers: {
Authorization: this.$auth.getToken('local'),
},
}
)
await this.$router.replace(`/mod/${this.mod.id}`)
} catch (err) {
this.$notify({
group: 'main',
title: 'An Error Occurred',
text: err.response.data.description,
type: 'error',
})
window.scrollTo({ top: 0, behavior: 'smooth' })
}
this.$nuxt.$loading.finish()
},
showPreviewImage(e) {
const reader = new FileReader()
this.icon = e.target.files[0]
reader.readAsDataURL(this.icon)
reader.onload = (event) => {
this.previewImage = event.target.result
}
},
},
}
</script>
<style lang="scss" scoped>
.title {
* {
display: inline;
}
}
label {
display: flex;
span {
flex: 2;
padding-right: var(--spacing-card-lg);
}
input,
.multiselect,
.input-group {
flex: 3;
height: fit-content;
}
}
.input-group {
display: flex;
flex-direction: column;
* {
margin-bottom: var(--spacing-card-sm);
}
}
.textarea-wrapper {
display: flex;
flex-direction: column;
align-items: stretch;
textarea {
flex: 1;
overflow-y: auto;
resize: none;
max-width: 100%;
}
}
.page-contents {
display: grid;
grid-template:
'header header header' auto
'advert advert advert' auto
'essentials essentials mod-icon' auto
'game-sides game-sides game-sides' auto
'description description description' auto
'versions versions versions' auto
'extra-links license license' auto
'donations donations .' auto
'footer footer footer' auto
/ 4fr 1fr 4fr;
column-gap: var(--spacing-card-md);
row-gap: var(--spacing-card-md);
}
header {
@extend %card;
grid-area: header;
padding: var(--spacing-card-md) var(--spacing-card-lg);
h2 {
margin: auto 0;
color: var(--color-text-dark);
font-weight: var(--font-weight-extrabold);
}
button {
margin-left: 0.5rem;
}
}
.advert {
grid-area: advert;
}
section {
@extend %card;
padding: var(--spacing-card-md) var(--spacing-card-lg);
}
section.essentials {
grid-area: essentials;
}
section.mod-icon {
grid-area: mod-icon;
img {
align-self: flex-start;
max-width: 50%;
margin-left: var(--spacing-card-lg);
}
}
section.game-sides {
grid-area: game-sides;
.columns {
flex-wrap: wrap;
span {
flex: 2;
}
.labeled-control {
flex: 2;
margin-left: var(--spacing-card-lg);
}
}
}
section.description {
grid-area: description;
& > .columns {
align-items: stretch;
min-height: 10rem;
max-height: 40rem;
& > * {
flex: 1;
max-width: 50%;
}
}
.markdown-body {
overflow-y: auto;
padding: 0 var(--spacing-card-sm);
}
}
section.extra-links {
grid-area: extra-links;
label {
align-items: center;
margin-top: var(--spacing-card-sm);
span {
flex: 1;
}
}
}
section.license {
grid-area: license;
label {
margin-top: var(--spacing-card-sm);
}
}
section.donations {
grid-area: donations;
}
.footer {
grid-area: footer;
}
.choose-image {
cursor: pointer;
}
</style>

View File

@@ -1,14 +1,11 @@
<template>
<ModPage :mod="mod" :versions="versions" :members="members">
<div class="markdown-body" v-html="body"></div>
<div v-compiled-markdown="body" class="markdown-body"></div>
</ModPage>
</template>
<script>
import axios from 'axios'
import xss from 'xss'
import marked from 'marked'
import ModPage from '@/components/ModPage'
export default {
@@ -31,17 +28,20 @@ export default {
members[i].avatar_url = res.data.avatar_url
}
res = await axios.get(mod.body_url)
const body = xss(marked(res.data))
const body = (await axios.get(mod.body_url)).data
const versions = []
for (const version of mod.versions) {
res = await axios.get(
`https://api.modrinth.com/api/v1/version/${version}`
)
versions.push(res.data)
try {
res = await axios.get(
`https://api.modrinth.com/api/v1/version/${version}`
)
versions.push(res.data)
} catch {
// eslint-disable-next-line no-console
console.log('Some versions may be missing...')
}
}
return {
@@ -102,9 +102,9 @@ export default {
<style lang="scss" scoped>
.markdown-body {
padding: 20px;
box-shadow: 0 2px 3px 1px var(--color-grey-2);
background: var(--color-bg);
border-radius: 0 0 var(--size-rounded-sm) var(--size-rounded-sm);
padding: 1rem;
margin-bottom: var(--spacing-card-md);
background: var(--color-raised-bg);
border-radius: var(--size-rounded-card);
}
</style>

View File

@@ -31,11 +31,15 @@ export default {
const versions = []
for (const version of mod.versions) {
res = await axios.get(
`https://api.modrinth.com/api/v1/version/${version}`
)
versions.push(res.data)
try {
res = await axios.get(
`https://api.modrinth.com/api/v1/version/${version}`
)
versions.push(res.data)
} catch {
// eslint-disable-next-line no-console
console.log('Some versions may be missing...')
}
}
return {

View File

@@ -1,109 +1,115 @@
<template>
<ModPage :mod="mod" :versions="versions" :members="members">
<div class="version">
<div class="header">
<h3>{{ version.name }}</h3>
<div
v-if="
this.$auth.loggedIn &&
members.find((x) => x.user_id === this.$auth.user.id)
<div class="version-header">
<h4>{{ version.name }}</h4>
<span v-if="version.version_type === 'release'" class="badge green">
Release
</span>
<span v-if="version.version_type === 'beta'" class="badge yellow">
Beta
</span>
<span v-if="version.version_type === 'alpha'" class="badge red">
Alpha
</span>
<span>
{{ version.version_number }}
</span>
<Categories :categories="version.loaders" />
<a
:href="primaryFile.url"
class="download-button"
@click.prevent="
downloadFile(primaryFile.hashes.sha1, primaryFile.url)
"
class="user-actions"
>
<button class="trash red">
<TrashIcon />
</button>
<button class="upload" @click="showPopup = !showPopup">
<UploadIcon />
</button>
<DownloadIcon />
Download
</a>
</div>
<div class="stats">
<div class="stat">
<DownloadIcon />
<div class="info">
<h4>Downloads</h4>
<p class="value">{{ version.downloads }}</p>
</div>
</div>
<div class="stat">
<CalendarIcon />
<div class="info">
<h4>Created</h4>
<p
v-tooltip="
$dayjs(version.published).format(
'[Created on] YYYY-MM-DD [at] HH:mm A'
)
"
class="value"
>
{{ $dayjs(version.published).fromNow() }}
</p>
</div>
</div>
<div class="stat">
<TagIcon />
<div class="info">
<h4>Available For</h4>
<p class="value">
{{ version.game_versions.join(', ') }}
</p>
</div>
</div>
</div>
<div class="markdown-body" v-html="changelog"></div>
<hr />
<div class="columns metadata">
<div class="author">
<img :src="version.author.avatar_url" />
<p>{{ version.author.name }}</p>
</div>
<p>{{ version.downloads }} Downloads</p>
<div>
<FabricIcon
v-if="version.loaders.includes('fabric')"
stroke="#AC6C3A"
/>
<ForgeIcon
v-if="version.loaders.includes('forge')"
stroke="#8B81E6"
/>
</div>
<div class="game-versions">
<p v-for="gameVersion in version.game_versions" :key="gameVersion">
{{ gameVersion }}
</p>
</div>
</div>
<hr />
<div v-compiled-markdown="changelog" class="markdown-body"></div>
<div class="files">
<div v-for="file in version.files" :key="file.hashes.sha1">
<p>{{ file.filename }}</p>
<a :href="file.url" download>
<div class="text-wrapper">
<p>{{ file.filename }}</p>
<div
v-if="
$auth.loggedIn &&
members.find((x) => x.user_id === $auth.user.id)
"
class="actions"
>
<button @click="deleteFile(file.hashes.sha1)">Delete File</button>
<button @click="makePrimary(file.hashes.sha1)">
Make Primary
</button>
</div>
</div>
<a
:href="file.url"
@click.prevent="downloadFile(file.hash, file.url)"
>
<DownloadIcon />
</a>
</div>
</div>
<FileInput class="file-input" @change="addFiles" />
</div>
<Popup :show-popup="showPopup">
<h3 class="popup-title">Upload Files</h3>
<FileInput
input-id="version-files"
input-accept="application/*"
default-text="Upload Files"
:input-multiple="true"
@change="addFiles"
>
<label class="required" title="The files associated with the version">
Version Files
</label>
</FileInput>
<div class="popup-buttons">
<button
class="trash-button"
@click="
showPopup = false
filesToUpload = []
"
>
<TrashIcon />
</button>
<button class="default-button" @click="uploadFiles">Upload</button>
</div>
</Popup>
</ModPage>
</template>
<script>
import axios from 'axios'
import ModPage from '@/components/ModPage'
import xss from 'xss'
import marked from 'marked'
import Popup from '@/components/Popup'
import Categories from '@/components/Categories'
import FileInput from '@/components/FileInput'
import DownloadIcon from '~/assets/images/utils/download.svg?inline'
import UploadIcon from '~/assets/images/utils/upload.svg?inline'
import TrashIcon from '~/assets/images/utils/trash.svg?inline'
import ForgeIcon from '~/assets/images/categories/forge.svg?inline'
import FabricIcon from '~/assets/images/categories/fabric.svg?inline'
import CalendarIcon from '~/assets/images/utils/calendar.svg?inline'
import TagIcon from '~/assets/images/utils/tag.svg?inline'
export default {
components: {
Popup,
FileInput,
Categories,
ModPage,
ForgeIcon,
FabricIcon,
DownloadIcon,
UploadIcon,
TrashIcon,
CalendarIcon,
TagIcon,
},
auth: false,
async asyncData(data) {
@@ -125,11 +131,15 @@ export default {
const versions = []
for (const version of mod.versions) {
res = await axios.get(
`https://api.modrinth.com/api/v1/version/${version}`
)
versions.push(res.data)
try {
res = await axios.get(
`https://api.modrinth.com/api/v1/version/${version}`
)
versions.push(res.data)
} catch {
// eslint-disable-next-line no-console
console.log('Some versions may be missing...')
}
}
const version = versions.find((x) => x.id === data.params.version)
@@ -138,8 +148,13 @@ export default {
let changelog = ''
if (version.changelog_url) {
res = await axios.get(version.changelog_url)
changelog = xss(marked(res.data))
changelog = (await axios.get(version.changelog_url)).data
}
let primaryFile = version.files.find((file) => file.primary)
if (!primaryFile) {
primaryFile = version.files[0]
}
return {
@@ -148,16 +163,55 @@ export default {
members,
version,
changelog,
primaryFile,
}
},
data() {
return {
showPopup: false,
filesToUpload: [],
}
},
methods: {
addFiles(e) {
async downloadFile(hash, url) {
await axios.get(
`https://api.modrinth.com/api/v1/version_file/${hash}/download`
)
const elem = document.createElement('a')
elem.download = hash
elem.href = url
elem.click()
},
async deleteFile(hash) {
const config = {
headers: {
Authorization: this.$auth.getToken('local'),
},
}
await axios.delete(
`https://api.modrinth.com/api/v1/version_file/${hash}`,
config
)
},
async makePrimary(hash) {
const config = {
headers: {
Authorization: this.$auth.getToken('local'),
},
}
await axios.patch(
`https://api.modrinth.com/api/v1/version/${this.version.id}`,
{
primary_file: {
sha1: hash,
},
},
config
)
},
async addFiles(e) {
this.filesToUpload = e.target.files
for (let i = 0; i < e.target.files.length; i++) {
@@ -165,8 +219,7 @@ export default {
'-' + i
)
}
},
async uploadFiles() {
this.$nuxt.$loading.start()
const formData = new FormData()
@@ -254,70 +307,38 @@ export default {
<style lang="scss" scoped>
.version {
background: var(--color-bg);
border-radius: 0 0 0.5rem 0.5rem;
box-shadow: 0 2px 3px 1px var(--color-grey-2);
padding: 1em;
margin-bottom: var(--spacing-card-md);
background: var(--color-raised-bg);
border-radius: var(--size-rounded-card);
padding: 1rem;
.header {
h3 {
display: inline-block;
}
.user-actions {
float: right;
button {
cursor: pointer;
margin-right: 10px;
padding: 5px;
border: none;
border-radius: var(--size-rounded-sm);
}
.trash {
color: #9b2c2c;
background-color: var(--color-bg);
}
.upload {
color: var(--color-text);
background-color: var(--color-grey-1);
* {
margin: auto 0;
}
}
}
}
hr {
margin: 20px 0;
color: var(--color-grey-1);
}
.metadata {
.version-header {
display: flex;
align-items: center;
justify-content: space-between;
.author {
h4,
span {
margin: auto 0.5rem auto 0;
}
.download-button {
margin-left: auto;
padding: 0.5rem;
color: var(--color-button-text);
background-color: var(--color-button-bg);
justify-self: flex-end;
display: flex;
align-items: center;
img {
height: 50px;
width: 50px;
margin-right: 10px;
}
}
}
border-radius: var(--size-rounded-sm);
.game-versions {
max-width: 200px;
p {
margin: 0 0 0 10px;
padding: 4px;
font-size: 15px;
color: var(--color-text);
background-color: var(--color-grey-1);
display: inline-block;
&:hover,
&:focus {
background-color: var(--color-button-bg-hover);
}
svg {
margin-right: 0.25rem;
}
}
}
@@ -326,71 +347,77 @@ export default {
div {
display: flex;
margin-right: 10px;
border: 1px solid var(--color-grey-1);
border-radius: var(--size-rounded-sm);
margin-right: 0.5rem;
background: var(--color-bg);
border-radius: var(--size-rounded-control);
p {
margin-left: 10px;
margin-right: 10px;
.text-wrapper {
display: flex;
flex-direction: column;
padding: 0.5rem;
.actions {
width: 100%;
display: flex;
justify-content: space-between;
max-height: 3rem;
font-size: var(--font-size-sm);
button {
display: flex;
align-items: center;
svg {
margin-left: 0.25rem;
}
}
}
}
a {
display: table-cell;
display: flex;
align-items: center;
margin-left: auto;
width: 40px;
height: 60px;
background-color: var(--color-grey-1);
color: var(--color-grey-3);
width: 2.5rem;
height: auto;
background-color: var(--color-button-bg);
color: var(--color-button-text);
border-radius: 0 var(--size-rounded-control) var(--size-rounded-control)
0;
svg {
margin-top: 15px;
vertical-align: center;
height: 30px;
width: 40px;
}
&:hover,
&:focus {
background-color: var(--color-grey-3);
color: var(--color-grey-4);
background-color: var(--color-button-bg-hover);
color: var(--color-button-text-hover);
}
}
}
}
}
.popup-title {
margin-bottom: 40px;
}
.popup-buttons {
margin-top: 40px;
.stats {
display: flex;
justify-content: left;
align-items: center;
flex-wrap: wrap;
margin: 0.5rem 0;
.stat {
margin-right: 0.75rem;
@extend %stat;
.default-button {
border-radius: var(--size-rounded-sm);
cursor: pointer;
border: none;
padding: 10px;
background-color: var(--color-grey-1);
color: var(--color-grey-5);
&:hover,
&:focus {
color: var(--color-grey-4);
svg {
padding: 0.25rem;
border-radius: 50%;
background-color: var(--color-button-bg);
}
}
}
.trash-button {
cursor: pointer;
margin-right: 10px;
padding: 5px;
border: none;
border-radius: var(--size-rounded-sm);
color: #9b2c2c;
background-color: var(--color-bg);
}
.file-input {
margin-top: 2rem;
}
</style>

View File

@@ -5,20 +5,29 @@
<tr>
<th></th>
<th>Name</th>
<th>Number</th>
<th>Loaders</th>
<th>Game Versions</th>
<th>Version</th>
<th>Mod Loader</th>
<th>Minecraft Version</th>
<th>Status</th>
<th>Downloads</th>
<th>Published</th>
<th>Date Published</th>
</tr>
</thead>
<tbody>
<tr v-for="version in versions" :key="version.id">
<td>
<nuxt-link :to="'/mod/' + mod.id + '/version/' + version.id">
<a
:href="findPrimary(version).url"
class="download"
@click.prevent="
downloadFile(
findPrimary(version).hashes.sha1,
findPrimary(version).url
)
"
>
<DownloadIcon />
</nuxt-link>
</a>
</td>
<td>
<nuxt-link :to="'/mod/' + mod.id + '/version/' + version.id">
@@ -27,14 +36,8 @@
</td>
<td>{{ version.version_number }}</td>
<td>
<FabricIcon
v-if="version.loaders.includes('fabric')"
stroke="#AC6C3A"
/>
<ForgeIcon
v-if="version.loaders.includes('forge')"
stroke="#8B81E6"
/>
<FabricIcon v-if="version.loaders.includes('fabric')" />
<ForgeIcon v-if="version.loaders.includes('forge')" />
</td>
<td>{{ version.game_versions.join(', ') }}</td>
<td>
@@ -62,10 +65,6 @@
class="create-version-popup-body"
>
<h3>New Version</h3>
<div v-if="currentError" class="error">
<h4>Error</h4>
<p>{{ currentError }}</p>
</div>
<label
for="version-title"
class="required"
@@ -110,7 +109,7 @@
<label
title="The version number of this version. Preferably following semantic versioning"
>
Loaders
Mod Loaders
</label>
<multiselect
v-model="createdVersion.loaders"
@@ -128,7 +127,7 @@
placeholder="Choose loaders..."
/>
<label title="The versions of minecraft that this mod version supports">
Game Versions
Minecraft Versions
</label>
<multiselect
v-model="createdVersion.game_versions"
@@ -155,7 +154,7 @@
/>
<FileInput
input-id="version-files"
input-accept="application/java-archive,application/zip"
accept="application/java-archive,application/zip"
default-text="Upload Files"
:input-multiple="true"
@change="updateVersionFiles"
@@ -237,11 +236,15 @@ export default {
const versions = []
for (const version of mod.versions) {
res = await axios.get(
`https://api.modrinth.com/api/v1/version/${version}`
)
versions.push(res.data)
try {
res = await axios.get(
`https://api.modrinth.com/api/v1/version/${version}`
)
versions.push(res.data)
} catch {
// eslint-disable-next-line no-console
console.log('Some versions may be missing...')
}
}
res = await axios.get(`https://api.modrinth.com/api/v1/tag/loader`)
@@ -320,6 +323,29 @@ export default {
this.$nuxt.$loading.finish()
},
findPrimary(version) {
let file = version.files.find((x) => x.primary)
if (!file) {
file = version.files[0]
}
if (!file) {
file = { url: `/mod/${this.mod.id}/version/${version.id}` }
}
return file
},
async downloadFile(hash, url) {
await axios.get(
`https://api.modrinth.com/api/v1/version_file/${hash}/download`
)
const elem = document.createElement('a')
elem.download = hash
elem.href = url
elem.click()
},
},
head() {
return {
@@ -368,10 +394,10 @@ export default {
<style lang="scss" scoped>
table {
background: var(--color-bg);
border-collapse: collapse;
border-radius: 0 0 0.5rem 0.5rem;
box-shadow: 0 2px 3px 1px var(--color-grey-2);
margin-bottom: var(--spacing-card-md);
background: var(--color-raised-bg);
border-radius: var(--size-rounded-card);
table-layout: fixed;
width: 100%;
@@ -383,7 +409,7 @@ table {
tr:first-child {
th,
td {
border-bottom: 1px solid var(--color-grey-2);
border-bottom: 1px solid var(--color-divider);
}
}
@@ -391,14 +417,14 @@ table {
td {
&:first-child {
text-align: center;
width: 5%;
width: 7%;
svg {
color: var(--color-grey-3);
color: var(--color-text);
&:hover,
&:focus {
color: var(--color-grey-5);
color: var(--color-text-hover);
}
}
}
@@ -406,23 +432,23 @@ table {
&:nth-child(2),
&:nth-child(5) {
padding-left: 0;
width: 15%;
width: 12%;
}
}
th {
color: #718096;
color: var(--color-heading);
font-size: 0.8rem;
letter-spacing: 0.02rem;
margin-bottom: 0.5rem;
margin-top: 1.5rem;
padding: 1rem 1rem;
padding: 0.75rem 1rem;
text-transform: uppercase;
}
td {
overflow: hidden;
padding: 0.25rem 1rem;
padding: 0.75rem 1rem;
img {
height: 3rem;
@@ -448,7 +474,7 @@ input {
outline: none;
border: none;
margin: 10px 0 30px;
background-color: var(--color-grey-1);
background-color: var(--color-button-bg);
color: var(--color-text);
font-family: monospace;
}
@@ -482,21 +508,15 @@ input {
cursor: pointer;
border: none;
padding: 10px;
background-color: var(--color-grey-1);
color: var(--color-grey-5);
background-color: var(--color-button-bg);
color: var(--color-button-text);
&:hover,
&:focus {
color: var(--color-grey-4);
background-color: var(--color-button-bg-hover);
}
}
.error {
margin: 20px 0;
border-left: #e04e3e 7px solid;
padding: 5px 20px 20px 20px;
}
@media screen and (max-width: 400px) {
th,
td {

File diff suppressed because it is too large Load Diff

View File

@@ -1,276 +1,294 @@
<template>
<div class="columns">
<div class="content column-grow-5">
<h2>Mods</h2>
<section class="search-bar">
<div class="iconified-input column-grow-2">
<label class="hidden" for="search">Search Mods</label>
<input
id="search"
v-model="query"
type="search"
name="search"
placeholder="Search mods"
@input="onSearchChange(1)"
<div class="page-container">
<div class="page-contents">
<div class="content">
<section class="search-nav">
<div class="iconified-input column-grow-2">
<label class="hidden" for="search">Search Mods</label>
<input
id="search"
v-model="query"
type="search"
name="search"
placeholder="Search..."
autocomplete="off"
@input="onSearchChange(1)"
/>
<SearchIcon />
</div>
<div class="sort-paginate">
<div class="labeled-control">
<h3>Sort By</h3>
<Multiselect
v-model="sortType"
class="sort-types"
placeholder="Select one"
track-by="display"
label="display"
:options="sortTypes"
:searchable="false"
:close-on-select="true"
:show-labels="false"
:allow-empty="false"
@input="onSearchChange(1)"
>
<template slot="singleLabel" slot-scope="{ option }">{{
option.display
}}</template>
</Multiselect>
</div>
<div class="labeled-control per-page">
<h3>Per Page</h3>
<Multiselect
v-model="maxResults"
class="max-results"
placeholder="Select one"
:options="[5, 10, 15, 20, 50, 100]"
:searchable="false"
:close-on-select="true"
:show-labels="false"
:allow-empty="false"
@input="onSearchChange(currentPage)"
>
</Multiselect>
</div>
<div class="mobile-filters-button">
<button @click="toggleFiltersMenu">Filter</button>
</div>
</div>
<pagination
:current-page="currentPage"
:pages="pages"
@switch-page="onSearchChange"
></pagination>
</section>
<div class="results column-grow-4">
<client-only>
<EthicalAd type="text" />
</client-only>
<SearchResult
v-for="(result, index) in results"
:id="result.mod_id.split('-')[1]"
:key="result.mod_id"
:author="result.author"
:name="result.title"
:description="result.description"
:latest-version="result.latest_version"
:created-at="result.date_created"
:updated-at="result.date_modified"
:downloads="result.downloads.toString()"
:icon-url="result.icon_url"
:author-url="result.author_url"
:page-url="result.page_url"
:categories="result.categories"
:is-ad="index === -1"
:is-modrinth="result.host === 'modrinth'"
/>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="11" cy="11" r="8" />
<line x1="21" y1="21" x2="16.65" y2="16.65" />
</svg>
</div>
<div class="sort-paginate">
<Multiselect
v-model="sortType"
class="sort-types"
placeholder="Select one"
track-by="display"
label="display"
:options="sortTypes"
:searchable="false"
:close-on-select="true"
:show-labels="false"
:allow-empty="false"
@input="onSearchChange(1)"
>
<template slot="singleLabel" slot-scope="{ option }">{{
option.display
}}</template>
</Multiselect>
<div class="mobile-filters-button">
<button @click="toggleFiltersMenu">Filter...</button>
<div v-if="results.length === 0" class="no-results">
<p>No results found for your query!</p>
</div>
</div>
<pagination
:current-page="currentPage"
:pages="pages"
@switch-page="onSearchChange"
></pagination>
</section>
<div class="results column-grow-4">
<client-only>
<EthicalAd type="text" />
</client-only>
<SearchResult
v-for="(result, index) in results"
:id="result.mod_id"
:key="result.mod_id"
:author="result.author"
:name="result.title"
:description="result.description"
:latest-version="result.latest_version"
:created-at="result.date_created"
:updated-at="result.date_modified"
:downloads="result.downloads.toString()"
:icon-url="result.icon_url"
:author-url="result.author_url"
:page-url="result.page_url"
:categories="result.categories"
:is-ad="index === -1"
/>
<div v-if="results.length === 0" class="no-results">
<p>No results found for your query!</p>
</div>
<section v-if="pages.length > 1" class="search-bottom">
<div class="labeled-control">
<h3>Per Page</h3>
<Multiselect
v-model="maxResults"
class="max-results"
placeholder="Select one"
:options="[5, 10, 15, 20, 50, 100]"
:searchable="false"
:close-on-select="true"
:show-labels="false"
:allow-empty="false"
@input="onSearchChange(currentPage)"
>
</Multiselect>
</div>
<pagination
:current-page="currentPage"
:pages="pages"
@switch-page="onSearchChangeToTop"
></pagination>
</section>
</div>
<section v-if="pages.length > 1" class="search-bottom">
<Multiselect
v-model="maxResults"
class="max-results"
placeholder="Select one"
:options="[5, 10, 15, 20, 50, 100]"
:searchable="false"
:close-on-select="true"
:show-labels="false"
:allow-empty="false"
@input="onSearchChange(currentPage)"
>
</Multiselect>
<pagination
:current-page="currentPage"
:pages="pages"
@switch-page="onSearchChangeToTop"
></pagination>
<section id="filters" class="filters">
<div class="filters-wrapper">
<section class="filter-group">
<button class="filter-button-done" @click="toggleFiltersMenu">
Done
</button>
<button @click="clearFilters">Reset filters</button>
<h3>Categories</h3>
<SearchFilter
:active-filters="facets"
display-name="Technology"
facet-name="categories:technology"
@toggle="toggleFacet"
>
<TechCategory />
</SearchFilter>
<SearchFilter
:active-filters="facets"
display-name="Adventure"
facet-name="categories:adventure"
@toggle="toggleFacet"
>
<AdventureCategory />
</SearchFilter>
<SearchFilter
:active-filters="facets"
display-name="Magic"
facet-name="categories:magic"
@toggle="toggleFacet"
>
<MagicCategory />
</SearchFilter>
<SearchFilter
:active-filters="facets"
display-name="Utility"
facet-name="categories:utility"
@toggle="toggleFacet"
>
<UtilityCategory />
</SearchFilter>
<SearchFilter
:active-filters="facets"
display-name="Decoration"
facet-name="categories:decoration"
@toggle="toggleFacet"
>
<DecorationCategory />
</SearchFilter>
<SearchFilter
:active-filters="facets"
display-name="Library"
facet-name="categories:library"
@toggle="toggleFacet"
>
<LibraryCategory />
</SearchFilter>
<SearchFilter
:active-filters="facets"
display-name="Cursed"
facet-name="categories:cursed"
@toggle="toggleFacet"
>
<CursedCategory />
</SearchFilter>
<SearchFilter
:active-filters="facets"
display-name="World Generation"
facet-name="categories:worldgen"
@toggle="toggleFacet"
>
<WorldGenCategory />
</SearchFilter>
<SearchFilter
:active-filters="facets"
display-name="Storage"
facet-name="categories:storage"
@toggle="toggleFacet"
>
<StorageCategory />
</SearchFilter>
<SearchFilter
:active-filters="facets"
display-name="Food"
facet-name="categories:food"
@toggle="toggleFacet"
>
<FoodCategory />
</SearchFilter>
<SearchFilter
:active-filters="facets"
display-name="Equipment"
facet-name="categories:equipment"
@toggle="toggleFacet"
>
<EquipmentCategory />
</SearchFilter>
<SearchFilter
:active-filters="facets"
display-name="Miscellaneous"
facet-name="categories:misc"
@toggle="toggleFacet"
>
<MiscCategory />
</SearchFilter>
<h3>Mod Loaders</h3>
<SearchFilter
:active-filters="facets"
display-name="Fabric"
facet-name="categories:fabric"
@toggle="toggleFacet"
>
<FabricLoader />
</SearchFilter>
<SearchFilter
:active-filters="facets"
display-name="Forge"
facet-name="categories:forge"
@toggle="toggleFacet"
>
<ForgeLoader />
</SearchFilter>
<h3>Host</h3>
<SearchFilter
:active-filters="facets"
display-name="Modrinth"
facet-name="host:modrinth"
@toggle="toggleFacet"
>
<Modrinth />
</SearchFilter>
<SearchFilter
:active-filters="facets"
display-name="CurseForge"
facet-name="host:curseforge"
@toggle="toggleFacet"
>
<FlameAnvil />
</SearchFilter>
<h3>Versions</h3>
<SearchFilter
:active-filters="showVersions"
display-name="Include snapshots"
facet-name="snapshots"
style="margin-bottom: 10px"
@toggle="fillInitialVersions"
/>
</section>
<multiselect
v-model="selectedVersions"
:options="versions"
:loading="versions.length === 0"
:multiple="true"
:searchable="true"
:show-no-results="false"
:close-on-select="false"
:clear-on-select="false"
:show-labels="false"
:limit="6"
:hide-selected="true"
placeholder="Choose versions..."
@input="onSearchChange(1)"
></multiselect>
</div>
<m-footer class="footer" />
</section>
</div>
<section id="filters" class="filters">
<div class="filters-wrapper">
<section class="filter-group">
<button class="filter-button-done" @click="toggleFiltersMenu">
Done
</button>
<button @click="clearFilters">Clear Filters</button>
<h3>Categories</h3>
<SearchFilter
:active-filters="facets"
display-name="Technology"
facet-name="categories:technology"
@toggle="toggleFacet"
>
<TechCategory />
</SearchFilter>
<SearchFilter
:active-filters="facets"
display-name="Adventure"
facet-name="categories:adventure"
@toggle="toggleFacet"
>
<AdventureCategory />
</SearchFilter>
<SearchFilter
:active-filters="facets"
display-name="Magic"
facet-name="categories:magic"
@toggle="toggleFacet"
>
<MagicCategory />
</SearchFilter>
<SearchFilter
:active-filters="facets"
display-name="Utility"
facet-name="categories:utility"
@toggle="toggleFacet"
>
<UtilityCategory />
</SearchFilter>
<SearchFilter
:active-filters="facets"
display-name="Decoration"
facet-name="categories:decoration"
@toggle="toggleFacet"
>
<DecorationCategory />
</SearchFilter>
<SearchFilter
:active-filters="facets"
display-name="Library"
facet-name="categories:library"
@toggle="toggleFacet"
>
<LibraryCategory />
</SearchFilter>
<SearchFilter
:active-filters="facets"
display-name="Cursed"
facet-name="categories:cursed"
@toggle="toggleFacet"
>
<CursedCategory />
</SearchFilter>
<SearchFilter
:active-filters="facets"
display-name="Worldgen"
facet-name="categories:worldgen"
@toggle="toggleFacet"
>
<WorldGenCategory />
</SearchFilter>
<SearchFilter
:active-filters="facets"
display-name="Storage"
facet-name="categories:storage"
@toggle="toggleFacet"
>
<StorageCategory />
</SearchFilter>
<SearchFilter
:active-filters="facets"
display-name="Food"
facet-name="categories:food"
@toggle="toggleFacet"
>
<FoodCategory />
</SearchFilter>
<SearchFilter
:active-filters="facets"
display-name="Equipment"
facet-name="categories:equipment"
@toggle="toggleFacet"
>
<EquipmentCategory />
</SearchFilter>
<SearchFilter
:active-filters="facets"
display-name="Misc"
facet-name="categories:misc"
@toggle="toggleFacet"
>
<MiscCategory />
</SearchFilter>
<h3>Loaders</h3>
<SearchFilter
:active-filters="facets"
display-name="Forge"
facet-name="categories:forge"
@toggle="toggleFacet"
>
<ForgeLoader />
</SearchFilter>
<SearchFilter
:active-filters="facets"
display-name="Fabric"
facet-name="categories:fabric"
@toggle="toggleFacet"
>
<FabricLoader />
</SearchFilter>
<h3>Platforms</h3>
<SearchFilter
:active-filters="facets"
display-name="Modrinth"
facet-name="host:modrinth"
@toggle="toggleFacet"
>
</SearchFilter>
<SearchFilter
:active-filters="facets"
display-name="Curseforge"
facet-name="host:curseforge"
@toggle="toggleFacet"
>
</SearchFilter>
<h3>Versions</h3>
<SearchFilter
:active-filters="showVersions"
display-name="Snapshots"
facet-name="snapshots"
style="margin-bottom: 10px"
@toggle="fillInitialVersions"
/>
</section>
<multiselect
v-model="selectedVersions"
:options="versions"
:loading="versions.length === 0"
:multiple="true"
:searchable="true"
:show-no-results="false"
:close-on-select="false"
:clear-on-select="false"
:show-labels="false"
:limit="6"
:hide-selected="true"
placeholder="Choose versions..."
@input="onSearchChange(1)"
></multiselect>
</div>
</section>
</div>
</template>
<script>
import Multiselect from 'vue-multiselect'
import axios from 'axios'
import SearchResult from '@/components/ModResult'
import SearchResult from '@/components/ProjectCard'
import Pagination from '@/components/Pagination'
import SearchFilter from '@/components/SearchFilter'
import EthicalAd from '@/components/EthicalAd'
import MFooter from '@/components/MFooter'
import TechCategory from '~/assets/images/categories/tech.svg?inline'
import AdventureCategory from '~/assets/images/categories/adventure.svg?inline'
import CursedCategory from '~/assets/images/categories/cursed.svg?inline'
@@ -287,9 +305,15 @@ import WorldGenCategory from '~/assets/images/categories/worldgen.svg?inline'
import ForgeLoader from '~/assets/images/categories/forge.svg?inline'
import FabricLoader from '~/assets/images/categories/fabric.svg?inline'
import Modrinth from '~/assets/images/categories/modrinth.svg?inline'
import FlameAnvil from '~/assets/images/categories/flameanvil.svg?inline'
import SearchIcon from '~/assets/images/utils/search.svg?inline'
export default {
auth: false,
components: {
MFooter,
EthicalAd,
SearchResult,
Pagination,
@@ -309,6 +333,9 @@ export default {
WorldGenCategory,
ForgeLoader,
FabricLoader,
Modrinth,
FlameAnvil,
SearchIcon,
},
async fetch() {
if (this.$route.query.q) this.query = this.$route.query.q
@@ -345,12 +372,12 @@ export default {
currentPage: 1,
sortTypes: [
{ display: 'Relevance', name: 'relevance' },
{ display: 'Total Downloads', name: 'downloads' },
{ display: 'Newest', name: 'newest' },
{ display: 'Updated', name: 'updated' },
{ display: 'Download count', name: 'downloads' },
{ display: 'Recently created', name: 'newest' },
{ display: 'Recently updated', name: 'updated' },
],
sortType: { display: 'Relevance', name: 'relevance' },
maxResults: 5,
maxResults: 20,
}
},
methods: {
@@ -482,7 +509,7 @@ export default {
url += `&v=${encodeURIComponent(this.selectedVersions)}`
if (this.sortType.name !== 'relevance')
url += `&s=${encodeURIComponent(this.sortType.name)}`
if (this.maxResults > 5)
if (this.maxResults > 20)
url += `&m=${encodeURIComponent(this.maxResults)}`
window.history.pushState(new Date(), 'Mods', url)
@@ -525,17 +552,32 @@ export default {
<style src="vue-multiselect/dist/vue-multiselect.min.css"></style>
<style lang="scss">
.search-bar {
.search-nav {
align-items: center;
display: flex;
justify-content: space-between;
flex-flow: column;
background: var(--color-raised-bg);
border-radius: var(--size-rounded-card);
padding: 0.25rem 1rem 0.25rem 1rem;
margin-bottom: var(--spacing-card-md);
input {
border: none;
background: transparent;
min-width: 200px;
}
.iconified-input {
width: 100%;
}
.sort-paginate {
margin-left: 0.5rem;
margin-right: 0.5rem;
display: flex;
width: 100%;
.per-page {
margin-left: 0.5rem;
display: none;
}
}
@media screen and (min-width: 900px) {
flex-flow: row;
@@ -543,34 +585,45 @@ export default {
width: auto;
}
.sort-paginate {
display: block;
width: auto;
}
}
@media screen and (min-width: 1024px) {
.sort-paginate {
.per-page {
display: unset;
}
}
}
}
.search-bottom {
align-items: center;
display: flex;
justify-content: flex-end;
background: var(--color-raised-bg);
border-radius: var(--size-rounded-card);
padding: 0.25rem 1rem 0.25rem 1rem;
select {
width: 100px;
margin-right: 20px;
}
}
.content {
min-height: 96vh;
.labeled-control {
h3 {
@extend %small-label;
margin-left: 0.5rem;
}
}
.mobile-filters-button {
display: inline-block;
button {
background: var(--color-bg);
color: var(--color-text);
border: 2px solid var(--color-grey-3);
border-radius: var(--size-rounded-sm);
padding: 0.5rem;
margin-top: 0;
height: 2.5rem;
padding-left: 1rem;
padding-right: 1rem;
}
// Hide button on larger screens where it's not needed
@@ -581,48 +634,47 @@ export default {
.filters {
overflow-y: auto;
background-color: var(--color-bg);
border-left: 1px solid var(--color-grey-2);
position: fixed;
width: 100vw;
right: -100vw;
max-height: 100vh;
min-width: 15%;
top: 3.5rem;
height: calc(100vh - 3.5rem);
top: var(--size-navbar-height);
height: calc(100vh - var(--size-navbar-height));
transition: right 150ms;
background-color: var(--color-raised-bg);
flex-shrink: 0; // Stop shrinking when page contents change
.filters-wrapper {
padding: 0 0.75rem;
padding: 0.25rem 0.75rem 0.75rem 0.75rem;
}
h3 {
color: #718096;
font-size: 0.8rem;
letter-spacing: 0.02rem;
margin-bottom: 0.5rem;
margin-top: 1.5rem;
text-transform: uppercase;
@extend %large-label;
margin-top: 1.25em;
}
// Larger screens that don't need to collapse
@media screen and (min-width: 900px) {
position: sticky;
width: 215px;
padding-right: 1rem;
transition: none;
}
// Desktop
@media screen and (min-width: 1145px) {
top: 0;
height: 100vh;
}
&.active {
right: 0;
}
// Larger screens that don't need to collapse
@media screen and (min-width: 900px) {
top: 0;
right: auto;
position: unset;
height: unset;
max-height: unset;
transition: none;
margin-left: var(--spacing-card-lg);
overflow-y: unset;
padding-right: 1rem;
width: 18vw;
background-color: transparent;
.filters-wrapper {
background-color: var(--color-raised-bg);
border-radius: var(--size-rounded-card);
}
}
@media screen and (min-width: 1024px) {
width: 300px;
}
}
.filter-group {
@@ -631,17 +683,6 @@ export default {
button {
cursor: pointer;
width: 100%;
padding: 5px 0;
outline: none;
color: var(--color-grey-5);
background-color: var(--color-grey-1);
border: none;
border-radius: 5px;
&:hover {
background-color: var(--color-grey-2);
color: var(--color-text);
}
}
.filter-button-done {
@@ -654,65 +695,12 @@ export default {
display: none;
}
}
// Desktop
@media screen and (min-width: 1145px) {
margin-top: 2em;
}
}
.iconified-select {
margin-left: 1em;
align-items: center;
display: inline-flex;
flex-direction: row-reverse;
select {
padding-left: 2.5rem;
&:hover {
& + svg {
color: var(--color-grey-6);
}
}
&:focus {
& + svg {
color: var(--color-text);
}
}
}
svg {
color: var(--color-grey-5);
margin-right: -2rem;
width: 24px;
height: 24px;
}
}
select {
width: 220px;
padding: 0.5rem 1rem;
background: var(--color-bg);
border: 2px solid var(--color-grey-3);
border-radius: var(--size-rounded-sm);
color: var(--color-grey-9);
font-size: 1rem;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
&:hover {
border-color: var(--color-grey-4);
color: var(--color-text);
}
}
.sort-types {
min-width: 200px;
border: 2px solid var(--color-grey-3);
border-radius: var(--size-rounded-sm);
border: none;
border-radius: var(--size-rounded-control);
.multiselect__tags {
padding: 10px 50px 0 8px;
@@ -723,45 +711,14 @@ select {
.no-results {
text-align: center;
padding: 20px 0;
font-size: 30px;
font-size: 1.25rem;
color: var(--color-text);
background-color: var(--color-grey-1);
margin-bottom: var(--spacing-card-md);
background: var(--color-raised-bg);
border-radius: var(--size-rounded-card);
}
.max-results {
max-width: 80px;
}
.multiselect__content-wrapper {
overflow-x: hidden;
}
.multiselect__tags {
border: 2px solid var(--color-grey-3);
border-radius: var(--size-rounded-sm);
}
.multiselect__tags,
.multiselect__spinner {
background: var(--color-bg);
cursor: pointer;
}
.multiselect__spinner::before,
.multiselect__spinner::after {
border-top-color: var(--color-brand);
}
.multiselect__option--selected.multiselect__option--highlight,
.multiselect__option,
.multiselect__single,
.multiselect__input {
color: var(--color-text);
background: var(--color-bg);
}
.multiselect__option--highlight,
.multiselect__tag {
background: var(--color-brand);
}
</style>

View File

@@ -1,178 +0,0 @@
<template>
<div class="main">
<h1>Privacy Policy</h1>
<p>
At Modrinth, accessible from https://modrinth.com, one of our main
priorities is the privacy of our visitors. This Privacy Policy document
contains types of information that is collected and recorded by Modrinth
and how we use it.
</p>
<p>
If you have additional questions or require more information about our
Privacy Policy, do not hesitate to contact us.
</p>
<h2>Log Files</h2>
<p>
Modrinth follows a standard procedure of using log files. These files log
visitors when they visit websites. All hosting companies do this and a
part of hosting services' analytics. The information collected by log
files include internet protocol (IP) addresses, browser type, Internet
Service Provider (ISP), date and time stamp, referring/exit pages, and
possibly the number of clicks. These are not linked to any information
that is personally identifiable. The purpose of the information is for
analyzing trends, administering the site, tracking users' movement on the
website, and gathering demographic information.
</p>
<h2>Cookies and Web Beacons</h2>
<p>
Like any other website, Modrinth uses 'cookies'. These cookies are used to
store information including visitors' preferences, and the pages on the
website that the visitor accessed or visited. The information is used to
optimize the users' experience by customizing our web page content based
on visitors' browser type and/or other information.
</p>
<p>
For more general information on cookies, please read
<a href="https://www.privacypolicies.com/blog/cookies/"
>"What Are Cookies"</a
>.
</p>
<h2>Our Advertising Partners</h2>
<p>
Some of advertisers on our site may use cookies and web beacons. Our
advertising partners are listed below. Each of our advertising partners
has their own Privacy Policy for their policies on user data. For easier
access, we hyperlinked to their Privacy Policies below.
</p>
<ul>
<li>
<p>EthicalAds</p>
<p>
<a href="https://www.ethicalads.io/privacy-policy/"
>https://www.ethicalads.io/privacy-policy/</a
>
</p>
</li>
</ul>
<h2>Privacy Policies</h2>
<P
>You may consult this list to find the Privacy Policy for each of the
advertising partners of Modrinth.</P
>
<p>
Third-party ad servers or ad networks uses technologies like cookies,
JavaScript, or Web Beacons that are used in their respective
advertisements and links that appear on Modrinth, which are sent directly
to users' browser. They automatically receive your IP address when this
occurs. These technologies are used to measure the effectiveness of their
advertising campaigns and/or to personalize the advertising content that
you see on websites that you visit.
</p>
<p>
Note that Modrinth has no access to or control over these cookies that are
used by third-party advertisers.
</p>
<h2>Third Party Privacy Policies</h2>
<p>
Modrinth's Privacy Policy does not apply to other advertisers or websites.
Thus, we are advising you to consult the respective Privacy Policies of
these third-party ad servers for more detailed information. It may include
their practices and instructions about how to opt-out of certain options.
</p>
<p>
You can choose to disable cookies through your individual browser options.
To know more detailed information about cookie management with specific
web browsers, it can be found at the browsers' respective websites. What
Are Cookies?
</p>
<h2>Children's Information</h2>
<p>
Another part of our priority is adding protection for children while using
the internet. We encourage parents and guardians to observe, participate
in, and/or monitor and guide their online activity.
</p>
<p>
Modrinth does not knowingly collect any Personal Identifiable Information
from children under the age of 13. If you think that your child provided
this kind of information on our website, we strongly encourage you to
contact us immediately and we will do our best efforts to promptly remove
such information from our records.
</p>
<h2>Online Privacy Policy Only</h2>
<p>
This Privacy Policy applies only to our online activities and is valid for
visitors to our website with regards to the information that they shared
and/or collect in Modrinth. This policy is not applicable to any
information collected offline or via channels other than this website.
</p>
<h2>Consent</h2>
<p>
By using our website, you hereby consent to our Privacy Policy and agree
to its Terms and Conditions.
</p>
</div>
</template>
<script>
export default {
auth: false,
layout: 'home',
head: {
title: 'Privacy - Modrinth',
meta: [
{
hid: 'description',
name: 'description',
content:
'The privacy policy of Modrinth, an open source modding platform. Modrinth currently supports Minecraft, including the forge and fabric mod loaders.',
},
{
hid: 'apple-mobile-web-app-title',
name: 'apple-mobile-web-app-title',
content: 'Privacy Policy',
},
{
hid: 'og:title',
name: 'og:title',
content: 'Privacy Policy',
},
{
hid: 'og:url',
name: 'og:url',
content: `https://modrinth.com/privacy`,
},
],
},
}
</script>
<style lang="scss" scoped>
.main {
margin: 0 auto;
max-width: 800px;
}
</style>

View File

@@ -1,187 +0,0 @@
<template>
<div class="main">
<h1>Terms and Conditions</h1>
<h2>1. Terms</h2>
<p>
By accessing this Website, accessible from https://modrinth.com, you are
agreeing to be bound by these Website Terms and Conditions of Use and
agree that you are responsible for the agreement with any applicable local
laws. If you disagree with any of these terms, you are prohibited from
accessing this site. The materials contained in this Website are protected
by copyright and trade mark law.
</p>
<h2>2. Use License</h2>
<p>
Permission is granted to temporarily download one copy of the materials on
Guavy LLC's Website for personal, non-commercial transitory viewing only.
This is the grant of a license, not a transfer of title, and under this
license you may not:
</p>
<ul>
<li>modify or copy the materials;</li>
<li>
use the materials for any commercial purpose or for any public display;
</li>
<li>
attempt to reverse engineer any software contained on Guavy LLC's
Website;
</li>
<li>
remove any copyright or other proprietary notations from the materials;
or
</li>
<li>
transferring the materials to another person or "mirror" the materials
on any other server.
</li>
</ul>
<p>
This will let Guavy LLC to terminate upon violations of any of these
restrictions. Upon termination, your viewing right will also be terminated
and you should destroy any downloaded materials in your possession whether
it is printed or electronic format.
</p>
<h2>3. Disclaimer</h2>
<p>
All the materials on Guavy LLCs Website are provided "as is". Guavy LLC
makes no warranties, may it be expressed or implied, therefore negates all
other warranties. Furthermore, Guavy LLC does not make any representations
concerning the accuracy or reliability of the use of the materials on its
Website or otherwise relating to such materials or any sites linked to
this Website.
</p>
<h2>4. Limitations</h2>
<p>
Guavy LLC or its suppliers will not be hold accountable for any damages
that will arise with the use or inability to use the materials on Guavy
LLCs Website, even if Guavy LLC or an authorize representative of this
Website has been notified, orally or written, of the possibility of such
damage. Some jurisdiction does not allow limitations on implied warranties
or limitations of liability for incidental damages, these limitations may
not apply to you.
</p>
<h2>5. Revisions and Errata</h2>
<p>
The materials appearing on Guavy LLCs Website may include technical,
typographical, or photographic errors. Guavy LLC will not promise that any
of the materials in this Website are accurate, complete, or current. Guavy
LLC may change the materials contained on its Website at any time without
notice. Guavy LLC does not make any commitment to update the materials.
</p>
<h2>6. Links</h2>
<p>
Guavy LLC has not reviewed all of the sites linked to its Website and is
not responsible for the contents of any such linked site. The presence of
any link does not imply endorsement by Guavy LLC of the site. The use of
any linked website is at the users own risk.
</p>
<h2>7. Site Terms of Use Modifications</h2>
<p>
Guavy LLC may revise these Terms of Use for its Website at any time
without prior notice. By using this Website, you are agreeing to be bound
by the current version of these Terms and Conditions of Use.
</p>
<h2>8. Your Privacy</h2>
<p>Please read our <nuxt-link to="/privacy"> Privacy Policy</nuxt-link>.</p>
<h2>9. Governing Law</h2>
<p>
Any claim related to Guavy LLC's Website shall be governed by the laws of
us without regards to its conflict of law provisions.
</p>
<h2>10. Content</h2>
<p>
When you upload text, software, mods, scripts, graphics, photos, audio,
videos, links, interactive features and other materials that may be viewed
on, or accessed through Modrinth, we refer to it as “Content”.
</p>
<ul>
<li>
You must own or have the necessary licenses, rights, consents, and
permissions to store, share or distribute the Content that is uploaded
under your Modrinth account.
</li>
<li>
You are responsible for all activity and Content that is uploaded under
your Modrinth account.
</li>
<li>
You must not transmit any viruses, worms, malware, or any other code of
a destructive nature through Modrinth.
</li>
<li>
You retain all of your ownership rights to your Content. We do not claim
any ownership in or to any of your Content.
</li>
<li>
To enable us to provide the services of Modrinth, you hereby grant us a
worldwide, non-exclusive, royalty-free, and unrestricted license to use,
reproduce, distribute copies, prepare derivative works of, or display
Content in connection with Modrinth in any medium and for any purpose
(including commercial purposes), which is irrevocable.
</li>
</ul>
</div>
</template>
<script>
export default {
auth: false,
layout: 'home',
head: {
title: 'TOS - Modrinth',
meta: [
{
hid: 'description',
name: 'description',
content:
'The Terms of Service of Modrinth, an open source modding platform. Modrinth currently supports Minecraft, including the forge and fabric mod loaders.',
},
{
hid: 'apple-mobile-web-app-title',
name: 'apple-mobile-web-app-title',
content: 'Terms of Service',
},
{
hid: 'og:title',
name: 'og:title',
content: 'Terms of Service',
},
{
hid: 'og:url',
name: 'og:url',
content: `https://modrinth.com/tos`,
},
],
},
}
</script>
<style lang="scss" scoped>
.main {
margin: 0 auto;
max-width: 800px;
}
</style>

View File

@@ -1,49 +1,94 @@
<template>
<div class="content">
<div class="user-profile">
<img :src="user.avatar_url" :alt="user.username" />
<div class="info">
<h1>{{ user.username }}</h1>
<p class="joined-text">Joined {{ $dayjs(user.created).fromNow() }}</p>
<p v-if="user.bio" class="bio">{{ user.bio }}</p>
<p v-if="user.role === 'admin'" class="badge red">Admin</p>
<p v-if="user.role === 'moderator'" class="badge yellow">Moderator</p>
<p v-if="user.role === 'developer'" class="badge green">Developer</p>
<div class="page-container">
<div class="page-contents">
<div class="sidebar-l">
<div class="card">
<div class="user-info">
<img :src="user.avatar_url" :alt="user.username" />
<div class="text">
<h2>{{ user.username }}</h2>
<p v-if="user.role === 'admin'" class="badge red">Admin</p>
<p v-if="user.role === 'moderator'" class="badge yellow">
Moderator
</p>
<p v-if="user.role === 'developer'" class="badge green">
Developer
</p>
</div>
</div>
<p v-if="user.bio" class="bio">{{ user.bio }}</p>
</div>
<div class="card stats">
<div class="stat">
<CalendarIcon />
<div class="info">
<h4>Created</h4>
<p
v-tooltip="
$dayjs(user.created).format(
'[Created on] YYYY-MM-DD [at] HH:mm A'
)
"
class="value"
>
{{ $dayjs(user.created).fromNow() }}
</p>
</div>
</div>
<div class="stat">
<DownloadIcon />
<div class="info">
<h4>Downloads</h4>
<p class="value">
{{ sumDownloads() }}
</p>
</div>
</div>
</div>
<m-footer class="footer" />
</div>
<div class="content">
<client-only>
<EthicalAd />
</client-only>
<div class="mods">
<SearchResult
v-for="result in mods"
:id="result.mod_id"
:key="result.mod_id"
:name="result.title"
:description="result.description"
:created-at="result.published"
:updated-at="result.updated"
:downloads="result.downloads.toString()"
:icon-url="result.icon_url"
:author-url="result.author_url"
:categories="result.categories"
:is-modrinth="true"
/>
</div>
</div>
</div>
<client-only>
<EthicalAd />
</client-only>
<div class="user-mods">
<SearchResult
v-for="result in mods"
:id="result.mod_id"
:key="result.mod_id"
:name="result.title"
:description="result.description"
:latest-version="result.versions[0]"
:created-at="result.published"
:updated-at="result.updated"
:downloads="result.downloads.toString()"
:icon-url="result.icon_url"
:author-url="result.author_url"
:page-url="result.page_url"
:categories="result.categories"
/>
</div>
</div>
</template>
<script>
import axios from 'axios'
import SearchResult from '@/components/ModResult'
import SearchResult from '@/components/ProjectCard'
import EthicalAd from '@/components/EthicalAd'
import MFooter from '@/components/MFooter'
import CalendarIcon from '~/assets/images/utils/calendar.svg?inline'
import DownloadIcon from '~/assets/images/utils/download.svg?inline'
export default {
auth: false,
components: {
EthicalAd,
SearchResult,
CalendarIcon,
DownloadIcon,
MFooter,
},
async asyncData(data) {
let res = await axios.get(
@@ -67,56 +112,61 @@ export default {
user,
}
},
methods: {
formatNumber(x) {
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
},
sumDownloads() {
let sum = 0
for (const mod of this.mods) {
sum += mod.downloads
}
return this.formatNumber(sum)
},
},
}
</script>
<style lang="scss" scoped>
.user-profile {
@media screen and (min-width: 900px) {
display: inline-flex;
text-align: left;
.sidebar-l {
min-width: 21rem;
.user-info {
@extend %row;
img {
width: 6rem;
height: 6rem;
margin-right: var(--spacing-card-md);
border-radius: var(--size-rounded-icon);
}
.text {
h2 {
margin: 0;
color: var(--color-text-dark);
font-size: var(--font-size-lg);
}
.badge {
display: inline-block;
}
}
}
text-align: center;
margin-bottom: 20px;
margin-left: 15px;
.stats {
display: flex;
flex-wrap: wrap;
.stat {
@extend %stat;
img {
border-radius: var(--size-rounded-md);
width: 250px;
height: 250px;
}
.info {
@media screen and (min-width: 900px) {
margin-left: 15px;
}
h1 {
margin-bottom: 0;
}
.joined-text {
margin-top: 5px;
color: var(--color-grey-4);
}
.bio {
margin-top: 5px;
font-size: 16pt;
}
.badge {
display: inline-block;
svg {
padding: 0.25rem;
border-radius: 50%;
background-color: var(--color-button-bg);
}
}
}
}
.user-mods {
border-top: 1px solid var(--color-grey-1);
padding-top: 10px;
margin: 10px;
* {
margin-left: 0;
}
.mods {
}
</style>

View File

@@ -0,0 +1,17 @@
import Vue from 'vue'
import xss from 'xss'
import marked from 'marked'
function compileMarkdown(target, markdown) {
target.innerHTML = xss(marked(markdown))
}
Vue.directive('compiled-markdown', {
bind(el, binding, vnode) {
compileMarkdown(el, binding.value)
},
update(el, binding, vnode) {
compileMarkdown(el, binding.value)
},
})