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.
148 lines
4.4 KiB
TypeScript
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>
|
|
);
|
|
}
|