sendou.ink/app/form/fields/ImageFormField.tsx
Kalle 739e6f440b Add moderation note and autoValidate to image form field
Show a note under non-autoValidate image fields that uploads are
moderated before going public. Move autoValidate to a per-field schema
property (org logo only) driving both the action logic and the note.
Preview pending images on the team edit page while keeping them hidden
on the public page.
2026-06-06 10:56:52 +03:00

106 lines
2.6 KiB
TypeScript

import clsx from "clsx";
import Compressor from "compressorjs";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { SendouButton } from "~/components/elements/Button";
import { logger } from "~/utils/logger";
import {
type ImageFieldValue,
resolveImageFieldDimensions,
} from "../image-field";
import type { FormFieldProps } from "../types";
import { FormFieldWrapper } from "./FormFieldWrapper";
import styles from "./ImageFormField.module.css";
type ImageFormFieldProps = Omit<FormFieldProps<"image">, "onBlur"> & {
value: ImageFieldValue;
onChange: (value: ImageFieldValue) => void;
};
export function ImageFormField({
name,
label,
dimensions,
autoValidate,
error,
value,
onChange,
}: ImageFormFieldProps) {
const id = React.useId();
const { t } = useTranslation(["common"]);
const resolvedDimensions = resolveImageFieldDimensions(dimensions);
const previewUrl =
value?.type === "EXISTING"
? value.url
: value?.type === "NEW"
? value.dataUrl
: null;
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const uploadedFile = event.target.files?.[0];
if (!uploadedFile) return;
new Compressor(uploadedFile, {
width: resolvedDimensions.width,
height: resolvedDimensions.height,
maxWidth: resolvedDimensions.width,
maxHeight: resolvedDimensions.height,
resize: "cover",
mimeType: "image/webp",
success(result) {
const reader = new FileReader();
reader.onload = () =>
onChange({ type: "NEW", dataUrl: reader.result as string });
reader.onerror = () => logger.error("Failed to read compressed image");
reader.readAsDataURL(result);
},
error(err) {
logger.error(err.message);
},
});
};
const isBanner =
dimensions === "thick-banner" ||
(typeof dimensions === "object" && dimensions.width > dimensions.height);
return (
<FormFieldWrapper
id={id}
name={name}
label={label}
error={error}
bottomText={
autoValidate ? undefined : "forms:bottomTexts.imageModeration"
}
>
<div className="stack sm items-start">
{previewUrl ? (
<img
src={previewUrl}
alt=""
className={clsx(styles.preview, { [styles.banner]: isBanner })}
/>
) : null}
{value ? (
<SendouButton
variant="minimal-destructive"
size="small"
onPress={() => onChange(null)}
>
{t("common:actions.remove")}
</SendouButton>
) : (
<input
id={id}
type="file"
accept="image/png, image/jpeg, image/webp"
onChange={handleFileChange}
/>
)}
</div>
</FormFieldWrapper>
);
}