|
1 | 1 | "use client"; |
2 | | -import { Button } from "@/components/ui/button"; |
3 | | -import { Textarea } from "@/components/ui/textarea"; |
4 | | - |
5 | 2 | import { useEffect, useState } from "react"; |
6 | | -import { Sparkles } from "lucide-react"; |
| 3 | +import Link from "next/link"; |
| 4 | +import { Printer, Sparkles } from "lucide-react"; |
7 | 5 |
|
8 | | -import "@/app/globals.css"; |
| 6 | +import { Button } from "@/components/ui/button"; |
| 7 | +import { Textarea } from "@/components/ui/textarea"; |
9 | 8 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; |
10 | | -import Link from "next/link"; |
11 | 9 | import { Label } from "@/components/ui/label"; |
| 10 | + |
12 | 11 | import { fetchResources } from "@/util/fetchResources"; |
13 | 12 | import { Resource } from "@/types/resources"; |
| 13 | +import "@/app/globals.css"; |
| 14 | + |
| 15 | +import { PrintableReferralsReport } from "@/util/printReferrals"; |
14 | 16 |
|
15 | 17 | export default function Page() { |
16 | 18 | const [clientDescription, setClientDescription] = useState(""); |
17 | 19 | const [result, setResult] = useState<Resource[] | null>(null); |
18 | 20 | const [loading, setLoading] = useState(false); |
| 21 | + const [readyToPrint, setReadyToPrint] = useState(false); |
19 | 22 |
|
20 | 23 | useEffect(() => { |
21 | 24 | if (result === null) return; |
| 25 | + // optional side effects on result change |
22 | 26 | }, [result]); |
23 | 27 |
|
24 | 28 | async function handleClick() { |
25 | 29 | setLoading(true); |
26 | 30 | setResult(null); |
27 | | - |
28 | 31 | try { |
29 | | - const resources = await fetchResources(clientDescription); |
| 32 | + const resources = await fetchResources(clientDescription); // returns Resource[] |
30 | 33 | setResult(resources); |
| 34 | + setReadyToPrint(true); |
31 | 35 | } catch (e: unknown) { |
32 | 36 | const message = e instanceof Error ? e.message : "Unknown error"; |
33 | 37 | console.error(message); |
34 | | - setResult([]); // or keep `null` if you prefer to hide the results area |
| 38 | + setResult([]); // or keep null to hide area |
35 | 39 | } finally { |
36 | 40 | setLoading(false); |
37 | 41 | } |
38 | 42 | } |
39 | 43 |
|
| 44 | + function handlePrint() { |
| 45 | + // App chrome will hide; print-only section will show |
| 46 | + window.print(); |
| 47 | + } |
| 48 | + |
40 | 49 | return ( |
41 | 50 | <> |
42 | | - <div className="flex flex-col gap-2 pl-20 pr-20 mt-8 mb-8"> |
43 | | - <div className="m-3"> |
44 | | - <Label |
45 | | - className="font-medium text-gray-900 text-lg mb-1" |
46 | | - htmlFor="clientDescriptionInput" |
47 | | - > |
48 | | - Tell us about your client |
49 | | - </Label> |
50 | | - <Textarea |
51 | | - placeholder="Add details about the client's specific situation, needs, and circumstances here..." |
52 | | - id="clientDescriptionInput" |
53 | | - value={clientDescription} |
54 | | - onChange={(e) => setClientDescription(e.target.value)} |
55 | | - className="min-h-[8rem] min-w-[16rem] text-base border-gray-300 focus:ring-blue-500 focus:border-blue-500" |
56 | | - /> |
57 | | - </div> |
58 | | - <Button |
59 | | - type="button" |
60 | | - onClick={() => void handleClick()} |
61 | | - disabled={!clientDescription.trim() || loading} |
62 | | - className="min-w-[16rem] ml-3 mr-3 generate-referrals-button text-lg pt-6 pb-6" |
63 | | - > |
64 | | - <Sparkles className="w-5 h-5" /> |
65 | | - {loading ? "Generating Resources..." : "Find Resources"} |
66 | | - </Button> |
67 | | - {displayResourcesFromResult(result)} |
68 | | - </div> |
69 | | - </> |
70 | | - ); |
71 | | -} |
| 51 | + {/* ----- App chrome (hidden when printing) ----- */} |
| 52 | + <div className="print:hidden"> |
| 53 | + {readyToPrint && ( |
| 54 | + <div className="space-y-6"> |
| 55 | + <div className="flex items-center gap-2 justify-end pr-20 pt-4"> |
| 56 | + <Button |
| 57 | + onClick={handlePrint} |
| 58 | + variant="outline" |
| 59 | + className="hover:bg-gray-100 hover:text-gray-900" |
| 60 | + > |
| 61 | + <Printer |
| 62 | + data-testid="printReferralsButton" |
| 63 | + className="w-4 h-4 mr-2" |
| 64 | + /> |
| 65 | + Print Referrals |
| 66 | + </Button> |
| 67 | + </div> |
| 68 | + </div> |
| 69 | + )} |
72 | 70 |
|
73 | | -function displayResourcesFromResult(result: Resource[] | null) { |
74 | | - if (result == null) return null; |
75 | | - if (result.length === 0) |
76 | | - return <div className="margin-top-2">No resources found.</div>; |
| 71 | + <div className="flex flex-col gap-2 pl-20 pr-20 mt-8 mb-8"> |
| 72 | + <div className="m-3"> |
| 73 | + <Label |
| 74 | + className="font-medium text-gray-900 text-lg mb-1" |
| 75 | + htmlFor="clientDescriptionInput" |
| 76 | + > |
| 77 | + Tell us about your client |
| 78 | + </Label> |
| 79 | + <Textarea |
| 80 | + placeholder="Add details about the client's specific situation, needs, and circumstances here..." |
| 81 | + id="clientDescriptionInput" |
| 82 | + value={clientDescription} |
| 83 | + onChange={(e) => setClientDescription(e.target.value)} |
| 84 | + className="min-h-[8rem] min-w-[16rem] text-base" |
| 85 | + data-testid="clientDescriptionInput" |
| 86 | + /> |
| 87 | + </div> |
77 | 88 |
|
78 | | - return ( |
79 | | - <div className="m-3"> |
80 | | - {result.map((r, i) => ( |
81 | | - <Card key={i} className="bg-white shadow-sm mb-5 min-w-[16rem]"> |
82 | | - <CardHeader> |
83 | | - <CardTitle className="text-xl font-semibold text-gray-900 flex items-center gap-2"> |
84 | | - <span className="flex-shrink-0 w-7 h-7 bg-blue-600 text-white rounded-full flex items-center justify-center text-m font-medium"> |
85 | | - {i + 1} |
86 | | - </span> |
87 | | - {r.name} |
88 | | - <Link |
89 | | - href={r.website} |
90 | | - rel="noopener noreferrer" |
91 | | - className="text-base text-gray-500 flex items-center gap-2" |
92 | | - > |
93 | | - {r.website} |
94 | | - </Link> |
95 | | - </CardTitle> |
96 | | - </CardHeader> |
97 | | - <CardContent className="ml-4 mr-4"> |
98 | | - {r.description && ( |
99 | | - <div className="text-bold mb-2">{r.description}</div> |
100 | | - )} |
| 89 | + <Button |
| 90 | + type="button" |
| 91 | + onClick={() => void handleClick()} |
| 92 | + disabled={!clientDescription.trim() || loading} |
| 93 | + className="min-w-[16rem] ml-3 mr-3 generate-referrals-button text-lg pt-6 pb-6" |
| 94 | + data-testid="findResourcesButton" |
| 95 | + > |
| 96 | + <Sparkles className="w-5 h-5" /> |
| 97 | + {loading ? "Generating Resources..." : "Find Resources"} |
| 98 | + </Button> |
101 | 99 |
|
102 | | - {r.addresses?.length > 0 && ( |
103 | | - <div className="mt-1"> |
104 | | - <span className="font-semibold"> |
105 | | - Address{r.addresses.length > 1 ? "es" : ""}: |
106 | | - </span>{" "} |
107 | | - {r.addresses.join(" | ")} |
108 | | - </div> |
109 | | - )} |
| 100 | + {/* On-screen results (normal cards) */} |
| 101 | + {result && result.length === 0 && ( |
| 102 | + <div className="m-3">No resources found.</div> |
| 103 | + )} |
| 104 | + {result && result.length > 0 && ( |
| 105 | + <div className="m-3"> |
| 106 | + {result.map((r, i) => ( |
| 107 | + <Card key={i} className="bg-white shadow-sm mb-5 min-w-[16rem]"> |
| 108 | + <CardHeader> |
| 109 | + <CardTitle className="text-xl font-semibold text-gray-900 flex items-center gap-2"> |
| 110 | + <span className="flex-shrink-0 w-7 h-7 bg-blue-600 text-white rounded-full flex items-center justify-center text-m font-medium"> |
| 111 | + {i + 1} |
| 112 | + </span> |
| 113 | + {r.name} |
| 114 | + {!!r.website && ( |
| 115 | + <Link |
| 116 | + href={r.website} |
| 117 | + rel="noopener noreferrer" |
| 118 | + target="_blank" |
| 119 | + className="text-base text-gray-500 flex items-center gap-2" |
| 120 | + > |
| 121 | + {r.website} |
| 122 | + </Link> |
| 123 | + )} |
| 124 | + </CardTitle> |
| 125 | + </CardHeader> |
| 126 | + <CardContent className="ml-4 mr-4"> |
| 127 | + {r.description && ( |
| 128 | + <div className="font-medium mb-2">{r.description}</div> |
| 129 | + )} |
| 130 | + {r.addresses?.length > 0 && ( |
| 131 | + <div className="mt-1"> |
| 132 | + <span className="font-semibold"> |
| 133 | + Address{r.addresses.length > 1 ? "es" : ""}: |
| 134 | + </span>{" "} |
| 135 | + {r.addresses.join(" | ")} |
| 136 | + </div> |
| 137 | + )} |
| 138 | + {r.phones?.length > 0 && ( |
| 139 | + <div className="mt-1"> |
| 140 | + <span className="font-semibold">Phone:</span>{" "} |
| 141 | + {r.phones.join(" | ")} |
| 142 | + </div> |
| 143 | + )} |
| 144 | + {r.emails?.length > 0 && ( |
| 145 | + <div className="mt-1"> |
| 146 | + <span className="font-semibold">Email:</span>{" "} |
| 147 | + {r.emails.join(" | ")} |
| 148 | + </div> |
| 149 | + )} |
| 150 | + </CardContent> |
| 151 | + </Card> |
| 152 | + ))} |
| 153 | + </div> |
| 154 | + )} |
| 155 | + </div> |
| 156 | + </div> |
110 | 157 |
|
111 | | - {r.phones?.length > 0 && ( |
112 | | - <div className="mt-1"> |
113 | | - <span className="font-semibold">Phone:</span>{" "} |
114 | | - {r.phones.join(" | ")} |
115 | | - </div> |
116 | | - )} |
117 | | - {r.emails?.length > 0 && ( |
118 | | - <div className="mt-1"> |
119 | | - <span className="font-semibold">Email:</span>{" "} |
120 | | - {r.emails.join(" | ")} |
121 | | - </div> |
122 | | - )} |
123 | | - </CardContent> |
124 | | - </Card> |
125 | | - ))} |
126 | | - </div> |
| 158 | + {/* ----- Print-only section ----- */} |
| 159 | + <div className="hidden print:block"> |
| 160 | + <PrintableReferralsReport resources={result ?? []} /> |
| 161 | + </div> |
| 162 | + </> |
127 | 163 | ); |
128 | 164 | } |
0 commit comments