You've already forked AstralRinth
forked from didirus/AstralRinth
Move many things over from Knossos (and other rearrangements) (#102)
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
ref="img"
|
||||
:class="`avatar size-${size} ${circle ? 'circle' : ''} ${noShadow ? 'no-shadow' : ''} ${
|
||||
pixelated ? 'pixelated' : ''
|
||||
}`"
|
||||
} ${raised ? 'raised' : ''}`"
|
||||
:src="src"
|
||||
:alt="alt"
|
||||
:loading="loading"
|
||||
@@ -12,7 +12,9 @@
|
||||
/>
|
||||
<svg
|
||||
v-else
|
||||
:class="`avatar size-${size} ${circle ? 'circle' : ''} ${noShadow ? 'no-shadow' : ''}`"
|
||||
:class="`avatar size-${size} ${circle ? 'circle' : ''} ${noShadow ? 'no-shadow' : ''} ${
|
||||
raised ? 'raised' : ''
|
||||
}`"
|
||||
xml:space="preserve"
|
||||
fill-rule="evenodd"
|
||||
stroke-linecap="round"
|
||||
@@ -32,51 +34,48 @@
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
src: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
alt: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'sm',
|
||||
validator(value) {
|
||||
return ['xs', 'sm', 'md', 'lg', 'none'].includes(value)
|
||||
},
|
||||
},
|
||||
circle: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
noShadow: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
loading: {
|
||||
type: String,
|
||||
default: 'lazy',
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const pixelated = ref(false)
|
||||
const img = ref(null)
|
||||
|
||||
defineProps({
|
||||
src: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
alt: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'sm',
|
||||
validator(value) {
|
||||
return ['xxs', 'xs', 'sm', 'md', 'lg', 'none'].includes(value)
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
pixelated: false,
|
||||
}
|
||||
circle: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
methods: {
|
||||
updatePixelated() {
|
||||
if (this.$refs.img && this.$refs.img.naturalWidth && this.$refs.img.naturalWidth <= 96) {
|
||||
this.pixelated = true
|
||||
} else {
|
||||
this.pixelated = false
|
||||
}
|
||||
},
|
||||
noShadow: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
loading: {
|
||||
type: String,
|
||||
default: 'lazy',
|
||||
},
|
||||
raised: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
function updatePixelated() {
|
||||
pixelated.value = !!(img.value && img.value.naturalWidth && img.value.naturalWidth <= 96)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -91,6 +90,12 @@ export default {
|
||||
max-width: var(--size) !important;
|
||||
max-height: var(--size) !important;
|
||||
|
||||
&.size-xxs {
|
||||
--size: 1.25rem;
|
||||
box-shadow: var(--shadow-inset), var(--shadow-card);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
&.size-xs {
|
||||
--size: 2.5rem;
|
||||
box-shadow: var(--shadow-inset), var(--shadow-card);
|
||||
@@ -128,5 +133,9 @@ export default {
|
||||
&.pixelated {
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
&.raised {
|
||||
background-color: var(--color-raised-bg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,27 +1,38 @@
|
||||
<template>
|
||||
<span :class="'version-badge ' + color + ' type--' + type">
|
||||
<template v-if="color"> <span class="circle" /> {{ 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'"> <ScaleIcon /> Moderator </template>
|
||||
<template v-else-if="type === 'admin'"> <ModrinthIcon /> Modrinth Team</template>
|
||||
<template v-else-if="type === 'moderator'"> <ScaleIcon /> Moderator</template>
|
||||
<template v-else-if="type === 'creator'"><BoxIcon /> Creator</template>
|
||||
|
||||
<!-- Project statuses -->
|
||||
<template v-else-if="type === 'approved'"><ListIcon /> Listed</template>
|
||||
<template v-else-if="type === 'approved-general'"><CheckIcon /> Approved</template>
|
||||
<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'"><FileTextIcon /> 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'"><XIcon /> Rejected</template>
|
||||
<template v-else-if="type === 'processing'"> <UpdatedIcon /> Under review </template>
|
||||
<template v-else-if="type === 'processing'"> <UpdatedIcon /> Under review</template>
|
||||
|
||||
<!-- Team members -->
|
||||
<template v-else-if="type === 'accepted'"><CheckIcon /> Accepted</template>
|
||||
<template v-else-if="type === 'pending'"> <UpdatedIcon /> Pending </template>
|
||||
<template v-else> <span class="circle" /> {{ type }} </template>
|
||||
<template v-else-if="type === 'pending'"> <UpdatedIcon /> Pending</template>
|
||||
|
||||
<!-- Transaction statuses (pending, processing reused) -->
|
||||
<template v-else-if="type === 'processed'"><CheckIcon /> Processed</template>
|
||||
<template v-else-if="type === 'failed'"><XIcon /> Failed</template>
|
||||
<template v-else-if="type === 'returned'"><XIcon /> Returned</template>
|
||||
|
||||
<!-- Report status -->
|
||||
<template v-else-if="type === 'closed'"> <XIcon /> Closed</template>
|
||||
|
||||
<!-- Other -->
|
||||
<template v-else> <span class="circle" /> {{ capitalizeString(type) }} </template>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
@@ -39,7 +50,8 @@ import {
|
||||
CheckIcon,
|
||||
LockIcon,
|
||||
CalendarIcon,
|
||||
} from '@/components'
|
||||
capitalizeString,
|
||||
} from '@'
|
||||
|
||||
defineProps({
|
||||
type: {
|
||||
@@ -52,7 +64,6 @@ defineProps({
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.version-badge {
|
||||
display: flex;
|
||||
@@ -75,8 +86,11 @@ defineProps({
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
&.type--closed,
|
||||
&.type--withheld,
|
||||
&.type--rejected,
|
||||
&.type--returned,
|
||||
&.type--failed,
|
||||
&.red {
|
||||
--badge-color: var(--color-red);
|
||||
}
|
||||
@@ -91,7 +105,8 @@ defineProps({
|
||||
|
||||
&.type--accepted,
|
||||
&.type--admin,
|
||||
&.type--success,
|
||||
&.type--processed,
|
||||
&.type--approved-general,
|
||||
&.green {
|
||||
--badge-color: var(--color-green);
|
||||
}
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
<script setup>
|
||||
import { defineProps, ref } from 'vue'
|
||||
import { Bar } from 'vue-chartjs'
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
BarElement,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
} from 'chart.js'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
ChartJS.register(Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale)
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
formatLabels: {
|
||||
type: Function,
|
||||
default: (label) => dayjs(label).format('MMM D'),
|
||||
},
|
||||
})
|
||||
|
||||
const decimalToRgba = (decimalColor, alpha = 0.75) => {
|
||||
const red = (decimalColor >> 16) & 255
|
||||
const green = (decimalColor >> 8) & 255
|
||||
const blue = decimalColor & 255
|
||||
|
||||
return `rgba(${red}, ${green}, ${blue}, ${alpha})`
|
||||
}
|
||||
|
||||
const chartData = ref({
|
||||
labels: props.data.labels.map((date) => props.formatLabels(date)),
|
||||
datasets: props.data.data.map((project) => ({
|
||||
label: project.title,
|
||||
borderColor: decimalToRgba(project.color, 1),
|
||||
borderWidth: 2,
|
||||
borderSkipped: 'bottom',
|
||||
backgroundColor: decimalToRgba(project.color, 0.5),
|
||||
data: project.data,
|
||||
})),
|
||||
})
|
||||
|
||||
const chartOptions = ref({
|
||||
responsive: true,
|
||||
scales: {
|
||||
x: {
|
||||
stacked: true,
|
||||
grid: {
|
||||
color: getComputedStyle(document.documentElement).getPropertyValue('--color-button-bg'),
|
||||
},
|
||||
ticks: {
|
||||
color: getComputedStyle(document.documentElement).getPropertyValue('--color-base'),
|
||||
},
|
||||
},
|
||||
y: {
|
||||
stacked: true,
|
||||
grid: {
|
||||
color: getComputedStyle(document.documentElement).getPropertyValue('--color-button-bg'),
|
||||
},
|
||||
ticks: {
|
||||
color: getComputedStyle(document.documentElement).getPropertyValue('--color-base'),
|
||||
},
|
||||
},
|
||||
},
|
||||
interaction: {
|
||||
mode: 'index',
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'right',
|
||||
align: 'start',
|
||||
labels: {
|
||||
color: getComputedStyle(document.documentElement).getPropertyValue('--color-base'),
|
||||
font: {
|
||||
size: 12,
|
||||
family: 'Inter',
|
||||
},
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
position: 'nearest',
|
||||
backgroundColor: getComputedStyle(document.documentElement).getPropertyValue(
|
||||
'--color-raised-bg'
|
||||
),
|
||||
borderColor: getComputedStyle(document.documentElement).getPropertyValue('--color-button-bg'),
|
||||
borderWidth: 1,
|
||||
titleColor: getComputedStyle(document.documentElement).getPropertyValue('--color-contrast'),
|
||||
titleFont: {
|
||||
size: 16,
|
||||
family: 'Inter',
|
||||
},
|
||||
bodyColor: getComputedStyle(document.documentElement).getPropertyValue('--color-base'),
|
||||
bodyFont: {
|
||||
size: 12,
|
||||
family: 'Inter',
|
||||
},
|
||||
boxPadding: 8,
|
||||
intersect: false,
|
||||
padding: 12,
|
||||
displayColors: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Bar id="my-chart-id" :options="chartOptions" :data="chartData" />
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -1,51 +0,0 @@
|
||||
<template>
|
||||
<nav class="breadcrumbs">
|
||||
<template v-for="(link, index) in linkStack" :key="index">
|
||||
<RouterLink
|
||||
:to="link.href"
|
||||
class="breadcrumb goto-link"
|
||||
:class="{ trim: link.allowTrimming ? link.allowTrimming : false }"
|
||||
>
|
||||
{{ link.label }}
|
||||
</RouterLink>
|
||||
<ChevronRightIcon />
|
||||
</template>
|
||||
<span class="breadcrumb">{{ currentTitle }}</span>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ChevronRightIcon } from '@/components'
|
||||
defineProps({
|
||||
linkStack: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
currentTitle: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.breadcrumbs {
|
||||
display: flex;
|
||||
margin-bottom: var(--gap-lg);
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
svg {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
}
|
||||
|
||||
a.breadcrumb {
|
||||
padding-block: var(--gap-xs);
|
||||
&.trim {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { ExternalIcon, UnknownIcon } from '@/components'
|
||||
import { ExternalIcon, UnknownIcon } from '@'
|
||||
|
||||
import { computed } from 'vue'
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { Button, DropdownIcon } from '@/components'
|
||||
import { Button, DropdownIcon } from '@'
|
||||
|
||||
import { reactive } from 'vue'
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { CheckIcon, DropdownIcon } from '@/components'
|
||||
import { CheckIcon, DropdownIcon } from '@'
|
||||
</script>
|
||||
<script>
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
v-for="item in items"
|
||||
:key="item"
|
||||
class="btn"
|
||||
:class="{ selected: selected === item }"
|
||||
:class="{ selected: selected === item, capitalize: capitalize }"
|
||||
@click="toggleItem(item)"
|
||||
>
|
||||
<CheckIcon v-if="selected === item" />
|
||||
@@ -13,7 +13,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { CheckIcon, Button } from '@/components'
|
||||
import { CheckIcon, Button } from '@'
|
||||
</script>
|
||||
<script>
|
||||
import { defineComponent } from 'vue'
|
||||
@@ -36,6 +36,10 @@ export default defineComponent({
|
||||
default: (x) => x,
|
||||
type: Function,
|
||||
},
|
||||
capitalize: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
computed: {
|
||||
@@ -72,7 +76,9 @@ export default defineComponent({
|
||||
flex-wrap: wrap;
|
||||
|
||||
.btn {
|
||||
text-transform: capitalize;
|
||||
&.capitalize {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 1em;
|
||||
|
||||
21
lib/components/base/ConditionalNuxtLink.vue
Normal file
21
lib/components/base/ConditionalNuxtLink.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<router-link v-if="isLink" :to="to">
|
||||
<slot />
|
||||
</router-link>
|
||||
<span v-else>
|
||||
<slot />
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
to: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
isLink: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<button class="code" :class="{ copied }" title="Copy code to clipboard" @click="copyText">
|
||||
{{ text }}
|
||||
<span>{{ text }}</span>
|
||||
<CheckIcon v-if="copied" />
|
||||
<ClipboardCopyIcon v-else />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { CheckIcon, ClipboardCopyIcon } from '@/components'
|
||||
import { CheckIcon, ClipboardCopyIcon } from '@'
|
||||
</script>
|
||||
|
||||
<script>
|
||||
@@ -34,7 +34,7 @@ export default {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.code {
|
||||
display: flex;
|
||||
display: inline-flex;
|
||||
grid-gap: 0.5rem;
|
||||
font-family: var(--mono-font);
|
||||
font-size: var(--font-size-sm);
|
||||
@@ -47,6 +47,12 @@ export default {
|
||||
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;
|
||||
|
||||
span {
|
||||
max-width: 10rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
|
||||
34
lib/components/base/DoubleIcon.vue
Normal file
34
lib/components/base/DoubleIcon.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div class="double-icon">
|
||||
<slot name="primary" />
|
||||
<div class="secondary">
|
||||
<slot name="secondary" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.double-icon {
|
||||
position: relative;
|
||||
height: fit-content;
|
||||
line-height: 0;
|
||||
|
||||
.secondary {
|
||||
position: absolute;
|
||||
bottom: -4px;
|
||||
right: -4px;
|
||||
background-color: var(--color-bg);
|
||||
padding: var(--spacing-card-xs);
|
||||
border-radius: 50%;
|
||||
aspect-ratio: 1 / 1;
|
||||
width: fit-content;
|
||||
height: fit-content;
|
||||
line-height: 0;
|
||||
|
||||
svg {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -61,7 +61,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { DropdownIcon } from '@/components'
|
||||
import { DropdownIcon } from '@'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
|
||||
@@ -4,7 +4,11 @@
|
||||
A {{ type }}
|
||||
</span>
|
||||
<span
|
||||
v-else-if="!['resourcepack', 'shader'].includes(type) && !(type === 'plugin' && search)"
|
||||
v-else-if="
|
||||
!['resourcepack', 'shader'].includes(type) &&
|
||||
!(type === 'plugin' && search) &&
|
||||
!categories.includes('datapack')
|
||||
"
|
||||
class="environment"
|
||||
>
|
||||
<template v-if="clientSide === 'optional' && serverSide === 'optional'">
|
||||
@@ -44,7 +48,7 @@
|
||||
</span>
|
||||
</template>
|
||||
<script setup>
|
||||
import { GlobeIcon, ClientIcon, ServerIcon, InfoIcon } from '@/components'
|
||||
import { GlobeIcon, ClientIcon, ServerIcon, InfoIcon } from '@'
|
||||
</script>
|
||||
<script>
|
||||
import { defineComponent } from 'vue'
|
||||
@@ -80,6 +84,13 @@ export default defineComponent({
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
categories: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default() {
|
||||
return []
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { fileIsValid } from '@/helpers/utils.js'
|
||||
import { fileIsValid } from '@'
|
||||
import { defineComponent } from 'vue'
|
||||
export default defineComponent({
|
||||
props: {
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
<template>
|
||||
<Line :options="chartOptions" :data="chartData" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { Line } from 'vue-chartjs'
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
Title,
|
||||
Tooltip,
|
||||
PointElement,
|
||||
LineElement,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
Filler,
|
||||
} from 'chart.js'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
ChartJS.register(Title, Tooltip, PointElement, LineElement, CategoryScale, LinearScale, Filler)
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
formatLabels: {
|
||||
type: Function,
|
||||
default: (label) => dayjs(label).format('MMM D'),
|
||||
},
|
||||
})
|
||||
|
||||
const decimalToRgba = (decimalColor, alpha = 0.75) => {
|
||||
const red = (decimalColor >> 16) & 255
|
||||
const green = (decimalColor >> 8) & 255
|
||||
const blue = decimalColor & 255
|
||||
|
||||
return `rgba(${red}, ${green}, ${blue}, ${alpha})`
|
||||
}
|
||||
|
||||
const chartData = ref({
|
||||
labels: props.data.labels.map((date) => props.formatLabels(date)),
|
||||
datasets: props.data.data.map((project) => ({
|
||||
label: project.title,
|
||||
backgroundColor: decimalToRgba(project.color, 0.5),
|
||||
borderColor: decimalToRgba(project.color),
|
||||
data: project.data,
|
||||
})),
|
||||
})
|
||||
|
||||
const chartOptions = ref({
|
||||
responsive: true,
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
color: getComputedStyle(document.documentElement).getPropertyValue('--color-button-bg'),
|
||||
},
|
||||
ticks: {
|
||||
color: getComputedStyle(document.documentElement).getPropertyValue('--color-base'),
|
||||
},
|
||||
},
|
||||
y: {
|
||||
grid: {
|
||||
color: getComputedStyle(document.documentElement).getPropertyValue('--color-button-bg'),
|
||||
},
|
||||
ticks: {
|
||||
color: getComputedStyle(document.documentElement).getPropertyValue('--color-base'),
|
||||
},
|
||||
},
|
||||
},
|
||||
interaction: {
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
axis: 'xy',
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'right',
|
||||
align: 'start',
|
||||
labels: {
|
||||
color: getComputedStyle(document.documentElement).getPropertyValue('--color-base'),
|
||||
font: {
|
||||
size: 12,
|
||||
family: 'Inter',
|
||||
},
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
position: 'nearest',
|
||||
backgroundColor: getComputedStyle(document.documentElement).getPropertyValue(
|
||||
'--color-raised-bg'
|
||||
),
|
||||
borderColor: getComputedStyle(document.documentElement).getPropertyValue('--color-button-bg'),
|
||||
borderWidth: 1,
|
||||
titleColor: getComputedStyle(document.documentElement).getPropertyValue('--color-contrast'),
|
||||
titleFont: {
|
||||
size: 14,
|
||||
family: 'Inter',
|
||||
},
|
||||
bodyColor: getComputedStyle(document.documentElement).getPropertyValue('--color-base'),
|
||||
bodyFont: {
|
||||
size: 12,
|
||||
family: 'Inter',
|
||||
},
|
||||
boxPadding: 8,
|
||||
intersect: false,
|
||||
padding: 12,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
/*
|
||||
The data for the graph should look like this
|
||||
|
||||
downloads, views, likes = {
|
||||
dates: [ '2021-01-01', '2021-01-02', '2021-01-03' ], // Last 2 weeks
|
||||
data: [
|
||||
{
|
||||
title: projectName,
|
||||
color: projectColor,
|
||||
data: [ ... ],
|
||||
},
|
||||
...
|
||||
]
|
||||
}
|
||||
*/
|
||||
</script>
|
||||
@@ -1,172 +0,0 @@
|
||||
<template>
|
||||
<div v-if="shown">
|
||||
<div
|
||||
:class="{ shown: actuallyShown }"
|
||||
class="tauri-overlay"
|
||||
data-tauri-drag-region
|
||||
@click="() => (closable ? hide() : {})"
|
||||
/>
|
||||
<div
|
||||
:class="{
|
||||
shown: actuallyShown,
|
||||
noblur: props.noblur,
|
||||
}"
|
||||
class="modal-overlay"
|
||||
@click="() => (closable ? hide() : {})"
|
||||
/>
|
||||
<div class="modal-container" :class="{ shown: actuallyShown }">
|
||||
<div class="modal-body">
|
||||
<div v-if="props.header" class="header">
|
||||
<h1>{{ props.header }}</h1>
|
||||
<button v-if="closable" class="btn icon-only transparent" @click="hide">
|
||||
<XIcon />
|
||||
</button>
|
||||
</div>
|
||||
<div class="content">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else></div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { XIcon } from '@/components'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
header: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
noblur: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
closable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
})
|
||||
|
||||
const shown = ref(false)
|
||||
const actuallyShown = ref(false)
|
||||
|
||||
function show() {
|
||||
shown.value = true
|
||||
setTimeout(() => {
|
||||
actuallyShown.value = true
|
||||
}, 50)
|
||||
}
|
||||
|
||||
function hide() {
|
||||
actuallyShown.value = false
|
||||
setTimeout(() => {
|
||||
shown.value = false
|
||||
}, 300)
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
show,
|
||||
hide,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tauri-overlay {
|
||||
position: fixed;
|
||||
visibility: hidden;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
z-index: 20;
|
||||
|
||||
&.shown {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-overlay {
|
||||
visibility: hidden;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 19;
|
||||
transition: all 0.3s ease-in-out;
|
||||
|
||||
&.shown {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
background: hsla(0, 0%, 0%, 0.5);
|
||||
backdrop-filter: blur(3px);
|
||||
}
|
||||
|
||||
&.noblur {
|
||||
backdrop-filter: none;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 21;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
|
||||
&.shown {
|
||||
visibility: visible;
|
||||
.modal-body {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
position: fixed;
|
||||
box-shadow: var(--shadow-raised), var(--shadow-inset);
|
||||
border-radius: var(--radius-lg);
|
||||
max-height: calc(100% - 2 * var(--gap-lg));
|
||||
overflow-y: auto;
|
||||
width: 600px;
|
||||
pointer-events: auto;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background-color: var(--color-bg);
|
||||
padding: var(--gap-md) var(--gap-lg);
|
||||
|
||||
h1 {
|
||||
font-weight: bold;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.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(--gap-lg));
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,126 +0,0 @@
|
||||
<template>
|
||||
<Modal ref="modal" :header="props.title" :noblur="noblur">
|
||||
<div class="modal-delete">
|
||||
<div class="markdown-body" v-html="renderString(props.description)" />
|
||||
<label v-if="props.hasToType" for="confirmation" class="confirmation-label">
|
||||
<span>
|
||||
<strong>To verify, type</strong>
|
||||
<em class="confirmation-text">{{ props.confirmationText }}</em>
|
||||
<strong>below:</strong>
|
||||
</span>
|
||||
</label>
|
||||
<div class="confirmation-input">
|
||||
<input
|
||||
v-if="props.hasToType"
|
||||
id="confirmation"
|
||||
v-model="confirmation_typed"
|
||||
type="text"
|
||||
placeholder="Type here..."
|
||||
@input="type"
|
||||
/>
|
||||
</div>
|
||||
<div class="input-group push-right">
|
||||
<button class="btn" @click="modal.hide()">
|
||||
<XIcon />
|
||||
Cancel
|
||||
</button>
|
||||
<button class="btn btn-danger" :disabled="action_disabled" @click="proceed">
|
||||
<TrashIcon />
|
||||
{{ props.proceedLabel }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { renderString } from '@/helpers/parse'
|
||||
import { XIcon, TrashIcon, Modal } from '@/components'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
confirmationText: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
hasToType: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: 'No title defined',
|
||||
required: true,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: 'No description defined',
|
||||
required: true,
|
||||
},
|
||||
proceedLabel: {
|
||||
type: String,
|
||||
default: 'Proceed',
|
||||
},
|
||||
noblur: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['proceed'])
|
||||
const modal = ref(null)
|
||||
|
||||
const action_disabled = ref(props.hasToType)
|
||||
const confirmation_typed = ref('')
|
||||
|
||||
function proceed() {
|
||||
modal.value.hide()
|
||||
emit('proceed')
|
||||
}
|
||||
|
||||
function type() {
|
||||
if (props.hasToType) {
|
||||
action_disabled.value =
|
||||
confirmation_typed.value.toLowerCase() !== props.confirmationText.toLowerCase()
|
||||
}
|
||||
}
|
||||
|
||||
function show() {
|
||||
modal.value.show()
|
||||
}
|
||||
|
||||
defineExpose({ show })
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.modal-delete {
|
||||
padding: var(--gap-lg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.markdown-body {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.confirmation-label {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.confirmation-text {
|
||||
padding-right: 0.25ch;
|
||||
margin: 0 0.25rem;
|
||||
}
|
||||
|
||||
.confirmation-input {
|
||||
input {
|
||||
width: 20rem;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.button-group {
|
||||
margin-left: auto;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,140 +0,0 @@
|
||||
<template>
|
||||
<Modal ref="modal" :header="`Report ${props.itemType}`" :noblur="noblur">
|
||||
<div class="modal-report">
|
||||
<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
|
||||
<router-link to="/legal/terms">ToS</router-link>
|
||||
and
|
||||
<router-link to="/legal/rules">Rules</router-link>
|
||||
. Rest assured, we’ll keep your identifying information private.
|
||||
</p>
|
||||
<p v-if="props.itemType === 'project' || props.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.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="report-label" for="report-type">
|
||||
<span>
|
||||
<strong>Reason</strong>
|
||||
</span>
|
||||
</label>
|
||||
<DropdownSelect
|
||||
id="report-type"
|
||||
v-model="reportType"
|
||||
:options="props.reportTypes"
|
||||
default-value="Choose report type"
|
||||
class="multiselect"
|
||||
/>
|
||||
</div>
|
||||
<label class="report-label" for="additional-information">
|
||||
<strong>Additional information</strong>
|
||||
<span> Include links and images if possible. Markdown formatting is supported. </span>
|
||||
</label>
|
||||
<div>
|
||||
<div v-if="bodyViewType === 'source'" class="text-input textarea-wrapper">
|
||||
<Chips v-model="bodyViewType" class="separator" :items="['source', 'preview']" />
|
||||
<textarea id="body" v-model="body" spellcheck="true" />
|
||||
</div>
|
||||
<div v-else class="preview" v-html="renderString(body)"></div>
|
||||
</div>
|
||||
<div class="input-group push-right">
|
||||
<Button @click="cancel">
|
||||
<XIcon />
|
||||
Cancel
|
||||
</Button>
|
||||
<Button color="primary" @click="submitReport">
|
||||
<CheckIcon />
|
||||
Report
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
<script setup>
|
||||
import { Modal, Chips, XIcon, CheckIcon, DropdownSelect } from '@/components'
|
||||
import { renderString } from '@/helpers/parse.js'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
itemType: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
itemId: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
reportTypes: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
submitReport: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
noblur: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const reportType = ref('')
|
||||
const body = ref('')
|
||||
const bodyViewType = ref('source')
|
||||
|
||||
const modal = ref(null)
|
||||
|
||||
function cancel() {
|
||||
reportType.value = ''
|
||||
body.value = ''
|
||||
bodyViewType.value = 'source'
|
||||
|
||||
modal.value.hide()
|
||||
}
|
||||
|
||||
function show() {
|
||||
modal.value.show()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
show,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.modal-report {
|
||||
padding: var(--gap-lg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.markdown-body {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.report-label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.text-input {
|
||||
height: 12rem;
|
||||
gap: 1rem;
|
||||
|
||||
textarea {
|
||||
// here due to a bug in safari
|
||||
max-height: 9rem;
|
||||
}
|
||||
|
||||
.preview {
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -115,6 +115,14 @@ function stopTimer(notif) {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 750px) {
|
||||
bottom: calc(var(--size-mobile-navbar-height, 15px) + 10px) !important;
|
||||
|
||||
&.browse-menu-open {
|
||||
bottom: calc(var(--size-mobile-navbar-height-expanded, 15px) + 10px) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.notifs-enter-active,
|
||||
|
||||
@@ -36,8 +36,7 @@
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import PopoutMenu from '@/components/base/PopoutMenu.vue'
|
||||
import Button from '@/components/base/Button.vue'
|
||||
import { Button, PopoutMenu } from '@'
|
||||
|
||||
defineProps({
|
||||
options: {
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { GapIcon, LeftArrowIcon, RightArrowIcon } from '@/components'
|
||||
import { GapIcon, LeftArrowIcon, RightArrowIcon } from '@'
|
||||
</script>
|
||||
<script>
|
||||
import { defineComponent } from 'vue'
|
||||
@@ -77,7 +77,7 @@ export default defineComponent({
|
||||
pages() {
|
||||
let pages = []
|
||||
|
||||
if (this.count > 4) {
|
||||
if (this.count > 7) {
|
||||
if (this.page + 3 >= this.count) {
|
||||
pages = [
|
||||
1,
|
||||
@@ -88,7 +88,7 @@ export default defineComponent({
|
||||
this.count - 1,
|
||||
this.count,
|
||||
]
|
||||
} else if (this.page > 4) {
|
||||
} else if (this.page > 5) {
|
||||
pages = [1, '-', this.page - 1, this.page, this.page + 1, '-', this.count]
|
||||
} else {
|
||||
pages = [1, 2, 3, 4, 5, '-', this.count]
|
||||
@@ -103,6 +103,9 @@ export default defineComponent({
|
||||
methods: {
|
||||
switchPage(newPage) {
|
||||
this.$emit('switch-page', newPage)
|
||||
if (newPage !== null && newPage !== '' && !isNaN(newPage)) {
|
||||
this.$emit('switch-page', Math.min(Math.max(newPage, 1), this.count))
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { Pie } from 'vue-chartjs'
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
Title,
|
||||
Tooltip,
|
||||
PieController,
|
||||
ArcElement,
|
||||
Legend,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
} from 'chart.js'
|
||||
|
||||
ChartJS.register(Title, Tooltip, PieController, ArcElement, Legend, CategoryScale, LinearScale)
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const decimalToRgba = (decimalColor, alpha = 1) => {
|
||||
const red = (decimalColor >> 16) & 255
|
||||
const green = (decimalColor >> 8) & 255
|
||||
const blue = decimalColor & 255
|
||||
|
||||
return `rgba(${red}, ${green}, ${blue}, ${alpha})`
|
||||
}
|
||||
|
||||
const chartData = ref({
|
||||
labels: props.data.data.map((project) => project.title),
|
||||
datasets: [
|
||||
{
|
||||
label: props.data.title,
|
||||
backgroundColor: props.data.data.map((project) => decimalToRgba(project.color, 0.5)),
|
||||
borderColor: props.data.data.map((project) => decimalToRgba(project.color)),
|
||||
data: props.data.data.map((project) => project.data),
|
||||
fill: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const chartOptions = ref({
|
||||
responsive: true,
|
||||
elements: {
|
||||
point: {
|
||||
radius: 0,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'right',
|
||||
labels: {
|
||||
color: getComputedStyle(document.documentElement).getPropertyValue('--color-base'),
|
||||
font: {
|
||||
size: 12,
|
||||
family: 'Inter',
|
||||
},
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
position: 'nearest',
|
||||
backgroundColor: getComputedStyle(document.documentElement).getPropertyValue(
|
||||
'--color-raised-bg'
|
||||
),
|
||||
borderColor: getComputedStyle(document.documentElement).getPropertyValue('--color-button-bg'),
|
||||
borderWidth: 1,
|
||||
titleColor: getComputedStyle(document.documentElement).getPropertyValue('--color-contrast'),
|
||||
titleFont: {
|
||||
size: 16,
|
||||
family: 'Inter',
|
||||
},
|
||||
bodyColor: getComputedStyle(document.documentElement).getPropertyValue('--color-base'),
|
||||
bodyFont: {
|
||||
size: 12,
|
||||
family: 'Inter',
|
||||
},
|
||||
boxPadding: 8,
|
||||
intersect: false,
|
||||
padding: 12,
|
||||
displayColors: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Pie :options="chartOptions" :data="chartData" />
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -34,6 +34,7 @@
|
||||
:server-side="serverSide"
|
||||
:type="projectTypeDisplay"
|
||||
:search="search"
|
||||
:categories="categories"
|
||||
/>
|
||||
</Categories>
|
||||
<div class="stats">
|
||||
@@ -65,7 +66,9 @@
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { formatNumber } from '@/helpers'
|
||||
import {
|
||||
Badge,
|
||||
HeartIcon,
|
||||
@@ -75,12 +78,13 @@ import {
|
||||
Avatar,
|
||||
Categories,
|
||||
EnvironmentIndicator,
|
||||
} from '@/components'
|
||||
import { formatNumber } from '@/helpers/utils.js'
|
||||
} from '@'
|
||||
|
||||
import dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
dayjs.extend(relativeTime)
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue'
|
||||
export default defineComponent({
|
||||
@@ -152,10 +156,6 @@ export default defineComponent({
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
hasModMessage: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
serverSide: {
|
||||
type: String,
|
||||
required: false,
|
||||
@@ -306,8 +306,7 @@ export default defineComponent({
|
||||
img,
|
||||
svg {
|
||||
border-radius: var(--radius-lg);
|
||||
border: 4px solid var(--color-raised-bg);
|
||||
border-bottom: none;
|
||||
box-shadow: -2px -2px 0 2px var(--color-raised-bg), 2px -2px 0 2px var(--color-raised-bg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,9 +25,11 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import BisectIcon from '@/assets/external/bh.svg'
|
||||
import { BisectIcon } from '@'
|
||||
|
||||
const props = defineProps({
|
||||
external: {
|
||||
type: Boolean,
|
||||
@@ -38,6 +40,7 @@ const props = defineProps({
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
|
||||
const target = computed(() => (props.external ? '_blank' : '_self'))
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,270 +0,0 @@
|
||||
<script setup>
|
||||
import {
|
||||
Button,
|
||||
Modal,
|
||||
ClipboardCopyIcon,
|
||||
LinkIcon,
|
||||
ShareIcon,
|
||||
MailIcon,
|
||||
GlobeIcon,
|
||||
TwitterIcon,
|
||||
MastodonIcon,
|
||||
RedditIcon,
|
||||
} from '@/components'
|
||||
import { computed, ref, nextTick } from 'vue'
|
||||
import QrcodeVue from 'qrcode.vue'
|
||||
|
||||
const props = defineProps({
|
||||
header: {
|
||||
type: String,
|
||||
default: 'Share',
|
||||
},
|
||||
shareTitle: {
|
||||
type: String,
|
||||
default: 'Modrinth',
|
||||
},
|
||||
shareText: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
link: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const shareModal = ref(null)
|
||||
|
||||
const qrCode = ref(null)
|
||||
const qrImage = ref(null)
|
||||
const content = ref(null)
|
||||
const url = ref(null)
|
||||
const canShare = ref(false)
|
||||
const share = () => {
|
||||
navigator.share(
|
||||
props.link
|
||||
? {
|
||||
title: props.shareTitle.toString(),
|
||||
text: props.shareText,
|
||||
url: url.value,
|
||||
}
|
||||
: {
|
||||
title: props.shareTitle.toString(),
|
||||
text: content.value,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const show = async (passedContent) => {
|
||||
content.value = props.shareText ? `${props.shareText}\n\n${passedContent}` : passedContent
|
||||
shareModal.value.show()
|
||||
if (props.link) {
|
||||
url.value = passedContent
|
||||
nextTick(() => {
|
||||
console.log(qrCode.value)
|
||||
fetch(qrCode.value.getElementsByTagName('canvas')[0].toDataURL('image/png'))
|
||||
.then((res) => res.blob())
|
||||
.then((blob) => {
|
||||
console.log(blob)
|
||||
qrImage.value = blob
|
||||
})
|
||||
})
|
||||
}
|
||||
if (navigator.canShare({ title: props.shareTitle.toString(), text: content.value })) {
|
||||
canShare.value = true
|
||||
}
|
||||
}
|
||||
|
||||
const copyImage = async () => {
|
||||
const item = new ClipboardItem({ 'image/png': qrImage.value })
|
||||
await navigator.clipboard.write([item])
|
||||
}
|
||||
|
||||
const copyText = async () => {
|
||||
await navigator.clipboard.writeText(url.value ?? content.value)
|
||||
}
|
||||
|
||||
const sendEmail = computed(
|
||||
() =>
|
||||
`mailto:user@test.com
|
||||
?subject=${encodeURIComponent(props.shareTitle)}
|
||||
&body=` + encodeURIComponent(content.value)
|
||||
)
|
||||
|
||||
const sendTweet = computed(
|
||||
() => `https://twitter.com/intent/tweet?text=` + encodeURIComponent(content.value)
|
||||
)
|
||||
|
||||
const sendToot = computed(() => `https://tootpick.org/#text=` + encodeURIComponent(content.value))
|
||||
|
||||
const postOnReddit = computed(
|
||||
() =>
|
||||
`https://www.reddit.com/submit?title=${encodeURIComponent(props.shareTitle)}&text=` +
|
||||
encodeURIComponent(content.value)
|
||||
)
|
||||
|
||||
defineExpose({
|
||||
show,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal ref="shareModal" :header="header">
|
||||
<div class="share-body">
|
||||
<div v-if="link" class="qr-wrapper">
|
||||
<div ref="qrCode">
|
||||
<QrcodeVue :value="url" class="qr-code" margin="3" />
|
||||
</div>
|
||||
<Button v-tooltip="'Copy QR code'" icon-only class="copy-button" @click="copyImage">
|
||||
<ClipboardCopyIcon />
|
||||
</Button>
|
||||
</div>
|
||||
<div v-else class="resizable-textarea-wrapper">
|
||||
<textarea v-model="content" />
|
||||
<Button v-tooltip="'Copy Text'" icon-only class="copy-button transparent" @click="copyText">
|
||||
<ClipboardCopyIcon />
|
||||
</Button>
|
||||
</div>
|
||||
<div class="all-buttons">
|
||||
<div v-if="link" class="iconified-input">
|
||||
<LinkIcon />
|
||||
<input type="text" :value="url" readonly />
|
||||
<Button v-tooltip="'Copy Text'" @click="copyText">
|
||||
<ClipboardCopyIcon />
|
||||
</Button>
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<Button v-if="canShare" v-tooltip="'Share'" icon-only @click="share">
|
||||
<ShareIcon />
|
||||
</Button>
|
||||
<a v-tooltip="'Send as an email'" class="btn icon-only" target="_blank" :href="sendEmail">
|
||||
<MailIcon />
|
||||
</a>
|
||||
<a
|
||||
v-if="link"
|
||||
v-tooltip="'Open link in browser'"
|
||||
class="btn icon-only"
|
||||
target="_blank"
|
||||
:href="url"
|
||||
>
|
||||
<GlobeIcon />
|
||||
</a>
|
||||
<a
|
||||
v-tooltip="'Toot about it'"
|
||||
class="btn mastodon icon-only"
|
||||
target="_blank"
|
||||
:href="sendToot"
|
||||
>
|
||||
<MastodonIcon />
|
||||
</a>
|
||||
<a
|
||||
v-tooltip="'Tweet about it'"
|
||||
class="btn twitter icon-only"
|
||||
target="_blank"
|
||||
:href="sendTweet"
|
||||
>
|
||||
<TwitterIcon />
|
||||
</a>
|
||||
<a
|
||||
v-tooltip="'Share on Reddit'"
|
||||
class="btn reddit icon-only"
|
||||
target="_blank"
|
||||
:href="postOnReddit"
|
||||
>
|
||||
<RedditIcon />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.share-body {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--gap-sm);
|
||||
padding: var(--gap-lg);
|
||||
}
|
||||
|
||||
.all-buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap-sm);
|
||||
flex-grow: 1;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.iconified-input {
|
||||
width: 100%;
|
||||
|
||||
input {
|
||||
flex-basis: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.button-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--gap-sm);
|
||||
|
||||
.btn {
|
||||
fill: var(--color-contrast);
|
||||
color: var(--color-contrast);
|
||||
|
||||
&.reddit {
|
||||
background-color: #ff4500;
|
||||
}
|
||||
|
||||
&.mastodon {
|
||||
background-color: #563acc;
|
||||
}
|
||||
|
||||
&.twitter {
|
||||
background-color: #1da1f2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.qr-wrapper {
|
||||
position: relative;
|
||||
margin: 0 auto;
|
||||
|
||||
&:hover {
|
||||
.copy-button {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.qr-code {
|
||||
background-color: white !important;
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.copy-button {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
margin: var(--gap-sm);
|
||||
transition: all 0.2s ease-in-out;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.resizable-textarea-wrapper {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.btn {
|
||||
opacity: 1;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -33,5 +33,3 @@ export default {
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
Reference in New Issue
Block a user