You've already forked AstralRinth
forked from didirus/AstralRinth
Add TailwindCSS (#1252)
* Setup TailwindCSS * Fully setup configuration * Refactor some tailwind variables
This commit is contained in:
@@ -43,32 +43,32 @@
|
||||
/>
|
||||
<path
|
||||
d="M51.28 316.13c10.59 125 115.54 223.3 243.27 223.3 96.51 0 180.02-56.12 219.63-137.46l48.61 16.83c-46.78 101.34-149.35 171.75-268.24 171.75C138.6 590.55 10.71 469.38 0 316.13h51.28ZM.78 265.24C15.86 116.36 141.73 0 294.56 0c162.97 0 295.28 132.31 295.28 295.28 0 26.14-3.4 51.49-9.8 75.63l-48.48-16.78a244.28 244.28 0 0 0 7.15-58.85c0-134.75-109.4-244.15-244.15-244.15-124.58 0-227.49 93.5-242.32 214.11H.8Z"
|
||||
class="ring ring--large"
|
||||
class="ring--large ring"
|
||||
/>
|
||||
<path
|
||||
d="M293.77 153.17c-78.49.07-142.2 63.83-142.2 142.34 0 78.56 63.79 142.34 142.35 142.34 3.98 0 7.93-.16 11.83-.49l14.22 49.76a194.65 194.65 0 0 1-26.05 1.74c-106.72 0-193.36-86.64-193.36-193.35 0-106.72 86.64-193.35 193.36-193.35 2.64 0 5.28.05 7.9.16l-8.05 50.85Zm58.2-42.13c78.39 24.67 135.3 97.98 135.3 184.47 0 80.07-48.77 148.83-118.2 178.18l-14.17-49.55c48.08-22.85 81.36-71.89 81.36-128.63 0-60.99-38.44-113.07-92.39-133.32l8.1-51.15Z"
|
||||
class="ring ring--small"
|
||||
class="ring--small ring"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const loading = useLoading()
|
||||
const loading = useLoading();
|
||||
|
||||
const config = useRuntimeConfig()
|
||||
const config = useRuntimeConfig();
|
||||
|
||||
const api = computed(() => {
|
||||
const apiUrl = config.public.apiBaseUrl
|
||||
if (apiUrl.startsWith('https://api.modrinth.com')) {
|
||||
return 'prod'
|
||||
} else if (apiUrl.startsWith('https://staging-api.modrinth.com')) {
|
||||
return 'staging'
|
||||
} else if (apiUrl.startsWith('localhost') || apiUrl.startsWith('127.0.0.1')) {
|
||||
return 'localhost'
|
||||
const apiUrl = config.public.apiBaseUrl;
|
||||
if (apiUrl.startsWith("https://api.modrinth.com")) {
|
||||
return "prod";
|
||||
} else if (apiUrl.startsWith("https://staging-api.modrinth.com")) {
|
||||
return "staging";
|
||||
} else if (apiUrl.startsWith("localhost") || apiUrl.startsWith("127.0.0.1")) {
|
||||
return "localhost";
|
||||
}
|
||||
return 'foreign'
|
||||
})
|
||||
return "foreign";
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -35,8 +35,8 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const pixelated = ref(false)
|
||||
const img = ref(null)
|
||||
const pixelated = ref(false);
|
||||
const img = ref(null);
|
||||
|
||||
defineProps({
|
||||
src: {
|
||||
@@ -45,13 +45,13 @@ defineProps({
|
||||
},
|
||||
alt: {
|
||||
type: String,
|
||||
default: '',
|
||||
default: "",
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'sm',
|
||||
default: "sm",
|
||||
validator(value) {
|
||||
return ['xxs', 'xs', 'sm', 'md', 'lg'].includes(value)
|
||||
return ["xxs", "xs", "sm", "md", "lg"].includes(value);
|
||||
},
|
||||
},
|
||||
circle: {
|
||||
@@ -64,19 +64,19 @@ defineProps({
|
||||
},
|
||||
loading: {
|
||||
type: String,
|
||||
default: 'eager',
|
||||
default: "eager",
|
||||
},
|
||||
raised: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
function updatePixelated() {
|
||||
if (img.value && img.value.naturalWidth && img.value.naturalWidth <= 96) {
|
||||
pixelated.value = true
|
||||
pixelated.value = true;
|
||||
} else {
|
||||
pixelated.value = false
|
||||
pixelated.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -85,8 +85,10 @@ function updatePixelated() {
|
||||
.avatar {
|
||||
border-radius: var(--size-rounded-icon);
|
||||
box-shadow: var(--shadow-inset-lg), var(--shadow-card);
|
||||
height: var(--size);
|
||||
width: var(--size);
|
||||
min-height: var(--size);
|
||||
min-width: var(--size);
|
||||
max-height: var(--size);
|
||||
max-width: var(--size);
|
||||
background-color: var(--color-button-bg);
|
||||
object-fit: contain;
|
||||
|
||||
|
||||
@@ -35,19 +35,19 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ModrinthIcon from '~/assets/images/logo.svg?component'
|
||||
import ModeratorIcon from '~/assets/images/sidebar/admin.svg?component'
|
||||
import CreatorIcon from '~/assets/images/utils/box.svg?component'
|
||||
import ListIcon from '~/assets/images/utils/list.svg?component'
|
||||
import EyeOffIcon from '~/assets/images/utils/eye-off.svg?component'
|
||||
import DraftIcon from '~/assets/images/utils/file-text.svg?component'
|
||||
import CrossIcon from '~/assets/images/utils/x.svg?component'
|
||||
import ArchiveIcon from '~/assets/images/utils/archive.svg?component'
|
||||
import ProcessingIcon from '~/assets/images/utils/updated.svg?component'
|
||||
import CheckIcon from '~/assets/images/utils/check.svg?component'
|
||||
import LockIcon from '~/assets/images/utils/lock.svg?component'
|
||||
import CalendarIcon from '~/assets/images/utils/calendar.svg?component'
|
||||
import CloseIcon from '~/assets/images/utils/check-circle.svg?component'
|
||||
import ModrinthIcon from "~/assets/images/logo.svg?component";
|
||||
import ModeratorIcon from "~/assets/images/sidebar/admin.svg?component";
|
||||
import CreatorIcon from "~/assets/images/utils/box.svg?component";
|
||||
import ListIcon from "~/assets/images/utils/list.svg?component";
|
||||
import EyeOffIcon from "~/assets/images/utils/eye-off.svg?component";
|
||||
import DraftIcon from "~/assets/images/utils/file-text.svg?component";
|
||||
import CrossIcon from "~/assets/images/utils/x.svg?component";
|
||||
import ArchiveIcon from "~/assets/images/utils/archive.svg?component";
|
||||
import ProcessingIcon from "~/assets/images/utils/updated.svg?component";
|
||||
import CheckIcon from "~/assets/images/utils/check.svg?component";
|
||||
import LockIcon from "~/assets/images/utils/lock.svg?component";
|
||||
import CalendarIcon from "~/assets/images/utils/calendar.svg?component";
|
||||
import CloseIcon from "~/assets/images/utils/check-circle.svg?component";
|
||||
|
||||
defineProps({
|
||||
type: {
|
||||
@@ -56,9 +56,9 @@ defineProps({
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: '',
|
||||
default: "",
|
||||
},
|
||||
})
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ChevronRightIcon from '~/assets/images/utils/chevron-right.svg?component'
|
||||
import ChevronRightIcon from "~/assets/images/utils/chevron-right.svg?component";
|
||||
|
||||
defineProps({
|
||||
linkStack: {
|
||||
@@ -26,7 +26,7 @@ defineProps({
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -25,8 +25,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CheckIcon from '~/assets/images/utils/check.svg?component'
|
||||
import DropdownIcon from '~/assets/images/utils/dropdown.svg?component'
|
||||
import CheckIcon from "~/assets/images/utils/check.svg?component";
|
||||
import DropdownIcon from "~/assets/images/utils/dropdown.svg?component";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -36,7 +36,7 @@ export default {
|
||||
props: {
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
default: "",
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
@@ -56,15 +56,15 @@ export default {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
emits: ["update:modelValue"],
|
||||
methods: {
|
||||
toggle() {
|
||||
if (!this.disabled) {
|
||||
this.$emit('update:modelValue', !this.modelValue)
|
||||
this.$emit("update:modelValue", !this.modelValue);
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -105,7 +105,9 @@ export default {
|
||||
color: var(--color-button-text);
|
||||
background-color: var(--color-button-bg);
|
||||
border-radius: var(--size-rounded-control);
|
||||
box-shadow: var(--shadow-inset-sm), 0 0 0 0 transparent;
|
||||
box-shadow:
|
||||
var(--shadow-inset-sm),
|
||||
0 0 0 0 transparent;
|
||||
|
||||
&.checked {
|
||||
background-color: var(--color-brand);
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CheckIcon from '~/assets/images/utils/check.svg?component'
|
||||
import CheckIcon from "~/assets/images/utils/check.svg?component";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -42,32 +42,32 @@ export default {
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
emits: ["update:modelValue"],
|
||||
computed: {
|
||||
selected: {
|
||||
get() {
|
||||
return this.modelValue
|
||||
return this.modelValue;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('update:modelValue', value)
|
||||
this.$emit("update:modelValue", value);
|
||||
},
|
||||
},
|
||||
},
|
||||
created() {
|
||||
if (this.items.length > 0 && this.neverEmpty) {
|
||||
this.selected = this.items[0]
|
||||
this.selected = this.items[0];
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleItem(item) {
|
||||
if (this.selected === item && !this.neverEmpty) {
|
||||
this.selected = null
|
||||
this.selected = null;
|
||||
} else {
|
||||
this.selected = item
|
||||
this.selected = item;
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -95,7 +95,9 @@ export default {
|
||||
.selected {
|
||||
color: var(--color-button-text-active);
|
||||
background-color: var(--color-brand-highlight);
|
||||
box-shadow: inset 0 0 0 transparent, 0 0 0 2px var(--color-brand);
|
||||
box-shadow:
|
||||
inset 0 0 0 transparent,
|
||||
0 0 0 2px var(--color-brand);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
<div class="markdown-body">
|
||||
<p>
|
||||
Your new collection will be created as a public collection with
|
||||
{{ projectIds.length > 0 ? projectIds.length : 'no' }}
|
||||
{{ projectIds.length !== 1 ? 'projects' : 'project' }}.
|
||||
{{ projectIds.length > 0 ? projectIds.length : "no" }}
|
||||
{{ projectIds.length !== 1 ? "projects" : "project" }}.
|
||||
</p>
|
||||
</div>
|
||||
<label for="name">
|
||||
@@ -40,61 +40,61 @@
|
||||
</Modal>
|
||||
</template>
|
||||
<script setup>
|
||||
import { XIcon as CrossIcon, CheckIcon } from '@modrinth/assets'
|
||||
import { Modal, Button } from '@modrinth/ui'
|
||||
import { XIcon as CrossIcon, CheckIcon } from "@modrinth/assets";
|
||||
import { Modal, Button } from "@modrinth/ui";
|
||||
|
||||
const router = useNativeRouter()
|
||||
const router = useNativeRouter();
|
||||
|
||||
const name = ref('')
|
||||
const description = ref('')
|
||||
const name = ref("");
|
||||
const description = ref("");
|
||||
|
||||
const modal = ref()
|
||||
const modal = ref();
|
||||
|
||||
const props = defineProps({
|
||||
projectIds: {
|
||||
type: Array,
|
||||
default() {
|
||||
return []
|
||||
return [];
|
||||
},
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
async function create() {
|
||||
startLoading()
|
||||
startLoading();
|
||||
try {
|
||||
const result = await useBaseFetch('collection', {
|
||||
method: 'POST',
|
||||
const result = await useBaseFetch("collection", {
|
||||
method: "POST",
|
||||
body: {
|
||||
name: name.value.trim(),
|
||||
description: description.value.trim(),
|
||||
projects: props.projectIds,
|
||||
},
|
||||
apiVersion: 3,
|
||||
})
|
||||
});
|
||||
|
||||
await initUserCollections()
|
||||
await initUserCollections();
|
||||
|
||||
modal.value.hide()
|
||||
await router.push(`/collection/${result.id}`)
|
||||
modal.value.hide();
|
||||
await router.push(`/collection/${result.id}`);
|
||||
} catch (err) {
|
||||
addNotification({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
group: "main",
|
||||
title: "An error occurred",
|
||||
text: err?.data?.description || err?.message || err,
|
||||
type: 'error',
|
||||
})
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
stopLoading()
|
||||
stopLoading();
|
||||
}
|
||||
function show() {
|
||||
name.value = ''
|
||||
description.value = ''
|
||||
modal.value.show()
|
||||
name.value = "";
|
||||
description.value = "";
|
||||
modal.value.show();
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
show,
|
||||
})
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -17,5 +17,5 @@ defineProps({
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CheckIcon from '~/assets/images/utils/check.svg?component'
|
||||
import ClipboardCopyIcon from '~/assets/images/utils/clipboard-copy.svg?component'
|
||||
import CheckIcon from "~/assets/images/utils/check.svg?component";
|
||||
import ClipboardCopyIcon from "~/assets/images/utils/clipboard-copy.svg?component";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -24,15 +24,15 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
copied: false,
|
||||
}
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async copyText() {
|
||||
await navigator.clipboard.writeText(this.text)
|
||||
this.copied = true
|
||||
await navigator.clipboard.writeText(this.text);
|
||||
this.copied = true;
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -48,7 +48,10 @@ 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,
|
||||
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 {
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
class="drop-area"
|
||||
@drop.stop.prevent="
|
||||
(event) => {
|
||||
$refs.drop_area.style.visibility = 'hidden'
|
||||
$refs.drop_area.style.visibility = 'hidden';
|
||||
|
||||
if (event.dataTransfer && event.dataTransfer.files && fileAllowed) {
|
||||
$emit('change', event.dataTransfer.files)
|
||||
$emit('change', event.dataTransfer.files);
|
||||
}
|
||||
}
|
||||
"
|
||||
@@ -22,45 +22,45 @@ export default {
|
||||
props: {
|
||||
accept: {
|
||||
type: String,
|
||||
default: '',
|
||||
default: "",
|
||||
},
|
||||
},
|
||||
emits: ['change'],
|
||||
emits: ["change"],
|
||||
data() {
|
||||
return {
|
||||
fileAllowed: false,
|
||||
}
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
document.addEventListener('dragenter', this.allowDrag)
|
||||
document.addEventListener("dragenter", this.allowDrag);
|
||||
},
|
||||
methods: {
|
||||
allowDrag(event) {
|
||||
const file = event.dataTransfer?.items[0]
|
||||
const file = event.dataTransfer?.items[0];
|
||||
|
||||
if (
|
||||
file &&
|
||||
this.accept
|
||||
.split(',')
|
||||
.reduce((acc, t) => acc || file.type.startsWith(t) || file.type === t || t === '*', false)
|
||||
.split(",")
|
||||
.reduce((acc, t) => acc || file.type.startsWith(t) || file.type === t || t === "*", false)
|
||||
) {
|
||||
this.fileAllowed = true
|
||||
event.dataTransfer.dropEffect = 'copy'
|
||||
event.preventDefault()
|
||||
this.fileAllowed = true;
|
||||
event.dataTransfer.dropEffect = "copy";
|
||||
event.preventDefault();
|
||||
|
||||
if (this.$refs.drop_area) {
|
||||
this.$refs.drop_area.style.visibility = 'visible'
|
||||
this.$refs.drop_area.style.visibility = "visible";
|
||||
}
|
||||
} else {
|
||||
this.fileAllowed = false
|
||||
this.fileAllowed = false;
|
||||
|
||||
if (this.$refs.drop_area) {
|
||||
this.$refs.drop_area.style.visibility = 'hidden'
|
||||
this.$refs.drop_area.style.visibility = "hidden";
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -73,13 +73,15 @@ export default {
|
||||
z-index: 10;
|
||||
visibility: hidden;
|
||||
background-color: hsla(0, 0%, 0%, 0.5);
|
||||
transition: visibility 0.2s ease-in-out, background-color 0.1s ease-in-out;
|
||||
transition:
|
||||
visibility 0.2s ease-in-out,
|
||||
background-color 0.1s ease-in-out;
|
||||
display: flex;
|
||||
|
||||
&::before {
|
||||
--indent: 4rem;
|
||||
|
||||
content: ' ';
|
||||
content: " ";
|
||||
position: relative;
|
||||
top: var(--indent);
|
||||
left: var(--indent);
|
||||
|
||||
@@ -48,25 +48,25 @@
|
||||
</span>
|
||||
</template>
|
||||
<script setup>
|
||||
import InfoIcon from '~/assets/images/utils/info.svg?component'
|
||||
import ClientIcon from '~/assets/images/utils/client.svg?component'
|
||||
import GlobeIcon from '~/assets/images/utils/globe.svg?component'
|
||||
import ServerIcon from '~/assets/images/utils/server.svg?component'
|
||||
import InfoIcon from "~/assets/images/utils/info.svg?component";
|
||||
import ClientIcon from "~/assets/images/utils/client.svg?component";
|
||||
import GlobeIcon from "~/assets/images/utils/globe.svg?component";
|
||||
import ServerIcon from "~/assets/images/utils/server.svg?component";
|
||||
|
||||
defineProps({
|
||||
type: {
|
||||
type: String,
|
||||
default: 'mod',
|
||||
default: "mod",
|
||||
},
|
||||
serverSide: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
default: "",
|
||||
},
|
||||
clientSide: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
default: "",
|
||||
},
|
||||
typeOnly: {
|
||||
type: Boolean,
|
||||
@@ -87,12 +87,12 @@ defineProps({
|
||||
type: Array,
|
||||
required: false,
|
||||
default() {
|
||||
return []
|
||||
return [];
|
||||
},
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const tags = useTags()
|
||||
const tags = useTags();
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.environment {
|
||||
|
||||
@@ -18,14 +18,14 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { fileIsValid } from '~/helpers/fileUtils.js'
|
||||
import { fileIsValid } from "~/helpers/fileUtils.js";
|
||||
|
||||
export default {
|
||||
components: {},
|
||||
props: {
|
||||
prompt: {
|
||||
type: String,
|
||||
default: 'Select file',
|
||||
default: "Select file",
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
@@ -59,33 +59,33 @@ export default {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['change'],
|
||||
emits: ["change"],
|
||||
data() {
|
||||
return {
|
||||
files: [],
|
||||
}
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
addFiles(files, shouldNotReset) {
|
||||
if (!shouldNotReset || this.shouldAlwaysReset) {
|
||||
this.files = files
|
||||
this.files = files;
|
||||
}
|
||||
|
||||
const validationOptions = { maxSize: this.maxSize, alertOnInvalid: true }
|
||||
this.files = [...this.files].filter((file) => fileIsValid(file, validationOptions))
|
||||
const validationOptions = { maxSize: this.maxSize, alertOnInvalid: true };
|
||||
this.files = [...this.files].filter((file) => fileIsValid(file, validationOptions));
|
||||
|
||||
if (this.files.length > 0) {
|
||||
this.$emit('change', this.files)
|
||||
this.$emit("change", this.files);
|
||||
}
|
||||
},
|
||||
handleDrop(e) {
|
||||
this.addFiles(e.dataTransfer.files)
|
||||
this.addFiles(e.dataTransfer.files);
|
||||
},
|
||||
handleChange(e) {
|
||||
this.addFiles(e.target.files)
|
||||
this.addFiles(e.target.files);
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -7,12 +7,12 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
type MessageType = 'information' | 'warning'
|
||||
type MessageType = "information" | "warning";
|
||||
const props = withDefaults(defineProps<{ messageType?: MessageType }>(), {
|
||||
messageType: 'information',
|
||||
})
|
||||
const cardClassByType = computed(() => `message-banner__content_${props.messageType}`)
|
||||
const ariaLabelByType = computed(() => `Banner with ${props.messageType} message`)
|
||||
messageType: "information",
|
||||
});
|
||||
const cardClassByType = computed(() => `message-banner__content_${props.messageType}`);
|
||||
const ariaLabelByType = computed(() => `Banner with ${props.messageType} message`);
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CrossIcon from '~/assets/images/utils/x.svg?component'
|
||||
import CrossIcon from "~/assets/images/utils/x.svg?component";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -39,31 +39,31 @@ export default {
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const cosmetics = useCosmetics()
|
||||
const cosmetics = useCosmetics();
|
||||
|
||||
return { cosmetics }
|
||||
return { cosmetics };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
shown: false,
|
||||
actuallyShown: false,
|
||||
}
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
show() {
|
||||
this.shown = true
|
||||
this.shown = true;
|
||||
setTimeout(() => {
|
||||
this.actuallyShown = true
|
||||
}, 50)
|
||||
this.actuallyShown = true;
|
||||
}, 50);
|
||||
},
|
||||
hide() {
|
||||
this.actuallyShown = false
|
||||
this.actuallyShown = false;
|
||||
setTimeout(() => {
|
||||
this.shown = false
|
||||
}, 300)
|
||||
this.shown = false;
|
||||
}, 300);
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -34,10 +34,10 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { renderString } from '@modrinth/utils'
|
||||
import CrossIcon from '~/assets/images/utils/x.svg?component'
|
||||
import TrashIcon from '~/assets/images/utils/trash.svg?component'
|
||||
import Modal from '~/components/ui/Modal.vue'
|
||||
import { renderString } from "@modrinth/utils";
|
||||
import CrossIcon from "~/assets/images/utils/x.svg?component";
|
||||
import TrashIcon from "~/assets/images/utils/trash.svg?component";
|
||||
import Modal from "~/components/ui/Modal.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -48,7 +48,7 @@ export default {
|
||||
props: {
|
||||
confirmationText: {
|
||||
type: String,
|
||||
default: '',
|
||||
default: "",
|
||||
},
|
||||
hasToType: {
|
||||
type: Boolean,
|
||||
@@ -56,46 +56,46 @@ export default {
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: 'No title defined',
|
||||
default: "No title defined",
|
||||
required: true,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: 'No description defined',
|
||||
default: "No description defined",
|
||||
required: true,
|
||||
},
|
||||
proceedLabel: {
|
||||
type: String,
|
||||
default: 'Proceed',
|
||||
default: "Proceed",
|
||||
},
|
||||
},
|
||||
emits: ['proceed'],
|
||||
emits: ["proceed"],
|
||||
data() {
|
||||
return {
|
||||
action_disabled: this.hasToType,
|
||||
confirmation_typed: '',
|
||||
}
|
||||
confirmation_typed: "",
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
renderString,
|
||||
cancel() {
|
||||
this.$refs.modal.hide()
|
||||
this.$refs.modal.hide();
|
||||
},
|
||||
proceed() {
|
||||
this.$refs.modal.hide()
|
||||
this.$emit('proceed')
|
||||
this.$refs.modal.hide();
|
||||
this.$emit("proceed");
|
||||
},
|
||||
type() {
|
||||
if (this.hasToType) {
|
||||
this.action_disabled =
|
||||
this.confirmation_typed.toLowerCase() !== this.confirmationText.toLowerCase()
|
||||
this.confirmation_typed.toLowerCase() !== this.confirmationText.toLowerCase();
|
||||
}
|
||||
},
|
||||
show() {
|
||||
this.$refs.modal.show()
|
||||
this.$refs.modal.show();
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -73,10 +73,10 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Multiselect } from 'vue-multiselect'
|
||||
import CrossIcon from '~/assets/images/utils/x.svg?component'
|
||||
import CheckIcon from '~/assets/images/utils/right-arrow.svg?component'
|
||||
import Modal from '~/components/ui/Modal.vue'
|
||||
import { Multiselect } from "vue-multiselect";
|
||||
import CrossIcon from "~/assets/images/utils/x.svg?component";
|
||||
import CheckIcon from "~/assets/images/utils/right-arrow.svg?component";
|
||||
import Modal from "~/components/ui/Modal.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -93,122 +93,122 @@ export default {
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const tags = useTags()
|
||||
const tags = useTags();
|
||||
|
||||
return { tags }
|
||||
return { tags };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
name: '',
|
||||
slug: '',
|
||||
description: '',
|
||||
name: "",
|
||||
slug: "",
|
||||
description: "",
|
||||
manualSlug: false,
|
||||
visibilities: [
|
||||
{
|
||||
actual: 'approved',
|
||||
display: 'Public',
|
||||
actual: "approved",
|
||||
display: "Public",
|
||||
},
|
||||
{
|
||||
actual: 'private',
|
||||
display: 'Private',
|
||||
actual: "private",
|
||||
display: "Private",
|
||||
},
|
||||
{
|
||||
actual: 'unlisted',
|
||||
display: 'Unlisted',
|
||||
actual: "unlisted",
|
||||
display: "Unlisted",
|
||||
},
|
||||
],
|
||||
visibility: {
|
||||
actual: 'approved',
|
||||
display: 'Public',
|
||||
actual: "approved",
|
||||
display: "Public",
|
||||
},
|
||||
}
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
cancel() {
|
||||
this.$refs.modal.hide()
|
||||
this.$refs.modal.hide();
|
||||
},
|
||||
async createProject() {
|
||||
startLoading()
|
||||
startLoading();
|
||||
|
||||
const formData = new FormData()
|
||||
const formData = new FormData();
|
||||
|
||||
const auth = await useAuth()
|
||||
const auth = await useAuth();
|
||||
|
||||
const projectData = {
|
||||
title: this.name.trim(),
|
||||
project_type: 'mod',
|
||||
project_type: "mod",
|
||||
slug: this.slug,
|
||||
description: this.description.trim(),
|
||||
body: '',
|
||||
body: "",
|
||||
requested_status: this.visibility.actual,
|
||||
initial_versions: [],
|
||||
team_members: [
|
||||
{
|
||||
user_id: auth.value.user.id,
|
||||
name: auth.value.user.username,
|
||||
role: 'Owner',
|
||||
role: "Owner",
|
||||
},
|
||||
],
|
||||
categories: [],
|
||||
client_side: 'required',
|
||||
server_side: 'required',
|
||||
license_id: 'LicenseRef-Unknown',
|
||||
client_side: "required",
|
||||
server_side: "required",
|
||||
license_id: "LicenseRef-Unknown",
|
||||
is_draft: true,
|
||||
}
|
||||
};
|
||||
|
||||
if (this.organizationId) {
|
||||
projectData.organization_id = this.organizationId
|
||||
projectData.organization_id = this.organizationId;
|
||||
}
|
||||
|
||||
formData.append('data', JSON.stringify(projectData))
|
||||
formData.append("data", JSON.stringify(projectData));
|
||||
|
||||
try {
|
||||
await useBaseFetch('project', {
|
||||
method: 'POST',
|
||||
await useBaseFetch("project", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
headers: {
|
||||
'Content-Disposition': formData,
|
||||
"Content-Disposition": formData,
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
this.$refs.modal.hide()
|
||||
this.$refs.modal.hide();
|
||||
await this.$router.push({
|
||||
name: 'type-id',
|
||||
name: "type-id",
|
||||
params: {
|
||||
type: 'project',
|
||||
type: "project",
|
||||
id: this.slug,
|
||||
},
|
||||
})
|
||||
});
|
||||
} catch (err) {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
group: "main",
|
||||
title: "An error occurred",
|
||||
text: err.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
stopLoading()
|
||||
stopLoading();
|
||||
},
|
||||
show() {
|
||||
this.projectType = this.tags.projectTypes[0].display
|
||||
this.name = ''
|
||||
this.slug = ''
|
||||
this.description = ''
|
||||
this.manualSlug = false
|
||||
this.$refs.modal.show()
|
||||
this.projectType = this.tags.projectTypes[0].display;
|
||||
this.name = "";
|
||||
this.slug = "";
|
||||
this.description = "";
|
||||
this.manualSlug = false;
|
||||
this.$refs.modal.show();
|
||||
},
|
||||
updatedName() {
|
||||
if (!this.manualSlug) {
|
||||
this.slug = this.name
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replaceAll(' ', '-')
|
||||
.replaceAll(/[^a-zA-Z0-9!@$()`.+,_"-]/g, '')
|
||||
.replaceAll(/--+/gm, '-')
|
||||
.replaceAll(" ", "-")
|
||||
.replaceAll(/[^a-zA-Z0-9!@$()`.+,_"-]/g, "")
|
||||
.replaceAll(/--+/gm, "-");
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -253,7 +253,7 @@
|
||||
>
|
||||
<div
|
||||
v-for="(option, index) in selectedOptions[steps[currentStepIndex].id].filter(
|
||||
(x) => x.fillers && x.fillers.length > 0
|
||||
(x) => x.fillers && x.fillers.length > 0,
|
||||
)"
|
||||
:key="index"
|
||||
>
|
||||
@@ -339,9 +339,9 @@ import {
|
||||
XIcon as CrossIcon,
|
||||
EyeOffIcon,
|
||||
ExitIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { MarkdownEditor, OverflowMenu } from '@modrinth/ui'
|
||||
import Categories from '~/components/ui/search/Categories.vue'
|
||||
} from "@modrinth/assets";
|
||||
import { MarkdownEditor, OverflowMenu } from "@modrinth/ui";
|
||||
import Categories from "~/components/ui/search/Categories.vue";
|
||||
|
||||
const props = defineProps({
|
||||
project: {
|
||||
@@ -357,72 +357,72 @@ const props = defineProps({
|
||||
required: true,
|
||||
default: () => {},
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const steps = computed(() =>
|
||||
[
|
||||
{
|
||||
id: 'title',
|
||||
question: 'Is this title free of useless information?',
|
||||
id: "title",
|
||||
question: "Is this title free of useless information?",
|
||||
shown: true,
|
||||
rules: [
|
||||
'No unnecessary data (mod loaders, game versions, etc)',
|
||||
'No emojis / useless text decorators',
|
||||
"No unnecessary data (mod loaders, game versions, etc)",
|
||||
"No emojis / useless text decorators",
|
||||
],
|
||||
examples: [
|
||||
'✅ NoobMod [1.8+] • Kill all noobs in your world!',
|
||||
'[FABRIC] My Optimization Pack',
|
||||
'[1.17-1.20.4] LagFixer ⚡️ Best Performance Solution! ⭕ Well optimized ✅ Folia supported! (BETA)',
|
||||
"✅ NoobMod [1.8+] • Kill all noobs in your world!",
|
||||
"[FABRIC] My Optimization Pack",
|
||||
"[1.17-1.20.4] LagFixer ⚡️ Best Performance Solution! ⭕ Well optimized ✅ Folia supported! (BETA)",
|
||||
],
|
||||
exceptions: [
|
||||
'Loaders and/or game versions allowed if this project is a port of another mod. (ex: Gravestones for 1.20)',
|
||||
'Loaders allowed if they choose to separate their project into Forge and Fabric variants (discouraged)',
|
||||
"Loaders and/or game versions allowed if this project is a port of another mod. (ex: Gravestones for 1.20)",
|
||||
"Loaders allowed if they choose to separate their project into Forge and Fabric variants (discouraged)",
|
||||
],
|
||||
options: [
|
||||
{
|
||||
name: 'Contains useless info',
|
||||
name: "Contains useless info",
|
||||
resultingMessage: `## Misuse of Title
|
||||
Per section 5.2 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#miscellaneous) we ask that you limit the title to just the name of your project. Additional information, such as themes, tags, supported versions or loaders, etc. should be saved for the Summary or Description. When changing your project title, remember to also ensure that your project slug (URL) matches and accurately represents your project.`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'slug',
|
||||
question: 'Is the slug accurate and appropriate?',
|
||||
id: "slug",
|
||||
question: "Is the slug accurate and appropriate?",
|
||||
shown: true,
|
||||
rules: ['Matches title / not misleading (acronyms are OK)'],
|
||||
rules: ["Matches title / not misleading (acronyms are OK)"],
|
||||
options: [
|
||||
{
|
||||
name: 'Misused',
|
||||
name: "Misused",
|
||||
resultingMessage: `## Misuse of Slug
|
||||
Per section 5.2 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#miscellaneous) your project slug (URL) must accurately represent your project. `,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'summary',
|
||||
id: "summary",
|
||||
question: `Is the project's summary sufficient?`,
|
||||
shown: true,
|
||||
rules: [
|
||||
'The summary should provide a brief overview of your project that informs and entices users.',
|
||||
"The summary should provide a brief overview of your project that informs and entices users.",
|
||||
`Should not be the exact same as the project's title`,
|
||||
'Should not include any markdown formatting.',
|
||||
"Should not include any markdown formatting.",
|
||||
],
|
||||
options: [
|
||||
{
|
||||
name: 'Insufficient',
|
||||
name: "Insufficient",
|
||||
resultingMessage: `## Insufficient Summary
|
||||
Per section 5.3 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#miscellaneous) Your project summary should provide a brief overview of your project that informs and entices users.
|
||||
This is the first thing most people will see about your mod other than the Logo, so it's important it be accurate, reasonably detailed, and exciting.`,
|
||||
},
|
||||
{
|
||||
name: 'Repeat of title',
|
||||
name: "Repeat of title",
|
||||
resultingMessage: `## Insufficient Summary
|
||||
Per section 5.3 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#miscellaneous) your Summary can not be the same as your project's Title. Your project summary should provide a brief overview of your project that informs and entices users.
|
||||
This is the first thing most people will see about your mod other than the Logo, so it's important it be accurate, reasonably detailed, and exciting.`,
|
||||
},
|
||||
{
|
||||
name: 'Formatting',
|
||||
name: "Formatting",
|
||||
resultingMessage: `## Insufficient Summary
|
||||
Per section 5.3 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#miscellaneous) your Summary can not include any extra formatting such as lists, or links. Your project summary should provide a brief overview of your project that informs and entices users.
|
||||
This is the first thing most people will see about your mod other than the Logo, so it's important it be accurate, reasonably detailed, and exciting.`,
|
||||
@@ -430,33 +430,33 @@ This is the first thing most people will see about your mod other than the Logo,
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'description',
|
||||
id: "description",
|
||||
question: `Is the project's description sufficient?`,
|
||||
navigate: `/${props.project.project_type}/${props.project.slug}`,
|
||||
shown: true,
|
||||
rules: [
|
||||
'Should answer what the project specifically does or adds ',
|
||||
'Should answer why someone should want to download the project ',
|
||||
'Should indicate any other critical information the user must know before downloading',
|
||||
'Should be accessible (no fancy characters / non-standard text, no image-only descriptions, must have English component, etc)',
|
||||
"Should answer what the project specifically does or adds ",
|
||||
"Should answer why someone should want to download the project ",
|
||||
"Should indicate any other critical information the user must know before downloading",
|
||||
"Should be accessible (no fancy characters / non-standard text, no image-only descriptions, must have English component, etc)",
|
||||
],
|
||||
options: [
|
||||
{
|
||||
name: 'Insufficient',
|
||||
name: "Insufficient",
|
||||
resultingMessage: `## Insufficient Description
|
||||
Per section 2.1 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#general-expectations) your project's Description should clearly inform the reader of the content, purpose, and appeal of your project.
|
||||
Currently, it looks like there are some missing details.
|
||||
%EXPLAINER%`,
|
||||
fillers: [
|
||||
{
|
||||
id: 'EXPLAINER',
|
||||
question: 'Please elaborate on how the author can improve their description.',
|
||||
id: "EXPLAINER",
|
||||
question: "Please elaborate on how the author can improve their description.",
|
||||
large: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Insufficient (default packs)',
|
||||
name: "Insufficient (default packs)",
|
||||
resultingMessage: `## Insufficient Description
|
||||
Per section 2.1 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#general-expectations) your project's Description should clearly inform the reader of the content, purpose, and appeal of your project.
|
||||
Currently, it looks like there are some missing details.
|
||||
@@ -465,7 +465,7 @@ See descriptions like [Simply Optimized](https://modrinth.com/modpack/sop) or [A
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'Insufficient (default projects)',
|
||||
name: "Insufficient (default projects)",
|
||||
resultingMessage: `## Insufficient Description
|
||||
Per section 2.1 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#general-expectations) your project's Description should clearly inform the reader of the content, purpose, and appeal of your project.
|
||||
Currently, it looks like there are some missing details.
|
||||
@@ -474,35 +474,35 @@ See descriptions like [Sodium](https://modrinth.com/mod/sodium) or [LambDynamicL
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'Non-english',
|
||||
name: "Non-english",
|
||||
resultingMessage: `## No English Description
|
||||
Per section 2.2 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#accessibility) a project's Summary and Description must be in English, unless meant exclusively for non-English use, such as translations. You may include your non-English Description if you would like but we ask that you also add an English translation of the Description to your Description page, if you would like to use an online translator to do this, we recommend [DeepL](https://www.deepl.com/translator).`,
|
||||
},
|
||||
{
|
||||
name: 'Unfinished',
|
||||
name: "Unfinished",
|
||||
resultingMessage: `## Unfinished Description
|
||||
It looks like your project Description is still a WIP seeing as %REASON%. Please remember to submit only when ready, as it is important your project meets the requirements of Section 2.1 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#general-expectations), if you have any questions on this feel free to reach out!`,
|
||||
},
|
||||
{
|
||||
name: 'Headers as body text',
|
||||
name: "Headers as body text",
|
||||
resultingMessage: `## Description Accessibility
|
||||
In accordance with section 2.2 of [Modrinth's Content Rules](https://modrinth.com/legal/rules) we request that \`# header\`s not be used as body text. Headers are interpreted differently by screen-readers and thus should generally only be used for things like separating sections of your Description. If you would like to emphasize a particular sentence or paragraph, instead consider using \`**bold**\` text using the **B** button above the text editor.`,
|
||||
},
|
||||
{
|
||||
name: 'Image-only',
|
||||
name: "Image-only",
|
||||
resultingMessage: `## Image Descriptions
|
||||
In accordance with section 2.2 of [Modrinth's Content Rules](https://modrinth.com/legal/rules) we ask that you provide a text alternative to your current Description. It is important that your Description contains enough detail about your project that a user can have a full understanding of it from text alone. A text-based transcription allows for those using screen readers, and users with slow internet connections unable to load images to be able to access the contents of your Description. This also acts as a backup in case the image in your Description ever goes offline for some reason.
|
||||
We appreciate how much effort you put into your Description, but accessibility is important to us at Modrinth, if you would like you could put the transcription of your Description entirely in a \`details\` tag, so as to not spoil the visuals of your Description.`,
|
||||
},
|
||||
{
|
||||
name: 'Non-standard text',
|
||||
name: "Non-standard text",
|
||||
resultingMessage: `## Description Accessibility
|
||||
Per section 2 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#clear-and-honest-function) your description must be plainly readable and accessible. Using non-standard text characters like Zalgo or "fancy text" in place of text anywhere in your project, including the Description, Summary, or Title can make your project pages inaccessible. This is important for users who rely on Screen Readers and for search engines in order to provide relevant results to users. Please remove any instances of this type of text.`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'links',
|
||||
id: "links",
|
||||
question: `Are the project's links accessible and not misleading?`,
|
||||
shown:
|
||||
props.project.issues_url ||
|
||||
@@ -516,49 +516,49 @@ Per section 2 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#cle
|
||||
],
|
||||
options: [
|
||||
{
|
||||
name: 'Links are misused',
|
||||
name: "Links are misused",
|
||||
resultingMessage: `## Misuse of External Resources
|
||||
Per section 5.4 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#miscellaneous) all links must lead to correctly labeled publicly available resources that are directly related to your project.`,
|
||||
},
|
||||
{
|
||||
name: 'Not accessible (source)',
|
||||
name: "Not accessible (source)",
|
||||
resultingMessage: `## Unreachable Links
|
||||
Per section 5.4 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#miscellaneous) all links must lead to correctly labeled publicly available resources that are directly related to your project.
|
||||
Currently, your Source link directs to a Page Not Found error, likely because your repository is private, make sure to make your repository public before resubmitting your project!`,
|
||||
},
|
||||
{
|
||||
name: 'Not accessible (other)',
|
||||
name: "Not accessible (other)",
|
||||
resultingMessage: `## Unreachable Links
|
||||
Per section 5.4 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#miscellaneous) all links must lead to correctly labeled publicly available resources that are directly related to your project.
|
||||
Currently, your %LINK% link is inaccessible!`,
|
||||
fillers: [
|
||||
{
|
||||
id: 'LINK',
|
||||
question: 'Please specify the link type that is inaccessible.',
|
||||
id: "LINK",
|
||||
question: "Please specify the link type that is inaccessible.",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'categories',
|
||||
id: "categories",
|
||||
question: `Are the project's tags/categories accurate?`,
|
||||
shown: props.project.categories.length > 0 || props.project.additional_categories.length > 0,
|
||||
options: [
|
||||
{
|
||||
name: 'Inaccurate',
|
||||
name: "Inaccurate",
|
||||
resultingMessage: `## Misuse of Tags
|
||||
Per section 5.1 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#miscellaneous), it is important that the metadata of your projects is accurate. Including that selected tags honestly represent your project.`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'side-types',
|
||||
id: "side-types",
|
||||
question: `Is the project's environment information accurate?`,
|
||||
shown: ['mod', 'modpack'].includes(props.project.project_type),
|
||||
shown: ["mod", "modpack"].includes(props.project.project_type),
|
||||
options: [
|
||||
{
|
||||
name: 'Inaccurate (modpack)',
|
||||
name: "Inaccurate (modpack)",
|
||||
resultingMessage: `## Incorrect Environment Information
|
||||
Per section 5.1 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#miscellaneous), it is important that the metadata of your projects is accurate, including whether the project runs on the client or server side.
|
||||
For a brief rundown of how this works:
|
||||
@@ -567,7 +567,7 @@ Most other modpacks that change how the game is played are going to be required
|
||||
When in doubt, test for yourself or check the requirements of the mods in your pack.`,
|
||||
},
|
||||
{
|
||||
name: 'Inaccurate (mod)',
|
||||
name: "Inaccurate (mod)",
|
||||
resultingMessage: `## Environment Information
|
||||
Per section 5.1 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#miscellaneous), it is important that the metadata of your projects is accurate, including whether the project runs on the client or server side.
|
||||
For a brief rundown of how this works:
|
||||
@@ -578,48 +578,48 @@ A mod that adds features, entities, or new blocks and items, generally will be r
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'gallery',
|
||||
id: "gallery",
|
||||
navigate: `/${props.project.project_type}/${props.project.slug}/gallery`,
|
||||
question: `Are the project's gallery images relevant?`,
|
||||
shown: props.project.gallery.length > 0,
|
||||
options: [
|
||||
{
|
||||
name: 'Not relevant',
|
||||
name: "Not relevant",
|
||||
resultingMessage: `## Unrelated Gallery Images
|
||||
Per section 5.5 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#miscellaneous) any images in your project's Gallery must be relevant to the project and also include a Title.`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'versions',
|
||||
id: "versions",
|
||||
navigate: `/${props.project.project_type}/${props.project.slug}/versions`,
|
||||
question: `Are these project's files correct?`,
|
||||
shown: !['modpack'].includes(props.project.project_type),
|
||||
shown: !["modpack"].includes(props.project.project_type),
|
||||
rules: [
|
||||
'A multi-loader project should not use additional files for more loaders',
|
||||
'Modpacks must be uploaded as MRPACK files. Be sure to check the project type is modpack (if not their file is malformed)',
|
||||
"A multi-loader project should not use additional files for more loaders",
|
||||
"Modpacks must be uploaded as MRPACK files. Be sure to check the project type is modpack (if not their file is malformed)",
|
||||
],
|
||||
options: [
|
||||
{
|
||||
name: 'Incorrect additional files',
|
||||
name: "Incorrect additional files",
|
||||
resultingMessage: `## Incorrect Use of Additional Files
|
||||
It looks like you've uploaded multiple \`mod.jar\` files to one Version as Additional Files. Per section 5.7 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#miscellaneous) each Version of your project must include only one \`mod.jar\` that corresponds to its respective Minecraft and loader versions. This allows users to easily find and download the file they need for the version they're on with ease. The Additional Files feature can be used for things like a \`Sources.jar\`.
|
||||
Please upload each version of your mod separately, thank you.`,
|
||||
},
|
||||
{
|
||||
name: 'Invalid file type (modpacks)',
|
||||
name: "Invalid file type (modpacks)",
|
||||
resultingMessage: `## Modpacks on Modrinth
|
||||
It looks like you've uploaded your Modpack as a \`.zip\`, unfortunately, this is invalid and is why your project type is "Mod". I recommend taking a look at our support page about [Modrinth Modpacks](https://support.modrinth.com/en/articles/8802250-modpacks-on-modrinth), and once you're ready feel free to resubmit your project as a \`.mrpack\`. Don't forget to delete the old files from your Versions!`,
|
||||
},
|
||||
{
|
||||
name: 'Invalid file type (resourcepacks)',
|
||||
name: "Invalid file type (resourcepacks)",
|
||||
resultingMessage: `## Resource Packs on Modrinth
|
||||
It looks like you've selected loaders for your Resource Pack that are causing it to be marked as a different project type. Resource Packs must only be uploaded with the "Resource Pack" loader selected. Please re-upload all versions of your resource pack and make sure to only select "Resource Pack" as the loader.`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'copyright',
|
||||
id: "copyright",
|
||||
question: `Does the author have proper permissions to post this project?`,
|
||||
shown: true,
|
||||
rules: [
|
||||
@@ -628,20 +628,20 @@ It looks like you've selected loaders for your Resource Pack that are causing it
|
||||
],
|
||||
options: [
|
||||
{
|
||||
name: 'Re-upload',
|
||||
name: "Re-upload",
|
||||
resultingMessage: `## Reuploads are forbidden
|
||||
This project appears to contain content from %ORIGINAL_PROJECT% by %ORIGINAL_AUTHOR%.
|
||||
Per section 4 of [Modrinth's Content Rules](https://modrinth.com/legal/rules) this is strictly forbidden.
|
||||
If you believe this is an error, or you can verify you are the creator and rightful owner of this content please let us know. Otherwise, we ask that you **do not resubmit this project**.`,
|
||||
fillers: [
|
||||
{
|
||||
id: 'ORIGINAL_PROJECT',
|
||||
question: 'What is the title of the original project?',
|
||||
id: "ORIGINAL_PROJECT",
|
||||
question: "What is the title of the original project?",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'ORIGINAL_AUTHOR',
|
||||
question: 'What is the author of the original project?',
|
||||
id: "ORIGINAL_AUTHOR",
|
||||
question: "What is the author of the original project?",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
@@ -649,25 +649,25 @@ If you believe this is an error, or you can verify you are the creator and right
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'rule-following',
|
||||
id: "rule-following",
|
||||
question: `Does this project follow our content rules?`,
|
||||
navigate: `/${props.project.project_type}/${props.project.slug}`,
|
||||
shown: true,
|
||||
rules: [
|
||||
'Should not be a cheat/hack (without a server-side opt-out)',
|
||||
'Should not contain sexually explicit / inappropriate content',
|
||||
'Should not be excessively profane',
|
||||
'Should not promote any illegal activity (including illicit drugs + substances)',
|
||||
'Anything else infringing of our content rules (see 1.1-12, 3.1-3)',
|
||||
"Should not be a cheat/hack (without a server-side opt-out)",
|
||||
"Should not contain sexually explicit / inappropriate content",
|
||||
"Should not be excessively profane",
|
||||
"Should not promote any illegal activity (including illicit drugs + substances)",
|
||||
"Anything else infringing of our content rules (see 1.1-12, 3.1-3)",
|
||||
],
|
||||
options: [
|
||||
{
|
||||
name: 'No',
|
||||
name: "No",
|
||||
resultingMessage: `%MESSAGE%`,
|
||||
fillers: [
|
||||
{
|
||||
id: 'MESSAGE',
|
||||
question: 'Please explain to the user how it infringes on our content rules.',
|
||||
id: "MESSAGE",
|
||||
question: "Please explain to the user how it infringes on our content rules.",
|
||||
large: true,
|
||||
},
|
||||
],
|
||||
@@ -675,88 +675,88 @@ If you believe this is an error, or you can verify you are the creator and right
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'modpack-permissions',
|
||||
question: 'Modpack permissions',
|
||||
shown: ['modpack'].includes(props.project.project_type),
|
||||
id: "modpack-permissions",
|
||||
question: "Modpack permissions",
|
||||
shown: ["modpack"].includes(props.project.project_type),
|
||||
options: [],
|
||||
},
|
||||
{
|
||||
id: 'private-server',
|
||||
id: "private-server",
|
||||
question: `Is this pack for a private server?`,
|
||||
shown: ['modpack'].includes(props.project.project_type),
|
||||
shown: ["modpack"].includes(props.project.project_type),
|
||||
rules: [
|
||||
'Select this if you are withholding this pack since it is for a private server (for circumstances you would normally reject for).',
|
||||
"Select this if you are withholding this pack since it is for a private server (for circumstances you would normally reject for).",
|
||||
],
|
||||
options: [
|
||||
{
|
||||
name: 'Private server (withhold)',
|
||||
name: "Private server (withhold)",
|
||||
resultingMessage: `## Private Server
|
||||
Under normal circumstances, your project would be rejected due to the issues listed above. However, since your project is intended for a specific server and not for general use, these requirements will be waived and your project will be withheld. This means it will be unlisted and accessible only through a direct link, without appearing in public search results. If you're fine with this, no further action is needed. Otherwise, feel free to resubmit once all issues have been addressed. `,
|
||||
},
|
||||
],
|
||||
},
|
||||
].filter((x) => x.shown)
|
||||
)
|
||||
].filter((x) => x.shown),
|
||||
);
|
||||
|
||||
const currentStepIndex = ref(0)
|
||||
const selectedOptions = ref({})
|
||||
const currentStepIndex = ref(0);
|
||||
const selectedOptions = ref({});
|
||||
|
||||
function toggleOption(stepId, option) {
|
||||
if (!selectedOptions.value[stepId]) {
|
||||
selectedOptions.value[stepId] = []
|
||||
selectedOptions.value[stepId] = [];
|
||||
}
|
||||
|
||||
const index = selectedOptions.value[stepId].findIndex((x) => x.name === option.name)
|
||||
const index = selectedOptions.value[stepId].findIndex((x) => x.name === option.name);
|
||||
if (index === -1) {
|
||||
selectedOptions.value[stepId].push(option)
|
||||
selectedOptions.value[stepId].push(option);
|
||||
} else {
|
||||
selectedOptions.value[stepId].splice(index, 1)
|
||||
selectedOptions.value[stepId].splice(index, 1);
|
||||
}
|
||||
|
||||
const instance = getCurrentInstance()
|
||||
instance?.proxy?.$forceUpdate()
|
||||
const instance = getCurrentInstance();
|
||||
instance?.proxy?.$forceUpdate();
|
||||
}
|
||||
|
||||
function previousPage() {
|
||||
currentStepIndex.value -= 1
|
||||
generatedMessage.value = false
|
||||
currentStepIndex.value -= 1;
|
||||
generatedMessage.value = false;
|
||||
|
||||
if (steps.value[currentStepIndex.value].navigate) {
|
||||
navigateTo(steps.value[currentStepIndex.value].navigate)
|
||||
navigateTo(steps.value[currentStepIndex.value].navigate);
|
||||
}
|
||||
}
|
||||
|
||||
async function nextPage() {
|
||||
currentStepIndex.value += 1
|
||||
currentStepIndex.value += 1;
|
||||
|
||||
if (steps.value[currentStepIndex.value].navigate) {
|
||||
navigateTo(steps.value[currentStepIndex.value].navigate)
|
||||
navigateTo(steps.value[currentStepIndex.value].navigate);
|
||||
}
|
||||
|
||||
if (steps.value[currentStepIndex.value].id === 'modpack-permissions') {
|
||||
await initializeModPackData()
|
||||
if (steps.value[currentStepIndex.value].id === "modpack-permissions") {
|
||||
await initializeModPackData();
|
||||
}
|
||||
}
|
||||
|
||||
async function initializeModPackData() {
|
||||
startLoading()
|
||||
startLoading();
|
||||
try {
|
||||
const raw = await useBaseFetch(`moderation/project/${props.project.id}`, { internal: true })
|
||||
const projects = []
|
||||
const raw = await useBaseFetch(`moderation/project/${props.project.id}`, { internal: true });
|
||||
const projects = [];
|
||||
|
||||
for (const [hash, fileName] of Object.entries(raw.unknown_files)) {
|
||||
projects.push({
|
||||
type: 'unknown',
|
||||
type: "unknown",
|
||||
hash,
|
||||
file_name: fileName,
|
||||
status: null,
|
||||
approved: null,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
for (const [hash, file] of Object.entries(raw.flame_files)) {
|
||||
projects.push({
|
||||
type: 'flame',
|
||||
type: "flame",
|
||||
hash,
|
||||
file_name: file.file_name,
|
||||
status: null,
|
||||
@@ -764,130 +764,130 @@ async function initializeModPackData() {
|
||||
id: file.id,
|
||||
url: file.url,
|
||||
approved: null,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
for (const [hash, file] of Object.entries(raw.identified)) {
|
||||
if (file.status !== 'yes' && file.status !== 'with-attribution-and-source') {
|
||||
if (file.status !== "yes" && file.status !== "with-attribution-and-source") {
|
||||
projects.push({
|
||||
type: 'identified',
|
||||
type: "identified",
|
||||
hash,
|
||||
file_name: file.file_name,
|
||||
status: file.status,
|
||||
approved: null,
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
modPackData.value = projects
|
||||
modPackData.value = projects;
|
||||
} catch (err) {
|
||||
const app = useNuxtApp()
|
||||
const app = useNuxtApp();
|
||||
app.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
group: "main",
|
||||
title: "An error occurred",
|
||||
text: err.data ? err.data.description : err,
|
||||
type: 'error',
|
||||
})
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
stopLoading()
|
||||
stopLoading();
|
||||
}
|
||||
|
||||
const modPackData = ref(null)
|
||||
const modPackIndex = ref(0)
|
||||
const modPackData = ref(null);
|
||||
const modPackIndex = ref(0);
|
||||
|
||||
const fileApprovalTypes = ref([
|
||||
{
|
||||
id: 'yes',
|
||||
name: 'Yes',
|
||||
id: "yes",
|
||||
name: "Yes",
|
||||
},
|
||||
{
|
||||
id: 'with-attribution-and-source',
|
||||
name: 'With attribution and source',
|
||||
id: "with-attribution-and-source",
|
||||
name: "With attribution and source",
|
||||
},
|
||||
{
|
||||
id: 'with-attribution',
|
||||
name: 'With attribution',
|
||||
id: "with-attribution",
|
||||
name: "With attribution",
|
||||
},
|
||||
{
|
||||
id: 'no',
|
||||
name: 'No',
|
||||
id: "no",
|
||||
name: "No",
|
||||
},
|
||||
{
|
||||
id: 'permanent-no',
|
||||
name: 'Permanent no',
|
||||
id: "permanent-no",
|
||||
name: "Permanent no",
|
||||
},
|
||||
{
|
||||
id: 'unidentified',
|
||||
name: 'Unidentified',
|
||||
id: "unidentified",
|
||||
name: "Unidentified",
|
||||
},
|
||||
])
|
||||
]);
|
||||
const filePermissionTypes = ref([
|
||||
{
|
||||
id: true,
|
||||
name: 'Yes',
|
||||
name: "Yes",
|
||||
},
|
||||
{
|
||||
id: false,
|
||||
name: 'No',
|
||||
name: "No",
|
||||
},
|
||||
])
|
||||
]);
|
||||
|
||||
const message = ref('')
|
||||
const generatedMessage = ref(false)
|
||||
const loadingMessage = ref(false)
|
||||
const message = ref("");
|
||||
const generatedMessage = ref(false);
|
||||
const loadingMessage = ref(false);
|
||||
async function generateMessage() {
|
||||
message.value = ''
|
||||
loadingMessage.value = true
|
||||
message.value = "";
|
||||
loadingMessage.value = true;
|
||||
function printMods(mods, msg) {
|
||||
if (mods.length === 0) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
message.value += msg
|
||||
message.value += '\n\n'
|
||||
message.value += msg;
|
||||
message.value += "\n\n";
|
||||
|
||||
for (const mod of mods) {
|
||||
message.value += `- ${mod}\n`
|
||||
message.value += `- ${mod}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
if (modPackData.value && modPackData.value.length > 0) {
|
||||
const updateProjects = {}
|
||||
const updateProjects = {};
|
||||
|
||||
const attributeMods = []
|
||||
const noMods = []
|
||||
const permanentNoMods = []
|
||||
const unidentifiedMods = []
|
||||
const attributeMods = [];
|
||||
const noMods = [];
|
||||
const permanentNoMods = [];
|
||||
const unidentifiedMods = [];
|
||||
|
||||
for (const project of modPackData.value) {
|
||||
if (project.type === 'unknown') {
|
||||
if (project.type === "unknown") {
|
||||
updateProjects[project.hash] = {
|
||||
type: 'unknown',
|
||||
type: "unknown",
|
||||
status: project.status,
|
||||
proof: project.proof,
|
||||
title: project.title,
|
||||
link: project.url,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (project.type === 'flame') {
|
||||
if (project.type === "flame") {
|
||||
updateProjects[project.hash] = {
|
||||
type: 'flame',
|
||||
type: "flame",
|
||||
status: project.status,
|
||||
id: project.id,
|
||||
link: project.url,
|
||||
title: project.title,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (project.status === 'with-attribution' && !project.approved) {
|
||||
attributeMods.push(project.file_name)
|
||||
} else if (project.status === 'unidentified' && !project.approved) {
|
||||
unidentifiedMods.push(project.file_name)
|
||||
} else if (project.status === 'no' && !project.approved) {
|
||||
noMods.push(project.file_name)
|
||||
} else if (project.status === 'permanent-no') {
|
||||
permanentNoMods.push(project.file_name)
|
||||
if (project.status === "with-attribution" && !project.approved) {
|
||||
attributeMods.push(project.file_name);
|
||||
} else if (project.status === "unidentified" && !project.approved) {
|
||||
unidentifiedMods.push(project.file_name);
|
||||
} else if (project.status === "no" && !project.approved) {
|
||||
noMods.push(project.file_name);
|
||||
} else if (project.status === "permanent-no") {
|
||||
permanentNoMods.push(project.file_name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -895,17 +895,17 @@ async function generateMessage() {
|
||||
try {
|
||||
await useBaseFetch(`moderation/project`, {
|
||||
internal: true,
|
||||
method: 'POST',
|
||||
method: "POST",
|
||||
body: updateProjects,
|
||||
})
|
||||
});
|
||||
} catch (err) {
|
||||
const app = useNuxtApp()
|
||||
const app = useNuxtApp();
|
||||
app.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
group: "main",
|
||||
title: "An error occurred",
|
||||
text: err.data ? err.data.description : err,
|
||||
type: 'error',
|
||||
})
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -915,106 +915,109 @@ async function generateMessage() {
|
||||
permanentNoMods.length > 0 ||
|
||||
unidentifiedMods.length > 0
|
||||
) {
|
||||
message.value += '## Copyrighted Content \n'
|
||||
message.value += "## Copyrighted Content \n";
|
||||
|
||||
printMods(
|
||||
attributeMods,
|
||||
"The following content has attribution requirements, meaning that you must link back to the page where you originally found this content in your modpack description or version changelog (e.g. linking a mod's CurseForge page if you got it from CurseForge):"
|
||||
)
|
||||
"The following content has attribution requirements, meaning that you must link back to the page where you originally found this content in your modpack description or version changelog (e.g. linking a mod's CurseForge page if you got it from CurseForge):",
|
||||
);
|
||||
printMods(
|
||||
noMods,
|
||||
'The following content is not allowed in Modrinth modpacks due to licensing restrictions. Please contact the author(s) directly for permission or remove the content from your modpack:'
|
||||
)
|
||||
"The following content is not allowed in Modrinth modpacks due to licensing restrictions. Please contact the author(s) directly for permission or remove the content from your modpack:",
|
||||
);
|
||||
printMods(
|
||||
permanentNoMods,
|
||||
"The following content is not allowed in Modrinth modpacks, regardless of permission obtained. This may be because it breaks Modrinth's content rules or because the authors, upon being contacted for permission, have declined. Please remove the content from your modpack:"
|
||||
)
|
||||
"The following content is not allowed in Modrinth modpacks, regardless of permission obtained. This may be because it breaks Modrinth's content rules or because the authors, upon being contacted for permission, have declined. Please remove the content from your modpack:",
|
||||
);
|
||||
printMods(
|
||||
unidentifiedMods,
|
||||
'The following content could not be identified. Please provide proof of its origin along with proof that you have permission to include it:'
|
||||
)
|
||||
"The following content could not be identified. Please provide proof of its origin along with proof that you have permission to include it:",
|
||||
);
|
||||
|
||||
message.value += '\n\n'
|
||||
message.value += "\n\n";
|
||||
}
|
||||
}
|
||||
|
||||
for (const options of Object.values(selectedOptions.value)) {
|
||||
for (const option of options) {
|
||||
let addonMessage = option.resultingMessage
|
||||
let addonMessage = option.resultingMessage;
|
||||
|
||||
if (option.fillers && option.fillers.length > 0) {
|
||||
for (const filler of option.fillers) {
|
||||
addonMessage = addonMessage.replace(new RegExp(`%${filler.id}%`, 'g'), filler.value ?? '')
|
||||
addonMessage = addonMessage.replace(
|
||||
new RegExp(`%${filler.id}%`, "g"),
|
||||
filler.value ?? "",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
message.value += addonMessage
|
||||
message.value += '\n\n'
|
||||
message.value += addonMessage;
|
||||
message.value += "\n\n";
|
||||
}
|
||||
}
|
||||
generatedMessage.value = true
|
||||
loadingMessage.value = false
|
||||
currentStepIndex.value += 1
|
||||
await navigateTo(`/${props.project.project_type}/${props.project.slug}/moderation`)
|
||||
generatedMessage.value = true;
|
||||
loadingMessage.value = false;
|
||||
currentStepIndex.value += 1;
|
||||
await navigateTo(`/${props.project.project_type}/${props.project.slug}/moderation`);
|
||||
}
|
||||
|
||||
const done = ref(false)
|
||||
const done = ref(false);
|
||||
async function sendMessage(status) {
|
||||
startLoading()
|
||||
startLoading();
|
||||
try {
|
||||
await useBaseFetch(`project/${props.project.id}`, {
|
||||
method: 'PATCH',
|
||||
method: "PATCH",
|
||||
body: {
|
||||
status,
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
if (message.value) {
|
||||
await useBaseFetch(`thread/${props.project.thread_id}`, {
|
||||
method: 'POST',
|
||||
method: "POST",
|
||||
body: {
|
||||
body: {
|
||||
type: 'text',
|
||||
type: "text",
|
||||
body: message.value,
|
||||
},
|
||||
},
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
await props.resetProject()
|
||||
done.value = true
|
||||
await props.resetProject();
|
||||
done.value = true;
|
||||
} catch (err) {
|
||||
const app = useNuxtApp()
|
||||
const app = useNuxtApp();
|
||||
app.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
group: "main",
|
||||
title: "An error occurred",
|
||||
text: err.data ? err.data.description : err,
|
||||
type: 'error',
|
||||
})
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
stopLoading()
|
||||
stopLoading();
|
||||
}
|
||||
|
||||
const router = useNativeRouter()
|
||||
const router = useNativeRouter();
|
||||
|
||||
async function goToNextProject() {
|
||||
const project = props.futureProjects[0]
|
||||
const project = props.futureProjects[0];
|
||||
|
||||
if (!project) {
|
||||
await navigateTo('/moderation/review')
|
||||
await navigateTo("/moderation/review");
|
||||
}
|
||||
|
||||
await router.push({
|
||||
name: 'type-id',
|
||||
name: "type-id",
|
||||
params: {
|
||||
type: 'project',
|
||||
type: "project",
|
||||
id: project,
|
||||
},
|
||||
state: {
|
||||
showChecklist: true,
|
||||
projects: props.futureProjects.slice(1),
|
||||
},
|
||||
})
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1042,7 +1045,9 @@ async function goToNextProject() {
|
||||
.option-selected {
|
||||
color: var(--color-contrast);
|
||||
background-color: var(--color-brand-highlight);
|
||||
box-shadow: inset 0 0 0 transparent, 0 0 0 2px var(--color-brand);
|
||||
box-shadow:
|
||||
inset 0 0 0 transparent,
|
||||
0 0 0 2px var(--color-brand);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const route = useNativeRoute()
|
||||
const route = useNativeRoute();
|
||||
|
||||
const props = defineProps({
|
||||
links: {
|
||||
@@ -35,59 +35,59 @@ const props = defineProps({
|
||||
default: null,
|
||||
type: String,
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const sliderPositionX = ref(0)
|
||||
const sliderPositionY = ref(18)
|
||||
const selectedElementWidth = ref(0)
|
||||
const activeIndex = ref(-1)
|
||||
const oldIndex = ref(-1)
|
||||
const sliderPositionX = ref(0);
|
||||
const sliderPositionY = ref(18);
|
||||
const selectedElementWidth = ref(0);
|
||||
const activeIndex = ref(-1);
|
||||
const oldIndex = ref(-1);
|
||||
|
||||
const filteredLinks = computed(() =>
|
||||
props.links.filter((x) => (x.shown === undefined ? true : x.shown))
|
||||
)
|
||||
const positionToMoveX = computed(() => `${sliderPositionX.value}px`)
|
||||
const positionToMoveY = computed(() => `${sliderPositionY.value}px`)
|
||||
const sliderWidth = computed(() => `${selectedElementWidth.value}px`)
|
||||
props.links.filter((x) => (x.shown === undefined ? true : x.shown)),
|
||||
);
|
||||
const positionToMoveX = computed(() => `${sliderPositionX.value}px`);
|
||||
const positionToMoveY = computed(() => `${sliderPositionY.value}px`);
|
||||
const sliderWidth = computed(() => `${selectedElementWidth.value}px`);
|
||||
|
||||
function pickLink() {
|
||||
console.log('link is picking')
|
||||
console.log("link is picking");
|
||||
|
||||
activeIndex.value = props.query
|
||||
? filteredLinks.value.findIndex(
|
||||
(x) => (x.href === '' ? undefined : x.href) === route.path[props.query]
|
||||
(x) => (x.href === "" ? undefined : x.href) === route.path[props.query],
|
||||
)
|
||||
: filteredLinks.value.findIndex((x) => x.href === decodeURIComponent(route.path))
|
||||
: filteredLinks.value.findIndex((x) => x.href === decodeURIComponent(route.path));
|
||||
|
||||
if (activeIndex.value !== -1) {
|
||||
startAnimation()
|
||||
startAnimation();
|
||||
} else {
|
||||
oldIndex.value = -1
|
||||
sliderPositionX.value = 0
|
||||
selectedElementWidth.value = 0
|
||||
oldIndex.value = -1;
|
||||
sliderPositionX.value = 0;
|
||||
selectedElementWidth.value = 0;
|
||||
}
|
||||
}
|
||||
|
||||
const linkElements = ref()
|
||||
const linkElements = ref();
|
||||
|
||||
function startAnimation() {
|
||||
const el = linkElements.value[activeIndex.value].$el
|
||||
const el = linkElements.value[activeIndex.value].$el;
|
||||
|
||||
sliderPositionX.value = el.offsetLeft
|
||||
sliderPositionY.value = el.offsetTop + el.offsetHeight
|
||||
selectedElementWidth.value = el.offsetWidth
|
||||
sliderPositionX.value = el.offsetLeft;
|
||||
sliderPositionY.value = el.offsetTop + el.offsetHeight;
|
||||
selectedElementWidth.value = el.offsetWidth;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('resize', pickLink)
|
||||
pickLink()
|
||||
})
|
||||
window.addEventListener("resize", pickLink);
|
||||
pickLink();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', pickLink)
|
||||
})
|
||||
window.removeEventListener("resize", pickLink);
|
||||
});
|
||||
|
||||
watch(route, () => pickLink())
|
||||
watch(route, () => pickLink());
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {}
|
||||
export default {};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ChevronRightIcon from '~/assets/images/utils/chevron-right.svg?component'
|
||||
import ChevronRightIcon from "~/assets/images/utils/chevron-right.svg?component";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -55,7 +55,7 @@ export default {
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -184,8 +184,8 @@
|
||||
class="iconified-button square-button brand-button button-transparent"
|
||||
@click="
|
||||
() => {
|
||||
acceptTeamInvite(notification.body.team_id)
|
||||
read()
|
||||
acceptTeamInvite(notification.body.team_id);
|
||||
read();
|
||||
}
|
||||
"
|
||||
>
|
||||
@@ -196,8 +196,8 @@
|
||||
class="iconified-button square-button danger-button button-transparent"
|
||||
@click="
|
||||
() => {
|
||||
removeSelfFromTeam(notification.body.team_id)
|
||||
read()
|
||||
removeSelfFromTeam(notification.body.team_id);
|
||||
read();
|
||||
}
|
||||
"
|
||||
>
|
||||
@@ -222,8 +222,8 @@
|
||||
class="iconified-button brand-button"
|
||||
@click="
|
||||
() => {
|
||||
acceptTeamInvite(notification.body.team_id)
|
||||
read()
|
||||
acceptTeamInvite(notification.body.team_id);
|
||||
read();
|
||||
}
|
||||
"
|
||||
>
|
||||
@@ -233,8 +233,8 @@
|
||||
class="iconified-button danger-button"
|
||||
@click="
|
||||
() => {
|
||||
removeSelfFromTeam(notification.body.team_id)
|
||||
read()
|
||||
removeSelfFromTeam(notification.body.team_id);
|
||||
read();
|
||||
}
|
||||
"
|
||||
>
|
||||
@@ -288,29 +288,29 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { renderString } from '@modrinth/utils'
|
||||
import InvitationIcon from '~/assets/images/utils/user-plus.svg?component'
|
||||
import ModerationIcon from '~/assets/images/sidebar/admin.svg?component'
|
||||
import NotificationIcon from '~/assets/images/sidebar/notifications.svg?component'
|
||||
import ReadIcon from '~/assets/images/utils/check-circle.svg?component'
|
||||
import CalendarIcon from '~/assets/images/utils/calendar.svg?component'
|
||||
import VersionIcon from '~/assets/images/utils/version.svg?component'
|
||||
import CheckIcon from '~/assets/images/utils/check.svg?component'
|
||||
import CrossIcon from '~/assets/images/utils/x.svg?component'
|
||||
import ExternalIcon from '~/assets/images/utils/external.svg?component'
|
||||
import ThreadSummary from '~/components/ui/thread/ThreadSummary.vue'
|
||||
import { getProjectLink, getVersionLink } from '~/helpers/projects.js'
|
||||
import { getUserLink } from '~/helpers/users.js'
|
||||
import { acceptTeamInvite, removeSelfFromTeam } from '~/helpers/teams.js'
|
||||
import { markAsRead } from '~/helpers/notifications.js'
|
||||
import DoubleIcon from '~/components/ui/DoubleIcon.vue'
|
||||
import Avatar from '~/components/ui/Avatar.vue'
|
||||
import Badge from '~/components/ui/Badge.vue'
|
||||
import CopyCode from '~/components/ui/CopyCode.vue'
|
||||
import Categories from '~/components/ui/search/Categories.vue'
|
||||
import { renderString } from "@modrinth/utils";
|
||||
import InvitationIcon from "~/assets/images/utils/user-plus.svg?component";
|
||||
import ModerationIcon from "~/assets/images/sidebar/admin.svg?component";
|
||||
import NotificationIcon from "~/assets/images/sidebar/notifications.svg?component";
|
||||
import ReadIcon from "~/assets/images/utils/check-circle.svg?component";
|
||||
import CalendarIcon from "~/assets/images/utils/calendar.svg?component";
|
||||
import VersionIcon from "~/assets/images/utils/version.svg?component";
|
||||
import CheckIcon from "~/assets/images/utils/check.svg?component";
|
||||
import CrossIcon from "~/assets/images/utils/x.svg?component";
|
||||
import ExternalIcon from "~/assets/images/utils/external.svg?component";
|
||||
import ThreadSummary from "~/components/ui/thread/ThreadSummary.vue";
|
||||
import { getProjectLink, getVersionLink } from "~/helpers/projects.js";
|
||||
import { getUserLink } from "~/helpers/users.js";
|
||||
import { acceptTeamInvite, removeSelfFromTeam } from "~/helpers/teams.js";
|
||||
import { markAsRead } from "~/helpers/notifications.js";
|
||||
import DoubleIcon from "~/components/ui/DoubleIcon.vue";
|
||||
import Avatar from "~/components/ui/Avatar.vue";
|
||||
import Badge from "~/components/ui/Badge.vue";
|
||||
import CopyCode from "~/components/ui/CopyCode.vue";
|
||||
import Categories from "~/components/ui/search/Categories.vue";
|
||||
|
||||
const app = useNuxtApp()
|
||||
const emit = defineEmits(['update:notifications'])
|
||||
const app = useNuxtApp();
|
||||
const emit = defineEmits(["update:notifications"]);
|
||||
|
||||
const props = defineProps({
|
||||
notification: {
|
||||
@@ -333,34 +333,34 @@ const props = defineProps({
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const flags = useFeatureFlags()
|
||||
const tags = useTags()
|
||||
const flags = useFeatureFlags();
|
||||
const tags = useTags();
|
||||
|
||||
const type = computed(() =>
|
||||
!props.notification.body || props.notification.body.type === 'legacy_markdown'
|
||||
!props.notification.body || props.notification.body.type === "legacy_markdown"
|
||||
? null
|
||||
: props.notification.body.type
|
||||
)
|
||||
const thread = computed(() => props.notification.extra_data.thread)
|
||||
const report = computed(() => props.notification.extra_data.report)
|
||||
const project = computed(() => props.notification.extra_data.project)
|
||||
const version = computed(() => props.notification.extra_data.version)
|
||||
const user = computed(() => props.notification.extra_data.user)
|
||||
const organization = computed(() => props.notification.extra_data.organization)
|
||||
const invitedBy = computed(() => props.notification.extra_data.invited_by)
|
||||
: props.notification.body.type,
|
||||
);
|
||||
const thread = computed(() => props.notification.extra_data.thread);
|
||||
const report = computed(() => props.notification.extra_data.report);
|
||||
const project = computed(() => props.notification.extra_data.project);
|
||||
const version = computed(() => props.notification.extra_data.version);
|
||||
const user = computed(() => props.notification.extra_data.user);
|
||||
const organization = computed(() => props.notification.extra_data.organization);
|
||||
const invitedBy = computed(() => props.notification.extra_data.invited_by);
|
||||
|
||||
const threadLink = computed(() => {
|
||||
if (report.value) {
|
||||
return `/dashboard/report/${report.value.id}`
|
||||
return `/dashboard/report/${report.value.id}`;
|
||||
} else if (project.value) {
|
||||
return `${getProjectLink(project.value)}/moderation#messages`
|
||||
return `${getProjectLink(project.value)}/moderation#messages`;
|
||||
}
|
||||
return '#'
|
||||
})
|
||||
return "#";
|
||||
});
|
||||
|
||||
const hasBody = computed(() => !type.value || thread.value || type.value === 'project_update')
|
||||
const hasBody = computed(() => !type.value || thread.value || type.value === "project_update");
|
||||
|
||||
async function read() {
|
||||
try {
|
||||
@@ -369,54 +369,54 @@ async function read() {
|
||||
...(props.notification.grouped_notifs
|
||||
? props.notification.grouped_notifs.map((notif) => notif.id)
|
||||
: []),
|
||||
]
|
||||
const updateNotifs = await markAsRead(ids)
|
||||
const newNotifs = updateNotifs(props.notifications)
|
||||
emit('update:notifications', newNotifs)
|
||||
];
|
||||
const updateNotifs = await markAsRead(ids);
|
||||
const newNotifs = updateNotifs(props.notifications);
|
||||
emit("update:notifications", newNotifs);
|
||||
} catch (err) {
|
||||
app.$notify({
|
||||
group: 'main',
|
||||
title: 'Error marking notification as read',
|
||||
group: "main",
|
||||
title: "Error marking notification as read",
|
||||
text: err.data ? err.data.description : err,
|
||||
type: 'error',
|
||||
})
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function performAction(notification, actionIndex) {
|
||||
startLoading()
|
||||
startLoading();
|
||||
try {
|
||||
await read()
|
||||
await read();
|
||||
|
||||
if (actionIndex !== null) {
|
||||
await useBaseFetch(`${notification.actions[actionIndex].action_route[1]}`, {
|
||||
method: notification.actions[actionIndex].action_route[0].toUpperCase(),
|
||||
})
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
app.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
group: "main",
|
||||
title: "An error occurred",
|
||||
text: err.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
stopLoading()
|
||||
stopLoading();
|
||||
}
|
||||
|
||||
function getMessages() {
|
||||
const messages = []
|
||||
const messages = [];
|
||||
if (props.notification.body.message_id) {
|
||||
messages.push(props.notification.body.message_id)
|
||||
messages.push(props.notification.body.message_id);
|
||||
}
|
||||
if (props.notification.grouped_notifs) {
|
||||
for (const notif of props.notification.grouped_notifs) {
|
||||
if (notif.body.message_id) {
|
||||
messages.push(notif.body.message_id)
|
||||
messages.push(notif.body.message_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
return messages
|
||||
return messages;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -424,35 +424,35 @@ function getMessages() {
|
||||
.notification {
|
||||
display: grid;
|
||||
grid-template:
|
||||
'icon title'
|
||||
'actions actions'
|
||||
'date date';
|
||||
"icon title"
|
||||
"actions actions"
|
||||
"date date";
|
||||
grid-template-columns: min-content 1fr;
|
||||
grid-template-rows: min-content min-content min-content;
|
||||
gap: var(--spacing-card-sm);
|
||||
|
||||
&.compact {
|
||||
grid-template:
|
||||
'icon title actions'
|
||||
'date date date';
|
||||
"icon title actions"
|
||||
"date date date";
|
||||
grid-template-columns: min-content 1fr auto;
|
||||
grid-template-rows: auto min-content;
|
||||
}
|
||||
|
||||
&.has-body {
|
||||
grid-template:
|
||||
'icon title'
|
||||
'body body'
|
||||
'actions actions'
|
||||
'date date';
|
||||
"icon title"
|
||||
"body body"
|
||||
"actions actions"
|
||||
"date date";
|
||||
grid-template-columns: min-content 1fr;
|
||||
grid-template-rows: min-content auto auto min-content;
|
||||
|
||||
&.compact {
|
||||
grid-template:
|
||||
'icon title actions'
|
||||
'body body body'
|
||||
'date date date';
|
||||
"icon title actions"
|
||||
"body body body"
|
||||
"date date date";
|
||||
grid-template-columns: min-content 1fr auto;
|
||||
grid-template-rows: min-content auto min-content;
|
||||
}
|
||||
|
||||
@@ -18,10 +18,10 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
const notifications = useNotifications()
|
||||
const notifications = useNotifications();
|
||||
|
||||
function stopTimer(notif) {
|
||||
clearTimeout(notif.timer)
|
||||
clearTimeout(notif.timer);
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -54,51 +54,51 @@
|
||||
</Modal>
|
||||
</template>
|
||||
<script setup>
|
||||
import { XIcon as CrossIcon, CheckIcon } from '@modrinth/assets'
|
||||
import { Modal, Button } from '@modrinth/ui'
|
||||
import { XIcon as CrossIcon, CheckIcon } from "@modrinth/assets";
|
||||
import { Modal, Button } from "@modrinth/ui";
|
||||
|
||||
const router = useNativeRouter()
|
||||
const router = useNativeRouter();
|
||||
|
||||
const name = ref('')
|
||||
const slug = ref('')
|
||||
const description = ref('')
|
||||
const manualSlug = ref(false)
|
||||
const name = ref("");
|
||||
const slug = ref("");
|
||||
const description = ref("");
|
||||
const manualSlug = ref(false);
|
||||
|
||||
const modal = ref()
|
||||
const modal = ref();
|
||||
|
||||
async function createProject() {
|
||||
startLoading()
|
||||
startLoading();
|
||||
try {
|
||||
const value = {
|
||||
name: name.value.trim(),
|
||||
description: description.value.trim(),
|
||||
slug: slug.value.trim().replace(/ +/g, ''),
|
||||
}
|
||||
slug: slug.value.trim().replace(/ +/g, ""),
|
||||
};
|
||||
|
||||
const result = await useBaseFetch('organization', {
|
||||
method: 'POST',
|
||||
const result = await useBaseFetch("organization", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(value),
|
||||
apiVersion: 3,
|
||||
})
|
||||
});
|
||||
|
||||
modal.value.hide()
|
||||
modal.value.hide();
|
||||
|
||||
await router.push(`/organization/${result.slug}`)
|
||||
await router.push(`/organization/${result.slug}`);
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
console.error(err);
|
||||
addNotification({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
group: "main",
|
||||
title: "An error occurred",
|
||||
text: err.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
stopLoading()
|
||||
stopLoading();
|
||||
}
|
||||
function show() {
|
||||
name.value = ''
|
||||
description.value = ''
|
||||
modal.value.show()
|
||||
name.value = "";
|
||||
description.value = "";
|
||||
modal.value.show();
|
||||
}
|
||||
|
||||
function updateSlug() {
|
||||
@@ -106,15 +106,15 @@ function updateSlug() {
|
||||
slug.value = name.value
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replaceAll(' ', '-')
|
||||
.replaceAll(/[^a-zA-Z0-9!@$()`.+,_"-]/g, '')
|
||||
.replaceAll(/--+/gm, '-')
|
||||
.replaceAll(" ", "-")
|
||||
.replaceAll(/[^a-zA-Z0-9!@$()`.+,_"-]/g, "")
|
||||
.replaceAll(/--+/gm, "-");
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
show,
|
||||
})
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
<Modal ref="modalOpen" header="Transfer Projects">
|
||||
<div class="universal-modal items">
|
||||
<div class="table">
|
||||
<div class="table-row table-head">
|
||||
<div class="table-cell check-cell">
|
||||
<div class="table-head table-row">
|
||||
<div class="check-cell table-cell">
|
||||
<Checkbox
|
||||
:model-value="selectedProjects.length === props.projects.length"
|
||||
@update:model-value="toggleSelectedProjects()"
|
||||
@@ -17,7 +17,7 @@
|
||||
<div class="table-cell" />
|
||||
</div>
|
||||
<div v-for="project in props.projects" :key="`project-${project.id}`" class="table-row">
|
||||
<div class="table-cell check-cell">
|
||||
<div class="check-cell table-cell">
|
||||
<Checkbox
|
||||
:disabled="(project.permissions & EDIT_DETAILS) === EDIT_DETAILS"
|
||||
:model-value="selectedProjects.includes(project)"
|
||||
@@ -59,9 +59,9 @@
|
||||
<span>{{
|
||||
$formatProjectType(
|
||||
$getProjectTypeForDisplay(
|
||||
project.project_types?.[0] ?? 'project',
|
||||
project.loaders
|
||||
)
|
||||
project.project_types?.[0] ?? "project",
|
||||
project.loaders,
|
||||
),
|
||||
)
|
||||
}}</span>
|
||||
</div>
|
||||
@@ -88,13 +88,13 @@
|
||||
<span>
|
||||
{{
|
||||
selectedProjects.length === props.projects.length
|
||||
? 'All'
|
||||
? "All"
|
||||
: selectedProjects.length
|
||||
}}
|
||||
</span>
|
||||
<span>
|
||||
{{ ' ' }}
|
||||
{{ selectedProjects.length === 1 ? 'project' : 'projects' }}
|
||||
{{ " " }}
|
||||
{{ selectedProjects.length === 1 ? "project" : "projects" }}
|
||||
</span>
|
||||
</span>
|
||||
</Button>
|
||||
@@ -109,39 +109,39 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { BoxIcon, SettingsIcon, TransferIcon, XIcon } from '@modrinth/assets'
|
||||
import { Button, Modal, Checkbox, CopyCode, Avatar } from '@modrinth/ui'
|
||||
import { BoxIcon, SettingsIcon, TransferIcon, XIcon } from "@modrinth/assets";
|
||||
import { Button, Modal, Checkbox, CopyCode, Avatar } from "@modrinth/ui";
|
||||
|
||||
const modalOpen = ref(null)
|
||||
const modalOpen = ref(null);
|
||||
|
||||
const props = defineProps({
|
||||
projects: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
// define emit for submission
|
||||
const emit = defineEmits(['submit'])
|
||||
const emit = defineEmits(["submit"]);
|
||||
|
||||
const selectedProjects = ref([])
|
||||
const selectedProjects = ref([]);
|
||||
|
||||
const toggleSelectedProjects = () => {
|
||||
if (selectedProjects.value.length === props.projects.length) {
|
||||
selectedProjects.value = []
|
||||
selectedProjects.value = [];
|
||||
} else {
|
||||
selectedProjects.value = props.projects
|
||||
selectedProjects.value = props.projects;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmitHandler = () => {
|
||||
if (selectedProjects.value.length === 0) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
emit('submit', selectedProjects.value)
|
||||
selectedProjects.value = []
|
||||
modalOpen.value?.hide()
|
||||
}
|
||||
emit("submit", selectedProjects.value);
|
||||
selectedProjects.value = [];
|
||||
modalOpen.value?.hide();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -175,7 +175,7 @@ const onSubmitHandler = () => {
|
||||
|
||||
.table-row {
|
||||
display: grid;
|
||||
grid-template: 'checkbox icon name type settings' 'checkbox icon id type settings';
|
||||
grid-template: "checkbox icon name type settings" "checkbox icon id type settings";
|
||||
grid-template-columns:
|
||||
min-content min-content minmax(min-content, 2fr)
|
||||
minmax(min-content, 1fr) min-content;
|
||||
@@ -207,7 +207,7 @@ const onSubmitHandler = () => {
|
||||
}
|
||||
|
||||
.table-head {
|
||||
grid-template: 'checkbox settings';
|
||||
grid-template: "checkbox settings";
|
||||
grid-template-columns: min-content minmax(min-content, 1fr);
|
||||
|
||||
:nth-child(2),
|
||||
@@ -222,7 +222,7 @@ const onSubmitHandler = () => {
|
||||
@media screen and (max-width: 560px) {
|
||||
.table-row {
|
||||
display: grid;
|
||||
grid-template: 'checkbox icon name settings' 'checkbox icon id settings' 'checkbox icon type settings' 'checkbox icon status settings';
|
||||
grid-template: "checkbox icon name settings" "checkbox icon id settings" "checkbox icon type settings" "checkbox icon status settings";
|
||||
grid-template-columns: min-content min-content minmax(min-content, 1fr) min-content;
|
||||
|
||||
:nth-child(5) {
|
||||
@@ -231,7 +231,7 @@ const onSubmitHandler = () => {
|
||||
}
|
||||
|
||||
.table-head {
|
||||
grid-template: 'checkbox settings';
|
||||
grid-template: "checkbox settings";
|
||||
grid-template-columns: min-content minmax(min-content, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,9 +51,9 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import GapIcon from '~/assets/images/utils/gap.svg?component'
|
||||
import LeftArrowIcon from '~/assets/images/utils/left-arrow.svg?component'
|
||||
import RightArrowIcon from '~/assets/images/utils/right-arrow.svg?component'
|
||||
import GapIcon from "~/assets/images/utils/gap.svg?component";
|
||||
import LeftArrowIcon from "~/assets/images/utils/left-arrow.svg?component";
|
||||
import RightArrowIcon from "~/assets/images/utils/right-arrow.svg?component";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -73,47 +73,47 @@ export default {
|
||||
linkFunction: {
|
||||
type: Function,
|
||||
default() {
|
||||
return () => '/'
|
||||
return () => "/";
|
||||
},
|
||||
},
|
||||
},
|
||||
emits: ['switch-page'],
|
||||
emits: ["switch-page"],
|
||||
computed: {
|
||||
pages() {
|
||||
let pages = []
|
||||
let pages = [];
|
||||
|
||||
if (this.count > 7) {
|
||||
if (this.page + 3 >= this.count) {
|
||||
pages = [
|
||||
1,
|
||||
'-',
|
||||
"-",
|
||||
this.count - 4,
|
||||
this.count - 3,
|
||||
this.count - 2,
|
||||
this.count - 1,
|
||||
this.count,
|
||||
]
|
||||
];
|
||||
} else if (this.page > 5) {
|
||||
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]
|
||||
pages = [1, 2, 3, 4, 5, "-", this.count];
|
||||
}
|
||||
} else {
|
||||
pages = Array.from({ length: this.count }, (_, i) => i + 1)
|
||||
pages = Array.from({ length: this.count }, (_, i) => i + 1);
|
||||
}
|
||||
|
||||
return pages
|
||||
return pages;
|
||||
},
|
||||
},
|
||||
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))
|
||||
this.$emit("switch-page", newPage);
|
||||
if (newPage !== null && newPage !== "" && !isNaN(newPage)) {
|
||||
this.$emit("switch-page", Math.min(Math.max(newPage, 1), this.count));
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -126,7 +126,10 @@ 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,
|
||||
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 {
|
||||
|
||||
@@ -90,15 +90,15 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Categories from '~/components/ui/search/Categories.vue'
|
||||
import Badge from '~/components/ui/Badge.vue'
|
||||
import EnvironmentIndicator from '~/components/ui/EnvironmentIndicator.vue'
|
||||
import Categories from "~/components/ui/search/Categories.vue";
|
||||
import Badge from "~/components/ui/Badge.vue";
|
||||
import EnvironmentIndicator from "~/components/ui/EnvironmentIndicator.vue";
|
||||
|
||||
import CalendarIcon from '~/assets/images/utils/calendar.svg?component'
|
||||
import EditIcon from '~/assets/images/utils/updated.svg?component'
|
||||
import DownloadIcon from '~/assets/images/utils/download.svg?component'
|
||||
import HeartIcon from '~/assets/images/utils/heart.svg?component'
|
||||
import Avatar from '~/components/ui/Avatar.vue'
|
||||
import CalendarIcon from "~/assets/images/utils/calendar.svg?component";
|
||||
import EditIcon from "~/assets/images/utils/updated.svg?component";
|
||||
import DownloadIcon from "~/assets/images/utils/download.svg?component";
|
||||
import HeartIcon from "~/assets/images/utils/heart.svg?component";
|
||||
import Avatar from "~/components/ui/Avatar.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -114,15 +114,15 @@ export default {
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
default: 'modrinth-0',
|
||||
default: "modrinth-0",
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'mod',
|
||||
default: "mod",
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: 'Project Name',
|
||||
default: "Project Name",
|
||||
},
|
||||
author: {
|
||||
type: String,
|
||||
@@ -130,11 +130,11 @@ export default {
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: 'A _type description',
|
||||
default: "A _type description",
|
||||
},
|
||||
iconUrl: {
|
||||
type: String,
|
||||
default: '#',
|
||||
default: "#",
|
||||
required: false,
|
||||
},
|
||||
downloads: {
|
||||
@@ -149,7 +149,7 @@ export default {
|
||||
},
|
||||
createdAt: {
|
||||
type: String,
|
||||
default: '0000-00-00',
|
||||
default: "0000-00-00",
|
||||
},
|
||||
updatedAt: {
|
||||
type: String,
|
||||
@@ -158,7 +158,7 @@ export default {
|
||||
categories: {
|
||||
type: Array,
|
||||
default() {
|
||||
return []
|
||||
return [];
|
||||
},
|
||||
},
|
||||
status: {
|
||||
@@ -172,12 +172,12 @@ export default {
|
||||
serverSide: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
default: "",
|
||||
},
|
||||
clientSide: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
default: "",
|
||||
},
|
||||
moderation: {
|
||||
type: Boolean,
|
||||
@@ -216,25 +216,25 @@ export default {
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const tags = useTags()
|
||||
const tags = useTags();
|
||||
|
||||
return { tags }
|
||||
return { tags };
|
||||
},
|
||||
computed: {
|
||||
projectTypeDisplay() {
|
||||
return this.$getProjectTypeForDisplay(this.type, this.categories)
|
||||
return this.$getProjectTypeForDisplay(this.type, this.categories);
|
||||
},
|
||||
toColor() {
|
||||
let color = this.color
|
||||
let color = this.color;
|
||||
|
||||
color >>>= 0
|
||||
const b = color & 0xff
|
||||
const g = (color & 0xff00) >>> 8
|
||||
const r = (color & 0xff0000) >>> 16
|
||||
return 'rgba(' + [r, g, b, 1].join(',') + ')'
|
||||
color >>>= 0;
|
||||
const b = color & 0xff;
|
||||
const g = (color & 0xff00) >>> 8;
|
||||
const r = (color & 0xff0000) >>> 16;
|
||||
return "rgba(" + [r, g, b, 1].join(",") + ")";
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -247,9 +247,9 @@ export default {
|
||||
|
||||
.display-mode--list .project-card {
|
||||
grid-template:
|
||||
'icon title stats'
|
||||
'icon description stats'
|
||||
'icon tags stats';
|
||||
"icon title stats"
|
||||
"icon description stats"
|
||||
"icon tags stats";
|
||||
grid-template-columns: min-content 1fr auto;
|
||||
grid-template-rows: min-content 1fr min-content;
|
||||
column-gap: var(--spacing-card-md);
|
||||
@@ -258,20 +258,20 @@ export default {
|
||||
|
||||
@media screen and (max-width: 750px) {
|
||||
grid-template:
|
||||
'icon title'
|
||||
'icon description'
|
||||
'icon tags'
|
||||
'stats stats';
|
||||
"icon title"
|
||||
"icon description"
|
||||
"icon tags"
|
||||
"stats stats";
|
||||
grid-template-columns: min-content auto;
|
||||
grid-template-rows: min-content 1fr min-content min-content;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 550px) {
|
||||
grid-template:
|
||||
'icon title'
|
||||
'icon description'
|
||||
'tags tags'
|
||||
'stats stats';
|
||||
"icon title"
|
||||
"icon description"
|
||||
"tags tags"
|
||||
"stats stats";
|
||||
grid-template-columns: min-content auto;
|
||||
grid-template-rows: min-content 1fr min-content min-content;
|
||||
}
|
||||
@@ -280,7 +280,7 @@ export default {
|
||||
.display-mode--gallery .project-card,
|
||||
.display-mode--grid .project-card {
|
||||
padding: 0 0 var(--spacing-card-bg) 0;
|
||||
grid-template: 'gallery gallery' 'icon title' 'description description' 'tags tags' 'stats stats';
|
||||
grid-template: "gallery gallery" "icon title" "description description" "tags tags" "stats stats";
|
||||
grid-template-columns: min-content 1fr;
|
||||
grid-template-rows: min-content min-content 1fr min-content min-content;
|
||||
row-gap: var(--spacing-card-sm);
|
||||
@@ -311,7 +311,9 @@ export default {
|
||||
img,
|
||||
svg {
|
||||
border-radius: var(--size-rounded-lg);
|
||||
box-shadow: -2px -2px 0 2px var(--color-raised-bg), 2px -2px 0 2px var(--color-raised-bg);
|
||||
box-shadow:
|
||||
-2px -2px 0 2px var(--color-raised-bg),
|
||||
2px -2px 0 2px var(--color-raised-bg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -502,10 +504,10 @@ export default {
|
||||
.small-mode {
|
||||
@media screen and (min-width: 750px) {
|
||||
grid-template:
|
||||
'icon title'
|
||||
'icon description'
|
||||
'icon tags'
|
||||
'stats stats' !important;
|
||||
"icon title"
|
||||
"icon description"
|
||||
"icon tags"
|
||||
"stats stats" !important;
|
||||
grid-template-columns: min-content auto !important;
|
||||
grid-template-rows: min-content 1fr min-content min-content !important;
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
v-tooltip="nag.title"
|
||||
:aria-label="nag.title"
|
||||
class="circle"
|
||||
:class="'circle ' + (!nag.condition ? 'done ' : '') + nag.status"
|
||||
:class="'circle ' + (!nag.condition ? 'done' : '') + nag.status"
|
||||
>
|
||||
<CheckIcon v-if="!nag.condition" />
|
||||
<RequiredIcon v-else-if="nag.status === 'required'" />
|
||||
@@ -106,17 +106,17 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { formatProjectType } from '~/plugins/shorthands.js'
|
||||
import { formatProjectType } from "~/plugins/shorthands.js";
|
||||
|
||||
import ChevronRightIcon from '~/assets/images/utils/chevron-right.svg?component'
|
||||
import DropdownIcon from '~/assets/images/utils/dropdown.svg?component'
|
||||
import CheckIcon from '~/assets/images/utils/check.svg?component'
|
||||
import CrossIcon from '~/assets/images/utils/x.svg?component'
|
||||
import RequiredIcon from '~/assets/images/utils/asterisk.svg?component'
|
||||
import SuggestionIcon from '~/assets/images/utils/lightbulb.svg?component'
|
||||
import ModerationIcon from '~/assets/images/sidebar/admin.svg?component'
|
||||
import SendIcon from '~/assets/images/utils/send.svg?component'
|
||||
import { acceptTeamInvite, removeTeamMember } from '~/helpers/teams.js'
|
||||
import ChevronRightIcon from "~/assets/images/utils/chevron-right.svg?component";
|
||||
import DropdownIcon from "~/assets/images/utils/dropdown.svg?component";
|
||||
import CheckIcon from "~/assets/images/utils/check.svg?component";
|
||||
import CrossIcon from "~/assets/images/utils/x.svg?component";
|
||||
import RequiredIcon from "~/assets/images/utils/asterisk.svg?component";
|
||||
import SuggestionIcon from "~/assets/images/utils/lightbulb.svg?component";
|
||||
import ModerationIcon from "~/assets/images/sidebar/admin.svg?component";
|
||||
import SendIcon from "~/assets/images/utils/send.svg?component";
|
||||
import { acceptTeamInvite, removeTeamMember } from "~/helpers/teams.js";
|
||||
|
||||
const props = defineProps({
|
||||
project: {
|
||||
@@ -126,7 +126,7 @@ const props = defineProps({
|
||||
versions: {
|
||||
type: Array,
|
||||
default() {
|
||||
return []
|
||||
return [];
|
||||
},
|
||||
},
|
||||
currentMember: {
|
||||
@@ -147,7 +147,7 @@ const props = defineProps({
|
||||
},
|
||||
routeName: {
|
||||
type: String,
|
||||
default: '',
|
||||
default: "",
|
||||
},
|
||||
auth: {
|
||||
type: Object,
|
||||
@@ -162,12 +162,12 @@ const props = defineProps({
|
||||
default() {
|
||||
return () => {
|
||||
addNotification({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: 'setProcessing function not found',
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
group: "main",
|
||||
title: "An error occurred",
|
||||
text: "setProcessing function not found",
|
||||
type: "error",
|
||||
});
|
||||
};
|
||||
},
|
||||
},
|
||||
toggleCollapsed: {
|
||||
@@ -175,12 +175,12 @@ const props = defineProps({
|
||||
default() {
|
||||
return () => {
|
||||
addNotification({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: 'toggleCollapsed function not found',
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
group: "main",
|
||||
title: "An error occurred",
|
||||
text: "toggleCollapsed function not found",
|
||||
type: "error",
|
||||
});
|
||||
};
|
||||
},
|
||||
},
|
||||
updateMembers: {
|
||||
@@ -188,81 +188,81 @@ const props = defineProps({
|
||||
default() {
|
||||
return () => {
|
||||
addNotification({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: 'updateMembers function not found',
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
group: "main",
|
||||
title: "An error occurred",
|
||||
text: "updateMembers function not found",
|
||||
type: "error",
|
||||
});
|
||||
};
|
||||
},
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const featuredGalleryImage = computed(() => props.project.gallery.find((img) => img.featured))
|
||||
const featuredGalleryImage = computed(() => props.project.gallery.find((img) => img.featured));
|
||||
|
||||
const nags = computed(() => [
|
||||
{
|
||||
condition: props.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.',
|
||||
status: 'required',
|
||||
title: "Upload a version",
|
||||
id: "upload-version",
|
||||
description: "At least one version is required for a project to be submitted for review.",
|
||||
status: "required",
|
||||
link: {
|
||||
path: 'versions',
|
||||
title: 'Visit versions page',
|
||||
hide: props.routeName === 'type-id-versions',
|
||||
path: "versions",
|
||||
title: "Visit versions page",
|
||||
hide: props.routeName === "type-id-versions",
|
||||
},
|
||||
},
|
||||
{
|
||||
condition:
|
||||
props.project.body === '' || props.project.body.startsWith('# Placeholder description'),
|
||||
title: 'Add a description',
|
||||
id: 'add-description',
|
||||
props.project.body === "" || props.project.body.startsWith("# Placeholder description"),
|
||||
title: "Add a description",
|
||||
id: "add-description",
|
||||
description:
|
||||
"A description that clearly describes the project's purpose and function is required.",
|
||||
status: 'required',
|
||||
status: "required",
|
||||
link: {
|
||||
path: 'settings/description',
|
||||
title: 'Visit description settings',
|
||||
hide: props.routeName === 'type-id-settings-description',
|
||||
path: "settings/description",
|
||||
title: "Visit description settings",
|
||||
hide: props.routeName === "type-id-settings-description",
|
||||
},
|
||||
},
|
||||
{
|
||||
condition: !props.project.icon_url,
|
||||
title: 'Add an icon',
|
||||
id: 'add-icon',
|
||||
title: "Add an icon",
|
||||
id: "add-icon",
|
||||
description:
|
||||
'Your project should have a nice-looking icon to uniquely identify your project at a glance.',
|
||||
status: 'suggestion',
|
||||
"Your project should have a nice-looking icon to uniquely identify your project at a glance.",
|
||||
status: "suggestion",
|
||||
link: {
|
||||
path: 'settings',
|
||||
title: 'Visit general settings',
|
||||
hide: props.routeName === 'type-id-settings',
|
||||
path: "settings",
|
||||
title: "Visit general settings",
|
||||
hide: props.routeName === "type-id-settings",
|
||||
},
|
||||
},
|
||||
{
|
||||
condition: props.project.gallery.length === 0 || !featuredGalleryImage,
|
||||
title: 'Feature a gallery image',
|
||||
id: 'feature-gallery-image',
|
||||
description: 'Featured gallery images may be the first impression of many users.',
|
||||
status: 'suggestion',
|
||||
title: "Feature a gallery image",
|
||||
id: "feature-gallery-image",
|
||||
description: "Featured gallery images may be the first impression of many users.",
|
||||
status: "suggestion",
|
||||
link: {
|
||||
path: 'gallery',
|
||||
title: 'Visit gallery page',
|
||||
hide: props.routeName === 'type-id-gallery',
|
||||
path: "gallery",
|
||||
title: "Visit gallery page",
|
||||
hide: props.routeName === "type-id-gallery",
|
||||
},
|
||||
},
|
||||
{
|
||||
hide: props.project.versions.length === 0,
|
||||
condition: props.project.categories.length < 1,
|
||||
title: 'Select tags',
|
||||
id: 'select-tags',
|
||||
description: 'Select all tags that apply to your project.',
|
||||
status: 'suggestion',
|
||||
title: "Select tags",
|
||||
id: "select-tags",
|
||||
description: "Select all tags that apply to your project.",
|
||||
status: "suggestion",
|
||||
link: {
|
||||
path: 'settings/tags',
|
||||
title: 'Visit tag settings',
|
||||
hide: props.routeName === 'type-id-settings-tags',
|
||||
path: "settings/tags",
|
||||
title: "Visit tag settings",
|
||||
hide: props.routeName === "type-id-settings-tags",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -273,110 +273,110 @@ const nags = computed(() => [
|
||||
props.project.discord_url ||
|
||||
props.project.donation_urls.length > 0
|
||||
),
|
||||
title: 'Add external links',
|
||||
id: 'add-links',
|
||||
title: "Add external links",
|
||||
id: "add-links",
|
||||
description:
|
||||
'Add any relevant links targeted outside of Modrinth, such as sources, issues, or a Discord invite.',
|
||||
status: 'suggestion',
|
||||
"Add any relevant links targeted outside of Modrinth, such as sources, issues, or a Discord invite.",
|
||||
status: "suggestion",
|
||||
link: {
|
||||
path: 'settings/links',
|
||||
title: 'Visit links settings',
|
||||
hide: props.routeName === 'type-id-settings-links',
|
||||
path: "settings/links",
|
||||
title: "Visit links settings",
|
||||
hide: props.routeName === "type-id-settings-links",
|
||||
},
|
||||
},
|
||||
{
|
||||
hide:
|
||||
props.project.versions.length === 0 ||
|
||||
props.project.project_type === 'resourcepack' ||
|
||||
props.project.project_type === 'plugin' ||
|
||||
props.project.project_type === 'shader' ||
|
||||
props.project.project_type === 'datapack',
|
||||
props.project.project_type === "resourcepack" ||
|
||||
props.project.project_type === "plugin" ||
|
||||
props.project.project_type === "shader" ||
|
||||
props.project.project_type === "datapack",
|
||||
condition:
|
||||
props.project.client_side === 'unknown' ||
|
||||
props.project.server_side === 'unknown' ||
|
||||
(props.project.client_side === 'unsupported' && props.project.server_side === 'unsupported'),
|
||||
title: 'Select supported environments',
|
||||
id: 'select-environments',
|
||||
props.project.client_side === "unknown" ||
|
||||
props.project.server_side === "unknown" ||
|
||||
(props.project.client_side === "unsupported" && props.project.server_side === "unsupported"),
|
||||
title: "Select supported environments",
|
||||
id: "select-environments",
|
||||
description: `Select if the ${formatProjectType(
|
||||
props.project.project_type
|
||||
props.project.project_type,
|
||||
).toLowerCase()} functions on the client-side and/or server-side.`,
|
||||
status: 'required',
|
||||
status: "required",
|
||||
link: {
|
||||
path: 'settings',
|
||||
title: 'Visit general settings',
|
||||
hide: props.routeName === 'type-id-settings',
|
||||
path: "settings",
|
||||
title: "Visit general settings",
|
||||
hide: props.routeName === "type-id-settings",
|
||||
},
|
||||
},
|
||||
{
|
||||
condition: props.project.license.id === 'LicenseRef-Unknown',
|
||||
title: 'Select license',
|
||||
id: 'select-license',
|
||||
condition: props.project.license.id === "LicenseRef-Unknown",
|
||||
title: "Select license",
|
||||
id: "select-license",
|
||||
description: `Select the license your ${formatProjectType(
|
||||
props.project.project_type
|
||||
props.project.project_type,
|
||||
).toLowerCase()} is distributed under.`,
|
||||
status: 'required',
|
||||
status: "required",
|
||||
link: {
|
||||
path: 'settings/license',
|
||||
title: 'Visit license settings',
|
||||
hide: props.routeName === 'type-id-settings-license',
|
||||
path: "settings/license",
|
||||
title: "Visit license settings",
|
||||
hide: props.routeName === "type-id-settings-license",
|
||||
},
|
||||
},
|
||||
{
|
||||
condition: props.project.status === 'draft',
|
||||
title: 'Submit for review',
|
||||
id: 'submit-for-review',
|
||||
condition: props.project.status === "draft",
|
||||
title: "Submit for review",
|
||||
id: "submit-for-review",
|
||||
description:
|
||||
'Your project is only viewable by members of the project. It must be reviewed by moderators in order to be published.',
|
||||
status: 'review',
|
||||
"Your project is only viewable by members of the project. It must be reviewed by moderators in order to be published.",
|
||||
status: "review",
|
||||
link: null,
|
||||
action: {
|
||||
onClick: submitForReview,
|
||||
title: 'Submit for review',
|
||||
disabled: () => nags.value.filter((x) => x.condition && x.status === 'required').length > 0,
|
||||
title: "Submit for review",
|
||||
disabled: () => nags.value.filter((x) => x.condition && x.status === "required").length > 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
condition: props.tags.rejectedStatuses.includes(props.project.status),
|
||||
title: 'Resubmit for review',
|
||||
id: 'resubmit-for-review',
|
||||
title: "Resubmit for review",
|
||||
id: "resubmit-for-review",
|
||||
description: `Your project has been ${props.project.status} by
|
||||
Modrinth's staff. In most cases, you can resubmit for review after
|
||||
addressing the staff's message.`,
|
||||
status: 'review',
|
||||
status: "review",
|
||||
link: {
|
||||
path: 'moderation',
|
||||
title: 'Visit moderation page',
|
||||
hide: props.routeName === 'type-id-moderation',
|
||||
path: "moderation",
|
||||
title: "Visit moderation page",
|
||||
hide: props.routeName === "type-id-moderation",
|
||||
},
|
||||
},
|
||||
])
|
||||
]);
|
||||
|
||||
const showInvitation = computed(() => {
|
||||
if (props.allMembers && props.auth) {
|
||||
const member = props.allMembers.find((x) => x.user.id === props.auth.user.id)
|
||||
return member && !member.accepted
|
||||
const member = props.allMembers.find((x) => x.user.id === props.auth.user.id);
|
||||
return member && !member.accepted;
|
||||
}
|
||||
return false
|
||||
})
|
||||
return false;
|
||||
});
|
||||
|
||||
const acceptInvite = () => {
|
||||
acceptTeamInvite(props.project.team)
|
||||
props.updateMembers()
|
||||
}
|
||||
acceptTeamInvite(props.project.team);
|
||||
props.updateMembers();
|
||||
};
|
||||
|
||||
const declineInvite = () => {
|
||||
removeTeamMember(props.project.team, props.auth.user.id)
|
||||
props.updateMembers()
|
||||
}
|
||||
removeTeamMember(props.project.team, props.auth.user.id);
|
||||
props.updateMembers();
|
||||
};
|
||||
|
||||
const submitForReview = async () => {
|
||||
if (
|
||||
!props.acknowledgedMessage ||
|
||||
nags.value.filter((x) => x.condition && x.status === 'required').length === 0
|
||||
nags.value.filter((x) => x.condition && x.status === "required").length === 0
|
||||
) {
|
||||
await props.setProcessing()
|
||||
await props.setProcessing();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -70,10 +70,10 @@
|
||||
class="iconified-button"
|
||||
@click="
|
||||
() => {
|
||||
selectedLoaders = []
|
||||
selectedGameVersions = []
|
||||
selectedVersionTypes = []
|
||||
updateQuery()
|
||||
selectedLoaders = [];
|
||||
selectedGameVersions = [];
|
||||
selectedVersionTypes = [];
|
||||
updateQuery();
|
||||
}
|
||||
"
|
||||
>
|
||||
@@ -84,51 +84,51 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Multiselect } from 'vue-multiselect'
|
||||
import Checkbox from '~/components/ui/Checkbox.vue'
|
||||
import ClearIcon from '~/assets/images/utils/clear.svg?component'
|
||||
import { Multiselect } from "vue-multiselect";
|
||||
import Checkbox from "~/components/ui/Checkbox.vue";
|
||||
import ClearIcon from "~/assets/images/utils/clear.svg?component";
|
||||
|
||||
const props = defineProps({
|
||||
versions: {
|
||||
type: Array,
|
||||
default() {
|
||||
return []
|
||||
return [];
|
||||
},
|
||||
},
|
||||
})
|
||||
const emit = defineEmits(['switch-page'])
|
||||
});
|
||||
const emit = defineEmits(["switch-page"]);
|
||||
|
||||
const router = useNativeRouter()
|
||||
const route = useNativeRoute()
|
||||
const router = useNativeRouter();
|
||||
const route = useNativeRoute();
|
||||
|
||||
const tags = useTags()
|
||||
const tags = useTags();
|
||||
|
||||
const tempLoaders = new Set()
|
||||
let tempVersions = new Set()
|
||||
const tempReleaseChannels = new Set()
|
||||
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)
|
||||
tempLoaders.add(loader);
|
||||
}
|
||||
for (const gameVersion of version.game_versions) {
|
||||
tempVersions.add(gameVersion)
|
||||
tempVersions.add(gameVersion);
|
||||
}
|
||||
tempReleaseChannels.add(version.version_type)
|
||||
tempReleaseChannels.add(version.version_type);
|
||||
}
|
||||
|
||||
tempVersions = Array.from(tempVersions)
|
||||
tempVersions = Array.from(tempVersions);
|
||||
|
||||
const loaderFilters = shallowRef(Array.from(tempLoaders))
|
||||
const loaderFilters = shallowRef(Array.from(tempLoaders));
|
||||
const gameVersionFilters = shallowRef(
|
||||
tags.value.gameVersions.filter((gameVer) => tempVersions.includes(gameVer.version))
|
||||
)
|
||||
const versionTypeFilters = shallowRef(Array.from(tempReleaseChannels))
|
||||
const includeSnapshots = ref(route.query.s === 'true')
|
||||
tags.value.gameVersions.filter((gameVer) => tempVersions.includes(gameVer.version)),
|
||||
);
|
||||
const versionTypeFilters = shallowRef(Array.from(tempReleaseChannels));
|
||||
const includeSnapshots = ref(route.query.s === "true");
|
||||
|
||||
const selectedGameVersions = shallowRef(getArrayOrString(route.query.g) ?? [])
|
||||
const selectedLoaders = shallowRef(getArrayOrString(route.query.l) ?? [])
|
||||
const selectedVersionTypes = shallowRef(getArrayOrString(route.query.c) ?? [])
|
||||
const selectedGameVersions = shallowRef(getArrayOrString(route.query.g) ?? []);
|
||||
const selectedLoaders = shallowRef(getArrayOrString(route.query.l) ?? []);
|
||||
const selectedVersionTypes = shallowRef(getArrayOrString(route.query.c) ?? []);
|
||||
|
||||
async function updateQuery() {
|
||||
await router.replace({
|
||||
@@ -139,8 +139,8 @@ async function updateQuery() {
|
||||
c: selectedVersionTypes.value.length === 0 ? undefined : selectedVersionTypes.value,
|
||||
s: includeSnapshots.value ? true : undefined,
|
||||
},
|
||||
})
|
||||
emit('switch-page', 1)
|
||||
});
|
||||
emit("switch-page", 1);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup>
|
||||
import dayjs from 'dayjs'
|
||||
import { formatNumber, formatMoney } from '@modrinth/utils'
|
||||
import VueApexCharts from 'vue3-apexcharts'
|
||||
import dayjs from "dayjs";
|
||||
import { formatNumber, formatMoney } from "@modrinth/utils";
|
||||
import VueApexCharts from "vue3-apexcharts";
|
||||
|
||||
const props = defineProps({
|
||||
name: {
|
||||
@@ -18,7 +18,7 @@ const props = defineProps({
|
||||
},
|
||||
formatLabels: {
|
||||
type: Function,
|
||||
default: (label) => dayjs(label).format('MMM D'),
|
||||
default: (label) => dayjs(label).format("MMM D"),
|
||||
},
|
||||
colors: {
|
||||
type: Array,
|
||||
@@ -26,11 +26,11 @@ const props = defineProps({
|
||||
},
|
||||
prefix: {
|
||||
type: String,
|
||||
default: '',
|
||||
default: "",
|
||||
},
|
||||
suffix: {
|
||||
type: String,
|
||||
default: '',
|
||||
default: "",
|
||||
},
|
||||
hideToolbar: {
|
||||
type: Boolean,
|
||||
@@ -46,7 +46,7 @@ const props = defineProps({
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'bar',
|
||||
default: "bar",
|
||||
},
|
||||
hideTotal: {
|
||||
type: Boolean,
|
||||
@@ -58,11 +58,11 @@ const props = defineProps({
|
||||
},
|
||||
legendPosition: {
|
||||
type: String,
|
||||
default: 'right',
|
||||
default: "right",
|
||||
},
|
||||
xAxisType: {
|
||||
type: String,
|
||||
default: 'datetime',
|
||||
default: "datetime",
|
||||
},
|
||||
percentStacked: {
|
||||
type: Boolean,
|
||||
@@ -76,14 +76,14 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
function formatTooltipValue(value, props) {
|
||||
return props.isMoney ? formatMoney(value, false) : formatNumber(value, false)
|
||||
return props.isMoney ? formatMoney(value, false) : formatNumber(value, false);
|
||||
}
|
||||
|
||||
function generateListEntry(value, index, _, w, props) {
|
||||
const color = w.globals.colors?.[index]
|
||||
const color = w.globals.colors?.[index];
|
||||
|
||||
return `<div class="list-entry">
|
||||
<span class="circle" style="background-color: ${color}"></span>
|
||||
@@ -93,35 +93,35 @@ function generateListEntry(value, index, _, w, props) {
|
||||
<div class="value">
|
||||
${props.prefix}${formatTooltipValue(value, props)}${props.suffix}
|
||||
</div>
|
||||
</div>`
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function generateTooltip({ series, seriesIndex, dataPointIndex, w }, props) {
|
||||
const label = w.globals.lastXAxis.categories?.[dataPointIndex]
|
||||
const label = w.globals.lastXAxis.categories?.[dataPointIndex];
|
||||
|
||||
const formattedLabel = props.formatLabels(label)
|
||||
const formattedLabel = props.formatLabels(label);
|
||||
|
||||
let tooltip = `<div class="bar-tooltip">
|
||||
<div class="seperated-entry title">
|
||||
<div class="label">${formattedLabel}</div>`
|
||||
<div class="label">${formattedLabel}</div>`;
|
||||
|
||||
// Logic for total and percent stacked
|
||||
if (!props.hideTotal) {
|
||||
if (props.percentStacked) {
|
||||
const total = series.reduce((a, b) => a + (b?.[dataPointIndex] || 0), 0)
|
||||
const percentValue = (100 * series[seriesIndex][dataPointIndex]) / total
|
||||
const total = series.reduce((a, b) => a + (b?.[dataPointIndex] || 0), 0);
|
||||
const percentValue = (100 * series[seriesIndex][dataPointIndex]) / total;
|
||||
tooltip += `<div class="value">${props.prefix}${formatNumber(percentValue)}%${
|
||||
props.suffix
|
||||
}</div>`
|
||||
}</div>`;
|
||||
} else {
|
||||
const totalValue = series.reduce((a, b) => a + (b?.[dataPointIndex] || 0), 0)
|
||||
const totalValue = series.reduce((a, b) => a + (b?.[dataPointIndex] || 0), 0);
|
||||
tooltip += `<div class="value">${props.prefix}${formatTooltipValue(totalValue, props)}${
|
||||
props.suffix
|
||||
}</div>`
|
||||
}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
tooltip += '</div><hr class="card-divider" />'
|
||||
tooltip += '</div><hr class="card-divider" />';
|
||||
|
||||
// Logic for generating list entries
|
||||
if (props.percentStacked) {
|
||||
@@ -130,10 +130,10 @@ function generateTooltip({ series, seriesIndex, dataPointIndex, w }, props) {
|
||||
seriesIndex,
|
||||
seriesIndex,
|
||||
w,
|
||||
props
|
||||
)
|
||||
props,
|
||||
);
|
||||
} else {
|
||||
const returnTopN = 5
|
||||
const returnTopN = 5;
|
||||
|
||||
const listEntries = series
|
||||
.map((value, index) => [
|
||||
@@ -144,13 +144,13 @@ function generateTooltip({ series, seriesIndex, dataPointIndex, w }, props) {
|
||||
.sort((a, b) => b[0] - a[0])
|
||||
.slice(0, returnTopN) // Return only the top X entries
|
||||
.map((value) => value[1])
|
||||
.join('')
|
||||
.join("");
|
||||
|
||||
tooltip += listEntries
|
||||
tooltip += listEntries;
|
||||
}
|
||||
|
||||
tooltip += '</div>'
|
||||
return tooltip
|
||||
tooltip += "</div>";
|
||||
return tooltip;
|
||||
}
|
||||
|
||||
const chartOptions = computed(() => {
|
||||
@@ -158,19 +158,19 @@ const chartOptions = computed(() => {
|
||||
chart: {
|
||||
id: props.name,
|
||||
fontFamily:
|
||||
'Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Roboto, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif',
|
||||
foreColor: 'var(--color-base)',
|
||||
"Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Roboto, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif",
|
||||
foreColor: "var(--color-base)",
|
||||
selection: {
|
||||
enabled: true,
|
||||
fill: {
|
||||
color: 'var(--color-brand)',
|
||||
color: "var(--color-brand)",
|
||||
},
|
||||
},
|
||||
toolbar: {
|
||||
show: false,
|
||||
},
|
||||
stacked: props.stacked,
|
||||
stackType: props.percentStacked ? '100%' : 'normal',
|
||||
stackType: props.percentStacked ? "100%" : "normal",
|
||||
zoom: {
|
||||
autoScaleYaxis: true,
|
||||
},
|
||||
@@ -183,7 +183,7 @@ const chartOptions = computed(() => {
|
||||
categories: props.labels,
|
||||
labels: {
|
||||
style: {
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
borderRadius: "var(--radius-sm)",
|
||||
},
|
||||
},
|
||||
axisTicks: {
|
||||
@@ -207,8 +207,8 @@ const chartOptions = computed(() => {
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
borderColor: 'var(--color-button-bg)',
|
||||
tickColor: 'var(--color-button-bg)',
|
||||
borderColor: "var(--color-button-bg)",
|
||||
tickColor: "var(--color-button-bg)",
|
||||
},
|
||||
legend: {
|
||||
show: !props.hideLegend,
|
||||
@@ -216,16 +216,16 @@ const chartOptions = computed(() => {
|
||||
showForZeroSeries: false,
|
||||
showForSingleSeries: false,
|
||||
showForNullSeries: false,
|
||||
fontSize: 'var(--font-size-nm)',
|
||||
fontSize: "var(--font-size-nm)",
|
||||
fontFamily:
|
||||
'Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Roboto, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif',
|
||||
"Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Roboto, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif",
|
||||
onItemClick: {
|
||||
toggleDataSeries: true,
|
||||
},
|
||||
},
|
||||
markers: {
|
||||
size: 0,
|
||||
strokeColor: 'var(--color-contrast)',
|
||||
strokeColor: "var(--color-contrast)",
|
||||
strokeWidth: 3,
|
||||
strokeOpacity: 1,
|
||||
fillOpacity: 1,
|
||||
@@ -236,29 +236,29 @@ const chartOptions = computed(() => {
|
||||
plotOptions: {
|
||||
bar: {
|
||||
horizontal: props.horizontalBar,
|
||||
columnWidth: '80%',
|
||||
endingShape: 'rounded',
|
||||
columnWidth: "80%",
|
||||
endingShape: "rounded",
|
||||
borderRadius: 5,
|
||||
borderRadiusApplication: 'end',
|
||||
borderRadiusWhenStacked: 'last',
|
||||
borderRadiusApplication: "end",
|
||||
borderRadiusWhenStacked: "last",
|
||||
},
|
||||
},
|
||||
stroke: {
|
||||
curve: 'smooth',
|
||||
curve: "smooth",
|
||||
width: 2,
|
||||
},
|
||||
tooltip: {
|
||||
custom: (d) => generateTooltip(d, props),
|
||||
},
|
||||
fill:
|
||||
props.type === 'area'
|
||||
props.type === "area"
|
||||
? {
|
||||
colors: props.colors,
|
||||
type: 'gradient',
|
||||
type: "gradient",
|
||||
opacity: 1,
|
||||
gradient: {
|
||||
shade: 'light',
|
||||
type: 'vertical',
|
||||
shade: "light",
|
||||
type: "vertical",
|
||||
shadeIntensity: 0,
|
||||
gradientToColors: props.colors,
|
||||
inverseColors: true,
|
||||
@@ -269,40 +269,40 @@ const chartOptions = computed(() => {
|
||||
},
|
||||
}
|
||||
: {},
|
||||
}
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
const chart = ref(null)
|
||||
const chart = ref(null);
|
||||
|
||||
const legendValues = ref(
|
||||
[...props.data].map((project, index) => {
|
||||
return { name: project.name, visible: true, color: props.colors[index] }
|
||||
})
|
||||
)
|
||||
return { name: project.name, visible: true, color: props.colors[index] };
|
||||
}),
|
||||
);
|
||||
|
||||
const flipLegend = (legend, newVal) => {
|
||||
legend.visible = newVal
|
||||
chart.value.toggleSeries(legend.name)
|
||||
}
|
||||
legend.visible = newVal;
|
||||
chart.value.toggleSeries(legend.name);
|
||||
};
|
||||
|
||||
const resetChart = () => {
|
||||
if (!chart.value) return
|
||||
chart.value.updateSeries([...props.data])
|
||||
if (!chart.value) return;
|
||||
chart.value.updateSeries([...props.data]);
|
||||
chart.value.updateOptions({
|
||||
xaxis: {
|
||||
categories: props.labels,
|
||||
},
|
||||
})
|
||||
chart.value.resetSeries()
|
||||
});
|
||||
chart.value.resetSeries();
|
||||
legendValues.value.forEach((legend) => {
|
||||
legend.visible = true
|
||||
})
|
||||
}
|
||||
legend.visible = true;
|
||||
});
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
resetChart,
|
||||
flipLegend,
|
||||
})
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -86,7 +86,9 @@
|
||||
v-model="selectedRange"
|
||||
:options="selectableRanges"
|
||||
name="Time range"
|
||||
:display-name="(o: typeof selectableRanges[number] | undefined) => o?.label || 'Custom'"
|
||||
:display-name="
|
||||
(o: (typeof selectableRanges)[number] | undefined) => o?.label || 'Custom'
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -218,7 +220,7 @@
|
||||
:style="{
|
||||
width: formatPercent(
|
||||
count,
|
||||
analytics.formattedData.value.downloadsByCountry.sum
|
||||
analytics.formattedData.value.downloadsByCountry.sum,
|
||||
),
|
||||
backgroundColor: 'var(--color-brand)',
|
||||
}"
|
||||
@@ -266,7 +268,7 @@
|
||||
v-tooltip="
|
||||
`${
|
||||
Math.round(
|
||||
(count / analytics.formattedData.value.viewsByCountry.sum) * 10000
|
||||
(count / analytics.formattedData.value.viewsByCountry.sum) * 10000,
|
||||
) / 100
|
||||
}%`
|
||||
"
|
||||
@@ -289,56 +291,56 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Button, Card, DropdownSelect } from '@modrinth/ui'
|
||||
import { formatMoney, formatNumber, formatCategoryHeader } from '@modrinth/utils'
|
||||
import { UpdatedIcon, DownloadIcon } from '@modrinth/assets'
|
||||
import dayjs from 'dayjs'
|
||||
import { computed } from 'vue'
|
||||
import { Button, Card, DropdownSelect } from "@modrinth/ui";
|
||||
import { formatMoney, formatNumber, formatCategoryHeader } from "@modrinth/utils";
|
||||
import { UpdatedIcon, DownloadIcon } from "@modrinth/assets";
|
||||
import dayjs from "dayjs";
|
||||
import { computed } from "vue";
|
||||
|
||||
import { analyticsSetToCSVString, intToRgba } from '~/utils/analytics.js'
|
||||
import { analyticsSetToCSVString, intToRgba } from "~/utils/analytics.js";
|
||||
|
||||
import { UiChartsCompactChart as CompactChart, UiChartsChart as Chart } from '#components'
|
||||
import { UiChartsCompactChart as CompactChart, UiChartsChart as Chart } from "#components";
|
||||
|
||||
import PaletteIcon from '~/assets/icons/palette.svg?component'
|
||||
import PaletteIcon from "~/assets/icons/palette.svg?component";
|
||||
|
||||
const router = useNativeRouter()
|
||||
const theme = useTheme()
|
||||
const router = useNativeRouter();
|
||||
const theme = useTheme();
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
projects?: any[]
|
||||
projects?: any[];
|
||||
/**
|
||||
* @deprecated Use `ranges` instead
|
||||
*/
|
||||
resoloutions?: Record<string, number>
|
||||
ranges?: Record<number, [string, number] | string>
|
||||
personal?: boolean
|
||||
resoloutions?: Record<string, number>;
|
||||
ranges?: Record<number, [string, number] | string>;
|
||||
personal?: boolean;
|
||||
}>(),
|
||||
{
|
||||
projects: undefined,
|
||||
resoloutions: () => defaultResoloutions,
|
||||
ranges: () => defaultRanges,
|
||||
personal: false,
|
||||
}
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
const projects = ref(props.projects || [])
|
||||
const projects = ref(props.projects || []);
|
||||
|
||||
const selectableRanges = Object.entries(props.ranges).map(([duration, extra]) => ({
|
||||
label: typeof extra === 'string' ? extra : extra[0],
|
||||
label: typeof extra === "string" ? extra : extra[0],
|
||||
value: Number(duration),
|
||||
res: typeof extra === 'string' ? Number(duration) : extra[1],
|
||||
}))
|
||||
res: typeof extra === "string" ? Number(duration) : extra[1],
|
||||
}));
|
||||
|
||||
// const selectedChart = ref('downloads')
|
||||
const selectedChart = computed({
|
||||
get: () => {
|
||||
const id = (router.currentRoute.value.query?.chart as string | undefined) || 'downloads'
|
||||
const id = (router.currentRoute.value.query?.chart as string | undefined) || "downloads";
|
||||
// if the id is anything but the 3 charts we have or undefined, throw an error
|
||||
if (!['downloads', 'views', 'revenue'].includes(id)) {
|
||||
throw new Error(`Unknown chart ${id}`)
|
||||
if (!["downloads", "views", "revenue"].includes(id)) {
|
||||
throw new Error(`Unknown chart ${id}`);
|
||||
}
|
||||
return id
|
||||
return id;
|
||||
},
|
||||
set: (chart) => {
|
||||
router.push({
|
||||
@@ -346,153 +348,153 @@ const selectedChart = computed({
|
||||
...router.currentRoute.value.query,
|
||||
chart,
|
||||
},
|
||||
})
|
||||
});
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
// Chart refs
|
||||
const downloadsChart = ref()
|
||||
const viewsChart = ref()
|
||||
const revenueChart = ref()
|
||||
const tinyDownloadChart = ref()
|
||||
const tinyViewChart = ref()
|
||||
const tinyRevenueChart = ref()
|
||||
const downloadsChart = ref();
|
||||
const viewsChart = ref();
|
||||
const revenueChart = ref();
|
||||
const tinyDownloadChart = ref();
|
||||
const tinyViewChart = ref();
|
||||
const tinyRevenueChart = ref();
|
||||
|
||||
const selectedDisplayProjects = ref(props.projects || [])
|
||||
const selectedDisplayProjects = ref(props.projects || []);
|
||||
|
||||
const removeProjectFromDisplay = (id: string) => {
|
||||
selectedDisplayProjects.value = selectedDisplayProjects.value.filter((p) => p.id !== id)
|
||||
}
|
||||
selectedDisplayProjects.value = selectedDisplayProjects.value.filter((p) => p.id !== id);
|
||||
};
|
||||
|
||||
const addProjectToDisplay = (id: string) => {
|
||||
selectedDisplayProjects.value = [
|
||||
...selectedDisplayProjects.value,
|
||||
props.projects?.find((p) => p.id === id),
|
||||
].filter(Boolean)
|
||||
}
|
||||
].filter(Boolean);
|
||||
};
|
||||
|
||||
const projectIsOnDisplay = (id: string) => {
|
||||
return selectedDisplayProjects.value?.some((p) => p.id === id) ?? false
|
||||
}
|
||||
return selectedDisplayProjects.value?.some((p) => p.id === id) ?? false;
|
||||
};
|
||||
|
||||
const resetCharts = () => {
|
||||
downloadsChart.value?.resetChart()
|
||||
viewsChart.value?.resetChart()
|
||||
revenueChart.value?.resetChart()
|
||||
downloadsChart.value?.resetChart();
|
||||
viewsChart.value?.resetChart();
|
||||
revenueChart.value?.resetChart();
|
||||
|
||||
tinyDownloadChart.value?.resetChart()
|
||||
tinyViewChart.value?.resetChart()
|
||||
tinyRevenueChart.value?.resetChart()
|
||||
}
|
||||
tinyDownloadChart.value?.resetChart();
|
||||
tinyViewChart.value?.resetChart();
|
||||
tinyRevenueChart.value?.resetChart();
|
||||
};
|
||||
|
||||
const isUsingProjectColors = computed({
|
||||
get: () => {
|
||||
return (
|
||||
router.currentRoute.value.query?.colors === 'true' ||
|
||||
router.currentRoute.value.query?.colors === "true" ||
|
||||
router.currentRoute.value.query?.colors === undefined
|
||||
)
|
||||
);
|
||||
},
|
||||
set: (newValue) => {
|
||||
router.push({
|
||||
query: {
|
||||
...router.currentRoute.value.query,
|
||||
colors: newValue ? 'true' : 'false',
|
||||
colors: newValue ? "true" : "false",
|
||||
},
|
||||
})
|
||||
});
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const analytics = useFetchAllAnalytics(
|
||||
resetCharts,
|
||||
projects,
|
||||
selectedDisplayProjects,
|
||||
props.personal
|
||||
)
|
||||
props.personal,
|
||||
);
|
||||
|
||||
const { startDate, endDate, timeRange, timeResolution } = analytics
|
||||
const { startDate, endDate, timeRange, timeResolution } = analytics;
|
||||
|
||||
const selectedRange = computed({
|
||||
get: () => {
|
||||
return (
|
||||
selectableRanges.find((option) => option.value === timeRange.value) || {
|
||||
label: 'Custom',
|
||||
label: "Custom",
|
||||
value: timeRange.value,
|
||||
}
|
||||
)
|
||||
);
|
||||
},
|
||||
set: (newRange: { label: string; value: number; res?: number }) => {
|
||||
timeRange.value = newRange.value
|
||||
startDate.value = Date.now() - timeRange.value * 60 * 1000
|
||||
endDate.value = Date.now()
|
||||
timeRange.value = newRange.value;
|
||||
startDate.value = Date.now() - timeRange.value * 60 * 1000;
|
||||
endDate.value = Date.now();
|
||||
|
||||
if (newRange?.res) {
|
||||
timeResolution.value = newRange.res
|
||||
timeResolution.value = newRange.res;
|
||||
}
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const selectedDataSet = computed(() => {
|
||||
switch (selectedChart.value) {
|
||||
case 'downloads':
|
||||
return analytics.totalData.value.downloads
|
||||
case 'views':
|
||||
return analytics.totalData.value.views
|
||||
case 'revenue':
|
||||
return analytics.totalData.value.revenue
|
||||
case "downloads":
|
||||
return analytics.totalData.value.downloads;
|
||||
case "views":
|
||||
return analytics.totalData.value.views;
|
||||
case "revenue":
|
||||
return analytics.totalData.value.revenue;
|
||||
default:
|
||||
throw new Error(`Unknown chart ${selectedChart.value}`)
|
||||
throw new Error(`Unknown chart ${selectedChart.value}`);
|
||||
}
|
||||
})
|
||||
});
|
||||
const selectedDataSetProjects = computed(() => {
|
||||
return selectedDataSet.value.projectIds
|
||||
.map((id) => props.projects?.find((p) => p?.id === id))
|
||||
.filter(Boolean)
|
||||
})
|
||||
.filter(Boolean);
|
||||
});
|
||||
|
||||
const downloadSelectedSetAsCSV = () => {
|
||||
const selectedChartName = selectedChart.value
|
||||
const selectedChartName = selectedChart.value;
|
||||
|
||||
const csv = analyticsSetToCSVString(selectedDataSet.value)
|
||||
const csv = analyticsSetToCSVString(selectedDataSet.value);
|
||||
|
||||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' })
|
||||
const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
|
||||
|
||||
const link = document.createElement('a')
|
||||
const url = URL.createObjectURL(blob)
|
||||
link.setAttribute('href', url)
|
||||
link.setAttribute('download', `${selectedChartName}-data.csv`)
|
||||
link.style.visibility = 'hidden'
|
||||
document.body.appendChild(link)
|
||||
const link = document.createElement("a");
|
||||
const url = URL.createObjectURL(blob);
|
||||
link.setAttribute("href", url);
|
||||
link.setAttribute("download", `${selectedChartName}-data.csv`);
|
||||
link.style.visibility = "hidden";
|
||||
document.body.appendChild(link);
|
||||
|
||||
link.click()
|
||||
}
|
||||
link.click();
|
||||
};
|
||||
|
||||
const onDownloadSetAsCSV = useClientTry(async () => await downloadSelectedSetAsCSV())
|
||||
const onDownloadSetAsCSV = useClientTry(async () => await downloadSelectedSetAsCSV());
|
||||
const onToggleColors = () => {
|
||||
isUsingProjectColors.value = !isUsingProjectColors.value
|
||||
}
|
||||
isUsingProjectColors.value = !isUsingProjectColors.value;
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
const defaultResoloutions: Record<string, number> = {
|
||||
'5 minutes': 5,
|
||||
'30 minutes': 30,
|
||||
'An hour': 60,
|
||||
'12 hours': 720,
|
||||
'A day': 1440,
|
||||
'A week': 10080,
|
||||
}
|
||||
"5 minutes": 5,
|
||||
"30 minutes": 30,
|
||||
"An hour": 60,
|
||||
"12 hours": 720,
|
||||
"A day": 1440,
|
||||
"A week": 10080,
|
||||
};
|
||||
|
||||
const defaultRanges: Record<number, [string, number] | string> = {
|
||||
30: ['Last 30 minutes', 1],
|
||||
60: ['Last hour', 5],
|
||||
720: ['Last 12 hours', 15],
|
||||
1440: ['Last day', 60],
|
||||
10080: ['Last week', 720],
|
||||
43200: ['Last month', 1440],
|
||||
129600: ['Last quarter', 10080],
|
||||
525600: ['Last year', 20160],
|
||||
1051200: ['Last two years', 40320],
|
||||
}
|
||||
30: ["Last 30 minutes", 1],
|
||||
60: ["Last hour", 5],
|
||||
720: ["Last 12 hours", 15],
|
||||
1440: ["Last day", 60],
|
||||
10080: ["Last week", 720],
|
||||
43200: ["Last month", 1440],
|
||||
129600: ["Last quarter", 10080],
|
||||
525600: ["Last year", 20160],
|
||||
1051200: ["Last two years", 40320],
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -590,7 +592,9 @@ const defaultRanges: Record<number, [string, number] | string> = {
|
||||
.chart-button-base__selected {
|
||||
color: var(--color-contrast);
|
||||
background-color: var(--color-brand-highlight);
|
||||
box-shadow: inset 0 0 0 transparent, 0 0 0 2px var(--color-brand);
|
||||
box-shadow:
|
||||
inset 0 0 0 transparent,
|
||||
0 0 0 2px var(--color-brand);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-brand-highlight);
|
||||
@@ -662,7 +666,7 @@ const defaultRanges: Record<number, [string, number] | string> = {
|
||||
|
||||
.country-value {
|
||||
display: grid;
|
||||
grid-template-areas: 'flag text bar';
|
||||
grid-template-areas: "flag text bar";
|
||||
grid-template-columns: auto 1fr 10rem;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import { Card } from '@modrinth/ui'
|
||||
import VueApexCharts from 'vue3-apexcharts'
|
||||
import { Card } from "@modrinth/ui";
|
||||
import VueApexCharts from "vue3-apexcharts";
|
||||
|
||||
// let VueApexCharts
|
||||
// if (process.client) {
|
||||
@@ -10,11 +10,11 @@ import VueApexCharts from 'vue3-apexcharts'
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: String,
|
||||
default: '',
|
||||
default: "",
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
default: "",
|
||||
},
|
||||
data: {
|
||||
type: Array,
|
||||
@@ -26,11 +26,11 @@ const props = defineProps({
|
||||
},
|
||||
prefix: {
|
||||
type: String,
|
||||
default: '',
|
||||
default: "",
|
||||
},
|
||||
suffix: {
|
||||
type: String,
|
||||
default: '',
|
||||
default: "",
|
||||
},
|
||||
isMoney: {
|
||||
type: Boolean,
|
||||
@@ -38,17 +38,17 @@ const props = defineProps({
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: 'var(--color-brand)',
|
||||
default: "var(--color-brand)",
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
// no grid lines, no toolbar, no legend, no data labels
|
||||
const chartOptions = {
|
||||
chart: {
|
||||
id: props.title,
|
||||
fontFamily:
|
||||
'Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Roboto, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif',
|
||||
foreColor: 'var(--color-base)',
|
||||
"Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Roboto, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif",
|
||||
foreColor: "var(--color-base)",
|
||||
toolbar: {
|
||||
show: false,
|
||||
},
|
||||
@@ -61,16 +61,16 @@ const chartOptions = {
|
||||
parentHeightOffset: 0,
|
||||
},
|
||||
stroke: {
|
||||
curve: 'smooth',
|
||||
curve: "smooth",
|
||||
width: 2,
|
||||
},
|
||||
fill: {
|
||||
colors: [props.color],
|
||||
type: 'gradient',
|
||||
type: "gradient",
|
||||
opacity: 1,
|
||||
gradient: {
|
||||
shade: 'light',
|
||||
type: 'vertical',
|
||||
shade: "light",
|
||||
type: "vertical",
|
||||
shadeIntensity: 0,
|
||||
gradientToColors: [props.color],
|
||||
inverseColors: true,
|
||||
@@ -91,7 +91,7 @@ const chartOptions = {
|
||||
enabled: false,
|
||||
},
|
||||
xaxis: {
|
||||
type: 'datetime',
|
||||
type: "datetime",
|
||||
categories: props.labels,
|
||||
labels: {
|
||||
show: false,
|
||||
@@ -120,23 +120,23 @@ const chartOptions = {
|
||||
tooltip: {
|
||||
enabled: false,
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
const chart = ref(null)
|
||||
const chart = ref(null);
|
||||
|
||||
const resetChart = () => {
|
||||
chart.value?.updateSeries([...props.data])
|
||||
chart.value?.updateSeries([...props.data]);
|
||||
chart.value?.updateOptions({
|
||||
xaxis: {
|
||||
categories: props.labels,
|
||||
},
|
||||
})
|
||||
chart.value?.resetSeries()
|
||||
}
|
||||
});
|
||||
chart.value?.resetSeries();
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
resetChart,
|
||||
})
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { computed, defineComponent, h, onBeforeUnmount, ref, watch } from 'vue'
|
||||
import { startLoading, stopLoading, useNuxtApp } from '#imports'
|
||||
import { computed, defineComponent, h, onBeforeUnmount, ref, watch } from "vue";
|
||||
import { startLoading, stopLoading, useNuxtApp } from "#imports";
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ModrinthLoadingIndicator',
|
||||
name: "ModrinthLoadingIndicator",
|
||||
props: {
|
||||
throttle: {
|
||||
type: Number,
|
||||
@@ -19,115 +19,115 @@ export default defineComponent({
|
||||
color: {
|
||||
type: [String, Boolean],
|
||||
default:
|
||||
'repeating-linear-gradient(to right, var(--color-brand-green) 0%, var(--landing-green-label) 100%)',
|
||||
"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 nuxtApp = useNuxtApp();
|
||||
nuxtApp.hook("page:start", () => {
|
||||
startLoading();
|
||||
indicator.start();
|
||||
});
|
||||
nuxtApp.hook("page:finish", () => {
|
||||
stopLoading();
|
||||
indicator.finish();
|
||||
});
|
||||
onBeforeUnmount(() => indicator.clear);
|
||||
|
||||
const loading = useLoading()
|
||||
const loading = useLoading();
|
||||
|
||||
watch(loading, (newValue) => {
|
||||
if (newValue) {
|
||||
indicator.start()
|
||||
indicator.start();
|
||||
} else {
|
||||
indicator.finish()
|
||||
indicator.finish();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
return () =>
|
||||
h(
|
||||
'div',
|
||||
"div",
|
||||
{
|
||||
class: 'nuxt-loading-indicator',
|
||||
class: "nuxt-loading-indicator",
|
||||
style: {
|
||||
position: 'fixed',
|
||||
position: "fixed",
|
||||
top: 0,
|
||||
right: 0,
|
||||
left: 0,
|
||||
pointerEvents: 'none',
|
||||
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',
|
||||
transition: "width 0.1s, height 0.4s, opacity 0.4s",
|
||||
zIndex: 999999,
|
||||
},
|
||||
},
|
||||
slots
|
||||
)
|
||||
slots,
|
||||
);
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
function useLoadingIndicator(opts: { duration: number; throttle: number }) {
|
||||
const progress = ref(0)
|
||||
const isLoading = ref(false)
|
||||
const step = computed(() => 10000 / opts.duration)
|
||||
const progress = ref(0);
|
||||
const isLoading = ref(false);
|
||||
const step = computed(() => 10000 / opts.duration);
|
||||
|
||||
let _timer: any = null
|
||||
let _throttle: any = null
|
||||
let _timer: any = null;
|
||||
let _throttle: any = null;
|
||||
|
||||
function start() {
|
||||
clear()
|
||||
progress.value = 0
|
||||
clear();
|
||||
progress.value = 0;
|
||||
if (opts.throttle && process.client) {
|
||||
_throttle = setTimeout(() => {
|
||||
isLoading.value = true
|
||||
_startTimer()
|
||||
}, opts.throttle)
|
||||
isLoading.value = true;
|
||||
_startTimer();
|
||||
}, opts.throttle);
|
||||
} else {
|
||||
isLoading.value = true
|
||||
_startTimer()
|
||||
isLoading.value = true;
|
||||
_startTimer();
|
||||
}
|
||||
}
|
||||
function finish() {
|
||||
progress.value = 100
|
||||
_hide()
|
||||
progress.value = 100;
|
||||
_hide();
|
||||
}
|
||||
|
||||
function clear() {
|
||||
clearInterval(_timer)
|
||||
clearTimeout(_throttle)
|
||||
_timer = null
|
||||
_throttle = null
|
||||
clearInterval(_timer);
|
||||
clearTimeout(_throttle);
|
||||
_timer = null;
|
||||
_throttle = null;
|
||||
}
|
||||
|
||||
function _increase(num: number) {
|
||||
progress.value = Math.min(100, progress.value + num)
|
||||
progress.value = Math.min(100, progress.value + num);
|
||||
}
|
||||
|
||||
function _hide() {
|
||||
clear()
|
||||
clear();
|
||||
if (process.client) {
|
||||
setTimeout(() => {
|
||||
isLoading.value = false
|
||||
isLoading.value = false;
|
||||
setTimeout(() => {
|
||||
progress.value = 0
|
||||
}, 400)
|
||||
}, 500)
|
||||
progress.value = 0;
|
||||
}, 400);
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
function _startTimer() {
|
||||
if (process.client) {
|
||||
_timer = setInterval(() => {
|
||||
_increase(step.value)
|
||||
}, 100)
|
||||
_increase(step.value);
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,5 +137,5 @@ function useLoadingIndicator(opts: { duration: number; throttle: number }) {
|
||||
start,
|
||||
finish,
|
||||
clear,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<span class="title">{{ report.project.title }}</span>
|
||||
<span>{{
|
||||
$formatProjectType(
|
||||
$getProjectTypeForUrl(report.project.project_type, report.project.loaders)
|
||||
$getProjectTypeForUrl(report.project.project_type, report.project.loaders),
|
||||
)
|
||||
}}</span>
|
||||
</div>
|
||||
@@ -42,7 +42,7 @@
|
||||
<span class="title">{{ report.project.title }}</span>
|
||||
<span>{{
|
||||
$formatProjectType(
|
||||
$getProjectTypeForUrl(report.project.project_type, report.project.loaders)
|
||||
$getProjectTypeForUrl(report.project.project_type, report.project.loaders),
|
||||
)
|
||||
}}</span>
|
||||
</div>
|
||||
@@ -88,14 +88,14 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { renderHighlightedString } from '~/helpers/highlight.js'
|
||||
import Avatar from '~/components/ui/Avatar.vue'
|
||||
import Badge from '~/components/ui/Badge.vue'
|
||||
import ReportIcon from '~/assets/images/utils/report.svg?component'
|
||||
import UnknownIcon from '~/assets/images/utils/unknown.svg?component'
|
||||
import VersionIcon from '~/assets/images/utils/version.svg?component'
|
||||
import ThreadSummary from '~/components/ui/thread/ThreadSummary.vue'
|
||||
import CopyCode from '~/components/ui/CopyCode.vue'
|
||||
import { renderHighlightedString } from "~/helpers/highlight.js";
|
||||
import Avatar from "~/components/ui/Avatar.vue";
|
||||
import Badge from "~/components/ui/Badge.vue";
|
||||
import ReportIcon from "~/assets/images/utils/report.svg?component";
|
||||
import UnknownIcon from "~/assets/images/utils/unknown.svg?component";
|
||||
import VersionIcon from "~/assets/images/utils/version.svg?component";
|
||||
import ThreadSummary from "~/components/ui/thread/ThreadSummary.vue";
|
||||
import CopyCode from "~/components/ui/CopyCode.vue";
|
||||
|
||||
defineProps({
|
||||
report: {
|
||||
@@ -122,9 +122,9 @@ defineProps({
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const flags = useFeatureFlags()
|
||||
const flags = useFeatureFlags();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -21,10 +21,10 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import Breadcrumbs from '~/components/ui/Breadcrumbs.vue'
|
||||
import ConversationThread from '~/components/ui/thread/ConversationThread.vue'
|
||||
import ReportInfo from '~/components/ui/report/ReportInfo.vue'
|
||||
import { addReportMessage } from '~/helpers/threads.js'
|
||||
import Breadcrumbs from "~/components/ui/Breadcrumbs.vue";
|
||||
import ConversationThread from "~/components/ui/thread/ConversationThread.vue";
|
||||
import ReportInfo from "~/components/ui/report/ReportInfo.vue";
|
||||
import { addReportMessage } from "~/helpers/threads.js";
|
||||
|
||||
const props = defineProps({
|
||||
reportId: {
|
||||
@@ -39,76 +39,76 @@ const props = defineProps({
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const report = ref(null)
|
||||
const report = ref(null);
|
||||
|
||||
await fetchReport().then((result) => {
|
||||
report.value = result
|
||||
})
|
||||
report.value = result;
|
||||
});
|
||||
|
||||
const { data: rawThread } = await useAsyncData(`thread/${report.value.thread_id}`, () =>
|
||||
useBaseFetch(`thread/${report.value.thread_id}`)
|
||||
)
|
||||
const thread = computed(() => addReportMessage(rawThread.value, report.value))
|
||||
useBaseFetch(`thread/${report.value.thread_id}`),
|
||||
);
|
||||
const thread = computed(() => addReportMessage(rawThread.value, report.value));
|
||||
|
||||
async function updateThread(newThread) {
|
||||
rawThread.value = newThread
|
||||
report.value = await fetchReport()
|
||||
rawThread.value = newThread;
|
||||
report.value = await fetchReport();
|
||||
}
|
||||
|
||||
async function fetchReport() {
|
||||
const { data: rawReport } = await useAsyncData(`report/${props.reportId}`, () =>
|
||||
useBaseFetch(`report/${props.reportId}`)
|
||||
)
|
||||
rawReport.value.item_id = rawReport.value.item_id.replace(/"/g, '')
|
||||
useBaseFetch(`report/${props.reportId}`),
|
||||
);
|
||||
rawReport.value.item_id = rawReport.value.item_id.replace(/"/g, "");
|
||||
|
||||
const userIds = []
|
||||
userIds.push(rawReport.value.reporter)
|
||||
if (rawReport.value.item_type === 'user') {
|
||||
userIds.push(rawReport.value.item_id)
|
||||
const userIds = [];
|
||||
userIds.push(rawReport.value.reporter);
|
||||
if (rawReport.value.item_type === "user") {
|
||||
userIds.push(rawReport.value.item_id);
|
||||
}
|
||||
|
||||
const versionId = rawReport.value.item_type === 'version' ? rawReport.value.item_id : null
|
||||
const versionId = rawReport.value.item_type === "version" ? rawReport.value.item_id : null;
|
||||
|
||||
let users = []
|
||||
let users = [];
|
||||
if (userIds.length > 0) {
|
||||
const { data: usersVal } = await useAsyncData(`users?ids=${JSON.stringify(userIds)}`, () =>
|
||||
useBaseFetch(`users?ids=${encodeURIComponent(JSON.stringify(userIds))}`)
|
||||
)
|
||||
users = usersVal.value
|
||||
useBaseFetch(`users?ids=${encodeURIComponent(JSON.stringify(userIds))}`),
|
||||
);
|
||||
users = usersVal.value;
|
||||
}
|
||||
|
||||
let version = null
|
||||
let version = null;
|
||||
if (versionId) {
|
||||
const { data: versionVal } = await useAsyncData(`version/${versionId}`, () =>
|
||||
useBaseFetch(`version/${versionId}`)
|
||||
)
|
||||
version = versionVal.value
|
||||
useBaseFetch(`version/${versionId}`),
|
||||
);
|
||||
version = versionVal.value;
|
||||
}
|
||||
|
||||
const projectId = version
|
||||
? version.project_id
|
||||
: rawReport.value.item_type === 'project'
|
||||
? rawReport.value.item_id
|
||||
: null
|
||||
: rawReport.value.item_type === "project"
|
||||
? rawReport.value.item_id
|
||||
: null;
|
||||
|
||||
let project = null
|
||||
let project = null;
|
||||
if (projectId) {
|
||||
const { data: projectVal } = await useAsyncData(`project/${projectId}`, () =>
|
||||
useBaseFetch(`project/${projectId}`)
|
||||
)
|
||||
project = projectVal.value
|
||||
useBaseFetch(`project/${projectId}`),
|
||||
);
|
||||
project = projectVal.value;
|
||||
}
|
||||
|
||||
const reportData = rawReport.value
|
||||
reportData.project = project
|
||||
reportData.version = version
|
||||
reportData.reporterUser = users.find((user) => user.id === rawReport.value.reporter)
|
||||
if (rawReport.value.item_type === 'user') {
|
||||
reportData.user = users.find((user) => user.id === rawReport.value.item_id)
|
||||
const reportData = rawReport.value;
|
||||
reportData.project = project;
|
||||
reportData.version = version;
|
||||
reportData.reporterUser = users.find((user) => user.id === rawReport.value.reporter);
|
||||
if (rawReport.value.item_type === "user") {
|
||||
reportData.user = users.find((user) => user.id === rawReport.value.item_id);
|
||||
}
|
||||
return reportData
|
||||
return reportData;
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
v-for="report in reports.filter(
|
||||
(x) =>
|
||||
(moderation || x.reporterUser.id === auth.user.id) &&
|
||||
(viewMode === 'open' ? x.open : !x.open)
|
||||
(viewMode === 'open' ? x.open : !x.open),
|
||||
)"
|
||||
:key="report.id"
|
||||
:report="report"
|
||||
@@ -17,9 +17,9 @@
|
||||
<p v-if="reports.length === 0">You don't have any active reports.</p>
|
||||
</template>
|
||||
<script setup>
|
||||
import Chips from '~/components/ui/Chips.vue'
|
||||
import ReportInfo from '~/components/ui/report/ReportInfo.vue'
|
||||
import { addReportMessage } from '~/helpers/threads.js'
|
||||
import Chips from "~/components/ui/Chips.vue";
|
||||
import ReportInfo from "~/components/ui/report/ReportInfo.vue";
|
||||
import { addReportMessage } from "~/helpers/threads.js";
|
||||
|
||||
defineProps({
|
||||
moderation: {
|
||||
@@ -30,68 +30,68 @@ defineProps({
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const viewMode = ref('open')
|
||||
const reports = ref([])
|
||||
const viewMode = ref("open");
|
||||
const reports = ref([]);
|
||||
|
||||
let { data: rawReports } = await useAsyncData('report', () => useBaseFetch('report'))
|
||||
let { data: rawReports } = await useAsyncData("report", () => useBaseFetch("report"));
|
||||
|
||||
rawReports = rawReports.value.map((report) => {
|
||||
report.item_id = report.item_id.replace(/"/g, '')
|
||||
return report
|
||||
})
|
||||
report.item_id = report.item_id.replace(/"/g, "");
|
||||
return report;
|
||||
});
|
||||
|
||||
const reporterUsers = rawReports.map((report) => report.reporter)
|
||||
const reporterUsers = rawReports.map((report) => report.reporter);
|
||||
const reportedUsers = rawReports
|
||||
.filter((report) => report.item_type === 'user')
|
||||
.map((report) => report.item_id)
|
||||
const versionReports = rawReports.filter((report) => report.item_type === 'version')
|
||||
const versionIds = [...new Set(versionReports.map((report) => report.item_id))]
|
||||
const userIds = [...new Set(reporterUsers.concat(reportedUsers))]
|
||||
.filter((report) => report.item_type === "user")
|
||||
.map((report) => report.item_id);
|
||||
const versionReports = rawReports.filter((report) => report.item_type === "version");
|
||||
const versionIds = [...new Set(versionReports.map((report) => report.item_id))];
|
||||
const userIds = [...new Set(reporterUsers.concat(reportedUsers))];
|
||||
const threadIds = [
|
||||
...new Set(rawReports.filter((report) => report.thread_id).map((report) => report.thread_id)),
|
||||
]
|
||||
];
|
||||
|
||||
const [{ data: users }, { data: versions }, { data: threads }] = await Promise.all([
|
||||
await useAsyncData(`users?ids=${JSON.stringify(userIds)}`, () =>
|
||||
useBaseFetch(`users?ids=${encodeURIComponent(JSON.stringify(userIds))}`)
|
||||
useBaseFetch(`users?ids=${encodeURIComponent(JSON.stringify(userIds))}`),
|
||||
),
|
||||
await useAsyncData(`versions?ids=${JSON.stringify(versionIds)}`, () =>
|
||||
useBaseFetch(`versions?ids=${encodeURIComponent(JSON.stringify(versionIds))}`)
|
||||
useBaseFetch(`versions?ids=${encodeURIComponent(JSON.stringify(versionIds))}`),
|
||||
),
|
||||
await useAsyncData(`threads?ids=${JSON.stringify(threadIds)}`, () =>
|
||||
useBaseFetch(`threads?ids=${encodeURIComponent(JSON.stringify(threadIds))}`)
|
||||
useBaseFetch(`threads?ids=${encodeURIComponent(JSON.stringify(threadIds))}`),
|
||||
),
|
||||
])
|
||||
]);
|
||||
|
||||
const reportedProjects = rawReports
|
||||
.filter((report) => report.item_type === 'project')
|
||||
.map((report) => report.item_id)
|
||||
const versionProjects = versions.value.map((version) => version.project_id)
|
||||
const projectIds = [...new Set(reportedProjects.concat(versionProjects))]
|
||||
.filter((report) => report.item_type === "project")
|
||||
.map((report) => report.item_id);
|
||||
const versionProjects = versions.value.map((version) => version.project_id);
|
||||
const projectIds = [...new Set(reportedProjects.concat(versionProjects))];
|
||||
|
||||
const { data: projects } = await useAsyncData(`projects?ids=${JSON.stringify(projectIds)}`, () =>
|
||||
useBaseFetch(`projects?ids=${encodeURIComponent(JSON.stringify(projectIds))}`)
|
||||
)
|
||||
useBaseFetch(`projects?ids=${encodeURIComponent(JSON.stringify(projectIds))}`),
|
||||
);
|
||||
|
||||
reports.value = rawReports.map((report) => {
|
||||
report.reporterUser = users.value.find((user) => user.id === report.reporter)
|
||||
if (report.item_type === 'user') {
|
||||
report.user = users.value.find((user) => user.id === report.item_id)
|
||||
} else if (report.item_type === 'project') {
|
||||
report.project = projects.value.find((project) => project.id === report.item_id)
|
||||
} else if (report.item_type === 'version') {
|
||||
report.version = versions.value.find((version) => version.id === report.item_id)
|
||||
report.project = projects.value.find((project) => project.id === report.version.project_id)
|
||||
report.reporterUser = users.value.find((user) => user.id === report.reporter);
|
||||
if (report.item_type === "user") {
|
||||
report.user = users.value.find((user) => user.id === report.item_id);
|
||||
} else if (report.item_type === "project") {
|
||||
report.project = projects.value.find((project) => project.id === report.item_id);
|
||||
} else if (report.item_type === "version") {
|
||||
report.version = versions.value.find((version) => version.id === report.item_id);
|
||||
report.project = projects.value.find((project) => project.id === report.version.project_id);
|
||||
}
|
||||
if (report.thread_id) {
|
||||
report.thread = addReportMessage(
|
||||
threads.value.find((thread) => report.thread_id === thread.id),
|
||||
report
|
||||
)
|
||||
report,
|
||||
);
|
||||
}
|
||||
report.open = true
|
||||
return report
|
||||
})
|
||||
report.open = true;
|
||||
return report;
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -15,7 +15,7 @@ export default {
|
||||
categories: {
|
||||
type: Array,
|
||||
default() {
|
||||
return []
|
||||
return [];
|
||||
},
|
||||
},
|
||||
type: {
|
||||
@@ -24,9 +24,9 @@ export default {
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const tags = useTags()
|
||||
const tags = useTags();
|
||||
|
||||
return { tags }
|
||||
return { tags };
|
||||
},
|
||||
computed: {
|
||||
categoriesFiltered() {
|
||||
@@ -34,11 +34,11 @@ export default {
|
||||
.concat(this.tags.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),
|
||||
);
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Checkbox from '~/components/ui/Checkbox.vue'
|
||||
import Checkbox from "~/components/ui/Checkbox.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -25,30 +25,30 @@ export default {
|
||||
props: {
|
||||
facetName: {
|
||||
type: String,
|
||||
default: '',
|
||||
default: "",
|
||||
},
|
||||
displayName: {
|
||||
type: String,
|
||||
default: '',
|
||||
default: "",
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: '',
|
||||
default: "",
|
||||
},
|
||||
activeFilters: {
|
||||
type: Array,
|
||||
default() {
|
||||
return []
|
||||
return [];
|
||||
},
|
||||
},
|
||||
},
|
||||
emits: ['toggle'],
|
||||
emits: ["toggle"],
|
||||
methods: {
|
||||
toggle() {
|
||||
this.$emit('toggle', this.facetName)
|
||||
this.$emit("toggle", this.facetName);
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -163,7 +163,7 @@
|
||||
id: 'withhold-reply',
|
||||
color: 'danger',
|
||||
action: () => {
|
||||
sendReply('withheld')
|
||||
sendReply('withheld');
|
||||
},
|
||||
hoverFilled: true,
|
||||
disabled: project.status === 'withheld',
|
||||
@@ -174,7 +174,7 @@
|
||||
id: 'withhold',
|
||||
color: 'danger',
|
||||
action: () => {
|
||||
setStatus('withheld')
|
||||
setStatus('withheld');
|
||||
},
|
||||
hoverFilled: true,
|
||||
disabled: project.status === 'withheld',
|
||||
@@ -196,22 +196,22 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { OverflowMenu, MarkdownEditor } from '@modrinth/ui'
|
||||
import { DropdownIcon } from '@modrinth/assets'
|
||||
import { useImageUpload } from '~/composables/image-upload.ts'
|
||||
import CopyCode from '~/components/ui/CopyCode.vue'
|
||||
import ReplyIcon from '~/assets/images/utils/reply.svg?component'
|
||||
import SendIcon from '~/assets/images/utils/send.svg?component'
|
||||
import CloseIcon from '~/assets/images/utils/check-circle.svg?component'
|
||||
import CrossIcon from '~/assets/images/utils/x.svg?component'
|
||||
import EyeOffIcon from '~/assets/images/utils/eye-off.svg?component'
|
||||
import CheckIcon from '~/assets/images/utils/check.svg?component'
|
||||
import ModerationIcon from '~/assets/images/sidebar/admin.svg?component'
|
||||
import ThreadMessage from '~/components/ui/thread/ThreadMessage.vue'
|
||||
import { isStaff } from '~/helpers/users.js'
|
||||
import { isApproved, isRejected } from '~/helpers/projects.js'
|
||||
import Modal from '~/components/ui/Modal.vue'
|
||||
import Checkbox from '~/components/ui/Checkbox.vue'
|
||||
import { OverflowMenu, MarkdownEditor } from "@modrinth/ui";
|
||||
import { DropdownIcon } from "@modrinth/assets";
|
||||
import { useImageUpload } from "~/composables/image-upload.ts";
|
||||
import CopyCode from "~/components/ui/CopyCode.vue";
|
||||
import ReplyIcon from "~/assets/images/utils/reply.svg?component";
|
||||
import SendIcon from "~/assets/images/utils/send.svg?component";
|
||||
import CloseIcon from "~/assets/images/utils/check-circle.svg?component";
|
||||
import CrossIcon from "~/assets/images/utils/x.svg?component";
|
||||
import EyeOffIcon from "~/assets/images/utils/eye-off.svg?component";
|
||||
import CheckIcon from "~/assets/images/utils/check.svg?component";
|
||||
import ModerationIcon from "~/assets/images/sidebar/admin.svg?component";
|
||||
import ThreadMessage from "~/components/ui/thread/ThreadMessage.vue";
|
||||
import { isStaff } from "~/helpers/users.js";
|
||||
import { isApproved, isRejected } from "~/helpers/projects.js";
|
||||
import Modal from "~/components/ui/Modal.vue";
|
||||
import Checkbox from "~/components/ui/Checkbox.vue";
|
||||
|
||||
const props = defineProps({
|
||||
thread: {
|
||||
@@ -236,166 +236,166 @@ const props = defineProps({
|
||||
currentMember: {
|
||||
type: Object,
|
||||
default() {
|
||||
return null
|
||||
return null;
|
||||
},
|
||||
},
|
||||
auth: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update-thread'])
|
||||
const emit = defineEmits(["update-thread"]);
|
||||
|
||||
const app = useNuxtApp()
|
||||
const flags = useFeatureFlags()
|
||||
const app = useNuxtApp();
|
||||
const flags = useFeatureFlags();
|
||||
|
||||
const members = computed(() => {
|
||||
const members = {}
|
||||
const members = {};
|
||||
for (const member of props.thread.members) {
|
||||
members[member.id] = member
|
||||
members[member.id] = member;
|
||||
}
|
||||
return members
|
||||
})
|
||||
return members;
|
||||
});
|
||||
|
||||
const replyBody = ref('')
|
||||
const replyBody = ref("");
|
||||
|
||||
const sortedMessages = computed(() => {
|
||||
if (props.thread !== null) {
|
||||
return props.thread.messages
|
||||
.slice()
|
||||
.sort((a, b) => app.$dayjs(a.created) - app.$dayjs(b.created))
|
||||
.sort((a, b) => app.$dayjs(a.created) - app.$dayjs(b.created));
|
||||
}
|
||||
return []
|
||||
})
|
||||
return [];
|
||||
});
|
||||
|
||||
const modalSubmit = ref(null)
|
||||
const modalSubmit = ref(null);
|
||||
|
||||
async function updateThreadLocal() {
|
||||
let threadId = null
|
||||
let threadId = null;
|
||||
if (props.project) {
|
||||
threadId = props.project.thread_id
|
||||
threadId = props.project.thread_id;
|
||||
} else if (props.report) {
|
||||
threadId = props.report.thread_id
|
||||
threadId = props.report.thread_id;
|
||||
}
|
||||
let thread = null
|
||||
let thread = null;
|
||||
if (threadId) {
|
||||
thread = await useBaseFetch(`thread/${threadId}`)
|
||||
thread = await useBaseFetch(`thread/${threadId}`);
|
||||
}
|
||||
emit('update-thread', thread)
|
||||
emit("update-thread", thread);
|
||||
}
|
||||
|
||||
const imageIDs = ref([])
|
||||
const imageIDs = ref([]);
|
||||
|
||||
async function onUploadImage(file) {
|
||||
const response = await useImageUpload(file, { context: 'thread_message' })
|
||||
const response = await useImageUpload(file, { context: "thread_message" });
|
||||
|
||||
imageIDs.value.push(response.id)
|
||||
imageIDs.value.push(response.id);
|
||||
// Keep the last 10 entries of image IDs
|
||||
imageIDs.value = imageIDs.value.slice(-10)
|
||||
imageIDs.value = imageIDs.value.slice(-10);
|
||||
|
||||
return response.url
|
||||
return response.url;
|
||||
}
|
||||
|
||||
async function sendReply(status = null, privateMessage = false) {
|
||||
try {
|
||||
const body = {
|
||||
body: {
|
||||
type: 'text',
|
||||
type: "text",
|
||||
body: replyBody.value,
|
||||
private: privateMessage,
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
if (imageIDs.value.length > 0) {
|
||||
body.body = {
|
||||
...body.body,
|
||||
uploaded_images: imageIDs.value,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
await useBaseFetch(`thread/${props.thread.id}`, {
|
||||
method: 'POST',
|
||||
method: "POST",
|
||||
body,
|
||||
})
|
||||
});
|
||||
|
||||
replyBody.value = ''
|
||||
replyBody.value = "";
|
||||
|
||||
await updateThreadLocal()
|
||||
await updateThreadLocal();
|
||||
if (status !== null) {
|
||||
props.setStatus(status)
|
||||
props.setStatus(status);
|
||||
}
|
||||
} catch (err) {
|
||||
app.$notify({
|
||||
group: 'main',
|
||||
title: 'Error sending message',
|
||||
group: "main",
|
||||
title: "Error sending message",
|
||||
text: err.data ? err.data.description : err,
|
||||
type: 'error',
|
||||
})
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function closeReport(reply) {
|
||||
if (reply) {
|
||||
await sendReply()
|
||||
await sendReply();
|
||||
}
|
||||
|
||||
try {
|
||||
await useBaseFetch(`report/${props.report.id}`, {
|
||||
method: 'PATCH',
|
||||
method: "PATCH",
|
||||
body: {
|
||||
closed: true,
|
||||
},
|
||||
})
|
||||
await updateThreadLocal()
|
||||
});
|
||||
await updateThreadLocal();
|
||||
} catch (err) {
|
||||
app.$notify({
|
||||
group: 'main',
|
||||
title: 'Error closing report',
|
||||
group: "main",
|
||||
title: "Error closing report",
|
||||
text: err.data ? err.data.description : err,
|
||||
type: 'error',
|
||||
})
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function reopenReport() {
|
||||
try {
|
||||
await useBaseFetch(`report/${props.report.id}`, {
|
||||
method: 'PATCH',
|
||||
method: "PATCH",
|
||||
body: {
|
||||
closed: false,
|
||||
},
|
||||
})
|
||||
await updateThreadLocal()
|
||||
});
|
||||
await updateThreadLocal();
|
||||
} catch (err) {
|
||||
app.$notify({
|
||||
group: 'main',
|
||||
title: 'Error reopening report',
|
||||
group: "main",
|
||||
title: "Error reopening report",
|
||||
text: err.data ? err.data.description : err,
|
||||
type: 'error',
|
||||
})
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const replyWithSubmission = ref(false)
|
||||
const submissionConfirmation = ref(false)
|
||||
const replyWithSubmission = ref(false);
|
||||
const submissionConfirmation = ref(false);
|
||||
|
||||
function openResubmitModal(reply) {
|
||||
submissionConfirmation.value = false
|
||||
replyWithSubmission.value = reply
|
||||
modalSubmit.value.show()
|
||||
submissionConfirmation.value = false;
|
||||
replyWithSubmission.value = reply;
|
||||
modalSubmit.value.show();
|
||||
}
|
||||
|
||||
async function resubmit() {
|
||||
if (replyWithSubmission.value) {
|
||||
await sendReply('processing')
|
||||
await sendReply("processing");
|
||||
} else {
|
||||
await props.setStatus('processing')
|
||||
await props.setStatus("processing");
|
||||
}
|
||||
modalSubmit.value.hide()
|
||||
modalSubmit.value.hide();
|
||||
}
|
||||
|
||||
const requestedStatus = computed(() => props.project.requested_status ?? 'approved')
|
||||
const requestedStatus = computed(() => props.project.requested_status ?? "approved");
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -106,12 +106,12 @@ import {
|
||||
LockIcon,
|
||||
ModrinthIcon,
|
||||
ScaleIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { OverflowMenu, ConditionalNuxtLink } from '@modrinth/ui'
|
||||
import { renderString } from '@modrinth/utils'
|
||||
import Avatar from '~/components/ui/Avatar.vue'
|
||||
import Badge from '~/components/ui/Badge.vue'
|
||||
import { isStaff } from '~/helpers/users.js'
|
||||
} from "@modrinth/assets";
|
||||
import { OverflowMenu, ConditionalNuxtLink } from "@modrinth/ui";
|
||||
import { renderString } from "@modrinth/utils";
|
||||
import Avatar from "~/components/ui/Avatar.vue";
|
||||
import Badge from "~/components/ui/Badge.vue";
|
||||
import { isStaff } from "~/helpers/users.js";
|
||||
|
||||
const props = defineProps({
|
||||
message: {
|
||||
@@ -142,34 +142,34 @@ const props = defineProps({
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update-thread'])
|
||||
const emit = defineEmits(["update-thread"]);
|
||||
|
||||
const formattedMessage = computed(() => {
|
||||
const body = renderString(props.message.body.body)
|
||||
const body = renderString(props.message.body.body);
|
||||
if (props.forceCompact) {
|
||||
const hasImage = body.includes('<img')
|
||||
const noHtml = body.replace(/<\/?[^>]+(>|$)/g, '')
|
||||
const hasImage = body.includes("<img");
|
||||
const noHtml = body.replace(/<\/?[^>]+(>|$)/g, "");
|
||||
if (noHtml.trim()) {
|
||||
return noHtml
|
||||
return noHtml;
|
||||
} else if (hasImage) {
|
||||
return 'sent an image.'
|
||||
return "sent an image.";
|
||||
} else {
|
||||
return 'sent a message.'
|
||||
return "sent a message.";
|
||||
}
|
||||
}
|
||||
return body
|
||||
})
|
||||
return body;
|
||||
});
|
||||
|
||||
const formatRelativeTime = useRelativeTime()
|
||||
const timeSincePosted = ref(formatRelativeTime(props.message.created))
|
||||
const formatRelativeTime = useRelativeTime();
|
||||
const timeSincePosted = ref(formatRelativeTime(props.message.created));
|
||||
|
||||
async function deleteMessage() {
|
||||
await useBaseFetch(`message/${props.message.id}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
emit('update-thread')
|
||||
method: "DELETE",
|
||||
});
|
||||
emit("update-thread");
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -194,9 +194,9 @@ async function deleteMessage() {
|
||||
--gap-size: var(--spacing-card-sm);
|
||||
display: grid;
|
||||
grid-template:
|
||||
'icon author actions'
|
||||
'icon body actions'
|
||||
'date date date';
|
||||
"icon author actions"
|
||||
"icon body actions"
|
||||
"date date date";
|
||||
grid-template-columns: min-content auto 1fr;
|
||||
column-gap: var(--gap-size);
|
||||
row-gap: var(--spacing-card-xs);
|
||||
@@ -312,9 +312,9 @@ role-moderator {
|
||||
|
||||
&.has-body {
|
||||
grid-template:
|
||||
'icon author actions'
|
||||
'icon body actions'
|
||||
'date date date';
|
||||
"icon author actions"
|
||||
"icon body actions"
|
||||
"date date date";
|
||||
grid-template-columns: min-content auto 1fr;
|
||||
}
|
||||
}
|
||||
@@ -327,8 +327,8 @@ role-moderator {
|
||||
|
||||
&.has-body {
|
||||
grid-template:
|
||||
'icon author date actions'
|
||||
'icon body body actions';
|
||||
"icon author date actions"
|
||||
"icon body body actions";
|
||||
grid-template-columns: min-content auto 1fr;
|
||||
grid-template-rows: min-content 1fr auto;
|
||||
}
|
||||
|
||||
@@ -24,8 +24,8 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ChevronRightIcon from '~/assets/images/utils/chevron-right.svg?component'
|
||||
import ThreadMessage from '~/components/ui/thread/ThreadMessage.vue'
|
||||
import ChevronRightIcon from "~/assets/images/utils/chevron-right.svg?component";
|
||||
import ThreadMessage from "~/components/ui/thread/ThreadMessage.vue";
|
||||
|
||||
const props = defineProps({
|
||||
thread: {
|
||||
@@ -49,36 +49,36 @@ const props = defineProps({
|
||||
type: Array,
|
||||
required: false,
|
||||
default() {
|
||||
return []
|
||||
return [];
|
||||
},
|
||||
},
|
||||
auth: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const app = useNuxtApp()
|
||||
const app = useNuxtApp();
|
||||
|
||||
const members = computed(() => {
|
||||
const members = {}
|
||||
const members = {};
|
||||
for (const member of props.thread.members) {
|
||||
members[member.id] = member
|
||||
members[member.id] = member;
|
||||
}
|
||||
members[props.auth.user.id] = props.auth.user
|
||||
return members
|
||||
})
|
||||
members[props.auth.user.id] = props.auth.user;
|
||||
return members;
|
||||
});
|
||||
|
||||
const displayMessages = computed(() => {
|
||||
const sortedMessages = props.thread.messages
|
||||
.slice()
|
||||
.sort((a, b) => app.$dayjs(a.created) - app.$dayjs(b.created))
|
||||
.sort((a, b) => app.$dayjs(a.created) - app.$dayjs(b.created));
|
||||
if (props.messages.length > 0) {
|
||||
return sortedMessages.filter((msg) => props.messages.includes(msg.id))
|
||||
return sortedMessages.filter((msg) => props.messages.includes(msg.id));
|
||||
} else {
|
||||
return sortedMessages.length > 0 ? [sortedMessages[sortedMessages.length - 1]] : []
|
||||
return sortedMessages.length > 0 ? [sortedMessages[sortedMessages.length - 1]] : [];
|
||||
}
|
||||
})
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
Reference in New Issue
Block a user