Compare commits

...

No commits in common. "develop" and "ditorodev" have entirely different histories.

49 changed files with 13103 additions and 29314 deletions

6
.eslintrc.json Normal file
View File

@ -0,0 +1,6 @@
{
"extends": [
"next/core-web-vitals",
"plugin:storybook/recommended"
]
}

39
.gitignore vendored
View File

@ -1,2 +1,37 @@
node_modules
.local-ssl/
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
storybook.log

52
.storybook/main.ts Normal file
View File

@ -0,0 +1,52 @@
import type { StorybookConfig } from '@storybook/nextjs';
import path from 'path';
import TsConfigPathsPlugin from 'tsconfig-paths-webpack-plugin';
const config: StorybookConfig = {
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
webpackFinal: async (config: any) => {
config.module = config.module || {};
config.module.rules = config.module.rules || [];
config.resolve.plugins = config.resolve.plugins || [];
// This modifies the existing image rule to exclude .svg files
// since you want to handle those files with @svgr/webpack
const imageRule = config.module.rules.find((rule) => rule?.['test']?.test('.svg'));
if (imageRule) {
imageRule['exclude'] = /\.svg$/;
}
// Configure .svg files to be loaded with @svgr/webpack
config.module.rules.push({
test: /\.svg$/,
use: ['@svgr/webpack'],
});
config.resolve.plugins = config.resolve.plugins || [];
config.resolve.plugins.push(
new TsConfigPathsPlugin({
configFile: path.resolve(__dirname, "../tsconfig.json"),
})
);
return config;
},
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-onboarding',
'@storybook/addon-interactions',
{
name: '@storybook/addon-styling',
options: {},
},
],
framework: {
name: '@storybook/nextjs',
options: {},
},
docs: {
autodocs: 'tag',
},
};
export default config;

31
.storybook/preview.ts Normal file
View File

@ -0,0 +1,31 @@
import type { Preview } from '@storybook/react';
import { withThemeByClassName } from '@storybook/addon-styling';
import '../src/styles/globals.css';
const preview: Preview = {
parameters: {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
},
decorators: [
// Adds theme switching support.
// NOTE: requires setting "darkMode" to "class" in your tailwind config
withThemeByClassName({
themes: {
light: 'light',
dark: 'dark',
},
defaultTheme: 'light',
}),
],
};
export default preview;

View File

@ -1,9 +0,0 @@
MIT License
Copyright (c) 2023 Hammer Technologies LLC
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

38
README.md Normal file
View File

