Files
AstralRinth/apps/frontend/src/pages/report.vue

295 lines
7.6 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 aria-hidden="true" />
Submit
</Button>
</div>
</div>
</Card>
</div>
</template>
<script setup lang="ts">
import { Card, Button, MarkdownEditor, DropdownSelect } from "@modrinth/ui";
import { SaveIcon } from "@modrinth/assets";
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>