From c242533e7b77d1cbe3d52d8d789beaae2eb151ad Mon Sep 17 00:00:00 2001 From: "Kalle (Sendou)" <38327916+Sendouc@users.noreply.github.com> Date: Wed, 21 Oct 2020 21:22:04 +0300 Subject: [PATCH] react hook form with validation --- graphql/context.ts | 18 +- graphql/schema/user.ts | 8 +- lib/lists/weaponsWithHero.ts | 141 ++++++++++++++ lib/useMockT.ts | 3 + .../public/locales/en/translation.json | 5 +- package-lock.json | 84 ++++++++- package.json | 9 +- pages/api/graphql.ts | 4 +- scenes/Layout/components/TopNav.tsx | 2 - scenes/Profile/components/ProfileModal.tsx | 135 ++++++++++++++ scenes/Profile/index.tsx | 11 +- validators/Profile.ts | 173 ++++++++++++++++++ 12 files changed, 561 insertions(+), 32 deletions(-) create mode 100644 lib/lists/weaponsWithHero.ts create mode 100644 scenes/Profile/components/ProfileModal.tsx create mode 100644 validators/Profile.ts diff --git a/graphql/context.ts b/graphql/context.ts index fcae16097..e5ae78876 100644 --- a/graphql/context.ts +++ b/graphql/context.ts @@ -1,22 +1,14 @@ import { PrismaClient } from "@prisma/client"; import { NextApiRequest } from "next"; -import { getSession } from "next-auth/client"; const prisma = new PrismaClient({ log: ["query"] }); export interface Context { prisma: PrismaClient; - // FIXME: type - session: any; + req?: NextApiRequest; } -export const createContext = async (req: NextApiRequest): Promise => { - const session = await getSession({ req }); - - console.log({ session }); - - return { - prisma, - session, - }; -}; +export const createContext = ({ req }: { req: NextApiRequest }): Context => ({ + prisma, + req, +}); diff --git a/graphql/schema/user.ts b/graphql/schema/user.ts index 12f5f8d09..56081cf5f 100644 --- a/graphql/schema/user.ts +++ b/graphql/schema/user.ts @@ -5,6 +5,7 @@ import { queryType, stringArg, } from "@nexus/schema"; +import { getSession } from "next-auth/client"; export const User = objectType({ name: "User", @@ -107,8 +108,11 @@ export const Mutation = mutationType({ authorize: async (_root, _args, ctx) => { return true; }, - resolve: async (_root, args, ctx, a) => { - // FIXME: validate + resolve: async (_root, args, ctx) => { + const session = await getSession({ req: ctx.req }); + console.log({ session }); + + // FIXME: set custom url to lowerCase return await ctx.prisma.profile.upsert({ create: { user: { connect: { id: 4 } }, ...args.profile }, // FIXME: doing it like this makes removing values impossible? diff --git a/lib/lists/weaponsWithHero.ts b/lib/lists/weaponsWithHero.ts new file mode 100644 index 000000000..e4b28fd77 --- /dev/null +++ b/lib/lists/weaponsWithHero.ts @@ -0,0 +1,141 @@ +export const weaponsWithHero = [ + "Sploosh-o-matic", + "Neo Sploosh-o-matic", + "Sploosh-o-matic 7", + "Splattershot Jr.", + "Custom Splattershot Jr.", + "Kensa Splattershot Jr.", + "Splash-o-matic", + "Neo Splash-o-matic", + "Aerospray MG", + "Aerospray RG", + "Aerospray PG", + "Splattershot", + "Hero Shot Replica", + "Tentatek Splattershot", + "Octo Shot Replica", + "Kensa Splattershot", + ".52 Gal", + ".52 Gal Deco", + "Kensa .52 Gal", + "N-ZAP '85", + "N-ZAP '89", + "N-ZAP '83", + "Splattershot Pro", + "Forge Splattershot Pro", + "Kensa Splattershot Pro", + ".96 Gal", + ".96 Gal Deco", + "Jet Squelcher", + "Custom Jet Squelcher", + "L-3 Nozzlenose", + "L-3 Nozzlenose D", + "Kensa L-3 Nozzlenose", + "H-3 Nozzlenose", + "H-3 Nozzlenose D", + "Cherry H-3 Nozzlenose", + "Squeezer", + "Foil Squeezer", + "Luna Blaster", + "Luna Blaster Neo", + "Kensa Luna Blaster", + "Blaster", + "Hero Blaster Replica", + "Custom Blaster", + "Range Blaster", + "Custom Range Blaster", + "Grim Range Blaster", + "Rapid Blaster", + "Rapid Blaster Deco", + "Kensa Rapid Blaster", + "Rapid Blaster Pro", + "Rapid Blaster Pro Deco", + "Clash Blaster", + "Clash Blaster Neo", + "Carbon Roller", + "Carbon Roller Deco", + "Splat Roller", + "Hero Roller Replica", + "Krak-On Splat Roller", + "Kensa Splat Roller", + "Dynamo Roller", + "Gold Dynamo Roller", + "Kensa Dynamo Roller", + "Flingza Roller", + "Foil Flingza Roller", + "Inkbrush", + "Inkbrush Nouveau", + "Permanent Inkbrush", + "Octobrush", + "Herobrush Replica", + "Octobrush Nouveau", + "Kensa Octobrush", + "Classic Squiffer", + "New Squiffer", + "Fresh Squiffer", + "Splat Charger", + "Hero Charger Replica", + "Firefin Splat Charger", + "Kensa Charger", + "Splatterscope", + "Firefin Splatterscope", + "Kensa Splatterscope", + "E-liter 4K", + "Custom E-liter 4K", + "E-liter 4K Scope", + "Custom E-liter 4K Scope", + "Bamboozler 14 Mk I", + "Bamboozler 14 Mk II", + "Bamboozler 14 Mk III", + "Goo Tuber", + "Custom Goo Tuber", + "Slosher", + "Hero Slosher Replica", + "Slosher Deco", + "Soda Slosher", + "Tri-Slosher", + "Tri-Slosher Nouveau", + "Sloshing Machine", + "Sloshing Machine Neo", + "Kensa Sloshing Machine", + "Bloblobber", + "Bloblobber Deco", + "Explosher", + "Custom Explosher", + "Mini Splatling", + "Zink Mini Splatling", + "Kensa Mini Splatling", + "Heavy Splatling", + "Hero Splatling Replica", + "Heavy Splatling Deco", + "Heavy Splatling Remix", + "Hydra Splatling", + "Custom Hydra Splatling", + "Ballpoint Splatling", + "Ballpoint Splatling Nouveau", + "Nautilus 47", + "Nautilus 79", + "Dapple Dualies", + "Dapple Dualies Nouveau", + "Clear Dapple Dualies", + "Splat Dualies", + "Hero Dualie Replicas", + "Enperry Splat Dualies", + "Kensa Splat Dualies", + "Glooga Dualies", + "Glooga Dualies Deco", + "Kensa Glooga Dualies", + "Dualie Squelchers", + "Custom Dualie Squelchers", + "Dark Tetra Dualies", + "Light Tetra Dualies", + "Splat Brella", + "Hero Brella Replica", + "Sorella Brella", + "Tenta Brella", + "Tenta Sorella Brella", + "Tenta Camo Brella", + "Undercover Brella", + "Undercover Sorella Brella", + "Kensa Undercover Brella", +] as const; diff --git a/lib/useMockT.ts b/lib/useMockT.ts index 1fa41f0bb..0d3e81e4a 100644 --- a/lib/useMockT.ts +++ b/lib/useMockT.ts @@ -1514,6 +1514,9 @@ const translations = { "Best placement / power": "Best placement / power", noPlacementsPromptNew: "If you have reached Top 500 in a <1>finished X Rank season you can have it displayed here! Simply contact Sendou#0043 on Discord with your in-game nick. Once set up new results are added automatically and no further action is needed.", + "Twitter name": "Twitter name", + "Twitch name": "Twitch name", + "YouTube channel ID": "YouTube channel ID", }, freeagents: { loginPrompt: "Log in to make your own free agent post and start matching!", diff --git a/old/frontend-react/public/locales/en/translation.json b/old/frontend-react/public/locales/en/translation.json index a7c2c5e89..7a0043117 100644 --- a/old/frontend-react/public/locales/en/translation.json +++ b/old/frontend-react/public/locales/en/translation.json @@ -1441,7 +1441,10 @@ "Highest power": "Highest power", "Number of placements": "Number of placements", "Best placement / power": "Best placement / power", - "noPlacementsPromptNew": "If you have reached Top 500 in a <1>finished X Rank season you can have it displayed here! Simply contact Sendou#0043 on Discord with your in-game nick. Once set up new results are added automatically and no further action is needed." + "noPlacementsPromptNew": "If you have reached Top 500 in a <1>finished X Rank season you can have it displayed here! Simply contact Sendou#0043 on Discord with your in-game nick. Once set up new results are added automatically and no further action is needed.", + "Twitter name": "Twitter name", + "Twitch name": "Twitch name", + "YouTube channel ID": "YouTube channel ID" }, "freeagents": { "loginPrompt": "Log in to make your own free agent post and start matching!", diff --git a/package-lock.json b/package-lock.json index 687458357..1b0e0ccda 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@apollo/client": "^3.2.5", "@chakra-ui/core": "^1.0.0-rc.5", "@chakra-ui/theme-tools": "^1.0.0-rc.5", + "@hookform/resolvers": "^1.0.0", "@nexus/schema": "^0.16.0", "@prisma/client": "^2.9.0", "apollo-server-micro": "^2.18.2", @@ -22,9 +23,12 @@ "nexus-plugin-prisma": "^0.22.0", "react": "^16.14.0", "react-dom": "^16.14.0", + "react-hook-form": "^6.9.5", "react-icons": "^3.11.0", "react-markdown": "^5.0.0", - "react-string-replace": "^0.4.4" + "react-string-replace": "^0.4.4", + "validator": "^13.1.17", + "zod": "^1.11.9" }, "devDependencies": { "@graphql-codegen/cli": "1.18.0", @@ -32,8 +36,9 @@ "@graphql-codegen/typescript-operations": "1.17.8", "@graphql-codegen/typescript-react-apollo": "2.0.7", "@prisma/cli": "^2.9.0", - "@types/next-auth": "^3.1.13", + "@types/next-auth": "^3.1.14", "@types/react": "^16.9.53", + "@types/validator": "^13.1.0", "cypress": "^5.4.0", "prettier": "^2.1.2", "ts-node": "^9.0.0", @@ -3497,6 +3502,14 @@ "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.1.0.tgz", "integrity": "sha512-i9YbZPN3QgfighY/1X1Pu118VUz2Fmmhd6b2n0/O8YVgGGfw0FbUYoA97k7FkpGJ+pLCFEDLUmAPPV4D1kpeFw==" }, + "node_modules/@hookform/resolvers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-1.0.0.tgz", + "integrity": "sha512-YzBq6ZFw/uWGa3rXBNSHqnsE4hDXLrzdboDxPRKGjYHVzs1dBxjvELftP8iTmRPqP32VjnbVfUktX1CQ6Y7sog==", + "peerDependencies": { + "react-hook-form": ">=6.6.0" + } + }, "node_modules/@next/env": { "version": "9.5.5", "resolved": "https://registry.npmjs.org/@next/env/-/env-9.5.5.tgz", @@ -4129,9 +4142,9 @@ "integrity": "sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q==" }, "node_modules/@types/next-auth": { - "version": "3.1.13", - "resolved": "https://registry.npmjs.org/@types/next-auth/-/next-auth-3.1.13.tgz", - "integrity": "sha512-1HKyYcrPFOEuTmKmqDVbXA/1em1OrE65m+HaZP8/uC0Ups1xbUwCMeuJYn8p83MGNJI0JGF0D5vm6ZWdK2bxZw==", + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/@types/next-auth/-/next-auth-3.1.14.tgz", + "integrity": "sha512-XL70zlxHCmhAkHg4P6lE+gf9WUeM65UZsjwGUDGDPNQTrB3euwm7Z25WlLxG0Z0rsdZmBsXw5kiS+dmuZ/xqlA==", "dev": true, "dependencies": { "@types/node": "*", @@ -4227,6 +4240,12 @@ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.3.tgz", "integrity": "sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ==" }, + "node_modules/@types/validator": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.1.0.tgz", + "integrity": "sha512-gHUHI6pJaANIO2r6WcbT7+WMgbL9GZooR4tWpuBOETpDIqFNxwaJluE+6rj6VGYe8k6OkfhbHz2Fkm8kl06Igw==", + "dev": true + }, "node_modules/@types/warning": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.0.tgz", @@ -12729,6 +12748,14 @@ "react": "^16.8.0" } }, + "node_modules/react-hook-form": { + "version": "6.9.5", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-6.9.5.tgz", + "integrity": "sha512-kkUm6b4u1Iy3wCdHA0fal23sjGQMg8BBRAT3KBG9PEdls5e+7OX2Df20oBPgpXxo/GaIwk2Lh5DeVv5OeVjKsg==", + "peerDependencies": { + "react": "^16.8.0" + } + }, "node_modules/react-icons": { "version": "3.11.0", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-3.11.0.tgz", @@ -15590,6 +15617,14 @@ "integrity": "sha1-HBRHm0DxOXp1eC8RXkCGRHQzogA=", "dev": true }, + "node_modules/validator": { + "version": "13.1.17", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.1.17.tgz", + "integrity": "sha512-zL5QBoemJ3jYFb2/j38y7ljhwYGXVLUp8H6W1nVxadnAOvUOytec+L7BHh1oBQ82/TzWXHd+GSaxUWp4lROkLg==", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/verror": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", @@ -16651,6 +16686,11 @@ "tslib": "^1.9.3", "zen-observable": "^0.8.0" } + }, + "node_modules/zod": { + "version": "1.11.9", + "resolved": "https://registry.npmjs.org/zod/-/zod-1.11.9.tgz", + "integrity": "sha512-qZjs9DkvPYHOiOUdAtNcxOC0u5cv7tx9DCmlNZN0MxWeFvgqyr3XkXFqUlaSpmTiZ4A4YVkB2s1Zw2ENJ9/fSg==" } }, "dependencies": { @@ -19529,6 +19569,12 @@ "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.1.0.tgz", "integrity": "sha512-i9YbZPN3QgfighY/1X1Pu118VUz2Fmmhd6b2n0/O8YVgGGfw0FbUYoA97k7FkpGJ+pLCFEDLUmAPPV4D1kpeFw==" }, + "@hookform/resolvers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-1.0.0.tgz", + "integrity": "sha512-YzBq6ZFw/uWGa3rXBNSHqnsE4hDXLrzdboDxPRKGjYHVzs1dBxjvELftP8iTmRPqP32VjnbVfUktX1CQ6Y7sog==", + "requires": {} + }, "@next/env": { "version": "9.5.5", "resolved": "https://registry.npmjs.org/@next/env/-/env-9.5.5.tgz", @@ -20063,9 +20109,9 @@ "integrity": "sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q==" }, "@types/next-auth": { - "version": "3.1.13", - "resolved": "https://registry.npmjs.org/@types/next-auth/-/next-auth-3.1.13.tgz", - "integrity": "sha512-1HKyYcrPFOEuTmKmqDVbXA/1em1OrE65m+HaZP8/uC0Ups1xbUwCMeuJYn8p83MGNJI0JGF0D5vm6ZWdK2bxZw==", + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/@types/next-auth/-/next-auth-3.1.14.tgz", + "integrity": "sha512-XL70zlxHCmhAkHg4P6lE+gf9WUeM65UZsjwGUDGDPNQTrB3euwm7Z25WlLxG0Z0rsdZmBsXw5kiS+dmuZ/xqlA==", "dev": true, "requires": { "@types/node": "*", @@ -20161,6 +20207,12 @@ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.3.tgz", "integrity": "sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ==" }, + "@types/validator": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.1.0.tgz", + "integrity": "sha512-gHUHI6pJaANIO2r6WcbT7+WMgbL9GZooR4tWpuBOETpDIqFNxwaJluE+6rj6VGYe8k6OkfhbHz2Fkm8kl06Igw==", + "dev": true + }, "@types/warning": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.0.tgz", @@ -27011,6 +27063,12 @@ "use-sidecar": "^1.0.1" } }, + "react-hook-form": { + "version": "6.9.5", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-6.9.5.tgz", + "integrity": "sha512-kkUm6b4u1Iy3wCdHA0fal23sjGQMg8BBRAT3KBG9PEdls5e+7OX2Df20oBPgpXxo/GaIwk2Lh5DeVv5OeVjKsg==", + "requires": {} + }, "react-icons": { "version": "3.11.0", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-3.11.0.tgz", @@ -29238,6 +29296,11 @@ "integrity": "sha1-HBRHm0DxOXp1eC8RXkCGRHQzogA=", "dev": true }, + "validator": { + "version": "13.1.17", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.1.17.tgz", + "integrity": "sha512-zL5QBoemJ3jYFb2/j38y7ljhwYGXVLUp8H6W1nVxadnAOvUOytec+L7BHh1oBQ82/TzWXHd+GSaxUWp4lROkLg==" + }, "verror": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", @@ -30093,6 +30156,11 @@ "tslib": "^1.9.3", "zen-observable": "^0.8.0" } + }, + "zod": { + "version": "1.11.9", + "resolved": "https://registry.npmjs.org/zod/-/zod-1.11.9.tgz", + "integrity": "sha512-qZjs9DkvPYHOiOUdAtNcxOC0u5cv7tx9DCmlNZN0MxWeFvgqyr3XkXFqUlaSpmTiZ4A4YVkB2s1Zw2ENJ9/fSg==" } } } diff --git a/package.json b/package.json index 830ac7f55..b50f4383e 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@apollo/client": "^3.2.5", "@chakra-ui/core": "^1.0.0-rc.5", "@chakra-ui/theme-tools": "^1.0.0-rc.5", + "@hookform/resolvers": "^1.0.0", "@nexus/schema": "^0.16.0", "@prisma/client": "^2.9.0", "apollo-server-micro": "^2.18.2", @@ -31,9 +32,12 @@ "nexus-plugin-prisma": "^0.22.0", "react": "^16.14.0", "react-dom": "^16.14.0", + "react-hook-form": "^6.9.5", "react-icons": "^3.11.0", "react-markdown": "^5.0.0", - "react-string-replace": "^0.4.4" + "react-string-replace": "^0.4.4", + "validator": "^13.1.17", + "zod": "^1.11.9" }, "devDependencies": { "@graphql-codegen/cli": "1.18.0", @@ -41,8 +45,9 @@ "@graphql-codegen/typescript-operations": "1.17.8", "@graphql-codegen/typescript-react-apollo": "2.0.7", "@prisma/cli": "^2.9.0", - "@types/next-auth": "^3.1.13", + "@types/next-auth": "^3.1.14", "@types/react": "^16.9.53", + "@types/validator": "^13.1.0", "cypress": "^5.4.0", "prettier": "^2.1.2", "ts-node": "^9.0.0", diff --git a/pages/api/graphql.ts b/pages/api/graphql.ts index 3cecd38dd..1c6577ba7 100644 --- a/pages/api/graphql.ts +++ b/pages/api/graphql.ts @@ -10,8 +10,6 @@ export const config = { export default new ApolloServer({ schema, - context: ({ req }) => { - return createContext(req); - }, + context: createContext, tracing: process.env.NODE_ENV === "development", }).createHandler({ path: "/api/graphql" }); diff --git a/scenes/Layout/components/TopNav.tsx b/scenes/Layout/components/TopNav.tsx index 18967be49..bca7167aa 100644 --- a/scenes/Layout/components/TopNav.tsx +++ b/scenes/Layout/components/TopNav.tsx @@ -31,8 +31,6 @@ const TopNav = () => { if (loading) return ; - console.log({ user }); - if (!user) { return ( + + + + + + + ); +}; + +export default ProfileModal; diff --git a/scenes/Profile/index.tsx b/scenes/Profile/index.tsx index 423d63d14..40a8cd1fd 100644 --- a/scenes/Profile/index.tsx +++ b/scenes/Profile/index.tsx @@ -1,18 +1,27 @@ -import { Box } from "@chakra-ui/core"; +import { Box, Button } from "@chakra-ui/core"; import Markdown from "components/Markdown"; import MyHead from "components/MyHead"; import { GetUserByIdentifierQuery } from "generated/graphql"; +import { useTranslation } from "lib/useMockT"; +import { useState } from "react"; import AvatarWithInfo from "./components/AvatarWithInfo"; +import ProfileModal from "./components/ProfileModal"; interface Props { user: NonNullable; } const Profile: React.FC = ({ user }) => { + const { t } = useTranslation(); + const [showModal, setShowModal] = useState(false); return ( <> + + setShowModal(false)} /> {user.profile?.bio && ( diff --git a/validators/Profile.ts b/validators/Profile.ts new file mode 100644 index 000000000..e21642f67 --- /dev/null +++ b/validators/Profile.ts @@ -0,0 +1,173 @@ +//import isISO31661Alpha2 from "validator/es/lib/isISO31661Alpha2"; +import * as z from "zod"; + +export const ProfileSchema = z.object({ + bio: z.string().max(10000).optional(), + country: z + .string() + //.refine((val) => isISO31661Alpha2(val)) + .optional(), + customUrlPath: z.string().max(32).optional(), + sensMotion: z + .number() + .min(5) + .max(5) + .refine((val) => (val * 10) % 5 === 0) + .optional(), + sensStick: z + .number() + .min(5) + .max(5) + .refine((val) => (val * 10) % 5 === 0) + .optional(), + twitchName: z.string().max(25).optional(), + twitterName: z.string().max(15).optional(), + youtubeId: z.string().optional(), + // weaponsWithHero + weaponPool: z + .array( + z.enum([ + "Sploosh-o-matic", + "Neo Sploosh-o-matic", + "Sploosh-o-matic 7", + "Splattershot Jr.", + "Custom Splattershot Jr.", + "Kensa Splattershot Jr.", + "Splash-o-matic", + "Neo Splash-o-matic", + "Aerospray MG", + "Aerospray RG", + "Aerospray PG", + "Splattershot", + "Hero Shot Replica", + "Tentatek Splattershot", + "Octo Shot Replica", + "Kensa Splattershot", + ".52 Gal", + ".52 Gal Deco", + "Kensa .52 Gal", + "N-ZAP '85", + "N-ZAP '89", + "N-ZAP '83", + "Splattershot Pro", + "Forge Splattershot Pro", + "Kensa Splattershot Pro", + ".96 Gal", + ".96 Gal Deco", + "Jet Squelcher", + "Custom Jet Squelcher", + "L-3 Nozzlenose", + "L-3 Nozzlenose D", + "Kensa L-3 Nozzlenose", + "H-3 Nozzlenose", + "H-3 Nozzlenose D", + "Cherry H-3 Nozzlenose", + "Squeezer", + "Foil Squeezer", + "Luna Blaster", + "Luna Blaster Neo", + "Kensa Luna Blaster", + "Blaster", + "Hero Blaster Replica", + "Custom Blaster", + "Range Blaster", + "Custom Range Blaster", + "Grim Range Blaster", + "Rapid Blaster", + "Rapid Blaster Deco", + "Kensa Rapid Blaster", + "Rapid Blaster Pro", + "Rapid Blaster Pro Deco", + "Clash Blaster", + "Clash Blaster Neo", + "Carbon Roller", + "Carbon Roller Deco", + "Splat Roller", + "Hero Roller Replica", + "Krak-On Splat Roller", + "Kensa Splat Roller", + "Dynamo Roller", + "Gold Dynamo Roller", + "Kensa Dynamo Roller", + "Flingza Roller", + "Foil Flingza Roller", + "Inkbrush", + "Inkbrush Nouveau", + "Permanent Inkbrush", + "Octobrush", + "Herobrush Replica", + "Octobrush Nouveau", + "Kensa Octobrush", + "Classic Squiffer", + "New Squiffer", + "Fresh Squiffer", + "Splat Charger", + "Hero Charger Replica", + "Firefin Splat Charger", + "Kensa Charger", + "Splatterscope", + "Firefin Splatterscope", + "Kensa Splatterscope", + "E-liter 4K", + "Custom E-liter 4K", + "E-liter 4K Scope", + "Custom E-liter 4K Scope", + "Bamboozler 14 Mk I", + "Bamboozler 14 Mk II", + "Bamboozler 14 Mk III", + "Goo Tuber", + "Custom Goo Tuber", + "Slosher", + "Hero Slosher Replica", + "Slosher Deco", + "Soda Slosher", + "Tri-Slosher", + "Tri-Slosher Nouveau", + "Sloshing Machine", + "Sloshing Machine Neo", + "Kensa Sloshing Machine", + "Bloblobber", + "Bloblobber Deco", + "Explosher", + "Custom Explosher", + "Mini Splatling", + "Zink Mini Splatling", + "Kensa Mini Splatling", + "Heavy Splatling", + "Hero Splatling Replica", + "Heavy Splatling Deco", + "Heavy Splatling Remix", + "Hydra Splatling", + "Custom Hydra Splatling", + "Ballpoint Splatling", + "Ballpoint Splatling Nouveau", + "Nautilus 47", + "Nautilus 79", + "Dapple Dualies", + "Dapple Dualies Nouveau", + "Clear Dapple Dualies", + "Splat Dualies", + "Hero Dualie Replicas", + "Enperry Splat Dualies", + "Kensa Splat Dualies", + "Glooga Dualies", + "Glooga Dualies Deco", + "Kensa Glooga Dualies", + "Dualie Squelchers", + "Custom Dualie Squelchers", + "Dark Tetra Dualies", + "Light Tetra Dualies", + "Splat Brella", + "Hero Brella Replica", + "Sorella Brella", + "Tenta Brella", + "Tenta Sorella Brella", + "Tenta Camo Brella", + "Undercover Brella", + "Undercover Sorella Brella", + "Kensa Undercover Brella", + ]) + ) + .max(5) + .optional(), +});