Skip to content

Commit c74f428

Browse files
GH-181 Add placeholder docs automation (#181)
* Add dynamic placeholders documentation and API integration * lint fix, add placeholder documentation. * Add "Placeholders" to sidebar; improve type imports and button accessibility * revert * Update components/docs/eternalcore/placeholder/PlaceholderTable.tsx Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * Update components/docs/eternalcore/placeholder/hooks/usePlaceholders.ts Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * Refactor `usePlaceholders` hook: improve type imports, use `useCallback` for `filterPlaceholders`, and enhance error handling. * Refine `PlaceholderSearchBar`: improve focus styles, add shadow enhancements, and ensure consistent dark mode styling. --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
1 parent 6112fe4 commit c74f428

File tree

12 files changed

+365
-26
lines changed

12 files changed

+365
-26
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
"use client";
2+
3+
import { motion } from "framer-motion";
4+
5+
export function DynamicNoPlaceholderMessage() {
6+
return (
7+
<motion.div
8+
initial={{ opacity: 0 }}
9+
animate={{ opacity: 1 }}
10+
className="py-12 text-center text-gray-500 dark:text-gray-400"
11+
>
12+
No placeholders found.
13+
</motion.div>
14+
);
15+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
"use client";
2+
3+
import { DynamicNoPlaceholderMessage } from "@/components/docs/eternalcore/placeholder/DynamicNoPlaceholderMessage";
4+
import { usePlaceholders } from "@/components/docs/eternalcore/placeholder/hooks/usePlaceholders";
5+
import { PlaceholderCategoryButtons } from "@/components/docs/eternalcore/placeholder/PlaceholderCategoryButtons";
6+
import { PlaceholderSearchBar } from "@/components/docs/eternalcore/placeholder/PlaceholderSearchBar";
7+
import { PlaceholderTable } from "@/components/docs/eternalcore/placeholder/PlaceholderTable";
8+
9+
export default function DynamicPlaceholdersTable() {
10+
const {
11+
allPlaceholders,
12+
viewablePlaceholders,
13+
categories,
14+
activeCategory,
15+
searchQuery,
16+
error,
17+
loading,
18+
handleSearchChange,
19+
handleCategoryClick,
20+
} = usePlaceholders();
21+
22+
if (error) {
23+
return <div className="p-6 text-center text-red-500">Error: {error}</div>;
24+
}
25+
26+
if (loading) {
27+
return <div className="p-6 text-center text-gray-500 dark:text-gray-400">Loading...</div>;
28+
}
29+
30+
return (
31+
<div className="w-full">
32+
<div className="mb-6 flex flex-col gap-4">
33+
<PlaceholderSearchBar
34+
searchQuery={searchQuery}
35+
onSearchChange={handleSearchChange}
36+
viewableCount={viewablePlaceholders.length}
37+
totalCount={allPlaceholders.length}
38+
/>
39+
40+
<PlaceholderCategoryButtons
41+
categories={categories}
42+
activeCategory={activeCategory}
43+
onCategoryClick={handleCategoryClick}
44+
/>
45+
</div>
46+
47+
<PlaceholderTable placeholders={viewablePlaceholders} />
48+
49+
{!viewablePlaceholders.length && <DynamicNoPlaceholderMessage />}
50+
</div>
51+
);
52+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
"use client";
2+
3+
import { Tag } from "lucide-react";
4+
5+
import { cn } from "@/lib/utils";
6+
7+
interface PlaceholderCategoryButtonsProps {
8+
categories: string[];
9+
activeCategory: string;
10+
onCategoryClick: (category: string) => void;
11+
}
12+
13+
export function PlaceholderCategoryButtons({
14+
categories,
15+
activeCategory,
16+
onCategoryClick,
17+
}: PlaceholderCategoryButtonsProps) {
18+
return (
19+
<div className="flex flex-wrap gap-2">
20+
{categories.map((category) => (
21+
<button
22+
type="button"
23+
key={category}
24+
onClick={() => onCategoryClick(category)}
25+
className={cn(
26+
"cursor-pointer flex items-center gap-1.5 rounded-full px-3 py-1.5 text-xs font-medium transition-all",
27+
category === activeCategory
28+
? "bg-blue-500 text-white shadow-md shadow-blue-500/30"
29+
: "bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
30+
)}
31+
>
32+
<Tag size={12} />
33+
{category}
34+
</button>
35+
))}
36+
</div>
37+
);
38+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
"use client";
2+
3+
import { Search } from "lucide-react";
4+
import { type ChangeEvent, useState } from "react";
5+
6+
import { cn } from "@/lib/utils";
7+
8+
interface PlaceholderSearchBarProps {
9+
searchQuery: string;
10+
onSearchChange: (event: ChangeEvent<HTMLInputElement>) => void;
11+
viewableCount: number;
12+
totalCount: number;
13+
}
14+
15+
export function PlaceholderSearchBar({
16+
searchQuery,
17+
onSearchChange,
18+
viewableCount,
19+
totalCount,
20+
}: PlaceholderSearchBarProps) {
21+
const [isInputFocused, setIsInputFocused] = useState(false);
22+
23+
return (
24+
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
25+
<div className="relative w-full sm:w-80">
26+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" size={18} />
27+
<input
28+
value={searchQuery}
29+
onChange={onSearchChange}
30+
onFocus={() => setIsInputFocused(true)}
31+
onBlur={() => setIsInputFocused(false)}
32+
placeholder="Search placeholders..."
33+
className={cn(
34+
"w-full select-none rounded-lg border bg-white px-4 py-2.5 pl-10 pr-10 text-sm outline-hidden transition-all duration-200",
35+
isInputFocused
36+
? "border-blue-500 shadow-lg shadow-blue-500/20 ring-2 ring-blue-500/50 dark:shadow-blue-500/10"
37+
: "border-gray-300 shadow-xs dark:border-gray-700",
38+
"placeholder:text-gray-400 dark:bg-gray-800 dark:text-white dark:placeholder:text-gray-500"
39+
)}
40+
/>
41+
</div>
42+
<div className="text-sm text-gray-600 dark:text-gray-400">
43+
Placeholders: <span className="font-semibold">{viewableCount}</span> / {totalCount}
44+
</div>
45+
</div>
46+
);
47+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
"use client";
2+
3+
import { AnimatePresence, motion } from "framer-motion";
4+
5+
import type { Placeholder } from "@/components/docs/eternalcore/placeholder/types";
6+
import { cn } from "@/lib/utils";
7+
8+
interface PlaceholderTableProps {
9+
placeholders: Placeholder[];
10+
}
11+
12+
export function PlaceholderTable({ placeholders }: PlaceholderTableProps) {
13+
return (
14+
<div className="overflow-x-auto rounded-lg">
15+
<table className="w-full text-left text-sm">
16+
<thead className="bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-100">
17+
<tr>
18+
{["Placeholder", "Description", "Example", "Type", "Category", "Player Context"].map(
19+
(header) => (
20+
<th key={header} className="px-4 py-3 font-semibold">
21+
{header}
22+
</th>
23+
)
24+
)}
25+
</tr>
26+
</thead>
27+
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
28+
<AnimatePresence>
29+
{placeholders.map((p, i) => (
30+
<motion.tr
31+
key={p.name}
32+
initial={{ opacity: 0, y: 5 }}
33+
animate={{ opacity: 1, y: 0 }}
34+
exit={{ opacity: 0, y: -5 }}
35+
transition={{ duration: 0.2, delay: i * 0.01 }}
36+
className="hover:bg-gray-50 dark:hover:bg-gray-900/50"
37+
>
38+
<td className="px-4 py-2 font-mono text-blue-600 dark:text-blue-400">{p.name}</td>
39+
<td className="px-4 py-2 text-gray-600 dark:text-gray-300">{p.description}</td>
40+
<td className="px-4 py-2 font-mono text-green-600 dark:text-green-400">
41+
{p.example}
42+
</td>
43+
<td className="px-4 py-2">
44+
<span className="rounded bg-purple-100 px-2 py-0.5 text-xs font-medium text-purple-700 dark:bg-purple-900/30 dark:text-purple-300">
45+
{p.returnType}
46+
</span>
47+
</td>
48+
<td className="px-4 py-2">
49+
<span className="rounded bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-600 dark:bg-gray-700 dark:text-gray-300">
50+
{p.category}
51+
</span>
52+
</td>
53+
<td className="px-4 py-2 text-center">
54+
<span
55+
className={cn(
56+
"rounded px-2 py-0.5 text-xs font-medium",
57+
p.requiresPlayer
58+
? "bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-300"
59+
: "bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400"
60+
)}
61+
>
62+
{p.requiresPlayer ? "Required" : "Optional"}
63+
</span>
64+
</td>
65+
</motion.tr>
66+
))}
67+
</AnimatePresence>
68+
</tbody>
69+
</table>
70+
</div>
71+
);
72+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./usePlaceholders";
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
"use client";
2+
3+
import { create, insert, type Orama, search } from "@orama/orama";
4+
import type React from "react";
5+
import { useCallback, useEffect, useState } from "react";
6+
import type { Placeholder } from "@/components/docs/eternalcore/placeholder/types";
7+
8+
const placeholderSchema = {
9+
name: "string",
10+
description: "string",
11+
example: "string",
12+
returnType: "string",
13+
category: "string",
14+
requiresPlayer: "boolean",
15+
} as const;
16+
17+
type PlaceholderDB = Orama<typeof placeholderSchema>;
18+
19+
export function usePlaceholders() {
20+
const [allPlaceholders, setAllPlaceholders] = useState<Placeholder[]>([]);
21+
const [viewablePlaceholders, setViewablePlaceholders] = useState<Placeholder[]>([]);
22+
const [categories, setCategories] = useState<string[]>([]);
23+
const [activeCategory, setActiveCategory] = useState("All");
24+
const [searchQuery, setSearchQuery] = useState("");
25+
const [db, setDb] = useState<PlaceholderDB | undefined>();
26+
const [error, setError] = useState<string | undefined>();
27+
const [loading, setLoading] = useState(true);
28+
29+
useEffect(() => {
30+
const initializeData = async () => {
31+
try {
32+
setLoading(true);
33+
34+
const response = await fetch(
35+
"https://raw.githubusercontent.com/EternalCodeTeam/EternalCore/refs/heads/master/raw_eternalcore_placeholders.json"
36+
);
37+
38+
if (!response.ok) {
39+
setError(`Failed to fetch data: ${response.statusText}`);
40+
return;
41+
}
42+
43+
const data = (await response.json()) as Placeholder[];
44+
const sortedData = [...data].sort((a, b) => a.name.localeCompare(b.name, "pl"));
45+
46+
setAllPlaceholders(sortedData);
47+
setViewablePlaceholders(sortedData);
48+
49+
const uniqueCategories = Array.from(new Set(sortedData.map((p) => p.category))).sort();
50+
setCategories(["All", ...uniqueCategories]);
51+
52+
const oramaDb = create({ schema: placeholderSchema });
53+
await Promise.all(sortedData.map((p) => insert(oramaDb, p)));
54+
55+
setDb(oramaDb);
56+
} catch (exception) {
57+
setError(exception instanceof Error ? exception.message : "An unknown error occurred");
58+
} finally {
59+
setLoading(false);
60+
}
61+
};
62+
63+
initializeData();
64+
}, []);
65+
66+
const filterPlaceholders = useCallback(
67+
async (query = searchQuery, category = activeCategory) => {
68+
try {
69+
let filteredList =
70+
category === "All"
71+
? allPlaceholders
72+
: allPlaceholders.filter((p) => p.category === category);
73+
74+
if (query.trim() && db) {
75+
const searchResult = await search(db, {
76+
term: query,
77+
properties: ["name", "description", "example", "category"],
78+
tolerance: 1,
79+
});
80+
81+
const resultIds = new Set(searchResult.hits.map((hit) => hit.document.name));
82+
filteredList = filteredList.filter((p) => resultIds.has(p.name));
83+
}
84+
85+
setViewablePlaceholders(filteredList);
86+
} catch (exception) {
87+
setError(
88+
exception instanceof Error ? exception.message : "An unknown search error occurred"
89+
);
90+
}
91+
},
92+
[allPlaceholders, db, searchQuery, activeCategory]
93+
);
94+
95+
const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
96+
setSearchQuery(event.target.value);
97+
};
98+
99+
useEffect(() => {
100+
const timeout = setTimeout(() => {
101+
filterPlaceholders(searchQuery, activeCategory);
102+
}, 300);
103+
104+
return () => clearTimeout(timeout);
105+
}, [searchQuery, activeCategory, filterPlaceholders]);
106+
107+
const handleCategoryClick = (category: string) => {
108+
setActiveCategory(category);
109+
};
110+
111+
return {
112+
allPlaceholders,
113+
viewablePlaceholders,
114+
categories,
115+
activeCategory,
116+
searchQuery,
117+
error,
118+
loading,
119+
handleSearchChange,
120+
handleCategoryClick,
121+
};
122+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export * from "./DynamicNoPlaceholderMessage";
2+
export * from "./PlaceholderCategoryButtons";
3+
export * from "./PlaceholderSearchBar";
4+
export * from "./PlaceholderTable";
5+
export * from "./types";
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export interface Placeholder {
2+
name: string;
3+
description: string;
4+
example: string;
5+
returnType: string;
6+
category: string;
7+
requiresPlayer: boolean;
8+
}

components/mdx/mdx-components.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { ComponentProps, HTMLAttributes } from "react";
33
import Image from "next/image";
44

55
import DynamicCommandsTable from "@/components/docs/eternalcore/DynamicCommandsTable";
6+
import DynamicPlaceholdersTable from "@/components/docs/eternalcore/placeholder/DynamicPlaceholdersTable";
67
import { CodeBlock } from "@/components/mdx/CodeBlock";
78
import { CodeTab, CodeTabs } from "@/components/mdx/CodeTabs";
89
import { Heading } from "@/components/mdx/Heading";
@@ -43,6 +44,7 @@ export const components: MDXComponents = {
4344
CodeTabs,
4445
CodeTab,
4546
DynamicCommandsTable,
47+
DynamicPlaceholdersTable,
4648

4749
code: (props: ComponentProps<"code">) => {
4850
const { children, ...rest } = props;

0 commit comments

Comments
 (0)