feat: add history menu

This commit is contained in:
Thomas Bishop 2025-12-19 17:57:28 +00:00
parent 38a6b5c270
commit b5c23a1246
6 changed files with 164 additions and 23 deletions

View file

@ -9,28 +9,30 @@ import Settings from "./pages/settings"
import About from "./pages/about"
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 15 * 60 * 1000, // 15 minutes
retry: 3,
refetchOnWindowFocus: false,
},
},
defaultOptions: {
queries: {
staleTime: Infinity,
gcTime: Infinity,
retry: 1,
refetchOnWindowFocus: false,
refetchOnMount: false,
},
},
})
export default function App() {
return (
<BrowserRouter>
<QueryClientProvider client={queryClient}>
<Routes>
<Route index element={<Home />} />
<Route path="/settings" element={<Settings />} />
<Route path="/about" element={<About />} />
<Route path="/entries/:entry" element={<EntryTemplate />} />
<Route path="/tags/:tag" element={<TagTemplate />} />
</Routes>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
</BrowserRouter>
)
return (
<BrowserRouter>
<QueryClientProvider client={queryClient}>
<Routes>
<Route index element={<Home />} />
<Route path="/settings" element={<Settings />} />
<Route path="/about" element={<About />} />
<Route path="/entries/:entry" element={<EntryTemplate />} />
<Route path="/tags/:tag" element={<TagTemplate />} />
</Routes>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
</BrowserRouter>
)
}

View file

@ -1,14 +1,34 @@
import { SidebarTrigger } from "./ui/sidebar"
import { Separator } from "./ui/separator"
import Search from "@/containers/Search"
import { Button } from "./ui/button"
import { useState } from "react"
import History from "./History"
import { HistoryIcon } from "lucide-react"
export default function AppHeader({ pageTitle }: { pageTitle: string }) {
export default function AppHeader({
pageTitle,
historyOpen,
setHistoryOpen,
}: {
pageTitle: string
}) {
return (
<header className="sticky top-0 z-10 flex h-12 shrink-0 items-center gap-2 border-b bg-background transition-[width,height] ease-linear">
<div className="flex w-full items-center gap-1 px-4 lg:gap-2 lg:px-6">
<SidebarTrigger className="-ml-1" />
<Separator orientation="vertical" className="mx-2 data-[orientation=vertical]:h-4" />
<Search />
<Button
variant="outline"
size="icon"
className="rounded-none"
onClick={() => setHistoryOpen(true)}
>
<HistoryIcon />
</Button>
<History sheetOpen={historyOpen} setSheetOpen={setHistoryOpen} error={false} />
</div>
</header>
)

100
src/components/History.tsx Normal file
View file

@ -0,0 +1,100 @@
import { Link } from "react-router"
import { Badge } from "./ui/badge"
import { SheetContent, Sheet, SheetHeader, SheetTitle } from "./ui/sheet"
import { useLocation } from "react-router"
import { useQueryClient } from "@tanstack/react-query"
import { formatRelativeTime } from "@/lib/utils"
import { useEffect } from "react"
export default function History({ sheetOpen, setSheetOpen, error }) {
const location = useLocation()
const queryClient = useQueryClient()
const searchHistory = queryClient.getQueriesData({ queryKey: ["search_results"] })
const entriesHistory = queryClient.getQueryCache().findAll({
predicate: (query) => {
const firstKey = query.queryKey[0]
return (
typeof firstKey === "string" &&
firstKey.startsWith("entry_") &&
query.meta?.visited === true
)
},
})
const entries = entriesHistory
.map((query) => ({
title: query.state.data?.title,
visitedAt: query.options?.meta?.visitedAt,
queryKey: query.queryKey[0],
}))
.filter((x) => x.title !== undefined)
.sort((a, b) => b.visitedAt - a.visitedAt)
// Force Sheet close on renavigation (i.e search result selection)
useEffect(() => {
if (sheetOpen) {
setSheetOpen(false)
}
}, [location.pathname])
return (
<Sheet
open={sheetOpen}
onOpenChange={(open) => {
setSheetOpen(open)
}}
>
<SheetContent>
<SheetHeader className="border-b bg-sidebar">
<SheetTitle className="flex gap-3">Session history</SheetTitle>
</SheetHeader>
{error ? (
<div className="p-4 text-sm dark:text-red-300 text-red-700">
<div className="p-2 border-2 dark:border-red-800 border-red-500 dark:bg-red-900 bg-red-300">
Error fetching history.
</div>{" "}
</div>
) : (
<>
<div className="flex flex row justify-between bg-sidebar mx-2 pt-0">
<h3 className="font-medium text-sm p-1 ml-1">Entries</h3>
<Badge variant="secondary" className="rounded-none">
{entries.length || 0}
</Badge>
</div>
<div className="overflow-y-auto overflow-x-hidden p-4 pt-0">
{entries?.map((entry, i) => (
<div className="flex flex-row justify-between">
<Link
key={i}
to={`/entries/${entry.title}`}
className="text-foreground underline-offset-3 text-sm underline hover:text-gray-700 dark:hover:text-green-300 block mb-2"
>
{i === 0 ? (
<span className="bg-yellow-200 dark:bg-fuchsia-500">
{entry?.title.replace(/_/g, " ")}
</span>
) : (
<span className="">{entry?.title.replace(/_/g, " ")}</span>
)}
</Link>
<span className="text-[12px]">
{formatRelativeTime(entry.visitedAt)}
</span>
</div>
))}
</div>
<div className="flex flex row justify-between bg-sidebar mx-2 pt-0">
<h3 className="font-medium text-sm p-1 ml-1">Searches</h3>
<Badge variant="secondary" className="rounded-none">
0
</Badge>
</div>
</>
)}
</SheetContent>
</Sheet>
)
}

View file

@ -27,3 +27,14 @@ export const convertDateFriendly = (isoStamp) => {
const year = unixSeconds.getFullYear()
return `${day} ${month} ${year}`
}
export const formatRelativeTime = (timestamp) => {
const seconds = Math.floor((Date.now() - timestamp) / 1000)
if (seconds < 60) return `${seconds} secs`
const minutes = Math.floor(seconds / 60)
if (minutes < 60) return `${minutes} mins`
const hours = Math.floor(minutes / 60)
if (hours < 24) return `${hours} hours`
const days = Math.floor(hours / 24)
return `${days} days`
}

View file

@ -11,6 +11,7 @@ export default function EntryTemplate() {
const { data, isLoading } = useQuery({
queryKey: [`entry_${entry}`],
queryFn: () => api.get(`/entries/${entry}`).then((res) => res.data),
meta: { visited: true, visitedAt: Date.now() },
})
return (

View file

@ -2,14 +2,21 @@ import AppHeader from "@/components/AppHeader"
import { AppSidebar } from "@/containers/AppSidebar"
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"
import { ThemeProvider } from "@/context/ThemeProvider"
import { useState } from "react"
export default function MainTemplate({ children, pageTitle }) {
const [historyOpen, setHistoryOpen] = useState(false)
return (
<ThemeProvider storageKey="app-theme">
<SidebarProvider variant="inset">
<AppSidebar />
<SidebarInset className="flex flex-col h-screen">
<AppHeader pageTitle={pageTitle} />
<AppHeader
pageTitle={pageTitle}
historyOpen={historyOpen}
setHistoryOpen={setHistoryOpen}
/>
<main className="flex-1 overflow-x-auto">{children}</main>
</SidebarInset>
</SidebarProvider>