Compare commits
No commits in common. "develop" and "ditorodev" have entirely different histories.
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"extends": [
|
||||||
|
"next/core-web-vitals",
|
||||||
|
"plugin:storybook/recommended"
|
||||||
|
]
|
||||||
|
}
|
|
@ -1,2 +1,37 @@
|
||||||
node_modules
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
.local-ssl/
|
|
||||||
|
# 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
|
|
@ -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;
|
|
@ -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;
|
9
LICENSE
9
LICENSE
|
@ -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.
|
|
|
@ -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.
|
|
@ -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};'
|
|
||||||
})
|
|
||||||
]
|
|
||||||
}
|
|
||||||
})
|
|
|
@ -1,11 +1,11 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://ui.shadcn.com/schema.json",
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
"style": "default",
|
"style": "new-york",
|
||||||
"rsc": false,
|
"rsc": true,
|
||||||
"tsx": true,
|
"tsx": true,
|
||||||
"tailwind": {
|
"tailwind": {
|
||||||
"config": "tailwind.config.js",
|
"config": "tailwind.config.ts",
|
||||||
"css": "src/styles/global.css",
|
"css": "src/styles/globals.css",
|
||||||
"baseColor": "slate",
|
"baseColor": "slate",
|
||||||
"cssVariables": true
|
"cssVariables": true
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
reactStrictMode: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = nextConfig
|
File diff suppressed because it is too large
Load Diff
69
package.json
69
package.json
|
@ -1,42 +1,51 @@
|
||||||
{
|
{
|
||||||
"name": "@lumeweb/web3toybox.com",
|
"name": "lume-nft-explorer",
|
||||||
"type": "module",
|
"version": "0.1.0",
|
||||||
"version": "0.0.1",
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "astro dev",
|
"dev": "next dev",
|
||||||
"start": "astro dev",
|
"build": "next build",
|
||||||
"build": "astro build",
|
"start": "next start",
|
||||||
"preview": "astro preview",
|
"lint": "next lint",
|
||||||
"astro": "astro",
|
"storybook": "storybook dev -p 6006",
|
||||||
"local-setup-https": "mkdir -p ./.local-ssl && mkcert -key-file ./.local-ssl/localhost-key.pem -cert-file ./.local-ssl/localhost.pem localhost"
|
"build-storybook": "storybook build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/react": "^3.0.3",
|
"@radix-ui/react-dialog": "^1.0.4",
|
||||||
"@astrojs/tailwind": "^5.0.2",
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
"@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-slot": "^1.0.2",
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
"@types/react": "^18.2.31",
|
"@radix-ui/react-toast": "^1.1.4",
|
||||||
"@types/react-dom": "^18.2.14",
|
"@types/node": "20.5.3",
|
||||||
"astro": "^3.3.2",
|
"@types/react": "18.2.21",
|
||||||
|
"@types/react-dom": "18.2.7",
|
||||||
|
"autoprefixer": "10.4.15",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.0.0",
|
"clsx": "^2.0.0",
|
||||||
"json-data-uri": "^0.2.0",
|
"eslint": "8.47.0",
|
||||||
"lucide-react": "^0.288.0",
|
"eslint-config-next": "13.4.19",
|
||||||
"react": "^18.2.0",
|
"framer-motion": "^10.16.4",
|
||||||
"react-dom": "^18.2.0",
|
"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",
|
"tailwind-merge": "^1.14.0",
|
||||||
"tailwindcss": "^3.3.3",
|
"tailwindcss": "3.3.3",
|
||||||
"tailwindcss-animate": "^1.0.7"
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"typescript": "5.1.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"patch-package": "^8.0.0",
|
"@storybook/addon-essentials": "^7.4.0",
|
||||||
"vite-plugin-optimizer": "^1.4.2"
|
"@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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,6 @@
|
||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
|
@ -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 |
|
@ -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 |
|
@ -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
|
||||||
|
<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">
|
||||||
|
->
|
||||||
|
</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">
|
||||||
|
->
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
|
||||||
|
Learn about Next.js in an interactive course with 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">
|
||||||
|
->
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
|
||||||
|
Discover and deploy boilerplate example Next.js 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">
|
||||||
|
->
|
||||||
|
</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 |
|
@ -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()
|
||||||
|
};
|
||||||
|
};
|
|
@ -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
|
||||||
|
};
|
|
@ -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
|
|
@ -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
|
||||||
|
};
|
|
@ -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>
|
||||||
|
};
|
|
@ -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);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
|
@ -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;
|
|
@ -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
|
||||||
|
}
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -5,25 +5,26 @@ import { cva, type VariantProps } from "class-variance-authority"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const buttonVariants = cva(
|
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: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
default:
|
||||||
|
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
||||||
destructive:
|
destructive:
|
||||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
||||||
outline:
|
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:
|
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",
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||||
link: "text-primary underline-offset-4 hover:underline",
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
default: "h-10 px-4 py-2",
|
default: "h-9 px-4 py-2",
|
||||||
sm: "h-9 rounded-md px-3",
|
sm: "h-8 rounded-md px-3 text-xs",
|
||||||
lg: "h-11 rounded-md px-8",
|
lg: "h-10 rounded-md px-8",
|
||||||
icon: "h-10 w-10",
|
icon: "h-9 w-9",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
|
@ -35,7 +36,7 @@ const buttonVariants = cva(
|
||||||
|
|
||||||
export interface ButtonProps
|
export interface ButtonProps
|
||||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
VariantProps<typeof buttonVariants> {
|
VariantProps<typeof buttonVariants> {
|
||||||
asChild?: boolean
|
asChild?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 }
|
|
|
@ -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,
|
||||||
|
}
|
|
@ -3,7 +3,7 @@ import * as React from "react"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
export interface InputProps
|
export interface InputProps
|
||||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
extends React.InputHTMLAttributes<HTMLInputElement> { }
|
||||||
|
|
||||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||||
({ className, type, ...props }, ref) => {
|
({ className, type, ...props }, ref) => {
|
||||||
|
@ -11,7 +11,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||||
<input
|
<input
|
||||||
type={type}
|
type={type}
|
||||||
className={cn(
|
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
|
className
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
|
|
@ -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,
|
||||||
|
}
|
|
@ -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,
|
||||||
|
}
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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 +0,0 @@
|
||||||
/// <reference types="astro/client" />
|
|
|
@ -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
|
@ -0,0 +1,6 @@
|
||||||
|
import '@/styles/globals.css'
|
||||||
|
import type { AppProps } from 'next/app'
|
||||||
|
|
||||||
|
export default function App({ Component, pageProps }: AppProps) {
|
||||||
|
return <Component {...pageProps} />
|
||||||
|
}
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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' })
|
||||||
|
}
|
|
@ -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>
|
|
|
@ -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>
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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")],
|
|
||||||
}
|
|
|
@ -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")],
|
||||||
|
}
|
|
@ -1,13 +1,22 @@
|
||||||
{
|
{
|
||||||
"extends": "astro/tsconfigs/strict",
|
"compilerOptions": {
|
||||||
"compilerOptions": {
|
"target": "es5",
|
||||||
"jsx": "react-jsx",
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
"jsxImportSource": "react",
|
"allowJs": true,
|
||||||
"baseUrl": ".",
|
"skipLibCheck": true,
|
||||||
"paths": {
|
"strict": true,
|
||||||
"@/*": [
|
"noEmit": true,
|
||||||
"./src/*"
|
"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"]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue