feat: create Recent Edits component
This commit is contained in:
parent
5507e9aa02
commit
5b77317b5f
8 changed files with 372 additions and 82 deletions
34
package-lock.json
generated
34
package-lock.json
generated
|
|
@ -16,6 +16,7 @@
|
|||
"@tailwindcss/vite": "^4.1.4",
|
||||
"@tanstack/react-query": "^5.83.0",
|
||||
"@tanstack/react-query-devtools": "^5.83.0",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"axios": "^1.10.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
|
|
@ -2112,6 +2113,39 @@
|
|||
"react": "^18 || ^19"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-table": {
|
||||
"version": "8.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz",
|
||||
"integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/table-core": "8.21.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8",
|
||||
"react-dom": ">=16.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/table-core": {
|
||||
"version": "8.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz",
|
||||
"integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
"@tailwindcss/vite": "^4.1.4",
|
||||
"@tanstack/react-query": "^5.83.0",
|
||||
"@tanstack/react-query-devtools": "^5.83.0",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"axios": "^1.10.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ export default function EntriesListSidebar() {
|
|||
queryFn: () => api.get("/entries").then((res) => res.data),
|
||||
})
|
||||
|
||||
console.log(entries)
|
||||
return (
|
||||
<Collapsible className="group/collapsible">
|
||||
<SidebarMenuItem key="entries">
|
||||
|
|
@ -30,7 +29,7 @@ export default function EntriesListSidebar() {
|
|||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<SidebarMenuSub>
|
||||
{entries?.entries.map((item, i) => (
|
||||
{entries?.data.map((item, i) => (
|
||||
<SidebarMenuItem key={i}>
|
||||
<a>
|
||||
<span className="text-xs">{item.title.replace(/_/g, " ")}</span>
|
||||
|
|
|
|||
104
src/components/RecentEditsDataTable.tsx
Normal file
104
src/components/RecentEditsDataTable.tsx
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
import {
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
getPaginationRowModel,
|
||||
} from "@tanstack/react-table"
|
||||
|
||||
import { Button } from "./ui/button"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table"
|
||||
|
||||
export function RecentEditsDataTable({ columns, data, loading }) {
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
})
|
||||
|
||||
const pageCount = table.getPageCount()
|
||||
const currentPage = table.getState().pagination?.pageIndex
|
||||
return (
|
||||
<div>
|
||||
<div className="rounded-none border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead key={header.id}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
)
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : loading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
Loading...
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<div className="flex items-center justify-between space-x-2 py-4">
|
||||
<div>
|
||||
<span className="text-sm text-muted-foreground">{`${currentPage + 1} of ${pageCount}`}</span>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
className="rounded-none"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => table.previousPage()}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
Previous
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="rounded-none"
|
||||
size="sm"
|
||||
onClick={() => table.nextPage()}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -8,41 +8,41 @@ import api from "../api/eolas-api"
|
|||
import { Badge } from "./ui/badge"
|
||||
|
||||
export default function TagListSidebar() {
|
||||
const { data: tags, isLoading } = useQuery({
|
||||
queryKey: ["tag_list"],
|
||||
queryFn: () => api.get("/tags").then((res) => res.data),
|
||||
})
|
||||
const { data: tags, isLoading } = useQuery({
|
||||
queryKey: ["tag_list"],
|
||||
queryFn: () => api.get("/tags").then((res) => res.data),
|
||||
})
|
||||
|
||||
console.log(tags)
|
||||
console.log(tags)
|
||||
|
||||
return (
|
||||
<Collapsible className="group/collapsible">
|
||||
<SidebarMenuItem key="tags">
|
||||
<CollapsibleTrigger asChild>
|
||||
<SidebarMenuButton asChild>
|
||||
<a href="#">
|
||||
<Tags />
|
||||
<span>Tags</span>
|
||||
<Badge className="ml-0" variant="secondary">
|
||||
{tags?.count}
|
||||
</Badge>
|
||||
return (
|
||||
<Collapsible className="group/collapsible">
|
||||
<SidebarMenuItem key="tags">
|
||||
<CollapsibleTrigger asChild>
|
||||
<SidebarMenuButton asChild>
|
||||
<a href="#">
|
||||
<Tags />
|
||||
<span>Tags</span>
|
||||
<Badge className="ml-0" variant="secondary">
|
||||
{tags?.count}
|
||||
</Badge>
|
||||
|
||||
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
|
||||
</a>
|
||||
</SidebarMenuButton>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<SidebarMenuSub>
|
||||
{tags?.tags.map((item, i) => (
|
||||
<SidebarMenuItem key={i}>
|
||||
<a>
|
||||
<span className="text-xs">{item}</span>
|
||||
</a>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
</SidebarMenuSub>
|
||||
</CollapsibleContent>
|
||||
</SidebarMenuItem>
|
||||
</Collapsible>
|
||||
)
|
||||
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
|
||||
</a>
|
||||
</SidebarMenuButton>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<SidebarMenuSub>
|
||||
{tags?.data.map((item, i) => (
|
||||
<SidebarMenuItem key={i}>
|
||||
<a>
|
||||
<span className="text-xs">{item}</span>
|
||||
</a>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
</SidebarMenuSub>
|
||||
</CollapsibleContent>
|
||||
</SidebarMenuItem>
|
||||
</Collapsible>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
114
src/components/ui/table.tsx
Normal file
114
src/components/ui/table.tsx
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Table({ className, ...props }: React.ComponentProps<"table">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="table-container"
|
||||
className="relative w-full overflow-x-auto"
|
||||
>
|
||||
<table
|
||||
data-slot="table"
|
||||
className={cn("w-full caption-bottom text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
||||
return (
|
||||
<thead
|
||||
data-slot="table-header"
|
||||
className={cn("[&_tr]:border-b", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
|
||||
return (
|
||||
<tbody
|
||||
data-slot="table-body"
|
||||
className={cn("[&_tr:last-child]:border-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
||||
return (
|
||||
<tfoot
|
||||
data-slot="table-footer"
|
||||
className={cn(
|
||||
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
||||
return (
|
||||
<tr
|
||||
data-slot="table-row"
|
||||
className={cn(
|
||||
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
||||
return (
|
||||
<th
|
||||
data-slot="table-head"
|
||||
className={cn(
|
||||
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
||||
return (
|
||||
<td
|
||||
data-slot="table-cell"
|
||||
className={cn(
|
||||
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableCaption({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"caption">) {
|
||||
return (
|
||||
<caption
|
||||
data-slot="table-caption"
|
||||
className={cn("text-muted-foreground mt-4 text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption,
|
||||
}
|
||||
54
src/containers/RecentEdits.tsx
Normal file
54
src/containers/RecentEdits.tsx
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import { RecentEditsDataTable } from "@/components/RecentEditsDataTable"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import api from "../api/eolas-api"
|
||||
|
||||
const columns = [
|
||||
{
|
||||
accessorKey: "title",
|
||||
header: "Title",
|
||||
},
|
||||
{
|
||||
accessorKey: "date",
|
||||
header: "Date",
|
||||
},
|
||||
{
|
||||
accessorKey: "time",
|
||||
header: "Time",
|
||||
},
|
||||
]
|
||||
|
||||
export default function RecentEdits() {
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ["entries_recent"],
|
||||
queryFn: () => api.get("/entries?limit=20&sort=date").then((res) => res.data),
|
||||
})
|
||||
|
||||
console.log(data)
|
||||
const parsed = data?.data?.map((entry) => {
|
||||
const [date, time] = entry?.last_modified?.split(" ")
|
||||
return {
|
||||
title: entry.title.replace(/_/g, " "),
|
||||
date: new Date(date).toLocaleString("en-GB", {
|
||||
day: "numeric",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
}),
|
||||
time: time,
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="border w-full">
|
||||
<div className="border-b py-2 px-4 lg:px-6 bg-sidebar">
|
||||
<h2 className="scroll-m-20 font-semibold">Recent edits</h2>
|
||||
</div>
|
||||
<div className="p-4 lg:p-6">
|
||||
<div className="container mx-auto py-2">
|
||||
<RecentEditsDataTable columns={columns} data={parsed || []} loading={isLoading} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,36 +1,29 @@
|
|||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import Main from "@/templates/Main"
|
||||
|
||||
// const scoreData = [
|
||||
// { label: "Total Posts", count: "548" },
|
||||
// { label: "Tags", count: "68" },
|
||||
// { label: "Backlinks", count: "766" },
|
||||
// ]
|
||||
import RecentEdits from "@/containers/RecentEdits"
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<>
|
||||
<Main pageTitle="Home">
|
||||
{/*
|
||||
<div className="grid grid-cols-3 gap-4 p-6 pb-0">
|
||||
{scoreData.map((item, index) => (
|
||||
<ScoreCard key={index} label={item.label} count={item.count} />
|
||||
))}
|
||||
return (
|
||||
<>
|
||||
<Main pageTitle="Home">
|
||||
<div className="flex-1 flex flex-col overflow-auto">
|
||||
<div className="@container/main flex flex-col">
|
||||
<div className="p-4 lg:p-6 flex flex-1">
|
||||
<div className="border w-full">
|
||||
<div className="border-b py-2 px-4 lg:px-6 bg-sidebar">
|
||||
<h2 className="scroll-m-20 font-semibold">Welcome</h2>
|
||||
</div>
|
||||
<div className="p-4 lg:p-6">
|
||||
<p className="leading-6 [&:not(:first-child)]:mt-6 font-normal">
|
||||
I'm Thomas. Eólas is my technical knowledge management system, or
|
||||
"second-brain", comprising notes from my study of software engineering
|
||||
and computer science.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
*/}
|
||||
|
||||
<div className="flex-1 flex flex-col overflow-auto">
|
||||
<div className="@container/main flex flex-col">
|
||||
<div className="p-4 lg:p-6 flex-1 flex">
|
||||
<Card className="border rounded-none shadow-none w-full p-2">
|
||||
<CardContent className="p-4 pt-6 overflow-auto">
|
||||
<h2 className="scroll-m-20 text-xl font-semibold">Welcome</h2>
|
||||
<p className="leading-7 [&:not(:first-child)]:mt-6 font-normal">
|
||||
I'm Thomas. Eólas is my technical knowledge management system, or
|
||||
"second-brain", comprising notes from my study of software engineering
|
||||
and computer science.
|
||||
</p>
|
||||
|
||||
{/*
|
||||
|
||||
<div className="bg-muted rounded-none p-3 font-medium text-sm text-muted-foreground my-4 mb-6">
|
||||
<p className="">
|
||||
{" "}
|
||||
|
|
@ -39,22 +32,13 @@ export default function Home() {
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<h2 className="scroll-m-20 text-lg font-semibold">Recent edits</h2>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Main>
|
||||
</>
|
||||
)
|
||||
*/}
|
||||
<div className="px-4 lg:px-6 flex-1 flex">
|
||||
<RecentEdits />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// const ScoreCard = ({ label, count }) => (
|
||||
// <div className="rounded-sm border bg-card text-card-foreground p-6">
|
||||
// <div className="flex flex-col space-y-1.5">
|
||||
// <h3 className="text-2xl font-semibold leading-none tracking-tight">{count}</h3>
|
||||
// <p className="text-sm text-muted-foreground">{label}</p>
|
||||
// </div>
|
||||
// </div>
|
||||
// )
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue