Skip to content

Commit

Permalink
Merge pull request #17 from AElfProject/feature/github-support
Browse files Browse the repository at this point in the history
feat: github support
  • Loading branch information
yongenaelf authored Jul 29, 2024
2 parents 4a2049c + 99a4200 commit 1177cc7
Show file tree
Hide file tree
Showing 9 changed files with 682 additions and 20 deletions.
3 changes: 2 additions & 1 deletion .env
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
BUILD_SERVER_BASE_URL=https://playground.test.aelf.dev
GA_TAG=
GA_TAG=
GITHUB_API_KEY=
100 changes: 100 additions & 0 deletions app/https:/github.com/[user]/[repo]/tree/[branch]/[...path]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { ImportGitHubForm } from "@/components/import-github-form";
import { Octokit } from "octokit";
import { throttling } from "@octokit/plugin-throttling";
import { getGitHubToken } from "@/lib/env";

interface Params {
path: string[];
user: string;
repo: string;
branch: string;
}

const MyOctokit = Octokit.plugin(throttling);

const getData = async ({ path, user, repo, branch }: Params) => {
const octokit = new MyOctokit({
auth: getGitHubToken(),
throttle: {
onRateLimit: (retryAfter, options, octokit, retryCount) => {
octokit.log.warn(
`Request quota exhausted for request ${options.method} ${options.url}`
);

if (retryCount < 1) {
// only retries once
octokit.log.info(`Retrying after ${retryAfter} seconds!`);
return true;
}
},
onSecondaryRateLimit: (retryAfter, options, octokit) => {
// does not retry, only logs a warning
octokit.log.warn(
`SecondaryRateLimit detected for request ${options.method} ${options.url}`
);
},
},
});

const contents = await octokit.rest.repos.getContent({
owner: user,
repo,
ref: `heads/${branch}`,
path: path.join("/"),
});

const folder = contents.data;

if (!Array.isArray(folder)) {
return "Not a folder.";
}

const srcSha = folder?.find((i) => i.name === "src")?.sha;

if (!srcSha) return "No src folder found.";

const src = await octokit.rest.git.getTree({
owner: user,
repo,
tree_sha: srcSha,
recursive: "true",
});

const data = src.data.tree;

let response: { path: string; contents: string }[] = [];

if (Array.isArray(data)) {
const files = data.filter((i) => i.type === "blob");

for (const file of files) {
const blob = await octokit.rest.git.getBlob({
owner: user,
repo,
file_sha: file.sha!,
});

response.push({
path: `src/${file.path!}`,
contents: Buffer.from(blob.data.content, "base64").toString("ascii"),
});
}
}

return response;
};

export default async function Page({
params: { path, user, repo, branch },
}: {
params: Params;
}) {
const data = await getData({ path, user, repo, branch });

return (
<div className="container px-4 py-12 md:px-6 lg:py-16">
<h1 className="text-2xl mb-2">Import GitHub repository</h1>
{typeof data === "string" ? data : <ImportGitHubForm data={data} />}
</div>
);
}
8 changes: 5 additions & 3 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
import Link from "next/link";
import { ReactNode } from "react";
import { Skeleton } from "@/components/ui/skeleton";
import { RepoUrlForm } from "@/components/repo-url-form";