@ -0,0 +1,38 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file.
[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`.
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

View File

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

View File

@ -1,11 +1,11 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": false,
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "src/styles/global.css",
"config": "tailwind.config.ts",
"css": "src/styles/globals.css",
"baseColor": "slate",
"cssVariables": true
},

6
next.config.js Normal file
View File

@ -0,0 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
}
module.exports = nextConfig

27292
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,42 +1,51 @@
{
"name": "@lumeweb/web3toybox.com",
"type": "module",
"version": "0.0.1",
"name": "lume-nft-explorer",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro",
"local-setup-https": "mkdir -p ./.local-ssl && mkcert -key-file ./.local-ssl/localhost-key.pem -cert-file ./.local-ssl/localhost.pem localhost"
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
},
"dependencies": {
"@astrojs/react": "^3.0.3",
"@astrojs/tailwind": "^5.0.2",
"@ethersproject/bytes": "^5.7.0",
"@lumeweb/kernel-dns-client": "^0.1.0-develop.7",
"@lumeweb/kernel-eth-client": "^0.1.0-develop.17",
"@lumeweb/kernel-handshake-client": "^0.1.0-develop.9",
"@lumeweb/kernel-ipfs-client": "^0.1.0-develop.25",
"@lumeweb/kernel-peer-discovery-client": "^0.0.2-develop.18",
"@lumeweb/kernel-swarm-client": "^0.1.0-develop.12",
"@lumeweb/sdk": "^0.1.0-develop.44",
"@radix-ui/react-dialog": "^1.0.4",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-slot": "^1.0.2",
"@types/react": "^18.2.31",
"@types/react-dom": "^18.2.14",
"astro": "^3.3.2",
"@radix-ui/react-toast": "^1.1.4",
"@types/node": "20.5.3",
"@types/react": "18.2.21",
"@types/react-dom": "18.2.7",
"autoprefixer": "10.4.15",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"json-data-uri": "^0.2.0",
"lucide-react": "^0.288.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"eslint": "8.47.0",
"eslint-config-next": "13.4.19",
"framer-motion": "^10.16.4",
"next": "13.4.19",
"postcss": "8.4.28",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-hook-form": "^7.46.1",
"tailwind-merge": "^1.14.0",
"tailwindcss": "^3.3.3",
"tailwindcss-animate": "^1.0.7"
"tailwindcss": "3.3.3",
"tailwindcss-animate": "^1.0.7",
"typescript": "5.1.6"
},
"devDependencies": {
"patch-package": "^8.0.0",
"vite-plugin-optimizer": "^1.4.2"
"@storybook/addon-essentials": "^7.4.0",
"@storybook/addon-interactions": "^7.4.0",
"@storybook/addon-links": "^7.4.0",
"@storybook/addon-onboarding": "^1.0.8",
"@storybook/addon-styling": "^1.3.7",
"@storybook/blocks": "^7.4.0",
"@storybook/nextjs": "^7.4.0",
"@storybook/react": "^7.4.0",
"@storybook/testing-library": "^0.2.0",
"@svgr/webpack": "^8.1.0",
"storybook": "^7.4.0",
"tsconfig-paths-webpack-plugin": "^4.1.0"
}
}

11319
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

1
public/next.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

1
public/vercel.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>

After

Width:  |  Height:  |  Size: 629 B

118
src/app/page.tsx Normal file
View File

@ -0,0 +1,118 @@
import Image from 'next/image'
import { Inter } from 'next/font/google'
const inter = Inter({ subsets: ['latin'] })
export default function Home() {
return (
<main
className={`flex min-h-screen flex-col items-center justify-between p-24 ${inter.className}`}
>
<div className="z-10 max-w-5xl w-full items-center justify-between font-mono text-sm lg:flex">
<p className="fixed left-0 top-0 flex w-full justify-center border-b border-gray-300 bg-gradient-to-b from-zinc-200 pb-6 pt-8 backdrop-blur-2xl dark:border-neutral-800 dark:bg-zinc-800/30 dark:from-inherit lg:static lg:w-auto lg:rounded-xl lg:border lg:bg-gray-200 lg:p-4 lg:dark:bg-zinc-800/30">
Get started by editing&nbsp;
<code className="font-mono font-bold">src/pages/index.tsx</code>
</p>
<div className="fixed bottom-0 left-0 flex h-48 w-full items-end justify-center bg-gradient-to-t from-white via-white dark:from-black dark:via-black lg:static lg:h-auto lg:w-auto lg:bg-none">
<a
className="pointer-events-none flex place-items-center gap-2 p-8 lg:pointer-events-auto lg:p-0"
href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
By{' '}
<Image
src="/vercel.svg"
alt="Vercel Logo"
className="dark:invert"
width={100}
height={24}
priority
/>
</a>
</div>
</div>
<div className="relative flex place-items-center before:absolute before:h-[300px] before:w-[480px] before:-translate-x-1/2 before:rounded-full before:bg-gradient-radial before:from-white before:to-transparent before:blur-2xl before:content-[''] after:absolute after:-z-20 after:h-[180px] after:w-[240px] after:translate-x-1/3 after:bg-gradient-conic after:from-sky-200 after:via-blue-200 after:blur-2xl after:content-[''] before:dark:bg-gradient-to-br before:dark:from-transparent before:dark:to-blue-700/10 after:dark:from-sky-900 after:dark:via-[#0141ff]/40 before:lg:h-[360px]">
<Image
className="relative dark:drop-shadow-[0_0_0.3rem_#ffffff70] dark:invert"
src="/next.svg"
alt="Next.js Logo"
width={180}
height={37}
priority
/>
</div>
<div className="mb-32 grid text-center lg:max-w-5xl lg:w-full lg:mb-0 lg:grid-cols-4 lg:text-left">
<a
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=default-template-tw&utm_campaign=create-next-app"
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
target="_blank"
rel="noopener noreferrer"
>
<h2 className={`mb-3 text-2xl font-semibold`}>
Docs{' '}
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
-&gt;
</span>
</h2>
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
Find in-depth information about Next.js features and API.
</p>
</a>
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=default-template-tw&utm_campaign=create-next-app"
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
target="_blank"
rel="noopener noreferrer"
>
<h2 className={`mb-3 text-2xl font-semibold`}>
Learn{' '}
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
-&gt;
</span>
</h2>
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
Learn about Next.js in an interactive course with&nbsp;quizzes!
</p>
</a>
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=default-template-tw&utm_campaign=create-next-app"
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
target="_blank"
rel="noopener noreferrer"
>
<h2 className={`mb-3 text-2xl font-semibold`}>
Templates{' '}
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
-&gt;
</span>
</h2>
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
Discover and deploy boilerplate example Next.js&nbsp;projects.
</p>
</a>
<a
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=default-template-tw&utm_campaign=create-next-app"
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
target="_blank"
rel="noopener noreferrer"
>
<h2 className={`mb-3 text-2xl font-semibold`}>
Deploy{' '}
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
-&gt;
</span>
</h2>
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
Instantly deploy your Next.js site to a shareable URL with Vercel.
</p>
</a>
</div>
</main>
)
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 112 KiB

View File

@ -0,0 +1,85 @@
import { Variant, AnimatePresence, m } from "framer-motion";
import React from "react";
const SwitchableComponentContext = React.createContext<SwitchableComponentContextType | undefined>(undefined);
export const SwitchableComponentProvider = ({ children }: React.PropsWithChildren) => {
const [visibleComponent, setVisibleComponent] = React.useState<SwitchableComponentType>();
return <SwitchableComponentContext.Provider
value={{ visibleComponent: visibleComponent ?? DEFAULT_COMPONENT, setVisibleComponent }}
>
{children}
</SwitchableComponentContext.Provider>
}
export function useSwitchableComponent(initialValue?: SwitchableComponentType) {
const contextValue = React.useContext(SwitchableComponentContext);
if (contextValue === undefined) {
throw new Error('useSwitchableComponent hook is being used outside of its context. Please ensure that it is wrapped within a <SwitchableComponentProvider>.');
}
React.useEffect(() => {
// Set the initial value if it's provided
if (initialValue && contextValue.visibleComponent) {
contextValue.setVisibleComponent(initialValue);
}
}, [initialValue]);
return contextValue;
}
const variants: Record<string, Variant> = {
hidden: { y: 50, opacity: 0, position: 'absolute' },
show: {
y: 0,
x: 0,
opacity: 1,
position: 'relative',
transition: {
type: "tween",
ease: 'easeInOut'
},
},
exit: { y: -50, opacity: 0, position: 'absolute' }
};
export const SwitchableComponent = ({ children, index }: React.PropsWithChildren<{ index: string }>) => {
const [width, setWidth] = React.useState<number>()
return (
<AnimatePresence>
<m.div
key={index}
initial="hidden"
animate="show"
exit="exit"
variants={variants}
className="h-full w-full"
style={{maxWidth: width ?? 'auto'}}
onTransitionEnd={(e) => setWidth(e.currentTarget.getBoundingClientRect().width!)}
>
{children}
</m.div>
</AnimatePresence>
);
};
type SwitchableComponentType<T extends {} = {}> = {
index: string,
render: (props: T | any) => ReturnType<React.FC>
}
type SwitchableComponentContextType<T = unknown> = {
visibleComponent: SwitchableComponentType<T extends {} ? T : any>,
setVisibleComponent: React.Dispatch<React.SetStateAction<SwitchableComponentType<T extends {} ? T : any> | undefined>>
}
const DEFAULT_COMPONENT = { render: () => undefined, index: Symbol('DEFAULT_COMPONENT').toString() }
// Factory function
export function makeSwitchable<T extends {}>(Component: React.FC<T>, index: string) {
return {
render(props: T) { return <Component {...props} /> },
index: index || Symbol(Component.name).toString()
};
};

View File

@ -0,0 +1,17 @@
import { StoryFn, Meta } from '@storybook/react';
import LumeDashboard from './LumeDashboard';
import LumeProvider from '../LumeProvider';
export default {
title: 'LumeDashboard',
component: LumeDashboard,
} as Meta<typeof LumeDashboard>;
const Template: StoryFn<typeof LumeDashboard> = (args) => <LumeProvider>
<LumeDashboard {...args} />
</LumeProvider>;
export const Primary = Template.bind({});
Primary.args = {
// Add initial props here
};

View File

@ -0,0 +1,117 @@
import * as Dialog from "@radix-ui/react-dialog"
import { Chain, useLume } from "@/components/lume/LumeProvider"
import Logo from "@/assets/lume-logo.png"
const SYNCSTATE_TO_TEXT: Record<Chain['syncState'], string> = {
done: 'Synced',
error: 'Issue',
syncing: 'Syncing'
}
const SYNC_STATE_TO_TW_COLOR: Record<Chain['syncState'], string> = {
'done': 'text-primary',
'error': 'text-red-500',
'syncing': 'text-orange-500',
}
const LumeDashboard = () => {
const { chains } = useLume()
const contentChains = chains.filter(c => c.type === 'content');
const blockchainChains = chains.filter(c => c.type === 'blockchain');
return (
<Dialog.Root>
<Dialog.Trigger>Open</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay className="fixed z-40 inset-0 bg-black bg-opacity-50 backdrop-blur-sm" />
<Dialog.Content className="fixed p-5 z-50 right-0 bottom-0 top-0 w-[300px] bg-neutral-950 text-white border-black border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500">
<div className="w-[calc(100%+38px)] border-b pb-3 -mx-5 px-5 border-neutral-900">
<img src={Logo.src} className="w-24" />
</div>
<div className="mt-4 mb-8">
<h2 className="text-xl mb-4"> Content </h2>
<div className="grid grid-cols-2">
{contentChains.map((chain, index) => <ChainIndicator key={`Content_ChainIndicator_${index}`} chain={chain} />)}
</div>
</div>
<div className="mt-4 mb-8">
<h2 className="text-xl mb-4"> Blockchain </h2>
<div className="grid grid-cols-2">
{blockchainChains.map((chain, index) => <ChainIndicator key={`Blockchain_ChainIndicator_${index}`} chain={chain} />)}
</div>
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
)
}
const ChainIndicator = ({ chain }: { chain: Chain }) => {
return <div key={chain.chainId} className="flex flex-row gap-x-2 items-center ">
<CircularProgress chain={chain} />
<div className="flex flex-col">
<span>{chain.name}</span>
<span className={`text-[12px] -mt-1 ${SYNC_STATE_TO_TW_COLOR[chain.syncState]}`}> {SYNCSTATE_TO_TEXT[chain.syncState]} </span>
</div>
</div>
}
const CircularProgress = ({
chain,
className
}: {
chain: Chain
className?: string
}) => {
const progressOffset = ((100 - chain.progress) / 100) * 282.74 // These math are not mathing
const textOffset = (chain.progress / 100) * (30 - 44) + 44
return (
<svg
className={`${className} ${SYNC_STATE_TO_TW_COLOR[chain.syncState]}`}
width="36"
height="36"
viewBox="0 0 100 100"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
style={{ transform: "rotate(-90deg)" }}
>
<circle
r="45"
cx="50"
cy="50"
fill="transparent"
stroke="#e0e0e0"
stroke-width="4px"
stroke-dasharray="282.74px"
stroke-dashoffset="0"
></circle>
<circle
r="45"
cx="50"
cy="50"
stroke="currentColor"
stroke-width="4px"
stroke-linecap="round"
stroke-dashoffset={`${progressOffset}px`}
fill="transparent"
stroke-dasharray="282.74px"
></circle>
<text
x={textOffset}
y="57.5px"
fill="currentColor"
font-size="26px"
font-weight="normal"
style={{ transform: "rotate(90deg) translate(0px, -98px)" }}
>
{chain.progress}
</text>
</svg>
)
}
export default LumeDashboard

View File

@ -0,0 +1,14 @@
import { StoryFn, Meta } from '@storybook/react';
import LumeIdentity from './LumeIdentity';
export default {
title: 'LumeIdentity',
component: LumeIdentity,
} as Meta<typeof LumeIdentity>;
const Template: StoryFn<typeof LumeIdentity> = (args) => <LumeIdentity {...args} />;
export const Primary = Template.bind({});
Primary.args = {
// Add initial props here
};

View File

@ -0,0 +1,84 @@
import React from 'react';
import LumeLogoBg from '@/assets/lume-logo-bg.svg';
import { Button } from '@/components/ui/button';
import { SwitchableComponent, SwitchableComponentProvider, useSwitchableComponent } from '@/components/SwitchableComponent';
import ComponentList from "./components";
import { LumeIdentityContext, Session } from './LumeIdentityContext';
import { LazyMotion, domAnimation } from 'framer-motion';
import * as Dialog from '@radix-ui/react-dialog';
type Props = {}
const LumeIdentity: React.FC<Props> = ({ }) => {
const { visibleComponent, setVisibleComponent } = useSwitchableComponent(ComponentList.SubmitButton)
const isSubmitButtonInView = [ComponentList.SubmitButton.index].includes(visibleComponent.index)
const isLoginWithAccountKey = [ComponentList.SeedPhraseInput.index].includes(visibleComponent.index)
const isCreatingAccount = [ComponentList.SetupAccountKey.index].includes(visibleComponent.index)
const isShowingSeedPhrase = [ComponentList.SeedPhraseGeneration.index].includes(visibleComponent.index)
const isFinalStep = [ComponentList.SeedPhraseGeneration.index].includes(visibleComponent.index)
const shouldShowBackButton = isCreatingAccount
const coloredOrLine = isSubmitButtonInView ? 'text-primary' : 'text-border'
return <div className="relative w-96 max-w-full inline-flex flex-col items-center justify-center gap-2.5 bg-zinc-950 px-8 py-11 transition-[height] duration-100 [&>*]:transition-all [&>*]:duration-100 overflow-hidden">
<div className="absolute left-[168px] top-[-8px] h-64 w-[280px] overflow-hidden">
<LumeLogoBg />
</div>
<div className="w-full z-10 flex flex-col items-center justify-center gap-10">
<div className="flex flex-col items-start justify-start gap-10">
<h2 className="w-full text-5xl font-normal leading-14 text-white">
{isSubmitButtonInView || isLoginWithAccountKey ? 'Sign in with Lume' : null}
{isCreatingAccount && !isShowingSeedPhrase ? 'Set up your account key' : null}
{isShowingSeedPhrase ? "Here's your account key" : null}
</h2>
</div>
<div className="flex flex-col items-start justify-start gap-2.5">
<LazyMotion features={domAnimation}>
<SwitchableComponent index={visibleComponent.index}>
<visibleComponent.render />
</SwitchableComponent>
</LazyMotion>
{!isFinalStep ? <>
<div className={`relative h-7 w-full overflow-hidden ${coloredOrLine}`}>
<svg width="409" height="28" className="max-w-full -left-1/2" viewBox="0 0 409 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" clipRule="evenodd" d="M183 13H0V14H183V13ZM224 14H409V13H224V14Z" fill="currentColor" />
<path d="M199.75 19.0781C198.359 19.0781 197.299 18.6562 196.57 17.8125C195.841 16.9688 195.477 15.7344 195.477 14.1094C195.477 12.4896 195.839 11.2656 196.562 10.4375C197.292 9.60417 198.354 9.1875 199.75 9.1875C201.151 9.1875 202.216 9.60417 202.945 10.4375C203.674 11.2656 204.039 12.4896 204.039 14.1094C204.039 15.7344 203.674 16.9688 202.945 17.8125C202.216 18.6562 201.151 19.0781 199.75 19.0781ZM199.75 18.0234C200.729 18.0234 201.479 17.6901 202 17.0234C202.521 16.3516 202.781 15.3802 202.781 14.1094C202.781 12.8385 202.521 11.8776 202 11.2266C201.484 10.5703 200.734 10.2422 199.75 10.2422C198.771 10.2422 198.023 10.5703 197.508 11.2266C196.992 11.8776 196.734 12.8385 196.734 14.1094C196.734 15.3854 196.992 16.3568 197.508 17.0234C198.023 17.6901 198.771 18.0234 199.75 18.0234ZM206.742 19.0234C206.367 19.0234 206.18 18.9219 206.18 18.7188V9.69531C206.18 9.54948 206.214 9.44531 206.281 9.38281C206.349 9.3151 206.456 9.28125 206.602 9.28125H208.938C210.047 9.28125 210.88 9.47656 211.438 9.86719C212 10.2578 212.281 10.8802 212.281 11.7344C212.281 12.3125 212.141 12.7995 211.859 13.1953C211.583 13.5859 211.193 13.8802 210.688 14.0781V14.1328C210.958 14.2266 211.18 14.4089 211.352 14.6797C211.529 14.9453 211.724 15.3411 211.938 15.8672L213.023 18.6094C213.049 18.7031 213.062 18.7682 213.062 18.8047C213.062 18.9505 212.867 19.0234 212.477 19.0234H212.336C212.195 19.0234 212.073 19.0052 211.969 18.9688C211.87 18.9271 211.807 18.8724 211.781 18.8047L210.727 16.0781C210.591 15.724 210.432 15.4479 210.25 15.25C210.068 15.0521 209.852 14.9141 209.602 14.8359C209.352 14.7578 209.036 14.7188 208.656 14.7188H207.406V18.7188C207.406 18.9219 207.221 19.0234 206.852 19.0234H206.742ZM209.234 13.6641C209.562 13.6641 209.862 13.5859 210.133 13.4297C210.409 13.2734 210.625 13.0651 210.781 12.8047C210.943 12.5391 211.023 12.2578 211.023 11.9609C211.023 11.4141 210.857 11.0078 210.523 10.7422C210.19 10.4714 209.729 10.3359 209.141 10.3359H207.406V13.6641H209.234Z" fill="currentColor" />
</svg>
</div>
<Button className="h-12 w-full" variant={isSubmitButtonInView ? undefined : 'outline'} onClick={() => setVisibleComponent(shouldShowBackButton ? ComponentList.SubmitButton : ComponentList.SetupAccountKey)}>
<span className="text-center text-lg font-normal leading-normal">{shouldShowBackButton ? 'Go Back' : 'Create an Account'}</span>
</Button>
</> : null}
</div>
</div>
</div>
};
// @ditorodev: We should see how we improve this to be more like Google's SSO
// contrast is not very good, as im testing on a train with a lot of sunlight
// hitting my screen, it is almost impossible to see whats happening the outline
// buttons have no contrast
export default () => {
const [session, setSession] = React.useState<Session>();
return <LumeIdentityContext.Provider value={{ session, setSession }}>
<Dialog.Root>
<Dialog.Trigger asChild>
<button className='bg-primary text-primary-foreground p-2 px-4 text-sm font-semibold font-mono rounded-md'>
Open Lume
</button>
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay className="bg-black/90 data-[state=open]:animate-overlayShow fixed inset-0" />
<Dialog.Content className='w-full h-full flex items-center justify-center'>
<SwitchableComponentProvider>
<LumeIdentity />
</SwitchableComponentProvider>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
</LumeIdentityContext.Provider>
};

View File

@ -0,0 +1,43 @@
import React from "react";
export type Session = string;
export const LumeIdentityContext = React.createContext<{
session: Session | undefined;
setSession: React.Dispatch<React.SetStateAction<Session | undefined>>;
} | undefined>(undefined);
export function useLumeIndentity() {
const contextValue = React.useContext(LumeIdentityContext);
// When the `session` changes we want to update the `session` in the local storage?
React.useEffect(() => {
if (contextValue?.session) {
localStorage.setItem('lume-session', contextValue.session);
} else {
localStorage.removeItem('lume-session');
}
}, [contextValue?.session]);
// Get the session from the local storage
React.useEffect(() => {
const session = localStorage.getItem('lume-session');
if (session) {
contextValue?.setSession(session);
}
}, []);
if (contextValue === undefined) {
throw new Error('useLumeIndentity hook is being used outside of its context. Please ensure that it is wrapped within a <LumeIdentityProvider>.');
}
return {
isSignedIn: !!contextValue.session,
signIn: (key: string) => {
console.log('signing in with key', key);
// TODO: From the key generate a session, and store it
contextValue.setSession('session');
},
signOut: () => {
contextValue.setSession(undefined);
},
};
}

View File

@ -0,0 +1,150 @@
import { makeSwitchable, useSwitchableComponent } from "@/components/SwitchableComponent";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { CheckIcon, ClipboardCopyIcon, ExclamationTriangleIcon } from "@radix-ui/react-icons";
import { AnimatePresence, m } from "framer-motion";
import React from "react";
import { useLumeIndentity } from "./LumeIdentityContext";
// Extracted components
const SubmitButtonComponent = ({ }) => {
const { setVisibleComponent } = useSwitchableComponent();
return (
<Button className='w-full h-12' variant={"outline"} onClick={() => setVisibleComponent(components.SeedPhraseInput)}>
<span className="text-center text-lg font-normal leading-normal">Sign in with Account Key</span>
</Button>
)
};
const SeedPhraseInputComponent = ({ }) => {
const { signIn } = useLumeIndentity();
return (
<m.form className='flex-col flex gap-y-4' onSubmit={(e) => {
e.preventDefault();
const target = e.target as typeof e.target & {
elements: {
seedPhrase: { value: string };
}
};
const seedPhrase = target.elements.seedPhrase.value;
signIn(seedPhrase)
}}>
<Input className='h-12 w-full text-lg' name="seedPhrase" />
<m.div
initial={{ y: 50 }}
animate={{ y: 0 }}
exit={{ y: -50 }}
transition={{ type: "just", delay: 0.1 }}
className="h-12"
>
<Button className='w-full h-full' role="submit">
<span className="text-center text-lg font-normal leading-normal">Sign in</span>
</Button>
</m.div>
</m.form>
);
};
const SetupAccountKeyComponent = () => {
const { setVisibleComponent } = useSwitchableComponent();
const [width, setWidth] = React.useState<number>();
return (
<m.div
initial={{ y: 50 }}
animate={{ y: 0 }}
exit={{ y: -50, height: 'auto' }}
transition={{ type: "just", delay: 0.1 }}
className="min-h-12 h-full max-w-full"
style={{ maxWidth: width ?? 'auto' }}
ref={(t) => setTimeout(() => setWidth(t?.getBoundingClientRect().width!), 0)}
>
<Button className='w-full h-full' onClick={() => setVisibleComponent(components.SeedPhraseGeneration)}>
<span className="text-center text-lg font-normal leading-normal">I get it, I'll keep it safe. Let's see the key.</span>
</Button>
</m.div>
)
};
const SeedPhraseGenerationComponent = ({ phraseLength = 12 }) => {
const [buttonClickedState, setButtonClickedState] = React.useState<"idle" | "clicked">("idle");
const [step, setStep] = React.useState<number>(0);
const { signIn } = useLumeIndentity();
const phrases = React.useMemo(() => {
// TODO: Replace with actual BIP39 or whatever is used for phrase generation
return Array(phraseLength).fill("a phrase")
}, [phraseLength]);
const key = React.useMemo(() => {
return phrases.join(" ");
}, [phrases]);
const copyPhrasesToClipboard = () => {
navigator.clipboard.writeText(phrases.join(" "));
setButtonClickedState("clicked");
setTimeout(() => setButtonClickedState("idle"), 1000);
}
return (
<div className="relative">
<AnimatePresence>
{step === 1 ? <m.div className={`z-10 absolute top-0 bottom-0 left-0 right-0 bg-black pointer-events-none`}
initial={{ opacity: 0 }}
animate={{ opacity: 0.75, top: -200, left: -20, right: -20, bottom: 120 }}
transition={{ type: "tween", duration: 0.1 }}
></m.div> : null}
</AnimatePresence>
<div className="z-20 relative mb-2.5 w-full h-full flex-wrap justify-center items-center gap-2.5 inline-flex">
{phrases.map((phrase, index) => (
<div key={`SeedPhrase_${index}`} className={`justify-center items-center gap-2.5 flex w-[calc(33%-10px)] h-10 rounded border border-current relative ring-current text-neutral-700`}>
<span className=" text-white text-md font-normal leading-normal w-full h-fit px-2.5 bg-transparent text-center">{phrase}</span>
<span className="left-[6px] top-0 absolute text-current text-xs font-normal leading-normal">{index + 1}</span>
</div>
))}
<AnimatePresence>
{step === 1 ? <m.div className="text-red-400 flex flex-row gap-5 py-8"
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 50 }}
transition={{ type: "linear", delay: 0.2, duration: 0.5 }}
>
<ExclamationTriangleIcon className="w-14 h-14" />
<span>Make sure to write this down for safe keeping.</span>
</m.div> : null}
</AnimatePresence>
<Button className={`w-full h-12 ${buttonClickedState === 'clicked' ? '!text-primary !border-primary' : ''}`} variant="outline" onClick={copyPhrasesToClipboard}>
{buttonClickedState === 'clicked' ? <CheckIcon className="w-5 h-5 mr-2.5" /> : <ClipboardCopyIcon className="w-5 h-5 mr-2.5" />}
{buttonClickedState === 'clicked' ? 'Copied!' : 'Copy Account Key'}
</Button>
</div>
{step === 0 ? (
<Button className="z-20 w-full h-12 text-white bg-neutral-700 hover:bg-neutral-800" variant="secondary" onClick={() => setStep(1)}>
Continue
</Button>
) : null}
<AnimatePresence>
{step === 1 ? <m.div className="z-20 w-full h-12"
initial={{ opacity: 0, y: -50 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 50 }}
transition={{ type: "linear", delay: 2, duration: 0.5 }}
>
<Button className="w-full h-full" onClick={() => signIn(key)}>
Sign In
</Button>
</m.div> : null}
</AnimatePresence>
</div>
)
};
// Usage
const components = {
SubmitButton: makeSwitchable(SubmitButtonComponent, 'submit-button'),
SeedPhraseInput: makeSwitchable(SeedPhraseInputComponent, 'seed-phrase-input'),
SetupAccountKey: makeSwitchable(SetupAccountKeyComponent, 'setup-account-key'),
SeedPhraseGeneration: makeSwitchable(SeedPhraseGenerationComponent, 'seed-phrase-form'),
};
export default components;

View File

@ -0,0 +1,73 @@
import React, { useContext } from 'react';
type LumeSyncState = 'syncing' | 'done' | 'error'
export type Chain = {
syncState: LumeSyncState,
name: string,
chainId: string,
active: boolean,
progress: number, // in porcentage
logs: string[],
type: 'blockchain' | 'content',
peerCount?: number
}
type LumeObject = {
chains: Chain[],
activeResolver: 'local' | 'rpc'
}
type LumeContext = {
lume: LumeObject
}
const LumeContext = React.createContext<LumeContext | undefined>(undefined);
const LumeProvider = ({ children }: { children: React.ReactNode }) => {
const [lume, setLume] = React.useState<LumeObject>({
chains: [
{
name: 'Ethereum',
syncState: 'done',
chainId: '1',
active: true,
progress: 100,
logs: [],
type: 'blockchain'
},
{
name: "IPFS",
syncState: 'syncing',
chainId: '2',
active: false,
progress: 50,
logs: [],
type: 'content',
peerCount: 3
}
],
activeResolver: 'local',
});
// Here you can add the logic to update the lume state
return (
<LumeContext.Provider value={{ lume }}>
{children}
</LumeContext.Provider>
);
};
export default LumeProvider;
export function useLume() {
const ctx = useContext(LumeContext);
if (!ctx) {
throw new Error('useLume must be used within a LumeProvider');
}
const { lume } = ctx;
return lume
}

View File

@ -1,472 +0,0 @@
import React, {
createContext,
createRef,
type ReactNode,
useContext,
useEffect,
useState,
} from "react";
import {
type AuthContextType,
AuthProvider,
LumeDashboard,
LumeIdentity,
LumeIdentityTrigger,
type LumeStatusContextType,
LumeStatusProvider,
NetworksProvider,
useAuth,
useLumeStatus,
useNetworks,
LumeDashboardTrigger,
} from "@lumeweb/sdk";
import * as kernel from "@lumeweb/libkernel/kernel";
import { kernelLoaded } from "@lumeweb/libkernel/kernel";
import {
dnsClient,
ethClient,
ipfsClient,
networkRegistryClient,
peerDiscoveryClient,
swarmClient,
} from "@/lib/clients";
import { ethers } from "ethers";
import * as ethersBytes from "@ethersproject/bytes";
import { createProvider } from "@lumeweb/kernel-eth-client";
// @ts-ignore
import jdu from "json-data-uri";
import { ERC721_ABI } from "@/lib/erc721-abi";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import LogoImg from "@/assets/lume-logo.png";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
let BOOT_FUNCTIONS: (() => Promise<any>)[] = [];
export const AppContext = createContext<any>(undefined);
export function useApp() {
const context = useContext(AppContext);
if (!context) {
throw new Error("useApp must be used within an AppProvider");
}
return context;
}
interface AppProviderProps {
children: ReactNode;
}
const provider = createProvider();
const ERC721_TRANSFER_EVENT_SIGNATURE = ethers.id(
"Transfer(address,address,uint256)"
);
async function findPotentialERC721Contracts(
address: string
): Promise<string[]> {
const logs = await provider.getLogs({
fromBlock: 1,
toBlock: "latest",
topics: [
ERC721_TRANSFER_EVENT_SIGNATURE,
null,
ethersBytes.hexZeroPad(address, 32),
],
});
const potentialContracts = new Set<string>();
logs.forEach((log: any) => potentialContracts.add(log.address));
const confirmedERC721Contracts: string[] = [];
for (let contractAddress of potentialContracts) {
if (await isERC721(contractAddress)) {
confirmedERC721Contracts.push(contractAddress);
}
}
return confirmedERC721Contracts;
}
const TRANSFER_EVENT_SIGNATURE = ethers.id("Transfer(address,address,uint256)");
async function fetchTokensViaTransferEvent(
address: string,
contractAddress: string
): Promise<number[]> {
const logs = await provider.getLogs({
fromBlock: 0,
toBlock: "latest",
address: contractAddress,
topics: [
TRANSFER_EVENT_SIGNATURE,
null,
ethersBytes.hexZeroPad(address, 32),
],
});
const tokenIds: number[] = [];
logs.forEach((log) => {
if (log.topics && log.topics.length === 4) {
const tokenIdBigNumber = ethers.toNumber(log.topics[3]);
tokenIds.push(tokenIdBigNumber);
}
});
return tokenIds;
}
async function fetchOwnedNFTs(
address: string,
confirmedERC721Contracts: string[]
): Promise<{ contract: string; tokenId: number; metadata: any }[]> {
const ownedNFTs = [];
for (let contractAddress of confirmedERC721Contracts) {
const contract = new ethers.Contract(contractAddress, ERC721_ABI, provider);
let tokenIds: number[] = [];
try {
const balance = await contract.balanceOf(address);
for (let i = 0; i < balance; i++) {
const tokenId = await contract.tokenOfOwnerByIndex(address, i);
tokenIds.push(tokenId.toNumber());
}
} catch (error) {
// If tokenOfOwnerByIndex is not available, fall back to fetchTokensViaTransferEvent
tokenIds = await fetchTokensViaTransferEvent(address, contractAddress);
}
for (let tokenId of tokenIds) {
try {
const uri = await contract.tokenURI(tokenId);
// const metadata = await fetchMetadataFromURI(uri);
ownedNFTs.push({
contract: contractAddress,
tokenId: tokenId,
metadata: uri,
});
} catch (error: any) {
console.error(
`Failed to fetch metadata for token ${tokenId} from contract ${contractAddress}: ${error.message}`
);
}
}
}
return ownedNFTs;
}
async function isERC721(address: string): Promise<boolean> {
const contract = new ethers.Contract(address, ERC721_ABI, provider);
try {
// Try calling some ERC-721 methods to confirm if this is an ERC-721 contract.
await contract.name();
await contract.symbol();
return true;
} catch (error) {
return false;
}
}
const AppProvider: React.FC<AppProviderProps> = ({ children }) => {
return <AppContext.Provider value={{}}>{children}</AppContext.Provider>;
};
async function boot(status: LumeStatusContextType, auth: AuthContextType) {
kernel.init().then(() => {
status.setInited(true);
});
await kernelLoaded();
auth.setIsLoggedIn(true);
BOOT_FUNCTIONS.push(
async () =>
await swarmClient.addRelay(
"2d7ae1517caf4aae4de73c6d6f400765d2dd00b69d65277a29151437ef1c7d1d"
)
);
// IRC
BOOT_FUNCTIONS.push(
async () =>
await peerDiscoveryClient.register(
"zrjHTx8tSQFWnmZ9JzK7XmJirqJQi2WRBLYp3fASaL2AfBQ"
)
);
BOOT_FUNCTIONS.push(
async () => await networkRegistryClient.registerType("content")
);
BOOT_FUNCTIONS.push(
async () => await networkRegistryClient.registerType("blockchain")
);
BOOT_FUNCTIONS.push(async () => await ethClient.register());
BOOT_FUNCTIONS.push(async () => await ipfsClient.register());
const resolvers = [
"zrjEYq154PS7boERAbRAKMyRGzAR6CTHVRG6mfi5FV4q9FA", // ENS
];
for (const resolver of resolvers) {
BOOT_FUNCTIONS.push(async () => dnsClient.registerResolver(resolver));
}
BOOT_FUNCTIONS.push(async () => status.setReady(true));
await bootup();
await Promise.all([ethClient.ready(), ipfsClient.ready()]);
}
async function bootup() {
for (const entry of Object.entries(BOOT_FUNCTIONS)) {
console.log(entry[1].toString());
await entry[1]();
}
}
function LoginDash() {
const { isLoggedIn } = useAuth();
const { ready, inited } = useLumeStatus();
return (
<>
{!isLoggedIn && (
<LumeIdentity>
<LumeIdentityTrigger asChild>
<Button variant={"default"} disabled={!inited}>
Login
</Button>
</LumeIdentityTrigger>
</LumeIdentity>
)}
{isLoggedIn && (
<LumeDashboard disabled={!ready}>
<LumeDashboardTrigger asChild>
<Button variant={"default"} disabled={!inited}>
Open Dashboard
</Button>
</LumeDashboardTrigger>
</LumeDashboard>
)}
</>
);
}
async function asyncIterableToUint8Array(asyncIterable: any) {
const chunks = [];
let totalLength = 0;
for await (const chunk of asyncIterable) {
chunks.push(chunk);
totalLength += chunk.length;
}
const result = new Uint8Array(totalLength);
let offset = 0;
for (const chunk of chunks) {
result.set(chunk, offset);
offset += chunk.length;
}
return result;
}
function uint8ArrayToBase64(byteArray: Uint8Array) {
let base64 = "";
const characters =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let padding = 0;
for (let i = 0; i < byteArray.length; i += 3) {
const a = byteArray[i];
const b = byteArray[i + 1];
const c = byteArray[i + 2];
const triplet = (a << 16) + ((b || 0) << 8) + (c || 0);
base64 += characters.charAt((triplet & 0xfc0000) >> 18);
base64 += characters.charAt((triplet & 0x03f000) >> 12);
base64 += characters.charAt((triplet & 0x000fc0) >> 6);
base64 += characters.charAt(triplet & 0x00003f);
if (byteArray.length - i < 3) {
padding = 3 - (byteArray.length - i);
}
}
// Add padding if necessary
if (padding > 0) {
base64 = base64.slice(0, -padding) + (padding === 1 ? "=" : "==");
}
return base64;
}
function App() {
const status = useLumeStatus();
const auth = useAuth();
const [nftList, setNftList] = useState<any[]>([]);
useEffect(() => {
boot(status, auth);
}, []);
const { networks } = useNetworks();
const ipfsStatus = networks
.filter((item) => item.name.toLowerCase() === "ipfs")
?.pop();
const ethStatus = networks
.filter((item) => item.name.toLowerCase() === "ethereum")
?.pop();
const ready = ethStatus?.ready && status.ready;
const inputRef = createRef<HTMLInputElement>();
async function search(e: any | Event) {
e.preventDefault();
let address = inputRef?.current?.value as string;
address = await ethers.resolveAddress(address, provider);
const contracts = await findPotentialERC721Contracts(address);
const nfts = await fetchOwnedNFTs(address, contracts);
const list = [];
for (const nft of nfts) {
let meta;
if (typeof nft.metadata === "string") {
try {
meta = await (await fetch(nft.metadata)).json();
} catch (e) {
meta = {
image: "", // TODO: Improve this by bringing an actual image
name: "Failed to Load",
fail: true,
};
}
} else {
meta = jdu.parse(nft.metadata);
}
let image;
if (!meta.fail) {
const imageCID = meta.image.replace("ipfs://", "");
image = await asyncIterableToUint8Array(
ipfsClient.cat(imageCID).iterable()
);
} else {
image = meta.image;
}
list.push({
image,
name: meta.name,
base64: meta.fail,
});
setNftList(list);
}
}
return (
<div className="mx-auto px-4 sm:px-6 lg:px-8 py-8 min-h-screen h-full w-screen bg-zinc-900 flex items-center flex-col p-8 space-y-3">
<Card className="w-full bg-zinc-950 border-zinc-800 shadow-xl max-w-4xl">
<CardHeader className="flex flex-row justify-between">
<div className="flex gap-x-2 items-center justify-center text-zinc-500">
<img src={LogoImg.src} className="w-20 h-7" />
<h2 className="border-l border-current pl-2">NFT Explorer</h2>
</div>
<div className="w-32 flex justify-end">
<LoginDash />
</div>
</CardHeader>
<CardContent>
<form className="flex items-center mb-4" onSubmit={search}>
<div className="relative w-full">
<svg
className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-zinc-600"
fill="none"
height="24"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="11" cy="11" r="8" />
<path d="m21 21-4.3-4.3" />
</svg>
<Input
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.eth"
type="search"
disabled={!ready}
ref={inputRef}
/>
</div>
<Button className="ml-4" variant="default" disabled={!ready}>
Search
</Button>
</form>
</CardContent>
</Card>
{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">
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>
</span>
) : null}
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4">
{nftList.map((nft, index) => (
<img
key={index}
alt={nft.name}
className="aspect-square object-cover border border-zinc-200 w-full rounded-lg overflow-hidden dark:border-zinc-800"
height="300"
src={
nft.base64
? nft.image
: `data:image/png;base64,${uint8ArrayToBase64(nft.image)}`
}
width="300"
/>
))}
</div>
</div>
);
}
export default function () {
return (
<AppProvider>
<LumeStatusProvider>
<AuthProvider>
<NetworksProvider>
<App />
</NetworksProvider>
</AuthProvider>
</LumeStatusProvider>
</AppProvider>
);
}

View File

@ -5,25 +5,26 @@ import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex rounded-md items-center justify-center whitespace-nowrap text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
"text-neutral-600 border border-current bg-transparent shadow-sm hover:border-white hover:text-white",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
@ -35,7 +36,7 @@ const buttonVariants = cva(
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
VariantProps<typeof buttonVariants> {
asChild?: boolean
}

View File

@ -1,79 +0,0 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn(
"text-2xl font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View File

@ -0,0 +1,123 @@
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { Cross2Icon } from "@radix-ui/react-icons"
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = ({
className,
...props
}: DialogPrimitive.DialogPortalProps) => (
<DialogPrimitive.Portal className={cn(className)} {...props} />
)
DialogPortal.displayName = DialogPrimitive.Portal.displayName
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg md:w-full",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<Cross2Icon className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}

View File

@ -3,7 +3,7 @@ import * as React from "react"
import { cn } from "@/lib/utils"
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
extends React.InputHTMLAttributes<HTMLInputElement> { }
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
@ -11,7 +11,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
"flex h-9 w-full text-white rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 focus:border-white",
className
)}
ref={ref}

144
src/components/ui/sheet.tsx Normal file
View File

@ -0,0 +1,144 @@
"use client"
import * as React from "react"
import * as SheetPrimitive from "@radix-ui/react-dialog"
import { Cross2Icon } from "@radix-ui/react-icons"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const Sheet = SheetPrimitive.Root
const SheetTrigger = SheetPrimitive.Trigger
const SheetClose = SheetPrimitive.Close
const SheetPortal = ({
className,
...props
}: SheetPrimitive.DialogPortalProps) => (
<SheetPrimitive.Portal className={cn(className)} {...props} />
)
SheetPortal.displayName = SheetPrimitive.Portal.displayName
const SheetOverlay = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
ref={ref}
/>
))
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
const sheetVariants = cva(
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
{
variants: {
side: {
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
bottom:
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
right:
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
},
},
defaultVariants: {
side: "right",
},
}
)
interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> {}
const SheetContent = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Content>,
SheetContentProps
>(({ side = "right", className, children, ...props }, ref) => (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
ref={ref}
className={cn(sheetVariants({ side }), className)}
{...props}
>
{children}
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
<Cross2Icon className="h-4 w-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
))
SheetContent.displayName = SheetPrimitive.Content.displayName
const SheetHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
)
SheetHeader.displayName = "SheetHeader"
const SheetFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
SheetFooter.displayName = "SheetFooter"
const SheetTitle = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold text-foreground", className)}
{...props}
/>
))
SheetTitle.displayName = SheetPrimitive.Title.displayName
const SheetDescription = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
SheetDescription.displayName = SheetPrimitive.Description.displayName
export {
Sheet,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
}

127
src/components/ui/toast.tsx Normal file
View File

@ -0,0 +1,127 @@
import * as React from "react"
import { Cross2Icon } from "@radix-ui/react-icons"
import * as ToastPrimitives from "@radix-ui/react-toast"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const ToastProvider = ToastPrimitives.Provider
const ToastViewport = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Viewport
ref={ref}
className={cn(
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
className
)}
{...props}
/>
))
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
const toastVariants = cva(
"group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
{
variants: {
variant: {
default: "border bg-background text-foreground",
destructive:
"destructive group border-destructive bg-destructive text-destructive-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => {
return (
<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({ variant }), className)}
{...props}
/>
)
})
Toast.displayName = ToastPrimitives.Root.displayName
const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Action
ref={ref}
className={cn(
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
className
)}
{...props}
/>
))
ToastAction.displayName = ToastPrimitives.Action.displayName
const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Close
ref={ref}
className={cn(
"absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
className
)}
toast-close=""
{...props}
>
<Cross2Icon className="h-4 w-4" />
</ToastPrimitives.Close>
))
ToastClose.displayName = ToastPrimitives.Close.displayName
const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title
ref={ref}
className={cn("text-sm font-semibold [&+div]:text-xs", className)}
{...props}
/>
))
ToastTitle.displayName = ToastPrimitives.Title.displayName
const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Description
ref={ref}
className={cn("text-sm opacity-90", className)}
{...props}
/>
))
ToastDescription.displayName = ToastPrimitives.Description.displayName
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
type ToastActionElement = React.ReactElement<typeof ToastAction>
export {
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
}

View File

@ -0,0 +1,35 @@
"use client"
import {
Toast,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
} from "@/components/ui/toast"
import { useToast } from "@/components/ui/use-toast"
export function Toaster() {
const { toasts } = useToast()
return (
<ToastProvider>
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && (
<ToastDescription>{description}</ToastDescription>
)}
</div>
{action}
<ToastClose />
</Toast>
)
})}
<ToastViewport />
</ToastProvider>
)
}

View File

@ -0,0 +1,192 @@
// Inspired by react-hot-toast library
import * as React from "react"
import type {
ToastActionElement,
ToastProps,
} from "@/components/ui/toast"
const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000
type ToasterToast = ToastProps & {
id: string
title?: React.ReactNode
description?: React.ReactNode
action?: ToastActionElement
}
const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
} as const
let count = 0
function genId() {
count = (count + 1) % Number.MAX_VALUE
return count.toString()
}
type ActionType = typeof actionTypes
type Action =
| {
type: ActionType["ADD_TOAST"]
toast: ToasterToast
}
| {
type: ActionType["UPDATE_TOAST"]
toast: Partial<ToasterToast>
}
| {
type: ActionType["DISMISS_TOAST"]
toastId?: ToasterToast["id"]
}
| {
type: ActionType["REMOVE_TOAST"]
toastId?: ToasterToast["id"]
}
interface State {
toasts: ToasterToast[]
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) {
return
}
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId)
dispatch({
type: "REMOVE_TOAST",
toastId: toastId,
})
}, TOAST_REMOVE_DELAY)
toastTimeouts.set(toastId, timeout)
}
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "ADD_TOAST":
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
}
case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t
),
}
case "DISMISS_TOAST": {
const { toastId } = action
// ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId)
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id)
})
}
return {
...state,
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
open: false,
}
: t
),
}
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
toasts: [],
}
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
}
}
}
const listeners: Array<(state: State) => void> = []
let memoryState: State = { toasts: [] }
function dispatch(action: Action) {
memoryState = reducer(memoryState, action)
listeners.forEach((listener) => {
listener(memoryState)
})
}
type Toast = Omit<ToasterToast, "id">
function toast({ ...props }: Toast) {
const id = genId()
const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
})
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
dispatch({
type: "ADD_TOAST",
toast: {
...props,
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss()
},
},
})
return {
id: id,
dismiss,
update,
}
}
function useToast() {
const [state, setState] = React.useState<State>(memoryState)
React.useEffect(() => {
listeners.push(setState)
return () => {
const index = listeners.indexOf(setState)
if (index > -1) {
listeners.splice(index, 1)
}
}
}, [state])
return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
}
}
export { useToast, toast }

1
src/env.d.ts vendored
View File

@ -1 +0,0 @@
/// <reference types="astro/client" />

View File

@ -1,25 +0,0 @@
import { createClient as createDnsClient } from "@lumeweb/kernel-dns-client";
import { createClient as createIpfsClient } from "@lumeweb/kernel-ipfs-client";
import { createClient as createSwarmClient } from "@lumeweb/kernel-swarm-client";
import { createClient as createPeerDiscoveryClient } from "@lumeweb/kernel-peer-discovery-client";
import { createClient as createNetworkRegistryClient } from "@lumeweb/kernel-network-registry-client";
import { createClient as createHandshakeClient } from "@lumeweb/kernel-handshake-client";
import { createClient as createEthClient } from "@lumeweb/kernel-eth-client";
const dnsClient = createDnsClient();
const ipfsClient = createIpfsClient();
const swarmClient = createSwarmClient();
const peerDiscoveryClient = createPeerDiscoveryClient();
const networkRegistryClient = createNetworkRegistryClient();
const handshakeClient = createHandshakeClient();
const ethClient = createEthClient();
export {
dnsClient,
ipfsClient,
swarmClient,
peerDiscoveryClient,
networkRegistryClient,
handshakeClient,
ethClient,
};

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

6
src/pages/_app.tsx Normal file
View File

@ -0,0 +1,6 @@
import '@/styles/globals.css'
import type { AppProps } from 'next/app'
export default function App({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />
}

13
src/pages/_document.tsx Normal file
View File

@ -0,0 +1,13 @@
import { Html, Head, Main, NextScript } from 'next/document'
export default function Document() {
return (
<Html lang="en">
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
)
}

13
src/pages/api/hello.ts Normal file
View File

@ -0,0 +1,13 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next'
type Data = {
name: string
}
export default function handler(
req: NextApiRequest,
res: NextApiResponse<Data>
) {
res.status(200).json({ name: 'John Doe' })
}

View File

@ -1,70 +0,0 @@
---
import {Button} from '@/components/ui/button';
import {Card} from '@/components/ui/card';
import {CardContent} from '@/components/ui/card';
import {CardDescription} from '@/components/ui/card';
import {CardFooter} from '@/components/ui/card';
import {CardHeader} from '@/components/ui/card';
import {CardTitle} from '@/components/ui/card';
import LogoImg from "../assets/lume-logo.png";
import '@/styles/global.css'
---
<html lang="en">
<head>
<meta charset="utf-8"/>
<link rel="icon" type="image/svg+xml" href="/favicon.svg"/>
<meta name="viewport" content="width=device-width"/>
<meta name="generator" content={Astro.generator}/>
<title>Web3 Toybox | Powered By Lume Web</title>
</head>
<body>
<div class="min-h-screen h-full w-full bg-zinc-900 flex flex-col items-center justify-center p-8 space-y-6">
<Card className="max-w-3xl bg-zinc-950 border-zinc-800 shadow-xl">
<CardHeader>
<img src={LogoImg.src} class="w-20 mb-8" />
<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 web3 use cases by example in a truly decentralized way - no infura, no gateways, no censorship.</CardDescription>
</CardHeader>
<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">
<CardHeader>
<CardTitle className='text-white'>NFT Gallery</CardTitle>
</CardHeader>
<CardContent>
<CardDescription className='text-zinc-500'>
Here, you can explore all your favorite ENS and Ethereum addresses and discover the unique NFTs they hold. Whether you're a seasoned collector or a newcomer to the space, our gallery offers a comprehensive and engaging way to interact with these digital assets.
</CardDescription>
</CardContent>
<CardFooter>
<a href="/nft-gallery" class="w-full">
<Button variant="default" className="mt-4 w-full">
Start Exploring
</Button>
</a>
</CardFooter>
</Card>
<Card className="w-full max-w-[300px] bg-zinc-950 border-zinc-700 border-dashed">
<CardHeader>
<CardTitle className='text-zinc-400 mb-3'>More Coming Soon...</CardTitle>
<CardDescription className='text-zinc-400'>
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">
<Button variant="link" className='h-auto p-0 inline pl-1'>
Join Discord
</Button>
</a>
</CardDescription>
</CardHeader>
</Card>
</CardContent>
<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">
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>
<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>
</Card>
</div>
</body>
</html>

View File

@ -1,18 +0,0 @@
---
import App from "@/components/nft-gallery/App"
// NEVER MOVE THIS, FIXING THE LOGIN BUTTON ERROR
import "@lumeweb/sdk/lib/style.css"
import "@/styles/global.css"
---
<html lang="en">
<head>
<meta charset="utf-8"/>
<link rel="icon" type="image/svg+xml" href="/favicon.svg"/>
<meta name="viewport" content="width=device-width"/>
<meta name="generator" content={Astro.generator}/>
<title>NFT Gallery | Web3 Toybox</title>
</head>
<body>
<App client:only="react"/>
</body>
</html>

View File

@ -1,77 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 113 49% 55%;
--primary-foreground: black;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 0 0% 32%;
--muted-foreground: 0 0% 32%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 0 0% 32%;
--input: 0 0% 32%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

76
src/styles/globals.css Normal file
View File

@ -0,0 +1,76 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 113 49% 55%;
--primary-foreground: black;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 0 0% 32%;
--muted-foreground: 0 0% 32%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 0 0% 32%;
--input: 0 0% 32%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

View File

@ -1,71 +0,0 @@
/** @type {import('tailwindcss').Config} */
export default {
darkMode: ["class"],
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: { height: 0 },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: 0 },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [require("tailwindcss-animate")],
}

76
tailwind.config.ts Normal file
View File

@ -0,0 +1,76 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ["class"],
content: [
'./pages/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
],
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: { height: 0 },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: 0 },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [require("tailwindcss-animate")],
}

View File

@ -1,13 +1,22 @@
{
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "react",
"baseUrl": ".",
"paths": {
"@/*": [
"./src/*"
]
}
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}