diff --git a/app/drizzle/constants.ts b/app/drizzle/constants.ts index 5a54afcf..8032e0f8 100644 --- a/app/drizzle/constants.ts +++ b/app/drizzle/constants.ts @@ -1,6 +1,14 @@ export const GameAssetTypes = ["STATIC", "ANIMATION"] as const; export type GameAssetType = (typeof GameAssetTypes)[number]; +export const CoreVillages = [ + "Shine", + "Tsukimori", + "Glacier", + "Shroud", + "Current", +] as const; + export const LetterRanks = ["D", "C", "B", "A", "S", "H"] as const; export type LetterRank = (typeof LetterRanks)[number]; diff --git a/app/package.json b/app/package.json index 51befcf2..b282d83b 100644 --- a/app/package.json +++ b/app/package.json @@ -36,6 +36,7 @@ "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-popover": "^1.0.7", + "@radix-ui/react-radio-group": "^1.2.1", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-switch": "^1.0.3", @@ -74,6 +75,7 @@ "dotenv": "^16.1.4", "drizzle-orm": "0.33.0", "drizzle-zod": "0.5.1", + "embla-carousel-react": "^8.5.1", "emoji-picker-react": "^4.9.2", "eslint-plugin-drizzle": "^0.2.3", "honeycomb-grid": "4.1.5", diff --git a/app/pnpm-lock.yaml b/app/pnpm-lock.yaml index 3545d839..71012ecb 100644 --- a/app/pnpm-lock.yaml +++ b/app/pnpm-lock.yaml @@ -50,6 +50,9 @@ importers: '@radix-ui/react-popover': specifier: ^1.0.7 version: 1.0.7(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-radio-group': + specifier: ^1.2.1 + version: 1.2.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@radix-ui/react-select': specifier: ^2.0.0 version: 2.0.0(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -164,6 +167,9 @@ importers: drizzle-zod: specifier: 0.5.1 version: 0.5.1(drizzle-orm@0.33.0(@opentelemetry/api@1.9.0)(@planetscale/database@1.16.0)(@types/pg@8.6.1)(@types/react@18.2.48)(mysql2@3.10.1)(react@18.2.0))(zod@3.23.8) + embla-carousel-react: + specifier: ^8.5.1 + version: 8.5.1(react@18.2.0) emoji-picker-react: specifier: ^4.9.2 version: 4.9.2(react@18.2.0) @@ -1517,6 +1523,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-context@1.1.1': + resolution: {integrity: sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-dialog@1.0.0': resolution: {integrity: sha512-Yn9YU+QlHYLWwV1XfKiqnGVpWYWk6MeBVM6x/bcoyPvxgjQGoeT35482viLPctTMWoMw0PoHgqfSox7Ig+957Q==} peerDependencies: @@ -1724,6 +1739,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-presence@1.1.1': + resolution: {integrity: sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-primitive@1.0.0': resolution: {integrity: sha512-EyXe6mnRlHZ8b6f4ilTDrXmkLShICIuOTTj0GX4w1rp+wSxf3+TD05u1UOITC8VsJ2a9nwHvdXtOXEOl0Cw/zQ==} peerDependencies: @@ -1756,6 +1784,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-radio-group@1.2.1': + resolution: {integrity: sha512-kdbv54g4vfRjja9DNWPMxKvXblzqbpEC8kspEkZ6dVP7kQksGCn+iZHkcCz2nb00+lPdRvxrqy4WrvvV1cNqrQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-roving-focus@1.1.0': resolution: {integrity: sha512-EA6AMGeq9AEeQDeSH0aZgG198qkfHSbvWTf1HvoDmOB5bBG/qTxjYMWUKMnYiV6J/iP/J8MEFSuB2zRU2n7ODA==} peerDependencies: @@ -1949,6 +1990,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-previous@1.1.0': + resolution: {integrity: sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-use-rect@1.0.1': resolution: {integrity: sha512-Cq5DLuSiuYVKNU8orzJMbl15TXilTnJKUCltMVQg53BQOF1/C5toAaGrowkgksdBQ9H+SRL23g0HDmg9tvmxXw==} peerDependencies: @@ -1967,6 +2017,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-size@1.1.0': + resolution: {integrity: sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-visually-hidden@1.0.3': resolution: {integrity: sha512-D4w41yN5YRKtu464TLnByKzMDG/JlMPHtfZgQAu9v6mNakUqGUI9vUrfQKz8NK41VMm/xbZbh76NUTVtIYqOMA==} peerDependencies: @@ -3473,6 +3532,19 @@ packages: electron-to-chromium@1.5.11: resolution: {integrity: sha512-R1CccCDYqndR25CaXFd6hp/u9RaaMcftMkphmvuepXr5b1vfLkRml6aWVeBhXJ7rbevHkKEMJtz8XqPf7ffmew==} + embla-carousel-react@8.5.1: + resolution: {integrity: sha512-z9Y0K84BJvhChXgqn2CFYbfEi6AwEr+FFVVKm/MqbTQ2zIzO1VQri6w67LcfpVF0AjbhwVMywDZqY4alYkjW5w==} + peerDependencies: + react: ^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + + embla-carousel-reactive-utils@8.5.1: + resolution: {integrity: sha512-n7VSoGIiiDIc4MfXF3ZRTO59KDp820QDuyBDGlt5/65+lumPHxX2JLz0EZ23hZ4eg4vZGUXwMkYv02fw2JVo/A==} + peerDependencies: + embla-carousel: 8.5.1 + + embla-carousel@8.5.1: + resolution: {integrity: sha512-JUb5+FOHobSiWQ2EJNaueCNT/cQU9L6XWBbWmorWPQT9bkbk+fhsuLr8wWrzXKagO3oWszBO7MSx+GfaRk4E6A==} + emoji-picker-react@4.9.2: resolution: {integrity: sha512-pdvLKpto0DMrjE+/8V9QeYjrMcOkJmqBn3GyCSG2zanY32rN2cnWzBUmzArvapAjzBvgf7hNmJP8xmsdu0cmJA==} engines: {node: '>=10'} @@ -6958,6 +7030,12 @@ snapshots: optionalDependencies: '@types/react': 18.2.48 + '@radix-ui/react-context@1.1.1(@types/react@18.2.48)(react@18.2.0)': + dependencies: + react: 18.2.0 + optionalDependencies: + '@types/react': 18.2.48 + '@radix-ui/react-dialog@1.0.0(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@babel/runtime': 7.23.9 @@ -7198,6 +7276,16 @@ snapshots: '@types/react': 18.2.48 '@types/react-dom': 18.2.18 + '@radix-ui/react-presence@1.1.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.48)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.48 + '@types/react-dom': 18.2.18 + '@radix-ui/react-primitive@1.0.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@babel/runtime': 7.23.9 @@ -7224,6 +7312,24 @@ snapshots: '@types/react': 18.2.48 '@types/react-dom': 18.2.18 + '@radix-ui/react-radio-group@1.2.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-context': 1.1.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-direction': 1.1.0(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-presence': 1.1.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-roving-focus': 1.1.0(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-use-previous': 1.1.0(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-use-size': 1.1.0(@types/react@18.2.48)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.48 + '@types/react-dom': 18.2.18 + '@radix-ui/react-roving-focus@1.1.0(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@radix-ui/primitive': 1.1.0 @@ -7444,6 +7550,12 @@ snapshots: optionalDependencies: '@types/react': 18.2.48 + '@radix-ui/react-use-previous@1.1.0(@types/react@18.2.48)(react@18.2.0)': + dependencies: + react: 18.2.0 + optionalDependencies: + '@types/react': 18.2.48 + '@radix-ui/react-use-rect@1.0.1(@types/react@18.2.48)(react@18.2.0)': dependencies: '@babel/runtime': 7.23.9 @@ -7460,6 +7572,13 @@ snapshots: optionalDependencies: '@types/react': 18.2.48 + '@radix-ui/react-use-size@1.1.0(@types/react@18.2.48)(react@18.2.0)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.48)(react@18.2.0) + react: 18.2.0 + optionalDependencies: + '@types/react': 18.2.48 + '@radix-ui/react-visually-hidden@1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@babel/runtime': 7.23.9 @@ -9050,6 +9169,18 @@ snapshots: electron-to-chromium@1.5.11: {} + embla-carousel-react@8.5.1(react@18.2.0): + dependencies: + embla-carousel: 8.5.1 + embla-carousel-reactive-utils: 8.5.1(embla-carousel@8.5.1) + react: 18.2.0 + + embla-carousel-reactive-utils@8.5.1(embla-carousel@8.5.1): + dependencies: + embla-carousel: 8.5.1 + + embla-carousel@8.5.1: {} + emoji-picker-react@4.9.2(react@18.2.0): dependencies: flairup: 0.0.38 diff --git a/app/src/app/register/page.tsx b/app/src/app/register/page.tsx index 1d4c5dca..c091b725 100644 --- a/app/src/app/register/page.tsx +++ b/app/src/app/register/page.tsx @@ -1,9 +1,8 @@ "use client"; -import { useState, useEffect, useMemo } from "react"; +import { useState, useEffect } from "react"; import React from "react"; import Link from "next/link"; -import dynamic from "next/dynamic"; import { useRouter } from "next/navigation"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; @@ -28,24 +27,26 @@ import { import { Input } from "@/components/ui/input"; import { Checkbox } from "@/components/ui/checkbox"; import { Button } from "@/components/ui/button"; -import { UserPlus } from "lucide-react"; -import { fetchMap } from "@/libs/travel/globe"; +import { MonitorPlay } from "lucide-react"; import { useUserData } from "@/utils/UserContext"; import { api } from "@/app/_trpc/client"; import { registrationSchema } from "@/validators/register"; import { attributes } from "@/validators/register"; import { colors, skin_colors } from "@/validators/register"; import { genders } from "@/validators/register"; -import { showMutationToast } from "@/libs/toast"; +import { showMutationToast, showFormErrorsToast } from "@/libs/toast"; +import { Label } from "@/components/ui/label"; +import { Carousel, CarouselContent, CarouselItem } from "@/components/ui/carousel"; +import { CarouselNext, CarouselPrevious } from "@/components/ui/carousel"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +import type { CarouselApi } from "@/components/ui/carousel"; import type { RegistrationSchema } from "@/validators/register"; -const Map = dynamic(() => import("@/layout/Map")); - const Register: React.FC = () => { - const [map, setMap] = useState> | null>(null); - void useMemo(async () => { - setMap(await fetchMap()); - }, []); + // Carousel state + const [cApi, setCApi] = useState(); + const [current, setCurrent] = useState(0); + const [count, setCount] = useState(0); // Router const router = useRouter(); @@ -56,9 +57,6 @@ const Register: React.FC = () => { // User data const { data: userData, status: userStatus } = useUserData(); - // Available villages - const { data: villages } = api.village.getAll.useQuery(undefined); - // Create avatar mutation const createAvatar = api.avatar.createAvatar.useMutation(); @@ -76,11 +74,45 @@ const Register: React.FC = () => { // Form handling const form = useForm({ + mode: "all", resolver: zodResolver(registrationSchema), + defaultValues: { + username: "", + gender: undefined, + hair_color: undefined, + eye_color: undefined, + skin_color: undefined, + attribute_1: undefined, + attribute_2: undefined, + attribute_3: undefined, + question1: undefined, + question2: undefined, + question3: undefined, + question4: undefined, + question5: undefined, + question6: undefined, + }, }); + // Carousel control + useEffect(() => { + void form.trigger(); + if (!cApi) return; + + setCount(cApi.scrollSnapList().length); + setCurrent(cApi.selectedScrollSnap() + 1); + + cApi.on("select", () => { + setCurrent(cApi.selectedScrollSnap() + 1); + }); + }, [cApi, form]); + // Handle username changes const watchUsername = form.watch("username", ""); + const watchGender = form.watch("gender", undefined); + const watchAttr1 = form.watch("attribute_1", undefined); + const watchAttr2 = form.watch("attribute_2", undefined); + const watchAttr3 = form.watch("attribute_3", undefined); const errors = form.formState.errors; // Checking for unique username @@ -122,15 +154,10 @@ const Register: React.FC = () => { // Handle form submit const handleCreateCharacter = form.handleSubmit( (data) => createCharacter(data), - (errors) => console.error(errors), + (error) => showFormErrorsToast(error), ); // Options used for select fields - const option_attributes = attributes.map((attribute, index) => ( - - {attribute} - - )); const option_colors = colors.map((color, index) => ( {color} @@ -143,294 +170,689 @@ const Register: React.FC = () => { )); return ( - - {isPending && } + {!isPending && ( -
- -
-
- ( - - Select username - - - - - - )} - /> - ( - - Select village - + <> + + +
+ + + + ( + + Select username + + + +
+ + Public display name. + + +
+
+ )} + /> +
+ + ( +
+ + Select gender + +
+ + Gender of your ninja + + +
+
+
+
+ {watchGender === "Male" && ( +

+ )} + {watchGender === "Female" && ( +

+ )} +
+
+
+ )} + /> +
+ + ( + + Hair color + +
+ + Attribute 1 + + +
+
+ )} + /> + ( + + Eye color + +
+ + Attribute 2 + + +
+
+ )} + /> + ( + + Skin color + +
+ + Attribute 3 + + +
+
+ )} + /> +
+ + ( + + Attribute #1 + +
+ + Customize + + +
+
+ )} + /> + ( + + Attribute #2 + +
+ + Customize + + +
+
+ )} + /> + ( + + Attribute #3 + +
+ + Customize + + +
+
+ )} + /> +
+ + ( + + + What environment feels most like home to you? + + + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+ )} + /> +
+ + ( + + + Which element do you feel most connected to? + + + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+ )} + /> +
+ + ( + + + What type of activity do you prefer? + + + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+ )} + /> +
+ + ( + + + How do you approach a problem? + + + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+ )} + /> +
+ + ( + + + What kind of landscape inspires you the most? + + + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+ )} + /> +
+ + ( + + + What motivates you the most? + + + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+ )} + /> +
+ +
+ ( + + + + +
+ + + {" "} + I have read & agree to the Terms of Service + + +
+
+ )} + /> + ( + + + + +
+ + + I have read & agree to the Privacy Policy + + +
+
+ )} + /> + ( + + + + +
+ + I accept that this is Early Access + + + Things (even if purchased with real money) may + radically change. + +
+
+ )} + /> +
+
- - - )} - /> - {villages && map && ( - - )} + +
+ +
+
+
+ + +
-
- ( - - Select gender - - - - )} - /> - ( - - Hair color - - - - )} - /> - ( - - Eye color - - - - )} - /> - ( - - Skin color - - - - )} - /> - ( - - Attribute #1 - - - - )} - /> - ( - - Attribute #2 - - - - )} - /> - ( - - Attribute #3 - - - - )} - /> -
-
- ( - - - - -
- - - {" "} - I have read & agree to the Terms of Service - - -
-
- )} - /> - ( - - - - -
- - - I have read & agree to the Privacy Policy - - -
-
- )} - /> - ( - - - - -
- I accept that this is Early Access - - Things (even if purchased with real money) may radically change. - -
-
- )} - /> - - - + + + +

