feat(playground): integrate Gitea API for file tree retrieval and update PlaygroundSidebar component

This commit is contained in:
ngc2207 2024-12-14 17:52:15 +08:00
parent a872a33052
commit 08831481b8
5 changed files with 116 additions and 46 deletions

3
.gitignore vendored
View File

@ -44,3 +44,6 @@ cache
# bun
bun.lockb
# vscode
.vscode

View File

@ -21,6 +21,7 @@
"@radix-ui/react-tooltip": "^1.1.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cross-fetch": "^4.0.0",
"gitea-js": "^1.22.0",
"jotai": "^2.10.3",
"lucide-react": "^0.468.0",

22
src/app/actions/index.ts Normal file
View File

@ -0,0 +1,22 @@
import api from "@/lib/gitea";
import { GitEntry, APIError } from "gitea-js";
export async function retrieveTreeStructure(
owner: string,
repo: string,
sha: string
): Promise<GitEntry[]> {
try {
const response = await api.repos.getTree(owner, repo, sha, {
recursive: true,
});
return response.data.tree || [];
} catch (error) {
if ((error as APIError).message) {
console.error("Gitea API Error", error);
} else {
console.error("Unexpected Error", error);
}
throw new Error("Failed to retrieve tree structure");
}
}

View File

@ -1,3 +1,4 @@
import { auth } from "@/auth";
import * as React from "react";
import {
Sidebar,
@ -11,45 +12,78 @@ import {
SidebarMenuSub,
SidebarRail,
} from "@/components/ui/sidebar";
import { GitEntry } from "gitea-js";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { retrieveTreeStructure } from "@/app/actions";
import { ChevronRight, File, Folder } from "lucide-react";
const data = {
tree: [
[
"app",
[
"api",
["hello", ["route.ts"]],
"page.tsx",
"layout.tsx",
["blog", ["page.tsx"]],
],
],
[
"components",
["ui", "button.tsx", "card.tsx"],
"header.tsx",
"footer.tsx",
],
["lib", ["util.ts"]],
["public", "favicon.ico", "vercel.svg"],
".eslintrc.json",
".gitignore",
"next.config.js",
"tailwind.config.js",
"package.json",
"README.md",
],
};
interface FileTree {
name: string;
type: "blob" | "tree";
children?: { [key: string]: FileTree };
}
export function PlaygroundSidebar({
export function buildFileTree(tree: GitEntry[]): FileTree {
const root: FileTree = { name: "", type: "tree", children: {} };
tree.forEach((entry: GitEntry) => {
if (!entry.path) return;
const pathParts = entry.path.split("/");
let currentLevel = root;
pathParts.forEach((part: string) => {
if (!currentLevel.children) {
currentLevel.children = {};
}
if (!currentLevel.children[part]) {
currentLevel.children[part] = {
name: part,
type: entry.type as "blob" | "tree",
children: {},
};
}
currentLevel = currentLevel.children[part];
});
});
if (root.children) {
const sortedChildren = Object.values(root.children).sort((a, b) => {
if (a.type === "tree" && b.type === "blob") return -1;
if (a.type === "blob" && b.type === "tree") return 1;
return a.name.localeCompare(b.name);
});
root.children = sortedChildren.reduce((acc, child) => {
acc[child.name] = child;
return acc;
}, {} as { [key: string]: FileTree });
}
return root;
}
export async function PlaygroundSidebar({
...props
}: React.ComponentProps<typeof Sidebar>) {
const session = await auth();
const username = session?.user?.name ?? "";
let fileTree: FileTree = { name: "", type: "tree", children: {} };
if (username) {
const tree: GitEntry[] = await retrieveTreeStructure(
username,
"playground",
"main"
);
fileTree = buildFileTree(tree);
}
return (
<Sidebar {...props}>
<SidebarContent>
@ -57,9 +91,10 @@ export function PlaygroundSidebar({
<SidebarGroupLabel>Files</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{data.tree.map((item, index) => (
<Tree key={index} item={item} />
))}
{fileTree.children &&
Object.values(fileTree.children).map((item, index) => (
<Tree key={index} item={item} />
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
@ -69,25 +104,20 @@ export function PlaygroundSidebar({
);
}
function Tree({ item }: { item: string | any[] }) {
const [name, ...items] = Array.isArray(item) ? item : [item];
if (!items.length) {
function Tree({ item }: { item: FileTree }) {
const { name, type, children } = item;
if (type === "blob") {
return (
<SidebarMenuButton
isActive={name === "button.tsx"}
className="data-[active=true]:bg-transparent"
>
<SidebarMenuButton className="data-[active=true]:bg-transparent">
<File />
{name}
</SidebarMenuButton>
);
}
return (
<SidebarMenuItem>
<Collapsible
className="group/collapsible [&[data-state=open]>button>svg:first-child]:rotate-90"
defaultOpen={name === "components" || name === "ui"}
>
<Collapsible className="group/collapsible [&[data-state=open]>button>svg:first-child]:rotate-90">
<CollapsibleTrigger asChild>
<SidebarMenuButton>
<ChevronRight className="transition-transform" />
@ -97,9 +127,10 @@ function Tree({ item }: { item: string | any[] }) {
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{items.map((subItem, index) => (
<Tree key={index} item={subItem} />
))}
{children &&
Object.values(children).map((subItem, index) => (
<Tree key={index} item={subItem} />
))}
</SidebarMenuSub>
</CollapsibleContent>
</Collapsible>

13
src/lib/gitea.ts Normal file
View File

@ -0,0 +1,13 @@
import fetch from "cross-fetch";
import { giteaApi } from "gitea-js";
if (!process.env.GITEA_URL) {
throw new Error("GITEA_URL environment variable is not defined");
}
const api = giteaApi(process.env.GITEA_URL, {
token: process.env.GITEA_TOKEN,
customFetch: fetch,
});
export default api;