You've already forked AstralRinth
forked from didirus/AstralRinth
Migrate to Nuxt 3 (#933)
* Migrate to Nuxt 3 * Update vercel config * remove tsconfig comment * Changelog experiment + working proj pages * Fix package json * Prevent vercel complaining * fix deploy (hopefully) * Tag generator * Switch to yarn * Vercel pls 🙏 * Fix tag generation bug * Make (most) non-logged in pages work * fix base build * Linting + state * Eradicate axios, make most user pages work * Fix checkbox state being set incorrectly * Make most things work * Final stretch * Finish (most) things * Move to update model value * Fix modal text getting blurred from transforms (#964) * Adjust nav-link border radius when focused (#961) * Transition between animation states on TextLogo (#955) * Transition between animation states on TextLogo * Remove unused refs * Fixes from review * Disable tabbing to pagination arrows when disabled (#972) * Make position of the "no results" text on grid/gallery views consistent (fixes #963) (#965) * Fix position of the "no results" text on grid view * fix padding * Remove extra margin on main page, fixes #957 (#959) * Fix layout shift and placeholder line height (#973) * Fix a lot of issues * Fix more nuxt 3 issues * fix not all versions showing up (temp) * inline inter css file * More nuxt 3 fixes * [skip ci] broken- backup changes * Change modpack warnings to blue instead of red (#991) * Fix some hydration issues * Update nuxt * Fix some images not showing * Add pagination to versions page + fix lag * Make changelog page consistent with versions page * sync before merge * Delete old file * Fix actions failing * update branch * Fixes navbar transition animation. (#1012) * Fixes navbar transition animation. * Fixes Y-axis animation. Fixes mobile menu. Removes highlightjs prop. * Changes xss call to renderString. * Fixes renderString call. * Removes unnecessary styling. * Reverts mobile nav change. * Nuxt 3 Lazy Loading Search (#1022) * Uses lazyFetch for results. onSearchChange refreshes. Adds loading circle. * Removes console.log * Preserves old page when paging. * Diagnosing filtering bugs. * Fix single facet filtering * Implements useAuth in settings/account. * tiny ssr fix * Updating nuxt.config checklist. * Implements useAuth in revenue, moneitzation, and dashboard index pages. * Fixes setups. * Eliminates results when path changes. Adds animated logo. * Ensures loading animation renders on search page. --------- Co-authored-by: Jai A <jaiagr+gpg@pm.me> * Fix navigation issues * Square button fix (#1023) * Removes checklist from nuxt.config. * Modifies Nuxt CI to build after linting. * Fixes prettierignore file. * bug fixes * Update whitelist domains * Page improvements, fix CLS * Fix a lot of things * Fix project type redirect * Fix 404 errors * Fix user settings + hydration error * Final fixes * fix(creator-section): border radius on icons not aligning with bg (#1027) Co-authored-by: MagnusHJensen <magnus.holm.jensen@lego.dk> * Improvements to the mobile navbar (#984) * Transition between animation states on TextLogo * Remove unused refs * Fixes from review * Improvements to the mobile nav menu * fix avatar alt text * Nevermind, got confused for a moment * Tab bar, menu layout improvements * Highlight search icon when menu is open * Update layouts/default.vue Co-authored-by: Magnus Jensen <magnushjensen.mail@gmail.com> * Fix some issues * Use caret instead * Run prettier * Add create a project --------- Co-authored-by: Magnus Jensen <magnushjensen.mail@gmail.com> Co-authored-by: Geometrically <18202329+Geometrically@users.noreply.github.com> Co-authored-by: Jai A <jaiagr+gpg@pm.me> * Fix mobile menu issues * More issues * Fix lint --------- Co-authored-by: Kaeden Murphy <kmurphy@kaedenmurphy.dev> Co-authored-by: triphora <emmaffle@modrinth.com> Co-authored-by: Zach Baird <30800863+ZachBaird@users.noreply.github.com> Co-authored-by: stairman06 <36215135+stairman06@users.noreply.github.com> Co-authored-by: Zachary Baird <zdb1994@yahoo.com> Co-authored-by: Magnus Jensen <magnushjensen.mail@gmail.com> Co-authored-by: MagnusHJensen <magnus.holm.jensen@lego.dk>
This commit is contained in:
@@ -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);
|
||||
@@ -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)">
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
: ''
|
||||
},
|
||||
|
||||
@@ -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, we’ll
|
||||
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, we’ll 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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -7,9 +7,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'NavStack',
|
||||
}
|
||||
export default {}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -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);
|
||||
|
||||
107
components/ui/Notifications.vue
Normal file
107
components/ui/Notifications.vue
Normal file
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<div class="vue-notification-group">
|
||||
<transition-group name="notifs">
|
||||
<div
|
||||
v-for="(item, index) in notifications"
|
||||
:key="item.id"
|
||||
class="vue-notification-wrapper"
|
||||
@click="notifications.splice(index, 1)"
|
||||
@mouseenter="stopTimer(item)"
|
||||
@mouseleave="setNotificationTimer(item)"
|
||||
>
|
||||
<div class="vue-notification-template vue-notification" :class="{ [item.type]: true }">
|
||||
<div class="notification-title" v-html="item.title"></div>
|
||||
<div class="notification-content" v-html="item.text"></div>
|
||||
</div>
|
||||
</div>
|
||||
</transition-group>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
const notifications = useNotifications()
|
||||
|
||||
function stopTimer(notif) {
|
||||
clearTimeout(notif.timer)
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.vue-notification {
|
||||
background: var(--color-special-blue) !important;
|
||||
border-left: 5px solid var(--color-special-blue) !important;
|
||||
color: var(--color-brand-inverted) !important;
|
||||
|
||||
box-sizing: border-box;
|
||||
text-align: left;
|
||||
font-size: 12px;
|
||||
padding: 10px;
|
||||
margin: 0 5px 5px;
|
||||
|
||||
&.success {
|
||||
background: var(--color-special-green) !important;
|
||||
border-left-color: var(--color-special-green) !important;
|
||||
}
|
||||
|
||||
&.warn {
|
||||
background: var(--color-special-orange) !important;
|
||||
border-left-color: var(--color-special-orange) !important;
|
||||
}
|
||||
|
||||
&.error {
|
||||
background: var(--color-special-red) !important;
|
||||
border-left-color: var(--color-special-red) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.vue-notification-group {
|
||||
position: fixed;
|
||||
right: 25px;
|
||||
bottom: 25px;
|
||||
z-index: 99999999;
|
||||
width: 300px;
|
||||
|
||||
.vue-notification-wrapper {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.vue-notification-template {
|
||||
border-radius: var(--size-rounded-card);
|
||||
margin: 0;
|
||||
|
||||
.notification-title {
|
||||
font-size: var(--font-size-lg);
|
||||
margin-right: auto;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.notification-content {
|
||||
margin-right: auto;
|
||||
font-size: var(--font-size-md);
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 750px) {
|
||||
transition: bottom 0.25s ease-in-out;
|
||||
bottom: calc(var(--size-mobile-navbar-height) + 10px) !important;
|
||||
|
||||
&.browse-menu-open {
|
||||
bottom: calc(var(--size-mobile-navbar-height-expanded) + 10px) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.notifs-enter-active,
|
||||
.notifs-leave-active,
|
||||
.notifs-move {
|
||||
transition: all 0.5s;
|
||||
}
|
||||
.notifs-enter-from,
|
||||
.notifs-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -2,6 +2,7 @@
|
||||
<div v-if="count > 1" class="columns paginates">
|
||||
<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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
142
components/ui/modrinth-loading-indicator.ts
Normal file
142
components/ui/modrinth-loading-indicator.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import { computed, defineComponent, h, onBeforeUnmount, ref, watch } from 'vue'
|
||||
import { useNuxtApp } from '#app'
|
||||
import { startLoading, stopLoading } from '#imports'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ModrinthLoadingIndicator',
|
||||
props: {
|
||||
throttle: {
|
||||
type: Number,
|
||||
default: 50,
|
||||
},
|
||||
duration: {
|
||||
type: Number,
|
||||
default: 500,
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
default: 3,
|
||||
},
|
||||
color: {
|
||||
type: [String, Boolean],
|
||||
default:
|
||||
'repeating-linear-gradient(to right, var(--color-brand-green) 0%, var(--landing-green-label) 100%)',
|
||||
},
|
||||
},
|
||||
setup(props, { slots }) {
|
||||
const indicator = useLoadingIndicator({
|
||||
duration: props.duration,
|
||||
throttle: props.throttle,
|
||||
})
|
||||
|
||||
const nuxtApp = useNuxtApp()
|
||||
nuxtApp.hook('page:start', () => {
|
||||
startLoading()
|
||||
indicator.start()
|
||||
})
|
||||
nuxtApp.hook('page:finish', () => {
|
||||
stopLoading()
|
||||
indicator.finish()
|
||||
})
|
||||
onBeforeUnmount(() => indicator.clear)
|
||||
|
||||
const loading = useLoading()
|
||||
|
||||
watch(loading, (newValue, _oldValue) => {
|
||||
if (newValue) {
|
||||
indicator.start()
|
||||
} else {
|
||||
indicator.finish()
|
||||
}
|
||||
})
|
||||
|
||||
return () =>
|
||||
h(
|
||||
'div',
|
||||
{
|
||||
class: 'nuxt-loading-indicator',
|
||||
style: {
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
right: 0,
|
||||
left: 0,
|
||||
pointerEvents: 'none',
|
||||
width: `${indicator.progress.value}%`,
|
||||
height: `${props.height}px`,
|
||||
opacity: indicator.isLoading.value ? 1 : 0,
|
||||
background: props.color || undefined,
|
||||
backgroundSize: `${(100 / indicator.progress.value) * 100}% auto`,
|
||||
transition: 'width 0.1s, height 0.4s, opacity 0.4s',
|
||||
zIndex: 999999,
|
||||
},
|
||||
},
|
||||
slots
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
function useLoadingIndicator(opts: { duration: number; throttle: number }) {
|
||||
const progress = ref(0)
|
||||
const isLoading = ref(false)
|
||||
const step = computed(() => 10000 / opts.duration)
|
||||
|
||||
let _timer: any = null
|
||||
let _throttle: any = null
|
||||
|
||||
function start() {
|
||||
clear()
|
||||
progress.value = 0
|
||||
if (opts.throttle && process.client) {
|
||||
_throttle = setTimeout(() => {
|
||||
isLoading.value = true
|
||||
_startTimer()
|
||||
}, opts.throttle)
|
||||
} else {
|
||||
isLoading.value = true
|
||||
_startTimer()
|
||||
}
|
||||
}
|
||||
function finish() {
|
||||
progress.value = 100
|
||||
_hide()
|
||||
}
|
||||
|
||||
function clear() {
|
||||
clearInterval(_timer)
|
||||
clearTimeout(_throttle)
|
||||
_timer = null
|
||||
_throttle = null
|
||||
}
|
||||
|
||||
function _increase(num: number) {
|
||||
progress.value = Math.min(100, progress.value + num)
|
||||
}
|
||||
|
||||
function _hide() {
|
||||
clear()
|
||||
if (process.client) {
|
||||
setTimeout(() => {
|
||||
isLoading.value = false
|
||||
setTimeout(() => {
|
||||
progress.value = 0
|
||||
}, 400)
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
|
||||
function _startTimer() {
|
||||
if (process.client) {
|
||||
_timer = setInterval(() => {
|
||||
_increase(step.value)
|
||||
}, 100)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
progress,
|
||||
isLoading,
|
||||
start,
|
||||
finish,
|
||||
clear,
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,6 @@
|
||||
|
||||
<script>
|
||||
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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user