Skip to content

Commit

Permalink
Extensions list (#35)
Browse files Browse the repository at this point in the history
  • Loading branch information
Pabl0cks authored Sep 27, 2024
1 parent 0fb5d26 commit 804b47e
Show file tree
Hide file tree
Showing 7 changed files with 302 additions and 5 deletions.
1 change: 1 addition & 0 deletions packages/nextjs/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@
# More info: https://nextjs.org/docs/pages/building-your-application/configuring/environment-variables
NEXT_PUBLIC_ALCHEMY_API_KEY=
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=
BGAPP_API_URL=
99 changes: 99 additions & 0 deletions packages/nextjs/components/ExtensionCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { useState } from "react";
import Image from "next/image";
import { usePlausible } from "next-plausible";
import CopyToClipboard from "react-copy-to-clipboard";
import { CheckCircleIcon, DocumentDuplicateIcon } from "@heroicons/react/24/outline";
import { Address } from "~~/components/scaffold-eth";

type Extension = {
name: string;
description: string;
github: string;
installCommand: string;
builder: string;
coBuilders: string[];
youtube?: string;
};

export const ExtensionCard = ({ extension, isCurated }: { extension: Extension; isCurated: boolean }) => {
const [commandCopied, setCommandCopied] = useState(false);
const plausible = usePlausible();

const handleInstallClick = () => {
setCommandCopied(true);
setTimeout(() => {
setCommandCopied(false);
}, 800);

const githubRepo = isCurated
? `Curated/${extension.name}`
: extension.github.split("github.com/")[1] || extension.github;

// Track the click event with GitHub repo as a prop
plausible("extensionCopyClick", { props: { id: githubRepo } });
};

return (
<div className="card bg-base-100 shadow-xl mb-8 flex flex-col mx-1">
<div className="card-body flex-grow">
<h2 className="card-title">{extension.name}</h2>
<div className="flex justify-between items-start mb-2">
<div className="flex items-center gap-2">
{extension.github && (
<a href={extension.github} className="inline-block" target="_blank" rel="noopener noreferrer">
<Image src="/icon-github.svg" alt="github icon" width={24} height={24} />
</a>
)}
{extension.youtube && (
<a href={extension.youtube} className="inline-block" target="_blank" rel="noopener noreferrer">
<Image src="/icon-youtube.svg" alt="youtube icon" width={24} height={24} />
</a>
)}
</div>
{isCurated ? (
<div className="badge badge-secondary p-3">Curated</div>
) : (
<div>
<Address address={extension.builder} disableAddressLink />
{extension.coBuilders && extension.coBuilders.length > 0 && (
<div className="text-sm mt-2">
{extension.coBuilders.map((coBuilder, index) => (
<Address key={index} address={coBuilder} disableAddressLink />
))}
</div>
)}
</div>
)}
</div>
<p
className="overflow-hidden flex-grow"
style={{
display: "-webkit-box",
WebkitBoxOrient: "vertical",
WebkitLineClamp: 5,
maxHeight: "7.5em",
}}
>
{extension.description}
</p>
{!isCurated && (
<div className="mt-2 text-sm text-yellow-600 bg-yellow-100 p-2 rounded">
⚠️ 3rd-party extension. Verify the source before installing.
</div>
)}
</div>
<div className="card-actions mx-4 p-4 pt-0 pb-6 mt-auto">
<CopyToClipboard text={extension.installCommand} onCopy={handleInstallClick}>
<div className="flex items-center border-2 border-gray-300 rounded-xl px-3 sm:px-5 py-1 gap-2 cursor-pointer w-full">
<p className="m-0 text-center text-sm xl:text-base flex-grow">{extension.installCommand}</p>
{commandCopied ? (
<CheckCircleIcon className="text-xl font-normal h-6 w-4 flex-shrink-0" aria-hidden="true" />
) : (
<DocumentDuplicateIcon className="text-xl font-normal h-6 w-4 flex-shrink-0" aria-hidden="true" />
)}
</div>
</CopyToClipboard>
</div>
</div>
);
};
144 changes: 144 additions & 0 deletions packages/nextjs/pages/extensions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { useState } from "react";
import Image from "next/image";
import Link from "next/link";
import type { GetStaticProps, NextPage } from "next";
import { MagnifyingGlassIcon } from "@heroicons/react/24/solid";
import { ExtensionCard } from "~~/components/ExtensionCard";
import { MetaHeader } from "~~/components/MetaHeader";
import curatedExtensions from "~~/public/extensions.json";

const BGAPP_API_URL = process.env.BGAPP_API_URL;

type Extension = {
name: string;
description: string;
github: string;
installCommand: string;
builder: string;
coBuilders: string[];
youtube?: string;
};

interface ExtensionsListProps {
thirdPartyExtensions: Extension[];
}

const ExtensionsList: NextPage<ExtensionsListProps> = ({ thirdPartyExtensions }) => {
const [searchQuery, setSearchQuery] = useState("");

const allExtensions = [...curatedExtensions.curated, ...thirdPartyExtensions];

const filteredExtensions = allExtensions.filter(extension => {
if (searchQuery.length < 3) return true;
const lowerCaseSearch = searchQuery.toLowerCase();
return (
extension.name.toLowerCase().includes(lowerCaseSearch) ||
extension.description.toLowerCase().includes(lowerCaseSearch)
);
});

return (
<>
<MetaHeader
title="Extensions List | Scaffold-ETH 2"
description="List of available extensions for Scaffold-ETH 2"
/>
<div className="container mx-auto p-4 min-h-screen flex flex-col -mb-16">
{/* Header section */}
<div className="flex items-center justify-between mb-8">
<Link href="/" className="flex items-center gap-2">
<div className="flex relative w-8 sm:w-10 h-8 sm:h-10">
<Image alt="SE2 logo" className="cursor-pointer" fill src="/logo.svg" />
</div>
<span className="text-xl sm:text-2xl font-medium">Scaffold-ETH 2</span>
</Link>
<Link href="/" className="btn btn-sm btn-ghost">
Back to Home
</Link>
</div>

<div className="flex flex-col items-center justify-center gap-4 mb-4">
<h1 className="text-3xl md:text-4xl font-bold text-center">Extensions List</h1>
<div className="relative w-full max-w-xs">
<input
type="text"
placeholder="Search extensions"
className="input input-bordered w-full pr-10 text-sm md:text-base"
onChange={e => setSearchQuery(e.target.value)}
/>
<MagnifyingGlassIcon className="absolute right-3 top-1/2 transform -translate-y-1/2 h-4 w-4 md:h-5 md:w-5 text-gray-400" />
</div>
</div>
<p className="text-base md:text-lg mb-8 text-center max-w-4xl mx-auto">
Explore our Curated (by BuidlGuidl) and community-contributed extensions for Scaffold-ETH 2.{" "}
<br className="hidden md:inline"></br> To install an extension, simply copy and run the installation command
provided for each extension.
</p>

{/* Combined extensions list */}
<div className="flex-grow">
{filteredExtensions.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 pb-16">
{filteredExtensions.map((extension, index) => (
<ExtensionCard
key={index}
extension={extension}
isCurated={curatedExtensions.curated.includes(extension)}
/>
))}
</div>
) : (
<div className="flex items-center justify-center flex-grow">
<p className="text-center text-lg font-light">- No extensions found matching your search -</p>
</div>
)}
</div>
</div>
</>
);
};

// get third party extensions from buidlguidl app (builds with "extension" type)
export const getStaticProps: GetStaticProps<ExtensionsListProps> = async () => {
try {
if (!BGAPP_API_URL) {
throw new Error("BGAPP_API_URL environment variable is not set");
}

const response = await fetch(`${BGAPP_API_URL}/builds?type=extension`);
const data = await response.json();
const formattedExtensions = data.map((ext: any) => {
const githubUrlParts = ext.branch.split("/");
const githubUsername = githubUrlParts[3];
const repoName = githubUrlParts[4];

return {
name: ext.name,
description: ext.desc,
github: ext.branch,
installCommand: `npx create-eth@latest -e ${githubUsername}/${repoName}`,
builder: ext.builder,
coBuilders: ext.coBuilders || [],
youtube: ext.videoUrl || null,
};
});

return {
props: {
thirdPartyExtensions: formattedExtensions,
},
// Revalidate every 6 hours (21600 seconds)
revalidate: 21600,
};
} catch (error) {
console.error("Error fetching third-party extensions:", error);
return {
props: {
thirdPartyExtensions: [],
},
revalidate: 21600,
};
}
};

export default ExtensionsList;
17 changes: 12 additions & 5 deletions packages/nextjs/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -234,11 +234,18 @@ const Home: NextPage = () => {
</div>
</CopyToClipboard>
</div>
<p className="m-auto text-center lg:text-left lg:mx-0 max-w-[400px] lg:max-w-none lg:pr-6 link">
<TrackedLink id="Extensions" href="https://docs.scaffoldeth.io/extensions/">
Learn more about extensions
</TrackedLink>
</p>
<div className="space-y-3">
<p className="m-auto text-center lg:text-left lg:mx-0 max-w-[400px] lg:max-w-none lg:pr-6 link">
<TrackedLink id="Extensions" href="https://docs.scaffoldeth.io/extensions/">
Learn more about extensions
</TrackedLink>
</p>
<p className="m-auto text-center lg:text-left lg:mx-0 max-w-[400px] lg:max-w-none lg:pr-6 link">
<TrackedLink id="ExtensionsList" href="/extensions">
Check out all the available extensions
</TrackedLink>
</p>
</div>
</div>
</div>
</div>
Expand Down
44 changes: 44 additions & 0 deletions packages/nextjs/public/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"curated": [
{
"name": "subgraph",
"description": "This Scaffold-ETH 2 extension helps you build and test subgraphs locally for your contracts. It also enables interaction with the front-end and facilitates easy deployment to Subgraph Studio.",
"github": "https://github.com/scaffold-eth/create-eth-extensions/tree/subgraph",
"installCommand": "npx create-eth@latest -e subgraph",
"builder": "0x1A2d838c4bbd1e73d162d0777d142c1d783Cb831",
"coBuilders": []
},
{
"name": "eip-712",
"description": "An implementation of EIP-712, allowing you to send, sign, and verify typed messages in a user-friendly manner.",
"github": "https://github.com/scaffold-eth/create-eth-extensions/tree/eip-712",
"installCommand": "npx create-eth@latest -e eip-712",
"builder": "0x1A2d838c4bbd1e73d162d0777d142c1d783Cb831",
"coBuilders": []
},
{
"name": "ponder",
"description": "This Scaffold-ETH 2 extension comes pre-configured with ponder.sh, providing an example to help you get started quickly.",
"github": "https://github.com/scaffold-eth/create-eth-extensions/tree/ponder",
"installCommand": "npx create-eth@latest -e ponder",
"builder": "0x5dCb5f4F39Caa6Ca25380cfc42280330b49d3c93",
"coBuilders": []
},
{
"name": "onchainkit",
"description": "This Scaffold-ETH 2 extension comes pre-configured with onchainkit, providing an example to help you get started quickly.",
"github": "https://github.com/scaffold-eth/create-eth-extensions/tree/onchainkit",
"installCommand": "npx create-eth@latest -e onchainkit",
"builder": "0x45334F41aAA464528CD5bc0F582acadC49Eb0Cd1",
"coBuilders": []
},
{
"name": "erc-20",
"description": "This extension introduces an ERC-20 token contract and demonstrates how to interact with it, including getting a holder balance and transferring tokens.",
"github": "https://github.com/scaffold-eth/create-eth-extensions/tree/erc-20",
"installCommand": "npx create-eth@latest -e erc-20",
"builder": "0x5dCb5f4F39Caa6Ca25380cfc42280330b49d3c93",
"coBuilders": ["0x60583563D5879C2E59973E5718c7DE2147971807"]
}
]
}
1 change: 1 addition & 0 deletions packages/nextjs/public/icon-github.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions packages/nextjs/public/icon-youtube.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 804b47e

Please sign in to comment.