mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2025-12-29 11:26:13 +08:00
Add quote reporting feature (#2372) by Bruception
* Add initial quote reporting logic * Remove report status * Add initial frontend * Add submit logic * Add report quote button to rating popup * Refactor quoteId argument * Remove console log * Add captcha to request validation schema * Use captcha id for value and reset * Update report data schema * Hide report popup on complete * quote search styling update * updated report quote styling * tofixed * tofixed * moved report button to the result screen styling changes * resetting captcha after hiding to avoid ugly animation * select2 styling update * lowercase Co-authored-by: Miodec <bartnikjack@gmail.com>
This commit is contained in:
parent
776b0290ec
commit
974e50ec48
16 changed files with 435 additions and 48 deletions
40
backend/api/controllers/quotes.js
Normal file
40
backend/api/controllers/quotes.js
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
const { v4: uuidv4 } = require("uuid");
|
||||
const ReportDAO = require("../../dao/report");
|
||||
const UserDAO = require("../../dao/user");
|
||||
const MonkeyError = require("../../handlers/error");
|
||||
const Captcha = require("../../handlers/captcha");
|
||||
|
||||
class QuotesController {
|
||||
static async reportQuote(req, res) {
|
||||
const { uid } = req.decodedToken;
|
||||
|
||||
const user = await UserDAO.getUser(uid);
|
||||
if (user.cannotReport) {
|
||||
throw new MonkeyError(403, "You don't have permission to do this.");
|
||||
}
|
||||
|
||||
const { quoteId, quoteLanguage, reason, comment, captcha } = req.body;
|
||||
|
||||
if (!(await Captcha.verify(captcha))) {
|
||||
throw new MonkeyError(400, "Captcha check failed.");
|
||||
}
|
||||
|
||||
const newReport = {
|
||||
id: uuidv4(),
|
||||
type: "quote",
|
||||
timestamp: new Date().getTime(),
|
||||
uid,
|
||||
details: {
|
||||
contentId: `${quoteLanguage}-${quoteId}`,
|
||||
reason,
|
||||
comment,
|
||||
},
|
||||
};
|
||||
|
||||
await ReportDAO.createReport(newReport);
|
||||
|
||||
res.sendStatus(200);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = QuotesController;
|
||||
|
|
@ -3,8 +3,12 @@ const { authenticateRequest } = require("../../middlewares/auth");
|
|||
const { Router } = require("express");
|
||||
const NewQuotesController = require("../controllers/new-quotes");
|
||||
const QuoteRatingsController = require("../controllers/quote-ratings");
|
||||
const QuotesController = require("../controllers/quotes");
|
||||
const RateLimit = require("../../middlewares/rate-limit");
|
||||
const { requestValidation } = require("../../middlewares/apiUtils");
|
||||
const {
|
||||
asyncHandlerWrapper,
|
||||
requestValidation,
|
||||
} = require("../../middlewares/apiUtils");
|
||||
const SUPPORTED_QUOTE_LANGUAGES = require("../../constants/quoteLanguages");
|
||||
|
||||
const quotesRouter = Router();
|
||||
|
|
@ -71,11 +75,10 @@ quotesRouter.post(
|
|||
)
|
||||
.required(),
|
||||
comment: joi.string().allow("").max(250).required(),
|
||||
captcha: joi.string().required(),
|
||||
},
|
||||
}),
|
||||
(req, res) => {
|
||||
res.sendStatus(200);
|
||||
}
|
||||
asyncHandlerWrapper(QuotesController.reportQuote)
|
||||
);
|
||||
|
||||
module.exports = quotesRouter;
|
||||
|
|
|
|||
29
backend/dao/report.js
Normal file
29
backend/dao/report.js
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
const MonkeyError = require("../handlers/error");
|
||||
const { mongoDB } = require("../init/mongodb");
|
||||
|
||||
const MAX_REPORTS = 100;
|
||||
|
||||
class ReportDAO {
|
||||
static async createReport(report) {
|
||||
const reports = await mongoDB().collection("reports").find().toArray();
|
||||
|
||||
if (reports.length >= MAX_REPORTS) {
|
||||
throw new MonkeyError(
|
||||
503,
|
||||
"Reports are not being accepted at this time. Please try again later."
|
||||
);
|
||||
}
|
||||
|
||||
const reportAlreadyExists = reports.find((existingReport) => {
|
||||
return existingReport.details.contentId === report.details.contentId;
|
||||
});
|
||||
|
||||
if (reportAlreadyExists) {
|
||||
throw new MonkeyError(409, "A report for this content already exists.");
|
||||
}
|
||||
|
||||
await mongoDB().collection("reports").insertOne(report);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ReportDAO;
|
||||
|
|
@ -1,11 +1,33 @@
|
|||
const joi = require("joi");
|
||||
const MonkeyError = require("../handlers/error");
|
||||
|
||||
/**
|
||||
* This utility serves as an alternative to wrapping express handlers with try/catch statements.
|
||||
* Any routes that use an async handler function should wrap the handler with this function.
|
||||
* Without this, any errors thrown will not be caught by the error handling middleware, and
|
||||
* the app will hang!
|
||||
*/
|
||||
function asyncHandlerWrapper(handler) {
|
||||
return async (req, res, next) => {
|
||||
try {
|
||||
const handlerData = await handler(req, res);
|
||||
if (!res.headersSent && handlerData) {
|
||||
res.json(handlerData);
|
||||
}
|
||||
next();
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function requestValidation(validationSchema) {
|
||||
return (req, res, next) => {
|
||||
// In dev environments, as an alternative to token authentication,
|
||||
// you can pass the authentication middleware by having a user id in the body.
|
||||
// Inject the user id into the schema so that validation will not fail.
|
||||
/**
|
||||
* In dev environments, as an alternative to token authentication,
|
||||
* you can pass the authentication middleware by having a user id in the body.
|
||||
* Inject the user id into the schema so that validation will not fail.
|
||||
*/
|
||||
if (process.env.MODE === "dev") {
|
||||
validationSchema.body = {
|
||||
uid: joi.any(),
|
||||
|
|
@ -28,5 +50,6 @@ function requestValidation(validationSchema) {
|
|||
}
|
||||
|
||||
module.exports = {
|
||||
asyncHandlerWrapper,
|
||||
requestValidation,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -142,6 +142,7 @@ const refactoredSrc = [
|
|||
"./src/js/popups/quote-search-popup.js",
|
||||
"./src/js/popups/quote-submit-popup.js",
|
||||
"./src/js/popups/quote-approve-popup.js",
|
||||
"./src/js/popups/quote-report-popup.js",
|
||||
"./src/js/popups/rate-quote-popup.js",
|
||||
"./src/js/popups/version-popup.js",
|
||||
"./src/js/popups/support-popup.js",
|
||||
|
|
|
|||
|
|
@ -666,6 +666,7 @@ $(document).keydown((event) => {
|
|||
!$("#quoteSearchPopupWrapper").hasClass("hidden") ||
|
||||
!$("#quoteSubmitPopupWrapper").hasClass("hidden") ||
|
||||
!$("#quoteApprovePopupWrapper").hasClass("hidden") ||
|
||||
!$("#quoteReportPopupWrapper").hasClass("hidden") ||
|
||||
!$("#wordFilterPopupWrapper").hasClass("hidden");
|
||||
|
||||
const allowTyping =
|
||||
|
|
|
|||
138
src/js/popups/quote-report-popup.js
Normal file
138
src/js/popups/quote-report-popup.js
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
import * as Misc from "./misc";
|
||||
import * as Notifications from "./notifications";
|
||||
import axiosInstance from "./axios-instance";
|
||||
import Config from "./config";
|
||||
import * as Loader from "./loader";
|
||||
|
||||
const CAPTCHA_ID = 1;
|
||||
|
||||
const state = {
|
||||
previousPopupShowCallback: undefined,
|
||||
quoteToReport: undefined,
|
||||
};
|
||||
|
||||
const defaultOptions = {
|
||||
quoteId: -1,
|
||||
previousPopupShowCallback: () => {},
|
||||
noAnim: false,
|
||||
};
|
||||
|
||||
export async function show(options = defaultOptions) {
|
||||
if ($("#quoteReportPopupWrapper").hasClass("hidden")) {
|
||||
const { quoteId, previousPopupShowCallback, noAnim } = options;
|
||||
|
||||
state.previousPopupShowCallback = previousPopupShowCallback;
|
||||
|
||||
const { quotes } = await Misc.getQuotes(Config.language);
|
||||
state.quoteToReport = quotes.find((quote) => {
|
||||
return quote.id === quoteId;
|
||||
});
|
||||
|
||||
$("#quoteReportPopup .quote").text(state.quoteToReport.text);
|
||||
$("#quoteReportPopup .reason").val("Grammatical error");
|
||||
$("#quoteReportPopup .comment").val("");
|
||||
$("#quoteReportPopup .characterCount").text("-");
|
||||
$("#quoteReportPopup .reason").select2({
|
||||
minimumResultsForSearch: Infinity,
|
||||
});
|
||||
$("#quoteReportPopupWrapper")
|
||||
.stop(true, true)
|
||||
.css("opacity", 0)
|
||||
.removeClass("hidden")
|
||||
.animate({ opacity: 1 }, noAnim ? 0 : 100, (e) => {
|
||||
$("#quoteReportPopup textarea").focus().select();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function hide() {
|
||||
if (!$("#quoteReportPopupWrapper").hasClass("hidden")) {
|
||||
$("#quoteReportPopupWrapper")
|
||||
.stop(true, true)
|
||||
.css("opacity", 1)
|
||||
.animate(
|
||||
{
|
||||
opacity: 0,
|
||||
},
|
||||
100,
|
||||
(e) => {
|
||||
grecaptcha.reset(CAPTCHA_ID);
|
||||
$("#quoteReportPopupWrapper").addClass("hidden");
|
||||
if (state.previousPopupShowCallback) {
|
||||
state.previousPopupShowCallback();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function submitReport() {
|
||||
const captchaResponse = grecaptcha.getResponse(CAPTCHA_ID);
|
||||
if (!captchaResponse) {
|
||||
Notifications.add("Please complete the captcha.");
|
||||
return;
|
||||
}
|
||||
|
||||
const requestBody = {
|
||||
quoteId: state.quoteToReport.id.toString(),
|
||||
quoteLanguage: Config.language,
|
||||
reason: $("#quoteReportPopup .reason").val(),
|
||||
comment: $("#quoteReportPopup .comment").val(),
|
||||
captcha: captchaResponse,
|
||||
};
|
||||
|
||||
if (!requestBody.reason) {
|
||||
Notifications.add("Please select a valid report reason.");
|
||||
return;
|
||||
}
|
||||
|
||||
const characterDifference = requestBody.comment.length - 250;
|
||||
if (characterDifference > 0) {
|
||||
Notifications.add(
|
||||
`Report comment is ${characterDifference} character(s) too long.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
Loader.show();
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await axiosInstance.post("/quotes/report", requestBody);
|
||||
} catch (e) {
|
||||
Loader.hide();
|
||||
let msg = e?.response?.data?.message ?? e.message;
|
||||
Notifications.add("Failed to report quote: " + msg, -1);
|
||||
return;
|
||||
}
|
||||
|
||||
Loader.hide();
|
||||
if (response.status !== 200) {
|
||||
Notifications.add(response.data.message);
|
||||
} else {
|
||||
Notifications.add("Report submitted. Thank you!", 1);
|
||||
hide();
|
||||
}
|
||||
}
|
||||
|
||||
$("#quoteReportPopupWrapper").on("mousedown", (e) => {
|
||||
if ($(e.target).attr("id") === "quoteReportPopupWrapper") {
|
||||
hide();
|
||||
}
|
||||
});
|
||||
|
||||
$("#quoteReportPopup .comment").on("input", (e) => {
|
||||
setTimeout(() => {
|
||||
const len = $("#quoteReportPopup .comment").val().length;
|
||||
$("#quoteReportPopup .characterCount").text(len);
|
||||
if (len > 250) {
|
||||
$("#quoteReportPopup .characterCount").addClass("red");
|
||||
} else {
|
||||
$("#quoteReportPopup .characterCount").removeClass("red");
|
||||
}
|
||||
}, 1);
|
||||
});
|
||||
|
||||
$("#quoteReportPopup .submit").on("click", async (e) => {
|
||||
await submitReport();
|
||||
});
|
||||
|
|
@ -5,6 +5,7 @@ import * as ManualRestart from "./manual-restart-tracker";
|
|||
import * as TestLogic from "./test-logic";
|
||||
import * as QuoteSubmitPopup from "./quote-submit-popup";
|
||||
import * as QuoteApprovePopup from "./quote-approve-popup";
|
||||
import * as QuoteReportPopup from "./quote-report-popup";
|
||||
import * as DB from "./db";
|
||||
import * as TestUI from "./test-ui";
|
||||
|
||||
|
|
@ -40,6 +41,8 @@ async function updateResults(searchText) {
|
|||
let resultsList = $("#quoteSearchResults");
|
||||
let resultListLength = 0;
|
||||
|
||||
const isNotAuthed = !firebase.auth().currentUser;
|
||||
|
||||
found.forEach(async (quote) => {
|
||||
let lengthDesc;
|
||||
if (quote.length < 101) {
|
||||
|
|
@ -55,10 +58,16 @@ async function updateResults(searchText) {
|
|||
resultsList.append(`
|
||||
<div class="searchResult" id="${quote.id}">
|
||||
<div class="text">${quote.text}</div>
|
||||
<div class="id"><div class="sub">id</div>${quote.id}</div>
|
||||
<div class="id"><div class="sub">id</div><span class="quote-id">${
|
||||
quote.id
|
||||
}</span></div>
|
||||
<div class="length"><div class="sub">length</div>${lengthDesc}</div>
|
||||
<div class="source"><div class="sub">source</div>${quote.source}</div>
|
||||
<div class="resultChevron"><i class="fas fa-chevron-right"></i></div>
|
||||
<div class="icon-button report ${
|
||||
isNotAuthed && "hidden"
|
||||
}" aria-label="Report quote" data-balloon-pos="left">
|
||||
<i class="fas fa-flag"></i>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
|
@ -73,9 +82,13 @@ async function updateResults(searchText) {
|
|||
}
|
||||
}
|
||||
|
||||
export async function show() {
|
||||
export async function show(clearText = true) {
|
||||
if ($("#quoteSearchPopupWrapper").hasClass("hidden")) {
|
||||
$("#quoteSearchPopup input").val("");
|
||||
if (clearText) {
|
||||
$("#quoteSearchPopup input").val("");
|
||||
}
|
||||
|
||||
const quoteSearchInputValue = $("#quoteSearchPopup input").val();
|
||||
|
||||
if (!firebase.auth().currentUser) {
|
||||
$("#quoteSearchPopup #gotoSubmitQuoteButton").addClass("hidden");
|
||||
|
|
@ -94,13 +107,15 @@ export async function show() {
|
|||
.css("opacity", 0)
|
||||
.removeClass("hidden")
|
||||
.animate({ opacity: 1 }, 100, (e) => {
|
||||
$("#quoteSearchPopup input").focus().select();
|
||||
updateResults("");
|
||||
if (clearText) {
|
||||
$("#quoteSearchPopup input").focus().select();
|
||||
}
|
||||
updateResults(quoteSearchInputValue);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function hide(noAnim = false) {
|
||||
export function hide(noAnim = false, focusWords = true) {
|
||||
if (!$("#quoteSearchPopupWrapper").hasClass("hidden")) {
|
||||
$("#quoteSearchPopupWrapper")
|
||||
.stop(true, true)
|
||||
|
|
@ -112,7 +127,9 @@ export function hide(noAnim = false) {
|
|||
noAnim ? 0 : 100,
|
||||
(e) => {
|
||||
$("#quoteSearchPopupWrapper").addClass("hidden");
|
||||
TestUI.focusWords();
|
||||
if (focusWords) {
|
||||
TestUI.focusWords();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
@ -155,6 +172,9 @@ $(document).on(
|
|||
"click",
|
||||
"#quoteSearchPopup #quoteSearchResults .searchResult",
|
||||
(e) => {
|
||||
if (e.target.classList.contains("report")) {
|
||||
return;
|
||||
}
|
||||
selectedId = parseInt($(e.currentTarget).attr("id"));
|
||||
apply(selectedId);
|
||||
}
|
||||
|
|
@ -170,6 +190,20 @@ $(document).on("click", "#quoteSearchPopup #goToApproveQuotes", (e) => {
|
|||
QuoteApprovePopup.show(true);
|
||||
});
|
||||
|
||||
$(document).on("click", "#quoteSearchPopup .report", async (e) => {
|
||||
const quoteId = e.target.closest(".searchResult").id;
|
||||
const quoteIdSelectedForReport = parseInt(quoteId);
|
||||
|
||||
hide(true, false);
|
||||
QuoteReportPopup.show({
|
||||
quoteId: quoteIdSelectedForReport,
|
||||
noAnim: true,
|
||||
previousPopupShowCallback: () => {
|
||||
show(false);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// $("#quoteSearchPopup input").keypress((e) => {
|
||||
// if (e.keyCode == 13) {
|
||||
// if (!isNaN(document.getElementById("searchBox").value)) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import * as DB from "./db";
|
||||
import * as Loader from "./loader";
|
||||
import * as Notifications from "./notifications";
|
||||
import * as QuoteReportPopup from "./quote-report-popup";
|
||||
import axiosInstance from "./axios-instance";
|
||||
|
||||
let rating = 0;
|
||||
|
|
@ -80,9 +81,11 @@ function updateData() {
|
|||
updateRatingStats();
|
||||
}
|
||||
|
||||
export function show(quote) {
|
||||
export function show(quote, shouldReset = true) {
|
||||
if ($("#rateQuotePopupWrapper").hasClass("hidden")) {
|
||||
reset();
|
||||
if (shouldReset) {
|
||||
reset();
|
||||
}
|
||||
|
||||
currentQuote = quote;
|
||||
rating = 0;
|
||||
|
|
@ -92,6 +95,7 @@ export function show(quote) {
|
|||
if (alreadyRated) {
|
||||
rating = alreadyRated;
|
||||
}
|
||||
|
||||
refreshStars();
|
||||
updateData();
|
||||
$("#rateQuotePopupWrapper")
|
||||
|
|
@ -181,7 +185,9 @@ async function submit() {
|
|||
quoteStats.average = (
|
||||
Math.round((quoteStats.totalRating / quoteStats.ratings) * 10) / 10
|
||||
).toFixed(1);
|
||||
$(".pageTest #result #rateQuoteButton .rating").text(quoteStats.average);
|
||||
$(".pageTest #result #rateQuoteButton .rating").text(
|
||||
quoteStats.average.toFixed(1)
|
||||
);
|
||||
$(".pageTest #result #rateQuoteButton .icon").removeClass("far");
|
||||
$(".pageTest #result #rateQuoteButton .icon").addClass("fas");
|
||||
}
|
||||
|
|
@ -211,3 +217,14 @@ $("#rateQuotePopup .stars .star").mouseout((e) => {
|
|||
$("#rateQuotePopup .submitButton").click((e) => {
|
||||
submit();
|
||||
});
|
||||
|
||||
$("#rateQuotePopup #reportQuoteButton").click((e) => {
|
||||
hide();
|
||||
QuoteReportPopup.show({
|
||||
quoteId: parseInt(currentQuote.id),
|
||||
noAnim: true,
|
||||
previousPopupShowCallback: () => {
|
||||
show(currentQuote, false);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -558,7 +558,7 @@ export function updateRateQuote(randomQuote) {
|
|||
}
|
||||
RateQuotePopup.getQuoteStats(randomQuote).then((quoteStats) => {
|
||||
$(".pageTest #result #rateQuoteButton .rating").text(
|
||||
quoteStats.average ?? ""
|
||||
quoteStats.average.toFixed(1) ?? ""
|
||||
);
|
||||
$(".pageTest #result #rateQuoteButton")
|
||||
.css({ opacity: 0 })
|
||||
|
|
|
|||
|
|
@ -1596,6 +1596,7 @@ export async function finish(difficultyFailed = false) {
|
|||
|
||||
if (firebase.auth().currentUser == null) {
|
||||
$(".pageTest #result #rateQuoteButton").addClass("hidden");
|
||||
$(".pageTest #result #reportQuoteButton").addClass("hidden");
|
||||
try {
|
||||
firebase.analytics().logEvent("testCompletedNoLogin", completedEvent);
|
||||
} catch (e) {
|
||||
|
|
@ -1603,6 +1604,8 @@ export async function finish(difficultyFailed = false) {
|
|||
}
|
||||
notSignedInLastResult = completedEvent;
|
||||
dontSave = true;
|
||||
} else {
|
||||
$(".pageTest #result #reportQuoteButton").removeClass("hidden");
|
||||
}
|
||||
|
||||
Result.update(
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import * as ChallengeController from "./challenge-controller";
|
|||
import * as RateQuotePopup from "./rate-quote-popup";
|
||||
import * as UI from "./ui";
|
||||
import * as TestTimer from "./test-timer";
|
||||
import * as ReportQuotePopup from "./quote-report-popup";
|
||||
|
||||
export let currentWordElementIndex = 0;
|
||||
export let resultVisible = false;
|
||||
|
|
@ -997,6 +998,13 @@ $(".pageTest #rateQuoteButton").click(async (event) => {
|
|||
RateQuotePopup.show(TestLogic.randomQuote);
|
||||
});
|
||||
|
||||
$(".pageTest #reportQuoteButton").click(async (event) => {
|
||||
ReportQuotePopup.show({
|
||||
quoteId: parseInt(TestLogic.randomQuote.id),
|
||||
noAnim: false,
|
||||
});
|
||||
});
|
||||
|
||||
$(".pageTest #toggleBurstHeatmap").click(async (event) => {
|
||||
UpdateConfig.setBurstHeatmap(!Config.burstHeatmap);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -89,13 +89,13 @@ input[type="number"] {
|
|||
|
||||
.select2-container--default
|
||||
.select2-results__option--highlighted.select2-results__option--selectable {
|
||||
background-color: var(--sub-color);
|
||||
color: var(--text-color);
|
||||
background-color: var(--text-color);
|
||||
color: var(--bg-color);
|
||||
}
|
||||
|
||||
.select2-container--default .select2-results__option--selected {
|
||||
background-color: var(--main-color);
|
||||
color: var(--bg-color);
|
||||
background-color: var(--bg-color);
|
||||
color: var(--sub-color);
|
||||
}
|
||||
|
||||
.select2-container--open .select2-dropdown--below {
|
||||
|
|
@ -144,14 +144,14 @@ input[type="number"] {
|
|||
.select2-selection--single
|
||||
.select2-selection__arrow
|
||||
b {
|
||||
border-color: var(--main-color) transparent transparent transparent;
|
||||
border-color: var(--sub-color) transparent transparent transparent;
|
||||
}
|
||||
|
||||
.select2-container--default.select2-container--open
|
||||
.select2-selection--single
|
||||
.select2-selection__arrow
|
||||
b {
|
||||
border-color: var(--main-color) transparent;
|
||||
border-color: var(--sub-color) transparent;
|
||||
}
|
||||
|
||||
.select2-container--default .select2-search--dropdown .select2-search__field {
|
||||
|
|
|
|||
|
|
@ -400,6 +400,9 @@
|
|||
.searchResult {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 3fr 0fr;
|
||||
grid-template-areas:
|
||||
"text text text text"
|
||||
"id len source report";
|
||||
grid-auto-rows: auto;
|
||||
width: 100%;
|
||||
gap: 0.5rem;
|
||||
|
|
@ -411,48 +414,41 @@
|
|||
height: min-content;
|
||||
|
||||
.text {
|
||||
grid-column-start: 1;
|
||||
grid-column-end: 4;
|
||||
grid-row-start: 1;
|
||||
grid-row-end: 2;
|
||||
grid-area: text;
|
||||
overflow: visible;
|
||||
color: var(--text-color);
|
||||
}
|
||||
.id {
|
||||
grid-column-start: 1;
|
||||
grid-column-end: 2;
|
||||
grid-row-start: 2;
|
||||
grid-row-end: 3;
|
||||
grid-area: id;
|
||||
font-size: 0.8rem;
|
||||
color: var(--sub-color);
|
||||
}
|
||||
.length {
|
||||
grid-column-start: 2;
|
||||
grid-column-end: 3;
|
||||
grid-row-start: 2;
|
||||
grid-row-end: 3;
|
||||
grid-area: len;
|
||||
font-size: 0.8rem;
|
||||
color: var(--sub-color);
|
||||
}
|
||||
.source {
|
||||
grid-column-start: 3;
|
||||
grid-column-end: 4;
|
||||
grid-row-start: 2;
|
||||
grid-row-end: 3;
|
||||
grid-area: source;
|
||||
font-size: 0.8rem;
|
||||
color: var(--sub-color);
|
||||
}
|
||||
.resultChevron {
|
||||
grid-column-start: 4;
|
||||
grid-column-end: 5;
|
||||
grid-row-start: 1;
|
||||
grid-row-end: 3;
|
||||
grid-area: chevron;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-items: center;
|
||||
color: var(--sub-color);
|
||||
font-size: 2rem;
|
||||
}
|
||||
.report {
|
||||
grid-area: report;
|
||||
color: var(--sub-color);
|
||||
transition: 0.25s;
|
||||
&:hover {
|
||||
color: var(--text-color);
|
||||
}
|
||||
}
|
||||
.sub {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
|
@ -600,6 +596,58 @@
|
|||
}
|
||||
}
|
||||
|
||||
#quoteReportPopupWrapper {
|
||||
#quoteReportPopup {
|
||||
background: var(--bg-color);
|
||||
border-radius: var(--roundness);
|
||||
padding: 2rem;
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
width: 1000px;
|
||||
grid-template-rows: auto auto auto auto auto auto auto auto auto;
|
||||
height: auto;
|
||||
max-height: 40rem;
|
||||
overflow-y: scroll;
|
||||
|
||||
label {
|
||||
color: var(--sub-color);
|
||||
margin-bottom: -1rem;
|
||||
}
|
||||
|
||||
.text {
|
||||
color: var(--sub-color);
|
||||
}
|
||||
|
||||
.quote {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.5rem;
|
||||
color: var(--sub-color);
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
line-height: 1.2rem;
|
||||
min-height: 5rem;
|
||||
}
|
||||
|
||||
.characterCount {
|
||||
position: absolute;
|
||||
top: -1.25rem;
|
||||
right: 0.25rem;
|
||||
color: var(--sub-color);
|
||||
user-select: none;
|
||||
&.red {
|
||||
color: var(--error-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#resultEditTagsPanelWrapper {
|
||||
#resultEditTagsPanel {
|
||||
background: var(--bg-color);
|
||||
|
|
|
|||
|
|
@ -677,8 +677,15 @@
|
|||
}
|
||||
}
|
||||
|
||||
&.source #rateQuoteButton {
|
||||
padding: 0 0.5rem;
|
||||
&.source {
|
||||
#rateQuoteButton,
|
||||
#reportQuoteButton {
|
||||
padding: 0 0.25rem;
|
||||
}
|
||||
#rateQuoteButton {
|
||||
display: inline-grid;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -551,6 +551,33 @@
|
|||
<div id="submitQuoteButton" class="button">Submit</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="quoteReportPopupWrapper" class="popupWrapper hidden">
|
||||
<div id="quoteReportPopup" mode="">
|
||||
<div class="title">Report a Quote</div>
|
||||
<div class="text">
|
||||
Please report quotes responsibly. Misuse may result in you losing
|
||||
access to this feature.
|
||||
</div>
|
||||
<label>quote</label>
|
||||
<div class="quote"></div>
|
||||
<label>reason</label>
|
||||
<select name="report-reason" class="reason">
|
||||
<option value="Grammatical error">Grammatical error</option>
|
||||
<option value="Inappropriate content">Inappropriate content</option>
|
||||
<option value="Low quality content">Low quality content</option>
|
||||
</select>
|
||||
<label>comment</label>
|
||||
<div style="position: relative">
|
||||
<textarea class="comment" type="text" autocomplete="off"></textarea>
|
||||
<div class="characterCount">-</div>
|
||||
</div>
|
||||
<div
|
||||
class="g-recaptcha"
|
||||
data-sitekey="6Lc-V8McAAAAAJ7s6LGNe7MBZnRiwbsbiWts87aj"
|
||||
></div>
|
||||
<div class="button submit">Report</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="quoteApprovePopupWrapper" class="popupWrapper hidden">
|
||||
<div id="quoteApprovePopup" mode="">
|
||||
<div class="top">
|
||||
|
|
@ -1446,12 +1473,20 @@
|
|||
<div class="group source hidden">
|
||||
<div class="top">
|
||||
source
|
||||
<span
|
||||
id="reportQuoteButton"
|
||||
class="icon-button hidden"
|
||||
aria-label="Report quote"
|
||||
data-balloon-pos="up"
|
||||
style="display: inline-block"
|
||||
>
|
||||
<i class="icon fas fa-flag"></i>
|
||||
</span>
|
||||
<span
|
||||
id="rateQuoteButton"
|
||||
class="icon-button hidden"
|
||||
aria-label="Rate quote"
|
||||
data-balloon-pos="up"
|
||||
style="display: inline-block"
|
||||
>
|
||||
<i class="icon far fa-star"></i>
|
||||
<span class="rating"></span>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue