Objective
What you're building
The unglamorous 30 minutes that decides whether this feels shippable: a real PDF a client could actually receive, and honest handling of every state that isn't "the happy path with fast wifi."
PDF export with react-pdf
import { Document, Page, Text, StyleSheet, PDFDownloadLink } from '@react-pdf/renderer';
const styles = StyleSheet.create({
page: { padding: 40, fontSize: 11 },
heading: { fontSize: 18, marginBottom: 12 },
});
function ProposalPDF({ clientName, content }: { clientName: string; content: string }) {
return (
<Document>
<Page style={styles.page}>
<Text style={styles.heading}>Proposal for {clientName}</Text>
<Text>{content}</Text>
</Page>
</Document>
);
}
// In the UI:
<PDFDownloadLink document={<ProposalPDF clientName={clientName} content={content} />} fileName="proposal.pdf">
Download PDF
</PDFDownloadLink>
react-pdf renders PDFs client-side using its own component model (Document, Page, Text) -- not HTML converted to PDF, which is why the styling API looks different from regular CSS. This matters for a proposal generator specifically: a client receiving a plain text email feels informal; the same content as a formatted PDF feels like something they'd actually forward to a decision-maker.
Every state, not just the happy path
"It works when I demo it" and "it handles what actually happens" are different claims
By now you've tested the happy path repeatedly -- valid input, fast network, successful API call. Today, deliberately break it: submit the form with an empty client name, disconnect your network mid-generation, view the proposals page with zero proposals ever generated. Every one of these should show a clear, specific state — not a blank screen, not a raw error stack trace, not an infinite spinner.
This is the exact "teach debugging, not just building" principle applied to UX
Just like Course 01's error-handling lesson taught that a failing tool should return a clear message instead of a raw traceback, a failing UI state should tell the user what happened and what to do next -- "Generation failed, please try again" beats a silent blank screen, and "No proposals yet — generate your first one" beats an empty, unexplained list.
You demo your product to a friend and it works perfectly. Before calling this "done," what's the single most valuable additional test?
A guided demo where you control the exact sequence of actions tests the happy path you already know works. A friend using it unguided, in whatever order occurs to them, is the closest approximation to a real stranger's first contact with your product -- and it's specifically designed to surface the edge cases a controlled demo never will.
The honest test
Walk through the full user journey yourself, start to finish, as if you were a stranger: sign up, hit the free limit, upgrade, generate, download the PDF. At every step, ask "would a real user pay for this specific experience?" — not "does the code technically work."
What you're building today
Real loading/error/empty states across the app, a working PDF export with actual formatting, and a completed honest walkthrough — with a written note on anything that felt rough, fixed before moving to Lesson 6's launch.