Merge branch 'develop' of git.lumeweb.com:LumeWeb/web3toybox.com into develop

This commit is contained in:
Juan Di Toro 2023-10-31 12:50:05 +01:00
commit fff1df134c
5 changed files with 183 additions and 181 deletions

View File

@ -1,30 +1,30 @@
import { defineConfig } from "astro/config" import { defineConfig } from 'astro/config'
import * as fs from "node:fs" import * as fs from 'node:fs'
import react from "@astrojs/react" import react from '@astrojs/react'
import tailwind from "@astrojs/tailwind" import tailwind from '@astrojs/tailwind'
import optimizer from "vite-plugin-optimizer" import optimizer from 'vite-plugin-optimizer'
export default defineConfig({ export default defineConfig({
integrations: [react(), tailwind({ applyBaseStyles: false })], integrations: [react(), tailwind({ applyBaseStyles: false })],
vite: { vite: {
server: { server: process.env.MODE === 'development' ? {
https: { https: {
cert: fs.readFileSync("./.local-ssl/localhost.pem"), cert: fs.readFileSync('./.local-ssl/localhost.pem'),
key: fs.readFileSync("./.local-ssl/localhost-key.pem") key: fs.readFileSync('./.local-ssl/localhost-key.pem')
}, },
}, } : {},
build: { build: {
minify: false minify: false
}, },
resolve: { resolve: {
dedupe: ["@lumeweb/libportal", "@lumeweb/libweb", "@lumeweb/libkernel"] dedupe: ['@lumeweb/libportal', '@lumeweb/libweb', '@lumeweb/libkernel']
}, },
plugins: [ plugins: [
optimizer({ optimizer({
"node-fetch": 'node-fetch':
"const e = undefined; export default e;export {e as Response, e as FormData, e as Blob};" 'const e = undefined; export default e;export {e as Response, e as FormData, e as Blob};'
}) })
] ]
} }
}) })

4
package-lock.json generated
View File

@ -1,11 +1,11 @@
{ {
"name": "opposite-osiris", "name": "@lumeweb/web3toybox.com",
"version": "0.0.1", "version": "0.0.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "opposite-osiris", "name": "@lumeweb/web3toybox.com",
"version": "0.0.1", "version": "0.0.1",
"dependencies": { "dependencies": {
"@astrojs/react": "^3.0.3", "@astrojs/react": "^3.0.3",

View File

@ -1,5 +1,5 @@
{ {
"name": "opposite-osiris", "name": "@lumeweb/web3toybox.com",
"type": "module", "type": "module",
"version": "0.0.1", "version": "0.0.1",
"scripts": { "scripts": {

View File

@ -1,15 +1,15 @@
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button";
import "@lumeweb/sdk/lib/style.css" import "@lumeweb/sdk/lib/style.css";
import "@/styles/global.css" import "@/styles/global.css";
import React, { import React, {
createContext, createContext,
createRef, createRef,
type ReactNode, type ReactNode,
useContext, useContext,
useEffect, useEffect,
useState useState,
} from "react" } from "react";
import { import {
type AuthContextType, type AuthContextType,
AuthProvider, AuthProvider,
@ -22,53 +22,49 @@ import {
useAuth, useAuth,
useLumeStatus, useLumeStatus,
useNetworks, useNetworks,
LumeDashboardTrigger LumeDashboardTrigger,
} from "@lumeweb/sdk" } from "@lumeweb/sdk";
import * as kernel from "@lumeweb/libkernel/kernel" import * as kernel from "@lumeweb/libkernel/kernel";
import { kernelLoaded } from "@lumeweb/libkernel/kernel" import { kernelLoaded } from "@lumeweb/libkernel/kernel";
import { import {
dnsClient, dnsClient,
ethClient, ethClient,
ipfsClient, ipfsClient,
networkRegistryClient, networkRegistryClient,
peerDiscoveryClient, peerDiscoveryClient,
swarmClient swarmClient,
} from "@/lib/clients" } from "@/lib/clients";
import { ethers } from "ethers" import { ethers } from "ethers";
import * as ethersBytes from "@ethersproject/bytes" import * as ethersBytes from "@ethersproject/bytes";
import { createProvider } from "@lumeweb/kernel-eth-client" import { createProvider } from "@lumeweb/kernel-eth-client";
// @ts-ignore // @ts-ignore
import jdu from "json-data-uri" import jdu from "json-data-uri";
import { ERC721_ABI } from "@/lib/erc721-abi" import { ERC721_ABI } from "@/lib/erc721-abi";
import { import { Card, CardContent, CardHeader } from "@/components/ui/card";
Card, import LogoImg from "@/assets/lume-logo.png";
CardContent, let BOOT_FUNCTIONS: (() => Promise<any>)[] = [];
CardHeader,
} from "@/components/ui/card"
import LogoImg from "@/assets/lume-logo.png"
let BOOT_FUNCTIONS: (() => Promise<any>)[] = []
export const AppContext = createContext<any>(undefined) export const AppContext = createContext<any>(undefined);
export function useApp() { export function useApp() {
const context = useContext(AppContext) const context = useContext(AppContext);
if (!context) { if (!context) {
throw new Error("useApp must be used within an AppProvider") throw new Error("useApp must be used within an AppProvider");
} }
return context return context;
} }
interface AppProviderProps { interface AppProviderProps {
children: ReactNode children: ReactNode;
} }
const provider = createProvider() const provider = createProvider();
const ERC721_TRANSFER_EVENT_SIGNATURE = ethers.id( const ERC721_TRANSFER_EVENT_SIGNATURE = ethers.id(
"Transfer(address,address,uint256)" "Transfer(address,address,uint256)"
) );
async function findPotentialERC721Contracts( async function findPotentialERC721Contracts(
address: string address: string
@ -79,24 +75,24 @@ async function findPotentialERC721Contracts(
topics: [ topics: [
ERC721_TRANSFER_EVENT_SIGNATURE, ERC721_TRANSFER_EVENT_SIGNATURE,
null, null,
ethersBytes.hexZeroPad(address, 32) ethersBytes.hexZeroPad(address, 32),
] ],
}) });
const potentialContracts = new Set<string>() const potentialContracts = new Set<string>();
logs.forEach((log: any) => potentialContracts.add(log.address)) logs.forEach((log: any) => potentialContracts.add(log.address));
const confirmedERC721Contracts: string[] = [] const confirmedERC721Contracts: string[] = [];
for (let contractAddress of potentialContracts) { for (let contractAddress of potentialContracts) {
if (await isERC721(contractAddress)) { if (await isERC721(contractAddress)) {
confirmedERC721Contracts.push(contractAddress) confirmedERC721Contracts.push(contractAddress);
} }
} }
return confirmedERC721Contracts return confirmedERC721Contracts;
} }
const TRANSFER_EVENT_SIGNATURE = ethers.id("Transfer(address,address,uint256)") const TRANSFER_EVENT_SIGNATURE = ethers.id("Transfer(address,address,uint256)");
async function fetchTokensViaTransferEvent( async function fetchTokensViaTransferEvent(
address: string, address: string,
@ -109,93 +105,93 @@ async function fetchTokensViaTransferEvent(
topics: [ topics: [
TRANSFER_EVENT_SIGNATURE, TRANSFER_EVENT_SIGNATURE,
null, null,
ethersBytes.hexZeroPad(address, 32) ethersBytes.hexZeroPad(address, 32),
] ],
}) });
const tokenIds: number[] = [] const tokenIds: number[] = [];
logs.forEach((log) => { logs.forEach((log) => {
if (log.topics && log.topics.length === 4) { if (log.topics && log.topics.length === 4) {
const tokenIdBigNumber = ethers.toNumber(log.topics[3]) const tokenIdBigNumber = ethers.toNumber(log.topics[3]);
tokenIds.push(tokenIdBigNumber) tokenIds.push(tokenIdBigNumber);
} }
}) });
return tokenIds return tokenIds;
} }
async function fetchOwnedNFTs( async function fetchOwnedNFTs(
address: string, address: string,
confirmedERC721Contracts: string[] confirmedERC721Contracts: string[]
): Promise<{ contract: string; tokenId: number; metadata: any }[]> { ): Promise<{ contract: string; tokenId: number; metadata: any }[]> {
const ownedNFTs = [] const ownedNFTs = [];
for (let contractAddress of confirmedERC721Contracts) { for (let contractAddress of confirmedERC721Contracts) {
const contract = new ethers.Contract(contractAddress, ERC721_ABI, provider) const contract = new ethers.Contract(contractAddress, ERC721_ABI, provider);
let tokenIds: number[] = [] let tokenIds: number[] = [];
try { try {
const balance = await contract.balanceOf(address) const balance = await contract.balanceOf(address);
for (let i = 0; i < balance; i++) { for (let i = 0; i < balance; i++) {
const tokenId = await contract.tokenOfOwnerByIndex(address, i) const tokenId = await contract.tokenOfOwnerByIndex(address, i);
tokenIds.push(tokenId.toNumber()) tokenIds.push(tokenId.toNumber());
} }
} catch (error) { } catch (error) {
// If tokenOfOwnerByIndex is not available, fall back to fetchTokensViaTransferEvent // If tokenOfOwnerByIndex is not available, fall back to fetchTokensViaTransferEvent
tokenIds = await fetchTokensViaTransferEvent(address, contractAddress) tokenIds = await fetchTokensViaTransferEvent(address, contractAddress);
} }
for (let tokenId of tokenIds) { for (let tokenId of tokenIds) {
try { try {
const uri = await contract.tokenURI(tokenId) const uri = await contract.tokenURI(tokenId);
// const metadata = await fetchMetadataFromURI(uri); // const metadata = await fetchMetadataFromURI(uri);
ownedNFTs.push({ ownedNFTs.push({
contract: contractAddress, contract: contractAddress,
tokenId: tokenId, tokenId: tokenId,
metadata: uri metadata: uri,
}) });
} catch (error: any) { } catch (error: any) {
console.error( console.error(
`Failed to fetch metadata for token ${tokenId} from contract ${contractAddress}: ${error.message}` `Failed to fetch metadata for token ${tokenId} from contract ${contractAddress}: ${error.message}`
) );
} }
} }
} }
return ownedNFTs return ownedNFTs;
} }
async function isERC721(address: string): Promise<boolean> { async function isERC721(address: string): Promise<boolean> {
const contract = new ethers.Contract(address, ERC721_ABI, provider) const contract = new ethers.Contract(address, ERC721_ABI, provider);
try { try {
// Try calling some ERC-721 methods to confirm if this is an ERC-721 contract. // Try calling some ERC-721 methods to confirm if this is an ERC-721 contract.
await contract.name() await contract.name();
await contract.symbol() await contract.symbol();
return true return true;
} catch (error) { } catch (error) {
return false return false;
} }
} }
const AppProvider: React.FC<AppProviderProps> = ({ children }) => { const AppProvider: React.FC<AppProviderProps> = ({ children }) => {
return <AppContext.Provider value={{}}>{children}</AppContext.Provider> return <AppContext.Provider value={{}}>{children}</AppContext.Provider>;
} };
async function boot(status: LumeStatusContextType, auth: AuthContextType) { async function boot(status: LumeStatusContextType, auth: AuthContextType) {
kernel.init().then(() => { kernel.init().then(() => {
status.setInited(true) status.setInited(true);
}) });
await kernelLoaded() await kernelLoaded();
auth.setIsLoggedIn(true) auth.setIsLoggedIn(true);
BOOT_FUNCTIONS.push( BOOT_FUNCTIONS.push(
async () => async () =>
await swarmClient.addRelay( await swarmClient.addRelay(
"2d7ae1517caf4aae4de73c6d6f400765d2dd00b69d65277a29151437ef1c7d1d" "2d7ae1517caf4aae4de73c6d6f400765d2dd00b69d65277a29151437ef1c7d1d"
) )
) );
// IRC // IRC
BOOT_FUNCTIONS.push( BOOT_FUNCTIONS.push(
@ -203,40 +199,40 @@ async function boot(status: LumeStatusContextType, auth: AuthContextType) {
await peerDiscoveryClient.register( await peerDiscoveryClient.register(
"zrjHTx8tSQFWnmZ9JzK7XmJirqJQi2WRBLYp3fASaL2AfBQ" "zrjHTx8tSQFWnmZ9JzK7XmJirqJQi2WRBLYp3fASaL2AfBQ"
) )
) );
BOOT_FUNCTIONS.push( BOOT_FUNCTIONS.push(
async () => await networkRegistryClient.registerType("content") async () => await networkRegistryClient.registerType("content")
) );
BOOT_FUNCTIONS.push( BOOT_FUNCTIONS.push(
async () => await networkRegistryClient.registerType("blockchain") async () => await networkRegistryClient.registerType("blockchain")
) );
BOOT_FUNCTIONS.push(async () => await ethClient.register()) BOOT_FUNCTIONS.push(async () => await ethClient.register());
BOOT_FUNCTIONS.push(async () => await ipfsClient.register()) BOOT_FUNCTIONS.push(async () => await ipfsClient.register());
const resolvers = [ const resolvers = [
"zrjEYq154PS7boERAbRAKMyRGzAR6CTHVRG6mfi5FV4q9FA" // ENS "zrjEYq154PS7boERAbRAKMyRGzAR6CTHVRG6mfi5FV4q9FA", // ENS
] ];
for (const resolver of resolvers) { for (const resolver of resolvers) {
BOOT_FUNCTIONS.push(async () => dnsClient.registerResolver(resolver)) BOOT_FUNCTIONS.push(async () => dnsClient.registerResolver(resolver));
} }
BOOT_FUNCTIONS.push(async () => status.setReady(true)) BOOT_FUNCTIONS.push(async () => status.setReady(true));
await bootup() await bootup();
await Promise.all([ethClient.ready(), ipfsClient.ready()]) await Promise.all([ethClient.ready(), ipfsClient.ready()]);
} }
async function bootup() { async function bootup() {
for (const entry of Object.entries(BOOT_FUNCTIONS)) { for (const entry of Object.entries(BOOT_FUNCTIONS)) {
console.log(entry[1].toString()) console.log(entry[1].toString());
await entry[1]() await entry[1]();
} }
} }
function LoginDash() { function LoginDash() {
const { isLoggedIn } = useAuth() const { isLoggedIn } = useAuth();
const { ready, inited } = useLumeStatus() const { ready, inited } = useLumeStatus();
return ( return (
<> <>
@ -259,130 +255,130 @@ function LoginDash() {
</LumeDashboard> </LumeDashboard>
)} )}
</> </>
) );
} }
async function asyncIterableToUint8Array(asyncIterable: any) { async function asyncIterableToUint8Array(asyncIterable: any) {
const chunks = [] const chunks = [];
let totalLength = 0 let totalLength = 0;
for await (const chunk of asyncIterable) { for await (const chunk of asyncIterable) {
chunks.push(chunk) chunks.push(chunk);
totalLength += chunk.length totalLength += chunk.length;
} }
const result = new Uint8Array(totalLength) const result = new Uint8Array(totalLength);
let offset = 0 let offset = 0;
for (const chunk of chunks) { for (const chunk of chunks) {
result.set(chunk, offset) result.set(chunk, offset);
offset += chunk.length offset += chunk.length;
} }
return result return result;
} }
function uint8ArrayToBase64(byteArray: Uint8Array) { function uint8ArrayToBase64(byteArray: Uint8Array) {
let base64 = "" let base64 = "";
const characters = const characters =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let padding = 0 let padding = 0;
for (let i = 0; i < byteArray.length; i += 3) { for (let i = 0; i < byteArray.length; i += 3) {
const a = byteArray[i] const a = byteArray[i];
const b = byteArray[i + 1] const b = byteArray[i + 1];
const c = byteArray[i + 2] const c = byteArray[i + 2];
const triplet = (a << 16) + ((b || 0) << 8) + (c || 0) const triplet = (a << 16) + ((b || 0) << 8) + (c || 0);
base64 += characters.charAt((triplet & 0xfc0000) >> 18) base64 += characters.charAt((triplet & 0xfc0000) >> 18);
base64 += characters.charAt((triplet & 0x03f000) >> 12) base64 += characters.charAt((triplet & 0x03f000) >> 12);
base64 += characters.charAt((triplet & 0x000fc0) >> 6) base64 += characters.charAt((triplet & 0x000fc0) >> 6);
base64 += characters.charAt(triplet & 0x00003f) base64 += characters.charAt(triplet & 0x00003f);
if (byteArray.length - i < 3) { if (byteArray.length - i < 3) {
padding = 3 - (byteArray.length - i) padding = 3 - (byteArray.length - i);
} }
} }
// Add padding if necessary // Add padding if necessary
if (padding > 0) { if (padding > 0) {
base64 = base64.slice(0, -padding) + (padding === 1 ? "=" : "==") base64 = base64.slice(0, -padding) + (padding === 1 ? "=" : "==");
} }
return base64 return base64;
} }
function App() { function App() {
const status = useLumeStatus() const status = useLumeStatus();
const auth = useAuth() const auth = useAuth();
const [nftList, setNftList] = useState<any[]>([]) const [nftList, setNftList] = useState<any[]>([]);
useEffect(() => { useEffect(() => {
boot(status, auth) boot(status, auth);
}, []) }, []);
const { networks } = useNetworks() const { networks } = useNetworks();
const ipfsStatus = networks const ipfsStatus = networks
.filter((item) => item.name.toLowerCase() === "ipfs") .filter((item) => item.name.toLowerCase() === "ipfs")
?.pop() ?.pop();
const ethStatus = networks const ethStatus = networks
.filter((item) => item.name.toLowerCase() === "ethereum") .filter((item) => item.name.toLowerCase() === "ethereum")
?.pop() ?.pop();
const ready = ethStatus?.ready && status.ready const ready = ethStatus?.ready && status.ready;
const inputRef = createRef<HTMLInputElement>() const inputRef = createRef<HTMLInputElement>();
async function search(e: any | Event) { async function search(e: any | Event) {
e.preventDefault() e.preventDefault();
let address = inputRef?.current?.value as string let address = inputRef?.current?.value as string;
address = await ethers.resolveAddress(address, provider) address = await ethers.resolveAddress(address, provider);
const contracts = await findPotentialERC721Contracts(address) const contracts = await findPotentialERC721Contracts(address);
const nfts = await fetchOwnedNFTs(address, contracts) const nfts = await fetchOwnedNFTs(address, contracts);
const list = [] const list = [];
for (const nft of nfts) { for (const nft of nfts) {
let meta let meta;
if (typeof nft.metadata === "string") { if (typeof nft.metadata === "string") {
try { try {
meta = await (await fetch(nft.metadata)).json() meta = await (await fetch(nft.metadata)).json();
} catch (e) { } catch (e) {
meta = { meta = {
image: "", // TODO: Improve this by bringing an actual image image: "", // TODO: Improve this by bringing an actual image
name: "Failed to Load", name: "Failed to Load",
fail: true fail: true,
} };
} }
} else { } else {
meta = jdu.parse(nft.metadata) meta = jdu.parse(nft.metadata);
} }
let image let image;
if (!meta.fail) { if (!meta.fail) {
const imageCID = meta.image.replace("ipfs://", "") const imageCID = meta.image.replace("ipfs://", "");
image = await asyncIterableToUint8Array( image = await asyncIterableToUint8Array(
ipfsClient.cat(imageCID).iterable() ipfsClient.cat(imageCID).iterable()
) );
} else { } else {
image = meta.image image = meta.image;
} }
list.push({ list.push({
image, image,
name: meta.name, name: meta.name,
base64: meta.fail base64: meta.fail,
}) });
setNftList(list) setNftList(list);
} }
} }
@ -418,7 +414,7 @@ function App() {
</svg> </svg>
<Input <Input
className="pl-10 w-full bg-zinc-900 border-zinc-700 text-white ring-offset-primary" className="pl-10 w-full bg-zinc-900 border-zinc-700 text-white ring-offset-primary"
placeholder="Introduce ETH Address or ENS. eg: 0x00...ABC or vitalik.ens" placeholder="Introduce ETH Address or ENS. eg: 0x00...ABC or vitalik.eth"
type="search" type="search"
disabled={!ready} disabled={!ready}
ref={inputRef} ref={inputRef}
@ -433,7 +429,13 @@ function App() {
{auth.isLoggedIn && !ethStatus?.ready ? ( {auth.isLoggedIn && !ethStatus?.ready ? (
<span className="max-w-4xl w-full block my-1 p-4 rounded-lg opacity-80 bg-yellow-900/70 border border-yellow-500 text-yellow-500"> <span className="max-w-4xl w-full block my-1 p-4 rounded-lg opacity-80 bg-yellow-900/70 border border-yellow-500 text-yellow-500">
You'll need to wait for a couple minutes before we can start You'll need to wait for a couple minutes before we can start
searching. You are currently locally syncing to the ETH network. <b className="font-bold">Current Progress: {ethStatus?.sync ? `${ethStatus?.sync.toLocaleString()}%` : 'Initializing...'}</b> searching. You are currently locally syncing to the ETH network.{" "}
<b className="font-bold">
Current Progress:{" "}
{ethStatus?.sync
? `${ethStatus?.sync.toLocaleString()}%`
: "Initializing..."}
</b>
</span> </span>
) : null} ) : null}
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4"> <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4">
@ -453,7 +455,7 @@ function App() {
))} ))}
</div> </div>
</div> </div>
) );
} }
export default function () { export default function () {
@ -467,5 +469,5 @@ export default function () {
</AuthProvider> </AuthProvider>
</LumeStatusProvider> </LumeStatusProvider>
</AppProvider> </AppProvider>
) );
} }

