feat(pets): add optional nickname/Rufname shown alongside the pet's real name

Adds Pet.nickname (nullable, max 30 chars) and a shared formatPetName()
helper so the display format ("Rex (Rexi)") stays consistent across
feed, admin panel, messages, sidebar/nav, and mentions instead of
duplicating the formatting logic at each read site.
This commit is contained in:
admin committed 2026-08-15 13:25:54 +02:00
1 parent 4daac56bfe
commit 1293f7c879
28 files changed
+110 -51

No files matched your search

+2
View File
@@ -64,6 +64,8 @@
"namePlaceholder": "Name eingeben",
"nameRequired": "Dieses Feld ist erforderlich.",
"nameMaxLength": "Der Name darf maximal 30 Zeichen lang sein.",
"nickname": "Rufname / Spitzname (optional)",
"nicknamePlaceholder": "z. B. Rexi",
"species": "Tierart",
"selectSpecies": "Tierart auswählen",
"speciesRequired": "Bitte wähle eine Tierart aus.",
+2
View File
@@ -64,6 +64,8 @@
"namePlaceholder": "Enter pet name",
"nameRequired": "This field is required.",
"nameMaxLength": "Pet name must be 30 characters or fewer.",
"nickname": "Nickname (optional)",
"nicknamePlaceholder": "e.g. Rexy",
"species": "Species",
"selectSpecies": "Select species",
"speciesRequired": "Please select a species.",
+1
View File
@@ -147,6 +147,7 @@ model Pet {
breed Breed? @relation(fields: [breedId], references: [id])
breedId String?
name String
nickname String? @db.VarChar(30) // Rufname/Spitzname; shown after the real name, e.g. "Rex (Rexi)" — see src/lib/pet-display-name.ts
bio String?
adoptionStory String?
avatarKey String? // Supabase Storage object key, NOT a full URL
+1
View File
@@ -134,6 +134,7 @@ export default function EditPetPage() {
initialAvatarKey={pet.avatarKey ?? null}
initialValues={{
name: pet.name,
nickname: pet.nickname ?? null,
speciesId: pet.speciesId,
breedId: pet.breedId ?? null,
bio: pet.bio ?? null,
+3 -2
View File
@@ -1,4 +1,5 @@
import { notFound } from "next/navigation";
import { formatPetName } from "@/lib/pet-display-name";
import Link from "next/link";
import { currentUser } from "@clerk/nextjs/server";
import { getTranslations } from "next-intl/server";
@@ -71,7 +72,7 @@ export default async function PetProfilePage({ params }: PageProps) {
/>
<h1 className="flex items-center gap-1.5 text-xl font-semibold text-center">
{pet.name}
{formatPetName(pet)}
{pet.isVerified && (
<BadgeCheck
className="h-4 w-4 fill-blue-500 text-background"
@@ -136,7 +137,7 @@ export default async function PetProfilePage({ params }: PageProps) {
</div>
{/* ProfileActions: Follow + Message + options menu (client component) */}
<ProfileActions petId={pet.id} petName={pet.name} ownerId={pet.ownerId} />
<ProfileActions petId={pet.id} petName={formatPetName(pet)} ownerId={pet.ownerId} />
{pet.bio && (
<>
+3 -2
View File
@@ -1,6 +1,7 @@
"use client";
import { useState } from "react";
import { formatPetName } from "@/lib/pet-display-name";
import { useRouter, useParams } from "next/navigation";
import { toast } from "sonner";
import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
@@ -122,7 +123,7 @@ export default function AdminUsersPage() {
trpc.admin.setPetVerified.mutationOptions({
onSuccess: (pet) => {
void qc.invalidateQueries({ queryKey: trpc.admin.listUsers.queryKey() });
toast.success(pet.isVerified ? `${pet.name} verified.` : `${pet.name} un-verified.`);
toast.success(pet.isVerified ? `${formatPetName(pet)} verified.` : `${formatPetName(pet)} un-verified.`);
},
onError: (err) => {
toast.error(`Verification failed: ${err.message}`);
@@ -274,7 +275,7 @@ export default function AdminUsersPage() {
: "border-zinc-700 text-zinc-400 hover:border-zinc-600"
}`}
>
{pet.name}
{formatPetName(pet)}
<BadgeCheck
className={`h-3 w-3 ${pet.isVerified ? "fill-blue-400 text-zinc-950" : ""}`}
/>
+2 -1
View File
@@ -3,6 +3,7 @@
import type { MilestoneType } from "@prisma/client";
import { MILESTONE_META } from "@/lib/milestone-meta";
import { getAvatarUrl } from "@/lib/avatar-url";
import { formatPetName } from "@/lib/pet-display-name";
import { getMediaUrl } from "@/lib/media-url";
import { BlurImage } from "@/components/ui/blur-image";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
@@ -60,7 +61,7 @@ export function MilestoneCard({ post }: MilestoneCardProps) {
</Avatar>
<div className="flex-1 min-w-0">
<p className="text-base font-semibold text-foreground leading-tight truncate">
{pet.name}
{formatPetName(pet)}
</p>
<p className="text-xs text-muted-foreground">{formatDate(post.createdAt)}</p>
</div>
+3 -2
View File
@@ -1,6 +1,7 @@
"use client";
import { useState, useEffect, useCallback, memo } from "react";
import { formatPetName } from "@/lib/pet-display-name";
import { MoreHorizontal, MessageCircle, Sparkles, BadgeCheck } from "lucide-react";
import { toast } from "sonner";
import { useMutation } from "@tanstack/react-query";
@@ -246,7 +247,7 @@ function PostCardImpl({ post, onDeleted, variant = "feed" }: PostCardProps) {
<div className="flex-1 min-w-0">
<p className="flex items-center gap-1 text-base font-semibold text-foreground leading-tight truncate">
{pet.name}
{formatPetName(pet)}
{pet.isVerified && (
<BadgeCheck
className="h-3.5 w-3.5 shrink-0 fill-blue-500 text-background"
@@ -551,7 +552,7 @@ function PostCardImpl({ post, onDeleted, variant = "feed" }: PostCardProps) {
onOpenChange={setShowBlockDialog}
blockerPetId={activePetId}
blockedPetId={pet.id}
blockedPetName={pet.name}
blockedPetName={formatPetName(pet)}
/>
)}
</article>
+3 -2
View File
@@ -5,6 +5,7 @@ import { MoreHorizontal, Trash2, Loader2 } from "lucide-react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
import { useTRPC } from "@/trpc/client";
import { formatPetName } from "@/lib/pet-display-name";
import { useActivePet } from "@/context/ActivePetContext";
import { Dialog, DialogContent } from "@/components/ui/dialog";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
@@ -193,7 +194,7 @@ export function PostDetailDialog({ post, open, onClose, onDeleted }: PostDetailD
<PetAvatar name={pet.name} avatarKey={pet.avatarKey} className="w-8 h-8" />
</PawRing>
<div className="flex-1 min-w-0">
<p className="text-sm font-semibold text-foreground leading-tight truncate">{pet.name}</p>
<p className="text-sm font-semibold text-foreground leading-tight truncate">{formatPetName(pet)}</p>
<p className="text-xs text-muted-foreground">{formatRelativeTime(post.createdAt)}</p>
</div>
</Link>
@@ -560,7 +561,7 @@ export function PostDetailDialog({ post, open, onClose, onDeleted }: PostDetailD
onOpenChange={setShowBlockDialog}
blockerPetId={activePetId}
blockedPetId={pet.id}
blockedPetName={pet.name}
blockedPetName={formatPetName(pet)}
/>
)}
</>
+3 -2
View File
@@ -1,6 +1,7 @@
"use client";
import { useState } from "react";
import { formatPetName } from "@/lib/pet-display-name";
import Link from "next/link";
import { usePathname, useRouter } from "next/navigation";
import {
@@ -106,7 +107,7 @@ export function MobileNav({ pets }: MobileNavProps) {
className="h-6 w-6 shrink-0"
/>
<span className="font-medium text-foreground truncate max-w-[120px] flex items-center gap-1">
{activePet.name}
{formatPetName(activePet)}
{activePet.isVerified && (
<BadgeCheck className="h-3 w-3 shrink-0 fill-blue-500 text-background" />
)}
@@ -126,7 +127,7 @@ export function MobileNav({ pets }: MobileNavProps) {
className="h-6 w-6 shrink-0"
/>
<span className="flex-1 text-sm truncate flex items-center gap-1">
{pet.name}
{formatPetName(pet)}
{pet.isVerified && (
<BadgeCheck className="h-3 w-3 shrink-0 fill-blue-500 text-background" />
)}
+3 -2
View File
@@ -1,6 +1,7 @@
"use client";
import { useState, useEffect } from "react";
import { formatPetName } from "@/lib/pet-display-name";
import Link from "next/link";
import { usePathname, useRouter } from "next/navigation";
import {
@@ -148,7 +149,7 @@ export function Sidebar({ pets }: SidebarProps) {
{!collapsed && (
<>
<span className="text-sm font-medium text-foreground flex-1 truncate flex items-center gap-1">
{activePet.name}
{formatPetName(activePet)}
{activePet.isVerified && (
<BadgeCheck className="h-3.5 w-3.5 shrink-0 fill-blue-500 text-background" />
)}
@@ -170,7 +171,7 @@ export function Sidebar({ pets }: SidebarProps) {
className="h-6 w-6 shrink-0"
/>
<span className="flex-1 text-sm truncate flex items-center gap-1">
{pet.name}
{formatPetName(pet)}
{pet.isVerified && (
<BadgeCheck className="h-3 w-3 shrink-0 fill-blue-500 text-background" />
)}
+3 -2
View File
@@ -1,6 +1,7 @@
"use client";
import { useState, useEffect, useRef, useCallback } from "react";
import { formatPetName } from "@/lib/pet-display-name";
import { useRouter } from "next/navigation";
import { ArrowLeft, Send, Loader2 } from "lucide-react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
@@ -128,7 +129,7 @@ export function MessageThread({ conversationId, otherPet }: MessageThreadProps)
{otherPet.name[0]?.toUpperCase() ?? "?"}
</AvatarFallback>
</Avatar>
<span className="text-sm font-semibold">{otherPet.name}</span>
<span className="text-sm font-semibold">{formatPetName(otherPet)}</span>
</Link>
</div>
@@ -148,7 +149,7 @@ export function MessageThread({ conversationId, otherPet }: MessageThreadProps)
{otherPet.name[0]?.toUpperCase() ?? "?"}
</AvatarFallback>
</Avatar>
<p className="text-sm font-medium mt-2">{otherPet.name}</p>
<p className="text-sm font-medium mt-2">{formatPetName(otherPet)}</p>
<p className="text-xs">{t("empty")}</p>
</div>
)}
+23
View File
@@ -38,6 +38,11 @@ const petFormSchema = z.object({
.string()
.min(1, "This field is required.")
.max(30, "Pet name must be 30 characters or fewer."),
nickname: z
.string()
.max(30, "Nickname must be 30 characters or fewer.")
.optional()
.nullable(),
speciesId: z.string().min(1, "Please select a species."),
breedId: z.string().nullable().optional(),
bio: z
@@ -137,6 +142,7 @@ export function PetForm({
resolver: zodResolver(petFormSchema),
defaultValues: {
name: initialValues?.name ?? "",
nickname: initialValues?.nickname ?? "",
speciesId: initialValues?.speciesId ?? "",
breedId: initialValues?.breedId ?? null,
bio: initialValues?.bio ?? "",
@@ -154,6 +160,7 @@ export function PetForm({
if (mode === "create") {
createMutation.mutate({
name: data.name,
nickname: data.nickname || undefined,
speciesId: data.speciesId,
breedId: data.breedId ?? undefined,
bio: data.bio ?? undefined,
@@ -163,6 +170,7 @@ export function PetForm({
updateMutation.mutate({
petId: petId!,
name: data.name,
nickname: data.nickname || null,
breedId: data.breedId ?? null,
bio: data.bio ?? null,
adoptionStory: data.adoptionStory ?? null,
@@ -211,6 +219,21 @@ export function PetForm({
)}
</div>
{/* Nickname / Rufname — optional, shown after the real name everywhere, e.g. "Rex (Rexi)" */}
<div className="flex flex-col gap-1.5">
<Label htmlFor="pet-nickname">{t("nickname")}</Label>
<Input
id="pet-nickname"
maxLength={30}
placeholder={t("nicknamePlaceholder")}
aria-invalid={!!errors.nickname}
{...register("nickname")}
/>
{errors.nickname && (
<p className="text-xs text-destructive">{errors.nickname.message}</p>
)}
</div>
{/* Species selector — defer render until list loaded so SelectValue resolves names */}
<div className="flex flex-col gap-1.5">
<Label htmlFor="species">{t("species")}</Label>
+4 -1
View File
@@ -31,7 +31,10 @@ export async function createMentionNotifications(
const mentionedPets = await prisma.pet.findMany({
where: {
name: { in: names, mode: "insensitive" },
OR: [
{ name: { in: names, mode: "insensitive" } },
{ nickname: { in: names, mode: "insensitive" } },
],
id: { not: actorPetId },
},
select: { id: true },
+8
View File
@@ -0,0 +1,8 @@
/**
* Formats a pet's display name: real name first, nickname/Rufname after
* in parentheses when set — e.g. "Rex (Rexi)". Same format everywhere,
* including the admin panel, per explicit product decision.
*/
export function formatPetName(pet: { name: string; nickname?: string | null }): string {
return pet.nickname ? `${pet.name} (${pet.nickname})` : pet.name;
}
+15 -14
View File
@@ -291,7 +291,7 @@ export const adminRouter = router({
}
: undefined,
include: {
pet: { select: { id: true, name: true, avatarKey: true, ownerId: true } },
pet: { select: { id: true, name: true, nickname: true, avatarKey: true, ownerId: true } },
images: { orderBy: { position: "asc" }, take: 1 },
videoPost: { select: { muxPlaybackId: true } },
milestone: true,
@@ -299,7 +299,7 @@ export const adminRouter = router({
include: {
originalPost: {
include: {
pet: { select: { id: true, name: true } },
pet: { select: { id: true, name: true, nickname: true } },
images: { orderBy: { position: "asc" }, take: 1 },
milestone: true,
},
@@ -557,7 +557,7 @@ export const adminRouter = router({
await assertAdmin(ctx.prisma, ctx.userId);
return ctx.prisma.comment.findMany({
where: { postId: input.postId },
include: { pet: { select: { id: true, name: true, avatarKey: true } } },
include: { pet: { select: { id: true, name: true, nickname: true, avatarKey: true } } },
orderBy: { createdAt: "asc" },
});
}),
@@ -632,7 +632,7 @@ export const adminRouter = router({
...(input.role !== "all" && input.role !== "none" ? { adminRole: { role: input.role } } : {}),
},
include: {
pets: { select: { id: true, name: true, avatarKey: true, isVerified: true }, take: 3 },
pets: { select: { id: true, name: true, nickname: true, avatarKey: true, isVerified: true }, take: 3 },
ban: true,
shadowban: true,
adminRole: true,
@@ -864,6 +864,7 @@ export const adminRouter = router({
select: {
id: true,
name: true,
nickname: true,
avatarKey: true,
isVerified: true,
ownerId: true,
@@ -1060,8 +1061,8 @@ export const adminRouter = router({
const convs = await ctx.prisma.conversation.findMany({
where: { OR: [{ petAId: teamPet.id }, { petBId: teamPet.id }] },
include: {
petA: { select: { id: true, name: true, avatarKey: true } },
petB: { select: { id: true, name: true, avatarKey: true } },
petA: { select: { id: true, name: true, nickname: true, avatarKey: true } },
petB: { select: { id: true, name: true, nickname: true, avatarKey: true } },
messages: { orderBy: { createdAt: "desc" }, take: 1 },
_count: {
select: { messages: { where: { senderPetId: { not: teamPet.id }, readAt: null } } },
@@ -1095,7 +1096,7 @@ export const adminRouter = router({
where: { conversationId: input.conversationId },
orderBy: { createdAt: "asc" },
take: 200,
include: { senderPet: { select: { id: true, name: true, avatarKey: true } } },
include: { senderPet: { select: { id: true, name: true, nickname: true, avatarKey: true } } },
});
// Fire-and-forget — any admin opening the thread marks it read for the whole team.
@@ -1198,7 +1199,7 @@ export const adminRouter = router({
const reports = await ctx.prisma.report.findMany({
include: {
reporter: { select: { id: true, name: true, avatarKey: true } },
reporter: { select: { id: true, name: true, nickname: true, avatarKey: true } },
},
orderBy: { createdAt: "desc" },
take: 100,
@@ -1219,7 +1220,7 @@ export const adminRouter = router({
createdAt: true,
reactionCount: true,
commentCount: true,
pet: { select: { id: true, name: true, avatarKey: true } },
pet: { select: { id: true, name: true, nickname: true, avatarKey: true } },
images: {
orderBy: { position: "asc" },
take: 4,
@@ -1262,7 +1263,7 @@ export const adminRouter = router({
type: true,
createdAt: true,
hiddenAt: true,
pet: { select: { id: true, name: true, avatarKey: true } },
pet: { select: { id: true, name: true, nickname: true, avatarKey: true } },
images: {
orderBy: { position: "asc" },
select: { storageKey: true, position: true, blurhash: true },
@@ -1271,7 +1272,7 @@ export const adminRouter = router({
orderBy: { createdAt: "desc" },
take: 200,
select: {
pet: { select: { id: true, name: true, avatarKey: true } },
pet: { select: { id: true, name: true, nickname: true, avatarKey: true } },
},
},
},
@@ -1511,7 +1512,7 @@ export const adminRouter = router({
return ctx.prisma.adminRole.findMany({
include: {
owner: {
include: { pets: { select: { name: true, avatarKey: true }, take: 1 } },
include: { pets: { select: { name: true, nickname: true, avatarKey: true }, take: 1 } },
},
},
orderBy: { grantedAt: "desc" },
@@ -1594,7 +1595,7 @@ export const adminRouter = router({
const owners = moderatorIds.length > 0
? await ctx.prisma.owner.findMany({
where: { id: { in: moderatorIds } },
select: { id: true, pets: { select: { name: true }, take: 1 } },
select: { id: true, pets: { select: { name: true, nickname: true }, take: 1 } },
})
: [];
const nameByOwnerId = new Map(owners.map((o) => [o.id, o.pets[0]?.name ?? null]));
@@ -1630,7 +1631,7 @@ export const adminRouter = router({
orderBy: { createdAt: "desc" },
take: input.limit,
include: {
issuedBy: { select: { id: true, pets: { select: { name: true }, take: 1 } } },
issuedBy: { select: { id: true, pets: { select: { name: true, nickname: true }, take: 1 } } },
},
});
}),
+2 -2
View File
@@ -91,7 +91,7 @@ export const adsRouter = router({
return ctx.prisma.adComment.findMany({
where: { adId: input.adId },
include: {
pet: { select: { id: true, name: true, avatarKey: true } },
pet: { select: { id: true, name: true, nickname: true, avatarKey: true } },
},
orderBy: { createdAt: "asc" },
take: 100,
@@ -113,7 +113,7 @@ export const adsRouter = router({
ctx.prisma.adComment.create({
data: { adId: input.adId, petId: input.petId, body: input.body },
include: {
pet: { select: { id: true, name: true, avatarKey: true } },
pet: { select: { id: true, name: true, nickname: true, avatarKey: true } },
},
}),
ctx.prisma.advertisement.update({
+2 -2
View File
@@ -27,7 +27,7 @@ export const commentsRouter = router({
body: input.body,
},
include: {
pet: { select: { id: true, name: true, avatarKey: true } },
pet: { select: { id: true, name: true, nickname: true, avatarKey: true } },
},
}),
ctx.prisma.post.update({
@@ -77,7 +77,7 @@ export const commentsRouter = router({
orderBy: { createdAt: "asc" },
take: input.limit + 1,
include: {
pet: { select: { id: true, name: true, avatarKey: true } },
pet: { select: { id: true, name: true, nickname: true, avatarKey: true } },
},
});
+1
View File
@@ -79,6 +79,7 @@ export const exploreRouter = router({
select: {
id: true,
name: true,
nickname: true,
avatarKey: true,
isVerified: true,
owner: { select: { sponsorSince: true, sponsorPeriodEnd: true } },
+2
View File
@@ -31,6 +31,7 @@ const postInclude = {
select: {
id: true,
name: true,
nickname: true,
avatarKey: true,
isVerified: true,
owner: { select: { sponsorSince: true, sponsorPeriodEnd: true } },
@@ -47,6 +48,7 @@ const postInclude = {
select: {
id: true,
name: true,
nickname: true,
avatarKey: true,
isVerified: true,
owner: { select: { sponsorSince: true, sponsorPeriodEnd: true } },
+4 -2
View File
@@ -83,7 +83,7 @@ export const followsRouter = router({
include: {
images: { orderBy: { position: "asc" } },
milestone: true,
pet: { select: { id: true, name: true, avatarKey: true } },
pet: { select: { id: true, name: true, nickname: true, avatarKey: true } },
},
});
@@ -176,6 +176,7 @@ export const followsRouter = router({
select: {
id: true,
name: true,
nickname: true,
avatarKey: true,
species: { select: { name: true, translations: { where: { locale: ctx.locale } } } },
},
@@ -220,7 +221,7 @@ export const followsRouter = router({
},
},
select: {
followee: { select: { id: true, name: true, avatarKey: true } },
followee: { select: { id: true, name: true, nickname: true, avatarKey: true } },
},
orderBy: { createdAt: "desc" },
take: 100,
@@ -253,6 +254,7 @@ export const followsRouter = router({
select: {
id: true,
name: true,
nickname: true,
avatarKey: true,
species: { select: { name: true, translations: { where: { locale: ctx.locale } } } },
},
+4 -4
View File
@@ -50,8 +50,8 @@ export const conversationsRouter = router({
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 } },
petA: { select: { id: true, name: true, nickname: true, avatarKey: true } },
petB: { select: { id: true, name: true, nickname: true, avatarKey: true } },
messages: {
orderBy: { createdAt: "desc" },
take: 1,
@@ -188,7 +188,7 @@ export const messagesRouter = router({
orderBy: { createdAt: "asc" },
take: input.limit + 1,
include: {
senderPet: { select: { id: true, name: true, avatarKey: true } },
senderPet: { select: { id: true, name: true, nickname: true, avatarKey: true } },
},
});
@@ -237,7 +237,7 @@ export const messagesRouter = router({
senderPetId: input.senderPetId,
body: input.body,
},
include: { senderPet: { select: { id: true, name: true, avatarKey: true } } },
include: { senderPet: { select: { id: true, name: true, nickname: true, avatarKey: true } } },
}),
// Touch conversation updatedAt so inbox re-orders correctly
ctx.prisma.conversation.update({
+2 -2
View File
@@ -66,7 +66,7 @@ export const milestonesRouter = router({
include: {
images: { orderBy: { position: "asc" } },
milestone: true,
pet: { select: { id: true, name: true, avatarKey: true } },
pet: { select: { id: true, name: true, nickname: true, avatarKey: true } },
},
});
@@ -97,7 +97,7 @@ export const milestonesRouter = router({
include: {
milestone: true,
images: { orderBy: { position: "asc" } },
pet: { select: { id: true, name: true, avatarKey: true } },
pet: { select: { id: true, name: true, nickname: true, avatarKey: true } },
},
orderBy: { createdAt: "desc" },
take: input.limit,
+1 -1
View File
@@ -26,7 +26,7 @@ export const notificationsRouter = router({
orderBy: { createdAt: "desc" },
take: input.limit + 1,
include: {
actorPet: { select: { id: true, name: true, avatarKey: true } },
actorPet: { select: { id: true, name: true, nickname: true, avatarKey: true } },
post: { select: { id: true, petId: true, images: { take: 1, orderBy: { position: "asc" } } } },
comment: { select: { id: true, body: true } },
},
+4
View File
@@ -17,6 +17,7 @@ export const petsRouter = router({
.input(
z.object({
name: z.string().min(1).max(30),
nickname: z.string().max(30).optional(),
speciesId: z.string().min(1),
breedId: z.string().optional(),
bio: z.string().max(150).optional(),
@@ -48,6 +49,7 @@ export const petsRouter = router({
data: {
ownerId: ctx.userId,
name: input.name,
nickname: input.nickname ?? null,
speciesId: input.speciesId,
breedId: input.breedId ?? null,
bio: input.bio ?? null,
@@ -145,6 +147,7 @@ export const petsRouter = router({
z.object({
petId: z.string().min(1),
name: z.string().max(30).optional(),
nickname: z.string().max(30).optional().nullable(),
bio: z.string().max(150).optional().nullable(),
adoptionStory: z.string().max(500).optional().nullable(),
breedId: z.string().optional().nullable(),
@@ -161,6 +164,7 @@ export const petsRouter = router({
// Only update fields that were explicitly provided
const updateData: Record<string, unknown> = {};
if (data.name !== undefined) updateData.name = data.name;
if (data.nickname !== undefined) updateData.nickname = data.nickname;
if (data.bio !== undefined) updateData.bio = data.bio;
if (data.adoptionStory !== undefined)
updateData.adoptionStory = data.adoptionStory;
+6 -6
View File
@@ -129,7 +129,7 @@ export const postsRouter = router({
},
include: {
images: { orderBy: { position: "asc" } },
pet: { select: { id: true, name: true, avatarKey: true } },
pet: { select: { id: true, name: true, nickname: true, avatarKey: true } },
milestone: true,
},
});
@@ -197,7 +197,7 @@ export const postsRouter = router({
data: { caption: input.caption },
include: {
images: { orderBy: { position: "asc" } },
pet: { select: { id: true, name: true, avatarKey: true } },
pet: { select: { id: true, name: true, nickname: true, avatarKey: true } },
milestone: true,
},
});
@@ -249,14 +249,14 @@ export const postsRouter = router({
take: input.limit + 1,
include: {
images: { orderBy: { position: "asc" } },
pet: { select: { id: true, name: true, avatarKey: true } },
pet: { select: { id: true, name: true, nickname: true, avatarKey: true } },
milestone: true,
videoPost: { select: { status: true, muxPlaybackId: true } },
repost: {
include: {
originalPost: {
include: {
pet: { select: { id: true, name: true, avatarKey: true } },
pet: { select: { id: true, name: true, nickname: true, avatarKey: true } },
images: { orderBy: { position: "asc" } },
milestone: true,
},
@@ -291,14 +291,14 @@ export const postsRouter = router({
},
include: {
images: { orderBy: { position: "asc" } },
pet: { select: { id: true, name: true, avatarKey: true } },
pet: { select: { id: true, name: true, nickname: true, avatarKey: true } },
milestone: true,
videoPost: { select: { status: true, muxPlaybackId: true } },
repost: {
include: {
originalPost: {
include: {
pet: { select: { id: true, name: true, avatarKey: true } },
pet: { select: { id: true, name: true, nickname: true, avatarKey: true } },
images: { orderBy: { position: "asc" } },
milestone: true,
},
+2 -1
View File
@@ -20,6 +20,7 @@ export const searchRouter = router({
where: {
OR: [
{ name: { contains: input.query, mode: "insensitive" } },
{ nickname: { contains: input.query, mode: "insensitive" } },
{ breed: { name: { contains: input.query, mode: "insensitive" } } },
],
...(input.cursor ? { id: { gt: input.cursor } } : {}),
@@ -84,7 +85,7 @@ export const searchRouter = router({
include: {
post: {
include: {
pet: { select: { id: true, name: true, avatarKey: true } },
pet: { select: { id: true, name: true, nickname: true, avatarKey: true } },
images: { orderBy: { position: "asc" }, take: 1 },
},
},
+1 -1
View File
@@ -172,7 +172,7 @@ export const storiesRouter = router({
return ctx.prisma.storyView.findMany({
where: { storyId: input.storyId },
include: {
viewerPet: { select: { id: true, name: true, avatarKey: true } },
viewerPet: { select: { id: true, name: true, nickname: true, avatarKey: true } },
},
orderBy: { viewedAt: "desc" },
take: 200,