pr_01m47d24b0e6n91zwymwxg0vpx/apps/web/app/components/production-shot.tsx
Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 1 | import { Globe } from "lucide-react"; |
| 2 | import { useEffect, useRef, useState } from "react"; | |
| 3 | ||
| 4 | import { cn } from "../lib/cn"; | |
| 5 | ||
| 6 | /** | |
| 7 | * A thumbnail of production as it is now, linking to it. The screenshot is | |
| 8 | * taken once per deploy; until it has loaded, or when there is none yet, a | |
| 9 | * quiet frame with the address stands in. | |
| 10 | */ | |
| 11 | export function ProductionShot({ | |
| 12 | src, | |
| 13 | href, | |
| 14 | label, | |
| 15 | className, | |
| 16 | }: { | |
| 17 | /** Null when nothing is deployed yet. */ | |
| 18 | src: string | null; | |
| 19 | href: string | null; | |
| 20 | label: string; | |
| 21 | className?: string; | |
| 22 | }) { | |
| 23 | const image = useRef<HTMLImageElement>(null); | |
| 24 | const [state, setState] = useState<"loading" | "loaded" | "failed">("loading"); | |
| 25 | ||
| 26 | // An image that settled before hydration fired its events unheard. | |
| 27 | useEffect(() => { | |
| 28 | const img = image.current; | |
| 29 | if (!img?.complete) return; | |
| 30 | setState(img.naturalWidth > 0 ? "loaded" : "failed"); | |
| 31 | }, [src]); | |
| 32 | ||
| 33 | const frame = ( | |
| 34 | <span | |
| 35 | className={cn( | |
| 36 | "relative block aspect-[16/10] w-full overflow-hidden rounded-lg border border-line bg-bg", | |
| 37 | href && "transition-colors group-hover:border-line-strong", | |
| 38 | )} | |
| 39 | > | |
| 40 | {state !== "loaded" && <Placeholder label={label} waiting={src != null && state === "loading"} />} | |
| 41 | {src && state !== "failed" && ( | |
| 42 | <img | |
| 43 | ref={image} | |
| 44 | src={src} | |
| 45 | alt={`Screenshot of ${label}`} | |
| 46 | width={1280} | |
| 47 | height={800} | |
| 48 | decoding="async" | |
| 49 | onLoad={() => setState("loaded")} | |
| 50 | onError={() => setState("failed")} | |
| 51 | className={cn( | |
| 52 | "absolute inset-0 size-full object-cover object-top transition-opacity duration-300", | |
| 53 | state === "loaded" ? "opacity-100" : "opacity-0", | |
| 54 | )} | |
| 55 | /> | |
| 56 | )} | |
| 57 | </span> | |
| 58 | ); | |
| 59 | ||
| 60 | return href ? ( | |
| 61 | <a href={href} className={cn("group block", className)} aria-label={`Visit ${label}`}> | |
| 62 | {frame} | |
| 63 | </a> | |
| 64 | ) : ( | |
| 65 | <div className={className}>{frame}</div> | |
| 66 | ); | |
| 67 | } | |
| 68 | ||
| 69 | function Placeholder({ label, waiting }: { label: string; waiting: boolean }) { | |
| 70 | return ( | |
| 71 | <span className="absolute inset-0 flex flex-col"> | |
| 72 | <span className="flex items-center gap-1.5 border-b border-line bg-surface px-2.5 py-1.5"> | |
| 73 | <span className="size-1.5 rounded-full bg-line-strong" /> | |
| 74 | <span className="size-1.5 rounded-full bg-line-strong" /> | |
| 75 | <span className="size-1.5 rounded-full bg-line-strong" /> | |
| 76 | <span className="ml-1.5 truncate rounded bg-bg px-2 py-px font-mono text-[0.625rem] text-faint">{label}</span> | |
| 77 | </span> | |
| 78 | <span className="relative flex grow flex-col gap-2 p-3.5"> | |
| 79 | <span className="h-2 w-2/5 rounded-full bg-line/80" /> | |
| 80 | <span className="h-1.5 w-4/5 rounded-full bg-line/50" /> | |
| 81 | <span className="h-1.5 w-3/5 rounded-full bg-line/50" /> | |
| 82 | <span className="mt-auto flex items-center gap-1.5 text-[0.6875rem] text-faint"> | |
| 83 | <Globe size={12} className={waiting ? "animate-pulse" : undefined} /> | |
| 84 | {waiting ? "Taking a screenshot…" : "A screenshot appears after the next deploy"} | |
| 85 | </span> | |
| 86 | </span> | |
| 87 | </span> | |
| 88 | ); | |
| 89 | } |