mirror of
https://github.com/jakejarvis/hoot.git
synced 2025-10-18 22:34:25 -04:00
Refactor components for improved layout and accessibility: add vertical separator in AppHeader, adjust button sizes in GithubStars and ThemeToggle, and implement Tooltip in DomainReportView for enhanced user guidance.
This commit is contained in:
@@ -3,6 +3,7 @@ import { HeaderSearch } from "@/components/domain/header-search";
|
||||
import { GithubStars } from "@/components/github-stars";
|
||||
import { Logo } from "@/components/logo";
|
||||
import { ThemeToggle } from "@/components/theme-toggle";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
|
||||
export function AppHeader() {
|
||||
return (
|
||||
@@ -15,9 +16,10 @@ export function AppHeader() {
|
||||
<Logo className="h-10 w-10" aria-hidden="true" />
|
||||
</Link>
|
||||
<HeaderSearch />
|
||||
<div className="flex items-center gap-1.5 justify-self-end">
|
||||
<div className="flex h-full items-center gap-1.5 justify-self-end">
|
||||
{/* Server-fetched star count with link */}
|
||||
<GithubStars />
|
||||
<Separator orientation="vertical" className="!h-4" />
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</header>
|
||||
|
@@ -13,6 +13,11 @@ import { HeadersSection } from "@/components/domain/sections/headers-section";
|
||||
import { HostingEmailSection } from "@/components/domain/sections/hosting-email-section";
|
||||
import { RegistrationSection } from "@/components/domain/sections/registration-section";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { useDomainHistory } from "@/hooks/use-domain-history";
|
||||
import { useDomainQueries } from "@/hooks/use-domain-queries";
|
||||
import { captureClient } from "@/lib/analytics/client";
|
||||
@@ -69,36 +74,42 @@ export function DomainReportView({ domain }: { domain: string }) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<ScreenshotTooltip domain={domain}>
|
||||
<Link
|
||||
href={`https://${domain}`}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
className="flex items-center gap-2"
|
||||
onClick={() =>
|
||||
captureClient("external_domain_link_clicked", { domain })
|
||||
}
|
||||
>
|
||||
<Favicon domain={domain} size={20} className="rounded" />
|
||||
<h2 className="font-semibold text-xl tracking-tight">{domain}</h2>
|
||||
<ExternalLink
|
||||
className="size-3.5 text-muted-foreground/60"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Link>
|
||||
</ScreenshotTooltip>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleExportJson}
|
||||
disabled={areSecondarySectionsLoading}
|
||||
<ScreenshotTooltip domain={domain}>
|
||||
<Link
|
||||
href={`https://${domain}`}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
className="flex items-center gap-2"
|
||||
onClick={() =>
|
||||
captureClient("external_domain_link_clicked", { domain })
|
||||
}
|
||||
>
|
||||
<Download />
|
||||
<span className="hidden sm:inline-block">Export</span>
|
||||
</Button>
|
||||
</div>
|
||||
<Favicon domain={domain} size={20} className="rounded" />
|
||||
<h2 className="font-semibold text-xl tracking-tight">{domain}</h2>
|
||||
<ExternalLink
|
||||
className="size-3.5 text-muted-foreground/60"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Link>
|
||||
</ScreenshotTooltip>
|
||||
<Tooltip delayDuration={500}>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleExportJson}
|
||||
disabled={areSecondarySectionsLoading}
|
||||
>
|
||||
<Download />
|
||||
<span className="hidden sm:inline-block">Export</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="left">
|
||||
<p>
|
||||
Save this report as a <span className="font-mono">JSON</span>{" "}
|
||||
file.
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
|
@@ -6,7 +6,7 @@ import { ErrorWithRetry } from "./error-with-retry";
|
||||
describe("ErrorWithRetry", () => {
|
||||
it("calls onRetry when clicking Retry", async () => {
|
||||
const onRetry = vi.fn();
|
||||
render(<ErrorWithRetry message="Failed" onRetry={onRetry} />);
|
||||
render(<ErrorWithRetry message="Failed" onRetryAction={onRetry} />);
|
||||
await userEvent.click(screen.getByRole("button", { name: /retry/i }));
|
||||
expect(onRetry).toHaveBeenCalled();
|
||||
});
|
||||
|
@@ -4,15 +4,15 @@ import { Button } from "@/components/ui/button";
|
||||
|
||||
export function ErrorWithRetry({
|
||||
message,
|
||||
onRetry,
|
||||
onRetryAction,
|
||||
}: {
|
||||
message: string;
|
||||
onRetry: () => void;
|
||||
onRetryAction: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 text-destructive text-sm">
|
||||
{message}
|
||||
<Button variant="outline" size="sm" onClick={onRetry}>
|
||||
<Button variant="outline" size="sm" onClick={onRetryAction}>
|
||||
Retry
|
||||
</Button>
|
||||
</div>
|
||||
|
@@ -271,7 +271,7 @@ export function CertificatesSection({
|
||||
) : isError ? (
|
||||
<ErrorWithRetry
|
||||
message="Failed to load certificates."
|
||||
onRetry={onRetryAction}
|
||||
onRetryAction={onRetryAction}
|
||||
/>
|
||||
) : null}
|
||||
</Section>
|
||||
|
@@ -95,7 +95,10 @@ export function DnsRecordsSection({
|
||||
</DnsGroup>
|
||||
</div>
|
||||
) : isError ? (
|
||||
<ErrorWithRetry message="Failed to load DNS." onRetry={onRetryAction} />
|
||||
<ErrorWithRetry
|
||||
message="Failed to load DNS."
|
||||
onRetryAction={onRetryAction}
|
||||
/>
|
||||
) : null}
|
||||
</Section>
|
||||
);
|
||||
|
@@ -54,7 +54,7 @@ export function HeadersSection({
|
||||
) : isError ? (
|
||||
<ErrorWithRetry
|
||||
message="Failed to load headers."
|
||||
onRetry={onRetryAction}
|
||||
onRetryAction={onRetryAction}
|
||||
/>
|
||||
) : null}
|
||||
</Section>
|
||||
|
@@ -110,7 +110,7 @@ export function HostingEmailSection({
|
||||
) : isError ? (
|
||||
<ErrorWithRetry
|
||||
message="Failed to load hosting details."
|
||||
onRetry={onRetryAction}
|
||||
onRetryAction={onRetryAction}
|
||||
/>
|
||||
) : null}
|
||||
</Section>
|
||||
|
@@ -109,7 +109,7 @@ export function RegistrationSection({
|
||||
) : isError ? (
|
||||
<ErrorWithRetry
|
||||
message="Failed to load WHOIS."
|
||||
onRetry={onRetryAction}
|
||||
onRetryAction={onRetryAction}
|
||||
/>
|
||||
) : null}
|
||||
</Section>
|
||||
|
@@ -4,8 +4,8 @@ import { Button } from "@/components/ui/button";
|
||||
async function fetchRepoStars(): Promise<number | null> {
|
||||
try {
|
||||
const res = await fetch("https://api.github.com/repos/jakejarvis/hoot", {
|
||||
// Revalidate at most every 30 minutes to avoid rate limits
|
||||
next: { revalidate: 1800 },
|
||||
// Revalidate at most every 30 minutes to avoid rate limits (one day without access token)
|
||||
next: { revalidate: process.env.GITHUB_TOKEN ? 1800 : 86400 },
|
||||
headers: {
|
||||
Accept: "application/vnd.github+json",
|
||||
...(process.env.GITHUB_TOKEN
|
||||
@@ -28,26 +28,26 @@ export async function GithubStars() {
|
||||
const label = stars === null ? "0" : `${stars}`;
|
||||
|
||||
return (
|
||||
<Button variant="ghost" asChild>
|
||||
<Button variant="ghost" size="sm" asChild>
|
||||
<Link
|
||||
href="https://github.com/jakejarvis/hoot"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
className="group inline-flex select-none items-center gap-2 transition-colors"
|
||||
className="group flex select-none items-center gap-2 transition-colors"
|
||||
aria-label="Open GitHub repository"
|
||||
>
|
||||
<svg
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="shrink-0 group-hover:text-foreground"
|
||||
className="flex shrink-0 group-hover:text-foreground"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
|
||||
/>
|
||||
</svg>
|
||||
<span className="relative top-px inline-flex items-center text-[13px] text-muted-foreground leading-none group-hover:text-foreground">
|
||||
<span className="relative top-px flex items-center text-[13px] text-muted-foreground leading-none group-hover:text-foreground">
|
||||
{label}
|
||||
</span>
|
||||
</Link>
|
||||
|
@@ -17,7 +17,7 @@ export function ThemeToggle() {
|
||||
<Button
|
||||
aria-label="Toggle theme"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
size="sm"
|
||||
onClick={toggleTheme}
|
||||
>
|
||||
<Sun className="dark:-rotate-90 rotate-0 scale-100 transition-all dark:scale-0" />
|
||||
|
@@ -56,7 +56,7 @@ function TooltipContent({
|
||||
>
|
||||
{children}
|
||||
{hideArrow ? null : (
|
||||
<TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] bg-primary fill-primary" />
|
||||
<TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[1px] bg-primary fill-primary" />
|
||||
)}
|
||||
</TooltipPrimitive.Content>
|
||||
</TooltipPrimitive.Portal>
|
||||
|
Reference in New Issue
Block a user