mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-10-02 16:20:36 -05:00
Allow rejecting uploaded images + validate many at a time
This commit is contained in:
@@ -12,3 +12,13 @@ export function findById(id: number) {
|
||||
.where("UnvalidatedUserSubmittedImage.id", "=", id)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
export function deleteImageById(id: number) {
|
||||
return db.transaction().execute(async (trx) => {
|
||||
await trx.deleteFrom("Art").where("Art.imgId", "=", id).execute();
|
||||
await trx
|
||||
.deleteFrom("UnvalidatedUserSubmittedImage")
|
||||
.where("id", "=", id)
|
||||
.execute();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,7 +6,9 @@ import {
|
||||
badRequestIfFalsy,
|
||||
errorToastIfFalsy,
|
||||
parseRequestPayload,
|
||||
successToast,
|
||||
} from "~/utils/remix.server";
|
||||
import { assertUnreachable } from "~/utils/types";
|
||||
import * as ImageRepository from "../ImageRepository.server";
|
||||
import { validateImage } from "../queries/validateImage";
|
||||
import { validateImageSchema } from "../upload-schemas.server";
|
||||
@@ -20,12 +22,29 @@ export const action: ActionFunction = async ({ request }) => {
|
||||
|
||||
errorToastIfFalsy(isMod(user), "Only admins can validate images");
|
||||
|
||||
const image = badRequestIfFalsy(await ImageRepository.findById(data.imageId));
|
||||
switch (data._action) {
|
||||
case "VALIDATE": {
|
||||
for (const imageId of data.imageIds) {
|
||||
const image = badRequestIfFalsy(
|
||||
await ImageRepository.findById(imageId),
|
||||
);
|
||||
|
||||
validateImage(data.imageId);
|
||||
validateImage(imageId);
|
||||
|
||||
if (image.tournamentId) {
|
||||
clearTournamentDataCache(image.tournamentId);
|
||||
if (image.tournamentId) {
|
||||
clearTournamentDataCache(imageId);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "REJECT": {
|
||||
await ImageRepository.deleteImageById(data.imageId);
|
||||
|
||||
return successToast("The image was deleted");
|
||||
}
|
||||
default: {
|
||||
assertUnreachable(data);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { requireUserId } from "~/features/auth/core/user.server";
|
||||
import { isMod } from "~/permissions";
|
||||
import { notFoundIfFalsy } from "~/utils/remix.server";
|
||||
import { countAllUnvalidatedImg } from "../queries/countAllUnvalidatedImg.server";
|
||||
import { oneUnvalidatedImage } from "../queries/oneUnvalidatedImage";
|
||||
import { unvalidatedImages } from "../queries/unvalidatedImages";
|
||||
|
||||
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
const user = await requireUserId(request);
|
||||
@@ -11,7 +11,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
notFoundIfFalsy(isMod(user));
|
||||
|
||||
return {
|
||||
image: oneUnvalidatedImage(),
|
||||
images: unvalidatedImages(),
|
||||
unvalidatedImgCount: countAllUnvalidatedImg(),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { sql } from "~/db/sql";
|
||||
import type { Tables } from "~/db/tables";
|
||||
import { IMAGES_TO_VALIDATE_AT_ONCE } from "../upload-constants";
|
||||
|
||||
const stm = sql.prepare(/* sql */ `
|
||||
select
|
||||
"UnvalidatedUserSubmittedImage"."id",
|
||||
"UnvalidatedUserSubmittedImage"."url",
|
||||
"UnvalidatedUserSubmittedImage"."submitterUserId"
|
||||
"UnvalidatedUserSubmittedImage"."submitterUserId",
|
||||
"User"."username"
|
||||
from "UnvalidatedUserSubmittedImage"
|
||||
left join "User" on
|
||||
"UnvalidatedUserSubmittedImage"."submitterUserId" = "User"."id"
|
||||
left join "Team" on
|
||||
"UnvalidatedUserSubmittedImage"."id" = "Team"."avatarImgId" or
|
||||
"UnvalidatedUserSubmittedImage"."id" = "Team"."bannerImgId"
|
||||
@@ -16,14 +20,16 @@ const stm = sql.prepare(/* sql */ `
|
||||
"UnvalidatedUserSubmittedImage"."id" = "CalendarEvent"."avatarImgId"
|
||||
where "UnvalidatedUserSubmittedImage"."validatedAt" is null
|
||||
and ("Team"."id" is not null or "Art"."id" is not null or "CalendarEvent"."id" is not null)
|
||||
limit 1
|
||||
limit ${IMAGES_TO_VALIDATE_AT_ONCE}
|
||||
`);
|
||||
|
||||
type UnvalidatedImage = Pick<
|
||||
Tables["UserSubmittedImage"],
|
||||
"id" | "url" | "submitterUserId"
|
||||
>;
|
||||
> & {
|
||||
username: Tables["User"]["username"];
|
||||
};
|
||||
|
||||
export function oneUnvalidatedImage() {
|
||||
return stm.get() as UnvalidatedImage | null;
|
||||
export function unvalidatedImages() {
|
||||
return stm.all() as Array<UnvalidatedImage>;
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
import { Form, useLoaderData } from "@remix-run/react";
|
||||
import { Form, Link, useLoaderData } from "@remix-run/react";
|
||||
import * as React from "react";
|
||||
import { FormWithConfirm } from "~/components/FormWithConfirm";
|
||||
import { Main } from "~/components/Main";
|
||||
import { SubmitButton } from "~/components/SubmitButton";
|
||||
import { SendouButton } from "~/components/elements/Button";
|
||||
import { TrashIcon } from "~/components/icons/Trash";
|
||||
import { userSubmittedImage } from "~/utils/urls";
|
||||
|
||||
import { action } from "../actions/upload.admin.server";
|
||||
@@ -18,19 +22,63 @@ export default function ImageUploadAdminPage() {
|
||||
function ImageValidator() {
|
||||
const data = useLoaderData<typeof loader>();
|
||||
|
||||
if (!data.image) {
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies:
|
||||
React.useEffect(() => {
|
||||
window.scrollTo(0, 0);
|
||||
}, [data]);
|
||||
|
||||
if (data.images.length === 0) {
|
||||
return <>All validated!</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>{data.unvalidatedImgCount} left</div>
|
||||
<img src={userSubmittedImage(data.image.url)} alt="" />
|
||||
<Form method="post">
|
||||
<input type="hidden" name="imageId" value={data.image.id} />
|
||||
<SubmitButton>Ok</SubmitButton>
|
||||
<div className="text-lighter">{data.unvalidatedImgCount} left</div>
|
||||
<div className="stack md">
|
||||
{data.images.map((image, i) => {
|
||||
return (
|
||||
<div key={image.id}>
|
||||
<div className="text-lg font-bold stack horizontal md">
|
||||
{i + 1}){" "}
|
||||
<FormWithConfirm
|
||||
dialogHeading={`Reject image submitted by ${image.username}?`}
|
||||
deleteButtonText="Reject"
|
||||
fields={[
|
||||
["imageId", image.id],
|
||||
["_action", "REJECT"],
|
||||
]}
|
||||
>
|
||||
<SendouButton
|
||||
icon={<TrashIcon />}
|
||||
variant="minimal-destructive"
|
||||
size="medium"
|
||||
/>
|
||||
</FormWithConfirm>
|
||||
</div>
|
||||
<img src={userSubmittedImage(image.url)} alt="" />
|
||||
<Link
|
||||
to={`/u/${image.submitterUserId}`}
|
||||
className="text-xs"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
From: {image.username}
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<Form method="post" className="mt-12">
|
||||
<input
|
||||
type="hidden"
|
||||
name="imageIds"
|
||||
value={JSON.stringify(data.images.map((img) => img.id))}
|
||||
/>
|
||||
<SubmitButton size="big" className="mx-auto" _action="VALIDATE">
|
||||
All {data.images.length} above ok
|
||||
</SubmitButton>
|
||||
</Form>
|
||||
<div>From: {data.image.submitterUserId}</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import type { ImageUploadType } from "./upload-types";
|
||||
|
||||
export const MAX_UNVALIDATED_IMG_COUNT = 5;
|
||||
|
||||
export const IMAGES_TO_VALIDATE_AT_ONCE = 5;
|
||||
|
||||
export const IMAGE_TYPES = ["team-pfp", "org-pfp", "team-banner"] as const;
|
||||
|
||||
export const imgTypeToDimensions: Record<
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import { z } from "zod";
|
||||
import { id } from "~/utils/zod";
|
||||
import { _action, id, safeJSONParse } from "~/utils/zod";
|
||||
|
||||
export const validateImageSchema = z.object({
|
||||
const validateManySchema = z.object({
|
||||
_action: _action("VALIDATE"),
|
||||
imageIds: z.preprocess(safeJSONParse, z.array(id).min(1).max(5)),
|
||||
});
|
||||
|
||||
const rejectSchema = z.object({
|
||||
_action: _action("REJECT"),
|
||||
imageId: id,
|
||||
});
|
||||
|
||||
export const validateImageSchema = z.union([validateManySchema, rejectSchema]);
|
||||
|
||||
@@ -246,6 +246,10 @@
|
||||
margin-block-start: var(--s-6);
|
||||
}
|
||||
|
||||
.mt-12 {
|
||||
margin-block-start: var(--s-12);
|
||||
}
|
||||
|
||||
.mb-0 {
|
||||
margin-block-end: 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user