Modrinth/pages/report.vue
Sasha Sorokin 34fd9d29c8
Update Nuxt to v3.12.1 (#1720)
* Update Nuxt dependencies

* Fix ref access in ChartDisplay

* Fix feature flags cookie options type error

* Specify type-only imports

* Fix shorthands access to tags outside of reactive scope

* Replace most useRoute calls with useRoute from vue-router

Nuxt's version of this composable is horrendously broken (nuxt/nuxt#21340)

* Import all svgs with ?component parameter

Fixes weird hydration issues + gives correct type
2024-06-14 13:23:02 -07:00

294 lines
7.5 KiB
Vue

<template>
<div class="page">
<Card>
<div class="content">
<div>
<h1 class="card-title-adjustments">Submit a Report</h1>
<div>
<p>
Modding should be safe for everyone, so we take abuse and malicious intent seriously
at Modrinth. If you encounter content that violates our
<nuxt-link class="text-link" to="/legal/terms">Terms of Service</nuxt-link> or our
<nuxt-link class="text-link" to="/legal/rules">Rules</nuxt-link>, please report it to
us here.
</p>
<p>
This form is intended exclusively for reporting abuse or harmful content to Modrinth
staff. For bugs related to specific projects, please use the project's designated
Issues link or Discord channel.
</p>
<p>
Your privacy is important to us; rest assured that your identifying information will
be kept confidential.
</p>
</div>
</div>
<div class="report-info-section">
<div class="report-info-item">
<label for="report-item">Item type to report</label>
<DropdownSelect
id="report-item"
v-model="reportItem"
name="report-item"
:options="reportItems"
:display-name="capitalizeString"
:multiple="false"
:searchable="false"
:show-no-results="false"
:show-labels="false"
placeholder="Choose report item"
/>
</div>
<div class="report-info-item">
<label for="report-item-id">Item ID</label>
<input
id="report-item-id"
v-model="reportItemID"
type="text"
placeholder="ex. project ID"
autocomplete="off"
:disabled="reportItem === ''"
/>
</div>
<div class="report-info-item">
<label for="report-type">Reason for report</label>
<DropdownSelect
id="report-type"
v-model="reportType"
name="report-type"
:options="reportTypes"
:multiple="false"
:searchable="false"
:show-no-results="false"
:show-labels="false"
:display-name="capitalizeString"
placeholder="Choose report type"
/>
</div>
</div>
<div class="report-submission-section">
<div>
<p>
Please provide additional context about your report. Include links and images if
possible. <strong>Empty reports will be closed.</strong>
</p>
</div>
<MarkdownEditor v-model="reportBody" placeholder="" :on-image-upload="onImageUpload" />
</div>
<div class="submit-button">
<Button
id="submit-button"
color="primary"
:disabled="submitLoading || !canSubmit"
@click="submitReport"
>
<SaveIcon />
Submit
</Button>
</div>
</div>
</Card>
</div>
</template>
<script setup lang="ts">
import { Card, Button, MarkdownEditor, DropdownSelect, SaveIcon } from 'omorphia'
import { useImageUpload } from '~/composables/image-upload.ts'
const tags = useTags()
const route = useNativeRoute()
const accessQuery = (id: string): string => {
return route.query?.[id]?.toString() || ''
}
const submitLoading = ref<boolean>(false)
const uploadedImageIDs = ref<string[]>([])
const reportBody = ref<string>(accessQuery('body'))
const reportItem = ref<string>(accessQuery('item'))
const reportItemID = ref<string>(accessQuery('itemID'))
const reportType = ref<string>('')
const reportItems = ['project', 'version', 'user']
const reportTypes = computed(() => tags.value.reportTypes)
const canSubmit = computed(() => {
return (
reportItem.value !== '' &&
reportItemID.value !== '' &&
reportType.value !== '' &&
reportBody.value !== ''
)
})
const submissionValidation = () => {
if (!canSubmit.value) {
throw new Error('Please fill out all required fields')
}
if (reportItem.value === '') {
throw new Error('Please select a report item')
}
if (reportItemID.value === '') {
throw new Error('Please enter a report item ID')
}
if (reportType.value === '') {
throw new Error('Please select a report type')
}
if (reportBody.value === '') {
throw new Error('Please enter a report body')
}
return true
}
const capitalizeString = (value?: string) => {
if (!value) return ''
return value?.charAt(0).toUpperCase() + value?.slice(1)
}
const submitReport = async () => {
submitLoading.value = true
let data: {
[key: string]: unknown
} = {
report_type: reportType.value,
item_type: reportItem.value,
item_id: reportItemID.value,
body: reportBody.value,
}
function takeNLast<T>(arr: T[], n: number): T[] {
return arr.slice(Math.max(arr.length - n, 0))
}
if (uploadedImageIDs.value.length > 0) {
data = {
...data,
uploaded_images: takeNLast(uploadedImageIDs.value, 10),
}
}
try {
submissionValidation()
} catch (error) {
submitLoading.value = false
if (error instanceof Error) {
addNotification({
group: 'main',
title: 'An error occurred',
text: error.message,
type: 'error',
})
}
return
}
try {
const response = (await useBaseFetch('report', {
method: 'POST',
body: data,
})) as { id: string }
submitLoading.value = false
if (response?.id) {
navigateTo(`/dashboard/report/${response.id}`)
}
} catch (error) {
submitLoading.value = false
if (error instanceof Error) {
addNotification({
group: 'main',
title: 'An error occurred',
text: error.message,
type: 'error',
})
}
throw error
}
}
const onImageUpload = async (file: File) => {
const item = await useImageUpload(file, { context: 'report' })
uploadedImageIDs.value.push(item.id)
return item.url
}
</script>
<style scoped lang="scss">
.submit-button {
display: flex;
justify-content: flex-end;
width: 100%;
margin-top: var(--spacing-card-md);
}
.card-title-adjustments {
margin-block: var(--spacing-card-md) var(--spacing-card-sm);
}
.page {
padding: 0.5rem;
margin-left: auto;
margin-right: auto;
max-width: 56rem;
}
.content {
// TODO: Get rid of this hack when removing global styles from the website.
// Overflow decides the behavior of md editor but also clips the border.
// In the future, we should use ring instead of block-shadow for the
// green ring around the md editor
padding-inline: var(--gap-md);
padding-bottom: var(--gap-md);
margin-inline: calc(var(--gap-md) * -1);
display: grid;
// Disable horizontal stretch
grid-template-columns: minmax(0, 1fr);
overflow: hidden;
}
.report-info-section {
display: block;
width: 100%;
gap: var(--gap-md);
:global(.animated-dropdown) {
& > .selected {
height: 40px;
}
}
.report-info-item {
display: block;
width: 100%;
max-width: 100%;
label {
display: block;
margin-bottom: var(--gap-sm);
color: var(--color-text-dark);
font-size: var(--font-size-md);
font-weight: var(--font-weight-bold);
margin-block: var(--spacing-card-md) var(--spacing-card-sm);
}
}
}
</style>