Move many things over from Knossos (and other rearrangements) (#102)

This commit is contained in:
Emma Alexia
2023-10-16 21:18:23 -04:00
committed by GitHub
parent 46a6fee81d
commit 8369330053
68 changed files with 852 additions and 342 deletions

View File

@@ -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>

View File

@@ -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);
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -1,5 +1,5 @@
<script setup>
import { ExternalIcon, UnknownIcon } from '@/components'
import { ExternalIcon, UnknownIcon } from '@'
import { computed } from 'vue'

View File

@@ -1,5 +1,5 @@
<script setup>
import { Button, DropdownIcon } from '@/components'
import { Button, DropdownIcon } from '@'
import { reactive } from 'vue'

View File

@@ -24,7 +24,7 @@
</div>
</template>
<script setup>
import { CheckIcon, DropdownIcon } from '@/components'
import { CheckIcon, DropdownIcon } from '@'
</script>
<script>
import { defineComponent } from 'vue'

View File

@@ -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;

View 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>

View File

@@ -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;

View 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>

View File

@@ -61,7 +61,7 @@
</template>
<script setup>
import { DropdownIcon } from '@/components'
import { DropdownIcon } from '@'
import { computed, ref, watch } from 'vue'
const props = defineProps({

View File

@@ -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>

View File

@@ -13,7 +13,7 @@
</template>
<script>
import { fileIsValid } from '@/helpers/utils.js'
import { fileIsValid } from '@'
import { defineComponent } from 'vue'
export default defineComponent({
props: {

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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, well 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>

View File

@@ -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,

View File

@@ -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: {

View File

@@ -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))
}
},
},
})

View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -33,5 +33,3 @@ export default {
},
}
</script>
<style scoped></style>