| 1 | import { useState } from "react"; |
| 2 | |
| 3 | import { Button } from "~/components/ui/button"; |
| 4 | |
| 5 | import { describePreviewError } from "./errorCodeMessages"; |
| 6 | |
| 7 | interface Props { |
| 8 | url: string; |
| 9 | /** Chromium net error code, e.g. -105. */ |
| 10 | code: number; |
| 11 | /** Stringified Chromium error, e.g. "ERR_NAME_NOT_RESOLVED". */ |
| 12 | description: string; |
| 13 | onReload: () => void; |
| 14 | /** Reopens the page in a browser that may reach it, such as the environment's. */ |
| 15 | move?: { readonly label: string; readonly onMove: () => void }; |
| 16 | } |
| 17 | |
| 18 | /** |
| 19 | * A server tab's address was a file its browser cannot show (a PDF), so the |
| 20 | * browser downloaded it. Offers the address in the user's own browser, or the |
| 21 | * file the server already saved. |
| 22 | */ |
| 23 | export function PreviewFileNotShown({ |
| 24 | url, |
| 25 | fileName, |
| 26 | downloadUrl, |
| 27 | onOpen, |
| 28 | }: { |
| 29 | url: string; |
| 30 | fileName: string; |
| 31 | downloadUrl: string | null; |
| 32 | onOpen: () => void; |
| 33 | }) { |
| 34 | const host = safeHost(url) ?? url; |
| 35 | return ( |
| 36 | <div className="scrollbar-gutter-both relative flex h-full min-h-0 w-full overflow-y-auto bg-background"> |
| 37 | <div className="mx-auto flex w-full max-w-xl flex-1 flex-col px-8 py-12 sm:py-16"> |
| 38 | <ErrorIcon className="mb-6 size-12 text-muted-foreground/70" /> |
| 39 | <h1 className="mb-3 text-2xl font-semibold leading-tight text-foreground"> |
| 40 | This file can&rsquo;t be shown here |
| 41 | </h1> |
| 42 | <p className="text-sm leading-relaxed text-muted-foreground"> |
| 43 | <span className="font-semibold text-foreground">{fileName}</span> from {host} was |
| 44 | downloaded instead. |
| 45 | </p> |
| 46 | <div className="mt-auto flex items-center gap-2 pt-8"> |
| 47 | <div className="flex-1" /> |
| 48 | <Button type="button" variant="outline" size="sm" onClick={onOpen}> |
| 49 | Open in browser |
| 50 | </Button> |
| 51 | {downloadUrl ? ( |
| 52 | <Button type="button" size="sm" render={<a href={downloadUrl} download={fileName} />}> |
| 53 | Download |
| 54 | </Button> |
| 55 | ) : null} |
| 56 | </div> |
| 57 | </div> |
| 58 | </div> |
| 59 | ); |
| 60 | } |
| 61 | |
| 62 | /** Theme-aware tailwind port of Chromium's "This site can't be reached" page. */ |
| 63 | export function PreviewUnreachable({ url, code, description, onReload, move }: Props) { |
| 64 | const [showDetails, setShowDetails] = useState(false); |
| 65 | const host = safeHost(url) ?? url; |
| 66 | const friendly = describePreviewError(description); |
| 67 | const errorLabel = description.length > 0 ? description : `ERR_${Math.abs(code) || "FAILED"}`; |
| 68 | |
| 69 | return ( |
| 70 | <div className="scrollbar-gutter-both relative flex h-full min-h-0 w-full overflow-y-auto bg-background"> |
| 71 | <div className="mx-auto flex w-full max-w-xl flex-1 flex-col px-8 py-12 sm:py-16"> |
| 72 | <ErrorIcon className="mb-6 size-12 text-muted-foreground/70" /> |
| 73 | <h1 className="mb-3 text-2xl font-semibold leading-tight text-foreground"> |
| 74 | This site can&rsquo;t be reached |
| 75 | </h1> |
| 76 | <p className="text-sm leading-relaxed text-muted-foreground"> |
| 77 | <span className="font-semibold text-foreground">{host}</span>: {friendly}. |
| 78 | </p> |
| 79 | |
| 80 | {showDetails ? ( |
| 81 | <div className="mt-6 rounded-lg border border-border bg-muted/40 p-4 text-sm"> |
| 82 | <p className="mb-2 font-medium text-foreground">Try:</p> |
| 83 | <ul className="list-disc space-y-1 pl-5 text-muted-foreground"> |
| 84 | <li>Checking your connection</li> |
| 85 | <li>Confirming the dev server is running</li> |
| 86 | {move ? ( |
| 87 | <li>Opening it in the environment&rsquo;s browser, which reaches its network</li> |
| 88 | ) : null} |
| 89 | <li>Checking the proxy and the firewall</li> |
| 90 | </ul> |
| 91 | </div> |
| 92 | ) : null} |
| 93 | |
| 94 | <div className="mt-8 text-xs uppercase tracking-wide text-muted-foreground/70"> |
| 95 | {errorLabel} |
| 96 | </div> |
| 97 | |
| 98 | <div className="mt-auto flex items-center gap-2 pt-8"> |
| 99 | <Button |
| 100 | type="button" |
| 101 | variant="outline" |
| 102 | size="sm" |
| 103 | onClick={() => setShowDetails((value) => !value)} |
| 104 | > |
| 105 | {showDetails ? "Hide details" : "Details"} |
| 106 | </Button> |
| 107 | <div className="flex-1" /> |
| 108 | {move ? ( |
| 109 | <Button type="button" variant="outline" size="sm" onClick={move.onMove}> |
| 110 | {move.label} |
| 111 | </Button> |
| 112 | ) : null} |
| 113 | <Button type="button" size="sm" onClick={onReload}> |
| 114 | Reload |
| 115 | </Button> |
| 116 | </div> |
| 117 | </div> |
| 118 | </div> |
| 119 | ); |
| 120 | } |
| 121 | |
| 122 | function ErrorIcon({ className }: { className?: string }) { |
| 123 | return ( |
| 124 | <svg |
| 125 | viewBox="0 0 64 64" |
| 126 | fill="none" |
| 127 | stroke="currentColor" |
| 128 | strokeWidth="2.5" |
| 129 | className={className} |
| 130 | > |
| 131 | <path d="M16 12 L48 12 L48 52 L16 52 Z" /> |
| 132 | <path d="M22 22 L42 22 M22 30 L36 30 M22 38 L40 38" strokeLinecap="round" /> |
| 133 | <path d="M52 8 L12 56" strokeLinecap="round" /> |
| 134 | </svg> |
| 135 | ); |
| 136 | } |
| 137 | |
| 138 | function safeHost(url: string): string | null { |
| 139 | try { |
| 140 | return new URL(url).host; |
| 141 | } catch { |
| 142 | return null; |
| 143 | } |
| 144 | } |