Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions frontend/components/layout/AppShell.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
"use client"
import dynamic from "next/dynamic"
import { ChevronLeft } from "lucide-react"
import { Button } from "@/components/ui/button"
import { useRequestStore } from "@/store/requestStore"
import { useConnectionStore } from "@/store/connectionStore"
import TopBar from "./TopBar"
import RequestList from "@/components/requests/RequestList"
import RequestDetail from "@/components/requests/RequestDetail"
import SignalPulseEmptyState from "@/components/requests/SignalPulseEmptyState"

const RequestDetail = dynamic(() => import("@/components/requests/RequestDetail"), {
ssr: false,
loading: () => (
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">
Loading request...
</div>
),
})

export default function AppShell() {
const { requests, selectedId, selectRequest } = useRequestStore()
const { token } = useConnectionStore()
Expand All @@ -17,7 +26,7 @@ export default function AppShell() {
<div className="flex flex-col h-screen bg-background text-foreground">
<TopBar />

<div className="flex flex-1 overflow-hidden">
<main className="flex flex-1 overflow-hidden">
<div
className={`${selected ? "hidden md:flex" : "flex"} flex-col w-full md:w-80 border-r flex-shrink-0`}
>
Expand Down Expand Up @@ -52,7 +61,7 @@ export default function AppShell() {
)
)}
</div>
</div>
</main>
</div>
)
}
10 changes: 7 additions & 3 deletions frontend/components/layout/TopBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,13 @@ import { useTheme } from "@/app/providers"
import type { Theme } from "@/types"

const STATUS_CONFIG = {
connected: { label: "Connected", dot: "●", color: "text-green-500" },
reconnecting: { label: "Reconnecting", dot: "◌", color: "text-yellow-500 animate-pulse" },
disconnected: { label: "Disconnected", dot: "○", color: "text-red-500" },
connected: { label: "Connected", dot: "●", color: "text-green-700 dark:text-green-400" },
reconnecting: {
label: "Reconnecting",
dot: "◌",
color: "text-amber-700 dark:text-amber-300",
},
disconnected: { label: "Disconnected", dot: "○", color: "text-red-700 dark:text-red-400" },
} as const
const GITHUB_URL = "https://github.com/dgknttr/hooktray"

Expand Down
49 changes: 12 additions & 37 deletions frontend/components/requests/RequestList.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,18 @@
"use client"
import dynamic from "next/dynamic"
import { useRef, useEffect } from "react"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"
import { useRequestStore } from "@/store/requestStore"
import { useConnectionStore } from "@/store/connectionStore"
import RequestListItem from "./RequestListItem"
import RequestListEmptyState from "./RequestListEmptyState"
import type { MethodFilter } from "@/types"

const RequestListClearAction = dynamic(() => import("./RequestListClearAction"), {
ssr: false,
loading: () => null,
})

