├── .gitignore ├── .gitattributes ├── next.config.js ├── next-env.d.ts ├── public └── static │ └── empty.jpg ├── src ├── app │ ├── features │ │ ├── validators.ts │ │ ├── useIsElectricObject.ts │ │ ├── useOpenSeaAPI.ts │ │ ├── useHicEtNunc.ts │ │ ├── useConfig.ts │ │ └── useSlideshow.ts │ ├── components │ │ ├── FrameETH.tsx │ │ ├── FrameTZ.tsx │ │ ├── FrameGenericNFT.tsx │ │ ├── AddressTextInputs.tsx │ │ └── AddressPreview.tsx │ └── layouts │ │ └── MainLayout.tsx ├── pages │ ├── _app.tsx │ ├── api │ │ └── tezos.ts │ ├── frame.tsx │ └── index.tsx └── server │ └── services │ └── Tezos.ts ├── README.md ├── package.json ├── tsconfig.json ├── LICENSE └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .next 2 | node_modules 3 | .DS_Store 4 | .env -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | future: { 3 | webpack5: true 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /public/static/empty.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gimmixorg/primitive-objkts/HEAD/public/static/empty.jpg -------------------------------------------------------------------------------- /src/app/features/validators.ts: -------------------------------------------------------------------------------- 1 | export const validateETH = (address: string) => 2 | /^(0x){1}[0-9a-fA-F]{40}$/i.test(address); 3 | 4 | export const validateTZ = (address: string) => 5 | /^(tz|KT)[0-9a-z]{34}$/i.test(address); 6 | -------------------------------------------------------------------------------- /src/app/features/useIsElectricObject.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | const useIsElectricObject = () => { 4 | const [isEO, setEO] = useState(false); 5 | useEffect(() => { 6 | setEO( 7 | window.navigator.userAgent.toLowerCase().includes('electric objects') 8 | ); 9 | }, []); 10 | return isEO; 11 | }; 12 | 13 | export default useIsElectricObject; 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PRIMITIVE OBJKTS 2 | 3 | > Get a live updating view of your NFT collection onto your digital frame, or combine your friends and favorites into one big group frame to share. 4 | 5 | To run on your own server: 6 | ``` 7 | yarn install 8 | yarn start 9 | ``` 10 | 11 | You'll need to add a .env file with the following information for hic et nunc support: 12 | ``` 13 | CONSEIL_SERVER= 14 | CONSEIL_KEY= 15 | ``` 16 | 17 | You can get your server URL and api key from https://nautilus.cloud 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "primitive-objkts", 3 | "version": "0.1.0", 4 | "scripts": { 5 | "dev": "next dev", 6 | "build": "next build", 7 | "start": "next" 8 | }, 9 | "dependencies": { 10 | "conseiljs": "^5.0.8-3", 11 | "loglevel": "^1.7.1", 12 | "next": "^10.1.3", 13 | "react": "^17.0.2", 14 | "react-dom": "^17.0.2", 15 | "react-icons": "^4.2.0", 16 | "swr": "^0.5.5" 17 | }, 18 | "devDependencies": { 19 | "@types/node": "^14.14.41", 20 | "@types/react": "^17.0.3", 21 | "typescript": "^4.2.4" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import MainLayout from '@app/layouts/MainLayout'; 2 | import { NextComponentType, NextPageContext } from 'next'; 3 | import type { AppInitialProps } from 'next/app'; 4 | 5 | type AppProps = AppInitialProps & { 6 | Component: NextComponentType & { 7 | noLayout?: boolean; 8 | }; 9 | }; 10 | 11 | function MyApp({ Component, pageProps }: AppProps) { 12 | if (Component.noLayout) return ; 13 | return ( 14 | 15 | 16 | 17 | ); 18 | } 19 | 20 | export default MyApp; 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@app/*": ["src/app/*"], 6 | "@server/*": ["src/server/*"] 7 | }, 8 | "target": "es5", 9 | "lib": ["dom", "dom.iterable", "esnext"], 10 | "allowJs": true, 11 | "skipLibCheck": true, 12 | "strict": true, 13 | "noUnusedLocals": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noEmit": true, 16 | "esModuleInterop": true, 17 | "module": "esnext", 18 | "moduleResolution": "node", 19 | "resolveJsonModule": true, 20 | "isolatedModules": true, 21 | "jsx": "preserve" 22 | }, 23 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 24 | "exclude": ["node_modules"] 25 | } 26 | -------------------------------------------------------------------------------- /src/app/features/useOpenSeaAPI.ts: -------------------------------------------------------------------------------- 1 | import useSWR from 'swr'; 2 | 3 | export type NFTOpenSea = { 4 | id: number; 5 | image_url: string; 6 | name: string; 7 | description: string; 8 | owner: { 9 | user: { 10 | username: string; 11 | }; 12 | address: string; 13 | profile_img_url: string; 14 | }; 15 | }; 16 | 17 | const useOpenSeaAPI = (address: string) => { 18 | const { data } = useSWR<{ assets: NFTOpenSea[] }>( 19 | address 20 | ? `https://api.opensea.io/api/v1/assets?owner=${address}&format=json&limit=10&order_direction=desc&order_by=token_id` 21 | : null, 22 | { 23 | dedupingInterval: 60 * 60 * 1000, 24 | refreshInterval: 60 * 60 * 1000 25 | } 26 | ); 27 | const { assets } = data || { assets: [] }; 28 | return assets; 29 | }; 30 | 31 | export default useOpenSeaAPI; 32 | -------------------------------------------------------------------------------- /src/app/features/useHicEtNunc.ts: -------------------------------------------------------------------------------- 1 | import { AddressInputType } from '@app/components/AddressTextInputs'; 2 | import useSWR from 'swr'; 3 | 4 | export type NFTHicEtNunc = { 5 | id: string; 6 | name: string; 7 | description: string; 8 | tags: string[]; 9 | symbol: string; 10 | artifactUri: string; 11 | displayUri: string; 12 | thumbnailUri: string; 13 | creators: string[]; 14 | formats: Format[]; 15 | }; 16 | 17 | type Format = { 18 | uri: string; 19 | mimeType: string; 20 | }; 21 | 22 | const useHicEtNunc = (user: AddressInputType) => { 23 | const { data } = useSWR( 24 | user ? `/api/tezos?address=${user.address}&type=${user.type}` : null, 25 | { 26 | dedupingInterval: 60 * 60 * 1000, 27 | refreshInterval: 60 * 60 * 1000 28 | } 29 | ); 30 | return data || []; 31 | }; 32 | 33 | export default useHicEtNunc; 34 | -------------------------------------------------------------------------------- /src/pages/api/tezos.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getCollectionForAddress, 3 | getCreationsForAddress 4 | } from '@server/services/Tezos'; 5 | import { NextApiHandler } from 'next'; 6 | import { ApiError } from 'next/dist/next-server/server/api-utils'; 7 | 8 | const api: NextApiHandler = async (req, res) => { 9 | const { address, type }: { address?: string; type?: string } = req.query; 10 | if (!address) throw new ApiError(400, 'Missing address.'); 11 | if (type == 'creations') { 12 | const collection = await getCreationsForAddress(address); 13 | res.setHeader('Cache-Control', 'stale-while-revalidate=3600'); 14 | return res.json(collection.slice(0, 10)); 15 | } else { 16 | const collection = await getCollectionForAddress(address); 17 | res.setHeader('Cache-Control', 'stale-while-revalidate=3600'); 18 | return res.json(collection.slice(0, 10)); 19 | } 20 | }; 21 | 22 | export default api; 23 | -------------------------------------------------------------------------------- /src/app/components/FrameETH.tsx: -------------------------------------------------------------------------------- 1 | import useOpenSeaAPI, { NFTOpenSea } from '@app/features/useOpenSeaAPI'; 2 | import useSlideshow from '@app/features/useSlideshow'; 3 | import React from 'react'; 4 | import { AddressInputType } from './AddressTextInputs'; 5 | import FrameGenericNFT from './FrameGenericNFT'; 6 | 7 | const FrameETH = ({ 8 | user, 9 | turn, 10 | onComplete 11 | }: { 12 | user: AddressInputType; 13 | turn: number; 14 | onComplete: () => void; 15 | }) => { 16 | const collection = useOpenSeaAPI(user.address); 17 | const { nft: _nft, advanceToNext } = useSlideshow( 18 | collection, 19 | turn, 20 | onComplete 21 | ); 22 | const nft = _nft as NFTOpenSea; 23 | if (!nft) return null; 24 | return ( 25 | 32 | ); 33 | }; 34 | 35 | export default React.memo(FrameETH); 36 | -------------------------------------------------------------------------------- /src/app/features/useConfig.ts: -------------------------------------------------------------------------------- 1 | import { AddressInputType } from '@app/components/AddressTextInputs'; 2 | import { useRouter } from 'next/dist/client/router'; 3 | import { validateETH, validateTZ } from './validators'; 4 | 5 | type Config = { 6 | addresses: AddressInputType[]; 7 | mode: 'ordered' | 'random'; 8 | time: number; 9 | unit: 's' | 'm' | 'h'; 10 | fill: 'contain' | 'cover'; 11 | metadata: 'show' | 'hide'; 12 | }; 13 | 14 | const useConfig = (): Config => { 15 | const { query } = useRouter(); 16 | if (!query?.c) 17 | return { 18 | addresses: [], 19 | time: 0, 20 | mode: 'ordered', 21 | unit: 'm', 22 | fill: 'contain', 23 | metadata: 'show' 24 | }; 25 | const config: Config = JSON.parse(query.c as string); 26 | let { addresses, mode, time, unit, fill, metadata } = config; 27 | addresses = addresses.filter( 28 | a => validateETH(a.address) || validateTZ(a.address) 29 | ); 30 | 31 | return { addresses, mode, time, unit, fill, metadata }; 32 | }; 33 | 34 | export default useConfig; 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 GIMMIX LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/app/features/useSlideshow.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { NFTOpenSea } from './useOpenSeaAPI'; 3 | import { NFTHicEtNunc } from './useHicEtNunc'; 4 | import useConfig from './useConfig'; 5 | 6 | const useSlideshow = ( 7 | collection: Array, 8 | turn: number, 9 | onComplete: () => void 10 | ) => { 11 | const [index, setIndex] = useState(0); 12 | const nft = collection?.[index]; 13 | const { time, unit, mode } = useConfig(); 14 | 15 | useEffect(() => { 16 | if (mode == 'ordered') setIndex(0); 17 | else if (mode == 'random') 18 | setIndex(Math.floor(Math.random() * collection.length)); 19 | }, [collection?.length, turn, mode]); 20 | 21 | useEffect(() => { 22 | const timeout = setTimeout( 23 | () => advanceToNext(), 24 | time * (unit == 's' ? 1000 : unit == 'h' ? 60 * 60 * 1000 : 60 * 1000) 25 | ); 26 | return () => clearTimeout(timeout); 27 | }, [index, collection?.length]); 28 | 29 | const advanceToNext = () => { 30 | if (mode == 'ordered') { 31 | if (index + 1 >= collection.length) return onComplete(); 32 | setIndex(index => index + 1); 33 | } else if (mode == 'random') { 34 | return onComplete(); 35 | } 36 | }; 37 | 38 | return { nft, advanceToNext }; 39 | }; 40 | 41 | export default useSlideshow; 42 | -------------------------------------------------------------------------------- /src/app/components/FrameTZ.tsx: -------------------------------------------------------------------------------- 1 | import useTezos, { NFTHicEtNunc } from '@app/features/useHicEtNunc'; 2 | import useSlideshow from '@app/features/useSlideshow'; 3 | import React, { useState } from 'react'; 4 | import { AddressInputType } from './AddressTextInputs'; 5 | import FrameGenericNFT from './FrameGenericNFT'; 6 | 7 | const FrameTZ = ({ 8 | user, 9 | turn, 10 | onComplete 11 | }: { 12 | user: AddressInputType; 13 | turn: number; 14 | onComplete: () => void; 15 | }) => { 16 | const collection = useTezos(user); 17 | const { nft: _nft, advanceToNext } = useSlideshow( 18 | collection, 19 | turn, 20 | onComplete 21 | ); 22 | const [tryingAlt, setTryingAlt] = useState(false); 23 | const onFail = () => { 24 | if (tryingAlt) { 25 | setTryingAlt(false); 26 | advanceToNext(); 27 | } else { 28 | setTryingAlt(true); 29 | } 30 | }; 31 | const nft = _nft as NFTHicEtNunc; 32 | if (!nft) return null; 33 | return ( 34 | 44 | ); 45 | }; 46 | 47 | export default React.memo(FrameTZ); 48 | -------------------------------------------------------------------------------- /src/pages/frame.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Head from 'next/head'; 3 | import useConfig from '@app/features/useConfig'; 4 | import FrameETH from '@app/components/FrameETH'; 5 | import FrameTZ from '@app/components/FrameTZ'; 6 | 7 | const Frame = () => { 8 | const { addresses } = useConfig(); 9 | const [index, setIndex] = useState(0); 10 | const [turn, setTurn] = useState(0); 11 | const nextUser = () => { 12 | setIndex(index => (index + 1) % (addresses.length || 1)); 13 | setTurn(turn => turn + 1); 14 | }; 15 | const activeUser = addresses[index]; 16 | return ( 17 |
18 | 19 | 20 | Art from {addresses.map(a => a.name || a.address).join(', ')} 21 | 22 | 23 | 24 | 28 | 29 | a.name || a.address) 33 | .join(', ')}`} 34 | /> 35 | 41 | 45 | 49 | 50 | {addresses.length == 0 &&
} 51 | {activeUser?.address.startsWith('0x') && ( 52 | 53 | )} 54 | {activeUser?.address.startsWith('tz') && ( 55 | 56 | )} 57 | 75 |
76 | ); 77 | }; 78 | 79 | Frame.noLayout = true; 80 | 81 | export default Frame; 82 | -------------------------------------------------------------------------------- /src/app/components/FrameGenericNFT.tsx: -------------------------------------------------------------------------------- 1 | import useConfig from '@app/features/useConfig'; 2 | import React from 'react'; 3 | import { AddressInputType } from './AddressTextInputs'; 4 | 5 | const FrameGenericNFT = ({ 6 | image, 7 | name, 8 | description, 9 | user, 10 | advanceToNext 11 | }: { 12 | image: string; 13 | name: string; 14 | description: string; 15 | user: AddressInputType; 16 | advanceToNext: () => void; 17 | }) => { 18 | const { fill, metadata } = useConfig(); 19 | return ( 20 |
21 | 22 | {metadata == 'show' && ( 23 |
24 |
{name}
25 |
{description}
26 |
27 | {user.type == 'collection' ? ( 28 | <> 29 | In{' '} 30 | {user.name || 31 | `${user.address.slice(0, 6)}...${user.address.slice(-4)}`} 32 | 's collection 33 | 34 | ) : ( 35 | <> 36 | Created by{' '} 37 | {user.name || 38 | `${user.address.slice(0, 6)}...${user.address.slice(-4)}`} 39 | 40 | )} 41 |
42 |
43 | )} 44 | 100 |
101 | ); 102 | }; 103 | 104 | export default FrameGenericNFT; 105 | -------------------------------------------------------------------------------- /src/app/components/AddressTextInputs.tsx: -------------------------------------------------------------------------------- 1 | import { validateETH, validateTZ } from '@app/features/validators'; 2 | import React, { FormEventHandler, useEffect, useState } from 'react'; 3 | import AddressPreview from './AddressPreview'; 4 | 5 | export type AddressInputType = { 6 | name?: string; 7 | address: string; 8 | type: string; 9 | }; 10 | 11 | const AddressTextInputs = ({ 12 | addresses, 13 | setAddresses 14 | }: { 15 | addresses: AddressInputType[]; 16 | setAddresses: React.Dispatch>; 17 | }) => { 18 | const [text, setText] = useState(''); 19 | const onAddAddress: FormEventHandler = e => { 20 | e.preventDefault(); 21 | if (!isValid) return; 22 | setAddresses(a => [...a, { address: text, type: 'collection' }]); 23 | setText(''); 24 | }; 25 | const onChangeType = (index: number, type: string) => { 26 | setAddresses(_arr => { 27 | const arr = [..._arr]; 28 | arr[index].type = type; 29 | return arr; 30 | }); 31 | }; 32 | const [isValid, setIsValid] = useState(false); 33 | useEffect(() => { 34 | if (text.length) setIsValid(validateETH(text) || validateTZ(text)); 35 | }, [text]); 36 | return ( 37 | <> 38 | {addresses.length > 0 && ( 39 |
40 | {addresses.map((address, i) => ( 41 | onChangeType(i, type)} 46 | updateName={name => 47 | setAddresses(_addresses => { 48 | const addresses = [..._addresses]; 49 | addresses[i].name = name; 50 | return addresses; 51 | }) 52 | } 53 | onRemoveClick={() => 54 | setAddresses(_addresses => 55 | _addresses.filter(_address => _address != address) 56 | ) 57 | } 58 | /> 59 | ))} 60 |
61 | )} 62 | {addresses.length < 10 && ( 63 |
64 | setText(e.target.value)} 69 | placeholder="Address starting with 0x... or tz..." 70 | /> 71 | {' '} 74 |
75 | )} 76 | 104 | 105 | ); 106 | }; 107 | 108 | export default AddressTextInputs; 109 | -------------------------------------------------------------------------------- /src/app/components/AddressPreview.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { AddressInputType } from './AddressTextInputs'; 3 | import { FaMinus, FaUserAlt } from 'react-icons/fa'; 4 | import { validateETH } from '@app/features/validators'; 5 | const AddressPreview = ({ 6 | number, 7 | user, 8 | onChangeType, 9 | updateName, 10 | onRemoveClick 11 | }: { 12 | number: number; 13 | user: AddressInputType; 14 | onChangeType: (type: string) => void; 15 | updateName: (name: string) => void; 16 | onRemoveClick: () => void; 17 | }) => { 18 | const [nameInput, setNameInput] = useState(''); 19 | const [isSettingName, setIsSettingName] = useState(false); 20 | const hex = validateETH(user.address) 21 | ? user?.address?.slice(-6) 22 | : parseInt(user.address, 36) 23 | .toString(16) 24 | .replace(/0/g, '') 25 | .slice(-11) 26 | .slice(0, 6); 27 | const onSaveNameClick = () => { 28 | updateName(nameInput.slice(0, 20)); 29 | setIsSettingName(false); 30 | }; 31 | return ( 32 |
33 |
{number}
34 | {!isSettingName ? ( 35 | <> 36 |
setIsSettingName(true)}> 37 | {user.name || } 38 |
39 |
40 | {user.address.slice(0, 6)}...{user.address.slice(-4)} 41 |
42 |
43 |
44 | {validateETH(user.address) ? ( 45 | 'ETH COLLECTION' 46 | ) : ( 47 | <> 48 | TZ{' '} 49 | 56 | 57 | )} 58 |
59 |
60 | 63 |
64 | 65 | ) : ( 66 | <> 67 | setNameInput(e.target.value)} 72 | className="name-input" 73 | placeholder={`Set nickname for ${user.address.slice( 74 | 0, 75 | 6 76 | )}...${user.address.slice(-4)}`} 77 | /> 78 |
79 | 80 | 81 |
82 | 83 | )} 84 | 161 |
162 | ); 163 | }; 164 | 165 | export default AddressPreview; 166 | -------------------------------------------------------------------------------- /src/app/layouts/MainLayout.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head'; 2 | import React, { FunctionComponent } from 'react'; 3 | import Link from 'next/link'; 4 | 5 | const MainLayout: FunctionComponent = ({ children }) => { 6 | return ( 7 |
8 | 9 | PRIMITVE OBJKTS 10 | 11 | 15 | 16 | 20 | 26 | 30 | 34 | 35 |
36 | WORKS WITH{' '} 37 | 38 | HIC ET NUNC 39 | {' '} 40 | OBJKTs AND ETHEREUM NFTs. AUTO-ADJUSTS FOR ANY SCREEN, INCLUDING 41 | ELECTRIC OBJECTS EO1 AND EO2! 42 |
43 | 44 | 45 | 52 | 60 | 61 | 62 | 68 | 69 |
70 |
71 |
72 |
73 | 74 | PRIMITIVE OBJKTS 75 | 76 |
77 |
78 |
79 |
{children}
80 | 93 | 189 | 208 |
209 | ); 210 | }; 211 | 212 | export default MainLayout; 213 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import AddressTextInputs, { 2 | AddressInputType 3 | } from '@app/components/AddressTextInputs'; 4 | import React, { useState } from 'react'; 5 | 6 | const IndexPage = () => { 7 | const [addresses, setAddresses] = useState([ 8 | { 9 | address: 'tz1gqaKjfQBhUMCE6LhbkpuittRiWv5Z6w38', 10 | type: 'creations', 11 | name: 'jjjjjjjjjjohn' 12 | }, 13 | { 14 | address: 'tz1iGcF8HVtYJpCFAsLX6nwYgQgDR162VNBi', 15 | type: 'creations', 16 | name: 'kyt' 17 | }, 18 | { 19 | address: '0xcda72070e455bb31c7690a170224ce43623d0b6f', 20 | type: 'collection', 21 | name: 'Foundation' 22 | }, 23 | { address: 'tz1cHVbttfksw95gn51fdxdra4Zbqe36LspP', type: 'creations' } 24 | ]); 25 | const [time, setTime] = useState(10); 26 | const [unit, setUnit] = useState<'s' | 'm' | 'h'>('m'); 27 | const [mode, setMode] = useState<'ordered' | 'random'>('ordered'); 28 | const [metadata, setMetadata] = useState<'show' | 'hide'>('show'); 29 | const [fill, setFill] = useState<'contain' | 'cover'>('contain'); 30 | const config = { addresses, time, unit, mode, fill, metadata }; 31 | return ( 32 |
33 |
34 |
PUT WEB3 ART ON YOUR EO1 or EO2
35 |
36 | Get a live updating view of your NFT collection onto your digital 37 | frame, or combine your friends and favorites into one big group frame 38 | to share. 39 |
40 |
41 | Open this page in a desktop browser to create your frame. 42 |
43 |
44 | Enter up to 10 wallet addresses (Ethereum or Tezos). 45 |
46 |
47 | 51 |
52 |
53 |
Customize
54 |
55 |
Loop mode
56 |
57 |
58 |
setMode('ordered')} 60 | className={`mode ${mode == 'ordered' ? 'selected' : ''}`} 61 | > 62 | Ordered 63 |
64 |
setMode('random')} 66 | className={`mode ${mode == 'random' ? 'selected' : ''}`} 67 | > 68 | Random 69 |
70 |
71 |
72 |
73 |
Display mode
74 |
75 |
76 |
setFill('contain')} 78 | className={`fill ${fill == 'contain' ? 'selected' : ''}`} 79 | > 80 | Fit 81 |
82 |
setFill('cover')} 84 | className={`fill ${fill == 'cover' ? 'selected' : ''}`} 85 | > 86 | Fill 87 |
88 |
89 |
90 |
91 |
Metadata
92 |
93 |
setMetadata('show')} 98 | > 99 | Show 100 |
101 |
setMetadata('hide')} 106 | > 107 | Hide 108 |
109 |
110 |
111 |
112 |
Time per piece
113 |
114 | setTime(parseFloat(e.target.value))} 118 | min={1} 119 | required 120 | /> 121 |
122 |
setUnit('s')} 124 | className={`unit ${unit == 's' ? 'selected' : ''}`} 125 | > 126 | sec 127 |
128 |
setUnit('m')} 130 | className={`unit ${unit == 'm' ? 'selected' : ''}`} 131 | > 132 | min 133 |
134 |
setUnit('h')} 136 | className={`unit ${unit == 'h' ? 'selected' : ''}`} 137 | > 138 | hr 139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |