mirror of
https://github.com/trafficlunar/tomodachi-share.git
synced 2026-03-29 03:29:13 +00:00
feat: makeup filter, bad_quality report type
This commit is contained in:
parent
4f03d611eb
commit
fd11f996df
15 changed files with 233 additions and 15 deletions
|
|
@ -0,0 +1,2 @@
|
||||||
|
-- AlterEnum
|
||||||
|
ALTER TYPE "ReportReason" ADD VALUE 'BAD_QUALITY';
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "MiiMakeup" AS ENUM ('FULL', 'PARTIAL', 'NONE');
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "miis" ADD COLUMN "makeup" "MiiMakeup";
|
||||||
|
|
@ -76,10 +76,12 @@ model Mii {
|
||||||
description String? @db.VarChar(256)
|
description String? @db.VarChar(256)
|
||||||
platform MiiPlatform @default(THREE_DS)
|
platform MiiPlatform @default(THREE_DS)
|
||||||
|
|
||||||
instructions Json?
|
instructions Json?
|
||||||
|
gender MiiGender?
|
||||||
|
makeup MiiMakeup?
|
||||||
|
|
||||||
firstName String?
|
firstName String?
|
||||||
lastName String?
|
lastName String?
|
||||||
gender MiiGender?
|
|
||||||
islandName String?
|
islandName String?
|
||||||
allowedCopying Boolean?
|
allowedCopying Boolean?
|
||||||
|
|
||||||
|
|
@ -166,6 +168,12 @@ enum MiiGender {
|
||||||
NONBINARY
|
NONBINARY
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum MiiMakeup {
|
||||||
|
FULL
|
||||||
|
PARTIAL
|
||||||
|
NONE
|
||||||
|
}
|
||||||
|
|
||||||
enum ReportType {
|
enum ReportType {
|
||||||
MII
|
MII
|
||||||
USER
|
USER
|
||||||
|
|
@ -175,6 +183,7 @@ enum ReportReason {
|
||||||
INAPPROPRIATE
|
INAPPROPRIATE
|
||||||
SPAM
|
SPAM
|
||||||
COPYRIGHT
|
COPYRIGHT
|
||||||
|
BAD_QUALITY
|
||||||
OTHER
|
OTHER
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,8 @@ import { RateLimit } from "@/lib/rate-limit";
|
||||||
const reportSchema = z.object({
|
const reportSchema = z.object({
|
||||||
id: z.coerce.number({ error: "ID must be a number" }).int({ error: "ID must be an integer" }).positive({ error: "ID must be valid" }),
|
id: z.coerce.number({ error: "ID must be a number" }).int({ error: "ID must be an integer" }).positive({ error: "ID must be valid" }),
|
||||||
type: z.enum(["mii", "user"], { error: "Type must be either 'mii' or 'user'" }),
|
type: z.enum(["mii", "user"], { error: "Type must be either 'mii' or 'user'" }),
|
||||||
reason: z.enum(["inappropriate", "spam", "copyright", "other"], {
|
reason: z.enum(["inappropriate", "spam", "copyright", "bad_quality", "other"], {
|
||||||
message: "Reason must be either 'inappropriate', 'spam', 'copyright', or 'other'",
|
message: "Reason must be either 'inappropriate', 'spam', 'copyright', 'bad_quality' or 'other'",
|
||||||
}),
|
}),
|
||||||
notes: z.string().trim().max(256).optional(),
|
notes: z.string().trim().max(256).optional(),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import sharp from "sharp";
|
||||||
|
|
||||||
import qrcode from "qrcode-generator";
|
import qrcode from "qrcode-generator";
|
||||||
import { profanity } from "@2toad/profanity";
|
import { profanity } from "@2toad/profanity";
|
||||||
import { MiiGender, MiiPlatform } from "@prisma/client";
|
import { MiiGender, MiiMakeup, MiiPlatform } from "@prisma/client";
|
||||||
|
|
||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/lib/auth";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
|
|
@ -33,6 +33,7 @@ const submitSchema = z
|
||||||
|
|
||||||
// Switch
|
// Switch
|
||||||
gender: z.enum(MiiGender).default("MALE"),
|
gender: z.enum(MiiGender).default("MALE"),
|
||||||
|
makeup: z.enum(MiiMakeup).default("NONE"),
|
||||||
miiPortraitImage: z.union([z.instanceof(File), z.any()]).optional(),
|
miiPortraitImage: z.union([z.instanceof(File), z.any()]).optional(),
|
||||||
miiFeaturesImage: z.union([z.instanceof(File), z.any()]).optional(),
|
miiFeaturesImage: z.union([z.instanceof(File), z.any()]).optional(),
|
||||||
instructions: switchMiiInstructionsSchema,
|
instructions: switchMiiInstructionsSchema,
|
||||||
|
|
@ -106,6 +107,7 @@ export async function POST(request: NextRequest) {
|
||||||
description: formData.get("description"),
|
description: formData.get("description"),
|
||||||
|
|
||||||
gender: formData.get("gender") ?? undefined, // ZOD MOMENT
|
gender: formData.get("gender") ?? undefined, // ZOD MOMENT
|
||||||
|
makeup: formData.get("makeup") ?? undefined,
|
||||||
miiPortraitImage: formData.get("miiPortraitImage"),
|
miiPortraitImage: formData.get("miiPortraitImage"),
|
||||||
miiFeaturesImage: formData.get("miiFeaturesImage"),
|
miiFeaturesImage: formData.get("miiFeaturesImage"),
|
||||||
instructions: minifiedInstructions,
|
instructions: minifiedInstructions,
|
||||||
|
|
@ -139,6 +141,7 @@ export async function POST(request: NextRequest) {
|
||||||
description: uncensoredDescription,
|
description: uncensoredDescription,
|
||||||
qrBytesRaw,
|
qrBytesRaw,
|
||||||
gender,
|
gender,
|
||||||
|
makeup,
|
||||||
miiPortraitImage,
|
miiPortraitImage,
|
||||||
miiFeaturesImage,
|
miiFeaturesImage,
|
||||||
image1,
|
image1,
|
||||||
|
|
@ -209,6 +212,7 @@ export async function POST(request: NextRequest) {
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
instructions: minifiedInstructions,
|
instructions: minifiedInstructions,
|
||||||
|
makeup: makeup ?? "NONE",
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -253,6 +253,59 @@ export default async function MiiPage({ params }: Props) {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Makeup */}
|
||||||
|
{mii.platform === "SWITCH" && (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center gap-4 text-zinc-500 text-sm font-medium mb-2 mt-2 w-full">
|
||||||
|
<hr className="grow border-zinc-300" />
|
||||||
|
<span>Makeup</span>
|
||||||
|
<hr className="grow border-zinc-300" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div data-tooltip-span title={mii.makeup ?? "NULL"} className="flex gap-1">
|
||||||
|
{/* Tooltip */}
|
||||||
|
<div
|
||||||
|
className={`tooltip mt-1! ${
|
||||||
|
mii.makeup === "FULL"
|
||||||
|
? "bg-pink-400! border-pink-400! before:border-b-pink-400!"
|
||||||
|
: mii.makeup === "PARTIAL"
|
||||||
|
? "bg-purple-400! border-purple-400! before:border-b-purple-400!"
|
||||||
|
: "bg-gray-400! border-gray-400! before:border-b-gray-400!"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{mii.makeup === "FULL" ? "Full Makeup" : mii.makeup === "PARTIAL" ? "Partial Makeup" : "No Makeup"}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Full Makeup */}
|
||||||
|
<div
|
||||||
|
className={`rounded-xl flex justify-center items-center size-13 text-5xl border-2 shadow-sm ${
|
||||||
|
mii.makeup === "FULL" ? "bg-pink-100 border-pink-400" : "bg-white border-gray-300"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Icon icon="mdi:palette" className="text-pink-400" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Partial Makeup */}
|
||||||
|
<div
|
||||||
|
className={`rounded-xl flex justify-center items-center size-13 text-5xl border-2 shadow-sm ${
|
||||||
|
mii.makeup === "PARTIAL" ? "bg-purple-100 border-purple-400" : "bg-white border-gray-300"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Icon icon="mdi:lipstick" className="text-purple-400" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* No Makeup */}
|
||||||
|
<div
|
||||||
|
className={`rounded-xl flex justify-center items-center size-13 text-5xl border-2 shadow-sm ${
|
||||||
|
mii.makeup === "NONE" ? "bg-gray-200 border-gray-400" : "bg-white border-gray-300"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Icon icon="codex:cross" className="text-gray-400" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-span-2 flex flex-col gap-4 max-md:col-span-1">
|
<div className="col-span-2 flex flex-col gap-4 max-md:col-span-1">
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ import Countdown from "@/components/countdown";
|
||||||
import MiiList from "@/components/mii/list";
|
import MiiList from "@/components/mii/list";
|
||||||
import Skeleton from "@/components/mii/list/skeleton";
|
import Skeleton from "@/components/mii/list/skeleton";
|
||||||
|
|
||||||
|
export const revalidate = 60;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
|
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ export default function PrivacyPage() {
|
||||||
<div className="bg-amber-50 border-2 border-amber-500 rounded-2xl p-6">
|
<div className="bg-amber-50 border-2 border-amber-500 rounded-2xl p-6">
|
||||||
<h1 className="text-2xl font-bold">Terms of Service</h1>
|
<h1 className="text-2xl font-bold">Terms of Service</h1>
|
||||||
<h2 className="font-light">
|
<h2 className="font-light">
|
||||||
<strong className="font-medium">Effective Date:</strong> May 02, 2025
|
<strong className="font-medium">Effective Date:</strong> March 26, 2026
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<hr className="border-black/20 mt-1 mb-4" />
|
<hr className="border-black/20 mt-1 mb-4" />
|
||||||
|
|
@ -41,6 +41,7 @@ export default function PrivacyPage() {
|
||||||
<li>No impersonation of others.</li>
|
<li>No impersonation of others.</li>
|
||||||
<li>No malware, malicious links, or phishing content.</li>
|
<li>No malware, malicious links, or phishing content.</li>
|
||||||
<li>No harassment, hate speech, threats, or bullying towards others.</li>
|
<li>No harassment, hate speech, threats, or bullying towards others.</li>
|
||||||
|
<li>Miis must be high quality: for example, not following all instructions on the submit form correctly.</li>
|
||||||
<li>Avoid using inappropriate language. Profanity may be automatically censored.</li>
|
<li>Avoid using inappropriate language. Profanity may be automatically censored.</li>
|
||||||
<li>No use of automated scripts, bots, or scrapers to access or interact with the site.</li>
|
<li>No use of automated scripts, bots, or scrapers to access or interact with the site.</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,13 @@ import { useSearchParams } from "next/navigation";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { Icon } from "@iconify/react";
|
import { Icon } from "@iconify/react";
|
||||||
|
|
||||||
import { MiiGender, MiiPlatform } from "@prisma/client";
|
import { MiiGender, MiiMakeup, MiiPlatform } from "@prisma/client";
|
||||||
|
|
||||||
import PlatformSelect from "./platform-select";
|
import PlatformSelect from "./platform-select";
|
||||||
import TagFilter from "./tag-filter";
|
import TagFilter from "./tag-filter";
|
||||||
import GenderSelect from "./gender-select";
|
import GenderSelect from "./gender-select";
|
||||||
import OtherFilters from "./other-filters";
|
import OtherFilters from "./other-filters";
|
||||||
|
import MakeupSelect from "./makeup-select";
|
||||||
|
|
||||||
export default function FilterMenu() {
|
export default function FilterMenu() {
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
|
|
@ -19,6 +20,7 @@ export default function FilterMenu() {
|
||||||
|
|
||||||
const platform = (searchParams.get("platform") as MiiPlatform) || undefined;
|
const platform = (searchParams.get("platform") as MiiPlatform) || undefined;
|
||||||
const gender = (searchParams.get("gender") as MiiGender) || undefined;
|
const gender = (searchParams.get("gender") as MiiGender) || undefined;
|
||||||
|
const makeup = (searchParams.get("makeup") as MiiMakeup) || undefined;
|
||||||
const rawTags = searchParams.get("tags") || "";
|
const rawTags = searchParams.get("tags") || "";
|
||||||
const rawExclude = searchParams.get("exclude") || "";
|
const rawExclude = searchParams.get("exclude") || "";
|
||||||
const allowCopying = (searchParams.get("allowCopying") as unknown as boolean) || false;
|
const allowCopying = (searchParams.get("allowCopying") as unknown as boolean) || false;
|
||||||
|
|
@ -66,9 +68,10 @@ export default function FilterMenu() {
|
||||||
if (platform) count++;
|
if (platform) count++;
|
||||||
if (gender) count++;
|
if (gender) count++;
|
||||||
if (allowCopying) count++;
|
if (allowCopying) count++;
|
||||||
|
if (makeup) count++;
|
||||||
|
|
||||||
setFilterCount(count);
|
setFilterCount(count);
|
||||||
}, [tags, exclude, platform, gender, allowCopying]);
|
}, [tags, exclude, platform, gender, allowCopying, makeup]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
|
|
@ -114,6 +117,16 @@ export default function FilterMenu() {
|
||||||
</div>
|
</div>
|
||||||
<TagFilter isExclude />
|
<TagFilter isExclude />
|
||||||
|
|
||||||
|
{platform !== "THREE_DS" && (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center gap-4 text-zinc-500 text-sm font-medium w-full mt-2 mb-1">
|
||||||
|
<hr className="grow border-zinc-300" />
|
||||||
|
<span>Makeup</span>
|
||||||
|
<hr className="grow border-zinc-300" />
|
||||||
|
</div>
|
||||||
|
<MakeupSelect />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{platform !== "SWITCH" && (
|
{platform !== "SWITCH" && (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center gap-4 text-zinc-500 text-sm font-medium w-full mt-2 mb-1">
|
<div className="flex items-center gap-4 text-zinc-500 text-sm font-medium w-full mt-2 mb-1">
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { headers } from "next/headers";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
import { MiiGender, MiiPlatform, Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
import { Icon } from "@iconify/react";
|
import { Icon } from "@iconify/react";
|
||||||
|
|
||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
|
|
@ -29,7 +28,7 @@ export default async function MiiList({ searchParams, userId, inLikesPage }: Pro
|
||||||
const parsed = searchSchema.safeParse(searchParams);
|
const parsed = searchSchema.safeParse(searchParams);
|
||||||
if (!parsed.success) return <h1>{parsed.error.issues[0].message}</h1>;
|
if (!parsed.success) return <h1>{parsed.error.issues[0].message}</h1>;
|
||||||
|
|
||||||
const { q: query, sort, tags, exclude, platform, gender, allowCopying, page = 1, limit = 24, seed } = parsed.data;
|
const { q: query, sort, tags, exclude, platform, gender, makeup, allowCopying, page = 1, limit = 24, seed } = parsed.data;
|
||||||
|
|
||||||
// My Likes page
|
// My Likes page
|
||||||
let miiIdsLiked: number[] | undefined = undefined;
|
let miiIdsLiked: number[] | undefined = undefined;
|
||||||
|
|
@ -58,6 +57,8 @@ export default async function MiiList({ searchParams, userId, inLikesPage }: Pro
|
||||||
...(gender && { gender: { equals: gender } }),
|
...(gender && { gender: { equals: gender } }),
|
||||||
// Allow Copying
|
// Allow Copying
|
||||||
...(allowCopying && { allowedCopying: true }),
|
...(allowCopying && { allowedCopying: true }),
|
||||||
|
// Makeup
|
||||||
|
...(makeup && { makeup: { equals: makeup } }),
|
||||||
// Profiles
|
// Profiles
|
||||||
...(userId && { userId }),
|
...(userId && { userId }),
|
||||||
};
|
};
|
||||||
|
|
@ -79,6 +80,7 @@ export default async function MiiList({ searchParams, userId, inLikesPage }: Pro
|
||||||
tags: true,
|
tags: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
gender: true,
|
gender: true,
|
||||||
|
makeup: true,
|
||||||
allowedCopying: true,
|
allowedCopying: true,
|
||||||
// Mii liked check
|
// Mii liked check
|
||||||
...(session?.user?.id && {
|
...(session?.user?.id && {
|
||||||
|
|
|
||||||
75
src/components/mii/list/makeup-select.tsx
Normal file
75
src/components/mii/list/makeup-select.tsx
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
|
import { useState, useTransition } from "react";
|
||||||
|
import { Icon } from "@iconify/react";
|
||||||
|
import { MiiMakeup, MiiPlatform } from "@prisma/client";
|
||||||
|
|
||||||
|
export default function MakeupSelect() {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const [, startTransition] = useTransition();
|
||||||
|
|
||||||
|
const [selected, setSelected] = useState<MiiMakeup | null>((searchParams.get("makeup") as MiiMakeup) ?? null);
|
||||||
|
|
||||||
|
const handleClick = (makeup: MiiMakeup) => {
|
||||||
|
const filter = selected === makeup ? null : makeup;
|
||||||
|
setSelected(filter);
|
||||||
|
|
||||||
|
const params = new URLSearchParams(searchParams);
|
||||||
|
params.set("page", "1");
|
||||||
|
|
||||||
|
if (filter) {
|
||||||
|
params.set("makeup", filter);
|
||||||
|
} else {
|
||||||
|
params.delete("makeup");
|
||||||
|
}
|
||||||
|
|
||||||
|
startTransition(() => {
|
||||||
|
router.push(`?${params.toString()}`, { scroll: false });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex gap-0.5 w-fit">
|
||||||
|
{/* Full Makeup */}
|
||||||
|
<button
|
||||||
|
onClick={() => handleClick("FULL")}
|
||||||
|
aria-label="Filter for Full Makeup"
|
||||||
|
data-tooltip-span
|
||||||
|
className={`cursor-pointer rounded-xl flex justify-center items-center size-13 text-5xl border-2 transition-all ${
|
||||||
|
selected === "FULL" ? "bg-pink-100 border-pink-400 shadow-md" : "bg-white border-gray-300 hover:border-gray-400"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="tooltip bg-pink-400! border-pink-400! before:border-b-pink-400!">Full Makeup</div>
|
||||||
|
<Icon icon="mdi:palette" className="text-pink-400" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Partial Makeup */}
|
||||||
|
<button
|
||||||
|
onClick={() => handleClick("PARTIAL")}
|
||||||
|
aria-label="Filter for Partial Makeup"
|
||||||
|
data-tooltip-span
|
||||||
|
className={`cursor-pointer rounded-xl flex justify-center items-center size-13 text-5xl border-2 transition-all ${
|
||||||
|
selected === "PARTIAL" ? "bg-purple-100 border-purple-400 shadow-md" : "bg-white border-gray-300 hover:border-gray-400"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="tooltip bg-purple-400! border-purple-400! before:border-b-purple-400!">Partial Makeup</div>
|
||||||
|
<Icon icon="mdi:lipstick" className="text-purple-400" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* No Makeup */}
|
||||||
|
<button
|
||||||
|
onClick={() => handleClick("NONE")}
|
||||||
|
aria-label="Filter for No Makeup"
|
||||||
|
data-tooltip-span
|
||||||
|
className={`cursor-pointer rounded-xl flex justify-center items-center size-13 text-5xl border-2 transition-all ${
|
||||||
|
selected === "NONE" ? "bg-gray-200 border-gray-400 shadow-md" : "bg-white border-gray-300 hover:border-gray-400"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="tooltip bg-gray-400! border-gray-400! before:border-b-gray-400!">No Makeup</div>
|
||||||
|
<Icon icon="codex:cross" className="text-gray-400" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -13,6 +13,7 @@ const reasonMap: Record<ReportReason, string> = {
|
||||||
INAPPROPRIATE: "Inappropriate content",
|
INAPPROPRIATE: "Inappropriate content",
|
||||||
SPAM: "Spam",
|
SPAM: "Spam",
|
||||||
COPYRIGHT: "Copyrighted content",
|
COPYRIGHT: "Copyrighted content",
|
||||||
|
BAD_QUALITY: "Bad quality",
|
||||||
OTHER: "Other...",
|
OTHER: "Other...",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { FileWithPath } from "react-dropzone";
|
||||||
import { Icon } from "@iconify/react";
|
import { Icon } from "@iconify/react";
|
||||||
|
|
||||||
import qrcode from "qrcode-generator";
|
import qrcode from "qrcode-generator";
|
||||||
import { MiiGender, MiiPlatform } from "@prisma/client";
|
import { MiiGender, MiiMakeup, MiiPlatform } from "@prisma/client";
|
||||||
|
|
||||||
import { nameSchema, tagsSchema } from "@/lib/schemas";
|
import { nameSchema, tagsSchema } from "@/lib/schemas";
|
||||||
import { convertQrCode } from "@/lib/qr-codes";
|
import { convertQrCode } from "@/lib/qr-codes";
|
||||||
|
|
@ -52,6 +52,7 @@ export default function SubmitForm() {
|
||||||
|
|
||||||
const [platform, setPlatform] = useState<MiiPlatform>("SWITCH");
|
const [platform, setPlatform] = useState<MiiPlatform>("SWITCH");
|
||||||
const [gender, setGender] = useState<MiiGender>("MALE");
|
const [gender, setGender] = useState<MiiGender>("MALE");
|
||||||
|
const [makeup, setMakeup] = useState<MiiMakeup>("NONE");
|
||||||
const instructions = useRef<SwitchMiiInstructions>(defaultInstructions);
|
const instructions = useRef<SwitchMiiInstructions>(defaultInstructions);
|
||||||
|
|
||||||
const [error, setError] = useState<string | undefined>(undefined);
|
const [error, setError] = useState<string | undefined>(undefined);
|
||||||
|
|
@ -99,6 +100,7 @@ export default function SubmitForm() {
|
||||||
}
|
}
|
||||||
|
|
||||||
formData.append("gender", gender);
|
formData.append("gender", gender);
|
||||||
|
formData.append("makeup", makeup);
|
||||||
formData.append("miiPortraitImage", portraitBlob);
|
formData.append("miiPortraitImage", portraitBlob);
|
||||||
formData.append("miiFeaturesImage", featuresBlob);
|
formData.append("miiFeaturesImage", featuresBlob);
|
||||||
formData.append("instructions", JSON.stringify(instructions.current));
|
formData.append("instructions", JSON.stringify(instructions.current));
|
||||||
|
|
@ -326,6 +328,54 @@ export default function SubmitForm() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Makeup (switch only) */}
|
||||||
|
<div className={`w-full grid grid-cols-3 items-start ${platform === "SWITCH" ? "" : "hidden"}`}>
|
||||||
|
<label htmlFor="makeup" className="font-semibold py-2">
|
||||||
|
Makeup
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div className="col-span-2 flex gap-1">
|
||||||
|
{/* Full Makeup */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setMakeup("FULL")}
|
||||||
|
aria-label="Full makeup"
|
||||||
|
data-tooltip="Full Makeup"
|
||||||
|
className={`cursor-pointer rounded-xl flex justify-center items-center size-11 text-4xl border-2 transition-all after:bg-pink-400! after:border-pink-400! before:border-b-pink-400! ${
|
||||||
|
makeup === "FULL" ? "bg-pink-100 border-pink-400 shadow-md" : "bg-white border-gray-300 hover:border-gray-400"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Icon icon="mdi:palette" className="text-pink-400" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Partial Makeup */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setMakeup("PARTIAL")}
|
||||||
|
aria-label="Partial makeup"
|
||||||
|
data-tooltip="Partial Makeup"
|
||||||
|
className={`cursor-pointer rounded-xl flex justify-center items-center size-11 text-4xl border-2 transition-all after:bg-purple-400! after:border-purple-400! before:border-b-purple-400! ${
|
||||||
|
makeup === "PARTIAL" ? "bg-purple-100 border-purple-400 shadow-md" : "bg-white border-gray-300 hover:border-gray-400"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Icon icon="mdi:lipstick" className="text-purple-400" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* No Makeup */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setMakeup("NONE")}
|
||||||
|
aria-label="No makeup"
|
||||||
|
data-tooltip="No Makeup"
|
||||||
|
className={`cursor-pointer rounded-xl flex justify-center items-center size-11 text-4xl border-2 transition-all after:bg-gray-400! after:border-gray-400! before:border-b-gray-400! ${
|
||||||
|
makeup === "NONE" ? "bg-gray-200 border-gray-400 shadow-md" : "bg-white border-gray-300 hover:border-gray-400"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Icon icon="codex:cross" className="text-gray-400" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* (Switch Only) Mii Portrait */}
|
{/* (Switch Only) Mii Portrait */}
|
||||||
<div className={`${platform === "SWITCH" ? "" : "hidden"}`}>
|
<div className={`${platform === "SWITCH" ? "" : "hidden"}`}>
|
||||||
{/* Separator */}
|
{/* Separator */}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ import satori, { Font } from "satori";
|
||||||
import { Mii } from "@prisma/client";
|
import { Mii } from "@prisma/client";
|
||||||
|
|
||||||
const MIN_IMAGE_DIMENSIONS = [128, 128];
|
const MIN_IMAGE_DIMENSIONS = [128, 128];
|
||||||
const MAX_IMAGE_DIMENSIONS = [2000, 2000];
|
const MAX_IMAGE_DIMENSIONS = [8000, 8000];
|
||||||
const MAX_IMAGE_SIZE = 8 * 1024 * 1024; // 8 MB
|
const MAX_IMAGE_SIZE = 8 * 1024 * 1024; // 8 MB
|
||||||
const ALLOWED_MIME_TYPES = ["image/jpeg", "image/png", "image/gif", "image/webp"];
|
const ALLOWED_MIME_TYPES = ["image/jpeg", "image/png", "image/gif", "image/webp"];
|
||||||
|
|
||||||
|
|
@ -49,7 +49,7 @@ export async function validateImage(file: File): Promise<{ valid: boolean; error
|
||||||
metadata.height < MIN_IMAGE_DIMENSIONS[1] ||
|
metadata.height < MIN_IMAGE_DIMENSIONS[1] ||
|
||||||
metadata.height > MAX_IMAGE_DIMENSIONS[1]
|
metadata.height > MAX_IMAGE_DIMENSIONS[1]
|
||||||
) {
|
) {
|
||||||
return { valid: false, error: "Image dimensions are invalid. Resolution must be between 128x128 and 2000x2000" };
|
return { valid: false, error: "Image dimensions are invalid. Resolution must be between 128x128 and 8000x8000" };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for inappropriate content
|
// Check for inappropriate content
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { MiiGender, MiiPlatform } from "@prisma/client";
|
import { MiiGender, MiiMakeup, MiiPlatform } from "@prisma/client";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
// profanity censoring bypasses the regex in some of these but I think it's funny
|
// profanity censoring bypasses the regex in some of these but I think it's funny
|
||||||
|
|
@ -60,6 +60,7 @@ export const searchSchema = z.object({
|
||||||
),
|
),
|
||||||
platform: z.enum(MiiPlatform, { error: "Platform must be either 'THREE_DS', or 'SWITCH'" }).optional(),
|
platform: z.enum(MiiPlatform, { error: "Platform must be either 'THREE_DS', or 'SWITCH'" }).optional(),
|
||||||
gender: z.enum(MiiGender, { error: "Gender must be either 'MALE', 'FEMALE', or 'NONBINARY' if on Switch platform" }).optional(),
|
gender: z.enum(MiiGender, { error: "Gender must be either 'MALE', 'FEMALE', or 'NONBINARY' if on Switch platform" }).optional(),
|
||||||
|
makeup: z.enum(MiiMakeup, { error: "Makeup must be either 'FULL', 'PARTIAL', or 'NONE'" }).optional(),
|
||||||
allowCopying: z.coerce.boolean({ error: "Allow Copying must be either true or false" }).optional(),
|
allowCopying: z.coerce.boolean({ error: "Allow Copying must be either true or false" }).optional(),
|
||||||
// todo: incorporate tagsSchema
|
// todo: incorporate tagsSchema
|
||||||
// Pages
|
// Pages
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue