Compare commits

...

10 Commits

Author SHA1 Message Date
b73c9311f5 Complete "Updating Contacts with FormData" 2025-04-01 17:58:39 +02:00
434b7c8dc8 Remove caching. That caching is breaking the program in case of change.
Note: "use server" is not good because force the fetch to be action/static so the client doesn't call them again explicitly during render (I think?)
2025-04-01 17:58:11 +02:00
800c231580 Loading is not easily possible here. I will see later. 2025-04-01 17:21:15 +02:00
aa81948f29 Edit Contact but without loading? 2025-04-01 17:14:43 +02:00
a4536d20b4 Complete "Creating Contacts". 2025-04-01 16:40:35 +02:00
75337be90c Complete "URL Params in Loaders" and "Throwing Responses". 2025-04-01 14:10:20 +02:00
298d25a579 Complete "Layout Routes". 2025-04-01 12:17:08 +02:00
276bea81b0 Remove unused "Hydrate Fallback".
Note: As already said, this loading appear even when the app/layout.tsx is loading. This is not the behaviour I want. In fact the behaviour I wanted is impossible to be needed with Next.js and his "SSR first" approach.
2025-04-01 11:42:49 +02:00
595520296c Complete "Hydrate Fallback".
Note: I checked in React DevTool that the Suspend and the fallback is present, but the skeleton of the page is already rendered into the server so that Suspend is impossible to trigger!
You can see it as it is during long load, o I will probably remove it in the next commit.
2025-04-01 11:41:33 +02:00
4330157491 Complete "Loading Data" 2025-04-01 11:32:54 +02:00
12 changed files with 291 additions and 70 deletions

View File

@@ -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"

View 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>
);
}

View File

@@ -0,0 +1,5 @@
export default function ContactLoading() {
return (
<div>Loading Contact...</div>
);
}

View 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>
);
}

View 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}/>
);
}

View 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>
</>
);
}

View 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);
};

View 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>
);
}

View 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
View 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>
);
}

View File

@@ -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>
);
}
}

View File

@@ -1,8 +0,0 @@
import Form from "next/form";
import React from "react";
export default function Home() {
return (
<div></div>
);
}