feat: shared back navigation on detail pages
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { getContainer } from "@/modules/garden/server/queries";
|
||||
import { ContainerDetail } from "@/modules/garden/components/container-detail";
|
||||
import { DetailBackLink } from "@/components/detail-back-link";
|
||||
import { getShareLinksForEntity } from "@/modules/_core/share";
|
||||
import { ContainerDetail } from "@/modules/garden/components/container-detail";
|
||||
import { getContainer } from "@/modules/garden/server/queries";
|
||||
|
||||
export default async function ContainerPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
@@ -12,6 +13,7 @@ export default async function ContainerPage({ params }: { params: Promise<{ id:
|
||||
if (!container) notFound();
|
||||
return (
|
||||
<div className="page-content">
|
||||
<DetailBackLink href="/garden" label="Garden" className="mb-4" />
|
||||
<ContainerDetail container={container} shareLinks={shareLinks} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { DetailBackLink } from "@/components/detail-back-link";
|
||||
import { createContainer } from "@/modules/garden/server/actions";
|
||||
|
||||
export default function NewContainerPage() {
|
||||
@@ -14,21 +15,8 @@ export default function NewContainerPage() {
|
||||
|
||||
return (
|
||||
<div className="page-content max-w-lg">
|
||||
<div className="flex items-center gap-3 mb-1">
|
||||
<a href="/garden" className="btn btn-ghost btn-icon" aria-label="Back to garden">
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<polyline points="15 18 9 12 15 6" />
|
||||
</svg>
|
||||
</a>
|
||||
<div className="mb-1 flex flex-col gap-2">
|
||||
<DetailBackLink href="/garden" label="Garden" />
|
||||
<h1 className="page-title">New container</h1>
|
||||
</div>
|
||||
<form action={handleCreate} className="flex flex-col gap-4 mt-4">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { getPlant, listContainers } from "@/modules/garden/server/queries";
|
||||
import { DetailBackLink } from "@/components/detail-back-link";
|
||||
import { PlantForm } from "@/modules/garden/components/plant-form";
|
||||
import { getPlant, listContainers } from "@/modules/garden/server/queries";
|
||||
|
||||
export default async function EditPlantPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
@@ -9,6 +10,7 @@ export default async function EditPlantPage({ params }: { params: Promise<{ id:
|
||||
|
||||
return (
|
||||
<div className="page-content max-w-xl">
|
||||
<DetailBackLink href={`/garden/plants/${plant.id}`} label={plant.name} className="mb-3" />
|
||||
<h1 className="page-title">Edit Plant</h1>
|
||||
<PlantForm existingPlant={plant} containers={containers} />
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { DetailBackLink } from "@/components/detail-back-link";
|
||||
import { getShareLinksForEntity } from "@/modules/_core/share";
|
||||
import { PlantDetail } from "@/modules/garden/components/plant-detail";
|
||||
import { listCalendars } from "@/modules/garden/server/calendar-bridge";
|
||||
import { getCareLogs, getCareSchedules, getPlant } from "@/modules/garden/server/queries";
|
||||
import { PlantDetail } from "@/modules/garden/components/plant-detail";
|
||||
import { getShareLinksForEntity } from "@/modules/_core/share";
|
||||
|
||||
export default async function PlantPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
@@ -17,6 +18,7 @@ export default async function PlantPage({ params }: { params: Promise<{ id: stri
|
||||
|
||||
return (
|
||||
<div className="page-content">
|
||||
<DetailBackLink href="/garden?tab=plants" label="Plants" className="mb-4" />
|
||||
<PlantDetail
|
||||
plant={plant}
|
||||
careLogs={careLogs}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { listContainers } from "@/modules/garden/server/queries";
|
||||
import { DetailBackLink } from "@/components/detail-back-link";
|
||||
import { PlantForm } from "@/modules/garden/components/plant-form";
|
||||
import { listContainers } from "@/modules/garden/server/queries";
|
||||
|
||||
export default async function NewPlantPage({
|
||||
searchParams,
|
||||
@@ -11,21 +12,8 @@ export default async function NewPlantPage({
|
||||
|
||||
return (
|
||||
<div className="page-content max-w-xl">
|
||||
<div className="flex items-center gap-3 mb-1">
|
||||
<a href="/garden" className="btn btn-ghost btn-icon" aria-label="Back to garden">
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<polyline points="15 18 9 12 15 6" />
|
||||
</svg>
|
||||
</a>
|
||||
<div className="mb-1 flex flex-col gap-2">
|
||||
<DetailBackLink href="/garden?tab=plants" label="Plants" />
|
||||
<h1 className="page-title">Add Plant</h1>
|
||||
</div>
|
||||
<PlantForm containers={containers} defaultContainerId={params.containerId ?? null} />
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import { db } from "@/lib/db";
|
||||
import { getCurrentSession } from "@/lib/session";
|
||||
import { householdMembers, users } from "@/modules/_core/schema";
|
||||
import { DetailBackLink } from "@/components/detail-back-link";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { db } from "@/lib/db";
|
||||
import { getCurrentSession } from "@/lib/session";
|
||||
import { householdMembers, users } from "@/modules/_core/schema";
|
||||
import { renameHousehold } from "./actions";
|
||||
|
||||
export default async function HouseholdSettingsPage() {
|
||||
@@ -18,6 +19,7 @@ export default async function HouseholdSettingsPage() {
|
||||
|
||||
return (
|
||||
<div className="container max-w-2xl py-8 space-y-6">
|
||||
<DetailBackLink href="/settings" label="Settings" />
|
||||
<h1 className="text-2xl font-semibold">Household Settings</h1>
|
||||
|
||||
<Card>
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import Link from "next/link";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type DetailBackLinkProps = {
|
||||
href: string;
|
||||
label: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function DetailBackLink({ href, label, className }: DetailBackLinkProps) {
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
aria-label={`Back to ${label}`}
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1.5 text-sm text-muted-foreground transition-colors hover:text-foreground",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<ArrowLeft aria-hidden="true" />
|
||||
<span>{label}</span>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Archive, Plus, Trash2 } from "lucide-react";
|
||||
import { useEffect, useRef, useState, useTransition } from "react";
|
||||
import { DetailBackLink } from "@/components/detail-back-link";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { ShareButton } from "@/components/share-button";
|
||||
@@ -99,6 +100,7 @@ export function ListDetail({ initialList }: { initialList: ListDetailDto }) {
|
||||
|
||||
return (
|
||||
<div className="mx-auto grid w-full max-w-3xl gap-4">
|
||||
<DetailBackLink href="/lists" label="Lists" />
|
||||
<header className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="min-w-0 flex items-center gap-3 flex-1">
|
||||
<Input
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Pin, PinOff, Save, Trash2 } from "lucide-react";
|
||||
import { useState, useTransition } from "react";
|
||||
import { DetailBackLink } from "@/components/detail-back-link";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
@@ -65,6 +66,7 @@ export function NoteEditor({ note }: { note?: NoteDto }) {
|
||||
|
||||
return (
|
||||
<div className="mx-auto grid w-full max-w-6xl gap-4">
|
||||
<DetailBackLink href="/notes" label="Notes" />
|
||||
<header className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div className="flex items-center gap-2 min-w-0 flex-1">
|
||||
{pinned && <Pin className="size-3.5 text-[var(--accent)]" />}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test.describe("detail page back navigation", () => {
|
||||
test("plant detail returns to plants tab", async ({ page }) => {
|
||||
const suffix = Date.now().toString();
|
||||
const plantName = `Back Nav Plant ${suffix}`;
|
||||
|
||||
await page.goto("/garden?tab=plants");
|
||||
await page.getByRole("link", { name: /add plant/i }).click();
|
||||
await page.getByLabel("Name").fill(plantName);
|
||||
await page.getByRole("button", { name: /create/i }).click();
|
||||
await expect(page).toHaveURL(/\/garden\/plants\//);
|
||||
|
||||
const backLink = page.getByRole("link", { name: "Back to Plants" });
|
||||
await expect(backLink).toBeVisible();
|
||||
await backLink.click();
|
||||
|
||||
await expect(page).toHaveURL(/\/garden\?tab=plants$/);
|
||||
await expect(page.getByRole("heading", { name: "Garden" })).toBeVisible();
|
||||
|
||||
await page.getByText(plantName).click();
|
||||
await page.getByRole("button", { name: /delete/i }).click();
|
||||
await page.getByRole("button", { name: /confirm/i }).click();
|
||||
});
|
||||
|
||||
test("note detail returns to notes index", async ({ page }) => {
|
||||
const suffix = Date.now().toString();
|
||||
const title = `Back Nav Note ${suffix}`;
|
||||
|
||||
await page.goto("/notes/new");
|
||||
await page.getByLabel("Title").fill(title);
|
||||
await page.getByRole("button", { name: "Save note" }).click();
|
||||
await expect(page).toHaveURL(/\/notes\/[0-9a-f-]+$/);
|
||||
|
||||
const backLink = page.getByRole("link", { name: "Back to Notes" });
|
||||
await expect(backLink).toBeVisible();
|
||||
await backLink.click();
|
||||
|
||||
await expect(page).toHaveURL(/\/notes$/);
|
||||
await expect(page.getByRole("heading", { name: "Notes" })).toBeVisible();
|
||||
|
||||
await page.getByRole("link", { name: new RegExp(title) }).click();
|
||||
await page.getByRole("button", { name: "Delete note" }).click();
|
||||
});
|
||||
|
||||
test("list detail returns to lists index", async ({ page }) => {
|
||||
const suffix = Date.now().toString();
|
||||
const listName = `Back Nav List ${suffix}`;
|
||||
|
||||
await page.goto("/lists");
|
||||
await page.getByLabel("Type").fill("task");
|
||||
await page.getByLabel("Name").fill(listName);
|
||||
await page.getByRole("button", { name: "New list" }).click();
|
||||
await page.getByRole("link", { name: new RegExp(listName) }).click();
|
||||
|
||||
const backLink = page.getByRole("link", { name: "Back to Lists" });
|
||||
await expect(backLink).toBeVisible();
|
||||
await backLink.click();
|
||||
|
||||
await expect(page).toHaveURL(/\/lists$/);
|
||||
await expect(page.getByRole("heading", { name: "Lists" })).toBeVisible();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user