import { Form, Link, useFetcher, useLoaderData } from 'react-router-dom'
import { queryOptions, useSuspenseQuery } from '@tanstack/react-query'
import { getContact, updateContact } from '../contacts'
import type { ActionFunctionArgs, LoaderFunctionArgs } from 'react-router-dom'
import type { Contact } from '../contacts'
import type { QueryClient } from '@tanstack/react-query'
export const contactDetailQuery = (id: string) =>
queryOptions({
queryKey: ['contacts', 'detail', id],
queryFn: async () => {
const contact = await getContact(id)
if (!contact) {
throw new Response('', {
status: 404,
statusText: 'Not Found',
})
}
return contact
},
})
export const loader =
(queryClient: QueryClient) =>
async ({ params }: LoaderFunctionArgs) => {
if (!params.contactId) {
throw new Error('No contact ID provided')
}
await queryClient.ensureQueryData(contactDetailQuery(params.contactId))
return { contactId: params.contactId }
}
export const action =
(queryClient: QueryClient) =>
async ({ request, params }: ActionFunctionArgs) => {
const formData = await request.formData()
if (!params.contactId) {
throw new Error('No contact ID provided')
}
await updateContact(params.contactId, {
favorite: formData.get('favorite') === 'true',
})
await queryClient.invalidateQueries({ queryKey: ['contacts'] })
return null
}
export default function Contact() {
const { contactId } = useLoaderData() as Awaited<
ReturnType<ReturnType<typeof loader>>
>
const { data: contact } = useSuspenseQuery(contactDetailQuery(contactId))
return (
<div id="contact">
<div>
<img key={contact.avatar} src={contact.avatar || undefined} />
</div>
<div>
<h1>
{contact.first || contact.last ? (
<>
{contact.first} {contact.last}
</>
) : (
<i>No Name</i>
)}{' '}
<Favorite contact={contact} />
</h1>
{contact.twitter && (
<p>
<a target="_blank" href={`https://twitter.com/${contact.twitter}`}>
{contact.twitter}
</a>
</p>
)}
{contact.notes && <p>{contact.notes}</p>}
<div>
<Link to="edit" className="button">
Edit
</Link>
<Form
method="post"
action="destroy"
onSubmit={(event) => {
if (!confirm('Please confirm you want to delete this record.')) {
event.preventDefault()
}
}}
>
<button type="submit">Delete</button>
</Form>
</div>
</div>
</div>
)
}
function Favorite({ contact }: { contact: Contact }) {
const fetcher = useFetcher({ key: `contact:${contact.id}` })
let favorite = contact.favorite
if (fetcher.formData) {
favorite = fetcher.formData.get('favorite') === 'true'
}
return (
<fetcher.Form method="post">
<button
name="favorite"
value={favorite ? 'false' : 'true'}
title={favorite ? 'Remove from favorites' : 'Add to favorites'}
>
{favorite ? '★' : '☆'}
</button>
</fetcher.Form>
)
}
import { Form, Link, useFetcher, useLoaderData } from 'react-router-dom'
import { queryOptions, useSuspenseQuery } from '@tanstack/react-query'
import { getContact, updateContact } from '../contacts'
import type { ActionFunctionArgs, LoaderFunctionArgs } from 'react-router-dom'
import type { Contact } from '../contacts'
import type { QueryClient } from '@tanstack/react-query'
export const contactDetailQuery = (id: string) =>
queryOptions({
queryKey: ['contacts', 'detail', id],
queryFn: async () => {
const contact = await getContact(id)
if (!contact) {
throw new Response('', {
status: 404,
statusText: 'Not Found',
})
}
return contact
},
})
export const loader =
(queryClient: QueryClient) =>
async ({ params }: LoaderFunctionArgs) => {
if (!params.contactId) {
throw new Error('No contact ID provided')
}
await queryClient.ensureQueryData(contactDetailQuery(params.contactId))
return { contactId: params.contactId }
}
export const action =
(queryClient: QueryClient) =>
async ({ request, params }: ActionFunctionArgs) => {
const formData = await request.formData()
if (!params.contactId) {
throw new Error('No contact ID provided')
}
await updateContact(params.contactId, {
favorite: formData.get('favorite') === 'true',
})
await queryClient.invalidateQueries({ queryKey: ['contacts'] })
return null
}
export default function Contact() {
const { contactId } = useLoaderData() as Awaited<
ReturnType<ReturnType<typeof loader>>
>
const { data: contact } = useSuspenseQuery(contactDetailQuery(contactId))
return (
<div id="contact">
<div>
<img key={contact.avatar} src={contact.avatar || undefined} />
</div>
<div>
<h1>
{contact.first || contact.last ? (
<>
{contact.first} {contact.last}
</>
) : (
<i>No Name</i>
)}{' '}
<Favorite contact={contact} />
</h1>
{contact.twitter && (
<p>
<a target="_blank" href={`https://twitter.com/${contact.twitter}`}>
{contact.twitter}
</a>
</p>
)}
{contact.notes && <p>{contact.notes}</p>}
<div>
<Link to="edit" className="button">
Edit
</Link>
<Form
method="post"
action="destroy"
onSubmit={(event) => {
if (!confirm('Please confirm you want to delete this record.')) {
event.preventDefault()
}
}}
>
<button type="submit">Delete</button>
</Form>
</div>
</div>
</div>
)
}
function Favorite({ contact }: { contact: Contact }) {
const fetcher = useFetcher({ key: `contact:${contact.id}` })
let favorite = contact.favorite
if (fetcher.formData) {
favorite = fetcher.formData.get('favorite') === 'true'
}
return (
<fetcher.Form method="post">
<button
name="favorite"
value={favorite ? 'false' : 'true'}
title={favorite ? 'Remove from favorites' : 'Add to favorites'}
>
{favorite ? '★' : '☆'}
</button>
</fetcher.Form>
)
}