diff --git a/packages/components/src/registration/onboarding/DiscoveryDropdown.tsx b/packages/components/src/registration/onboarding/DiscoveryDropdown.tsx deleted file mode 100644 index a2c4fd9..0000000 --- a/packages/components/src/registration/onboarding/DiscoveryDropdown.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { Controller, useFormContext } from "react-hook-form"; - -import { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectTrigger, - SelectValue, -} from "@good-dog/ui/select"; - -export default function DiscoveryDropdown() { - const { control } = useFormContext<{ - discovery?: string; - }>(); - - return ( -
-

How did you hear about Good Dog?

- ( - - )} - /> -
- ); -} diff --git a/packages/components/src/registration/onboarding/MediaMakerForm.tsx b/packages/components/src/registration/onboarding/MediaMakerForm.tsx index 9d75057..e2f3223 100644 --- a/packages/components/src/registration/onboarding/MediaMakerForm.tsx +++ b/packages/components/src/registration/onboarding/MediaMakerForm.tsx @@ -5,14 +5,20 @@ import { z } from "zod"; import { zPreProcessEmptyString } from "@good-dog/trpc/utils"; import RegistrationInput from "../inputs/RegistrationInput"; -import DiscoveryDropdown from "./DiscoveryDropdown"; import OnboardingFormProvider from "./OnboardingFormProvider"; +import ReferralDropdown from "./ReferralDropdown"; +import { ReferralSource } from ".prisma/client"; const Schema = z.object({ role: z.literal("MEDIA_MAKER"), firstName: zPreProcessEmptyString(z.string()), lastName: zPreProcessEmptyString(z.string()), - discovery: z.string().optional(), + referral: z + .object({ + source: z.nativeEnum(ReferralSource), + customSource: zPreProcessEmptyString(z.string().optional()), + }) + .optional(), }); type FormValues = z.infer; @@ -51,7 +57,7 @@ export default function MediaMakerForm( label="Last Name" /> - + ); } diff --git a/packages/components/src/registration/onboarding/MusicianForm.tsx b/packages/components/src/registration/onboarding/MusicianForm.tsx index fae6df9..2a52409 100644 --- a/packages/components/src/registration/onboarding/MusicianForm.tsx +++ b/packages/components/src/registration/onboarding/MusicianForm.tsx @@ -3,13 +3,14 @@ import { useFieldArray, useFormContext } from "react-hook-form"; import { z } from "zod"; +import { ReferralSource } from "@good-dog/db"; import { zPreProcessEmptyString } from "@good-dog/trpc/utils"; import { Button } from "@good-dog/ui/button"; import RegistrationCheckbox from "../inputs/RegistrationCheckbox"; import RegistrationInput from "../inputs/RegistrationInput"; -import DiscoveryDropdown from "./DiscoveryDropdown"; import OnboardingFormProvider from "./OnboardingFormProvider"; +import ReferralDropdown from "./ReferralDropdown"; const Schema = z.object({ role: z.literal("MUSICIAN"), @@ -33,7 +34,12 @@ const Schema = z.object({ }), ) .optional(), - discovery: z.string().optional(), + referral: z + .object({ + source: z.nativeEnum(ReferralSource), + customSource: zPreProcessEmptyString(z.string().optional()), + }) + .optional(), }); type FormValues = z.infer; @@ -103,7 +109,7 @@ export default function MusicianForm(
- + ); } diff --git a/packages/components/src/registration/onboarding/OnboardingFormProvider.tsx b/packages/components/src/registration/onboarding/OnboardingFormProvider.tsx index 71286c5..e0c7734 100644 --- a/packages/components/src/registration/onboarding/OnboardingFormProvider.tsx +++ b/packages/components/src/registration/onboarding/OnboardingFormProvider.tsx @@ -7,6 +7,7 @@ import { useRouter } from "next/navigation"; import { zodResolver } from "@hookform/resolvers/zod"; import { FormProvider, useForm } from "react-hook-form"; +import type { ReferralType } from "@good-dog/db"; import { trpc } from "@good-dog/trpc/client"; import { Button } from "@good-dog/ui/button"; @@ -14,7 +15,10 @@ interface BaseValues { role: "MEDIA_MAKER" | "MUSICIAN"; firstName: string; lastName: string; - discovery?: string; + referral?: { + source: ReferralType; + customSource?: string; + }; } export default function OnboardingFormProvider< diff --git a/packages/components/src/registration/onboarding/ReferralDropdown.tsx b/packages/components/src/registration/onboarding/ReferralDropdown.tsx new file mode 100644 index 0000000..add965e --- /dev/null +++ b/packages/components/src/registration/onboarding/ReferralDropdown.tsx @@ -0,0 +1,74 @@ +import { useState } from "react"; +import { Controller, useFormContext } from "react-hook-form"; + +import { ReferralSource } from "@good-dog/db"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, +} from "@good-dog/ui/select"; + +export default function ReferralDropdown() { + const { control, register } = useFormContext<{ + source?: string; + customSource?: string; + }>(); + + const referralOptions = Object.values(ReferralSource); + + const [isOtherSelected, setIsOtherSelected] = useState(false); + + const handleSelectChange = (value: string) => { + setIsOtherSelected(value === "OTHER"); + }; + + return ( +
+

How did you hear about Good Dog?

+ ( + + )} + /> + + {isOtherSelected && ( +
+ + +
+ )} +
+ ); +} diff --git a/packages/db/prisma/migrations/20250119220136_added_referral_source/migration.sql b/packages/db/prisma/migrations/20250119220136_added_referral_source/migration.sql new file mode 100644 index 0000000..8741d8a --- /dev/null +++ b/packages/db/prisma/migrations/20250119220136_added_referral_source/migration.sql @@ -0,0 +1,20 @@ +-- CreateEnum +CREATE TYPE "ReferralSource" AS ENUM ('FRIEND', 'COLLEAGUE', 'GREEN_LINE_RECORDS', 'SOCIAL_MEDIA', 'OTHER'); + +-- CreateTable +CREATE TABLE "Referral" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "source" "ReferralSource" NOT NULL, + "customSource" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Referral_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Referral_userId_key" ON "Referral"("userId"); + +-- AddForeignKey +ALTER TABLE "Referral" ADD CONSTRAINT "Referral_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index 1d39213..b1bb749 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -37,10 +37,29 @@ model User { passwordResetReq PasswordResetReq? sessions Session[] sentInvites GroupInvite[] + referral Referral? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } +model Referral { + id String @id @default(uuid()) + user User @relation(fields: [userId], references: [userId]) + userId String @unique + source ReferralSource + customSource String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +enum ReferralSource { + FRIEND + COLLEAGUE + GREEN_LINE_RECORDS + SOCIAL_MEDIA + OTHER +} + model Session { sessionId String @id @default(uuid()) @map("id") userId String diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts index 901f3a0..6148355 100644 --- a/packages/db/src/index.ts +++ b/packages/db/src/index.ts @@ -1,3 +1,6 @@ -import { PrismaClient } from "@prisma/client"; +import { PrismaClient, ReferralSource } from "@prisma/client"; export const prisma = new PrismaClient(); + +export { ReferralSource }; +export type ReferralType = keyof typeof ReferralSource; diff --git a/packages/trpc/src/procedures/onboarding.ts b/packages/trpc/src/procedures/onboarding.ts index bbd3bdd..15a56b3 100644 --- a/packages/trpc/src/procedures/onboarding.ts +++ b/packages/trpc/src/procedures/onboarding.ts @@ -2,6 +2,8 @@ import { revalidatePath } from "next/cache"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; +import { ReferralSource } from "@good-dog/db"; + import { authenticatedProcedureBuilder } from "../internal/init"; import { zPreProcessEmptyString } from "../utils"; @@ -12,12 +14,23 @@ export const onboardingProcedure = authenticatedProcedureBuilder role: z.literal("MEDIA_MAKER"), firstName: z.string(), lastName: z.string(), - discovery: z.string().optional(), + referral: z + .object({ + source: z.nativeEnum(ReferralSource), + customSource: zPreProcessEmptyString(z.string().optional()), + }) + .optional(), }), z.object({ role: z.literal("MUSICIAN"), firstName: zPreProcessEmptyString(z.string()), lastName: zPreProcessEmptyString(z.string()), + referral: z + .object({ + source: z.nativeEnum(ReferralSource), + customSource: zPreProcessEmptyString(z.string().optional()), + }) + .optional(), groupName: zPreProcessEmptyString(z.string()), stageName: zPreProcessEmptyString(z.string().optional()), isSongWriter: z.boolean().optional(), @@ -36,7 +49,6 @@ export const onboardingProcedure = authenticatedProcedureBuilder }), ) .optional(), - discovery: z.string().optional(), }), ]), ) @@ -66,6 +78,14 @@ export const onboardingProcedure = authenticatedProcedureBuilder role: "MUSICIAN", firstName: input.firstName, lastName: input.lastName, + referral: input.referral + ? { + create: { + source: input.referral.source, + customSource: input.referral.customSource, + }, + } + : undefined, stageName: input.stageName, isSongWriter: input.isSongWriter, isAscapAffiliated: input.isAscapAffiliated,