const FILTERS: MethodFilter[] = ["ALL", "GET", "POST", "PUT", "PATCH", "DELETE"]
const FILTER_LABELS: Record<MethodFilter, string> = {
ALL: "All",
Expand All @@ -30,7 +24,7 @@ const FILTER_LABELS: Record<MethodFilter, string> = {
}

export default function RequestList() {
const { requests, selectedId, filter, autoScroll, selectRequest, setFilter, setAutoScroll, clearHistory } =
const { requests, selectedId, filter, autoScroll, selectRequest, setFilter, setAutoScroll } =
useRequestStore()
const { token } = useConnectionStore()

Expand All @@ -52,6 +46,7 @@ export default function RequestList() {
<div className="p-3 border-b space-y-2">
<ToggleGroup
value={[filter]}
aria-label="Filter requests by method"
onValueChange={(v: string[]) => {
const last = v[v.length - 1] as MethodFilter | undefined
if (last) setFilter(last)
Expand All @@ -76,27 +71,7 @@ export default function RequestList() {
Auto-scroll
</label>

<AlertDialog>
<AlertDialogTrigger
render={<Button variant="destructive" size="sm" className="text-xs h-6 px-2" />}
>
Clear
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Clear local history?</AlertDialogTitle>
<AlertDialogDescription>
This only clears requests stored in this browser.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={() => token && clearHistory(token)}>
Clear
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{requests.length > 0 ? <RequestListClearAction /> : null}
</div>
</div>

Expand All @@ -123,18 +98,18 @@ export default function RequestList() {
</div>

<div className="border-t px-3 py-2 flex items-center justify-between">
<span className="text-xs text-muted-foreground/60 font-medium">HookTray</span>
<span className="text-xs font-medium text-muted-foreground">HookTray</span>
<div className="flex items-center gap-3">
<a
href="https://github.com/dgknttr/hooktray"
target="_blank"
rel="noopener noreferrer"
className="text-xs text-muted-foreground/60 hover:text-muted-foreground transition-colors duration-150"
className="text-xs text-muted-foreground transition-colors duration-150 hover:text-foreground"
>
GitHub
</a>
<span className="text-muted-foreground/30 text-xs">·</span>
<span className="text-xs text-muted-foreground/60">Open source</span>
<span className="text-xs text-muted-foreground">Open source</span>
</div>
</div>
</div>
Expand Down
50 changes: 50 additions & 0 deletions frontend/components/requests/RequestListClearAction.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"use client"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"
import { useConnectionStore } from "@/store/connectionStore"
import { useRequestStore } from "@/store/requestStore"

export default function RequestListClearAction() {
const { clearHistory } = useRequestStore()
const { token } = useConnectionStore()

return (
<AlertDialog>
<AlertDialogTrigger
render={
<Button
variant="destructive"
size="sm"
className="h-6 border-red-200 bg-red-50 px-2 text-xs text-red-800 hover:bg-red-100 dark:border-red-900 dark:bg-red-950/40 dark:text-red-200 dark:hover:bg-red-950/60"
/>
}
>
Clear
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Clear local history?</AlertDialogTitle>
<AlertDialogDescription>
This only clears requests stored in this browser.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={() => token && clearHistory(token)}>
Clear
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}
11 changes: 5 additions & 6 deletions frontend/components/requests/RequestListEmptyState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
import { Inbox, Rows3 } from "lucide-react"

const PLACEHOLDER_ROWS = [
{ number: "#3", method: "POST", path: "/webhooks/order" },
{ number: "#2", method: "GET", path: "/health/check" },
{ number: "#1", method: "POST", path: "/stripe/events" },
{ number: "#3", method: "POST", path: "/webhooks/order", surface: "bg-background/90" },
{ number: "#2", method: "GET", path: "/health/check", surface: "bg-muted/30" },
{ number: "#1", method: "POST", path: "/stripe/events", surface: "bg-muted/20" },
]

export default function RequestListEmptyState() {
Expand All @@ -27,11 +27,10 @@ export default function RequestListEmptyState() {
</div>

<div className="mt-4 space-y-2" aria-hidden="true">
{PLACEHOLDER_ROWS.map((row, index) => (
{PLACEHOLDER_ROWS.map((row) => (
<div
key={row.number}
className="rounded-md border bg-background/70 px-2.5 py-2 opacity-70"
style={{ opacity: 0.55 - index * 0.12 }}
className={`rounded-md border px-2.5 py-2 ${row.surface}`}
>
<div className="flex min-w-0 items-center gap-2">
<span className="w-7 flex-shrink-0 text-right font-mono text-xs text-muted-foreground">
Expand Down
1 change: 1 addition & 0 deletions frontend/components/ui/toggle-group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ function ToggleGroup({
data-size={size}
data-spacing={spacing}
data-orientation={orientation}
role="toolbar"
style={{ "--gap": spacing } as React.CSSProperties}
className={cn(
"group/toggle-group flex w-fit flex-row items-center gap-[--spacing(var(--gap))] rounded-lg data-[size=sm]:rounded-[min(var(--radius-md),10px)] data-vertical:flex-col data-vertical:items-stretch",
Expand Down
14 changes: 14 additions & 0 deletions nginx/hooktray.conf.template
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,20 @@ server {
proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
}

# Fingerprinted Next.js build assets can be cached aggressively.
location ^~ /_next/static/ {
try_files $uri =404;
access_log off;
add_header Cache-Control "public, max-age=31536000, immutable";
}

# Public static assets are stable; purge CDN/cache when replacing them.
location ~* \.(?:svg|ico|png|jpg|jpeg|webp|woff2?)$ {
try_files $uri =404;
access_log off;
add_header Cache-Control "public, max-age=31536000";
}

# Frontend static files
location / {
try_files $uri $uri.html $uri/ /index.html;
Expand Down
Loading