View File

@ -23,8 +23,8 @@ import '@/styles/global.css'
<Card className="max-w-3xl bg-zinc-950 border-zinc-800 shadow-xl"> <Card className="max-w-3xl bg-zinc-950 border-zinc-800 shadow-xl">
<CardHeader> <CardHeader>
<img src={LogoImg.src} class="w-20 mb-8" /> <img src={LogoImg.src} class="w-20 mb-8" />
<CardTitle className='text-white'>Welcome to Web3Toybox</CardTitle> <CardTitle className='text-white'>Welcome to Web3 Toybox</CardTitle>
<CardDescription className='text-zinc-500 text-lg'>This is a place where you can play with different famous web3 apps in a truly decentralized way - no infura, no gateways, no censorship.</CardDescription> <CardDescription className='text-zinc-500 text-lg'>This is a place where you can play with different web3 use cases by example in a truly decentralized way - no infura, no gateways, no censorship.</CardDescription>
</CardHeader> </CardHeader>
<CardContent className='flex flex-col md:flex-row gap-y-5 md:gap-y-0 justify-between gap-x-5'> <CardContent className='flex flex-col md:flex-row gap-y-5 md:gap-y-0 justify-between gap-x-5'>
<Card className="md:max-w-[400px] w-full bg-zinc-900 border-zinc-800 hover:shadow-lg hover:ring-1 hover:ring-green-400/20 hover:shadow-green-400/20 hover:transform-gpu hover:-translate-y-[3px] transition-all duration-150"> <Card className="md:max-w-[400px] w-full bg-zinc-900 border-zinc-800 hover:shadow-lg hover:ring-1 hover:ring-green-400/20 hover:shadow-green-400/20 hover:transform-gpu hover:-translate-y-[3px] transition-all duration-150">
@ -43,12 +43,12 @@ import '@/styles/global.css'
</Button> </Button>
</a> </a>
</CardFooter> </CardFooter>
</Card> </Card>
<Card className="w-full max-w-[300px] bg-zinc-950 border-zinc-700 border-dashed"> <Card className="w-full max-w-[300px] bg-zinc-950 border-zinc-700 border-dashed">
<CardHeader> <CardHeader>
<CardTitle className='text-zinc-400 mb-3'>More Coming Soon...</CardTitle> <CardTitle className='text-zinc-400 mb-3'>More Coming Soon...</CardTitle>
<CardDescription className='text-zinc-400'> <CardDescription className='text-zinc-400'>
We're diligently developing more intriguing examples. If you have any suggestions, don't hesitate to join our Discord channel and propose or submit any examples you'd like to see here! We're are definitely interested in new ideas for demos! If you have any suggestions, don't hesitate to join our Discord channel and propose or submit any examples you'd like to see here!
<a href="https://discord.com/invite/qpC8ADp3rS"> <a href="https://discord.com/invite/qpC8ADp3rS">
<Button variant="link" className='h-auto p-0 inline pl-1'> <Button variant="link" className='h-auto p-0 inline pl-1'>
Join Discord Join Discord
@ -56,13 +56,13 @@ import '@/styles/global.css'
</a> </a>
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
</Card> </Card>
</CardContent> </CardContent>
<CardFooter className='flex flex-col space-y-3'> <CardFooter className='flex flex-col space-y-3'>
<span class="max-w-4xl w-full block my-2 p-4 rounded-lg opacity-80 bg-gray-900/70 border border-gray-600 text-gray-400"> <span class="max-w-4xl w-full block my-2 p-4 rounded-lg opacity-80 bg-gray-900/70 border border-gray-600 text-gray-400">
For an enhanced experience, we recommend opening the developer console while interacting with these demos. If you're unsure how to do this, you can find instructions <Button variant="link" className="inline p-0 h-auto w-auto text-md"><a href="https://support.google.com/campaignmanager/answer/2828688">here</a></Button>. For an enhanced experience, we recommend opening the developer console while interacting with these demos. If you're unsure how to do this, you can find instructions <Button variant="link" className="inline p-0 h-auto w-auto text-md"><a href="https://support.google.com/campaignmanager/answer/2828688">here</a></Button>.
</span> </span>
<p class="text-zinc-700 text-sm">Brought to you with 💚 by the <a href="https://lumeweb.com" class="text-zinc-500 underline">lumeweb.com</a> team alongside the <a href="https://sia.tech/about-sia-foundation" class="text-zinc-500 underline">Sia Foundation</a></p> <p class="text-zinc-700 text-sm">Brought to you with 💚 by the <a href="https://lumeweb.com" class="text-zinc-500 underline">Lume</a> team, and grant sponsored by the <a href="https://sia.tech/about-sia-foundation" class="text-zinc-500 underline">Sia Foundation</a></p>
</CardFooter> </CardFooter>
</Card> </Card>
</div> </div>