+ Step {current} / {count} +

+ )} + {isPending && } ); }; diff --git a/app/src/components/ui/carousel.tsx b/app/src/components/ui/carousel.tsx new file mode 100644 index 00000000..7d131814 --- /dev/null +++ b/app/src/components/ui/carousel.tsx @@ -0,0 +1,258 @@ +import * as React from "react"; +import useEmblaCarousel, { type UseEmblaCarouselType } from "embla-carousel-react"; +import { cn } from "src/libs/shadui"; +import { Button } from "src/components/ui/button"; +import { ArrowLeftIcon, ArrowRightIcon } from "@radix-ui/react-icons"; + +type CarouselApi = UseEmblaCarouselType[1]; +type UseCarouselParameters = Parameters; +type CarouselOptions = UseCarouselParameters[0]; +type CarouselPlugin = UseCarouselParameters[1]; + +type CarouselProps = { + opts?: CarouselOptions; + plugins?: CarouselPlugin; + orientation?: "horizontal" | "vertical"; + setApi?: (api: CarouselApi) => void; +}; + +type CarouselContextProps = { + carouselRef: ReturnType[0]; + api: ReturnType[1]; + scrollPrev: () => void; + scrollNext: () => void; + canScrollPrev: boolean; + canScrollNext: boolean; +} & CarouselProps; + +const CarouselContext = React.createContext(null); + +function useCarousel() { + const context = React.useContext(CarouselContext); + + if (!context) { + throw new Error("useCarousel must be used within a "); + } + + return context; +} + +const Carousel = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & CarouselProps +>( + ( + { + orientation = "horizontal", + opts, + setApi, + plugins, + className, + children, + ...props + }, + ref, + ) => { + const [carouselRef, api] = useEmblaCarousel( + { + ...opts, + axis: orientation === "horizontal" ? "x" : "y", + }, + plugins, + ); + const [canScrollPrev, setCanScrollPrev] = React.useState(false); + const [canScrollNext, setCanScrollNext] = React.useState(false); + + const onSelect = React.useCallback((api: CarouselApi) => { + if (!api) { + return; + } + + setCanScrollPrev(api.canScrollPrev()); + setCanScrollNext(api.canScrollNext()); + }, []); + + const scrollPrev = React.useCallback(() => { + api?.scrollPrev(); + }, [api]); + + const scrollNext = React.useCallback(() => { + api?.scrollNext(); + }, [api]); + + const handleKeyDown = React.useCallback( + (event: React.KeyboardEvent) => { + if (event.key === "ArrowLeft") { + event.preventDefault(); + scrollPrev(); + } else if (event.key === "ArrowRight") { + event.preventDefault(); + scrollNext(); + } + }, + [scrollPrev, scrollNext], + ); + + React.useEffect(() => { + if (!api || !setApi) { + return; + } + + setApi(api); + }, [api, setApi]); + + React.useEffect(() => { + if (!api) { + return; + } + + onSelect(api); + api.on("reInit", onSelect); + api.on("select", onSelect); + + return () => { + api?.off("select", onSelect); + }; + }, [api, onSelect]); + + return ( + +
+ {children} +
+
+ ); + }, +); +Carousel.displayName = "Carousel"; + +const CarouselContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const { carouselRef, orientation } = useCarousel(); + + return ( +
+
+
+ ); +}); +CarouselContent.displayName = "CarouselContent"; + +const CarouselItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const { orientation } = useCarousel(); + + return ( +
+ ); +}); +CarouselItem.displayName = "CarouselItem"; + +const CarouselPrevious = React.forwardRef< + HTMLButtonElement, + React.ComponentProps +>(({ className, variant = "outline", size = "icon", ...props }, ref) => { + const { orientation, scrollPrev, canScrollPrev } = useCarousel(); + if (!canScrollPrev) return null; + return ( + + ); +}); +CarouselPrevious.displayName = "CarouselPrevious"; + +const CarouselNext = React.forwardRef< + HTMLButtonElement, + React.ComponentProps +>(({ className, variant = "outline", size = "icon", ...props }, ref) => { + const { orientation, scrollNext, canScrollNext } = useCarousel(); + if (!canScrollNext) return null; + return ( + + ); +}); +CarouselNext.displayName = "CarouselNext"; + +export { + type CarouselApi, + Carousel, + CarouselContent, + CarouselItem, + CarouselPrevious, + CarouselNext, +}; diff --git a/app/src/components/ui/radio-group.tsx b/app/src/components/ui/radio-group.tsx new file mode 100644 index 00000000..ae2853d1 --- /dev/null +++ b/app/src/components/ui/radio-group.tsx @@ -0,0 +1,41 @@ +import * as React from "react"; +import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"; +import { cn } from "src/libs/shadui"; +import { DotFilledIcon } from "@radix-ui/react-icons"; + +const RadioGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ( + + ); +}); +RadioGroup.displayName = RadioGroupPrimitive.Root.displayName; + +const RadioGroupItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ( + + + + + + ); +}); +RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName; + +export { RadioGroup, RadioGroupItem }; diff --git a/app/src/components/ui/toast.tsx b/app/src/components/ui/toast.tsx index df871210..5ad9e537 100644 --- a/app/src/components/ui/toast.tsx +++ b/app/src/components/ui/toast.tsx @@ -75,7 +75,7 @@ const ToastClose = React.forwardRef< { // Query - const village = await fetchVillage(ctx.drizzle, input.village); + const villageName = getMostCommonElement([ + input.question1, + input.question2, + input.question3, + input.question4, + input.question5, + input.question6, + ]); + const villageData = await ctx.drizzle.query.village.findFirst({ + where: eq(village.name, villageName || "none"), + }); // Guard - if (!village) return errorResponse("Village not found"); - if (!village.allianceSystem) return errorResponse("Missing alliance system"); - if (village.type !== "VILLAGE") return errorResponse("Can only join villages"); + if (!villageData) return errorResponse("Village not found"); + if (!villageData.allianceSystem) return errorResponse("Missing alliance system"); + if (villageData.type !== "VILLAGE") + return errorResponse("Can only join villages"); // Mutate const unique_attributes = [ @@ -49,9 +60,9 @@ export const registerRouter = createTRPCRouter({ recruiterId: input.recruiter_userid, username: input.username, gender: input.gender, - villageId: input.village, + villageId: villageData.id, approvedTos: 1, - sector: village.sector, + sector: villageData.sector, immunityUntil: secondsFromNow(24 * 3600), }), ]); diff --git a/app/src/utils/array.ts b/app/src/utils/array.ts index 12c9f08a..3c82c53d 100644 --- a/app/src/utils/array.ts +++ b/app/src/utils/array.ts @@ -26,3 +26,28 @@ export const isInArray = ( ): item is A => { return array.includes(item as A); }; + +/** + * Get most common element out of an array + * @param arr + * @returns + */ +export const getMostCommonElement = (arr: T[]): T | undefined => { + const counts: Record = {} as Record; + + for (const item of arr) { + counts[item] = (counts[item] || 0) + 1; + } + + let maxCount = 0; + let mostCommon: T | undefined = undefined; + + for (const item of arr) { + if (counts[item] > maxCount) { + maxCount = counts[item]; + mostCommon = item; + } + } + + return mostCommon; +}; diff --git a/app/src/validators/register.ts b/app/src/validators/register.ts index 8847768b..c5a4f463 100644 --- a/app/src/validators/register.ts +++ b/app/src/validators/register.ts @@ -1,5 +1,6 @@ import { z } from "zod"; -import { FederalStatuses } from "../../drizzle/constants"; +import { FederalStatuses } from "@/drizzle/constants"; +import { CoreVillages } from "@/drizzle/constants"; // List of possible attributes export const attributes = [ @@ -26,7 +27,7 @@ export const usernameSchema = z .string() .trim() .regex(new RegExp("^[a-zA-Z0-9_]+$"), { - message: "Must only contain alphanumeric characters and no spaces", + message: "Alphanumeric, no spaces", }) .min(2) .max(12); @@ -34,7 +35,6 @@ export const usernameSchema = z export const registrationSchema = z .object({ username: usernameSchema, - village: z.string(), gender: z.enum(genders), hair_color: z.enum(colors), eye_color: z.enum(colors), @@ -46,6 +46,12 @@ export const registrationSchema = z read_privacy: z.literal(true), read_earlyaccess: z.literal(true), recruiter_userid: z.string().optional().nullish(), + question1: z.enum(CoreVillages), + question2: z.enum(CoreVillages), + question3: z.enum(CoreVillages), + question4: z.enum(CoreVillages), + question5: z.enum(CoreVillages), + question6: z.enum(CoreVillages), }) .strict() .required()