diff --git a/src/app/(app)/layout.tsx b/src/app/(app)/layout.tsx
index 5275a2d..1ef875d 100644
--- a/src/app/(app)/layout.tsx
+++ b/src/app/(app)/layout.tsx
@@ -1,10 +1,52 @@
+import { redirect } from "next/navigation";
+import { auth } from "@clerk/nextjs/server";
+import { createTRPCCaller } from "@/trpc/server";
+import { Sidebar } from "@/components/layout/Sidebar";
+import { MobileNav } from "@/components/layout/MobileNav";
+
/**
- * App shell layout — applied to all routes under /(app)/
- * Clerk's proxy.ts enforces authentication before this layout renders.
+ * Protected app shell layout — applied to all routes under /(app)/.
*
- * This is a minimal skeleton layout for Phase 1.
- * Full navigation (sidebar, pet switcher) is implemented in Plan 02.
+ * Auth enforcement:
+ * 1. Belt-and-suspenders: auth() check server-side — proxy.ts already gates the route,
+ * but the layout must not render pet data for unauthenticated requests.
+ * 2. If owner has no pets: redirect to /onboarding/pet (PET-01 prerequisite).
+ *
+ * Security (T-2-02): auth() called server-side — never trust the client to self-report auth.
+ * Security (T-2-03): pets.list scoped by ctx.userId — no cross-owner pet leakage.
+ *
+ * ActivePetProvider is already mounted in root layout.tsx so we do NOT re-wrap here.
*/
-export default function AppLayout({ children }: { children: React.ReactNode }) {
- return
{children}
;
+export default async function AppLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ // Belt-and-suspenders auth check (proxy.ts handles the primary gate)
+ const { userId } = await auth();
+ if (!userId) {
+ redirect("/log-in");
+ }
+
+ // Fetch the owner's pets via tRPC server caller (scoped by userId)
+ const caller = await createTRPCCaller();
+ const pets = await caller.pets.list();
+
+ // No pets → send to onboarding to create their first pet
+ if (pets.length === 0) {
+ redirect("/onboarding/pet");
+ }
+
+ return (
+
+ {/* Desktop sidebar — hidden on mobile */}
+
+
+ {/* Main content area */}
+ {children}
+
+ {/* Mobile bottom nav — hidden on desktop */}
+
+
+ );
}
diff --git a/src/app/(app)/onboarding/pet/page.tsx b/src/app/(app)/onboarding/pet/page.tsx
index 3b51763..8acf0d0 100644
--- a/src/app/(app)/onboarding/pet/page.tsx
+++ b/src/app/(app)/onboarding/pet/page.tsx
@@ -1,74 +1,32 @@
-"use client";
-
-import { useState } from "react";
-import { useRouter } from "next/navigation";
-import { useTRPC } from "@/trpc/client";
-import { useMutation } from "@tanstack/react-query";
-import { Button } from "@/components/ui/button";
-import { Input } from "@/components/ui/input";
-import { Label } from "@/components/ui/label";
-import { toast } from "sonner";
-
/**
- * Pet onboarding page — /onboarding/pet
- * Minimal form to create the first pet profile (walking skeleton).
+ * Onboarding pet page — /onboarding/pet
*
- * Full form (breed selector, avatar upload, bio, adoption story) is
- * implemented in Plan 02/03. This page proves the end-to-end tRPC flow.
+ * Landing page shown when an authenticated owner has no pets yet.
+ * The app shell layout.tsx redirects here when pets.length === 0.
+ *
+ * UI-SPEC: Pet Profile Screens — Create First Pet (onboarding)
+ * - Heading: "Add your first pet" (text-xl font-semibold)
+ * - Subheading: "Your pet will be their own star on OnlyPets." (text-sm text-muted-foreground)
+ * - PetForm placeholder (wired in Plan 03)
+ *
+ * This page is intentionally outside the (app) layout's pet-list check —
+ * owners are redirected HERE when they have no pets, so the shell must
+ * render without requiring an existing pet.
*/
export default function OnboardingPetPage() {
- const router = useRouter();
- const trpc = useTRPC();
- const [name, setName] = useState("");
-
- // pets.create mutation via tRPC
- const createPet = useMutation(
- trpc.pets.create.mutationOptions({
- onSuccess: (pet) => {
- toast.success(`Pet profile created for ${pet.name}!`);
- router.push("/pets");
- },
- onError: (err) => {
- toast.error(err.message || "Failed to create pet profile.");
- },
- })
- );
-
- const handleSubmit = (e: React.FormEvent) => {
- e.preventDefault();
- if (!name.trim()) return;
- // Using a placeholder speciesId — real species selector in Plan 02
- createPet.mutate({ name: name.trim(), speciesId: "placeholder" });
- };
-
return (
-
- Add your first pet
-
- Your pet will be their own star on OnlyPets.
-
+
+
+
Add your first pet
+
+ Your pet will be their own star on OnlyPets.
+
-