Skip to content

Commit c19a1eb

Browse files
committed
Add search functionality into commands and permission list.
1 parent c808043 commit c19a1eb

File tree

1 file changed

+175
-85
lines changed

1 file changed

+175
-85
lines changed
Lines changed: 175 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,106 +1,196 @@
1-
"use client";
1+
"use client"
22

3-
import React, { useEffect, useState } from "react";
3+
import { create, insert, search, type AnyOrama } from "@orama/orama"
4+
import { motion, AnimatePresence } from "framer-motion"
5+
import { Search } from "lucide-react"
6+
import { useEffect, useState } from "react"
47

5-
interface Command {
6-
name: string;
7-
permission: string;
8-
description: string;
9-
arguments: string;
10-
}
8+
import { cn } from "@/lib/utils"
119

12-
interface RawCommand {
13-
name?: string;
14-
permissions?: string[];
15-
descriptions?: string[];
16-
arguments?: string[];
10+
interface CommandData {
11+
name: string
12+
permission: string
13+
description: string
14+
arguments: string
1715
}
1816

19-
interface RawPermission {
20-
name?: string;
21-
permissions?: string[];
22-
descriptions?: string[];
17+
interface EternalCoreData {
18+
commands?: Array<{
19+
name: string
20+
permissions?: string[]
21+
descriptions?: string[]
22+
arguments?: string[]
23+
}>
24+
permissions?: Array<{
25+
name: string
26+
permissions?: string[]
27+
descriptions?: string[]
28+
}>
2329
}
2430

2531
export default function DynamicCommandsTable() {
26-
const [commands, setCommands] = useState<Command[]>([]);
27-
const [loading, setLoading] = useState(true);
28-
const [error, setError] = useState<string | null>(null);
32+
const [commands, setCommands] = useState<CommandData[]>([])
33+
const [filtered, setFiltered] = useState<CommandData[]>([])
34+
const [searchQuery, setSearchQuery] = useState("")
35+
const [db, setDb] = useState<AnyOrama | null>(null)
36+
const [focused, setFocused] = useState(false)
37+
const [error, setError] = useState<string | null>(null)
2938

3039
useEffect(() => {
31-
fetch(
32-
"https://raw.githubusercontent.com/EternalCodeTeam/EternalCore/refs/heads/master/raw_eternalcore_documentation.json"
33-
)
34-
.then((res) => {
35-
if (!res.ok) throw new Error("Failed to fetch commands");
36-
return res.json();
37-
})
38-
.then((data: any) => {
39-
if (
40-
!data ||
41-
typeof data !== "object" ||
42-
(!Array.isArray(data.commands) && !Array.isArray(data.permissions))
43-
) {
44-
setError("Invalid data formatting received from server");
45-
return;
46-
}
47-
48-
const commandsData = (data.commands || []).map(
49-
(cmd: RawCommand): Command => ({
50-
name: `/${cmd.name?.trim() || "unknown"}`,
51-
permission: cmd.permissions?.[0] || "-",
52-
description: cmd.descriptions?.[0] || "-",
53-
arguments: cmd.arguments?.join(", ") || "-",
54-
})
55-
);
40+
const load = async (): Promise<void> => {
41+
try {
42+
const res = await fetch(
43+
"https://raw.githubusercontent.com/EternalCodeTeam/EternalCore/refs/heads/master/raw_eternalcore_documentation.json"
44+
)
45+
46+
if (!res.ok) throw new Error("Failed to fetch data")
5647

57-
const permissionsData = (data.permissions || []).map(
58-
(perm: RawPermission): Command => ({
59-
name: perm.name?.trim() || "Unknown",
60-
permission: perm.permissions?.[0] || "-",
61-
description: perm.descriptions?.[0] || "-",
48+
const data = (await res.json()) as unknown as EternalCoreData
49+
50+
const commands =
51+
data.commands?.map(c => ({
52+
name: `/${c.name.trim()}`,
53+
permission: c.permissions?.[0] ?? "-",
54+
description: c.descriptions?.[0] ?? "-",
55+
arguments: c.arguments?.join(", ") ?? "-",
56+
})) ?? []
57+
58+
const perms =
59+
data.permissions?.map(p => ({
60+
name: p.name || "Unknown",
61+
permission: p.permissions?.[0] ?? "-",
62+
description: p.descriptions?.[0] ?? "-",
6263
arguments: "-",
64+
})) ?? []
65+
66+
const all = [...commands, ...perms].sort((a, b) =>
67+
a.name.replace(/^\//, "").localeCompare(b.name.replace(/^\//, ""), "pl", {
68+
sensitivity: "base",
6369
})
64-
);
70+
)
6571

66-
const combined = [...commandsData, ...permissionsData].sort((a, b) =>
67-
a.name
68-
.replace(/^\//, "")
69-
.localeCompare(b.name.replace(/^\//, ""), "pl", { sensitivity: "base" })
70-
);
72+
setCommands(all)
73+
setFiltered(all)
7174

72-
setCommands(combined);
73-
})
74-
.catch((e) => {
75-
setError((e as Error).message);
75+
const orama = create({
76+
schema: {
77+
name: "string",
78+
permission: "string",
79+
description: "string",
80+
arguments: "string",
81+
},
82+
});
83+
84+
for (const c of all) await insert(orama, c)
85+
setDb(orama)
86+
} catch (err) {
87+
const message = err instanceof Error ? err.message : "Unknown error"
88+
setError(message)
89+
}
90+
}
91+
92+
void load()
93+
}, [])
94+
95+
const handleSearch = async (q: string): Promise<void> => {
96+
setSearchQuery(q)
97+
if (!q.trim() || !db) {
98+
setFiltered(commands)
99+
return
100+
}
101+
102+
try {
103+
const res = await search(db, {
104+
term: q,
105+
properties: ["name", "permission", "description", "arguments"],
106+
tolerance: 1,
76107
})
77-
.finally(() => setLoading(false));
78-
}, []);
108+
setFiltered(res.hits.map((h) => h.document as unknown as CommandData));
109+
} catch {
110+
setFiltered(commands)
111+
}
112+
}
79113

80-
if (loading) return <div>Loading commands…</div>;
81-
if (error) return <div>Error: {error}</div>;
82-
if (!commands.length) return <div>No commands found.</div>;
114+
if (error) {
115+
return <div className="p-6 text-center text-red-500">Error: {error}</div>
116+
}
117+
118+
if (!commands.length) {
119+
return <div className="p-6 text-center text-gray-500 dark:text-gray-400">Loading…</div>
120+
}
121+
122+
const CommandCount = (
123+
<motion.div
124+
initial={{ opacity: 0, y: -5 }}
125+
animate={{ opacity: 1, y: 0 }}
126+
transition={{ duration: 0.4 }}
127+
className="text-sm text-gray-600 dark:text-gray-400"
128+
>
129+
Commands:{" "}
130+
<span className="font-semibold text-gray-800 dark:text-gray-200">{filtered.length}</span> /{" "}
131+
{commands.length}
132+
</motion.div>
133+
)
83134

84135
return (
85-
<table>
86-
<thead>
87-
<tr>
88-
<th>Source</th>
89-
<th>Permission</th>
90-
<th>Description</th>
91-
<th>Arguments</th>
92-
</tr>
93-
</thead>
94-
<tbody>
95-
{commands.map((c) => (
96-
<tr key={`${c.name}-${c.permission}`}>
97-
<td>{c.name}</td>
98-
<td>{c.permission}</td>
99-
<td>{c.description}</td>
100-
<td>{c.arguments}</td>
136+
<div className="w-full">
137+
<div className="mb-6 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
138+
<div className="relative w-full sm:w-80">
139+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" size={18} />
140+
<input
141+
value={searchQuery}
142+
onChange={e => void handleSearch(e.target.value)}
143+
onFocus={() => setFocused(true)}
144+
onBlur={() => setFocused(false)}
145+
placeholder="Search for commands and permissions..."
146+
className={cn(
147+
"w-full select-none rounded-lg border bg-white px-4 py-2.5 pl-10 pr-10 text-sm outline-none transition-all duration-200",
148+
focused
149+
? "border-blue-500 shadow-lg shadow-blue-500/20 ring-2 ring-blue-500/50 dark:shadow-blue-500/10"
150+
: "border-gray-300 shadow-sm dark:border-gray-700",
151+
"placeholder:text-gray-400 dark:bg-gray-800 dark:text-white dark:placeholder:text-gray-500"
152+
)}
153+
/>
154+
</div>
155+
{CommandCount}
156+
</div>
157+
158+
<div className="my-6 overflow-x-auto rounded-lg">
159+
<table className="w-full border-collapse text-left text-sm">
160+
<thead className="bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-100">
161+
<tr>
162+
{["Source", "Permission", "Description", "Argument's"].map(h => (
163+
<th
164+
key={h}
165+
className="px-4 py-2 text-sm font-semibold text-gray-700 dark:text-gray-200"
166+
>
167+
{h}
168+
</th>
169+
))}
101170
</tr>
102-
))}
103-
</tbody>
104-
</table>
105-
);
171+
</thead>
172+
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
173+
<AnimatePresence>
174+
{filtered.map((c, i) => (
175+
<motion.tr
176+
key={i}
177+
initial={{ opacity: 0, y: 3 }}
178+
animate={{ opacity: 1, y: 0 }}
179+
exit={{ opacity: 0, y: -3 }}
180+
transition={{ duration: 0.15 }}
181+
className="transition-colors hover:bg-gray-50 dark:hover:bg-gray-900"
182+
>
183+
{[c.name, c.permission, c.description, c.arguments].map((v, j) => (
184+
<td key={j} className="px-4 py-2 text-sm text-gray-600 dark:text-gray-300">
185+
{v}
186+
</td>
187+
))}
188+
</motion.tr>
189+
))}
190+
</AnimatePresence>
191+
</tbody>
192+
</table>
193+
</div>
194+
</div>
195+
)
106196
}

0 commit comments

Comments
 (0)