feat: mii descriptions

This commit is contained in:
trafficlunar 2025-05-07 21:04:01 +01:00
parent 8753358a48
commit 398580e72b
7 changed files with 34 additions and 6 deletions

View file

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "miis" ADD COLUMN "description" VARCHAR(256);

View file

@ -64,11 +64,12 @@ model Session {
}
model Mii {
id Int @id @default(autoincrement())
userId Int
name String @db.VarChar(64)
imageCount Int @default(0)
tags String[]
id Int @id @default(autoincrement())
userId Int
name String @db.VarChar(64)
imageCount Int @default(0)
tags String[]
description String? @db.VarChar(256)
firstName String
lastName String

View file

@ -24,6 +24,7 @@ const uploadsDirectory = path.join(process.cwd(), "uploads");
const submitSchema = z.object({
name: nameSchema,
tags: tagsSchema,
description: z.string().trim().max(256).optional(),
qrBytesRaw: z
.array(z.number(), { required_error: "A QR code is required" })
.length(372, { message: "QR code size is not a valid Tomodachi Life QR code" }),
@ -54,6 +55,7 @@ export async function POST(request: NextRequest) {
const parsed = submitSchema.safeParse({
name: formData.get("name"),
tags: rawTags,
description: formData.get("description"),
qrBytesRaw: rawQrBytesRaw,
image1: formData.get("image1"),
image2: formData.get("image2"),
@ -61,11 +63,12 @@ export async function POST(request: NextRequest) {
});
if (!parsed.success) return rateLimit.sendResponse({ error: parsed.error.errors[0].message }, 400);
const { name: uncensoredName, tags: uncensoredTags, qrBytesRaw, image1, image2, image3 } = parsed.data;
const { name: uncensoredName, tags: uncensoredTags, description: uncensoredDescription, qrBytesRaw, image1, image2, image3 } = parsed.data;
// Censor potential inappropriate words
const name = profanity.censor(uncensoredName);
const tags = uncensoredTags.map((t) => profanity.censor(t));
const description = uncensoredDescription && profanity.censor(uncensoredDescription);
// Validate image files
const images: File[] = [];
@ -97,6 +100,7 @@ export async function POST(request: NextRequest) {
userId: Number(session.user.id),
name,
tags,
description,
firstName: conversion.tomodachiLifeMii.firstName,
lastName: conversion.tomodachiLifeMii.lastName,

View file

@ -200,6 +200,9 @@ export default async function MiiPage({ params }: Props) {
{mii.createdAt.toLocaleTimeString("en-GB", { timeZone: "UTC" })} UTC
</h4>
</div>
{/* Description */}
{mii.description && <p className="text-sm mt-2 ml-2 bg-white/50 p-3 rounded-lg border border-orange-200">{mii.description}</p>}
</div>
{/* Buttons */}

View file

@ -65,6 +65,7 @@ export default function ReportMiiForm({ mii, likes }: Props) {
</label>
<textarea
rows={3}
maxLength={256}
placeholder="Type notes here for the report..."
className="pill input !rounded-xl resize-none col-span-2"
value={notes}

View file

@ -69,6 +69,7 @@ export default function ReportUserForm({ user }: Props) {
</label>
<textarea
rows={3}
maxLength={256}
placeholder="Type notes here for the report..."
className="pill input !rounded-xl resize-none col-span-2"
value={notes}

View file

@ -49,6 +49,7 @@ export default function SubmitForm() {
const [name, setName] = useState("");
const [tags, setTags] = useState<string[]>([]);
const [description, setDescription] = useState("");
const [qrBytesRaw, setQrBytesRaw] = useState<number[]>([]);
const handleSubmit = async () => {
@ -68,6 +69,7 @@ export default function SubmitForm() {
const formData = new FormData();
formData.append("name", name);
formData.append("tags", JSON.stringify(tags));
formData.append("description", description);
formData.append("qrBytesRaw", JSON.stringify(qrBytesRaw));
files.forEach((file, index) => {
// image1, image2, etc.
@ -190,6 +192,20 @@ export default function SubmitForm() {
<TagSelector tags={tags} setTags={setTags} />
</div>
<div className="w-full grid grid-cols-3 items-start">
<label htmlFor="reason-note" className="font-semibold py-2">
Description
</label>
<textarea
rows={3}
maxLength={256}
placeholder="(optional) Type a description..."
className="pill input !rounded-xl resize-none col-span-2"
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
</div>
{/* Separator */}
<div className="flex items-center gap-4 text-zinc-500 text-sm font-medium mt-8 mb-2">
<hr className="flex-grow border-zinc-300" />