Files
petfeed/src/components/post-creation/PhotoPostForm.tsx
T
admin 4ba6bbc0c3 feat(i18n): translate remaining hardcoded strings (audit finding #14)
Adds 6 new message namespaces (DeleteAccount, NotificationsPage,
PhotoPostForm, StoryForm, MilestoneForm, VideoUploadForm, StoryViewer,
StoryTray, RepostCard) plus a Health.share sub-namespace, with full
EN/DE parity (verified programmatically, 0 keys missing on either side):

- DeleteAccountDialog.tsx: was 100% hardcoded German for a destructive,
  irreversible action — now fully translated.
- notifications/page.tsx: mixed hardcoded English (FOLLOW/REACTION/...)
  with hardcoded German (BIRTHDAY/ADOPTION_DAY) in the same list.
- HealthDashboard.tsx + WeightChart.tsx: date formatting was hardcoded
  to toLocaleDateString("de-DE", ...) regardless of locale, unlike the
  DateWheelPicker in the same forms which correctly used useLocale().
  ShareDialog was entirely untranslated German alongside fully-translated
  sibling tabs.
- PhotoPostForm/MilestoneForm/StoryForm/VideoUploadForm: the entire
  post-creation flow had zero i18n despite the parent PostTypeSheet
  being fully translated.
- StoryViewer/StoryTray/RepostCard: no i18n at all.

health-card/[token]/page.tsx passes a fixed "de-DE" locale to the now
locale-aware WeightChart, since that whole page is a separate,
out-of-audit-scope public page that's intentionally all-German.
2026-07-22 19:55:01 +02:00

148 lines
4.4 KiB
TypeScript

"use client";
import { useState, useCallback } from "react";
import { useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
import { toast } from "sonner";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useTRPC } from "@/trpc/client";
import { useActivePet } from "@/context/ActivePetContext";
import { Button } from "@/components/ui/button";
import { MentionTextarea } from "@/components/ui/mention-textarea";
import { MultiImageUpload } from "@/components/post-creation/MultiImageUpload";
/**
* PhotoPostForm — multi-image upload + caption form for creating photo posts.
*
* Flow:
* 1. MultiImageUpload handles presigned URL + XHR PUT per image, collects storageKeys
* 2. Caption Textarea (optional, max 500)
* 3. "Share Post" CTA → posts.create({ petId, caption, imageKeys })
* 4. onSuccess: invalidate feed query, close Dialog, router.push('/feed') (D-03)
* 5. Toasts: "Post shared" on success, error copy on failure
*
* UI-SPEC copy:
* Caption placeholder: "Write a caption…"
* Caption error: "Caption must be 500 characters or fewer."
* CTA: "Share Post" (bg-orange-500)
* Success toast: "Post shared"
* Error toast: "Couldn't share your post. Please try again."
*
* D-02: Form lives inside a full-screen Dialog (no URL change).
* D-03: After posting, close Dialog and navigate to /feed.
*/
interface PhotoPostFormProps {
/** Called to close the parent Dialog */
onClose: () => void;
}
export function PhotoPostForm({ onClose }: PhotoPostFormProps) {
const t = useTranslations("PhotoPostForm");
const trpc = useTRPC();
const router = useRouter();
const queryClient = useQueryClient();
const { activePetId } = useActivePet();
const [imageKeys, setImageKeys] = useState<string[]>([]);
const [caption, setCaption] = useState("");
const [captionError, setCaptionError] = useState<string | null>(null);
const createMutation = useMutation(
trpc.posts.create.mutationOptions({
onSuccess: () => {
// Invalidate feed query so new post appears at top (D-03)
queryClient.invalidateQueries({ queryKey: [["trpc", "feed", "getFeed"]] });
onClose();
// Navigate to /feed with new post at top (D-03)
router.push("/feed");
toast.success(t("successToast"));
},
onError: () => {
toast.error(t("errorToast"));
},
})
);
const handleSubmit = useCallback(() => {
if (!activePetId) {
toast.error(t("noPetSelected"));
return;
}
if (imageKeys.length === 0) {
toast.error(t("noPhoto"));
return;
}
if (caption.length > 500) {
setCaptionError(t("captionError"));
return;
}
createMutation.mutate({
petId: activePetId,
caption: caption.trim() || null,
imageKeys,
});
}, [activePetId, imageKeys, caption, createMutation, t]);
if (!activePetId) {
return (
<div className="p-6 text-center text-sm text-muted-foreground">
{t("noPetSelected")}
</div>
);
}
return (
<div className="flex flex-col gap-4 p-4">
{/* Multi-image upload zone */}
<MultiImageUpload
petId={activePetId}
onKeysChange={setImageKeys}
/>
{/* Caption textarea */}
<div className="space-y-1">
<MentionTextarea
placeholder={t("captionPlaceholder")}
value={caption}
onChange={(val) => {
setCaption(val);
if (val.length > 500) {
setCaptionError(t("captionError"));
} else {
setCaptionError(null);
}
}}
maxLength={510}
rows={3}
className="text-sm resize-none"
/>
{captionError && (
<p className="text-xs text-destructive">{captionError}</p>
)}
{caption.length > 450 && !captionError && (
<p className="text-xs text-muted-foreground text-right">
{t("charactersRemaining", { count: 500 - caption.length })}
</p>
)}
</div>
{/* Share Post CTA — bg-orange-500 per UI-SPEC */}
<Button
onClick={handleSubmit}
disabled={
imageKeys.length === 0 ||
createMutation.isPending ||
!!captionError
}
className="w-full min-h-[44px] bg-orange-500 hover:bg-orange-600 text-white font-medium"
>
{createMutation.isPending ? t("sharing") : t("shareCta")}
</Button>
</div>
);
}