feat(i18n): translate pet-profile area (Edit Profile, Health, stats, dropdowns, ReportSheet, BlockDialog)
The entire /pets area (My Pets grid, pet profile page, followers/following lists, the profile options dropdown, ReportSheet, BlockDialog) had zero i18n despite ProfileTabs right next to it being fully translated — user reported still seeing English after switching the app to German. Adds 7 new message namespaces (PetsPage, PetProfile, FollowersPage, FollowingPage, ProfileActions, ReportSheet, BlockDialog) with full EN/DE parity, plus bio/adoptionStory placeholder keys to the existing PetForm namespace (two placeholders were hardcoded English template literals despite the rest of that form already being translated). Server Components (pets/page.tsx, pets/[petId]/page.tsx, followers/following pages) use getTranslations from next-intl/server instead of the useTranslations client hook.
This commit is contained in:
1 parent
4ba6bbc0c3
commit
b7719397e6
11 files changed
+176
-47
No files matched your search
@@ -69,7 +69,11 @@
|
||||
"selectSpeciesFirst": "Wähle zuerst eine Tierart aus",
|
||||
"noBreedsFound": "Keine Rassen gefunden.",
|
||||
"bio": "Bio",
|
||||
"bioPlaceholder": "Erzähl uns von {petName}…",
|
||||
"bioPlaceholderFallback": "Erzähl uns von deinem Tier…",
|
||||
"adoptionStory": "Adoptionsgeschichte",
|
||||
"adoptionStoryPlaceholder": "Wie kam {petName} zu deiner Familie?",
|
||||
"adoptionStoryPlaceholderFallback": "Wie kam dein Tier zu deiner Familie?",
|
||||
"birthday": "Geburtstag",
|
||||
"birthdayHint": "Für Geburtstags-Erinnerungen",
|
||||
"adoptedAt": "Adoptionsdatum",
|
||||
@@ -425,5 +429,58 @@
|
||||
},
|
||||
"RepostCard": {
|
||||
"pawedBackBy": "Paw-Back von"
|
||||
},
|
||||
"PetsPage": {
|
||||
"title": "Meine Tiere",
|
||||
"addPet": "Tier anlegen",
|
||||
"emptyTitle": "Füge dein erstes Tier hinzu",
|
||||
"emptyDesc": "Gib deinem Tier ein eigenes Profil auf PawFeed."
|
||||
},
|
||||
"PetProfile": {
|
||||
"editProfile": "Profil bearbeiten",
|
||||
"health": "Gesundheit",
|
||||
"managedBy": "Verwaltet von @{username}",
|
||||
"statsPosts": "Posts",
|
||||
"statsFollowers": "Follower",
|
||||
"statsFollowing": "Folgt",
|
||||
"adoptionStory": "Adoptionsgeschichte"
|
||||
},
|
||||
"FollowersPage": {
|
||||
"title": "{count} Follower",
|
||||
"empty": "{petName} hat noch keine Follower."
|
||||
},
|
||||
"FollowingPage": {
|
||||
"title": "Folgt {count}",
|
||||
"empty": "{petName} folgt noch niemandem."
|
||||
},
|
||||
"ProfileActions": {
|
||||
"message": "Nachricht",
|
||||
"cannotStartConversation": "Unterhaltung konnte nicht gestartet werden.",
|
||||
"moreOptions": "Weitere Optionen",
|
||||
"reportProfile": "Profil melden",
|
||||
"blockPet": "{petName} blockieren"
|
||||
},
|
||||
"ReportSheet": {
|
||||
"title": "Melden",
|
||||
"reasonQuestion": "Warum meldest du das?",
|
||||
"reasonSpam": "Spam",
|
||||
"reasonHarassment": "Belästigung oder Mobbing",
|
||||
"reasonInappropriate": "Unangemessene Inhalte",
|
||||
"reasonMisinformation": "Falsche oder irreführende Informationen",
|
||||
"reasonOther": "Etwas anderes",
|
||||
"notesPlaceholder": "Details hinzufügen (optional)",
|
||||
"charactersRemaining": "Noch {count} Zeichen",
|
||||
"submit": "Meldung absenden",
|
||||
"cancel": "Nicht melden",
|
||||
"successToast": "Meldung übermittelt. Wir prüfen sie in Kürze.",
|
||||
"errorToast": "Deine Meldung konnte nicht übermittelt werden. Bitte versuch es erneut."
|
||||
},
|
||||
"BlockDialog": {
|
||||
"title": "{petName} blockieren?",
|
||||
"description": "{petName}s Posts und Profil werden für dich ausgeblendet. Niemand erfährt, dass du blockiert hast.",
|
||||
"cancel": "Nicht blockieren",
|
||||
"confirm": "Blockieren",
|
||||
"successToast": "Du hast {petName} blockiert",
|
||||
"errorToast": "Tier konnte nicht blockiert werden. Bitte versuch es erneut."
|
||||
}
|
||||
}
|
||||
@@ -69,7 +69,11 @@
|
||||
"selectSpeciesFirst": "Select a species first",
|
||||
"noBreedsFound": "No breeds found.",
|
||||
"bio": "Bio",
|
||||
"bioPlaceholder": "Tell us about {petName}…",
|
||||
"bioPlaceholderFallback": "Tell us about your pet…",
|
||||
"adoptionStory": "Adoption story",
|
||||
"adoptionStoryPlaceholder": "How did {petName} join your family?",
|
||||
"adoptionStoryPlaceholderFallback": "How did your pet join your family?",
|
||||
"birthday": "Birthday",
|
||||
"birthdayHint": "For birthday reminders",
|
||||
"adoptedAt": "Adoption date",
|
||||
@@ -425,5 +429,58 @@
|
||||
},
|
||||
"RepostCard": {
|
||||
"pawedBackBy": "Pawed-back by"
|
||||
},
|
||||
"PetsPage": {
|
||||
"title": "My Pets",
|
||||
"addPet": "Add Pet",
|
||||
"emptyTitle": "Add your first pet",
|
||||
"emptyDesc": "Give your pet their own profile on PawFeed."
|
||||
},
|
||||
"PetProfile": {
|
||||
"editProfile": "Edit profile",
|
||||
"health": "Health",
|
||||
"managedBy": "Managed by @{username}",
|
||||
"statsPosts": "posts",
|
||||
"statsFollowers": "followers",
|
||||
"statsFollowing": "following",
|
||||
"adoptionStory": "Adoption story"
|
||||
},
|
||||
"FollowersPage": {
|
||||
"title": "{count} Followers",
|
||||
"empty": "{petName} doesn't have any followers yet."
|
||||
},
|
||||
"FollowingPage": {
|
||||
"title": "Following {count}",
|
||||
"empty": "{petName} isn't following anyone yet."
|
||||
},
|
||||
"ProfileActions": {
|
||||
"message": "Message",
|
||||
"cannotStartConversation": "Cannot start conversation.",
|
||||
"moreOptions": "More options",
|
||||
"reportProfile": "Report profile",
|
||||
"blockPet": "Block {petName}"
|
||||
},
|
||||
"ReportSheet": {
|
||||
"title": "Report",
|
||||
"reasonQuestion": "Why are you reporting this?",
|
||||
"reasonSpam": "Spam",
|
||||
"reasonHarassment": "Harassment or bullying",
|
||||
"reasonInappropriate": "Inappropriate content",
|
||||
"reasonMisinformation": "False or misleading information",
|
||||
"reasonOther": "Something else",
|
||||
"notesPlaceholder": "Add details (optional)",
|
||||
"charactersRemaining": "{count} characters remaining",
|
||||
"submit": "Submit Report",
|
||||
"cancel": "Don't report",
|
||||
"successToast": "Report submitted. We'll review it shortly.",
|
||||
"errorToast": "Couldn't submit your report. Please try again."
|
||||
},
|
||||
"BlockDialog": {
|
||||
"title": "Block {petName}?",
|
||||
"description": "{petName}'s posts and profile will be hidden from you. They won't know you blocked them.",
|
||||
"cancel": "Don't block",
|
||||
"confirm": "Block",
|
||||
"successToast": "You've blocked {petName}",
|
||||
"errorToast": "Couldn't block this pet. Please try again."
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { PetCard } from "@/components/pet/PetCard";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useActivePet } from "@/context/ActivePetContext";
|
||||
@@ -24,21 +25,22 @@ interface PetGridProps {
|
||||
}
|
||||
|
||||
export function PetGrid({ pets }: PetGridProps) {
|
||||
const t = useTranslations("PetsPage");
|
||||
const { activePetId, setActivePet } = useActivePet();
|
||||
const router = useRouter();
|
||||
|
||||
if (pets.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border-2 border-dashed border-muted p-12 text-center">
|
||||
<p className="text-base font-semibold">Add your first pet</p>
|
||||
<p className="text-base font-semibold">{t("emptyTitle")}</p>
|
||||
<p className="text-sm text-muted-foreground max-w-sm">
|
||||
Give your pet their own profile on PawFeed.
|
||||
{t("emptyDesc")}
|
||||
</p>
|
||||
<Button
|
||||
onClick={() => router.push("/onboarding/pet")}
|
||||
className="mt-2"
|
||||
>
|
||||
Add Pet
|
||||
{t("addPet")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
@@ -63,7 +65,7 @@ export function PetGrid({ pets }: PetGridProps) {
|
||||
className="flex flex-col items-center justify-center gap-2 rounded-xl border-2 border-dashed border-muted p-6 text-sm text-muted-foreground hover:border-muted-foreground/50 transition-colors min-h-[180px]"
|
||||
>
|
||||
<span className="text-2xl font-light">+</span>
|
||||
<span>Add Pet</span>
|
||||
<span>{t("addPet")}</span>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { MoreHorizontal, MessageCircle, Loader2 } from "lucide-react";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
@@ -40,6 +41,7 @@ interface ProfileActionsProps {
|
||||
}
|
||||
|
||||
export function ProfileActions({ petId, petName, ownerId }: ProfileActionsProps) {
|
||||
const t = useTranslations("ProfileActions");
|
||||
const { activePetId } = useActivePet();
|
||||
const router = useRouter();
|
||||
const trpc = useTRPC();
|
||||
@@ -49,7 +51,7 @@ export function ProfileActions({ petId, petName, ownerId }: ProfileActionsProps)
|
||||
const getOrCreateMutation = useMutation(
|
||||
trpc.conversations.getOrCreate.mutationOptions({
|
||||
onSuccess: (conv) => router.push(`/messages/${conv.id}`),
|
||||
onError: (err) => toast.error(err.message ?? "Cannot start conversation."),
|
||||
onError: (err) => toast.error(err.message ?? t("cannotStartConversation")),
|
||||
})
|
||||
);
|
||||
|
||||
@@ -88,7 +90,7 @@ export function ProfileActions({ petId, petName, ownerId }: ProfileActionsProps)
|
||||
) : (
|
||||
<MessageCircle className="h-4 w-4" />
|
||||
)}
|
||||
Message
|
||||
{t("message")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -98,7 +100,7 @@ export function ProfileActions({ petId, petName, ownerId }: ProfileActionsProps)
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
className="h-[44px] w-[44px] flex items-center justify-center rounded-md border border-border hover:bg-muted transition-colors cursor-pointer"
|
||||
aria-label="More options"
|
||||
aria-label={t("moreOptions")}
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</DropdownMenuTrigger>
|
||||
@@ -107,14 +109,14 @@ export function ProfileActions({ petId, petName, ownerId }: ProfileActionsProps)
|
||||
onClick={() => setReportOpen(true)}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
Report profile
|
||||
{t("reportProfile")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={() => setBlockOpen(true)}
|
||||
className="cursor-pointer text-destructive focus:text-destructive"
|
||||
>
|
||||
Block {petName}
|
||||
{t("blockPet", { petName })}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { createTRPCCaller } from "@/trpc/server";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { FollowersList } from "@/app/(app)/pets/[petId]/followers/FollowersList";
|
||||
@@ -20,6 +21,7 @@ interface PageProps {
|
||||
export default async function FollowersPage({ params }: PageProps) {
|
||||
const { petId } = await params;
|
||||
|
||||
const t = await getTranslations("FollowersPage");
|
||||
const trpc = await createTRPCCaller();
|
||||
|
||||
let pet;
|
||||
@@ -36,12 +38,12 @@ export default async function FollowersPage({ params }: PageProps) {
|
||||
return (
|
||||
<div className="max-w-[480px] mx-auto px-4 py-6">
|
||||
<h1 className="text-xl font-semibold mb-6">
|
||||
{followers.length} Followers
|
||||
{t("title", { count: followers.length })}
|
||||
</h1>
|
||||
|
||||
{followers.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground text-center py-8">
|
||||
{pet.name} doesn't have any followers yet.
|
||||
{t("empty", { petName: pet.name })}
|
||||
</p>
|
||||
) : (
|
||||
<ScrollArea className="h-[calc(100vh-120px)]">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { createTRPCCaller } from "@/trpc/server";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { FollowingList } from "@/app/(app)/pets/[petId]/following/FollowingList";
|
||||
@@ -20,6 +21,7 @@ interface PageProps {
|
||||
export default async function FollowingPage({ params }: PageProps) {
|
||||
const { petId } = await params;
|
||||
|
||||
const t = await getTranslations("FollowingPage");
|
||||
const trpc = await createTRPCCaller();
|
||||
|
||||
let pet;
|
||||
@@ -36,12 +38,12 @@ export default async function FollowingPage({ params }: PageProps) {
|
||||
return (
|
||||
<div className="max-w-[480px] mx-auto px-4 py-6">
|
||||
<h1 className="text-xl font-semibold mb-6">
|
||||
Following {following.length}
|
||||
{t("title", { count: following.length })}
|
||||
</h1>
|
||||
|
||||
{following.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground text-center py-8">
|
||||
{pet.name} isn't following anyone yet.
|
||||
{t("empty", { petName: pet.name })}
|
||||
</p>
|
||||
) : (
|
||||
<ScrollArea className="h-[calc(100vh-120px)]">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { currentUser } from "@clerk/nextjs/server";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { createTRPCCaller } from "@/trpc/server";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
@@ -28,6 +29,7 @@ function getInitials(name: string): string {
|
||||
export default async function PetProfilePage({ params }: PageProps) {
|
||||
const { petId } = await params;
|
||||
|
||||
const t = await getTranslations("PetProfile");
|
||||
const trpc = await createTRPCCaller();
|
||||
|
||||
let pet;
|
||||
@@ -74,7 +76,7 @@ export default async function PetProfilePage({ params }: PageProps) {
|
||||
{/* Owner attribution — only shown on own pet */}
|
||||
{isOwnPet && ownerUsername && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Managed by @{ownerUsername}
|
||||
{t("managedBy", { username: ownerUsername })}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -85,13 +87,13 @@ export default async function PetProfilePage({ params }: PageProps) {
|
||||
href={`/pets/${pet.id}/edit`}
|
||||
className={buttonVariants({ variant: "outline" })}
|
||||
>
|
||||
Edit profile
|
||||
{t("editProfile")}
|
||||
</Link>
|
||||
<Link
|
||||
href={`/pets/${pet.id}/health`}
|
||||
className={buttonVariants({ variant: "outline" })}
|
||||
>
|
||||
Health
|
||||
{t("health")}
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
@@ -101,21 +103,21 @@ export default async function PetProfilePage({ params }: PageProps) {
|
||||
<div className="flex justify-center gap-8 mb-6">
|
||||
<div className="flex flex-col items-center">
|
||||
<span className="text-xl font-semibold">0</span>
|
||||
<span className="text-xs text-muted-foreground">posts</span>
|
||||
<span className="text-xs text-muted-foreground">{t("statsPosts")}</span>
|
||||
</div>
|
||||
<Link
|
||||
href={`/pets/${pet.id}/followers`}
|
||||
className="flex flex-col items-center hover:opacity-80 transition-opacity"
|
||||
>
|
||||
<span className="text-xl font-semibold">{followers.length}</span>
|
||||
<span className="text-xs text-muted-foreground">followers</span>
|
||||
<span className="text-xs text-muted-foreground">{t("statsFollowers")}</span>
|
||||
</Link>
|
||||
<Link
|
||||
href={`/pets/${pet.id}/following`}
|
||||
className="flex flex-col items-center hover:opacity-80 transition-opacity"
|
||||
>
|
||||
<span className="text-xl font-semibold">{following.length}</span>
|
||||
<span className="text-xs text-muted-foreground">following</span>
|
||||
<span className="text-xs text-muted-foreground">{t("statsFollowing")}</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -135,7 +137,7 @@ export default async function PetProfilePage({ params }: PageProps) {
|
||||
<>
|
||||
<Separator className="mb-6" />
|
||||
<section className="mb-6">
|
||||
<h2 className="text-base font-semibold mb-2">Adoption story</h2>
|
||||
<h2 className="text-base font-semibold mb-2">{t("adoptionStory")}</h2>
|
||||
<p className="text-sm leading-relaxed">{pet.adoptionStory}</p>
|
||||
</section>
|
||||
</>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Link from "next/link";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { createTRPCCaller } from "@/trpc/server";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import { PetGrid } from "./PetGrid";
|
||||
@@ -15,6 +16,7 @@ export default async function PetsPage({
|
||||
}: {
|
||||
searchParams: Promise<{ deleteAccount?: string }>;
|
||||
}) {
|
||||
const t = await getTranslations("PetsPage");
|
||||
const trpc = await createTRPCCaller();
|
||||
const pets = await trpc.pets.list();
|
||||
const { deleteAccount } = await searchParams;
|
||||
@@ -22,10 +24,10 @@ export default async function PetsPage({
|
||||
return (
|
||||
<div className="container max-w-3xl mx-auto px-6 py-8">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h1 className="text-xl font-semibold">My Pets</h1>
|
||||
<h1 className="text-xl font-semibold">{t("title")}</h1>
|
||||
{/* base-nova Button doesn't support asChild — use Link with button styles */}
|
||||
<Link href="/onboarding/pet" className={buttonVariants({ variant: "default" })}>
|
||||
Add Pet
|
||||
{t("addPet")}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -312,7 +312,7 @@ export function PetForm({
|
||||
<Textarea
|
||||
id="bio"
|
||||
maxLength={150}
|
||||
placeholder={`Tell us about ${petName || "your pet"}…`}
|
||||
placeholder={petName ? t("bioPlaceholder", { petName }) : t("bioPlaceholderFallback")}
|
||||
aria-invalid={!!errors.bio}
|
||||
{...register("bio")}
|
||||
/>
|
||||
@@ -330,7 +330,7 @@ export function PetForm({
|
||||
<Textarea
|
||||
id="adoption-story"
|
||||
maxLength={500}
|
||||
placeholder={`How did ${petName || "your pet"} join your family?`}
|
||||
placeholder={petName ? t("adoptionStoryPlaceholder", { petName }) : t("adoptionStoryPlaceholderFallback")}
|
||||
aria-invalid={!!errors.adoptionStory}
|
||||
{...register("adoptionStory")}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
AlertDialog,
|
||||
@@ -43,19 +44,20 @@ export function BlockDialog({
|
||||
blockedPetId,
|
||||
blockedPetName,
|
||||
}: BlockDialogProps) {
|
||||
const t = useTranslations("BlockDialog");
|
||||
const trpc = useTRPC();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const blockMutation = useMutation(
|
||||
trpc.blocks.create.mutationOptions({
|
||||
onSuccess: () => {
|
||||
toast.success(`You've blocked ${blockedPetName}`);
|
||||
toast.success(t("successToast", { petName: blockedPetName }));
|
||||
onOpenChange(false);
|
||||
// Invalidate any cached block lists so feed hydration picks up the new block
|
||||
queryClient.invalidateQueries({ queryKey: ["follows"] });
|
||||
},
|
||||
onError: () => {
|
||||
toast.error("Couldn't block this pet. Please try again.");
|
||||
toast.error(t("errorToast"));
|
||||
},
|
||||
})
|
||||
);
|
||||
@@ -68,20 +70,19 @@ export function BlockDialog({
|
||||
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Block {blockedPetName}?</AlertDialogTitle>
|
||||
<AlertDialogTitle>{t("title", { petName: blockedPetName })}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{blockedPetName}'s posts and profile will be hidden from you.
|
||||
They won't know you blocked them.
|
||||
{t("description", { petName: blockedPetName })}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Don't block</AlertDialogCancel>
|
||||
<AlertDialogCancel>{t("cancel")}</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleBlock}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
disabled={blockMutation.isPending}
|
||||
>
|
||||
Block
|
||||
{t("confirm")}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { toast } from "sonner";
|
||||
import { ReportReason } from "@prisma/client";
|
||||
import {
|
||||
@@ -41,14 +42,6 @@ interface ReportSheetProps {
|
||||
targetPetId?: string;
|
||||
}
|
||||
|
||||
const REPORT_REASONS: { value: ReportReason; label: string }[] = [
|
||||
{ value: ReportReason.SPAM, label: "Spam" },
|
||||
{ value: ReportReason.HARASSMENT, label: "Harassment or bullying" },
|
||||
{ value: ReportReason.INAPPROPRIATE, label: "Inappropriate content" },
|
||||
{ value: ReportReason.MISINFORMATION, label: "False or misleading information" },
|
||||
{ value: ReportReason.OTHER, label: "Something else" },
|
||||
];
|
||||
|
||||
export function ReportSheet({
|
||||
open,
|
||||
onOpenChange,
|
||||
@@ -56,21 +49,30 @@ export function ReportSheet({
|
||||
targetPostId,
|
||||
targetPetId,
|
||||
}: ReportSheetProps) {
|
||||
const t = useTranslations("ReportSheet");
|
||||
const trpc = useTRPC();
|
||||
const [selectedReason, setSelectedReason] = useState<ReportReason | "">("");
|
||||
const [notes, setNotes] = useState("");
|
||||
|
||||
const REPORT_REASONS: { value: ReportReason; label: string }[] = [
|
||||
{ value: ReportReason.SPAM, label: t("reasonSpam") },
|
||||
{ value: ReportReason.HARASSMENT, label: t("reasonHarassment") },
|
||||
{ value: ReportReason.INAPPROPRIATE, label: t("reasonInappropriate") },
|
||||
{ value: ReportReason.MISINFORMATION, label: t("reasonMisinformation") },
|
||||
{ value: ReportReason.OTHER, label: t("reasonOther") },
|
||||
];
|
||||
|
||||
const reportMutation = useMutation(
|
||||
trpc.reports.create.mutationOptions({
|
||||
onSuccess: () => {
|
||||
toast.success("Report submitted. We'll review it shortly.");
|
||||
toast.success(t("successToast"));
|
||||
onOpenChange(false);
|
||||
// Reset form state
|
||||
setSelectedReason("");
|
||||
setNotes("");
|
||||
},
|
||||
onError: () => {
|
||||
toast.error("Couldn't submit your report. Please try again.");
|
||||
toast.error(t("errorToast"));
|
||||
},
|
||||
})
|
||||
);
|
||||
@@ -97,13 +99,13 @@ export function ReportSheet({
|
||||
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||
<SheetContent side="bottom" className="sm:side-right sm:max-w-[400px]">
|
||||
<SheetHeader>
|
||||
<SheetTitle className="text-xl font-semibold">Report</SheetTitle>
|
||||
<SheetTitle className="text-xl font-semibold">{t("title")}</SheetTitle>
|
||||
</SheetHeader>
|
||||
|
||||
<div className="mt-4 space-y-4">
|
||||
<div>
|
||||
<p className="text-sm font-medium mb-3">
|
||||
Why are you reporting this?
|
||||
{t("reasonQuestion")}
|
||||
</p>
|
||||
<RadioGroup
|
||||
value={selectedReason}
|
||||
@@ -126,7 +128,7 @@ export function ReportSheet({
|
||||
|
||||
<div>
|
||||
<Textarea
|
||||
placeholder="Add details (optional)"
|
||||
placeholder={t("notesPlaceholder")}
|
||||
value={notes}
|
||||
onChange={(e) => setNotes(e.target.value)}
|
||||
maxLength={500}
|
||||
@@ -135,7 +137,7 @@ export function ReportSheet({
|
||||
/>
|
||||
{notes.length > 450 && (
|
||||
<p className="text-xs text-muted-foreground mt-1 text-right">
|
||||
{500 - notes.length} characters remaining
|
||||
{t("charactersRemaining", { count: 500 - notes.length })}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -146,14 +148,14 @@ export function ReportSheet({
|
||||
disabled={!selectedReason || reportMutation.isPending}
|
||||
className="w-full min-h-[44px] bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
Submit Report
|
||||
{t("submit")}
|
||||
</Button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCancel}
|
||||
className="w-full text-sm text-muted-foreground hover:text-foreground py-2"
|
||||
>
|
||||
Don't report
|
||||
{t("cancel")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user