function HomeCard({
title,
Expand Down Expand Up @@ -81,14 +82,15 @@ export default function Home() {
</p>
</div>
<h3 className="text-xl font-bold">Choose from a template...</h3>
<div className="grid w-full max-w-3xl grid-cols-1 gap-6 md:grid-cols-2">
<div className="grid w-full grid-cols-1 gap-6 md:grid-cols-2">
{links.map((link) => (
<HomeCard key={link.title} {...link} />
))}
</div>

<h3 className="text-xl font-bold">... enter a GitHub repo url:</h3>
<RepoUrlForm />
<h3 className="text-xl font-bold">... or generate from a prompt:</h3>
<div className="grid w-full max-w-3xl grid-cols-1 gap-6 md:grid-cols-2">
<div className="grid w-full grid-cols-1 gap-6 md:grid-cols-2">
<div className="rounded-lg border bg-background p-6 shadow-sm">
<div className="flex flex-col space-y-3">
<Skeleton className="h-[125px] w-[250px] rounded-xl" />
Expand Down
122 changes: 122 additions & 0 deletions components/import-github-form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
"use client";

import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";

import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { useRouter, useParams } from "next/navigation";
import { db } from "@/data/db";
import { Loader2 } from "lucide-react";

const FormSchema = z.object({
name: z.string().min(2, {
message: "Name must be at least 2 characters.",
}),
template: z.string(),
});

export function ImportGitHubForm(props: {
data: { path: string; contents: string }[];
}) {
const { path, user, repo, branch } = useParams<{
path: string[];
user: string;
repo: string;
branch: string;
}>();
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: {
name: "",
template: `github.com/${user}/${repo}/tree/${branch}/${path.join("/")}`,
},
});

const router = useRouter();

async function onSubmit(data: z.infer<typeof FormSchema>) {
form.clearErrors();
try {
await db.workspaces.add({
name: data.name,
template: data.template,
dll: "",
});

await db.files.bulkAdd(
props.data.map(({ path, contents }) => ({
path: `/workspace/${data.name}/${encodeURIComponent(path)}`,
contents,
}))
);
await router.push(`/workspace/${data.name}`);
} catch (err) {
form.setError("name", { message: String(err) });
}
}

return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="w-full space-y-6">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input
className="w-full"
placeholder="workspace-name"
{...field}
/>
</FormControl>
<FormDescription>This is your workspace name.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="template"
render={({ field }) => (
<FormItem>
<FormLabel>Repo</FormLabel>
<FormControl>
<Input
className="w-full"
placeholder="workspace-name"
{...field}
disabled
/>
</FormControl>
<FormDescription>This is the source repo.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" disabled={form.formState.isSubmitting}>
{form.formState.isSubmitting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Please wait
</>
) : (
"Submit"
)}
</Button>
</form>
</Form>
);
}
70 changes: 70 additions & 0 deletions components/repo-url-form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"use client";

import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";

import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
FormField,
FormItem,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { useRouter } from "next/navigation";
import { Loader2 } from "lucide-react";

const FormSchema = z.object({
url: z.string(),
});

export function RepoUrlForm() {
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: {
url: "https://github.com/AElfProject/aelf-developer-tools/tree/master/templates/HelloWorldContract",
},
});

const router = useRouter();

async function onSubmit(data: z.infer<typeof FormSchema>) {
form.clearErrors();
try {
await router.push(`/${data.url}`);
} catch (err) {
form.setError("url", { message: String(err) });
}
}

return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="w-full flex">
<FormField
control={form.control}
name="url"
render={({ field }) => (
<FormItem className="flex-grow mr-3">
<FormControl>
<Input placeholder="workspace-name" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" disabled={form.formState.isSubmitting}>
{form.formState.isSubmitting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Please wait
</>
) : (
"Submit"
)}
</Button>
</form>
</Form>
);
}
31 changes: 15 additions & 16 deletions components/workspace/file-explorer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
import { db } from "@/data/db";
import Link from "next/link";
import { useParams } from "next/navigation";
import { useEffect, useState } from "react";
import useSWR from "swr";

type TOCProps = {
toc: TreeViewElement[];
Expand Down Expand Up @@ -96,23 +96,22 @@ function convert(data: string[]) {

const FileExplorer = () => {
const { id } = useParams();
const [paths, setPaths] = useState<string[]>([]);

useEffect(() => {
(async () => {
const files = await db.files.filter((file) =>
file.path.startsWith(`/workspace/${id}`)
);
const filesArray = await files.toArray();
setPaths(
filesArray.map((i) =>
decodeURIComponent(i.path.replace(`/workspace/${id}/`, ""))
)
);
})();
}, [id]);
const { data: toc } = useSWR(`file-explorer-${id}`, async () => {
const files = await db.files.filter((file) =>
file.path.startsWith(`/workspace/${id}`)
);
const filesArray = await files.toArray();
return convert(
filesArray.map((i) =>
decodeURIComponent(i.path.replace(`/workspace/${id}/`, ""))
)
);
});

return <TOC toc={convert(paths)} pathname={`/workspace/${id}`} />;
if (!toc) return <p>Loading...</p>;

return <TOC toc={toc} pathname={`/workspace/${id}`} />;
};

export default FileExplorer;
4 changes: 4 additions & 0 deletions lib/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,8 @@ export function getBuildServerBaseUrl() {

export function getGoogleAnalyticsTag() {
return getEnv("GA_TAG");
}

export function getGitHubToken() {
return getEnv("GITHUB_API_KEY");
}
Loading

0 comments on commit 1177cc7

Please sign in to comment.