feat: add history menu
This commit is contained in:
parent
38a6b5c270
commit
b5c23a1246
6 changed files with 164 additions and 23 deletions
|
|
@ -11,9 +11,11 @@ import About from "./pages/about"
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
queries: {
|
queries: {
|
||||||
staleTime: 15 * 60 * 1000, // 15 minutes
|
staleTime: Infinity,
|
||||||
retry: 3,
|
gcTime: Infinity,
|
||||||
|
retry: 1,
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
|
refetchOnMount: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,34 @@
|
||||||
import { SidebarTrigger } from "./ui/sidebar"
|
import { SidebarTrigger } from "./ui/sidebar"
|
||||||
import { Separator } from "./ui/separator"
|
import { Separator } from "./ui/separator"
|
||||||
import Search from "@/containers/Search"
|
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 (
|
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">
|
<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">
|
<div className="flex w-full items-center gap-1 px-4 lg:gap-2 lg:px-6">
|
||||||
<SidebarTrigger className="-ml-1" />
|
<SidebarTrigger className="-ml-1" />
|
||||||
<Separator orientation="vertical" className="mx-2 data-[orientation=vertical]:h-4" />
|
<Separator orientation="vertical" className="mx-2 data-[orientation=vertical]:h-4" />
|
||||||
<Search />
|
<Search />
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
className="rounded-none"
|
||||||
|
onClick={() => setHistoryOpen(true)}
|
||||||
|
>
|
||||||
|
<HistoryIcon />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<History sheetOpen={historyOpen} setSheetOpen={setHistoryOpen} error={false} />
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
100
src/components/History.tsx
Normal file
100
src/components/History.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -27,3 +27,14 @@ export const convertDateFriendly = (isoStamp) => {
|
||||||
const year = unixSeconds.getFullYear()
|
const year = unixSeconds.getFullYear()
|
||||||
return `${day} ${month} ${year}`
|
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`
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ export default function EntryTemplate() {
|
||||||
const { data, isLoading } = useQuery({
|
const { data, isLoading } = useQuery({
|
||||||
queryKey: [`entry_${entry}`],
|
queryKey: [`entry_${entry}`],
|
||||||
queryFn: () => api.get(`/entries/${entry}`).then((res) => res.data),
|
queryFn: () => api.get(`/entries/${entry}`).then((res) => res.data),
|
||||||
|
meta: { visited: true, visitedAt: Date.now() },
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,21 @@ import AppHeader from "@/components/AppHeader"
|
||||||
import { AppSidebar } from "@/containers/AppSidebar"
|
import { AppSidebar } from "@/containers/AppSidebar"
|
||||||
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"
|
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"
|
||||||
import { ThemeProvider } from "@/context/ThemeProvider"
|
import { ThemeProvider } from "@/context/ThemeProvider"
|
||||||
|
import { useState } from "react"
|
||||||
|
|
||||||
export default function MainTemplate({ children, pageTitle }) {
|
export default function MainTemplate({ children, pageTitle }) {
|
||||||
|
const [historyOpen, setHistoryOpen] = useState(false)
|
||||||
return (
|
return (
|
||||||
<ThemeProvider storageKey="app-theme">
|
<ThemeProvider storageKey="app-theme">
|
||||||
<SidebarProvider variant="inset">
|
<SidebarProvider variant="inset">
|
||||||
<AppSidebar />
|
<AppSidebar />
|
||||||
<SidebarInset className="flex flex-col h-screen">
|
<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>
|
<main className="flex-1 overflow-x-auto">{children}</main>
|
||||||
</SidebarInset>
|
</SidebarInset>
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue