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:
1 parent
4daac56bfe
commit
1293f7c879
28 files changed
+110
-51
No files matched your search
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 && (
|
||||
<>
|
||||
|
||||
@@ -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" : ""}`}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -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" />
|
||||
)}
|
||||
|
||||
@@ -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" />
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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
@@ -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 } } },
|
||||
},
|
||||
});
|
||||
}),
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 } },
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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 } },
|
||||
|
||||
@@ -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 } },
|
||||
|
||||
@@ -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 } } } },
|
||||
},
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 } },
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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 },
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user