Migrate to Nuxt 3 (#933)

* Migrate to Nuxt 3

* Update vercel config

* remove tsconfig comment

* Changelog experiment + working proj pages

* Fix package json

* Prevent vercel complaining

* fix deploy (hopefully)

* Tag generator

* Switch to yarn

* Vercel pls 🙏

* Fix tag generation bug

* Make (most) non-logged in pages work

* fix base build

* Linting + state

* Eradicate axios, make most user pages work

* Fix checkbox state being set incorrectly

* Make most things work

* Final stretch

* Finish (most) things

* Move to update model value

* Fix modal text getting blurred from transforms (#964)

* Adjust nav-link border radius when focused (#961)

* Transition between animation states on TextLogo (#955)

* Transition between animation states on TextLogo

* Remove unused refs

* Fixes from review

* Disable tabbing to pagination arrows when disabled (#972)

* Make position of the "no results" text on grid/gallery views consistent (fixes #963) (#965)

* Fix position of the "no results" text on grid view

* fix padding

* Remove extra margin on main page, fixes #957 (#959)

* Fix layout shift and placeholder line height (#973)

* Fix a lot of issues

* Fix more nuxt 3 issues

* fix not all versions showing up (temp)

* inline inter css file

* More nuxt 3 fixes

* [skip ci] broken- backup changes

* Change modpack warnings to blue instead of red (#991)

* Fix some hydration issues

* Update nuxt

* Fix some images not showing

* Add pagination to versions page + fix lag

* Make changelog page consistent with versions page

* sync before merge

* Delete old file

* Fix actions failing

* update branch

* Fixes navbar transition animation. (#1012)

* Fixes navbar transition animation.

* Fixes Y-axis animation. Fixes mobile menu. Removes highlightjs prop.

* Changes xss call to renderString.

* Fixes renderString call.

* Removes unnecessary styling.

* Reverts mobile nav change.

* Nuxt 3 Lazy Loading Search (#1022)

* Uses lazyFetch for results. onSearchChange refreshes. Adds loading circle.

* Removes console.log

* Preserves old page when paging.

* Diagnosing filtering bugs.

* Fix single facet filtering

* Implements useAuth in settings/account.

* tiny ssr fix

* Updating nuxt.config checklist.

* Implements useAuth in revenue, moneitzation, and dashboard index pages.

* Fixes setups.

* Eliminates results when path changes. Adds animated logo.

* Ensures loading animation renders on search page.

---------

Co-authored-by: Jai A <jaiagr+gpg@pm.me>

* Fix navigation issues

* Square button fix (#1023)

* Removes checklist from nuxt.config.

* Modifies Nuxt CI to build after linting.

* Fixes prettierignore file.

* bug fixes

* Update whitelist domains

* Page improvements, fix CLS

* Fix a lot of things

* Fix project type redirect

* Fix 404 errors

* Fix user settings + hydration error

* Final fixes

* fix(creator-section): border radius on icons not aligning with bg (#1027)

Co-authored-by: MagnusHJensen <magnus.holm.jensen@lego.dk>

* Improvements to the mobile navbar (#984)

* Transition between animation states on TextLogo

* Remove unused refs

* Fixes from review

* Improvements to the mobile nav menu

* fix avatar alt text

* Nevermind, got confused for a moment

* Tab bar, menu layout improvements

* Highlight search icon when menu is open

* Update layouts/default.vue

Co-authored-by: Magnus Jensen <magnushjensen.mail@gmail.com>

* Fix some issues

* Use caret instead

* Run prettier

* Add create a project

---------

Co-authored-by: Magnus Jensen <magnushjensen.mail@gmail.com>
Co-authored-by: Geometrically <18202329+Geometrically@users.noreply.github.com>
Co-authored-by: Jai A <jaiagr+gpg@pm.me>

* Fix mobile menu issues

* More issues

* Fix lint

---------

Co-authored-by: Kaeden Murphy <kmurphy@kaedenmurphy.dev>
Co-authored-by: triphora <emmaffle@modrinth.com>
Co-authored-by: Zach Baird <30800863+ZachBaird@users.noreply.github.com>
Co-authored-by: stairman06 <36215135+stairman06@users.noreply.github.com>
Co-authored-by: Zachary Baird <zdb1994@yahoo.com>
Co-authored-by: Magnus Jensen <magnushjensen.mail@gmail.com>
Co-authored-by: MagnusHJensen <magnus.holm.jensen@lego.dk>
This commit is contained in:
Geometrically
2023-03-09 10:05:32 -07:00
committed by GitHub
parent 5638f0f24b
commit 740357d120
145 changed files with 12371 additions and 37478 deletions

View File

@@ -5,51 +5,36 @@
<div class="MYYLVTXBPUVWMLVBPVSDLHADDRYFBF-2">
<a
href="https://exaroton.com/?utm_source=modrinth&utm_medium=text&utm_campaign=host&utm_content=top"
rel="noopener noreferrer nofollow sponsored"
rel="noopener nofollow sponsored"
target="_blank"
>
<ColorScheme>
<LightIcon
v-if="$colorMode.value === 'light'"
class="MYYLVTXBPUVWMLVBPVSDLHADDRYFBF-3"
/>
<DarkIcon v-else class="MYYLVTXBPUVWMLVBPVSDLHADDRYFBF-3" />
</ColorScheme>
<LightIcon
v-if="colorMode.value === 'light'"
class="MYYLVTXBPUVWMLVBPVSDLHADDRYFBF-3"
/>
<DarkIcon v-else class="MYYLVTXBPUVWMLVBPVSDLHADDRYFBF-3" />
<span>
<span> Host your Minecraft server on </span>
<strong>exaroton</strong>
<span>
- only pay while the server is running - billed per second.
</span>
<span> - only pay while the server is running - billed per second. </span>
</span>
</a>
</div>
</div>
<div class="MYYLVTXBPUVWMLVBPVSDLHADDRYFBF-4">
<a
rel="noopener noreferrer nofollow sponsored"
target="_blank"
href="https://adrinth.com"
>
Ads via Adrinth
</a>
<a rel="noopener sponsored" target="_blank" href="https://adrinth.com"> Ads via Adrinth </a>
</div>
</div>
</div>
</template>
<script>
import LightIcon from '~/assets/images/external/exaroton-light.svg?inline'
import DarkIcon from '~/assets/images/external/exaroton-dark.svg?inline'
<script setup>
import LightIcon from '~/assets/images/external/exaroton-light.svg'
import DarkIcon from '~/assets/images/external/exaroton-dark.svg'
export default {
components: {
LightIcon,
DarkIcon,
},
}
const colorMode = useTheme()
</script>
<style lang="scss">
<style>
.MYYLVTXBPUVWMLVBPVSDLHADDRYFBF {
position: relative;
margin-bottom: var(--spacing-card-md);

View File

@@ -5,17 +5,9 @@
width="100%"
height="100%"
viewBox="0 0 590 591"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xml:space="preserve"
xmlns:serif="http://www.serif.com/"
style="
fill-rule: evenodd;
clip-rule: evenodd;
stroke-linejoin: round;
stroke-miterlimit: 2;
"
style="fill-rule: evenodd; clip-rule: evenodd; stroke-linejoin: round; stroke-miterlimit: 2"
>
<g transform="matrix(1,0,0,1,652.392,-0.400578)">
<g transform="matrix(4.16667,0,0,4.16667,-735.553,0)">
@@ -26,23 +18,16 @@
/>
</g>
</g>
</g></svg
><svg
</g>
</svg>
<svg
class="rotate inner"
width="100%"
height="100%"
viewBox="0 0 590 591"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xml:space="preserve"
xmlns:serif="http://www.serif.com/"
style="
fill-rule: evenodd;
clip-rule: evenodd;
stroke-linejoin: round;
stroke-miterlimit: 2;
"
style="fill-rule: evenodd; clip-rule: evenodd; stroke-linejoin: round; stroke-miterlimit: 2"
>
<g transform="matrix(1,0,0,1,652.392,-0.400578)">
<g transform="matrix(4.16667,0,0,4.16667,-735.553,0)">
@@ -59,17 +44,9 @@
width="100%"
height="100%"
viewBox="0 0 590 591"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xml:space="preserve"
xmlns:serif="http://www.serif.com/"
style="
fill-rule: evenodd;
clip-rule: evenodd;
stroke-linejoin: round;
stroke-miterlimit: 2;
"
style="fill-rule: evenodd; clip-rule: evenodd; stroke-linejoin: round; stroke-miterlimit: 2"
>
<g transform="matrix(1,0,0,1,652.392,-0.400578)">
<g transform="matrix(4.16667,0,0,4.16667,-735.553,0)">

View File

@@ -7,7 +7,7 @@
stroke-miterlimit="2"
clip-rule="evenodd"
viewBox="0 0 3307 593"
:class="{ animate: $nuxt.$loading ? $nuxt.$loading.show : false }"
:class="{ animate: loading }"
>
<path
fill-rule="nonzero"
@@ -30,6 +30,10 @@
</svg>
</template>
<script setup>
const loading = useLoading()
</script>
<style lang="scss" scoped>
.animate {
.ring {

View File

@@ -2,18 +2,14 @@
<img
v-if="src"
ref="img"
:class="`avatar size-${size} ${circle ? 'circle' : ''} ${
noShadow ? 'no-shadow' : ''
}`"
:class="`avatar size-${size} ${circle ? 'circle' : ''} ${noShadow ? 'no-shadow' : ''}`"
:src="src"
:alt="alt"
:loading="loading"
/>
<svg
v-else
:class="`avatar size-${size} ${circle ? 'circle' : ''} ${
noShadow ? 'no-shadow' : ''
}`"
:class="`avatar size-${size} ${circle ? 'circle' : ''} ${noShadow ? 'no-shadow' : ''}`"
xml:space="preserve"
fill-rule="evenodd"
stroke-linecap="round"
@@ -35,7 +31,6 @@
<script>
export default {
name: 'Avatar',
props: {
src: {
type: String,
@@ -68,10 +63,7 @@ export default {
mounted() {
if (this.$refs.img && this.$refs.img.naturalWidth) {
const isPixelated = () => {
if (
this.$refs.img.naturalWidth < 96 &&
this.$refs.img.naturalWidth > 0
) {
if (this.$refs.img.naturalWidth < 96 && this.$refs.img.naturalWidth > 0) {
this.$refs.img.style.imageRendering = 'pixelated'
}
}

View File

@@ -1,16 +1,10 @@
<template>
<span :class="'version-badge ' + color + ' type--' + type">
<template v-if="color">
<span class="circle" /> {{ $capitalizeString(type) }}
</template>
<template v-if="color"> <span class="circle" /> {{ $capitalizeString(type) }} </template>
<!-- User roles -->
<template v-else-if="type === 'admin'">
<ModrinthIcon /> Modrinth Team
</template>
<template v-else-if="type === 'moderator'">
<ModeratorIcon /> Moderator
</template>
<template v-else-if="type === 'admin'"> <ModrinthIcon /> Modrinth Team </template>
<template v-else-if="type === 'moderator'"> <ModeratorIcon /> Moderator </template>
<template v-else-if="type === 'creator'"><CreatorIcon /> Creator</template>
<!-- Project statuses -->
@@ -18,45 +12,34 @@
<template v-else-if="type === 'unlisted'"><EyeOffIcon /> Unlisted</template>
<template v-else-if="type === 'withheld'"><EyeOffIcon /> Withheld</template>
<template v-else-if="type === 'private'"><LockIcon /> Private</template>
<template v-else-if="type === 'scheduled'">
<CalendarIcon /> Scheduled
</template>
<template v-else-if="type === 'scheduled'"> <CalendarIcon /> Scheduled </template>
<template v-else-if="type === 'draft'"><DraftIcon /> Draft</template>
<template v-else-if="type === 'archived'">
<ArchiveIcon /> Archived
</template>
<template v-else-if="type === 'archived'"> <ArchiveIcon /> Archived </template>
<template v-else-if="type === 'rejected'"><CrossIcon /> Rejected</template>
<template v-else-if="type === 'processing'">
<ProcessingIcon /> Under review
</template>
<template v-else-if="type === 'processing'"> <ProcessingIcon /> Under review </template>
<!-- Team members -->
<template v-else-if="type === 'accepted'"><CheckIcon /> Accepted</template>
<template v-else-if="type === 'pending'">
<ProcessingIcon /> Pending
</template>
<template v-else>
<span class="circle" /> {{ $capitalizeString(type) }}
</template>
<template v-else-if="type === 'pending'"> <ProcessingIcon /> Pending </template>
<template v-else> <span class="circle" /> {{ $capitalizeString(type) }} </template>
</span>
</template>
<script>
import ModrinthIcon from '~/assets/images/logo.svg?inline'
import ModeratorIcon from '~/assets/images/sidebar/admin.svg?inline'
import CreatorIcon from '~/assets/images/utils/box.svg?inline'
import ListIcon from '~/assets/images/utils/list.svg?inline'
import EyeOffIcon from '~/assets/images/utils/eye-off.svg?inline'
import DraftIcon from '~/assets/images/utils/file-text.svg?inline'
import CrossIcon from '~/assets/images/utils/x.svg?inline'
import ArchiveIcon from '~/assets/images/utils/archive.svg?inline'
import ProcessingIcon from '~/assets/images/utils/updated.svg?inline'
import CheckIcon from '~/assets/images/utils/check.svg?inline'
import LockIcon from '~/assets/images/utils/lock.svg?inline'
import CalendarIcon from '~/assets/images/utils/calendar.svg?inline'
import ModrinthIcon from '~/assets/images/logo.svg'
import ModeratorIcon from '~/assets/images/sidebar/admin.svg'
import CreatorIcon from '~/assets/images/utils/box.svg'
import ListIcon from '~/assets/images/utils/list.svg'
import EyeOffIcon from '~/assets/images/utils/eye-off.svg'
import DraftIcon from '~/assets/images/utils/file-text.svg'
import CrossIcon from '~/assets/images/utils/x.svg'
import ArchiveIcon from '~/assets/images/utils/archive.svg'
import ProcessingIcon from '~/assets/images/utils/updated.svg'
import CheckIcon from '~/assets/images/utils/check.svg'
import LockIcon from '~/assets/images/utils/lock.svg'
import CalendarIcon from '~/assets/images/utils/calendar.svg'
export default {
name: 'Badge',
components: {
ModrinthIcon,
ListIcon,

View File

@@ -9,25 +9,26 @@
class="checkbox"
role="checkbox"
:disabled="disabled"
:class="{ checked: value, collapsing: collapsingToggleStyle }"
:aria-label="description"
:aria-checked="value"
:class="{ checked: modelValue, collapsing: collapsingToggleStyle }"
:aria-label="description ?? label"
:aria-checked="modelValue"
>
<CheckIcon v-if="value && !collapsingToggleStyle" aria-hidden="true" />
<CheckIcon v-if="modelValue && !collapsingToggleStyle" aria-hidden="true" />
<DropdownIcon v-else-if="collapsingToggleStyle" aria-hidden="true" />
</button>
<!-- aria-hidden is set so screenreaders only use the <button>'s aria-label -->
<p v-if="label" aria-hidden="true">{{ label }}</p>
<p v-if="label" aria-hidden="true">
{{ label }}
</p>
<slot v-else />
</div>
</template>
<script>
import CheckIcon from '~/assets/images/utils/check.svg?inline'
import DropdownIcon from '~/assets/images/utils/dropdown.svg?inline'
import CheckIcon from '~/assets/images/utils/check.svg'
import DropdownIcon from '~/assets/images/utils/dropdown.svg'
export default {
name: 'Checkbox',
components: {
CheckIcon,
DropdownIcon,
@@ -43,9 +44,9 @@ export default {
},
description: {
type: String,
default: '',
default: null,
},
value: Boolean,
modelValue: Boolean,
clickEvent: {
type: Function,
default: () => {},
@@ -55,10 +56,11 @@ export default {
default: false,
},
},
emits: ['update:modelValue'],
methods: {
toggle() {
if (!this.disabled) {
this.$emit('input', !this.value)
this.$emit('update:modelValue', !this.modelValue)
}
},
},

View File

@@ -14,15 +14,14 @@
</template>
<script>
import CheckIcon from '~/assets/images/utils/check.svg?inline'
import CheckIcon from '~/assets/images/utils/check.svg'
export default {
name: 'Chips',
components: {
CheckIcon,
},
props: {
value: {
modelValue: {
required: true,
type: String,
},
@@ -39,13 +38,14 @@ export default {
type: Function,
},
},
emits: ['update:modelValue'],
computed: {
selected: {
get() {
return this.value
return this.modelValue
},
set(value) {
this.$emit('input', value)
this.$emit('update:modelValue', value)
},
},
},

View File

@@ -1,10 +1,5 @@
<template>
<button
class="code"
:class="{ copied }"
title="Copy code to clipboard"
@click="copyText"
>
<button class="code" :class="{ copied }" title="Copy code to clipboard" @click="copyText">
{{ text }}
<CheckIcon v-if="copied" />
<ClipboardCopyIcon v-else />
@@ -12,11 +7,10 @@
</template>
<script>
import CheckIcon from '~/assets/images/utils/check.svg?inline'
import ClipboardCopyIcon from '~/assets/images/utils/clipboard-copy.svg?inline'
import CheckIcon from '~/assets/images/utils/check.svg'
import ClipboardCopyIcon from '~/assets/images/utils/clipboard-copy.svg'
export default {
name: 'CopyCode',
components: {
CheckIcon,
ClipboardCopyIcon,
@@ -54,8 +48,8 @@ export default {
width: min-content;
border-radius: 10px;
user-select: text;
transition: opacity 0.5s ease-in-out, filter 0.2s ease-in-out,
transform 0.05s ease-in-out, outline 0.2s ease-in-out;
transition: opacity 0.5s ease-in-out, filter 0.2s ease-in-out, transform 0.05s ease-in-out,
outline 0.2s ease-in-out;
svg {
width: 1em;

View File

@@ -19,13 +19,13 @@
<script>
export default {
name: 'DropArea',
props: {
accept: {
type: String,
default: '',
},
},
emits: ['change'],
data() {
return {
fileAllowed: false,
@@ -38,28 +38,25 @@ export default {
allowDrag(event) {
const file = event.dataTransfer?.items[0]
console.log(file)
if (
file &&
this.accept
.split(',')
.reduce(
(acc, t) =>
acc || file.type.startsWith(t) || file.type === t || t === '*',
false
)
.reduce((acc, t) => acc || file.type.startsWith(t) || file.type === t || t === '*', false)
) {
this.fileAllowed = true
event.dataTransfer.dropEffect = 'copy'
event.preventDefault()
if (this.$refs.drop_area)
if (this.$refs.drop_area) {
this.$refs.drop_area.style.visibility = 'visible'
}
} else {
this.fileAllowed = false
if (this.$refs.drop_area)
if (this.$refs.drop_area) {
this.$refs.drop_area.style.visibility = 'hidden'
}
}
},
},

View File

@@ -15,9 +15,7 @@
<GlobeIcon aria-hidden="true" />
Client or server
</template>
<template
v-else-if="clientSide === 'required' && serverSide === 'required'"
>
<template v-else-if="clientSide === 'required' && serverSide === 'required'">
<GlobeIcon aria-hidden="true" />
Client and server
</template>
@@ -39,9 +37,7 @@
<ServerIcon aria-hidden="true" />
Server
</template>
<template
v-else-if="serverSide === 'unsupported' && clientSide === 'unsupported'"
>
<template v-else-if="serverSide === 'unsupported' && clientSide === 'unsupported'">
<GlobeIcon aria-hidden="true" />
Unsupported
</template>
@@ -52,12 +48,11 @@
</span>
</template>
<script>
import InfoIcon from '~/assets/images/utils/info.svg?inline'
import ClientIcon from '~/assets/images/utils/client.svg?inline'
import GlobeIcon from '~/assets/images/utils/globe.svg?inline'
import ServerIcon from '~/assets/images/utils/server.svg?inline'
import InfoIcon from '~/assets/images/utils/info.svg'
import ClientIcon from '~/assets/images/utils/client.svg'
import GlobeIcon from '~/assets/images/utils/globe.svg'
import ServerIcon from '~/assets/images/utils/server.svg'
export default {
name: 'EnvironmentIndicator',
components: {
InfoIcon,
ClientIcon,

View File

@@ -1,25 +1,15 @@
<template>
<label
:class="{ 'long-style': longStyle }"
@drop.prevent="handleDrop"
@dragover.prevent
>
<slot></slot>
<label :class="{ 'long-style': longStyle }" @drop.prevent="handleDrop" @dragover.prevent>
<slot />
{{ prompt }}
<input
type="file"
:multiple="multiple"
:accept="accept"
@change="handleChange"
/>
<input type="file" :multiple="multiple" :accept="accept" @change="handleChange" />
</label>
</template>
<script>
import { fileIsValid } from '~/plugins/fileUtils'
import { fileIsValid } from '~/helpers/fileUtils'
export default {
name: 'FileInput',
components: {},
props: {
prompt: {
@@ -54,6 +44,7 @@ export default {
default: false,
},
},
emits: ['change'],
data() {
return {
files: [],
@@ -61,12 +52,12 @@ export default {
},
methods: {
addFiles(files, shouldNotReset) {
if (!shouldNotReset || this.shouldAlwaysReset) this.files = files
if (!shouldNotReset || this.shouldAlwaysReset) {
this.files = files
}
const validationOptions = { maxSize: this.maxSize, alertOnInvalid: true }
this.files = [...this.files].filter((file) =>
fileIsValid(file, validationOptions)
)
this.files = [...this.files].filter((file) => fileIsValid(file, validationOptions))
if (this.files.length > 0) {
this.$emit('change', this.files)

View File

@@ -8,25 +8,26 @@
class="modal-overlay"
@click="hide"
/>
<div class="modal-body" :class="{ shown: shown }">
<div v-if="header" class="header">
<h1>{{ header }}</h1>
<button class="iconified-button icon-only transparent" @click="hide">
<CrossIcon />
</button>
</div>
<div class="content">
<slot></slot>
<div class="modal-container" :class="{ shown }">
<div class="modal-body">
<div v-if="header" class="header">
<h1>{{ header }}</h1>
<button class="iconified-button icon-only transparent" @click="hide">
<CrossIcon />
</button>
</div>
<div class="content">
<slot />
</div>
</div>
</div>
</div>
</template>
<script>
import CrossIcon from '~/assets/images/utils/x.svg?inline'
import CrossIcon from '~/assets/images/utils/x.svg'
export default {
name: 'Modal',
components: {
CrossIcon,
},
@@ -61,7 +62,6 @@ export default {
width: 100%;
height: 100%;
z-index: 20;
transition: all 0.3s ease-in-out;
&.shown {
@@ -76,46 +76,61 @@ export default {
}
}
.modal-body {
.modal-container {
position: fixed;
left: 50%;
transform: translate(-50%, -50%);
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
z-index: 21;
box-shadow: var(--shadow-raised), var(--shadow-inset);
border-radius: var(--size-rounded-lg);
max-height: calc(100% - 2 * var(--spacing-card-bg));
overflow-y: auto;
width: 600px;
visibility: hidden;
pointer-events: none;
.header {
display: flex;
justify-content: space-between;
align-items: center;
background-color: var(--color-bg);
padding: var(--spacing-card-md) var(--spacing-card-lg);
h1 {
font-size: 1.25rem;
&.shown {
visibility: visible;
.modal-body {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
}
.content {
background-color: var(--color-raised-bg);
}
.modal-body {
position: fixed;
box-shadow: var(--shadow-raised), var(--shadow-inset);
border-radius: var(--size-rounded-lg);
max-height: calc(100% - 2 * var(--spacing-card-bg));
overflow-y: auto;
width: 600px;
pointer-events: auto;
top: calc(100% + 400px);
visibility: hidden;
opacity: 0;
transition: all 0.25s ease-in-out;
.header {
display: flex;
justify-content: space-between;
align-items: center;
background-color: var(--color-bg);
padding: var(--spacing-card-md) var(--spacing-card-lg);
&.shown {
opacity: 1;
visibility: visible;
top: 50%;
}
h1 {
font-size: 1.25rem;
}
}
@media screen and (max-width: 650px) {
width: calc(100% - 2 * var(--spacing-card-bg));
.content {
background-color: var(--color-raised-bg);
}
transform: translateY(50vh);
visibility: hidden;
opacity: 0;
transition: all 0.25s ease-in-out;
@media screen and (max-width: 650px) {
width: calc(100% - 2 * var(--spacing-card-bg));
}
}
}
</style>

View File

@@ -1,7 +1,7 @@
<template>
<Modal ref="modal" :header="title">
<div class="modal-delete">
<div class="markdown-body" v-html="$xss($md.render(description))"></div>
<div class="markdown-body" v-html="renderString(description)" />
<label v-if="hasToType" for="confirmation" class="confirmation-label">
<span>
<strong>To verify, type</strong>
@@ -24,11 +24,7 @@
<CrossIcon />
Cancel
</button>
<button
class="iconified-button danger-button"
:disabled="action_disabled"
@click="proceed"
>
<button class="iconified-button danger-button" :disabled="action_disabled" @click="proceed">
<TrashIcon />
{{ proceedLabel }}
</button>
@@ -38,12 +34,12 @@
</template>
<script>
import CrossIcon from '~/assets/images/utils/x.svg?inline'
import TrashIcon from '~/assets/images/utils/trash.svg?inline'
import CrossIcon from '~/assets/images/utils/x.svg'
import TrashIcon from '~/assets/images/utils/trash.svg'
import Modal from '~/components/ui/Modal'
import { renderString } from '~/helpers/parse'
export default {
name: 'ModalConfirm',
components: {
CrossIcon,
TrashIcon,
@@ -73,6 +69,7 @@ export default {
default: 'Proceed',
},
},
emits: ['proceed'],
data() {
return {
action_disabled: this.hasToType,
@@ -80,6 +77,7 @@ export default {
}
},
methods: {
renderString,
cancel() {
this.$refs.modal.hide()
},
@@ -90,8 +88,7 @@ export default {
type() {
if (this.hasToType) {
this.action_disabled =
this.confirmation_typed.toLowerCase() !==
this.confirmationText.toLowerCase()
this.confirmation_typed.toLowerCase() !== this.confirmationText.toLowerCase()
}
},
show() {

View File

@@ -2,15 +2,10 @@
<Modal ref="modal" header="Create a project">
<div class="modal-creation universal-labels">
<div class="markdown-body">
<p>
New projects are created as drafts and can be found under your profile
page.
</p>
<p>New projects are created as drafts and can be found under your profile page.</p>
</div>
<label for="project-type">
<span class="label__title"
>Project type<span class="required">*</span></span
>
<span class="label__title">Project type<span class="required">*</span></span>
</label>
<Chips
id="project-type"
@@ -34,9 +29,7 @@
</label>
<div class="text-input-wrapper">
<div class="text-input-wrapper__before">
https://modrinth.com/{{
getProjectType() ? getProjectType().id : '???'
}}/
https://modrinth.com/{{ getProjectType() ? getProjectType().id : '???' }}/
</div>
<input
id="slug"
@@ -50,16 +43,11 @@
<label for="additional-information">
<span class="label__title">Summary<span class="required">*</span></span>
<span class="label__description"
>This appears in search and on the sidebar of your project's
page.</span
>This appears in search and on the sidebar of your project's page.</span
>
</label>
<div class="textarea-wrapper">
<textarea
id="additional-information"
v-model="description"
maxlength="256"
/>
<textarea id="additional-information" v-model="description" maxlength="256" />
</div>
<div class="push-right input-group">
<button class="iconified-button" @click="cancel">
@@ -76,13 +64,12 @@
</template>
<script>
import CrossIcon from '~/assets/images/utils/x.svg?inline'
import CheckIcon from '~/assets/images/utils/right-arrow.svg?inline'
import CrossIcon from '~/assets/images/utils/x.svg'
import CheckIcon from '~/assets/images/utils/right-arrow.svg'
import Modal from '~/components/ui/Modal'
import Chips from '~/components/ui/Chips'
export default {
name: 'ModalCreation',
components: {
Chips,
CrossIcon,
@@ -144,7 +131,7 @@ export default {
}
},
async createProject() {
this.$nuxt.$loading.start()
startLoading()
const projectType = this.getProjectType()
@@ -175,12 +162,11 @@ export default {
)
try {
await this.$axios({
url: 'project',
await useBaseFetch('project', {
method: 'POST',
data: formData,
body: formData,
headers: {
'Content-Type': 'multipart/form-data',
'Content-Disposition': formData,
Authorization: this.$auth.token,
},
})
@@ -191,6 +177,8 @@ export default {
params: {
type: projectType.id,
id: this.slug,
},
state: {
overrideProjectType: projectType.id,
},
})
@@ -198,11 +186,11 @@ export default {
this.$notify({
group: 'main',
title: 'An error occurred',
text: err.response.data.description,
text: err.data.description,
type: 'error',
})
}
this.$nuxt.$loading.finish()
stopLoading()
},
show() {
this.projectType = this.$tag.projectTypes[0].display

View File

@@ -2,27 +2,18 @@
<Modal ref="modal" header="Project moderation">
<div v-if="project !== null" class="moderation-modal universal-body">
<p>
A moderation message is optional, but it can be used to communicate
problems with a project's team members. The body is also optional and
supports markdown formatting!
A moderation message is optional, but it can be used to communicate problems with a
project's team members. The body is also optional and supports markdown formatting!
</p>
<div v-if="status" class="status">
<span>New project status: </span>
<Badge :type="status" />
</div>
<h3>Message title</h3>
<input
v-model="moderationMessage"
type="text"
placeholder="Enter the message..."
/>
<input v-model="moderationMessage" type="text" placeholder="Enter the message..." />
<h3>Message body</h3>
<div class="textarea-wrapper">
<Chips
v-model="bodyViewMode"
class="separator"
:items="['source', 'preview']"
/>
<Chips v-model="bodyViewMode" class="separator" :items="['source', 'preview']" />
<textarea
v-if="bodyViewMode === 'source'"
id="body"
@@ -34,20 +25,17 @@
: 'You must add a title before you add a body.'
"
/>
<div
v-else
v-highlightjs
class="markdown-body preview"
v-html="$xss($md.render(moderationMessageBody))"
></div>
<div v-else class="markdown-body preview" v-html="renderString(moderationMessageBody)" />
</div>
<div class="push-right input-group">
<button
v-if="moderationMessage || moderationMessageBody"
class="iconified-button"
@click="
moderationMessage = ''
moderationMessageBody = ''
() => {
moderationMessage = ''
moderationMessageBody = ''
}
"
>
<TrashIcon />
@@ -67,15 +55,15 @@
</template>
<script>
import TrashIcon from '~/assets/images/utils/trash.svg?inline'
import CrossIcon from '~/assets/images/utils/x.svg?inline'
import TrashIcon from '~/assets/images/utils/trash.svg'
import CrossIcon from '~/assets/images/utils/x.svg'
import Modal from '~/components/ui/Modal'
import Chips from '~/components/ui/Chips'
import Badge from '~/components/ui/Badge'
import CheckIcon from '~/assets/images/utils/check.svg?inline'
import CheckIcon from '~/assets/images/utils/check.svg'
import { renderString } from '~/helpers/parse'
export default {
name: 'ModalModeration',
components: {
TrashIcon,
CrossIcon,
@@ -102,9 +90,7 @@ export default {
return {
bodyViewMode: 'source',
moderationMessage:
this.project && this.project.moderation_message
? this.project.moderation_message
: '',
this.project && this.project.moderation_message ? this.project.moderation_message : '',
moderationMessageBody:
this.project && this.project.moderation_message_body
? this.project.moderation_message_body
@@ -112,26 +98,23 @@ export default {
}
},
methods: {
renderString,
async saveProject() {
this.$nuxt.$loading.start()
startLoading()
try {
const data = {
moderation_message: this.moderationMessage
? this.moderationMessage
: null,
moderation_message_body: this.moderationMessageBody
? this.moderationMessageBody
: null,
moderation_message: this.moderationMessage ? this.moderationMessage : null,
moderation_message_body: this.moderationMessageBody ? this.moderationMessageBody : null,
}
if (this.status) {
data.status = this.status
}
await this.$axios.patch(
`project/${this.project.id}`,
data,
this.$defaultHeaders()
)
await useBaseFetch(`project/${this.project.id}`, {
method: 'PATCH',
body: data,
...this.$defaultHeaders(),
})
this.$refs.modal.hide()
if (this.onClose !== null) {
@@ -141,25 +124,21 @@ export default {
this.$notify({
group: 'main',
title: 'An error occurred',
text: err.response ? err.response.data.description : err,
text: err.data ? err.data.description : err,
type: 'error',
})
}
this.$nuxt.$loading.finish()
stopLoading()
},
show() {
this.$refs.modal.show()
this.moderationMessage =
this.project &&
this.project.moderator_message &&
this.project.moderator_message.message
this.project && this.project.moderator_message && this.project.moderator_message.message
? this.project.moderator_message.message
: ''
this.moderationMessageBody =
this.project &&
this.project.moderator_message &&
this.project.moderator_message.body
this.project && this.project.moderator_message && this.project.moderator_message.body
? this.project.moderator_message.body
: ''
},

View File

@@ -3,18 +3,16 @@
<div class="modal-report legacy-label-styles">
<div class="markdown-body">
<p>
Modding should be safe for everyone, so we take abuse and malicious
intent seriously at Modrinth. We want to hear about harmful content on
the site that violates our
<nuxt-link to="/legal/terms">ToS</nuxt-link> and
<nuxt-link to="/legal/rules">Rules</nuxt-link>. Rest assured, well
keep your identifying information private.
Modding should be safe for everyone, so we take abuse and malicious intent seriously at
Modrinth. We want to hear about harmful content on the site that violates our
<nuxt-link to="/legal/terms"> ToS </nuxt-link> and
<nuxt-link to="/legal/rules"> Rules </nuxt-link>. Rest assured, well keep your
identifying information private.
</p>
<p v-if="itemType === 'project' || itemType === 'version'">
Please <strong>do not</strong> use this to report bugs with the
project itself. This form is only for submitting a report to Modrinth
staff. If the project has an Issues link or a Discord invite, consider
reporting it there.
Please <strong>do not</strong> use this to report bugs with the project itself. This form
is only for submitting a report to Modrinth staff. If the project has an Issues link or a
Discord invite, consider reporting it there.
</p>
</div>
<label class="report-label" for="report-type">
@@ -25,10 +23,8 @@
<multiselect
id="report-type"
v-model="reportType"
:options="$store.state.tag.reportTypes"
:custom-label="
(value) => value.charAt(0).toUpperCase() + value.slice(1)
"
:options="$tag.reportTypes"
:custom-label="(value) => value.charAt(0).toUpperCase() + value.slice(1)"
:multiple="false"
:searchable="false"
:show-no-results="false"
@@ -37,26 +33,14 @@
/>
<label class="report-label" for="additional-information">
<strong>Additional information</strong>
<span>
Include links and images if possible. Markdown formatting is
supported.
</span>
<span> Include links and images if possible. Markdown formatting is supported. </span>
</label>
<div class="textarea-wrapper">
<Chips
v-model="bodyViewType"
class="separator"
:items="['source', 'preview']"
/>
<Chips v-model="bodyViewType" class="separator" :items="['source', 'preview']" />
<div v-if="bodyViewType === 'source'" class="textarea-wrapper">
<textarea id="body" v-model="body" spellcheck="true" />
</div>
<div
v-else
v-highlightjs
class="preview"
v-html="$xss($md.render(body))"
></div>
<div v-else class="preview" v-html="renderString(body)" />
</div>
<div class="button-group">
<button class="iconified-button" @click="cancel">
@@ -74,13 +58,13 @@
<script>
import Multiselect from 'vue-multiselect'
import CrossIcon from '~/assets/images/utils/x.svg?inline'
import CheckIcon from '~/assets/images/utils/check.svg?inline'
import CrossIcon from '~/assets/images/utils/x.svg'
import CheckIcon from '~/assets/images/utils/check.svg'
import Modal from '~/components/ui/Modal'
import Chips from '~/components/ui/Chips'
import { renderString } from '~/helpers/parse'
export default {
name: 'ModalReport',
components: {
Chips,
CrossIcon,
@@ -106,6 +90,7 @@ export default {
}
},
methods: {
renderString,
cancel() {
this.reportType = ''
this.body = ''
@@ -114,7 +99,7 @@ export default {
this.$refs.modal.hide()
},
async submitReport() {
this.$nuxt.$loading.start()
startLoading()
try {
const data = {
report_type: this.reportType,
@@ -122,18 +107,22 @@ export default {
item_type: this.itemType,
body: this.body,
}
await this.$axios.post('report', data, this.$defaultHeaders())
await useBaseFetch('report', {
method: 'POST',
body: data,
...this.$defaultHeaders(),
})
this.$refs.modal.hide()
} catch (err) {
this.$notify({
group: 'main',
title: 'An error occurred',
text: err.response.data.description,
text: err.data.description,
type: 'error',
})
}
this.$nuxt.$loading.finish()
stopLoading()
},
show() {
this.$refs.modal.show()

View File

@@ -2,9 +2,8 @@
<Modal ref="modal" :header="'Transfer to ' + $formatWallet(wallet)">
<div class="modal-transfer">
<span
>You are initiating a transfer of your revenue from Modrinth's Creator
Monetization Program. How much of your
<strong>{{ $formatMoney(balance) }}</strong> balance would you like to
>You are initiating a transfer of your revenue from Modrinth's Creator Monetization Program.
How much of your <strong>{{ $formatMoney(balance) }}</strong> balance would you like to
transfer?</span
>
<div class="confirmation-input">
@@ -19,40 +18,30 @@
</div>
<div class="confirm-text">
<Checkbox
v-if="
isValidInput() &&
parseInput() >= minWithdraw &&
parseInput() <= balance
"
v-if="isValidInput() && parseInput() >= minWithdraw && parseInput() <= balance"
v-model="consentedFee"
description="Consent to fee"
>
<template v-if="wallet === 'venmo'"
>I acknowledge that $0.25 will be deducted from the amount I receive
to cover {{ $formatWallet(wallet) }} processing fees.</template
>
<template v-else
>I acknowledge that an estimated
{{ $formatMoney(calcProcessingFees()) }} will be deducted from the
amount I receive to cover {{ $formatWallet(wallet) }} processing
fees and that any excess will be returned to my Modrinth
balance.</template
>
<template v-if="wallet === 'venmo'">
I acknowledge that $0.25 will be deducted from the amount I receive to cover
{{ $formatWallet(wallet) }} processing fees.
</template>
<template v-else>
I acknowledge that an estimated
{{ $formatMoney(calcProcessingFees()) }} will be deducted from the amount I receive to
cover {{ $formatWallet(wallet) }} processing fees and that any excess will be returned
to my Modrinth balance.
</template>
</Checkbox>
<Checkbox
v-if="
isValidInput() &&
parseInput() >= minWithdraw &&
parseInput() <= balance
"
v-if="isValidInput() && parseInput() >= minWithdraw && parseInput() <= balance"
v-model="consentedAccount"
description="Confirm transfer"
>
I confirm that I an initiating a transfer to the following
{{ $formatWallet(wallet) }} account: {{ account }}
</Checkbox>
<span
v-else-if="validInput && parseInput() < minWithdraw"
class="invalid"
>
<span v-else-if="validInput && parseInput() < minWithdraw" class="invalid">
The amount must be at least {{ $formatMoney(minWithdraw) }}</span
>
<span v-else-if="validInput && parseInput() > balance" class="invalid">
@@ -84,14 +73,13 @@
</template>
<script>
import CrossIcon from '~/assets/images/utils/x.svg?inline'
import TransferIcon from '~/assets/images/utils/transfer.svg?inline'
import SettingsIcon from '~/assets/images/utils/settings.svg?inline'
import CrossIcon from '~/assets/images/utils/x.svg'
import TransferIcon from '~/assets/images/utils/transfer.svg'
import SettingsIcon from '~/assets/images/utils/settings.svg'
import Modal from '~/components/ui/Modal'
import Checkbox from '~/components/ui/Checkbox'
export default {
name: 'ModalTransfer',
components: {
Checkbox,
CrossIcon,
@@ -138,29 +126,27 @@ export default {
this.$refs.modal.hide()
},
async proceed() {
this.$nuxt.$loading.start()
startLoading()
try {
await this.$axios.post(
`user/${this.$auth.user.id}/payouts`,
{
await useBaseFetch(`user/${this.$auth.user.id}/payouts`, {
method: 'POST',
body: {
amount: Number(this.amount.replace('$', '')),
},
this.$defaultHeaders()
)
await this.$store.dispatch('auth/fetchUser', {
token: this.$auth.token,
...this.$defaultHeaders(),
})
await useAuth(this.$auth.token)
this.$refs.modal.hide()
} catch (err) {
this.$notify({
group: 'main',
title: 'An error occurred',
text: err.response.data.description,
text: err.data.description,
type: 'error',
})
}
this.$nuxt.$loading.finish()
stopLoading()
},
show() {
this.$refs.modal.show()

View File

@@ -1,5 +1,5 @@
<template>
<nav class="navigation" :class="{ 'use-animation': useAnimation }">
<nav class="navigation">
<NuxtLink
v-for="(link, index) in filteredLinks"
v-show="link.shown === undefined ? true : link.shown"
@@ -7,32 +7,24 @@
ref="linkElements"
:to="query ? (link.href ? `?${query}=${link.href}` : '?') : link.href"
class="nav-link button-animation"
:class="{ 'is-active': index === activeIndex }"
>
<span>{{ link.label }}</span>
</NuxtLink>
<div
class="nav-indicator"
:style="`visibility: ${
useAnimation && activeIndex !== -1 ? 'visible' : 'hidden'
}; left: ${indicator.left}px; right: ${indicator.right}px;
top: ${indicator.top}px; transition: left 350ms ${
indicator.direction === 'left'
? 'cubic-bezier(1,0,.3,1) -140ms'
: 'cubic-bezier(.75,-0.01,.24,.99) -40ms'
},right 350ms ${
indicator.direction === 'right'
? 'cubic-bezier(1,0,.3,1) -140ms'
: 'cubic-bezier(.75,-0.01,.24,.99) -40ms'
}, top 100ms ease-in-out`"
/>
:style="{
left: positionToMoveX,
top: positionToMoveY,
width: sliderWidth,
opacity: activeIndex === -1 ? 0 : 1,
}"
aria-hidden="true"
></div>
</nav>
</template>
<script>
export default {
name: 'NavRow',
props: {
links: {
default: () => [],
@@ -45,21 +37,26 @@ export default {
},
data() {
return {
useAnimation: false,
oldIndex: -1,
sliderPositionX: 0,
sliderPositionY: 18,
selectedElementWidth: 0,
activeIndex: -1,
indicator: {
left: 0,
right: 0,
top: 22,
direction: 'right',
},
oldIndex: -1,
}
},
computed: {
filteredLinks() {
return this.links.filter((x) => (x.shown === undefined ? true : x.shown))
},
positionToMoveX() {
return `${this.sliderPositionX}px`
},
positionToMoveY() {
return `${this.sliderPositionY}px`
},
sliderWidth() {
return `${this.selectedElementWidth}px`
},
},
watch: {
'$route.path': {
@@ -74,53 +71,34 @@ export default {
},
},
mounted() {
window.addEventListener('resize', this.pickLink)
this.pickLink()
},
unmounted() {
window.removeEventListener('resize', this.pickLink)
},
methods: {
pickLink() {
if (this.oldIndex === -1) {
this.useAnimation = false
setTimeout(() => {
this.useAnimation = true
}, 300)
}
this.activeIndex = this.query
? this.filteredLinks.findIndex(
(x) =>
(x.href === '' ? undefined : x.href) ===
this.$route.query[this.query]
)
: this.filteredLinks.findIndex(
(x) => x.href === decodeURIComponent(this.$route.path)
(x) => (x.href === '' ? undefined : x.href) === this.$route.path[this.query]
)
: this.filteredLinks.findIndex((x) => x.href === decodeURIComponent(this.$route.path))
if (this.activeIndex !== -1) {
this.startAnimation()
} else {
this.oldIndex = -1
this.sliderPositionX = 0
this.selectedElementWidth = 0
}
},
startAnimation() {
if (this.$refs.linkElements[this.activeIndex]) {
this.indicator.direction =
this.activeIndex < this.oldIndex ? 'left' : 'right'
const el = this.$refs.linkElements[this.activeIndex].$el
this.indicator.left =
this.$refs.linkElements[this.activeIndex].$el.offsetLeft
this.indicator.right =
this.$refs.linkElements[this.activeIndex].$el.parentElement
.offsetWidth -
this.$refs.linkElements[this.activeIndex].$el.offsetLeft -
this.$refs.linkElements[this.activeIndex].$el.offsetWidth
this.indicator.top =
this.$refs.linkElements[this.activeIndex].$el.offsetTop +
this.$refs.linkElements[this.activeIndex].$el.offsetHeight +
1
}
this.oldIndex = this.activeIndex
this.sliderPositionX = el.offsetLeft
this.sliderPositionY = el.offsetTop + el.offsetHeight
this.selectedElementWidth = el.offsetWidth
},
},
}
@@ -141,19 +119,6 @@ export default {
color: var(--color-text);
position: relative;
&::after {
content: '';
display: block;
position: absolute;
bottom: -5px;
width: 100%;
border-radius: var(--size-rounded-max);
height: 0.25rem;
transition: opacity 0.1s ease-in-out;
background-color: var(--color-brand);
opacity: 0;
}
&:hover {
color: var(--color-text);
@@ -166,7 +131,7 @@ export default {
opacity: 0.2;
}
&.is-active {
&.router-link-exact-active {
color: var(--color-text);
&::after {
@@ -186,11 +151,12 @@ export default {
.nav-indicator {
position: absolute;
height: 0.25rem;
bottom: -5px;
left: 0;
width: 3rem;
transition: all ease-in-out 0.2s;
border-radius: var(--size-rounded-max);
background-color: var(--color-brand);
transition-property: left, right, top;
transition-duration: 350ms;
visibility: hidden;
@media (prefers-reduced-motion) {
transition: none !important;

View File

@@ -7,9 +7,7 @@
</template>
<script>
export default {
name: 'NavStack',
}
export default {}
</script>
<style lang="scss" scoped>

View File

@@ -23,10 +23,9 @@
</template>
<script>
import ChevronRightIcon from '~/assets/images/utils/chevron-right.svg?inline'
import ChevronRightIcon from '~/assets/images/utils/chevron-right.svg'
export default {
name: 'NavStackItem',
components: {
ChevronRightIcon,
},
@@ -88,7 +87,13 @@ export default {
background-color: var(--background-color);
}
&.nuxt-link-exact-active {
&:focus-visible {
.nav-content {
border-radius: 0.25rem;
}
}
&.router-link-exact-active {
.nav-content {
color: var(--color-button-text-active);
background-color: var(--color-button-bg);

View File

@@ -0,0 +1,107 @@
<template>
<div class="vue-notification-group">
<transition-group name="notifs">
<div
v-for="(item, index) in notifications"
:key="item.id"
class="vue-notification-wrapper"
@click="notifications.splice(index, 1)"
@mouseenter="stopTimer(item)"
@mouseleave="setNotificationTimer(item)"
>
<div class="vue-notification-template vue-notification" :class="{ [item.type]: true }">
<div class="notification-title" v-html="item.title"></div>
<div class="notification-content" v-html="item.text"></div>
</div>
</div>
</transition-group>
</div>
</template>
<script setup>
const notifications = useNotifications()
function stopTimer(notif) {
clearTimeout(notif.timer)
}
</script>
<style lang="scss" scoped>
.vue-notification {
background: var(--color-special-blue) !important;
border-left: 5px solid var(--color-special-blue) !important;
color: var(--color-brand-inverted) !important;
box-sizing: border-box;
text-align: left;
font-size: 12px;
padding: 10px;
margin: 0 5px 5px;
&.success {
background: var(--color-special-green) !important;
border-left-color: var(--color-special-green) !important;
}
&.warn {
background: var(--color-special-orange) !important;
border-left-color: var(--color-special-orange) !important;
}
&.error {
background: var(--color-special-red) !important;
border-left-color: var(--color-special-red) !important;
}
}
.vue-notification-group {
position: fixed;
right: 25px;
bottom: 25px;
z-index: 99999999;
width: 300px;
.vue-notification-wrapper {
width: 100%;
overflow: hidden;
margin-bottom: 10px;
.vue-notification-template {
border-radius: var(--size-rounded-card);
margin: 0;
.notification-title {
font-size: var(--font-size-lg);
margin-right: auto;
font-weight: 600;
}
.notification-content {
margin-right: auto;
font-size: var(--font-size-md);
}
}
&:last-child {
margin: 0;
}
}
@media screen and (max-width: 750px) {
transition: bottom 0.25s ease-in-out;
bottom: calc(var(--size-mobile-navbar-height) + 10px) !important;
&.browse-menu-open {
bottom: calc(var(--size-mobile-navbar-height-expanded) + 10px) !important;
}
}
}
.notifs-enter-active,
.notifs-leave-active,
.notifs-move {
transition: all 0.5s;
}
.notifs-enter-from,
.notifs-leave-to {
opacity: 0;
}
</style>

View File

@@ -2,6 +2,7 @@
<div v-if="count > 1" class="columns paginates">
<a
:class="{ disabled: page === 1 }"
:tabindex="page === 1 ? -1 : 0"
class="left-arrow paginate has-icon"
aria-label="Previous Page"
:href="linkFunction(page - 1)"
@@ -38,12 +39,11 @@
:class="{
disabled: page === pages[pages.length - 1],
}"
:tabindex="page === pages[pages.length - 1] ? -1 : 0"
class="right-arrow paginate has-icon"
aria-label="Next Page"
:href="linkFunction(page + 1)"
@click.prevent="
page !== pages[pages.length - 1] ? switchPage(page + 1) : null
"
@click.prevent="page !== pages[pages.length - 1] ? switchPage(page + 1) : null"
>
<RightArrowIcon />
</a>
@@ -51,12 +51,11 @@
</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'
import GapIcon from '~/assets/images/utils/gap.svg'
import LeftArrowIcon from '~/assets/images/utils/left-arrow.svg'
import RightArrowIcon from '~/assets/images/utils/right-arrow.svg'
export default {
name: 'Pagination',
components: {
GapIcon,
LeftArrowIcon,
@@ -78,6 +77,7 @@ export default {
},
},
},
emits: ['switch-page'],
computed: {
pages() {
let pages = []
@@ -94,15 +94,7 @@ export default {
this.count,
]
} else if (this.page > 4) {
pages = [
1,
'-',
this.page - 1,
this.page,
this.page + 1,
'-',
this.count,
]
pages = [1, '-', this.page - 1, this.page, this.page + 1, '-', this.count]
} else {
pages = [1, 2, 3, 4, 5, '-', this.count]
}
@@ -131,8 +123,8 @@ a {
border-radius: 2rem;
background: var(--color-raised-bg);
transition: opacity 0.5s ease-in-out, filter 0.2s ease-in-out,
transform 0.05s ease-in-out, outline 0.2s ease-in-out;
transition: opacity 0.5s ease-in-out, filter 0.2s ease-in-out, transform 0.05s ease-in-out,
outline 0.2s ease-in-out;
&.page-number.current {
background: var(--color-brand);

View File

@@ -1,10 +1,7 @@
<template>
<article
class="project-card base-card padding-bg"
:aria-label="name"
role="listitem"
>
<article class="project-card base-card padding-bg" :aria-label="name" role="listitem">
<nuxt-link
:title="name"
class="icon"
tabindex="-1"
:to="`/${$getProjectTypeForUrl(type, categories)}/${id}`"
@@ -18,7 +15,7 @@
:to="`/${$getProjectTypeForUrl(type, categories)}/${id}`"
:style="color ? `background-color: ${toColor};` : ''"
>
<img v-if="featuredImage" :src="featuredImage" alt="gallery image" />
<img v-if="featuredImage" :src="featuredImage" alt="gallery image" loading="lazy" />
</nuxt-link>
<div class="title">
<nuxt-link :to="`/${$getProjectTypeForUrl(type, categories)}/${id}`">
@@ -28,24 +25,18 @@
</nuxt-link>
<p v-if="author" class="author">
by
<nuxt-link class="title-link" :to="'/user/' + author"
>{{ author }}
<nuxt-link class="title-link" :to="'/user/' + author">
{{ author }}
</nuxt-link>
</p>
<Badge
v-if="status && status !== 'approved'"
:type="status"
class="status"
/>
<Badge v-if="status && status !== 'approved'" :type="status" class="status" />
</div>
<p class="description">
{{ description }}
</p>
<Categories
:categories="
categories.filter(
(x) => !hideLoaders || !$tag.loaders.find((y) => y.name === x)
)
categories.filter((x) => !hideLoaders || !$tag.loaders.find((y) => y.name === x))
"
:type="type"
class="tags"
@@ -64,18 +55,14 @@
<DownloadIcon aria-hidden="true" />
<p>
<strong>{{ $formatNumber(downloads) }}</strong
><span class="stat-label">
download<span v-if="downloads !== '1'">s</span></span
>
><span class="stat-label"> download<span v-if="downloads !== '1'">s</span></span>
</p>
</div>
<div v-if="follows" class="stat">
<HeartIcon aria-hidden="true" />
<p>
<strong>{{ $formatNumber(follows) }}</strong
><span class="stat-label">
follower<span v-if="follows !== '1'">s</span></span
>
><span class="stat-label"> follower<span v-if="follows !== '1'">s</span></span>
</p>
</div>
<div class="buttons">
@@ -87,8 +74,7 @@
class="stat date"
>
<EditIcon aria-hidden="true" />
<span class="date-label">Updated </span
>{{ $dayjs(updatedAt).fromNow() }}
<span class="date-label">Updated </span>{{ fromNow(updatedAt) }}
</div>
<div
v-else
@@ -96,8 +82,7 @@
class="stat date"
>
<CalendarIcon aria-hidden="true" />
<span class="date-label">Published </span
>{{ $dayjs(createdAt).fromNow() }}
<span class="date-label">Published </span>{{ fromNow(createdAt) }}
</div>
</div>
</article>
@@ -108,14 +93,13 @@ import Categories from '~/components/ui/search/Categories'
import Badge from '~/components/ui/Badge'
import EnvironmentIndicator from '~/components/ui/EnvironmentIndicator'
import CalendarIcon from '~/assets/images/utils/calendar.svg?inline'
import EditIcon from '~/assets/images/utils/updated.svg?inline'
import DownloadIcon from '~/assets/images/utils/download.svg?inline'
import HeartIcon from '~/assets/images/utils/heart.svg?inline'
import CalendarIcon from '~/assets/images/utils/calendar.svg'
import EditIcon from '~/assets/images/utils/updated.svg'
import DownloadIcon from '~/assets/images/utils/download.svg'
import HeartIcon from '~/assets/images/utils/heart.svg'
import Avatar from '~/components/ui/Avatar'
export default {
name: 'ProjectCard',
components: {
EnvironmentIndicator,
Avatar,

View File

@@ -4,8 +4,7 @@
$auth.user &&
currentMember &&
nags.filter((x) => x.condition).length > 0 &&
(project.status === 'draft' ||
$tag.rejectedStatuses.includes(project.status))
(project.status === 'draft' || $tag.rejectedStatuses.includes(project.status))
"
class="author-actions universal-card"
>
@@ -19,6 +18,7 @@
v-for="nag in nags"
:key="`checklist-${nag.id}`"
v-tooltip="nag.title"
:aria-label="nag.title"
class="circle"
:class="'circle ' + (!nag.condition ? 'done ' : '') + nag.status"
>
@@ -41,25 +41,24 @@
</div>
</div>
<div v-if="!collapsed" class="grid-display width-16">
<div
v-for="nag in nags.filter((x) => x.condition)"
:key="nag.id"
class="grid-display__item"
>
<div v-for="nag in nags.filter((x) => x.condition)" :key="nag.id" class="grid-display__item">
<span class="label">
<RequiredIcon
v-if="nag.status === 'required'"
v-tooltip="'Required'"
aria-label="Required"
:class="nag.status"
/>
<SuggestionIcon
v-else-if="nag.status === 'suggestion'"
v-tooltip="'Suggestion'"
aria-label="Suggestion"
:class="nag.status"
/>
<ModerationIcon
v-else-if="nag.status === 'review'"
v-tooltip="'Review'"
aria-label="Review"
:class="nag.status"
/>{{ nag.title }}</span
>
@@ -71,6 +70,7 @@
$tag.rejectedStatuses.includes(project.status)
"
v-model="acknowledgedMessage"
description="Acknowledge staff message in sidebar"
>
I acknowledge that I have addressed the staff's message on the sidebar
</Checkbox>
@@ -78,15 +78,12 @@
v-if="nag.link"
:class="{ invisible: nag.link.hide }"
class="goto-link"
:to="`/${project.project_type}/${
project.slug ? project.slug : project.id
}/${nag.link.path}`"
:to="`/${project.project_type}/${project.slug ? project.slug : project.id}/${
nag.link.path
}`"
>
{{ nag.link.title }}
<ChevronRightIcon
class="featured-header-chevron"
aria-hidden="true"
/>
<ChevronRightIcon class="featured-header-chevron" aria-hidden="true" />
</NuxtLink>
<button
v-else-if="nag.action"
@@ -103,17 +100,16 @@
</template>
<script>
import ChevronRightIcon from '~/assets/images/utils/chevron-right.svg?inline'
import DropdownIcon from '~/assets/images/utils/dropdown.svg?inline'
import CheckIcon from '~/assets/images/utils/check.svg?inline'
import RequiredIcon from '~/assets/images/utils/asterisk.svg?inline'
import SuggestionIcon from '~/assets/images/utils/lightbulb.svg?inline'
import ModerationIcon from '~/assets/images/sidebar/admin.svg?inline'
import SendIcon from '~/assets/images/utils/send.svg?inline'
import ChevronRightIcon from '~/assets/images/utils/chevron-right.svg'
import DropdownIcon from '~/assets/images/utils/dropdown.svg'
import CheckIcon from '~/assets/images/utils/check.svg'
import RequiredIcon from '~/assets/images/utils/asterisk.svg'
import SuggestionIcon from '~/assets/images/utils/lightbulb.svg'
import ModerationIcon from '~/assets/images/sidebar/admin.svg'
import SendIcon from '~/assets/images/utils/send.svg'
import Checkbox from '~/components/ui/Checkbox'
export default {
name: 'ProjectPublishingChecklist',
components: {
Checkbox,
ChevronRightIcon,
@@ -131,7 +127,9 @@ export default {
},
versions: {
type: Array,
required: true,
default() {
return []
},
},
currentMember: {
type: Object,
@@ -189,8 +187,7 @@ export default {
return [
{
condition:
this.project.body === '' ||
this.project.body.startsWith('# Placeholder description'),
this.project.body === '' || this.project.body.startsWith('# Placeholder description'),
title: 'Add a description',
id: 'add-description',
description:
@@ -219,8 +216,7 @@ export default {
condition: !this.featuredGalleryImage,
title: 'Feature a gallery image',
id: 'feature-gallery-image',
description:
'Featured gallery images may be the first impression for many users.',
description: 'Featured gallery images may be the first impression of many users.',
status: 'suggestion',
link: {
path: 'gallery',
@@ -232,8 +228,7 @@ export default {
condition: this.versions.length < 1,
title: 'Upload a version',
id: 'upload-version',
description:
'At least one version is required for a project to be submitted for review.',
description: 'At least one version is required for a project to be submitted for review.',
status: 'required',
link: {
path: 'versions',
@@ -279,8 +274,7 @@ export default {
this.project.project_type === 'shader' ||
this.project.project_type === 'datapack',
condition:
this.project.client_side === 'unknown' ||
this.project.server_side === 'unknown',
this.project.client_side === 'unknown' || this.project.server_side === 'unknown',
title: 'Select supported environments',
id: 'select-environments',
description: `Select if the ${this.$formatProjectType(
@@ -320,8 +314,7 @@ export default {
onClick: this.submitForReview,
title: 'Submit for review',
disabled: () =>
this.nags.filter((x) => x.condition && x.status === 'required')
.length > 0,
this.nags.filter((x) => x.condition && x.status === 'required').length > 0,
},
},
{
@@ -339,8 +332,7 @@ export default {
title: 'Resubmit for review',
disabled: () =>
!this.acknowledgedMessage ||
this.nags.filter((x) => x.condition && x.status === 'required')
.length > 0,
this.nags.filter((x) => x.condition && x.status === 'required').length > 0,
},
},
]
@@ -380,8 +372,7 @@ export default {
async submitForReview() {
if (
!this.acknowledgedMessage ||
this.nags.filter((x) => x.condition && x.status === 'required')
.length === 0
this.nags.filter((x) => x.condition && x.status === 'required').length === 0
) {
await this.setProcessing()
}

View File

@@ -1,12 +1,14 @@
<template>
<div
v-if="getValidLoaders().length > 1 || getValidVersions().length > 1"
v-if="
loaderFilters.length > 1 || gameVersionFilters.length > 1 || versionTypeFilters.length > 1
"
class="card search-controls"
>
<Multiselect
v-if="getValidLoaders().length > 1"
v-if="loaderFilters.length > 1"
v-model="selectedLoaders"
:options="getValidLoaders()"
:options="loaderFilters"
:custom-label="(value) => value.charAt(0).toUpperCase() + value.slice(1)"
:multiple="true"
:searchable="false"
@@ -15,19 +17,16 @@
:clear-search-on-select="false"
:show-labels="false"
:allow-empty="true"
:disabled="getValidLoaders().length === 1"
placeholder="Filter loader..."
@input="updateVersionFilters()"
></Multiselect>
@update:model-value="updateVersionFilters()"
/>
<Multiselect
v-if="getValidVersions().length > 1"
v-if="gameVersionFilters.length > 1"
v-model="selectedGameVersions"
:options="
showSnapshots
? getValidVersions().map((x) => x.version)
: getValidVersions()
.filter((it) => it.version_type === 'release')
.map((x) => x.version)
includeSnapshots
? gameVersionFilters.map((x) => x.version)
: gameVersionFilters.filter((it) => it.version_type === 'release').map((x) => x.version)
"
:multiple="true"
:searchable="true"
@@ -37,12 +36,12 @@
:hide-selected="true"
:selectable="() => selectedGameVersions.length <= 6"
placeholder="Filter versions..."
@input="updateVersionFilters()"
></Multiselect>
@update:model-value="updateVersionFilters()"
/>
<Multiselect
v-if="getValidChannels().length > 1"
v-model="selectedChannels"
:options="getValidChannels()"
v-if="versionTypeFilters.length > 1"
v-model="selectedVersionTypes"
:options="versionTypeFilters"
:custom-label="(x) => $capitalizeString(x)"
:multiple="true"
:searchable="false"
@@ -52,29 +51,30 @@
:show-labels="false"
:allow-empty="true"
placeholder="Filter channels..."
@input="updateVersionFilters()"
></Multiselect>
@update:model-value="updateVersionFilters()"
/>
<Checkbox
v-if="
getValidVersions().length > 1 &&
getValidVersions().some((v) => v.version_type !== 'release')
gameVersionFilters.length > 1 &&
gameVersionFilters.some((v) => v.version_type !== 'release')
"
v-model="showSnapshots"
v-model="includeSnapshots"
label="Include snapshots"
description="Include snapshots"
:border="false"
@input="updateQuery"
@update:model-value="updateQuery"
/>
<button
title="Clear filters"
:disabled="
selectedLoaders.length === 0 && selectedGameVersions.length === 0
"
:disabled="selectedLoaders.length === 0 && selectedGameVersions.length === 0"
class="iconified-button"
@click="
selectedLoaders = []
selectedGameVersions = []
updateVersionFilters()
() => {
selectedLoaders = []
selectedGameVersions = []
selectedVersionTypes = []
updateVersionFilters()
}
"
>
<ClearIcon />
@@ -83,125 +83,81 @@
</div>
</template>
<script>
<script setup>
import Multiselect from 'vue-multiselect'
import Checkbox from '~/components/ui/Checkbox'
import ClearIcon from '~/assets/images/utils/clear.svg?inline'
export default {
name: 'VersionFilterControl',
components: {
Multiselect,
Checkbox,
ClearIcon,
},
props: {
versions: {
type: Array,
required: true,
},
},
data() {
return {
query: '',
showSnapshots: false,
cachedValidChannels: null,
cachedValidVersions: null,
cachedValidLoaders: null,
selectedGameVersions: [],
selectedLoaders: [],
selectedChannels: [],
}
},
fetch() {
this.selectedLoaders = this.$route.query.l?.split(',') || []
this.selectedGameVersions = this.$route.query.g?.split(',') || []
this.selectedChannels = this.$route.query.c?.split(',') || []
this.showSnapshots = this.$route.query.s === 'true'
this.updateVersionFilters()
},
methods: {
getValidChannels() {
if (!this.cachedValidChannels) {
this.cachedValidChannels = ['release', 'beta', 'alpha'].filter(
(channel) =>
this.versions.some((projVer) => projVer.version_type === channel)
)
}
return this.cachedValidChannels
},
getValidVersions() {
if (!this.cachedValidVersions) {
this.cachedValidVersions = this.$tag.gameVersions.filter((gameVer) =>
this.versions.some((projVer) =>
projVer.game_versions.includes(gameVer.version)
)
)
}
return this.cachedValidVersions
},
getValidLoaders() {
if (!this.cachedValidLoaders) {
const temp = new Set()
for (const version of this.versions) {
version.loaders.forEach((v) => {
temp.add(v)
})
}
this.cachedValidLoaders = Array.from(temp)
this.cachedValidLoaders.sort()
}
return this.cachedValidLoaders
},
async updateVersionFilters() {
this.selectedChannels = this.selectedChannels.filter((channel) =>
this.getValidChannels().includes(channel)
)
this.selectedLoaders = this.selectedLoaders.filter((loader) =>
this.getValidLoaders().includes(loader)
)
this.selectedGameVersions = this.selectedGameVersions.filter((version) =>
this.getValidVersions().some(
(validVersion) => validVersion.version === version
)
)
import ClearIcon from '~/assets/images/utils/clear.svg'
const temp = this.versions.filter(
(projectVersion) =>
(this.selectedGameVersions.length === 0 ||
this.selectedGameVersions.some((gameVersion) =>
projectVersion.game_versions.includes(gameVersion)
)) &&
(this.selectedLoaders.length === 0 ||
this.selectedLoaders.some((loader) =>
projectVersion.loaders.includes(loader)
)) &&
(this.selectedChannels.length === 0 ||
this.selectedChannels.includes(projectVersion.version_type))
)
await this.updateQuery()
this.$emit('updateVersions', temp)
},
async updateQuery() {
await this.$router.replace({
query: {
...this.$route.query,
l:
this.selectedLoaders.length === 0
? undefined
: this.selectedLoaders.join(','),
g:
this.selectedGameVersions.length === 0
? undefined
: this.selectedGameVersions.join(','),
c:
this.selectedChannels.length === 0
? undefined
: this.selectedChannels.join(','),
s: this.showSnapshots ? true : undefined,
},
})
const emit = defineEmits(['updateVersions'])
const props = defineProps({
versions: {
type: Array,
default() {
return []
},
},
})
const data = useNuxtApp()
const route = useRoute()
const tempLoaders = new Set()
let tempVersions = new Set()
const tempReleaseChannels = new Set()
for (const version of props.versions) {
for (const loader of version.loaders) {
tempLoaders.add(loader)
}
for (const gameVersion of version.game_versions) {
tempVersions.add(gameVersion)
}
tempReleaseChannels.add(version.version_type)
}
tempVersions = Array.from(tempVersions)
const loaderFilters = shallowRef(Array.from(tempLoaders))
const gameVersionFilters = shallowRef(
data.$tag.gameVersions.filter((gameVer) => tempVersions.includes(gameVer.version))
)
const versionTypeFilters = shallowRef(Array.from(tempReleaseChannels))
const includeSnapshots = ref(route.query.s === 'true')
const selectedGameVersions = shallowRef(route.query.g ?? [])
const selectedLoaders = shallowRef(route.query.l ?? [])
const selectedVersionTypes = shallowRef(route.query.c ?? [])
async function updateVersionFilters() {
const temp = props.versions.filter(
(projectVersion) =>
(selectedGameVersions.value.length === 0 ||
selectedGameVersions.value.some((gameVersion) =>
projectVersion.game_versions.includes(gameVersion)
)) &&
(selectedLoaders.value.length === 0 ||
selectedLoaders.value.some((loader) => projectVersion.loaders.includes(loader))) &&
(selectedVersionTypes.value.length === 0 ||
selectedVersionTypes.value.includes(projectVersion.version_type))
)
await updateQuery()
emit('updateVersions', temp)
}
async function updateQuery() {
const router = useRouter()
const route = useRoute()
await router.replace({
query: {
...route.query,
l: selectedLoaders.value.length === 0 ? undefined : selectedLoaders.value,
g: selectedGameVersions.value.length === 0 ? undefined : selectedGameVersions.value,
c: selectedVersionTypes.value.length === 0 ? undefined : selectedVersionTypes.value,
s: includeSnapshots.value ? true : undefined,
},
})
}
</script>
@@ -219,25 +175,4 @@ export default {
min-width: fit-content;
}
}
.circle-button {
display: flex;
max-width: 2rem;
padding: 0.5rem;
background-color: var(--color-button-bg);
border-radius: var(--size-rounded-max);
box-shadow: inset 0px -1px 1px rgba(17, 24, 39, 0.1);
&:hover,
&:focus-visible {
background-color: var(--color-button-bg-hover);
color: var(--color-button-text-hover);
}
&:active {
background-color: var(--color-button-bg-active);
color: var(--color-button-text-active);
}
svg {
height: 1rem;
width: 1rem;
}
}
</style>

View File

@@ -0,0 +1,142 @@
import { computed, defineComponent, h, onBeforeUnmount, ref, watch } from 'vue'
import { useNuxtApp } from '#app'
import { startLoading, stopLoading } from '#imports'
export default defineComponent({
name: 'ModrinthLoadingIndicator',
props: {
throttle: {
type: Number,
default: 50,
},
duration: {
type: Number,
default: 500,
},
height: {
type: Number,
default: 3,
},
color: {
type: [String, Boolean],
default:
'repeating-linear-gradient(to right, var(--color-brand-green) 0%, var(--landing-green-label) 100%)',
},
},
setup(props, { slots }) {
const indicator = useLoadingIndicator({
duration: props.duration,
throttle: props.throttle,
})
const nuxtApp = useNuxtApp()
nuxtApp.hook('page:start', () => {
startLoading()
indicator.start()
})
nuxtApp.hook('page:finish', () => {
stopLoading()
indicator.finish()
})
onBeforeUnmount(() => indicator.clear)
const loading = useLoading()
watch(loading, (newValue, _oldValue) => {
if (newValue) {
indicator.start()
} else {
indicator.finish()
}
})
return () =>
h(
'div',
{
class: 'nuxt-loading-indicator',
style: {
position: 'fixed',
top: 0,
right: 0,
left: 0,
pointerEvents: 'none',
width: `${indicator.progress.value}%`,
height: `${props.height}px`,
opacity: indicator.isLoading.value ? 1 : 0,
background: props.color || undefined,
backgroundSize: `${(100 / indicator.progress.value) * 100}% auto`,
transition: 'width 0.1s, height 0.4s, opacity 0.4s',
zIndex: 999999,
},
},
slots
)
},
})
function useLoadingIndicator(opts: { duration: number; throttle: number }) {
const progress = ref(0)
const isLoading = ref(false)
const step = computed(() => 10000 / opts.duration)
let _timer: any = null
let _throttle: any = null
function start() {
clear()
progress.value = 0
if (opts.throttle && process.client) {
_throttle = setTimeout(() => {
isLoading.value = true
_startTimer()
}, opts.throttle)
} else {
isLoading.value = true
_startTimer()
}
}
function finish() {
progress.value = 100
_hide()
}
function clear() {
clearInterval(_timer)
clearTimeout(_throttle)
_timer = null
_throttle = null
}
function _increase(num: number) {
progress.value = Math.min(100, progress.value + num)
}
function _hide() {
clear()
if (process.client) {
setTimeout(() => {
isLoading.value = false
setTimeout(() => {
progress.value = 0
}, 400)
}, 500)
}
}
function _startTimer() {
if (process.client) {
_timer = setInterval(() => {
_increase(step.value)
}, 100)
}
}
return {
progress,
isLoading,
start,
finish,
clear,
}
}

View File

@@ -11,7 +11,6 @@
<script>
export default {
name: 'Categories',
props: {
categories: {
type: Array,
@@ -30,8 +29,7 @@ export default {
.concat(this.$tag.loaders)
.filter(
(x) =>
this.categories.includes(x.name) &&
(!x.project_type || x.project_type === this.type)
this.categories.includes(x.name) && (!x.project_type || x.project_type === this.type)
)
},
},
@@ -44,7 +42,7 @@ export default {
flex-direction: row;
flex-wrap: wrap;
span ::v-deep {
:deep(span) {
display: flex;
align-items: center;
flex-direction: row;

View File

@@ -1,13 +1,15 @@
<template>
<Checkbox
class="filter"
:value="activeFilters.includes(facetName)"
:model-value="activeFilters.includes(facetName)"
:description="displayName"
@input="toggle()"
@update:model-value="toggle()"
>
<div class="filter-text">
<div v-if="icon" aria-hidden="true" class="icon" v-html="icon"></div>
<div v-else class="icon"><slot /></div>
<div v-if="icon" aria-hidden="true" class="icon" v-html="icon" />
<div v-else class="icon">
<slot />
</div>
<span aria-hidden="true"> {{ displayName }}</span>
</div>
</Checkbox>
@@ -17,7 +19,6 @@
import Checkbox from '~/components/ui/Checkbox'
export default {
name: 'SearchFilter',
components: {
Checkbox,
},
@@ -41,6 +42,7 @@ export default {
},
},
},
emits: ['toggle'],
methods: {
toggle() {
this.$emit('toggle', this.facetName)
@@ -50,10 +52,10 @@ export default {
</script>
<style lang="scss" scoped>
.filter ::v-deep {
.filter {
margin-bottom: 0.5rem;
.filter-text {
:deep(.filter-text) {
display: flex;
align-items: center;