diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 60ea129..917a605 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -41,6 +41,7 @@ model Pet {
bio String?
adoptionStory String?
avatarKey String? // Supabase Storage object key, NOT a full URL
+ dmPolicy DmPolicy @default(EVERYONE) // who can send this pet DMs
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -63,9 +64,21 @@ model Pet {
receivedNotifications Notification[] @relation("ReceivedNotifications")
sentNotifications Notification[] @relation("SentNotifications")
+ // Phase 6 back-relations
+ conversationsAsA Conversation[] @relation("ConvPetA")
+ conversationsAsB Conversation[] @relation("ConvPetB")
+ sentMessages Message[] @relation("MessageSender")
+
@@index([ownerId])
}
+// ─── Phase 6 Enums ───────────────────────────────────────────────────────────
+
+enum DmPolicy {
+ EVERYONE // anyone can send a DM
+ FOLLOWERS_ONLY // only pets that this pet follows can send a DM
+}
+
// ─── Phase 2 Enums ───────────────────────────────────────────────────────────
enum PostType {
@@ -296,6 +309,38 @@ model VideoPost {
@@index([muxAssetId])
}
+// ─── Phase 6 Models ──────────────────────────────────────────────────────────
+
+model Conversation {
+ id String @id @default(cuid())
+ petA Pet @relation("ConvPetA", fields: [petAId], references: [id], onDelete: Cascade)
+ petAId String
+ petB Pet @relation("ConvPetB", fields: [petBId], references: [id], onDelete: Cascade)
+ petBId String
+ messages Message[]
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt // updated on every new message for inbox ordering
+
+ @@unique([petAId, petBId]) // petAId is always < petBId (sorted on create) — deduplication
+ @@index([petAId])
+ @@index([petBId])
+}
+
+model Message {
+ id String @id @default(cuid())
+ conversation Conversation @relation(fields: [conversationId], references: [id], onDelete: Cascade)
+ conversationId String
+ senderPet Pet @relation("MessageSender", fields: [senderPetId], references: [id])
+ senderPetId String
+ body String @db.VarChar(1000)
+ readAt DateTime? // null = unread by recipient
+
+ createdAt DateTime @default(now())
+
+ @@index([conversationId, createdAt(sort: Asc)])
+ @@index([senderPetId])
+}
+
// ─── Phase 4 Enums ───────────────────────────────────────────────────────────
enum NotificationType {
diff --git a/src/app/(app)/messages/[conversationId]/page.tsx b/src/app/(app)/messages/[conversationId]/page.tsx
new file mode 100644
index 0000000..0279bab
--- /dev/null
+++ b/src/app/(app)/messages/[conversationId]/page.tsx
@@ -0,0 +1,46 @@
+"use client";
+
+import { useParams } from "next/navigation";
+import { useQuery } from "@tanstack/react-query";
+import { Loader2 } from "lucide-react";
+import { useTRPC } from "@/trpc/client";
+import { useActivePet } from "@/context/ActivePetContext";
+import { MessageThread } from "@/components/messages/MessageThread";
+
+export default function ConversationPage() {
+ const params = useParams<{ conversationId: string }>();
+ const conversationId = params.conversationId;
+ const trpc = useTRPC();
+ const { activePetId } = useActivePet();
+
+ // Load conversations to get the other pet's info
+ const { data: convList, isLoading } = useQuery({
+ ...trpc.conversations.list.queryOptions({ petId: activePetId ?? "" }),
+ enabled: !!activePetId,
+ });
+
+ const conversation = convList?.find((c) => c.id === conversationId);
+
+ if (isLoading || !activePetId) {
+ return (
+
- {/* FollowButton — hidden on own pet (FollowButton also guards this internally) */}
+ {/* FollowButton — hidden on own pet */}
{!isOwnPet && (
)}
+ {/* Message button — opens or creates a DM conversation */}
+ {!isOwnPet && (
+
+ getOrCreateMutation.mutate({
+ senderPetId: activePetId,
+ recipientPetId: petId,
+ })
+ }
+ disabled={getOrCreateMutation.isPending}
+ className="flex items-center gap-1.5"
+ >
+ {getOrCreateMutation.isPending ? (
+
+ ) : (
+
+ )}
+ Message
+
+ )}
+
{/* Options menu — only show for other pets' profiles */}
{!isOwnPet && (
<>
diff --git a/src/app/(app)/pets/[petId]/edit/page.tsx b/src/app/(app)/pets/[petId]/edit/page.tsx
index 2bc7ed1..452b39d 100644
--- a/src/app/(app)/pets/[petId]/edit/page.tsx
+++ b/src/app/(app)/pets/[petId]/edit/page.tsx
@@ -10,6 +10,14 @@ import { PetForm } from "@/components/pet/PetForm";
import { Separator } from "@/components/ui/separator";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
+import { Label } from "@/components/ui/label";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
import {
Dialog,
DialogContent,
@@ -39,6 +47,7 @@ export default function EditPetPage() {
const queryClient = useQueryClient();
const { activePetId, setActivePet } = useActivePet();
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
+ const [dmPolicy, setDmPolicy] = useState<"EVERYONE" | "FOLLOWERS_ONLY">("EVERYONE");
const {
data: pet,
@@ -48,6 +57,11 @@ export default function EditPetPage() {
...trpc.pets.byId.queryOptions({ petId }),
});
+ // Sync dmPolicy from fetched pet
+ useEffect(() => {
+ if (pet?.dmPolicy) setDmPolicy(pet.dmPolicy as "EVERYONE" | "FOLLOWERS_ONLY");
+ }, [pet?.dmPolicy]);
+
// Gracefully handle ownership violation — redirect instead of showing error screen
useEffect(() => {
if (isError) {
@@ -55,6 +69,12 @@ export default function EditPetPage() {
}
}, [isError, router]);
+ const updateDmPolicyMutation = useMutation({
+ ...trpc.pets.update.mutationOptions(),
+ onSuccess: () => toast.success("Privacy setting saved"),
+ onError: () => toast.error("Failed to save privacy setting"),
+ });
+
const deleteMutation = useMutation({
...trpc.pets.delete.mutationOptions(),
onSuccess: async () => {
@@ -101,6 +121,44 @@ export default function EditPetPage() {
onSuccess={() => router.push(`/pets/${pet.id}`)}
/>
+ {/* Privacy settings */}
+
+
+
Privacy
+
+
Who can message {pet.name}?
+
+ setDmPolicy(v as "EVERYONE" | "FOLLOWERS_ONLY")}
+ >
+
+
+
+
+ Everyone
+ Only pets I follow
+
+
+
+ updateDmPolicyMutation.mutate({ petId: pet.id, dmPolicy })
+ }
+ >
+ Save
+
+
+
+ {dmPolicy === "FOLLOWERS_ONLY"
+ ? "Only pets that you follow can send you direct messages."
+ : "Any pet on PawFeed can send you direct messages."}
+
+
+
+
{/* Destructive zone */}
diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx
index 44e3d7a..0f3b256 100644
--- a/src/components/layout/Sidebar.tsx
+++ b/src/components/layout/Sidebar.tsx
@@ -46,7 +46,7 @@ const navLinks = [
{ href: "/explore", label: "Explore", icon: Compass, active: true, special: false },
{ href: "/search", label: "Search", icon: Search, active: true, special: false },
{ href: "/notifications", label: "Notifications", icon: Bell, active: true, special: true },
- { href: "/messages", label: "Messages", icon: MessageCircle, active: false, special: false },
+ { href: "/messages", label: "Messages", icon: MessageCircle, active: true, special: false },
];
/**
diff --git a/src/components/messages/MessageThread.tsx b/src/components/messages/MessageThread.tsx
new file mode 100644
index 0000000..5198c5b
--- /dev/null
+++ b/src/components/messages/MessageThread.tsx
@@ -0,0 +1,250 @@
+"use client";
+
+import { useState, useEffect, useRef, useCallback } from "react";
+import { useRouter } from "next/navigation";
+import { ArrowLeft, Send, Loader2 } from "lucide-react";
+import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
+import { toast } from "sonner";
+import { useTRPC } from "@/trpc/client";
+import { useActivePet } from "@/context/ActivePetContext";
+import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
+import { Button } from "@/components/ui/button";
+import { Textarea } from "@/components/ui/textarea";
+import { getAvatarUrl } from "@/lib/avatar-url";
+import Link from "next/link";
+
+function formatTime(date: Date | string) {
+ return new Date(date).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
+}
+
+function formatDay(date: Date | string) {
+ const d = new Date(date);
+ const today = new Date();
+ const yesterday = new Date();
+ yesterday.setDate(today.getDate() - 1);
+ if (d.toDateString() === today.toDateString()) return "Today";
+ if (d.toDateString() === yesterday.toDateString()) return "Yesterday";
+ return d.toLocaleDateString([], { weekday: "long", month: "short", day: "numeric" });
+}
+
+interface MessageThreadProps {
+ conversationId: string;
+ otherPet: { id: string; name: string; avatarKey: string | null };
+}
+
+export function MessageThread({ conversationId, otherPet }: MessageThreadProps) {
+ const trpc = useTRPC();
+ const router = useRouter();
+ const queryClient = useQueryClient();
+ const { activePetId } = useActivePet();
+ const bottomRef = useRef
(null);
+ const [body, setBody] = useState("");
+
+ const { data, isLoading } = useQuery({
+ ...trpc.messages.list.queryOptions({
+ conversationId,
+ petId: activePetId ?? "",
+ limit: 50,
+ }),
+ enabled: !!activePetId,
+ refetchInterval: 3000, // poll every 3s for new messages
+ });
+
+ const sendMutation = useMutation(
+ trpc.messages.send.mutationOptions({
+ onSuccess: () => {
+ setBody("");
+ void queryClient.invalidateQueries(
+ trpc.messages.list.queryOptions({ conversationId, petId: activePetId ?? "", limit: 50 })
+ );
+ void queryClient.invalidateQueries(
+ trpc.conversations.list.queryOptions({ petId: activePetId ?? "" })
+ );
+ void queryClient.invalidateQueries(
+ trpc.conversations.unreadCount.queryOptions({ petId: activePetId ?? "" })
+ );
+ },
+ onError: () => toast.error("Failed to send message. Try again."),
+ })
+ );
+
+ // Scroll to bottom on new messages
+ useEffect(() => {
+ bottomRef.current?.scrollIntoView({ behavior: "smooth" });
+ }, [data?.messages]);
+
+ const handleSend = useCallback(() => {
+ if (!body.trim() || !activePetId) return;
+ sendMutation.mutate({
+ conversationId,
+ senderPetId: activePetId,
+ body: body.trim(),
+ });
+ }, [body, activePetId, conversationId, sendMutation]);
+
+ const handleKeyDown = useCallback(
+ (e: React.KeyboardEvent) => {
+ if (e.key === "Enter" && !e.shiftKey) {
+ e.preventDefault();
+ handleSend();
+ }
+ },
+ [handleSend]
+ );
+
+ const messages = data?.messages ?? [];
+ const otherAvatarUrl = getAvatarUrl(otherPet.avatarKey);
+
+ // Group messages by day
+ const grouped: Array<{ day: string; messages: typeof messages }> = [];
+ for (const msg of messages) {
+ const day = formatDay(msg.createdAt);
+ const last = grouped[grouped.length - 1];
+ if (last && last.day === day) {
+ last.messages.push(msg);
+ } else {
+ grouped.push({ day, messages: [msg] });
+ }
+ }
+
+ return (
+
+ {/* Header */}
+
+
router.back()}
+ className="p-2 -ml-2 rounded-full hover:bg-muted transition-colors"
+ aria-label="Back"
+ >
+
+
+
+
+ {otherAvatarUrl && }
+
+ {otherPet.name[0]?.toUpperCase() ?? "?"}
+
+
+
{otherPet.name}
+
+
+
+ {/* Message list */}
+
+ {isLoading && (
+
+
+
+ )}
+
+ {!isLoading && messages.length === 0 && (
+
+
+ {otherAvatarUrl && }
+
+ {otherPet.name[0]?.toUpperCase() ?? "?"}
+
+
+
{otherPet.name}
+
Say hello 👋
+
+ )}
+
+ {grouped.map(({ day, messages: dayMsgs }) => (
+
+ {/* Day divider */}
+
+
+
+ {dayMsgs.map((msg, i) => {
+ const isOwn = msg.senderPetId === activePetId;
+ const prevMsg = dayMsgs[i - 1];
+ const showAvatar = !isOwn && (!prevMsg || prevMsg.senderPetId !== msg.senderPetId);
+
+ return (
+
+ {/* Other pet avatar — only shown at first bubble in a run */}
+ {!isOwn && (
+
+ {showAvatar && (
+
+ {otherAvatarUrl && (
+
+ )}
+
+ {otherPet.name[0]?.toUpperCase() ?? "?"}
+
+
+ )}
+
+ )}
+
+
+
+ {msg.body}
+
+
+ {formatTime(msg.createdAt)}
+
+
+
+ );
+ })}
+
+
+ ))}
+
+
+
+
+ {/* Composer */}
+
+
+
+ {body.length > 900 && (
+
+ {1000 - body.length} characters left
+
+ )}
+
+
+ );
+}
diff --git a/src/trpc/routers/_app.ts b/src/trpc/routers/_app.ts
index 383a681..132e85b 100644
--- a/src/trpc/routers/_app.ts
+++ b/src/trpc/routers/_app.ts
@@ -15,6 +15,7 @@ import { exploreRouter } from "@/trpc/routers/explore";
import { searchRouter } from "@/trpc/routers/search";
import { notificationsRouter } from "@/trpc/routers/notifications";
import { videosRouter } from "@/trpc/routers/videos";
+import { conversationsRouter, messagesRouter } from "@/trpc/routers/messages";
export const appRouter = router({
pets: petsRouter,
@@ -33,6 +34,8 @@ export const appRouter = router({
search: searchRouter,
notifications: notificationsRouter,
videos: videosRouter,
+ conversations: conversationsRouter,
+ messages: messagesRouter,
});
// Export the type for use in the client
diff --git a/src/trpc/routers/messages.ts b/src/trpc/routers/messages.ts
new file mode 100644
index 0000000..1e6f1d3
--- /dev/null
+++ b/src/trpc/routers/messages.ts
@@ -0,0 +1,284 @@
+import "server-only";
+import { z } from "zod";
+import { TRPCError } from "@trpc/server";
+import { router, protectedProcedure } from "@/trpc/init";
+import type { PrismaClient } from "@prisma/client";
+
+/**
+ * Messages tRPC router — Phase 6 Direct Messages.
+ *
+ * Two sub-routers exported:
+ * conversationsRouter — list, getOrCreate, unreadCount
+ * messagesRouter — list, send, markRead
+ *
+ * Security:
+ * - All procedures verify pet ownership via ctx.userId before accessing data
+ * - DmPolicy check: FOLLOWERS_ONLY blocks DMs from non-followers
+ * - Block check: blocked pets cannot initiate or receive DMs
+ * - Conversation IDs are sorted (petAId < petBId) to prevent duplicates
+ */
+
+async function assertPetOwnership(prisma: PrismaClient, petId: string, userId: string) {
+ const pet = await prisma.pet.findUnique({ where: { id: petId } });
+ if (!pet || pet.ownerId !== userId) throw new TRPCError({ code: "FORBIDDEN" });
+ return pet;
+}
+
+/** Verify that petId is a participant of the conversation and belongs to ctx.userId */
+async function assertParticipant(
+ prisma: PrismaClient,
+ conversationId: string,
+ petId: string,
+ userId: string
+) {
+ const [conv] = await Promise.all([
+ prisma.conversation.findUnique({ where: { id: conversationId } }),
+ assertPetOwnership(prisma, petId, userId),
+ ]);
+ if (!conv || (conv.petAId !== petId && conv.petBId !== petId)) {
+ throw new TRPCError({ code: "FORBIDDEN" });
+ }
+ return conv;
+}
+
+export const conversationsRouter = router({
+ /**
+ * List all conversations for a pet, ordered by most recent message.
+ * Returns the other pet's info, last message preview, and unread count.
+ */
+ list: protectedProcedure
+ .input(z.object({ petId: z.string().min(1) }))
+ .query(async ({ ctx, input }) => {
+ await assertPetOwnership(ctx.prisma, input.petId, ctx.userId);
+
+ const convs = await ctx.prisma.conversation.findMany({
+ where: { OR: [{ petAId: input.petId }, { petBId: input.petId }] },
+ include: {
+ petA: { select: { id: true, name: true, avatarKey: true } },
+ petB: { select: { id: true, name: true, avatarKey: true } },
+ messages: {
+ orderBy: { createdAt: "desc" },
+ take: 1,
+ },
+ _count: {
+ select: {
+ messages: {
+ where: { senderPetId: { not: input.petId }, readAt: null },
+ },
+ },
+ },
+ },
+ orderBy: { updatedAt: "desc" },
+ });
+
+ return convs.map((c) => ({
+ id: c.id,
+ otherPet: c.petAId === input.petId ? c.petB : c.petA,
+ lastMessage: c.messages[0] ?? null,
+ unreadCount: c._count.messages,
+ updatedAt: c.updatedAt,
+ }));
+ }),
+
+ /**
+ * Find or create a conversation between two pets.
+ * Enforces DmPolicy and block checks before creating.
+ * Pet IDs are sorted alphabetically to ensure uniqueness.
+ */
+ getOrCreate: protectedProcedure
+ .input(
+ z.object({
+ senderPetId: z.string().min(1),
+ recipientPetId: z.string().min(1),
+ })
+ )
+ .mutation(async ({ ctx, input }) => {
+ await assertPetOwnership(ctx.prisma, input.senderPetId, ctx.userId);
+
+ if (input.senderPetId === input.recipientPetId) {
+ throw new TRPCError({ code: "BAD_REQUEST", message: "Cannot message yourself." });
+ }
+
+ const [recipient, block] = await Promise.all([
+ ctx.prisma.pet.findUnique({
+ where: { id: input.recipientPetId },
+ select: { id: true, dmPolicy: true },
+ }),
+ ctx.prisma.block.findFirst({
+ where: {
+ OR: [
+ { blockerPetId: input.senderPetId, blockedPetId: input.recipientPetId },
+ { blockerPetId: input.recipientPetId, blockedPetId: input.senderPetId },
+ ],
+ },
+ }),
+ ]);
+
+ if (!recipient) throw new TRPCError({ code: "NOT_FOUND" });
+ if (block) throw new TRPCError({ code: "FORBIDDEN", message: "Cannot send messages to this pet." });
+
+ // DmPolicy check
+ if (recipient.dmPolicy === "FOLLOWERS_ONLY") {
+ const follow = await ctx.prisma.follow.findUnique({
+ where: {
+ followerPetId_followeePetId: {
+ followerPetId: input.recipientPetId,
+ followeePetId: input.senderPetId,
+ },
+ },
+ });
+ if (!follow) {
+ throw new TRPCError({
+ code: "FORBIDDEN",
+ message: "This pet only accepts messages from pets they follow.",
+ });
+ }
+ }
+
+ // Sort IDs to ensure uniqueness constraint ([petAId, petBId])
+ const [petAId, petBId] = [input.senderPetId, input.recipientPetId].sort();
+
+ const conversation = await ctx.prisma.conversation.upsert({
+ where: { petAId_petBId: { petAId, petBId } },
+ update: {},
+ create: { petAId, petBId },
+ });
+
+ return conversation;
+ }),
+
+ /** Total unread message count across all conversations — used for inbox badge. */
+ unreadCount: protectedProcedure
+ .input(z.object({ petId: z.string().min(1) }))
+ .query(async ({ ctx, input }) => {
+ await assertPetOwnership(ctx.prisma, input.petId, ctx.userId);
+
+ const count = await ctx.prisma.message.count({
+ where: {
+ conversation: {
+ OR: [{ petAId: input.petId }, { petBId: input.petId }],
+ },
+ senderPetId: { not: input.petId },
+ readAt: null,
+ },
+ });
+
+ return { count };
+ }),
+});
+
+export const messagesRouter = router({
+ /**
+ * Paginated messages in a conversation, oldest-first (cursor on createdAt).
+ * Marks messages sent by the other pet as read on load.
+ */
+ list: protectedProcedure
+ .input(
+ z.object({
+ conversationId: z.string().min(1),
+ petId: z.string().min(1),
+ cursor: z.string().nullish(), // ISO createdAt for pagination
+ limit: z.number().min(1).max(50).default(30),
+ })
+ )
+ .query(async ({ ctx, input }) => {
+ await assertParticipant(ctx.prisma, input.conversationId, input.petId, ctx.userId);
+
+ const messages = await ctx.prisma.message.findMany({
+ where: {
+ conversationId: input.conversationId,
+ ...(input.cursor ? { createdAt: { gt: new Date(input.cursor) } } : {}),
+ },
+ orderBy: { createdAt: "asc" },
+ take: input.limit + 1,
+ include: {
+ senderPet: { select: { id: true, name: true, avatarKey: true } },
+ },
+ });
+
+ const hasNextPage = messages.length > input.limit;
+ const items = hasNextPage ? messages.slice(0, input.limit) : messages;
+ const nextCursor = hasNextPage ? items[items.length - 1].createdAt.toISOString() : null;
+
+ // Mark received messages as read (fire-and-forget)
+ ctx.prisma.message.updateMany({
+ where: {
+ conversationId: input.conversationId,
+ senderPetId: { not: input.petId },
+ readAt: null,
+ },
+ data: { readAt: new Date() },
+ }).catch(() => {});
+
+ return { messages: items, nextCursor };
+ }),
+
+ /** Send a message. Fires MESSAGE notification fire-and-forget. */
+ send: protectedProcedure
+ .input(
+ z.object({
+ conversationId: z.string().min(1),
+ senderPetId: z.string().min(1),
+ body: z.string().min(1).max(1000),
+ })
+ )
+ .mutation(async ({ ctx, input }) => {
+ const conv = await assertParticipant(
+ ctx.prisma,
+ input.conversationId,
+ input.senderPetId,
+ ctx.userId
+ );
+
+ const recipientPetId =
+ conv.petAId === input.senderPetId ? conv.petBId : conv.petAId;
+
+ const [message] = await ctx.prisma.$transaction([
+ ctx.prisma.message.create({
+ data: {
+ conversationId: input.conversationId,
+ senderPetId: input.senderPetId,
+ body: input.body,
+ },
+ include: { senderPet: { select: { id: true, name: true, avatarKey: true } } },
+ }),
+ // Touch conversation updatedAt so inbox re-orders correctly
+ ctx.prisma.conversation.update({
+ where: { id: input.conversationId },
+ data: { updatedAt: new Date() },
+ }),
+ ]);
+
+ // MESSAGE notification — fire-and-forget
+ ctx.prisma.notification.create({
+ data: {
+ recipientPetId,
+ type: "MESSAGE",
+ actorPetId: input.senderPetId,
+ },
+ }).catch(() => {});
+
+ return message;
+ }),
+
+ /** Mark all received messages in a conversation as read. */
+ markRead: protectedProcedure
+ .input(
+ z.object({
+ conversationId: z.string().min(1),
+ petId: z.string().min(1),
+ })
+ )
+ .mutation(async ({ ctx, input }) => {
+ await assertParticipant(ctx.prisma, input.conversationId, input.petId, ctx.userId);
+
+ await ctx.prisma.message.updateMany({
+ where: {
+ conversationId: input.conversationId,
+ senderPetId: { not: input.petId },
+ readAt: null,
+ },
+ data: { readAt: new Date() },
+ });
+ }),
+});
diff --git a/src/trpc/routers/pets.ts b/src/trpc/routers/pets.ts
index ee5b72b..064349c 100644
--- a/src/trpc/routers/pets.ts
+++ b/src/trpc/routers/pets.ts
@@ -117,6 +117,7 @@ export const petsRouter = router({
bio: z.string().max(150).optional().nullable(),
adoptionStory: z.string().max(500).optional().nullable(),
breedId: z.string().optional().nullable(),
+ dmPolicy: z.enum(["EVERYONE", "FOLLOWERS_ONLY"]).optional(),
})
)
.mutation(async ({ ctx, input }) => {
@@ -131,6 +132,7 @@ export const petsRouter = router({
if (data.adoptionStory !== undefined)
updateData.adoptionStory = data.adoptionStory;
if (data.breedId !== undefined) updateData.breedId = data.breedId;
+ if (data.dmPolicy !== undefined) updateData.dmPolicy = data.dmPolicy;
return ctx.prisma.pet.update({
where: { id: petId },