Compare commits
10 Commits
90c0dd6812
...
b73c9311f5
| Author | SHA1 | Date | |
|---|---|---|---|
| b73c9311f5 | |||
| 434b7c8dc8 | |||
| 800c231580 | |||
| aa81948f29 | |||
| a4536d20b4 | |||
| 75337be90c | |||
| 298d25a579 | |||
| 276bea81b0 | |||
| 595520296c | |||
| 4330157491 |
@@ -1,57 +1,60 @@
|
||||
"use client";
|
||||
|
||||
import Form from "next/form";
|
||||
import { ContactRecord } from "@/app/data";
|
||||
import Form from "next/form";
|
||||
import { use } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export default function Contact() {
|
||||
const contact = {
|
||||
first: "Your",
|
||||
last: "Name",
|
||||
avatar: "https://placecats.com/200/200",
|
||||
twitter: "your_handle",
|
||||
notes: "Some notes",
|
||||
favorite: true,
|
||||
};
|
||||
export default function Contact({
|
||||
contact
|
||||
}: {
|
||||
contact: Promise<ContactRecord>
|
||||
}) {
|
||||
const c = use(contact);
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<div id="contact">
|
||||
<div>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
alt={`${contact.first} ${contact.last} avatar`}
|
||||
key={contact.avatar}
|
||||
src={contact.avatar}
|
||||
alt={`${c.first} ${c.last} avatar`}
|
||||
key={c.avatar}
|
||||
src={c.avatar}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h1>
|
||||
{contact.first || contact.last ? (
|
||||
{c.first || c.last ? (
|
||||
<>
|
||||
{contact.first} {contact.last}
|
||||
{c.first} {c.last}
|
||||
</>
|
||||
) : (
|
||||
<i>No Name</i>
|
||||
)}
|
||||
<Favorite contact={contact}/>
|
||||
<Favorite contact={c}/>
|
||||
</h1>
|
||||
|
||||
{contact.twitter ? (
|
||||
{c.twitter ? (
|
||||
<p>
|
||||
<a
|
||||
href={`https://twitter.com/${contact.twitter}`}
|
||||
href={`https://twitter.com/${c.twitter}`}
|
||||
>
|
||||
{contact.twitter}
|
||||
{c.twitter}
|
||||
</a>
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{contact.notes ? <p>{contact.notes}</p> : null}
|
||||
{c.notes ? <p>{c.notes}</p> : null}
|
||||
|
||||
<div>
|
||||
<Form action="edit">
|
||||
<button type="submit">Edit</button>
|
||||
</Form>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => router.push(`/contacts/${c.id}/edit`)}
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
|
||||
<Form
|
||||
action="destroy"
|
||||
78
src/app/(sidebar)/contacts/[contactId]/edit/page.tsx
Normal file
78
src/app/(sidebar)/contacts/[contactId]/edit/page.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import Form from "next/form";
|
||||
import { fetchContact } from "@/app/(sidebar)/loaders";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { updateContact } from "@/app/data";
|
||||
|
||||
|
||||
async function editContact(contactId: string, formData: FormData) {
|
||||
"use server";
|
||||
const updates = Object.fromEntries(formData);
|
||||
await updateContact(contactId, updates);
|
||||
redirect(`/contacts/${contactId}`);
|
||||
}
|
||||
|
||||
export default async function EditContactPage({
|
||||
params
|
||||
}: {
|
||||
params: Promise<{ contactId: string }>
|
||||
}) {
|
||||
const { contactId } = await params;
|
||||
const contact = await fetchContact(contactId).then((c) => {
|
||||
if (c === null) notFound();
|
||||
return c;
|
||||
});
|
||||
const editThisContact = editContact.bind(null, contactId);
|
||||
|
||||
return (
|
||||
<Form key={contact.id} id="contact-form" action={editThisContact}>
|
||||
<p>
|
||||
<span>Name</span>
|
||||
<input
|
||||
aria-label="First name"
|
||||
defaultValue={contact.first}
|
||||
name="first"
|
||||
placeholder="First"
|
||||
type="text"
|
||||
/>
|
||||
<input
|
||||
aria-label="Last name"
|
||||
defaultValue={contact.last}
|
||||
name="last"
|
||||
placeholder="Last"
|
||||
type="text"
|
||||
/>
|
||||
</p>
|
||||
<label>
|
||||
<span>Twitter</span>
|
||||
<input
|
||||
defaultValue={contact.twitter}
|
||||
name="twitter"
|
||||
placeholder="@jack"
|
||||
type="text"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>Avatar URL</span>
|
||||
<input
|
||||
aria-label="Avatar URL"
|
||||
defaultValue={contact.avatar}
|
||||
name="avatar"
|
||||
placeholder="https://example.com/avatar.jpg"
|
||||
type="text"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>Notes</span>
|
||||
<textarea
|
||||
defaultValue={contact.notes}
|
||||
name="notes"
|
||||
rows={6}
|
||||
/>
|
||||
</label>
|
||||
<p>
|
||||
<button type="submit">Save</button>
|
||||
<button type="button">Cancel</button>
|
||||
</p>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
5
src/app/(sidebar)/contacts/[contactId]/loading.tsx
Normal file
5
src/app/(sidebar)/contacts/[contactId]/loading.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
export default function ContactLoading() {
|
||||
return (
|
||||
<div>Loading Contact...</div>
|
||||
);
|
||||
}
|
||||
8
src/app/(sidebar)/contacts/[contactId]/not-found.tsx
Normal file
8
src/app/(sidebar)/contacts/[contactId]/not-found.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<main id="error-page">
|
||||
<h1>404</h1>
|
||||
<p>The requested contact could not be found.</p>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
19
src/app/(sidebar)/contacts/[contactId]/page.tsx
Normal file
19
src/app/(sidebar)/contacts/[contactId]/page.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import Contact from "@/app/(sidebar)/contacts/[contactId]/Contact";
|
||||
import { fetchContact } from "@/app/(sidebar)/loaders";
|
||||
|
||||
export default async function ContactPage({
|
||||
params
|
||||
}: {
|
||||
params: Promise<{ contactId: string }>
|
||||
}) {
|
||||
const { contactId } = await params;
|
||||
const contact = fetchContact(contactId).then((c) => {
|
||||
if (c === null) notFound();
|
||||
return c;
|
||||
});
|
||||
|
||||
return (
|
||||
<Contact contact={contact}/>
|
||||
);
|
||||
}
|
||||
56
src/app/(sidebar)/layout.tsx
Normal file
56
src/app/(sidebar)/layout.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import React, { Suspense } from "react";
|
||||
import Link from "next/link";
|
||||
import Form from "next/form";
|
||||
import ContactList from "./sidebar-contacts";
|
||||
import { createEmptyContact } from "@/app/data";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { fetchContacts } from "@/app/(sidebar)/loaders";
|
||||
|
||||
async function newContact() {
|
||||
"use server";
|
||||
await createEmptyContact();
|
||||
revalidatePath('/');
|
||||
}
|
||||
|
||||
export default function SidebarRootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const contacts = fetchContacts();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div id="sidebar">
|
||||
<h1>
|
||||
<Link href="/about">React Router Contacts</Link>
|
||||
</h1>
|
||||
<div>
|
||||
<Form id="search-form" role="search" action="." formMethod="get">
|
||||
<input
|
||||
aria-label="Search contacts"
|
||||
id="q"
|
||||
name="q"
|
||||
placeholder="Search"
|
||||
type="search"/>
|
||||
<div
|
||||
aria-hidden
|
||||
hidden={true}
|
||||
id="search-spinner"/>
|
||||
</Form>
|
||||
<Form action={newContact}>
|
||||
<button type="submit">New</button>
|
||||
</Form>
|
||||
</div>
|
||||
<nav>
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<ContactList contacts={contacts}/>
|
||||
</Suspense>
|
||||
</nav>
|
||||
</div>
|
||||
<div id={'detail'}>
|
||||
{children}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
9
src/app/(sidebar)/loaders.ts
Normal file
9
src/app/(sidebar)/loaders.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { getContact, getContacts } from "@/app/data";
|
||||
|
||||
export const fetchContacts = async () => {
|
||||
return await getContacts();
|
||||
};
|
||||
|
||||
export const fetchContact = async (contactId: string) => {
|
||||
return await getContact(contactId);
|
||||
};
|
||||
15
src/app/(sidebar)/page.tsx
Normal file
15
src/app/(sidebar)/page.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from "react";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<p id="index-page">
|
||||
This is a demo for React Router.
|
||||
<br/>
|
||||
Check out{" "}
|
||||
<a href="https://reactrouter.com">
|
||||
the docs at reactrouter.com
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
27
src/app/(sidebar)/sidebar-contacts.tsx
Normal file
27
src/app/(sidebar)/sidebar-contacts.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { ContactRecord } from "@/app/data";
|
||||
import React, { use } from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function ContactList({ contacts }: { contacts: Promise<ContactRecord[]> }) {
|
||||
const allContacts = use(contacts);
|
||||
return (
|
||||
<ul>
|
||||
{allContacts.map((c) => (
|
||||
<li key={c.id}>
|
||||
<Link href={`/contacts/${c.id}`}>
|
||||
{c.first || c.last ? (
|
||||
<>
|
||||
{c.first} {c.last}
|
||||
</>
|
||||
) : (
|
||||
<i>No Name</i>
|
||||
)}
|
||||
{c.favorite ? (
|
||||
<span>★</span>
|
||||
) : null}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
45
src/app/about/page.tsx
Normal file
45
src/app/about/page.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import Link from "next/link";
|
||||
|
||||
export default function About() {
|
||||
return (
|
||||
<div id="about">
|
||||
<Link href="/">← Go to demo</Link>
|
||||
<h1>About React Router Contacts</h1>
|
||||
|
||||
<div>
|
||||
<p>
|
||||
This is a demo application showing off some of the
|
||||
powerful features of React Router, including
|
||||
dynamic routing, nested routes, loaders, actions,
|
||||
and more.
|
||||
</p>
|
||||
|
||||
<h2>Features</h2>
|
||||
<p>
|
||||
Explore the demo to see how React Router handles:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
Data loading and mutations with loaders and
|
||||
actions
|
||||
</li>
|
||||
<li>
|
||||
Nested routing with parent/child relationships
|
||||
</li>
|
||||
<li>URL-based routing with dynamic segments</li>
|
||||
<li>Pending and optimistic UI</li>
|
||||
</ul>
|
||||
|
||||
<h2>Learn More</h2>
|
||||
<p>
|
||||
Check out the official documentation at{" "}
|
||||
<a href="https://reactrouter.com">
|
||||
reactrouter.com
|
||||
</a>{" "}
|
||||
to learn more about building great web
|
||||
applications with React Router.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
import type { Metadata } from "next";
|
||||
import React from "react";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
import "./globals.css";
|
||||
import Form from "next/form";
|
||||
import Link from "next/link";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Next.js Address Book",
|
||||
@@ -18,42 +16,8 @@ export default function RootLayout({
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<div id="sidebar">
|
||||
<h1>React Router Contacts</h1>
|
||||
<div>
|
||||
<Form id="search-form" role="search" action="." formMethod="get">
|
||||
<input
|
||||
aria-label="Search contacts"
|
||||
id="q"
|
||||
name="q"
|
||||
placeholder="Search"
|
||||
type="search"
|
||||
/>
|
||||
<div
|
||||
aria-hidden
|
||||
hidden={true}
|
||||
id="search-spinner"
|
||||
/>
|
||||
</Form>
|
||||
<Form action="." formMethod="post">
|
||||
<button type="submit">New</button>
|
||||
</Form>
|
||||
</div>
|
||||
<nav>
|
||||
<ul>
|
||||
<li>
|
||||
<Link href={`/contacts/1`}>Your Name</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href={`/contacts/2`}>Your Friend</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
<div id={'detail'}>
|
||||
{children}
|
||||
</div>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import Form from "next/form";
|
||||
import React from "react";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div></div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user