Skip to content

Commit 3ac4515

Browse files
committed
Add internal resources page
1 parent f20230a commit 3ac4515

File tree

2 files changed

+107
-0
lines changed

2 files changed

+107
-0
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { readResources } from "@/resources/data"
2+
import { ResourcesTable } from "./resources-table"
3+
4+
export default async function InternalResourcesPage() {
5+
const resources = await readResources()
6+
7+
return <ResourcesTable resources={resources} />
8+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
"use client"
2+
3+
import SearchIcon from "@/app/conf/_design-system/pixelarticons/search.svg?svgr"
4+
import type { ResourceMetadata } from "@/resources/types"
5+
import clsx from "clsx"
6+
import { useState, useTransition } from "react"
7+
8+
function fuzzyMatch(text: string, query: string): boolean {
9+
const lowerText = text.toLowerCase()
10+
const lowerQuery = query.toLowerCase()
11+
let queryIndex = 0
12+
for (let i = 0; i < lowerText.length && queryIndex < lowerQuery.length; i++) {
13+
if (lowerText[i] === lowerQuery[queryIndex]) {
14+
queryIndex++
15+
}
16+
}
17+
return queryIndex === lowerQuery.length
18+
}
19+
20+
function matchesSearch(resource: ResourceMetadata, query: string): boolean {
21+
if (!query) return true
22+
const searchable = [
23+
resource.title,
24+
resource.url,
25+
resource.author,
26+
resource.kind,
27+
resource.description,
28+
...resource.tags,
29+
].join(" ")
30+
return fuzzyMatch(searchable, query)
31+
}
32+
33+
function Cell({ children }: { children: string | undefined }) {
34+
if (!children) return <td className="px-2 py-1" />
35+
return (
36+
<td className="max-w-[200px] truncate px-2 py-1" title={children}>
37+
{children}
38+
</td>
39+
)
40+
}
41+
42+
type ResourcesTableProps = {
43+
resources: ResourceMetadata[]
44+
}
45+
46+
export function ResourcesTable({ resources }: ResourcesTableProps) {
47+
const [search, setSearch] = useState("")
48+
const [query, setQuery] = useState("")
49+
const [isPending, startTransition] = useTransition()
50+
51+
const filtered = resources.filter(r => matchesSearch(r, query))
52+
53+
return (
54+
<div className="p-4">
55+
<label className="mb-4 flex items-center gap-2 border border-neu-300 bg-neu-0 p-2 focus-within:gql-focus-outline">
56+
<SearchIcon className="size-5 text-neu-800" />
57+
<input
58+
type="text"
59+
placeholder="Search..."
60+
value={search}
61+
autoFocus
62+
onChange={e => {
63+
setSearch(e.target.value)
64+
startTransition(() => setQuery(e.target.value))
65+
}}
66+
className="w-full bg-transparent font-mono text-sm placeholder:text-neu-600 focus:outline-none dark:placeholder:text-neu-400"
67+
/>
68+
</label>
69+
<table className="w-full font-mono text-sm">
70+
<thead className="typography-menu text-pri-base dark:text-pri-light">
71+
<tr>
72+
<th className="whitespace-nowrap px-2 py-1 text-left">Title</th>
73+
<th className="whitespace-nowrap px-2 py-1 text-left">URL</th>
74+
<th className="whitespace-nowrap px-2 py-1 text-left">Author</th>
75+
<th className="whitespace-nowrap px-2 py-1 text-left">Kind</th>
76+
<th className="whitespace-nowrap px-2 py-1 text-left">
77+
Description
78+
</th>
79+
<th className="whitespace-nowrap px-2 py-1 text-left">Duration</th>
80+
<th className="whitespace-nowrap px-2 py-1 text-left">Tags</th>
81+
</tr>
82+
</thead>
83+
<tbody>
84+
{filtered.map((resource, i) => (
85+
<tr key={i} className="border-t border-neu-200">
86+
<Cell>{resource.title}</Cell>
87+
<Cell>{resource.url}</Cell>
88+
<Cell>{resource.author}</Cell>
89+
<Cell>{resource.kind}</Cell>
90+
<Cell>{resource.description}</Cell>
91+
<Cell>{resource.duration}</Cell>
92+
<Cell>{resource.tags.join(", ")}</Cell>
93+
</tr>
94+
))}
95+
</tbody>
96+
</table>
97+
</div>
98+
)
99+
}

0 commit comments

Comments
 (0)