1import { useState } from "react";
2
3import { Button } from "~/components/ui/button";
4
5import { describePreviewError } from "./errorCodeMessages";
6
7interface 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 */
23export 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. */
63export 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
122function 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
138function safeHost(url: string): string | null {
139 try {
140 return new URL(url).host;
141 } catch {
142 return null;
143 }
144}