├── src ├── utils │ ├── reward-center.ts │ ├── tokens.ts │ ├── date.ts │ ├── activity.ts │ ├── debounce.ts │ ├── numbers.ts │ ├── promises.ts │ ├── referral.ts │ ├── cookies.ts │ ├── marketplaces.ts │ ├── assets.ts │ ├── hyperspace.ts │ ├── auction-house.ts │ ├── price.ts │ └── solana.ts ├── components │ ├── Chart │ │ ├── index.ts │ │ ├── ChartPreview.tsx │ │ ├── Chart.tsx │ │ ├── StyledBarChart.tsx │ │ ├── ChartTimeseries.tsx │ │ └── TinyLineChart.tsx │ ├── Carousel │ │ ├── index.ts │ │ └── Carousel.tsx │ ├── ReferralsAnimation.tsx │ ├── Price.tsx │ ├── Toolbar.tsx │ ├── Typography.tsx │ ├── Toggle.tsx │ ├── SortingArrow.tsx │ ├── CheckBox.tsx │ ├── Flex.tsx │ ├── Tooltip.tsx │ ├── Avatar.tsx │ ├── Grid.tsx │ ├── ButtonGroup.tsx │ ├── Hero.tsx │ ├── Attribute.tsx │ ├── Image.tsx │ ├── Form.tsx │ ├── Lightbox.tsx │ ├── Leaderboard │ │ ├── Info.tsx │ │ └── Banner.tsx │ ├── Select.tsx │ ├── Offer.tsx │ ├── Drop.tsx │ └── BulkListing │ │ └── ListingItem.tsx ├── infrastructure │ ├── cookies │ │ ├── index.ts │ │ └── cookies.ts │ ├── logger │ │ ├── index.ts │ │ └── log-error-debug.ts │ └── api │ │ ├── index.ts │ │ ├── utils.ts │ │ ├── api.ts │ │ └── fetcher.ts ├── modules │ ├── reward-center │ │ ├── index.ts │ │ └── RewardCenterProgram.ts │ ├── sol.ts │ ├── time.ts │ ├── address.ts │ ├── number.ts │ ├── candymachine │ │ └── index.ts │ └── bugsnag.ts ├── hooks │ ├── nft │ │ ├── index.ts │ │ ├── useDetails.ts │ │ ├── useOffers.ts │ │ └── useActivities.ts │ ├── nav.ts │ ├── collection │ │ ├── useDetail.ts │ │ ├── useTrendingSearch.ts │ │ └── useSeries.ts │ ├── currencies.ts │ ├── clipboard.tsx │ ├── outsidealert.ts │ ├── sidebar.ts │ ├── mobilesearch.ts │ ├── useAction.ts │ ├── login.ts │ ├── countdown.ts │ ├── metaplex.ts │ └── globalsearch.ts ├── Brice-Bold.woff2 ├── Hauora-Bold.woff2 ├── Hauora-Regular.woff2 ├── typings │ ├── i18next.d.ts │ └── swr.d.ts ├── libs │ └── consts.ts ├── fonts.ts ├── pages │ ├── r │ │ └── [refId].tsx │ ├── referrals │ │ └── index.tsx │ ├── og │ │ └── collections │ │ │ └── [slug].tsx │ └── api │ │ └── instructions │ │ ├── get-offers.tsx │ │ ├── get-listing.tsx │ │ ├── is-local-listing.tsx │ │ ├── close-listing.tsx │ │ ├── is-local-offer.tsx │ │ ├── create-listing.tsx │ │ ├── update-listing.tsx │ │ ├── buy-listing.tsx │ │ ├── close-offer.tsx │ │ ├── create-offer.tsx │ │ └── accept-offer.tsx ├── providers │ ├── BulkListProvider.tsx │ ├── WalletContextProvider.tsx │ ├── PointsProvider.tsx │ ├── AuctionHouseProvider.tsx │ └── CurrencyProvider.tsx └── app.config.ts ├── public ├── favicon.ico ├── fonts │ ├── Brice-Bold.otf │ ├── Hauora-Bold.otf │ └── Hauora-Regular.otf ├── images │ ├── placeholder.png │ ├── open_all_night.jpg │ ├── gradients │ │ ├── gradient-1.png │ │ ├── gradient-2.png │ │ ├── gradient-3.png │ │ ├── gradient-4.png │ │ ├── gradient-5.png │ │ ├── gradient-6.png │ │ ├── gradient-7.png │ │ └── gradient-8.png │ ├── holaplex-logo-text.png │ ├── launchpad │ │ └── motley-launchpad-nft.png │ ├── play.svg │ ├── card-list-active.svg │ ├── card-list.svg │ ├── live-light.svg │ ├── card-grid-large-active.svg │ ├── moonrank-logo.svg │ ├── card-grid-large.svg │ ├── uturn.svg │ ├── marketplaces │ │ ├── exchangeart.svg │ │ ├── elixir.svg │ │ ├── tensor-swap.svg │ │ ├── hyperspace.svg │ │ ├── magiceden.svg │ │ ├── solanart.svg │ │ ├── hadeswap.svg │ │ └── opensea.svg │ ├── leaderboard │ │ ├── popup │ │ │ ├── arrow.svg │ │ │ └── nft.svg │ │ ├── info.svg │ │ ├── bonuses │ │ │ └── gradient-border.svg │ │ └── medals │ │ │ └── gold.svg │ ├── card-grid-small-active.svg │ ├── card-grid-small.svg │ └── moon.svg ├── legal │ ├── motley_dao-privacy_policy.pdf │ └── motley_dao-terms_of_service.pdf ├── locales │ └── en │ │ ├── offers.json │ │ ├── launchpad.json │ │ ├── analytics.json │ │ ├── home.json │ │ ├── leaderboard.json │ │ ├── nft.json │ │ ├── profile.json │ │ ├── collection.json │ │ ├── referrals.json │ │ └── common.json └── vercel.svg ├── .config └── next │ └── env.js ├── prettier.config.js ├── styles ├── error.css ├── collection.css └── globals.css ├── postcss.config.js ├── .prettierrc ├── turbo.json ├── next-i18next.config.js ├── vercel.json ├── .env ├── .gitignore ├── tsconfig.json ├── .github └── workflows │ ├── build-vercel.yaml │ └── build-yarn.yaml ├── next.config.js ├── .eslintrc.json ├── Dockerfile └── package.json /src/utils/reward-center.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Chart/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Chart'; 2 | -------------------------------------------------------------------------------- /src/infrastructure/cookies/index.ts: -------------------------------------------------------------------------------- 1 | export * from './cookies'; 2 | -------------------------------------------------------------------------------- /src/infrastructure/logger/index.ts: -------------------------------------------------------------------------------- 1 | export * from './log-error-debug'; 2 | -------------------------------------------------------------------------------- /src/modules/reward-center/index.ts: -------------------------------------------------------------------------------- 1 | export * from './RewardCenterProgram'; 2 | -------------------------------------------------------------------------------- /src/hooks/nft/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useDetails'; 2 | export * from './useOffers'; 3 | -------------------------------------------------------------------------------- /src/infrastructure/api/index.ts: -------------------------------------------------------------------------------- 1 | export * from './api'; 2 | export * from './fetcher'; 3 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/motleylabs/nightmarket-oss/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /.config/next/env.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | NEXT_PUBLIC_RELEASE: new Date().toISOString(), 3 | } 4 | -------------------------------------------------------------------------------- /src/Brice-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/motleylabs/nightmarket-oss/HEAD/src/Brice-Bold.woff2 -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [require('prettier-plugin-tailwindcss')], 3 | }; 4 | -------------------------------------------------------------------------------- /src/Hauora-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/motleylabs/nightmarket-oss/HEAD/src/Hauora-Bold.woff2 -------------------------------------------------------------------------------- /src/Hauora-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/motleylabs/nightmarket-oss/HEAD/src/Hauora-Regular.woff2 -------------------------------------------------------------------------------- /styles/error.css: -------------------------------------------------------------------------------- 1 | .next-error-h1, .next-error-h1+div h2 { 2 | color: #222; 3 | opacity: 0.9; 4 | } 5 | -------------------------------------------------------------------------------- /public/fonts/Brice-Bold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/motleylabs/nightmarket-oss/HEAD/public/fonts/Brice-Bold.otf -------------------------------------------------------------------------------- /public/fonts/Hauora-Bold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/motleylabs/nightmarket-oss/HEAD/public/fonts/Hauora-Bold.otf -------------------------------------------------------------------------------- /public/images/placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/motleylabs/nightmarket-oss/HEAD/public/images/placeholder.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/fonts/Hauora-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/motleylabs/nightmarket-oss/HEAD/public/fonts/Hauora-Regular.otf -------------------------------------------------------------------------------- /public/images/open_all_night.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/motleylabs/nightmarket-oss/HEAD/public/images/open_all_night.jpg -------------------------------------------------------------------------------- /public/images/gradients/gradient-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/motleylabs/nightmarket-oss/HEAD/public/images/gradients/gradient-1.png -------------------------------------------------------------------------------- /public/images/gradients/gradient-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/motleylabs/nightmarket-oss/HEAD/public/images/gradients/gradient-2.png -------------------------------------------------------------------------------- /public/images/gradients/gradient-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/motleylabs/nightmarket-oss/HEAD/public/images/gradients/gradient-3.png -------------------------------------------------------------------------------- /public/images/gradients/gradient-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/motleylabs/nightmarket-oss/HEAD/public/images/gradients/gradient-4.png -------------------------------------------------------------------------------- /public/images/gradients/gradient-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/motleylabs/nightmarket-oss/HEAD/public/images/gradients/gradient-5.png -------------------------------------------------------------------------------- /public/images/gradients/gradient-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/motleylabs/nightmarket-oss/HEAD/public/images/gradients/gradient-6.png -------------------------------------------------------------------------------- /public/images/gradients/gradient-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/motleylabs/nightmarket-oss/HEAD/public/images/gradients/gradient-7.png -------------------------------------------------------------------------------- /public/images/gradients/gradient-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/motleylabs/nightmarket-oss/HEAD/public/images/gradients/gradient-8.png -------------------------------------------------------------------------------- /public/images/holaplex-logo-text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/motleylabs/nightmarket-oss/HEAD/public/images/holaplex-logo-text.png -------------------------------------------------------------------------------- /public/legal/motley_dao-privacy_policy.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/motleylabs/nightmarket-oss/HEAD/public/legal/motley_dao-privacy_policy.pdf -------------------------------------------------------------------------------- /public/legal/motley_dao-terms_of_service.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/motleylabs/nightmarket-oss/HEAD/public/legal/motley_dao-terms_of_service.pdf -------------------------------------------------------------------------------- /public/images/launchpad/motley-launchpad-nft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/motleylabs/nightmarket-oss/HEAD/public/images/launchpad/motley-launchpad-nft.png -------------------------------------------------------------------------------- /src/infrastructure/api/utils.ts: -------------------------------------------------------------------------------- 1 | export function getBaseApiUrl() { 2 | return { 3 | url: `${process.env.NEXT_PUBLIC_ANDROMEDA_ENDPOINT}`, 4 | }; 5 | } 6 | -------------------------------------------------------------------------------- /src/typings/i18next.d.ts: -------------------------------------------------------------------------------- 1 | import 'i18next'; 2 | 3 | declare module 'i18next' { 4 | interface CustomTypeOptions { 5 | returnNull: false; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true, 6 | "bracketSpacing": true, 7 | "printWidth": 100 8 | } 9 | -------------------------------------------------------------------------------- /src/infrastructure/api/api.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | export const api = axios.create({ 4 | baseURL: process.env.NEXT_PUBLIC_ANDROMEDA_ENDPOINT ?? '/api', 5 | }); 6 | -------------------------------------------------------------------------------- /src/hooks/nft/useDetails.ts: -------------------------------------------------------------------------------- 1 | import useSWR from 'swr'; 2 | 3 | import type { Nft } from '../../typings'; 4 | 5 | export const useDetails = (address: string) => useSWR(`/nfts/${address}`); 6 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "pipeline": { 4 | "build": { 5 | "outputs": [".next/**"] 6 | }, 7 | "lint": {} 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/components/Carousel/index.ts: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic'; 2 | 3 | const DynamicCarousel = dynamic(() => import('./Carousel'), { ssr: false }); 4 | 5 | export default DynamicCarousel; 6 | -------------------------------------------------------------------------------- /src/infrastructure/api/fetcher.ts: -------------------------------------------------------------------------------- 1 | import { api } from './api'; 2 | 3 | export async function fetcher(url: string, query?: string): Promise { 4 | return api.get(`${url}${query ? `?${query}` : ''}`).then((r) => r.data); 5 | } 6 | -------------------------------------------------------------------------------- /next-i18next.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | i18n: { 5 | defaultLocale: 'en', 6 | locales: ['en'], 7 | }, 8 | localePath: path.resolve('./public/locales'), 9 | returnNull: false, 10 | }; 11 | -------------------------------------------------------------------------------- /src/utils/tokens.ts: -------------------------------------------------------------------------------- 1 | export function hideTokenDetails(str: string, isTwoDotted?: boolean): string { 2 | const firstFour = str.slice(0, 4); 3 | const lastFour = str.slice(-4); 4 | 5 | return `${firstFour}..${isTwoDotted ? '' : '..'}${lastFour}`; 6 | } 7 | -------------------------------------------------------------------------------- /public/images/play.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/images/card-list-active.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/hooks/nav.ts: -------------------------------------------------------------------------------- 1 | import { Dispatch, SetStateAction, useState } from 'react'; 2 | 3 | export default function useNavigation(): [boolean, Dispatch>] { 4 | const [showNav, setShowNav] = useState(false); 5 | 6 | return [showNav, setShowNav]; 7 | } 8 | -------------------------------------------------------------------------------- /src/libs/consts.ts: -------------------------------------------------------------------------------- 1 | export const Time = { 2 | SECOND: 1, 3 | SECOND_MS: 1e3, 4 | MINUTE: 60, 5 | MINUTE_MS: 60e3, 6 | HOUR: 3600, 7 | HOUR_MS: 3600e3, 8 | DAY: 86400, 9 | DAY_MS: 86400e3, 10 | WEEK: 604800, 11 | WEEK_MS: 604800e3, 12 | } as const; 13 | -------------------------------------------------------------------------------- /public/images/card-list.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/hooks/nft/useOffers.ts: -------------------------------------------------------------------------------- 1 | import useSWR from 'swr'; 2 | 3 | import type { Offer } from '../../typings'; 4 | 5 | export const useOffers = (address: string) => 6 | useSWR(address !== '' ? `/nfts/offers?address=${address}` : null, { 7 | revalidateOnFocus: false, 8 | }); 9 | -------------------------------------------------------------------------------- /src/infrastructure/logger/log-error-debug.ts: -------------------------------------------------------------------------------- 1 | export function logErrorDebug(...args: Parameters) { 2 | if (process.env.DEBUG !== 'true') { 3 | return; 4 | } 5 | 6 | // eslint-disable-next-line no-console 7 | console.error('[DEBUG ERROR]', ...args); 8 | } 9 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "crons": [ 3 | { 4 | "path": "/collection/madlads", 5 | "schedule": "* * * * *" 6 | } 7 | ], 8 | "git": { 9 | "deploymentEnabled": { 10 | "holaplex-legacy": false 11 | } 12 | }, 13 | "github": { 14 | "silent": true 15 | }, 16 | "trailingSlash": false 17 | } 18 | -------------------------------------------------------------------------------- /src/hooks/collection/useDetail.ts: -------------------------------------------------------------------------------- 1 | import useSWR from 'swr'; 2 | 3 | import type { Collection } from '../../typings'; 4 | 5 | export const useDetail = (slug: string) => 6 | useSWR(`/collections/${slug}`, { 7 | revalidateOnFocus: false, 8 | revalidateIfStale: false, 9 | refreshInterval: 600000, 10 | }); 11 | -------------------------------------------------------------------------------- /src/typings/swr.d.ts: -------------------------------------------------------------------------------- 1 | import type * as swr from 'swr'; 2 | 3 | declare module 'swr' { 4 | export type MutateFn = ( 5 | data?: Data | Promise | mutateCallback, 6 | shouldRevalidate?: boolean 7 | ) => Promise; 8 | export interface Cache extends swr.Cache { 9 | clear(): void; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /public/images/live-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/utils/date.ts: -------------------------------------------------------------------------------- 1 | import { formatDistanceToNow, parseISO } from 'date-fns'; 2 | 3 | export function formatToNow(date: number): string | undefined { 4 | if (!date) { 5 | return undefined; 6 | } 7 | 8 | const createdAt = new Date(date * 1000).toISOString(); 9 | 10 | return formatDistanceToNow(parseISO(createdAt), { addSuffix: true }); 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/activity.ts: -------------------------------------------------------------------------------- 1 | export const getActivityTypes = (activity: 'listing' | 'bid' | 'transaction'): string[] => { 2 | switch (activity) { 3 | case 'listing': 4 | return ['listing', 'delisting', 'updatelisting']; 5 | case 'bid': 6 | return ['bid', 'cancelbid', 'updatebid']; 7 | default: 8 | return ['transaction']; 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /src/hooks/currencies.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | 3 | import { CurrencyContext } from '../providers/CurrencyProvider'; 4 | 5 | export function useCurrencies() { 6 | const context = useContext(CurrencyContext); 7 | 8 | if (context === null) { 9 | throw new Error('useCurrencies must be used within an CurrencyProvider'); 10 | } 11 | 12 | return context; 13 | } 14 | -------------------------------------------------------------------------------- /src/hooks/nft/useActivities.ts: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/router'; 2 | import useSWR from 'swr'; 3 | 4 | import type { NftActivitiesData } from '../../typings'; 5 | 6 | export const useActivities = () => { 7 | const { query } = useRouter(); 8 | 9 | return useSWR(`/nfts/activities?address=${query.address}`, { 10 | refreshInterval: 10 * 1000, 11 | }); 12 | }; 13 | -------------------------------------------------------------------------------- /public/locales/en/offers.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": { 3 | "title": "Nft Offers {{ address }}" 4 | }, 5 | "yours": "Your Offer", 6 | "all": "All Offers", 7 | "made": "made an offer", 8 | "cancel": "Cancel", 9 | "update": "Update", 10 | "view": "View", 11 | "accept": "Accept", 12 | "lastSale": "Last Sale", 13 | "noOffers": "No Offers", 14 | "placedOffer": "made an offer" 15 | } 16 | -------------------------------------------------------------------------------- /src/utils/debounce.ts: -------------------------------------------------------------------------------- 1 | export function debounce( 2 | callback: (...args: T) => PromiseLike | U, 3 | ms = 3000 4 | ) { 5 | let timer: ReturnType; 6 | 7 | return function (...args: T): Promise { 8 | clearTimeout(timer); 9 | 10 | return new Promise((resolve) => { 11 | timer = setTimeout(() => resolve(callback(...args)), ms); 12 | }); 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /src/components/ReferralsAnimation.tsx: -------------------------------------------------------------------------------- 1 | export function Animation(): JSX.Element { 2 | return
; 3 | } 4 | 5 | function Leaves() { 6 | return ( 7 |
8 | Background leaves 9 |
10 | ); 11 | } 12 | 13 | Animation.Leaves = Leaves; 14 | -------------------------------------------------------------------------------- /src/components/Price.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | 3 | import Icon from './Icon'; 4 | 5 | interface PriceProps { 6 | price: number | string | undefined; 7 | className?: string; 8 | } 9 | 10 | const Price = ({ price, className }: PriceProps) => ( 11 |
12 | 13 | {price} 14 |
15 | ); 16 | 17 | export default Price; 18 | -------------------------------------------------------------------------------- /src/fonts.ts: -------------------------------------------------------------------------------- 1 | import localFont from 'next/font/local'; 2 | 3 | export const BriceFont = localFont({ 4 | src: './Brice-Bold.woff2', 5 | weight: '700', 6 | style: 'normal', 7 | variable: '--font-brice', 8 | }); 9 | 10 | export const HauoraFont = localFont({ 11 | src: [ 12 | { path: './Hauora-Regular.woff2', weight: '400', style: 'normal' }, 13 | { path: './Hauora-Bold.woff2', weight: '700', style: 'normal' }, 14 | ], 15 | variable: '--font-hauora', 16 | }); 17 | -------------------------------------------------------------------------------- /src/hooks/collection/useTrendingSearch.ts: -------------------------------------------------------------------------------- 1 | import useSWR from 'swr'; 2 | 3 | import type { CollectionsTrendsData } from '../../typings'; 4 | 5 | export const useTrendingSearch = (searchTerm: string) => 6 | useSWR( 7 | !searchTerm ? `collections/trend?period=1d&sort_by=volume&order=desc&limit=10&offset=0` : null, 8 | { 9 | revalidateOnFocus: false, 10 | revalidateIfStale: false, 11 | refreshInterval: 600000, 12 | } 13 | ); 14 | -------------------------------------------------------------------------------- /src/pages/r/[refId].tsx: -------------------------------------------------------------------------------- 1 | import { GetServerSidePropsContext, NextPage } from 'next'; 2 | 3 | export async function getServerSideProps({ params }: GetServerSidePropsContext) { 4 | const { refId } = params!; 5 | const query = `ref=${refId}`; 6 | 7 | return { 8 | redirect: { 9 | destination: `/?${query}`, 10 | permanent: false, 11 | }, 12 | }; 13 | } 14 | 15 | const Referrals: NextPage = () => { 16 | return null; 17 | }; 18 | 19 | export default Referrals; 20 | -------------------------------------------------------------------------------- /src/utils/numbers.ts: -------------------------------------------------------------------------------- 1 | export const roundToPrecision = (number: number, decimalPlaces: 0 | 1 | 2 | 3 | 4 | 5 | 6) => { 2 | const p = Math.pow(10, decimalPlaces); 3 | return Math.round((number + Number.EPSILON) * p) / p; 4 | }; 5 | 6 | export const formatToLocaleNumber = (number: number, decimalPlaces: 0 | 1 | 2 | 3 | 4 | 5 | 6) => { 7 | return Number(Number(number).toFixed(0)).toLocaleString('en-US', { 8 | maximumFractionDigits: 3, 9 | minimumFractionDigits: 0, 10 | }); 11 | }; 12 | -------------------------------------------------------------------------------- /src/hooks/clipboard.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from 'react'; 2 | 3 | export default function useClipboard(textToCopy: string) { 4 | const [copied, setCopied] = useState(false); 5 | const copyText = useCallback(async () => { 6 | if (textToCopy) { 7 | await navigator.clipboard.writeText(textToCopy); 8 | setCopied(true); 9 | setTimeout(() => setCopied(false), 2000); 10 | } 11 | }, [textToCopy]); 12 | 13 | return { 14 | copyText, 15 | copied, 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /src/utils/promises.ts: -------------------------------------------------------------------------------- 1 | export const reduceSettledPromise = (settledPromise: PromiseSettledResult[]) => { 2 | return settledPromise.reduce( 3 | ( 4 | acc: { 5 | rejected: string[]; 6 | fulfilled: T[]; 7 | }, 8 | cur 9 | ) => { 10 | if (cur.status === 'fulfilled') acc.fulfilled.push(cur.value); 11 | if (cur.status === 'rejected') acc.rejected.push(cur.reason); 12 | return acc; 13 | }, 14 | { rejected: [], fulfilled: [] } 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /src/modules/sol.ts: -------------------------------------------------------------------------------- 1 | import { LAMPORTS_PER_SOL } from '@solana/web3.js'; 2 | 3 | /** 4 | * Converts lamports to sol 5 | */ 6 | export function toSol(lamports: number, precision = 5): number { 7 | const multiplier = Math.pow(10, precision); 8 | return Math.round((lamports / LAMPORTS_PER_SOL) * multiplier) / multiplier; 9 | } 10 | 11 | /** 12 | * Converts sol to lamports 13 | * @param priceInSol 14 | */ 15 | export function toLamports(priceInSol: number): number { 16 | return priceInSol * LAMPORTS_PER_SOL; 17 | } 18 | -------------------------------------------------------------------------------- /src/components/Toolbar.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import type { ReactNode } from 'react'; 3 | 4 | interface ToolbarProps { 5 | children: ReactNode; 6 | className?: string; 7 | } 8 | 9 | export function Toolbar({ children, className }: ToolbarProps) { 10 | return ( 11 |
17 | {children} 18 |
19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/hooks/outsidealert.ts: -------------------------------------------------------------------------------- 1 | import { RefObject, useEffect } from 'react'; 2 | 3 | export function useOutsideAlert(ref: RefObject, cb: VoidFunction) { 4 | useEffect(() => { 5 | const handleClickOutside = (event: Event) => { 6 | if (ref?.current && !ref?.current?.contains(event.target as Node)) { 7 | cb(); 8 | } 9 | }; 10 | document.addEventListener('mousedown', handleClickOutside); 11 | return () => { 12 | document.removeEventListener('mousedown', handleClickOutside); 13 | }; 14 | }, [cb, ref]); 15 | } 16 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | DEBUG=false 2 | 3 | NEXT_PUBLIC_BASE_URL= 4 | NEXT_PUBLIC_ANDROMEDA_ENDPOINT=https://api.nightmarket.io/api 5 | NEXT_PUBLIC_WEBSOCKET_ENDPOINT=ws://api.nightmarket.io 6 | NEXT_PUBLIC_SOLANA_RPC_URL=https://api.mainnet-beta.solana.com/ 7 | NEXT_PUBLIC_AUCTION_HOUSE_ADDRESS=FVmjRUm2ssXi5vZUhwzB2HfXzTVzvE73x3f5NmTtZ7C8 8 | NEXT_PUBLIC_AUCTION_HOUSE_PROGRAM_ADDRESS=rwdD3F6CgoCAoVaxcitXAeWRjQdiGc5AVABKCpQSMfd 9 | NEXT_PUBLIC_ADDRESS_LOOKUP_TABLE=HQma5N1kPpYiQBMUx4CqDSuUyjCzNHhvRtRz1qTBNtNp 10 | NEXT_PUBLIC_LEADERBOARD_ENDPOINT=https://pegasus.x.nightmarket.io 11 | -------------------------------------------------------------------------------- /src/hooks/sidebar.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from 'react'; 2 | 3 | interface SidebarContext { 4 | open: boolean; 5 | toggleSidebar: () => void; 6 | } 7 | 8 | export default function useSidebar(): SidebarContext { 9 | const [open, setSidebar] = useState(false); 10 | 11 | useEffect(() => { 12 | setSidebar(window.innerWidth >= 800); 13 | }, []); 14 | 15 | const toggleSidebar = useCallback(() => { 16 | setSidebar(!open); 17 | }, [open, setSidebar]); 18 | 19 | return { 20 | open, 21 | toggleSidebar, 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /public/images/card-grid-large-active.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/components/Typography.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | 3 | export enum FontWeight { 4 | Semibold = 'font-semibold', 5 | } 6 | 7 | export enum TextColor { 8 | Orange = 'text-orange-700', 9 | Gray = 'text-gray-300', 10 | } 11 | 12 | interface ParagraphProps { 13 | weight?: FontWeight; 14 | color?: TextColor; 15 | children: string | JSX.Element | JSX.Element[]; 16 | className?: string; 17 | } 18 | 19 | export function Paragraph({ weight, color, children, className }: ParagraphProps): JSX.Element { 20 | return

{children}

; 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | .env.local 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | 39 | .turbo 40 | 41 | .env.* -------------------------------------------------------------------------------- /src/hooks/collection/useSeries.ts: -------------------------------------------------------------------------------- 1 | import useSWR from 'swr'; 2 | 3 | import type { CollectionSeriesData } from '../../typings'; 4 | 5 | export const useSeries = ( 6 | slug: string, 7 | startTime: number, 8 | endTime: number, 9 | granularity: string, 10 | limit: number 11 | ) => 12 | useSWR( 13 | `/collections/series?address=${slug}&from_time=${startTime}&to_time=${endTime}&granularity=${granularity}&limit=${limit}&offset=0`, 14 | { 15 | revalidateOnFocus: false, 16 | revalidateIfStale: false, 17 | refreshInterval: 300000, 18 | } 19 | ); 20 | -------------------------------------------------------------------------------- /public/images/moonrank-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/utils/referral.ts: -------------------------------------------------------------------------------- 1 | import config from '../app.config'; 2 | 3 | export const getBuddyStats = async (address: string) => { 4 | // Disclaimer: Do not change the url structure. The query string structure is not standard but it works this way 5 | // The api provider is supposed to change it in the future 6 | const response = await fetch( 7 | `${config.referralUrl}referral/user&wallet=${address}&organisation=${config.referralOrg}`, 8 | { 9 | headers: { 10 | Authorization: config.referralKey, 11 | }, 12 | } 13 | ); 14 | 15 | const buddyStats = await response.json(); 16 | 17 | return buddyStats; 18 | }; 19 | -------------------------------------------------------------------------------- /public/images/card-grid-large.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "typeRoots": ["./@types", "./src/typings"] 18 | }, 19 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 20 | "exclude": ["node_modules", ".next", "out"] 21 | } 22 | -------------------------------------------------------------------------------- /src/hooks/mobilesearch.ts: -------------------------------------------------------------------------------- 1 | import { Dispatch, SetStateAction, useState } from 'react'; 2 | 3 | interface MobileSearchMethods { 4 | searchExpanded: boolean; 5 | setSearchExpanded: Dispatch>; 6 | showMobileSearch: boolean; 7 | setShowMobileSearch: Dispatch>; 8 | } 9 | 10 | export default function useMobileSearch(): MobileSearchMethods { 11 | const [searchExpanded, setSearchExpanded] = useState(false); 12 | const [showMobileSearch, setShowMobileSearch] = useState(false); 13 | 14 | return { 15 | searchExpanded, 16 | setSearchExpanded, 17 | showMobileSearch, 18 | setShowMobileSearch, 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /src/utils/cookies.ts: -------------------------------------------------------------------------------- 1 | export function setCookie(name: string, value: string, days: number) { 2 | let expires = ''; 3 | if (days) { 4 | const date = new Date(); 5 | date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000); 6 | expires = '; expires=' + date.toUTCString(); 7 | } 8 | 9 | document.cookie = name + '=' + (value || '') + expires + '; path=/; SameSite=Lax; Secure'; 10 | } 11 | 12 | export function getCookie(name: string) { 13 | var matches = document.cookie.match( 14 | new RegExp('(?:^|; )' + name.replace(/([\.$?*|{}\(\)\[\]\\\/\+^])/g, '\\$1') + '=([^;]*)') 15 | ); 16 | return matches ? decodeURIComponent(matches[1]) : undefined; 17 | } 18 | -------------------------------------------------------------------------------- /public/images/uturn.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/locales/en/launchpad.json: -------------------------------------------------------------------------------- 1 | { 2 | "phaseTitle": "Mint Phases", 3 | "supply": "Total Supply", 4 | "mintDate": "Mint Date", 5 | "preview": "Preview", 6 | "phases": { 7 | "mint": "Mint Now", 8 | "notAllowed": "You are not on the allowlist", 9 | "allowed": "You are on the allowlist", 10 | "soldout": "Sold out", 11 | "soldoutTime": "Sold out in", 12 | "finished": "Finished", 13 | "minted": "Minted", 14 | "minting": "Minting", 15 | "upcomingMint": "Mint in", 16 | "supply": "Supply", 17 | "price": "Price", 18 | "startingPrice": "Starting price", 19 | "finishedPrice": "Finished price", 20 | "dynamic": "Dynamic pricing" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/components/Toggle.tsx: -------------------------------------------------------------------------------- 1 | import type { ChangeEvent } from 'react'; 2 | 3 | export interface ToggleProps { 4 | value: boolean; 5 | onChange: (value: boolean) => void; 6 | classes?: string; 7 | } 8 | 9 | export const Toggle = ({ value, onChange, classes }: ToggleProps) => { 10 | const handleChange = (event: ChangeEvent) => { 11 | onChange(event.target.checked); 12 | }; 13 | 14 | return ( 15 |
16 | 20 |
21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /src/modules/time.ts: -------------------------------------------------------------------------------- 1 | import { addHours, formatDistanceToNow } from 'date-fns'; 2 | 3 | export function formatTimeAgo(date: Date) { 4 | const pastDate = new Date(date); 5 | 6 | const result = formatDistanceToNow(pastDate); 7 | return result; 8 | } 9 | 10 | export enum DateRangeOption { 11 | HOUR = '1', 12 | DAY = '24', 13 | WEEK = '168', 14 | MONTH = '720', 15 | } 16 | 17 | export function getDateTimeRange(dateRangeOption: DateRangeOption): { 18 | startTime: number; 19 | endTime: number; 20 | } { 21 | const now = new Date(); 22 | const endTime = addHours(now, 1).setMinutes(0, 0, 0) / 1000; 23 | const startTime = endTime - (parseInt(dateRangeOption, 10) + 1) * 3600; 24 | 25 | return { startTime, endTime }; 26 | } 27 | -------------------------------------------------------------------------------- /public/locales/en/analytics.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": { 3 | "floorPriceChartTitle": "Floor Price", 4 | "listedCountChartTitle": "Listed Count", 5 | "holderCountChartTitle": "Holder Count", 6 | "priceDistributionChartTitle": "Price Distribution", 7 | "holdersVsTokensHeldChartTitle": "Holders vs Tokens Held" 8 | }, 9 | "oneHour": "One Hour", 10 | "oneDay": "One Day", 11 | "oneWeek": "One Week", 12 | "oneMonth": "One Month", 13 | "profile": { 14 | "walletValueChartTitle": "Wallet Value", 15 | "totalAssetBreakdownChartTitle": "Total Asset Breakdown", 16 | "listedCountChartTitle": "Listed Count", 17 | "nftsBoughtVsNftsSoldChartTitle": "NFTs Bought vs NFTs Sold " 18 | }, 19 | "loading": "Loading...", 20 | "noData": "No Data" 21 | } 22 | -------------------------------------------------------------------------------- /src/components/SortingArrow.tsx: -------------------------------------------------------------------------------- 1 | import { ArrowSmallUpIcon, ArrowSmallDownIcon } from '@heroicons/react/24/outline'; 2 | 3 | interface SortingArrowProps { 4 | sortBy: string; 5 | orderBy: string; 6 | field: string; 7 | } 8 | 9 | export const SortingArrow = ({ orderBy, sortBy, field }: SortingArrowProps) => { 10 | return ( 11 |
12 | 16 | 20 |
21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /src/hooks/useAction.ts: -------------------------------------------------------------------------------- 1 | import type { ActionEvent, ActivityEvent, OfferEvent } from '../typings'; 2 | 3 | export const useAction = () => { 4 | const on = (eventType: string, listener: EventListenerOrEventListenerObject) => { 5 | document.addEventListener(eventType, listener); 6 | }; 7 | 8 | const off = (eventType: string, listener: EventListenerOrEventListenerObject) => { 9 | document.removeEventListener(eventType, listener); 10 | }; 11 | 12 | const trigger = (eventType: string, data?: ActionEvent | ActivityEvent | OfferEvent) => { 13 | document.dispatchEvent( 14 | new CustomEvent(eventType, { 15 | detail: data, 16 | }) 17 | ); 18 | }; 19 | 20 | return { 21 | on, 22 | off, 23 | trigger, 24 | }; 25 | }; 26 | -------------------------------------------------------------------------------- /src/hooks/login.ts: -------------------------------------------------------------------------------- 1 | import { useWallet } from '@solana/wallet-adapter-react'; 2 | import { useWalletModal } from '@solana/wallet-adapter-react-ui'; 3 | 4 | import { useMemo } from 'react'; 5 | 6 | type OnLoginFn = () => Promise; 7 | 8 | export default function useLogin(): OnLoginFn { 9 | const { wallet, connect } = useWallet(); 10 | const { setVisible } = useWalletModal(); 11 | 12 | const onOpenModal: OnLoginFn = useMemo(() => { 13 | return async function () { 14 | setVisible(true); 15 | 16 | return Promise.resolve(); 17 | }; 18 | }, [setVisible]); 19 | 20 | const onConnect: OnLoginFn = useMemo(() => { 21 | if (wallet === null) { 22 | return onOpenModal; 23 | } 24 | 25 | return connect; 26 | }, [wallet, connect, onOpenModal]); 27 | 28 | return onConnect; 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/build-vercel.yaml: -------------------------------------------------------------------------------- 1 | name: Validate Build - nextjs via vercel 2 | env: 3 | VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} 4 | VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} 5 | 6 | on: 7 | pull_request: 8 | types: [opened, reopened, synchronize] 9 | 10 | jobs: 11 | build: 12 | name: Validating nextjs build for Vercel 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v3 18 | 19 | - name: Install Vercel CLI 20 | run: npm install --global vercel@canary 21 | 22 | - name: Pull Vercel Environment Information 23 | run: vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }} 24 | 25 | - name: Build Project Image 26 | id: build-image 27 | run: vercel build --token=${{ secrets.VERCEL_TOKEN }} 28 | -------------------------------------------------------------------------------- /public/images/marketplaces/exchangeart.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /public/images/leaderboard/popup/arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const { i18n } = require('./next-i18next.config'); 2 | 3 | const env = require('./.config/next/env'); 4 | 5 | /** @type {import('next').NextConfig} */ 6 | const nextConfig = { 7 | output: 'standalone', 8 | reactStrictMode: true, 9 | swcMinify: true, 10 | i18n, 11 | env, 12 | eslint: { 13 | ignoreDuringBuilds: true, 14 | }, 15 | typescript: { 16 | ignoreBuildErrors: false, 17 | }, 18 | transpilePackages: ['react-hotjar'], 19 | webpack: (config) => { 20 | let modularizeImports = null; 21 | config.module.rules.some((rule) => 22 | rule.oneOf?.some((oneOf) => { 23 | modularizeImports = oneOf?.use?.options?.nextConfig?.modularizeImports; 24 | return modularizeImports; 25 | }) 26 | ); 27 | if (modularizeImports?.['ahooks']) delete modularizeImports['ahooks']; 28 | return config; 29 | }, 30 | }; 31 | 32 | module.exports = nextConfig; 33 | -------------------------------------------------------------------------------- /public/images/leaderboard/info.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /public/images/marketplaces/elixir.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.github/workflows/build-yarn.yaml: -------------------------------------------------------------------------------- 1 | name: Validate Build - nextjs via yarn 2 | 3 | on: 4 | pull_request: 5 | types: [opened, reopened, synchronize] 6 | push: 7 | branches: [dev, beta, main] 8 | 9 | jobs: 10 | build: 11 | name: Validating nextjs build via yarn 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v3 17 | 18 | - name: Build with yarn cache 19 | id: prepare-yarn 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: '16' 23 | cache: 'yarn' 24 | 25 | - name: Install dependencies 26 | id: install-deps 27 | run: yarn 28 | 29 | ## Disabled until lint rules are properly reviewed 30 | #- name: Lint the latest codebase 31 | # id: run-lint 32 | # run: yarn lint 33 | 34 | - name: build image 35 | id: build-image 36 | run: yarn build 37 | -------------------------------------------------------------------------------- /src/hooks/countdown.ts: -------------------------------------------------------------------------------- 1 | import { intervalToDuration } from 'date-fns'; 2 | import { useEffect, useState } from 'react'; 3 | 4 | interface TimeLeft { 5 | days?: number; 6 | hours?: number; 7 | minutes?: number; 8 | seconds?: number; 9 | } 10 | 11 | export default function useCountdown(date: Date): TimeLeft { 12 | const calculateTimeLeft = (): TimeLeft => { 13 | const currDate = new Date(); 14 | const diff = intervalToDuration({ 15 | start: currDate, 16 | end: date, 17 | }); 18 | const timeLeft: TimeLeft = { 19 | days: diff.days, 20 | hours: diff.hours, 21 | minutes: diff?.minutes, 22 | seconds: diff?.seconds, 23 | }; 24 | return timeLeft; 25 | }; 26 | const [timeLeft, setTimeLeft] = useState(calculateTimeLeft()); 27 | 28 | useEffect(() => { 29 | setTimeout(() => { 30 | setTimeLeft(calculateTimeLeft); 31 | }, 1000); 32 | }); 33 | 34 | return timeLeft; 35 | } 36 | -------------------------------------------------------------------------------- /src/providers/BulkListProvider.tsx: -------------------------------------------------------------------------------- 1 | import type { Dispatch, ReactNode, SetStateAction } from 'react'; 2 | import { createContext, useContext, useState } from 'react'; 3 | 4 | import type { Nft } from '../typings'; 5 | 6 | interface ContextProps { 7 | selected: Nft[]; 8 | setSelected: Dispatch>; 9 | } 10 | const initContext: ContextProps = { 11 | selected: [], 12 | setSelected: () => null, 13 | }; 14 | const BulkListContext = createContext(initContext); 15 | 16 | export const useBulkListContext = () => useContext(BulkListContext); 17 | 18 | interface ProviderProps { 19 | children: ReactNode; 20 | } 21 | const BulkListProvider = ({ children }: ProviderProps) => { 22 | const [selected, setSelected] = useState([]); 23 | return ( 24 | 25 | {children} 26 | 27 | ); 28 | }; 29 | export default BulkListProvider; 30 | -------------------------------------------------------------------------------- /src/pages/referrals/index.tsx: -------------------------------------------------------------------------------- 1 | import { GetServerSidePropsContext, NextPage } from 'next'; 2 | import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; 3 | import { useEffect, useState } from 'react'; 4 | 5 | import ReferralPage from '../../components/ReferralPage'; 6 | import { getCookie } from '../../utils/cookies'; 7 | import { COOKIE_REF } from '../_app'; 8 | 9 | export async function getServerSideProps({ locale }: GetServerSidePropsContext) { 10 | const i18n = await serverSideTranslations(locale as string, ['common', 'referrals']); 11 | 12 | return { 13 | props: { 14 | ...i18n, 15 | }, 16 | }; 17 | } 18 | 19 | const Referrals: NextPage = () => { 20 | const [refId, setRefId] = useState(''); 21 | 22 | useEffect(() => { 23 | const referrer = getCookie(COOKIE_REF); 24 | if (referrer) setRefId(referrer); 25 | }, []); 26 | 27 | return ; 28 | }; 29 | 30 | export default Referrals; 31 | -------------------------------------------------------------------------------- /src/utils/marketplaces.ts: -------------------------------------------------------------------------------- 1 | import marketplaces from '../marketplaces.json'; 2 | 3 | export interface Marketplace { 4 | marketplaceProgramAddress: string; 5 | auctionHouseAddress?: string | null; 6 | name: string; 7 | link: string; 8 | logo: string; 9 | buyNowEnabled?: boolean; 10 | } 11 | 12 | const multiTenantMarkets: Map = new Map([ 13 | ['hausS13jsjafwWwGqZTUQRmWyvyxn9EQpqMwV1PBBmk', true], 14 | ['RwDDvPp7ta9qqUwxbBfShsNreBaSsKvFcHzMxfBC3Ki', true], 15 | ['rwdD3F6CgoCAoVaxcitXAeWRjQdiGc5AVABKCpQSMfd', true], 16 | ]); 17 | 18 | export const getMarketplace = ( 19 | marketplaceAddress: string | undefined, 20 | auctionHouseAddress: string | undefined 21 | ): Marketplace | undefined => { 22 | return marketplaces.find( 23 | (m) => 24 | m.marketplaceProgramAddress === marketplaceAddress && 25 | (!multiTenantMarkets.get(m.marketplaceProgramAddress) || 26 | m.auctionHouseAddress === (auctionHouseAddress ?? undefined)) 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /src/components/CheckBox.tsx: -------------------------------------------------------------------------------- 1 | import { CheckIcon } from '@heroicons/react/24/outline'; 2 | 3 | import clsx from 'clsx'; 4 | 5 | interface CheckBoxProps { 6 | onClick: () => void; 7 | selected: boolean; 8 | label: string; 9 | containerClass?: string; 10 | } 11 | 12 | function CheckBox({ onClick, selected, label, containerClass }: CheckBoxProps): JSX.Element { 13 | return ( 14 |
18 |
19 | {selected ? ( 20 | 21 | ) : ( 22 |
23 | )} 24 |
25 | {label} 26 |
27 | ); 28 | } 29 | 30 | export default CheckBox; 31 | -------------------------------------------------------------------------------- /src/modules/address.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from '@solana/web3.js'; 2 | 3 | // shorten the checksummed version of the input address to have 4 characters at start and end 4 | export function shortenAddress(address?: string, chars = 4): string { 5 | if (!address) return ''; 6 | return `${address.slice(0, chars)}...${address.slice(-chars)}`; 7 | } 8 | 9 | export function isPublicKey(address: string | undefined): boolean { 10 | if (!address) { 11 | return false; 12 | } 13 | 14 | try { 15 | new PublicKey(address); 16 | return true; 17 | } catch { 18 | return false; 19 | } 20 | } 21 | 22 | export function addressAvatar(address: string | undefined): string { 23 | if (!address) { 24 | throw new Error('address string required'); 25 | } 26 | 27 | try { 28 | const publicKey = new PublicKey(address); 29 | 30 | const gradient = publicKey.toBytes().reduce((a, b) => a + b, 0) % 8; 31 | 32 | return `/images/gradients/gradient-${gradient + 1}.png`; 33 | } catch { 34 | return '/images/gradients/gradient-1.png'; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /public/images/marketplaces/tensor-swap.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /public/images/card-grid-small-active.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/utils/assets.ts: -------------------------------------------------------------------------------- 1 | export enum AssetSize { 2 | Original = 0, 3 | Tiny = 100, 4 | XSmall = 400, 5 | Small = 600, 6 | Medium = 800, 7 | Large = 1400, 8 | } 9 | 10 | const VALID_IMAGE_SIZES = [ 11 | AssetSize.Original, 12 | AssetSize.Tiny, 13 | AssetSize.XSmall, 14 | AssetSize.Small, 15 | AssetSize.Medium, 16 | AssetSize.Large, 17 | ]; 18 | 19 | const ASSET_BASE = 'https://cdn.helius.services/'; 20 | 21 | export function getImgOptAssetURL(url: string | undefined, size: AssetSize): string { 22 | if (!url) { 23 | return ''; 24 | } 25 | 26 | const encoded = encodeURIComponent(url); 27 | const validSize = VALID_IMAGE_SIZES.indexOf(size) > -1 ? size : 0; 28 | return `${ASSET_BASE}?url=${encoded}&width=${validSize}`; 29 | } 30 | 31 | const CF_ASSET_BASE = 'https://cdn.helius.services/'; 32 | 33 | export function getAssetURL(url: string | undefined, size: AssetSize): string { 34 | if (!url) { 35 | return ''; 36 | } 37 | 38 | const encoded = encodeURIComponent(url); 39 | const validSize = VALID_IMAGE_SIZES.indexOf(size) > -1 ? size : 0; 40 | return `${CF_ASSET_BASE}cdn-cgi/image/width=${validSize}/${encoded}`; 41 | } 42 | -------------------------------------------------------------------------------- /src/components/Flex.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import type { ReactNode } from 'react'; 3 | 4 | export enum FlexDirection { 5 | Col = 'flex-col', 6 | Row = 'flex-row', 7 | } 8 | 9 | export enum FlexJustify { 10 | Center = 'justify-center', 11 | Between = 'justify-between', 12 | Start = 'justify-start', 13 | } 14 | 15 | export enum FlexAlign { 16 | Center = 'items-center', 17 | Between = 'items-between', 18 | Start = 'items-start', 19 | } 20 | 21 | interface FlexProps { 22 | direction?: FlexDirection; 23 | children: ReactNode; 24 | className?: string; 25 | align?: FlexAlign; 26 | justify?: FlexJustify; 27 | block?: boolean; 28 | gap?: number; 29 | } 30 | 31 | export default function Flex({ 32 | children, 33 | direction = FlexDirection.Row, 34 | align, 35 | justify, 36 | className, 37 | block = false, 38 | gap = 0, 39 | }: FlexProps): JSX.Element { 40 | return ( 41 |
52 | {children} 53 |
54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /src/pages/og/collections/[slug].tsx: -------------------------------------------------------------------------------- 1 | import { GetServerSidePropsContext } from 'next'; 2 | 3 | import { api } from '../../../infrastructure/api'; 4 | import { Collection } from '../../../typings'; 5 | 6 | export async function getServerSideProps({ params, res }: GetServerSidePropsContext) { 7 | res.setHeader('Cache-Control', 'public, s-maxage=60, stale-while-revalidate'); 8 | 9 | try { 10 | const { data } = await api.get(`/collections/${params?.slug}`); 11 | 12 | if (data === null) { 13 | return { 14 | notFound: true, 15 | }; 16 | } 17 | 18 | return { 19 | redirect: { 20 | destination: 21 | `/api/og?name=${data.name}&image=${encodeURIComponent(data.image)}&owners=${ 22 | data.statistics.holders 23 | }` + 24 | `&volume=${data.statistics.volume1d}&floor=${data.statistics.floor1d}&listed=${data.statistics.listed1d}&verified=${data.isVerified}`, 25 | }, 26 | }; 27 | } catch (e) { 28 | return { 29 | redirect: { 30 | destination: '/', 31 | }, 32 | }; 33 | } 34 | } 35 | 36 | const OGCollection: React.FC = () =>
; 37 | 38 | export default OGCollection; 39 | -------------------------------------------------------------------------------- /public/images/marketplaces/hyperspace.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Chart/ChartPreview.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import type { ReactNode } from 'react'; 3 | 4 | export function ChartPreview({ 5 | title, 6 | dateRange, 7 | chart, 8 | className, 9 | min, 10 | max, 11 | }: { 12 | title: string; 13 | dateRange: string; 14 | chart: ReactNode; 15 | className?: string; 16 | min?: number; 17 | max?: number; 18 | }) { 19 | return ( 20 |
21 |
22 |

{title}

23 |

{dateRange}

24 |
25 |
26 | {!!max && ( 27 |
28 | {max} 29 |
30 | )} 31 |
{chart}
32 | {!!min && ( 33 |
34 | {min} 35 |
36 | )} 37 |
38 |
39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /src/hooks/metaplex.ts: -------------------------------------------------------------------------------- 1 | import { Metadata } from '@metaplex-foundation/mpl-token-metadata'; 2 | import { useConnection } from '@solana/wallet-adapter-react'; 3 | import { PublicKey } from '@solana/web3.js'; 4 | 5 | import { useCallback, useEffect, useState } from 'react'; 6 | 7 | import { getMetadataAccount } from '../utils/metaplex'; 8 | 9 | interface MetaplexProps { 10 | verifiedCollectionAddress: string | undefined; 11 | } 12 | 13 | export default function useMetaplex({ verifiedCollectionAddress }: MetaplexProps) { 14 | const [account, setAccount] = useState(null); 15 | const [loading, setLoading] = useState(true); 16 | const { connection } = useConnection(); 17 | 18 | const fetchAccountInfo = useCallback(async () => { 19 | if (verifiedCollectionAddress) { 20 | const accountPDA = getMetadataAccount(new PublicKey(verifiedCollectionAddress)); 21 | 22 | setAccount(await Metadata.fromAccountAddress(connection, accountPDA)); 23 | setLoading(false); 24 | } 25 | }, [connection, verifiedCollectionAddress]); 26 | 27 | useEffect(() => { 28 | if (verifiedCollectionAddress) fetchAccountInfo(); 29 | }, [verifiedCollectionAddress, connection, fetchAccountInfo]); 30 | 31 | return { loading, metadata: account }; 32 | } 33 | -------------------------------------------------------------------------------- /src/components/Tooltip.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import { ReactNode } from 'react'; 3 | import { usePopperTooltip, PopperOptions } from 'react-popper-tooltip'; 4 | 5 | //reference - https://www.npmjs.com/package/react-popper-tooltip 6 | 7 | interface TooltipProps { 8 | children: ReactNode; 9 | content: ReactNode; 10 | className?: string; 11 | placement?: PopperOptions['placement']; 12 | wrapperClass?: string; 13 | title?: string; 14 | } 15 | 16 | export default function Tooltip({ 17 | children, 18 | title, 19 | content, 20 | className, 21 | placement, 22 | wrapperClass, 23 | }: TooltipProps) { 24 | const { getTooltipProps, setTooltipRef, setTriggerRef, visible } = usePopperTooltip({ 25 | placement, 26 | }); 27 | 28 | return ( 29 | <> 30 |
31 | {children} 32 |
33 | {visible && ( 34 |
39 | {!!title &&
{title}
} 40 | {content} 41 |
42 | )} 43 | 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /src/utils/hyperspace.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { Base64 } from 'js-base64'; 3 | 4 | interface BuyNowResult { 5 | buffer?: Buffer; 6 | error?: string; 7 | } 8 | 9 | export const getBuyNowTransaction = async ( 10 | auctionHouseProgram: string, 11 | auctionHouseAddress: string, 12 | seller: string, 13 | buyer: string, 14 | price: string, 15 | tokenAddress: string 16 | ): Promise => { 17 | try { 18 | const { data } = await axios.post( 19 | `${process.env.NEXT_PUBLIC_ANDROMEDA_ENDPOINT}/nfts/buy`, 20 | { 21 | auctionHouseProgram: auctionHouseProgram, 22 | auctionHouseAddress: auctionHouseAddress, 23 | seller: seller, 24 | buyer, 25 | buyerBroker: buyer, 26 | price, 27 | mint: tokenAddress, 28 | }, 29 | { 30 | validateStatus: function (state) { 31 | return true; 32 | }, 33 | } 34 | ); 35 | 36 | if (data.error) { 37 | return { error: data.error }; 38 | } 39 | 40 | if (data.buffer.length > 0) { 41 | return { buffer: Buffer.from(Base64.toUint8Array(data.buffer)) }; 42 | } 43 | } catch (e: unknown) { 44 | // eslint-disable-next-line no-console 45 | console.error(e); 46 | return {}; 47 | } 48 | 49 | return {}; 50 | }; 51 | -------------------------------------------------------------------------------- /public/images/marketplaces/magiceden.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/components/Avatar.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import React from 'react'; 3 | 4 | import { getAssetURL, AssetSize } from '../utils/assets'; 5 | import Image from './Image'; 6 | 7 | export enum AvatarSize { 8 | Tiny, 9 | Small, 10 | Standard, 11 | Large, 12 | Jumbo, 13 | Gigantic, 14 | } 15 | 16 | interface AvatarProps { 17 | src: string; 18 | circle?: boolean; 19 | size: AvatarSize; 20 | } 21 | 22 | export function Avatar({ src, circle, size }: AvatarProps) { 23 | return ( 24 |
35 | avatar 43 |
44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /public/images/card-grid-small.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/pages/api/instructions/get-offers.tsx: -------------------------------------------------------------------------------- 1 | import { NightmarketClient, Offer } from '@motleylabs/mtly-nightmarket'; 2 | import { PublicKey } from '@solana/web3.js'; 3 | import { NextApiRequest, NextApiResponse } from 'next'; 4 | 5 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 6 | try { 7 | const { mint } = req.query; 8 | 9 | if (!mint) { 10 | return res.status(400).json({ error: 'Missing required parameters' }); 11 | } 12 | 13 | let mintPublicKey; 14 | 15 | try { 16 | mintPublicKey = new PublicKey(mint as string); 17 | } catch (error) { 18 | return res.status(400).json({ error: 'Invalid mint public key' }); 19 | } 20 | 21 | try { 22 | const nightmarketClient = new NightmarketClient(process.env.NEXT_PUBLIC_SOLANA_RPC_URL); 23 | 24 | const offers: Offer[] | null = await nightmarketClient.GetOffers(mintPublicKey); 25 | 26 | if (!offers) { 27 | return res.status(404).json({ error: 'There is no offer.' }); 28 | } 29 | 30 | res.status(200).json({ offers: offers }); 31 | } catch (error) { 32 | console.log(error); 33 | return res.status(400).json({ error: error }); 34 | } 35 | } catch (error) { 36 | console.log(error); 37 | return res.status(500).json({ error: 'Internal server error' }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/modules/number.ts: -------------------------------------------------------------------------------- 1 | const COMPACT_NUMBER_FORMATTER: Intl.NumberFormat = new Intl.NumberFormat('en-GB', { 2 | notation: 'compact', 3 | compactDisplay: 'short', 4 | }); 5 | 6 | /** 7 | * Converts the given number to "compact" format like 10K, 1.9M, etc. 8 | * 9 | * @param number number to make compact 10 | * @returns compacted string 11 | */ 12 | export function asCompactNumber(number: number): string { 13 | return COMPACT_NUMBER_FORMATTER.format(number); 14 | } 15 | 16 | const USD_FORMATTER: Intl.NumberFormat = new Intl.NumberFormat('en-US', { 17 | style: 'currency', 18 | currency: 'USD', 19 | }); 20 | 21 | const BASIC_NUMBER_FORMATTER = new Intl.NumberFormat('en-US', { 22 | maximumFractionDigits: 2, 23 | minimumFractionDigits: 0, 24 | }); 25 | 26 | export function asBasicNumber(number: number) { 27 | return !isNaN(number) ? BASIC_NUMBER_FORMATTER.format(number) : null; 28 | } 29 | 30 | /** 31 | * Formats the USD amount in US-currency format (e.g. $123,456.78) 32 | * 33 | * @param usd dollar amount to be formatted 34 | * @returns formatted dollar amount 35 | */ 36 | export function asUsdString(usd: number): string { 37 | return USD_FORMATTER.format(usd); 38 | } 39 | 40 | export function percentageDifference(startNum: number, endNum: number): number { 41 | const change = endNum - startNum; 42 | return (change / startNum) * 100; 43 | } 44 | -------------------------------------------------------------------------------- /src/pages/api/instructions/get-listing.tsx: -------------------------------------------------------------------------------- 1 | import { Listing, NightmarketClient } from '@motleylabs/mtly-nightmarket'; 2 | import { PublicKey } from '@solana/web3.js'; 3 | import { NextApiRequest, NextApiResponse } from 'next'; 4 | 5 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 6 | try { 7 | const { mint } = req.query; 8 | 9 | if (!mint) { 10 | return res.status(400).json({ error: 'Missing required parameters' }); 11 | } 12 | 13 | let mintPublicKey; 14 | 15 | try { 16 | mintPublicKey = new PublicKey(mint as string); 17 | } catch (error) { 18 | return res.status(400).json({ error: 'Invalid mint public key' }); 19 | } 20 | 21 | try { 22 | const nightmarketClient = new NightmarketClient(process.env.NEXT_PUBLIC_SOLANA_RPC_URL); 23 | 24 | const listing: Listing | null = await nightmarketClient.GetListing(mintPublicKey); 25 | 26 | if (!listing) { 27 | return res.status(404).json({ error: 'There is no listing.' }); 28 | } 29 | 30 | res.status(200).json({ listing: listing }); 31 | } catch (error) { 32 | console.log(error); 33 | return res.status(400).json({ error: error }); 34 | } 35 | } catch (error) { 36 | console.log(error); 37 | return res.status(500).json({ error: 'Internal server error' }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/components/Grid.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | 3 | import { FlexAlign, FlexDirection, FlexJustify } from './Flex'; 4 | 5 | interface RowProps { 6 | children: JSX.Element | undefined | null | (JSX.Element | null | undefined)[]; 7 | className?: string; 8 | align?: FlexAlign; 9 | justify?: FlexJustify; 10 | block?: boolean; 11 | } 12 | 13 | export function Row({ children, className, align, justify, block }: RowProps): JSX.Element { 14 | return ( 15 |
16 | {children} 17 |
18 | ); 19 | } 20 | 21 | interface ColProps { 22 | children: JSX.Element | undefined | boolean | null | (JSX.Element | boolean | null | undefined)[]; 23 | className?: string; 24 | align?: FlexAlign; 25 | direction?: FlexDirection; 26 | justify?: FlexJustify; 27 | span: number; 28 | gap?: number; 29 | } 30 | 31 | export function Col({ 32 | children, 33 | className, 34 | align, 35 | justify, 36 | span, 37 | direction = FlexDirection.Row, 38 | gap = 0, 39 | }: ColProps): JSX.Element { 40 | return ( 41 |
52 | {children} 53 |
54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /src/providers/WalletContextProvider.tsx: -------------------------------------------------------------------------------- 1 | import type { WalletContextState } from '@solana/wallet-adapter-react'; 2 | import { useConnection } from '@solana/wallet-adapter-react'; 3 | import { useWallet as useWalletAdapter } from '@solana/wallet-adapter-react'; 4 | 5 | import { createContext, useContext, useEffect, useState } from 'react'; 6 | 7 | import { getSolFromLamports } from '../utils/price'; 8 | 9 | type WalletContextValue = { 10 | balance: number | null; 11 | address?: string; 12 | } & WalletContextState; 13 | 14 | const WalletContext = createContext({} as WalletContextValue); 15 | 16 | export function WalletContextProvider({ children }: { children: React.ReactNode }) { 17 | const { connection } = useConnection(); 18 | const { publicKey, ...restParams } = useWalletAdapter(); 19 | const [balance, setBalance] = useState(null); 20 | 21 | useEffect(() => { 22 | if (!publicKey) return; 23 | connection.getBalance(publicKey).then((lamp) => setBalance(lamp)); 24 | }, [publicKey, balance, connection]); 25 | 26 | const returnValue = { 27 | ...restParams, 28 | publicKey, 29 | balance, 30 | address: publicKey?.toBase58(), 31 | }; 32 | 33 | return {children}; 34 | } 35 | 36 | export function useWalletContext() { 37 | return useContext(WalletContext); 38 | } 39 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "plugin:@typescript-eslint/recommended", 4 | "next", 5 | "next/core-web-vitals" 6 | ], 7 | "rules": { 8 | "no-console": "error", 9 | "no-debugger": "error", 10 | "react/button-has-type": "error", 11 | "react/jsx-no-useless-fragment": "error", 12 | "unused-imports/no-unused-imports": "error", 13 | "unused-imports/no-unused-vars": [ 14 | "error", 15 | { 16 | "vars": "all", 17 | "varsIgnorePattern": "^_", 18 | "args": "after-used", 19 | "argsIgnorePattern": "^_", 20 | "ignoreRestSiblings": true 21 | } 22 | ], 23 | "@typescript-eslint/explicit-module-boundary-types": "off", 24 | "@typescript-eslint/ban-ts-comment": [ 25 | "error", 26 | { 27 | "ts-expect-error": "allow-with-description", 28 | "ts-ignore": false, 29 | "ts-nocheck": false, 30 | "ts-check": false, 31 | "minimumDescriptionLength": 10 32 | } 33 | ], 34 | "@typescript-eslint/no-non-null-assertion": "error", 35 | "@typescript-eslint/no-var-requires": "off", 36 | "@typescript-eslint/no-explicit-any": [ 37 | "error", 38 | { 39 | "ignoreRestArgs": true 40 | } 41 | ], 42 | "@typescript-eslint/consistent-type-imports": "off", 43 | "@next/next/no-img-element": "off" 44 | }, 45 | "plugins": [ 46 | "@typescript-eslint", 47 | "unused-imports" 48 | ] 49 | } -------------------------------------------------------------------------------- /src/providers/PointsProvider.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext, useEffect, useState } from 'react'; 2 | import useSWRInfinite from 'swr/infinite'; 3 | 4 | import { LeaderBoardPointResponseType } from '../pages/leaderboard'; 5 | import { useWalletContext } from './WalletContextProvider'; 6 | 7 | type PointsContextValue = { 8 | points: number; 9 | }; 10 | 11 | const PointContext = createContext({} as PointsContextValue); 12 | 13 | export function PointsContextProvider({ children }: { children: React.ReactNode }) { 14 | const { address } = useWalletContext(); 15 | const [points, setPoints] = useState(0); 16 | const refreshIntervalTime = 5 * 1000; //5sec 17 | 18 | const getKey = (path: string) => { 19 | return `${process.env.NEXT_PUBLIC_LEADERBOARD_ENDPOINT}/${path}/${ 20 | address ?? '11111111111111111111111111111111' 21 | }`; 22 | }; 23 | 24 | const { data: userPointsData } = useSWRInfinite( 25 | () => getKey('points'), 26 | { refreshInterval: refreshIntervalTime } 27 | ); 28 | 29 | useEffect(() => { 30 | if (!address) return; 31 | 32 | if (userPointsData) { 33 | setPoints(userPointsData[0]?.points ?? 0); 34 | } 35 | }, [address, userPointsData]); 36 | 37 | const returnValue = { 38 | points, 39 | }; 40 | 41 | return {children}; 42 | } 43 | 44 | export function usePointContext() { 45 | return useContext(PointContext); 46 | } 47 | -------------------------------------------------------------------------------- /src/modules/candymachine/index.ts: -------------------------------------------------------------------------------- 1 | import { TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID } from '@solana/spl-token'; 2 | import { 3 | PublicKey, 4 | SystemProgram, 5 | SYSVAR_RENT_PUBKEY, 6 | TransactionInstruction, 7 | } from '@solana/web3.js'; 8 | 9 | export const createAssociatedTokenAccountInstruction = ( 10 | associatedTokenAddress: PublicKey, 11 | payer: PublicKey, 12 | walletAddress: PublicKey, 13 | splTokenMintAddress: PublicKey 14 | ) => { 15 | const keys = [ 16 | { pubkey: payer, isSigner: true, isWritable: true }, 17 | { pubkey: associatedTokenAddress, isSigner: false, isWritable: true }, 18 | { pubkey: walletAddress, isSigner: false, isWritable: false }, 19 | { pubkey: splTokenMintAddress, isSigner: false, isWritable: false }, 20 | { 21 | pubkey: SystemProgram.programId, 22 | isSigner: false, 23 | isWritable: false, 24 | }, 25 | { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, 26 | { 27 | pubkey: SYSVAR_RENT_PUBKEY, 28 | isSigner: false, 29 | isWritable: false, 30 | }, 31 | ]; 32 | return new TransactionInstruction({ 33 | keys, 34 | programId: ASSOCIATED_TOKEN_PROGRAM_ID, 35 | data: Buffer.from([]), 36 | }); 37 | }; 38 | 39 | export const getAtaForMint = async ( 40 | mint: PublicKey, 41 | buyer: PublicKey 42 | ): Promise<[PublicKey, number]> => { 43 | return await PublicKey.findProgramAddress( 44 | [buyer.toBuffer(), TOKEN_PROGRAM_ID.toBuffer(), mint.toBuffer()], 45 | ASSOCIATED_TOKEN_PROGRAM_ID 46 | ); 47 | }; 48 | -------------------------------------------------------------------------------- /public/images/leaderboard/bonuses/gradient-border.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 9 | 10 | 12 | 13 | 14 | 15 | 17 | 18 | 20 | 21 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/components/Chart/Chart.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic'; 2 | import type { TooltipProps } from 'recharts'; 3 | 4 | export const CustomLineChartTooltip = ({ active, payload }: TooltipProps) => { 5 | if (active && payload && payload.length) { 6 | return ( 7 |
8 |

{`${payload[0].value}`}

9 |
10 | ); 11 | } 12 | 13 | // eslint-disable-next-line react/jsx-no-useless-fragment 14 | return <>; 15 | }; 16 | 17 | export function Chart() { 18 | return
; 19 | } 20 | 21 | const DynamicStyledLineChart = dynamic( 22 | () => import('./StyledLineChart').then((mod) => mod.StyledLineChart), 23 | { 24 | ssr: false, 25 | } 26 | ); 27 | const DynamicTinyLineChart = dynamic( 28 | () => import('./TinyLineChart').then((mod) => mod.TinyLineChart), 29 | { 30 | ssr: false, 31 | } 32 | ); 33 | const DynamicStyledBarChart = dynamic( 34 | () => import('./StyledBarChart').then((mod) => mod.StyledBarChart), 35 | { 36 | ssr: false, 37 | } 38 | ); 39 | const DynamicChartTimeseries = dynamic( 40 | () => import('./ChartTimeseries').then((mod) => mod.ChartTimeseries), 41 | { 42 | ssr: false, 43 | } 44 | ); 45 | const DynamicChartPreview = dynamic( 46 | () => import('./ChartPreview').then((mod) => mod.ChartPreview), 47 | { 48 | ssr: false, 49 | } 50 | ); 51 | 52 | Chart.LineChart = DynamicStyledLineChart; 53 | Chart.TinyLineChart = DynamicTinyLineChart; 54 | Chart.BarChart = DynamicStyledBarChart; 55 | Chart.Timeseries = DynamicChartTimeseries; 56 | Chart.Preview = DynamicChartPreview; 57 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16-alpine AS deps 2 | RUN apk add --no-cache libc6-compat 3 | 4 | WORKDIR /app 5 | 6 | COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./ 7 | RUN \ 8 | if [ -f yarn.lock ]; then yarn --frozen-lockfile; \ 9 | elif [ -f package-lock.json ]; then npm ci; \ 10 | elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i; \ 11 | else echo "Lockfile not found." && exit 1; \ 12 | fi 13 | 14 | 15 | FROM node:16-alpine AS builder 16 | WORKDIR /app 17 | COPY --from=deps /app/node_modules ./node_modules 18 | COPY . . 19 | 20 | #Solana RPC 21 | ARG SOLANA_ENDPOINT 22 | ENV SOLANA_ENDPOINT $SOLANA_ENDPOINT 23 | ENV NEXT_PUBLIC_SOLANA_RPC_URL $SOLANA_ENDPOINT 24 | 25 | # Auction house 26 | ARG AUCTION_HOUSE_ADDRESS 27 | ENV NEXT_PUBLIC_AUCTION_HOUSE_ADDRESS $AUCTION_HOUSE_ADDRESS 28 | 29 | # Referral API 30 | ARG REFERRAL_KEY 31 | ENV NEXT_PUBLIC_REFERRAL_KEY $REFERRAL_KEY 32 | 33 | #bugsnag 34 | ARG BUGSNAG_API_KEY 35 | ENV NEXT_PUBLIC_BUGSNAG_API_KEY $BUGSNAG_API_KEY 36 | 37 | RUN yarn build 38 | 39 | FROM node:16-alpine AS runner 40 | WORKDIR /app 41 | 42 | ARG NODE_ENV 43 | ENV NEXT_PUBLIC_ENVIRONMENT $NODE_ENV 44 | ENV NODE_ENV $NODE_ENV 45 | 46 | RUN addgroup -g 1001 -S nodejs 47 | RUN adduser -S nextjs -u 1001 48 | 49 | COPY --from=builder /app/public ./public 50 | 51 | # https://nextjs.org/docs/advanced-features/output-file-tracing 52 | COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ 53 | COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static 54 | 55 | 56 | USER nextjs 57 | 58 | EXPOSE 3000 59 | 60 | ENV PORT 3000 61 | 62 | CMD ["node", "server.js"] 63 | -------------------------------------------------------------------------------- /public/locales/en/home.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": { 3 | "title": "Night Market", 4 | "description": "The new home of Solana NFTs." 5 | }, 6 | "hero": { 7 | "title": "The new home of Solana NFTs.", 8 | "subtitle": "You're early. Start listing and trading on Night Market to earn an airdrop of SAUCE!", 9 | "exploreNfts": "Explore NFTs", 10 | "sellNfts": "Sell NFTs" 11 | }, 12 | "trendingCollections": { 13 | "title": "Trending Collections" 14 | }, 15 | "topVolume": { 16 | "title": "Top Volume" 17 | }, 18 | "drops": { 19 | "title": "Upcoming Drops", 20 | "launchButton": "Launch with us", 21 | "moreLaunchesTitle": "More launches coming soon", 22 | "moreLaunchesDescription": "If you're an upcoming collection looking for white-glove service and community support, we'd love to learn more about you! Please apply below.", 23 | "drops": "Drops", 24 | "price": "Price", 25 | "supply": "Supply", 26 | "details": "Visit Mint", 27 | "view": "View", 28 | "motley": "Motley Friends is a global consortium of 10,000 rebels, builders, and creators. We are a community-powered experiment that redefines what is possible when blockchain meets culture. The official collection of Motley DAO & Night Market." 29 | }, 30 | "topMarketCap": { 31 | "title": "Top Marketcap" 32 | }, 33 | "followWallets": { 34 | "title": "Who to Follow" 35 | }, 36 | "report": { 37 | "sauceDistributedToUsers": "$SAUCE distributed to Night Market users.", 38 | "solDistributedToSauceHolders": "SOL distributed to $SAUCE holders. That's ${{sol}}!", 39 | "learnMore": "Learn More" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/components/Chart/StyledBarChart.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from 'react'; 2 | import { Bar, BarChart, CartesianGrid, Label, ResponsiveContainer, XAxis, YAxis } from 'recharts'; 3 | 4 | export function StyledBarChart(props: { 5 | height?: number; 6 | data: unknown[]; 7 | options?: { 8 | yDataKey?: string; 9 | }; 10 | children?: ReactNode; 11 | }) { 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 29 | 31 | 37 | 39 | 40 | 41 | {props.children} 42 | 43 | 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /src/app.config.ts: -------------------------------------------------------------------------------- 1 | export interface AppConfig { 2 | baseUrl: string; 3 | solanaRPCUrl: string; 4 | referralUrl: string; 5 | referralKey: string; 6 | referralOrg: string; 7 | socialMedia: { 8 | twitter?: string; 9 | discord?: string; 10 | medium?: string; 11 | }; 12 | offerMinimums: { 13 | percentageFloor: number; 14 | percentageListing: number; 15 | }; 16 | buddylink: { 17 | buddyBPS: number; 18 | }; 19 | website: string; 20 | status: string; 21 | tos: string; 22 | privacyPolicy: string; 23 | auctionHouse: string; 24 | auctionHouseProgram?: string; 25 | addressLookupTable: string; 26 | } 27 | 28 | const config: AppConfig = { 29 | baseUrl: 'https://nightmarket.io', // could also be an ENV variable 30 | solanaRPCUrl: process.env.NEXT_PUBLIC_SOLANA_RPC_URL as string, 31 | referralUrl: process.env.NEXT_PUBLIC_REFERRAL_URL as string, 32 | referralKey: process.env.NEXT_PUBLIC_REFERRAL_KEY as string, 33 | referralOrg: process.env.NEXT_PUBLIC_ORGANIZATION_NAME as string, 34 | socialMedia: { 35 | twitter: 'https://twitter.com/nightmarketio', 36 | discord: 'https://discord.gg/bn5z4A794E', 37 | medium: 'https://medium.com/@Motleydao', 38 | }, 39 | website: 'https://motleydao.com', 40 | status: 'https://motleylabs.cronitorstatus.com', 41 | tos: '', 42 | privacyPolicy: '', 43 | auctionHouse: process.env.NEXT_PUBLIC_AUCTION_HOUSE_ADDRESS as string, 44 | auctionHouseProgram: process.env.NEXT_PUBLIC_AUCTION_HOUSE_PROGRAM_ADDRESS, 45 | offerMinimums: { 46 | percentageFloor: 0.8, 47 | percentageListing: 0.8, 48 | }, 49 | buddylink: { 50 | buddyBPS: 0, 51 | }, 52 | addressLookupTable: process.env.NEXT_PUBLIC_ADDRESS_LOOKUP_TABLE as string, 53 | }; 54 | 55 | export default config; 56 | -------------------------------------------------------------------------------- /public/images/marketplaces/solanart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/components/ButtonGroup.tsx: -------------------------------------------------------------------------------- 1 | import { RadioGroup } from '@headlessui/react'; 2 | 3 | import clsx from 'clsx'; 4 | import type { ReactNode } from 'react'; 5 | 6 | interface ButtonGroupProps { 7 | style?: 'oval' | 'plain'; 8 | children: ReactNode; 9 | value: T; 10 | onChange: (value: T) => void; 11 | className?: string; 12 | } 13 | 14 | export function ButtonGroup({ 15 | style = 'oval', 16 | children, 17 | value, 18 | onChange, 19 | className, 20 | }: ButtonGroupProps): JSX.Element { 21 | return ( 22 | 30 | {children} 31 | 32 | ); 33 | } 34 | 35 | interface ButtonGroupButtonProps { 36 | children: ReactNode; 37 | active?: boolean; 38 | value: T; 39 | plain?: boolean; 40 | } 41 | 42 | function ButtonGroupOption({ children, value, plain }: ButtonGroupButtonProps): JSX.Element { 43 | return ( 44 | 47 | clsx( 48 | 'flex items-center justify-center text-base', 49 | !plain && 'h-12 w-28 rounded-full', 50 | checked 51 | ? plain 52 | ? 'border-b border-b-white text-white' 53 | : 'rounded-full bg-gray-800 text-white' 54 | : 'cursor-pointer bg-transparent text-gray-300 hover:bg-gray-800 hover:text-gray-200' 55 | ) 56 | } 57 | > 58 | {children} 59 | 60 | ); 61 | } 62 | 63 | ButtonGroup.Option = ButtonGroupOption; 64 | -------------------------------------------------------------------------------- /src/providers/AuctionHouseProvider.tsx: -------------------------------------------------------------------------------- 1 | import { useConnection, useWallet } from '@solana/wallet-adapter-react'; 2 | import { PublicKey } from '@solana/web3.js'; 3 | 4 | import { createContext, useContext, useEffect, useState } from 'react'; 5 | 6 | import config from '../app.config'; 7 | import type { AuctionHouse } from '../typings'; 8 | import { getAuctionHouseInfo } from '../utils/auction-house'; 9 | 10 | type AuctionHouseContextValue = { 11 | auctionHouse: AuctionHouse | null; 12 | isLoading: boolean; 13 | isFetched: boolean; 14 | }; 15 | const AuctionHouseContext = createContext({ 16 | auctionHouse: null, 17 | isLoading: false, 18 | isFetched: false, 19 | }); 20 | 21 | export function AuctionHouseContextProvider({ children }: { children: React.ReactNode }) { 22 | const wallet = useWallet(); 23 | const { connection } = useConnection(); 24 | const [auctionHouse, setAuctionHouse] = useState(null); 25 | const [isLoading, setIsLoading] = useState(false); 26 | const [isFetched, setIsFetched] = useState(false); 27 | 28 | useEffect(() => { 29 | if (wallet.connected && !auctionHouse && !isLoading && !isFetched) { 30 | setIsLoading(true); 31 | getAuctionHouseInfo(connection, new PublicKey(config.auctionHouse)).then((res) => { 32 | setAuctionHouse(res); 33 | setIsFetched(true); 34 | setIsLoading(false); 35 | }); 36 | } 37 | }, [auctionHouse, connection, isFetched, isLoading, wallet.connected]); 38 | 39 | return ( 40 | 41 | {children} 42 | 43 | ); 44 | } 45 | 46 | export function useAuctionHouseContext() { 47 | return useContext(AuctionHouseContext); 48 | } 49 | -------------------------------------------------------------------------------- /public/locales/en/leaderboard.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": { 3 | "title": "The Arcade", 4 | "description": "Join the Night Market arcade." 5 | }, 6 | "connectWallet": "Connect Wallet", 7 | "openNftMarketplace": "Open NFT Marketplace", 8 | "exploreNightMarket": "Explore Night Market", 9 | "tooltip": { 10 | "diamond": { 11 | "title": "We're keeping score", 12 | "content": "We are keeping track of your activity on the platform. All aboard!" 13 | }, 14 | "bonus": "Bonus points are earned when you interact with Bonus collections (they have a special icon) on the Marketplace" 15 | }, 16 | "banner": { 17 | "season": "Season", 18 | "leaderboard": "The Arcade", 19 | "description.1": "During Season 1, users who buy, sell, and list NFTs on Night Market will be rewarded with Tickets. Tickets will be redeemable for [REDACTED] at the end of the Season. 👀", 20 | "description.2": "The more you earn, the better the prizes will be. Happy Trading!", 21 | "howToEarn": "How to earn points?" 22 | }, 23 | "table": { 24 | "position": "Position", 25 | "name": "Name", 26 | "bonus": "Bonus", 27 | "tickerTickets": "24h Tickets", 28 | "totalTickets": "Total Tickets", 29 | "you": "You", 30 | "loadMore": "Load more" 31 | }, 32 | "info": { 33 | "totalEarnedPoints": "Total earned points", 34 | "earnedPointsIn24h": "Earned points in 24h", 35 | "totalParticipants": "Total participants", 36 | "participantsIn24h": "Participants in 24h" 37 | }, 38 | "popup": { 39 | "howToEarn": "How to Earn points?", 40 | "open": "Open", 41 | "nftMarketplace": "NFT Marketplace", 42 | "buySome": "Buy some NFT or Sell it. Participate in various activities such as auctions.", 43 | "youWill": "You will receive points for each such action." 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /public/locales/en/nft.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": { 3 | "title": "Nft {{ address }}" 4 | }, 5 | "details": "Details", 6 | "offers": "Offers", 7 | "activity": "Activity", 8 | "bid": "Make Offer", 9 | "neverSold": "Not Listed", 10 | "listed": "Current Price", 11 | "sellEarn": "Sell and earn", 12 | "buyEarn": "Buy and earn", 13 | "buy": "Buy Now", 14 | "view": "View on {{market}}", 15 | "logo": "{{market}} logo", 16 | "listedOn": "Listed on {{market}}.", 17 | "total": "Total", 18 | "cancel": "Cancel", 19 | "update": "Update", 20 | "updateListing": "Update Listing", 21 | "updateBid": "Update your Offer", 22 | "attributes": "Attributes", 23 | "viewerOffer": "Your Offer", 24 | "aboutCollection": "About the Collection", 25 | "mintAddress": "Mint Address", 26 | "tokenAddress": "Token Address", 27 | "collectionAddress": "On-chain Collection", 28 | "owner": "Current Owner", 29 | "cancelOffer": "Cancel Offer", 30 | "cancelListing": "Cancel Listing", 31 | "enforcement": "Royalty Enforcement", 32 | "royalties": "Creator Royalties", 33 | "acceptEarn": "Accept and Earn", 34 | "fee": "Marketplace Fee", 35 | "newPrice": "New Price", 36 | "collection": "Collection", 37 | "placeBid": "Make an Offer", 38 | "currentFloor": "Current Floor Price", 39 | "listingPrice": "Listing Price", 40 | "lastSold": "Last sold for", 41 | "price": "Price", 42 | "acceptOffer": "Accept Offer", 43 | "updateOffer": "Update Offer", 44 | "walletBalance": "Current Wallet Balance", 45 | "amount": "Amount", 46 | "submitOffer": "Submit Offer", 47 | "lastSale": "Last Sale Price", 48 | "listNft": "List for Sale", 49 | "highestOffer": "Highest Offer", 50 | "accept": "Accept", 51 | "twitterShareText": "Check out this NFT", 52 | "connectToBuy": "Connect a Wallet", 53 | "noOffers": "No offers yet", 54 | "noSales": "No sales history" 55 | } 56 | -------------------------------------------------------------------------------- /src/components/Carousel/Carousel.tsx: -------------------------------------------------------------------------------- 1 | import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/24/outline'; 2 | 3 | import { useRef, useState, useEffect } from 'react'; 4 | import type { SwiperProps } from 'swiper/react'; 5 | import { Swiper } from 'swiper/react'; 6 | import { Navigation, Grid } from 'swiper/modules'; 7 | 8 | const useSwiperRef = (): [T | null, React.Ref] => { 9 | const [wrapper, setWrapper] = useState(null); 10 | const ref = useRef(null); 11 | 12 | useEffect(() => { 13 | if (ref.current) { 14 | setWrapper(ref.current); 15 | } 16 | }, []); 17 | 18 | return [wrapper, ref]; 19 | }; 20 | 21 | export default function Carousel({ children, ...other }: SwiperProps): JSX.Element { 22 | const [nextEl, nextElRef] = useSwiperRef(); 23 | const [prevEl, prevElRef] = useSwiperRef(); 24 | 25 | return ( 26 |
27 | 35 | {children} 36 | 37 | 44 | 51 |
52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /src/components/Hero.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface HeroProps { 4 | children: React.ReactNode; 5 | } 6 | 7 | export default function Hero({ children }: HeroProps): JSX.Element { 8 | return ( 9 |
10 | {children} 11 |
12 | ); 13 | } 14 | 15 | interface HeroMainProps { 16 | children: React.ReactNode; 17 | } 18 | 19 | function HeroMain({ children }: HeroMainProps): JSX.Element { 20 | return ( 21 |
22 | {children} 23 |
24 | ); 25 | } 26 | 27 | Hero.Main = HeroMain; 28 | 29 | interface HeroTitleProps { 30 | children: React.ReactNode; 31 | } 32 | 33 | function HeroTitle({ children }: HeroTitleProps): JSX.Element { 34 | return

{children}

; 35 | } 36 | 37 | Hero.Title = HeroTitle; 38 | 39 | interface HeroSubTitleProps { 40 | children: React.ReactNode; 41 | } 42 | 43 | function HeroSubTitle({ children }: HeroSubTitleProps): JSX.Element { 44 | return

{children}

; 45 | } 46 | 47 | Hero.SubTitle = HeroSubTitle; 48 | 49 | interface HeroActionsProps { 50 | children: React.ReactNode; 51 | } 52 | 53 | function HeroActions({ children }: HeroActionsProps): JSX.Element { 54 | return ( 55 |
56 | {children} 57 |
58 | ); 59 | } 60 | 61 | Hero.Actions = HeroActions; 62 | 63 | interface HeroImageProps { 64 | children: React.ReactNode; 65 | } 66 | 67 | function HeroImage({ children }: HeroImageProps): JSX.Element { 68 | return ( 69 |
70 | {children} 71 |
72 | ); 73 | } 74 | 75 | Hero.Image = HeroImage; 76 | -------------------------------------------------------------------------------- /src/pages/api/instructions/is-local-listing.tsx: -------------------------------------------------------------------------------- 1 | import { Listing, NightmarketClient } from '@motleylabs/mtly-nightmarket'; 2 | import { PublicKey } from '@solana/web3.js'; 3 | import { NextApiRequest, NextApiResponse } from 'next'; 4 | 5 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 6 | try { 7 | const { seller, price, signature, blockTimestamp, auctionHouseProgram, auctionHouseAddress } = 8 | req.query; 9 | 10 | if ( 11 | !seller || 12 | !price || 13 | !signature || 14 | !blockTimestamp || 15 | !auctionHouseProgram || 16 | !auctionHouseAddress 17 | ) { 18 | return res.status(400).json({ error: 'Missing required parameters' }); 19 | } 20 | 21 | let sellerPublicKey; 22 | let priceNumber = Number(price); 23 | 24 | if (isNaN(priceNumber)) { 25 | return res.status(400).json({ error: 'Invalid price' }); 26 | } 27 | 28 | try { 29 | sellerPublicKey = new PublicKey(seller as string); 30 | } catch (error) { 31 | return res.status(400).json({ error: 'Invalid seller public key' }); 32 | } 33 | 34 | try { 35 | const nightmarketClient = new NightmarketClient(process.env.NEXT_PUBLIC_SOLANA_RPC_URL); 36 | 37 | const listing: Listing = { 38 | seller: seller as string, 39 | price: Number(price), 40 | signature: signature as string, 41 | blockTimestamp: Number(blockTimestamp), 42 | auctionHouseProgram: auctionHouseProgram as string, 43 | auctionHouseAddress: auctionHouseAddress as string, 44 | }; 45 | 46 | const val: boolean = nightmarketClient.IsLocalListing(listing); 47 | 48 | res.status(200).json({ isLocalListing: val }); 49 | } catch (error) { 50 | console.log(error); 51 | return res.status(400).json({ error: error }); 52 | } 53 | } catch (error) { 54 | console.log(error); 55 | return res.status(500).json({ error: 'Internal server error' }); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /public/locales/en/profile.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": { 3 | "title": "{{ name }} | Profile", 4 | "description": "Profile for {{ name }}" 5 | }, 6 | "collected": "Collected", 7 | "created": "Created", 8 | "activity": "Activity", 9 | "analytics": "Analytics", 10 | "followers": "Followers", 11 | "following": "Following", 12 | "portfolioValue": "Portfolio Value", 13 | "netWorth": "Net Worth", 14 | "totalNFTs": "Total NFTs", 15 | "listedNFTs": "Listed NFTs", 16 | "all": "All", 17 | "nfts": "NFTs", 18 | "noNFTs": "No NFTs", 19 | "listings": "Listings", 20 | "offers": "Offers", 21 | "affiliate": "Affiliate", 22 | "sales": "Sales", 23 | "twitterShareText": "Check out this Profile", 24 | "offersFilter": { 25 | "allOffers": "All Offers", 26 | "offersReceived": "Offers Received", 27 | "offersPlaced": "Offers Placed" 28 | }, 29 | "accept": "Accept", 30 | "update": "Update", 31 | "portfolioDisclaimer": "This value is the sum of the floor prices of the held collections, displayed in SOL.", 32 | "bulkListing": { 33 | "title": "Bulk Listing", 34 | "lastSalePrice": "Last Sale Price", 35 | "selectUnlisted": "Select all unlisted ({{ tokenCount }})", 36 | "listSelected": "List selected ({{ tokenCount }})", 37 | "inCart": "Listing Cart", 38 | "noTokensInCart": "No NFTs added to the cart yet", 39 | "globalPrice": "Global Price", 40 | "globalPriceTooltip": "Global Price allows you to set one price for each selected NFT for a Bulk listing", 41 | "pnl": "PnL", 42 | "pnlTooltip": "PnL stands for profit and loss, and it can be either realized or unrealized.", 43 | "pnlExplanation": "Note: if last sale price per NFT is not detected, the PnL value will not be accurate.", 44 | "estimatedNetworkFee": "Estimated Network Fee", 45 | "estimatedRent": "Estimated Rent", 46 | "estimatedRentDetail": "This will be refunded when sold or de-listed", 47 | "pleaseWait": "Please Wait", 48 | "listNow": "List now ({{ tokenCount }})" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /public/locales/en/collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": { 3 | "title": "{{name}}" 4 | }, 5 | "card": { 6 | "supply": "Supply", 7 | "floor": "Floor", 8 | "floorPrice": "{{price}}", 9 | "count": "{{amount}} NFTs" 10 | }, 11 | "sort": { 12 | "priceHighToLow": "Price: Descending", 13 | "priceLowToHigh": "Price: Ascending", 14 | "recentlyListed": "Recently Listed", 15 | "rarityLowToHigh": "Rarity: Ascending", 16 | "rarityHighToLow": "Rarity: Descending", 17 | "lastSaleLowToHigh": "Last Sale: Ascending", 18 | "lastSaleHighToLow": "Last Sale: Descending", 19 | "lastUpdateLowToHigh": "Updated: Ascending" 20 | }, 21 | "collection": "Collection", 22 | "globalFloor": "Global Floor", 23 | "1hVolume": "1 Hour Volume", 24 | "24hVolume": "24 Hour Volume", 25 | "7dVolume": "7 Day Volume", 26 | "30dVolume": "30 Day Volume", 27 | "timeInterval": { 28 | "hour": "1 Hour", 29 | "day": "1 Day", 30 | "week": "1 Week", 31 | "month": "1 Month" 32 | }, 33 | "trendingCollectionsSort": { 34 | "byVolumeTraded": "By Volume Traded", 35 | "byFloorPrice": "By Floor Price", 36 | "byListingsCount": "By Listings Count" 37 | }, 38 | "search": "Search...", 39 | "empty": "No matches found", 40 | "priceMinMaxCompare": "Price range max must be larger than price range min.", 41 | "showMoreCollections": "Show more collections", 42 | "floorPrice": "Floor Price", 43 | "estimatedValue": "Est. Value", 44 | "estimatedMarketcap": "Est. Marketcap", 45 | "supply": "Supply", 46 | "all": "All", 47 | "listings": "Listings", 48 | "offers": "Offers", 49 | "sales": "Sales", 50 | "holders": "Holders", 51 | "floor": "Floor Price", 52 | "volume": "Total Volume", 53 | "nfts": "NFTs", 54 | "apply": "Apply", 55 | "activity": "Activity", 56 | "analytics": "Analytics", 57 | "about": "About", 58 | "descriptionTitle": "About this collection", 59 | "creatorsTitle": "Creators", 60 | "twitterShareText": "Check out this collection", 61 | "filters": "Filters" 62 | } 63 | -------------------------------------------------------------------------------- /src/components/Attribute.tsx: -------------------------------------------------------------------------------- 1 | import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/24/outline'; 2 | 3 | import type { CollectionAttribute } from '../typings'; 4 | 5 | export function Attribute() { 6 | return
; 7 | } 8 | 9 | function AttributeOption({ 10 | variant, 11 | selected, 12 | count, 13 | percent, 14 | onClick, 15 | }: { 16 | variant: string; 17 | selected: boolean; 18 | count: number; 19 | percent: number; 20 | onClick: () => void; 21 | }): JSX.Element { 22 | return ( 23 |
24 | {variant} 25 |
26 | 27 | {count} ({percent}%) 28 | 29 | {selected ? ( 30 | 31 | ) : ( 32 |
33 | )} 34 |
35 |
36 | ); 37 | } 38 | 39 | Attribute.Option = AttributeOption; 40 | 41 | function AttributeOptionHeader({ 42 | group, 43 | isOpen, 44 | }: { 45 | group: CollectionAttribute; 46 | isOpen: boolean; 47 | }): JSX.Element { 48 | return ( 49 | <> 50 | {group.name} 51 |
52 | {isOpen ? ( 53 | 54 | ) : ( 55 | 56 | )} 57 |
58 | 59 | ); 60 | } 61 | 62 | Attribute.Header = AttributeOptionHeader; 63 | 64 | function AttributeOptionSkeleton(): JSX.Element { 65 | return ; 66 | } 67 | 68 | Attribute.Skeleton = AttributeOptionSkeleton; 69 | -------------------------------------------------------------------------------- /src/pages/api/instructions/close-listing.tsx: -------------------------------------------------------------------------------- 1 | import { Action, NightmarketClient } from '@motleylabs/mtly-nightmarket'; 2 | import { Connection, PublicKey, TransactionMessage, VersionedTransaction } from '@solana/web3.js'; 3 | import { NextApiRequest, NextApiResponse } from 'next'; 4 | 5 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 6 | try { 7 | const { mint, seller } = req.query; 8 | 9 | if (!mint || !seller) { 10 | return res.status(400).json({ error: 'Missing required parameters' }); 11 | } 12 | 13 | let mintPublicKey; 14 | let sellerPublicKey; 15 | 16 | try { 17 | mintPublicKey = new PublicKey(mint as string); 18 | sellerPublicKey = new PublicKey(seller as string); 19 | } catch (error) { 20 | return res.status(400).json({ error: 'Invalid mint or seller public key' }); 21 | } 22 | 23 | try { 24 | const nightmarketClient = new NightmarketClient(process.env.NEXT_PUBLIC_SOLANA_RPC_URL); 25 | 26 | const txRes: Action = await nightmarketClient.CloseListing(mintPublicKey, sellerPublicKey); 27 | 28 | if (!!txRes.err) { 29 | return res.status(400).json({ error: txRes.err }); 30 | } 31 | 32 | const connection = new Connection( 33 | process.env.NEXT_PUBLIC_SOLANA_RPC_URL ?? 'https://api.mainnet-beta.solana.com/' 34 | ); 35 | 36 | const { blockhash } = await connection.getLatestBlockhash(); 37 | 38 | const messageV0 = new TransactionMessage({ 39 | payerKey: sellerPublicKey, 40 | recentBlockhash: blockhash, 41 | instructions: txRes.instructions, 42 | }).compileToV0Message(txRes.altAccounts); 43 | 44 | const transactionV0 = new VersionedTransaction(messageV0); 45 | 46 | res.status(200).json({ tx: transactionV0.serialize() }); 47 | } catch (error) { 48 | console.log(error); 49 | return res.status(400).json({ error: error }); 50 | } 51 | } catch (error) { 52 | console.log(error); 53 | return res.status(500).json({ error: 'Internal server error' }); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/infrastructure/cookies/cookies.ts: -------------------------------------------------------------------------------- 1 | import type { IncomingMessage, ServerResponse } from 'http'; 2 | import type { GetServerSidePropsContext } from 'next'; 3 | import { setCookie as set, destroyCookie, parseCookies } from 'nookies'; 4 | 5 | import { Time } from '../../libs/consts'; 6 | 7 | const { hostname } = new URL(process.env.NEXT_PUBLIC_BASE_URL as string); 8 | 9 | const domain = process.env.NODE_ENV === 'production' ? `.${hostname}` : hostname; 10 | const path = '/'; 11 | 12 | export function getAllCookies(ctx: { req: IncomingMessage } | null = null) { 13 | return parseCookies(ctx); 14 | } 15 | 16 | export function getCookie(name: string, ctx: GetServerSidePropsContext | null = null) { 17 | const cookies = getAllCookies(ctx); 18 | 19 | return cookies[name]; 20 | } 21 | 22 | export function setCookie( 23 | key: string, 24 | value: T, 25 | params: { 26 | sameSite?: string; 27 | encode?: (value: T) => string; 28 | maxAge?: string; 29 | expires?: Date; 30 | ctx?: GetServerSidePropsContext | null; 31 | } = { 32 | ctx: null, 33 | } 34 | ): void { 35 | const { ctx, ...restParams } = params; 36 | 37 | set(ctx, key, value as unknown as string, { 38 | domain, 39 | path, 40 | encode: (value: string) => value, 41 | maxAge: Time.WEEK, 42 | sameSite: 'strict', 43 | ...restParams, 44 | }); 45 | } 46 | 47 | export function removeCookie(key: string, ctx: { res?: ServerResponse } | null = null) { 48 | destroyCookie(ctx, key, { domain, path }); 49 | } 50 | 51 | export function getSetCookiesList(cookies: string) { 52 | if (!cookies.length) { 53 | return []; 54 | } 55 | 56 | return cookies 57 | .split(' Secure, ') 58 | .map((s) => (s.includes('Secure') ? s : s.concat(' Secure'))) 59 | .filter((i) => !!i); 60 | } 61 | 62 | export function replaceSetCookieDomain(cookies: string) { 63 | if (!cookies.length) { 64 | return []; 65 | } 66 | 67 | const cookie = cookies 68 | .split('; ') 69 | .map((s) => (s.includes('Domain') ? 'Domain=localhost' : s)) 70 | .join('; '); 71 | 72 | return getSetCookiesList(cookie); 73 | } 74 | -------------------------------------------------------------------------------- /src/pages/api/instructions/is-local-offer.tsx: -------------------------------------------------------------------------------- 1 | import { NightmarketClient, Offer } from '@motleylabs/mtly-nightmarket'; 2 | import { PublicKey } from '@solana/web3.js'; 3 | import { NextApiRequest, NextApiResponse } from 'next'; 4 | 5 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 6 | try { 7 | const { 8 | buyer, 9 | seller, 10 | price, 11 | signature, 12 | blockTimestamp, 13 | auctionHouseProgram, 14 | auctionHouseAddress, 15 | } = req.query; 16 | 17 | if ( 18 | !buyer || 19 | !price || 20 | !signature || 21 | !blockTimestamp || 22 | !auctionHouseProgram || 23 | !auctionHouseAddress 24 | ) { 25 | return res.status(400).json({ error: 'Missing required parameters' }); 26 | } 27 | 28 | let buyerPublicKey; 29 | let sellerPublicKey; 30 | let priceNumber = Number(price); 31 | 32 | if (isNaN(priceNumber)) { 33 | return res.status(400).json({ error: 'Invalid price' }); 34 | } 35 | 36 | try { 37 | buyerPublicKey = new PublicKey(buyer as string); 38 | if (seller) sellerPublicKey = new PublicKey(seller as string); 39 | } catch (error) { 40 | return res.status(400).json({ error: 'Invalid buyer or seller public key' }); 41 | } 42 | 43 | try { 44 | const nightmarketClient = new NightmarketClient(process.env.NEXT_PUBLIC_SOLANA_RPC_URL); 45 | 46 | const offer: Offer = { 47 | seller: seller as string, 48 | buyer: buyer as string, 49 | price: Number(price), 50 | signature: signature as string, 51 | blockTimestamp: Number(blockTimestamp), 52 | auctionHouseProgram: auctionHouseProgram as string, 53 | auctionHouseAddress: auctionHouseAddress as string, 54 | }; 55 | 56 | const val: boolean = nightmarketClient.IsLocalOffer(offer); 57 | 58 | res.status(200).json({ isLocalOffer: val }); 59 | } catch (error) { 60 | console.log(error); 61 | return res.status(400).json({ error: error }); 62 | } 63 | } catch (error) { 64 | console.log(error); 65 | return res.status(500).json({ error: 'Internal server error' }); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /public/images/marketplaces/hadeswap.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Image.tsx: -------------------------------------------------------------------------------- 1 | import { Transition } from '@headlessui/react'; 2 | 3 | import clsx from 'clsx'; 4 | import type { DetailedHTMLProps, ImgHTMLAttributes } from 'react'; 5 | import { useEffect, useState, Fragment } from 'react'; 6 | 7 | export enum ImgBackdrop { 8 | Gray = 'bg-gray-700', 9 | Cell = 'bg-gray-800', 10 | } 11 | 12 | interface ImgProps 13 | extends DetailedHTMLProps, HTMLImageElement> { 14 | backdrop?: ImgBackdrop; 15 | fallbackSrc?: string; 16 | } 17 | 18 | function Img({ 19 | src, 20 | className, 21 | alt, 22 | backdrop = ImgBackdrop.Gray, 23 | fallbackSrc, 24 | ...props 25 | }: ImgProps): JSX.Element { 26 | const [hideImage, setHideImage] = useState(true); 27 | const [hasImageError, setImageError] = useState(false); 28 | const [currentSrc, setCurrentSrc] = useState(''); 29 | 30 | useEffect(() => { 31 | setHideImage(true); 32 | setCurrentSrc(src as string); 33 | 34 | const image = new Image(); 35 | image.decoding = 'async'; 36 | image.onload = () => { 37 | setHideImage(false); 38 | }; 39 | 40 | image.onerror = () => { 41 | if (fallbackSrc) { 42 | setCurrentSrc(fallbackSrc); 43 | setHideImage(false); 44 | setImageError(false); 45 | } else { 46 | setImageError(true); 47 | } 48 | }; 49 | 50 | image.src = src as string; 51 | if (image.complete && image.naturalWidth !== 0) { 52 | setHideImage(false); 53 | } 54 | }, [src, fallbackSrc]); 55 | 56 | if (hasImageError || hideImage) { 57 | return ( 58 |
63 | ); 64 | } 65 | 66 | return ( 67 | null} 76 | show={true} 77 | > 78 | {alt} 79 | 80 | ); 81 | } 82 | 83 | export default Img; 84 | -------------------------------------------------------------------------------- /src/components/Form.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import { 3 | DetailedHTMLProps, 4 | FormHTMLAttributes, 5 | LabelHTMLAttributes, 6 | forwardRef, 7 | LegacyRef, 8 | InputHTMLAttributes, 9 | } from 'react'; 10 | import { FieldError } from 'react-hook-form'; 11 | 12 | export function Form({ 13 | children, 14 | ...props 15 | }: DetailedHTMLProps, HTMLFormElement>): JSX.Element { 16 | return
{children}
; 17 | } 18 | 19 | interface FormLabelProps 20 | extends DetailedHTMLProps, HTMLLabelElement> { 21 | name: string; 22 | } 23 | 24 | function FormLabel({ name, className, children, ...props }: FormLabelProps): JSX.Element { 25 | return ( 26 | 30 | ); 31 | } 32 | 33 | interface FormErrorProps { 34 | message?: string; 35 | } 36 | 37 | function FormError({ message }: FormErrorProps): JSX.Element | null { 38 | if (message) { 39 | return

{message}

; 40 | } 41 | 42 | return null; 43 | } 44 | 45 | Form.Error = FormError; 46 | 47 | Form.Label = FormLabel; 48 | 49 | interface FormInputProps 50 | extends DetailedHTMLProps, HTMLInputElement> { 51 | className?: string; 52 | icon?: JSX.Element; 53 | error?: FieldError; 54 | font?: boolean; 55 | } 56 | 57 | const FormInput = forwardRef(function FormInput( 58 | { className, icon, error, font, ...props }: FormInputProps, 59 | ref 60 | ) { 61 | return ( 62 |
69 | {icon && icon} 70 | | undefined} 73 | className={clsx('w-full bg-transparent', { 'pl-2': icon, 'text-base': font })} 74 | /> 75 |
76 | ); 77 | }); 78 | 79 | Form.Input = FormInput; 80 | -------------------------------------------------------------------------------- /src/pages/api/instructions/create-listing.tsx: -------------------------------------------------------------------------------- 1 | import { Action, NightmarketClient } from '@motleylabs/mtly-nightmarket'; 2 | import { Connection, PublicKey, TransactionMessage, VersionedTransaction } from '@solana/web3.js'; 3 | import { NextApiRequest, NextApiResponse } from 'next'; 4 | 5 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 6 | try { 7 | const { price, mint, seller } = req.query; 8 | 9 | if (!price || !mint || !seller) { 10 | return res.status(400).json({ error: 'Missing required parameters' }); 11 | } 12 | 13 | let mintPublicKey; 14 | let sellerPublicKey; 15 | let priceNumber; 16 | 17 | priceNumber = Number(price); 18 | 19 | if (isNaN(priceNumber)) { 20 | return res.status(400).json({ error: 'Invalid price' }); 21 | } 22 | 23 | try { 24 | mintPublicKey = new PublicKey(mint as string); 25 | sellerPublicKey = new PublicKey(seller as string); 26 | } catch (error) { 27 | return res.status(400).json({ error: 'Invalid mint or seller public key' }); 28 | } 29 | 30 | try { 31 | const nightmarketClient = new NightmarketClient(process.env.NEXT_PUBLIC_SOLANA_RPC_URL); 32 | 33 | const txRes: Action = await nightmarketClient.CreateListing( 34 | mintPublicKey, 35 | priceNumber, 36 | sellerPublicKey 37 | ); 38 | 39 | if (!!txRes.err) { 40 | return res.status(400).json({ error: txRes.err }); 41 | } 42 | 43 | const connection = new Connection( 44 | process.env.NEXT_PUBLIC_SOLANA_RPC_URL ?? 'https://api.mainnet-beta.solana.com/' 45 | ); 46 | 47 | const { blockhash } = await connection.getLatestBlockhash(); 48 | 49 | const messageV0 = new TransactionMessage({ 50 | payerKey: sellerPublicKey, 51 | recentBlockhash: blockhash, 52 | instructions: txRes.instructions, 53 | }).compileToV0Message(txRes.altAccounts); 54 | 55 | const transactionV0 = new VersionedTransaction(messageV0); 56 | 57 | res.status(200).json({ tx: transactionV0.serialize() }); 58 | } catch (error) { 59 | console.log(error); 60 | return res.status(400).json({ error: error }); 61 | } 62 | } catch (error) { 63 | console.log(error); 64 | return res.status(500).json({ error: 'Internal server error' }); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/pages/api/instructions/update-listing.tsx: -------------------------------------------------------------------------------- 1 | import { Action, NightmarketClient } from '@motleylabs/mtly-nightmarket'; 2 | import { Connection, PublicKey, TransactionMessage, VersionedTransaction } from '@solana/web3.js'; 3 | import { NextApiRequest, NextApiResponse } from 'next'; 4 | 5 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 6 | try { 7 | const { price, mint, seller } = req.query; 8 | 9 | if (!price || !mint || !seller) { 10 | return res.status(400).json({ error: 'Missing required parameters' }); 11 | } 12 | 13 | let mintPublicKey; 14 | let sellerPublicKey; 15 | let priceNumber; 16 | 17 | priceNumber = Number(price); 18 | 19 | if (isNaN(priceNumber)) { 20 | return res.status(400).json({ error: 'Invalid price' }); 21 | } 22 | 23 | try { 24 | mintPublicKey = new PublicKey(mint as string); 25 | sellerPublicKey = new PublicKey(seller as string); 26 | } catch (error) { 27 | return res.status(400).json({ error: 'Invalid mint or seller public key' }); 28 | } 29 | 30 | try { 31 | const nightmarketClient = new NightmarketClient(process.env.NEXT_PUBLIC_SOLANA_RPC_URL); 32 | 33 | const txRes: Action = await nightmarketClient.UpdateListing( 34 | mintPublicKey, 35 | priceNumber, 36 | sellerPublicKey 37 | ); 38 | 39 | if (!!txRes.err) { 40 | return res.status(400).json({ error: txRes.err }); 41 | } 42 | 43 | const connection = new Connection( 44 | process.env.NEXT_PUBLIC_SOLANA_RPC_URL ?? 'https://api.mainnet-beta.solana.com/' 45 | ); 46 | 47 | const { blockhash } = await connection.getLatestBlockhash(); 48 | 49 | const messageV0 = new TransactionMessage({ 50 | payerKey: sellerPublicKey, 51 | recentBlockhash: blockhash, 52 | instructions: txRes.instructions, 53 | }).compileToV0Message(txRes.altAccounts); 54 | 55 | const transactionV0 = new VersionedTransaction(messageV0); 56 | 57 | res.status(200).json({ tx: transactionV0.serialize() }); 58 | } catch (error) { 59 | console.log(error); 60 | return res.status(400).json({ error: error }); 61 | } 62 | } catch (error) { 63 | console.log(error); 64 | return res.status(500).json({ error: 'Internal server error' }); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/components/Lightbox.tsx: -------------------------------------------------------------------------------- 1 | import { ArrowsPointingOutIcon } from '@heroicons/react/24/outline'; 2 | 3 | import clsx from 'clsx'; 4 | import React, { useRef } from 'react'; 5 | 6 | interface LightboxProps { 7 | children: JSX.Element | JSX.Element[]; 8 | show: boolean; 9 | onDismiss: () => void; 10 | } 11 | 12 | function Lightbox({ children, show, onDismiss }: LightboxProps): JSX.Element { 13 | const expandedRef = useRef(null!); 14 | 15 | return ( 16 |
33 |
40 |
{children}
41 |
42 |
43 | ); 44 | } 45 | 46 | interface LightboxExpandProps { 47 | onClick: () => void; 48 | } 49 | 50 | function LightboxShow({ onClick }: LightboxExpandProps): JSX.Element { 51 | return ( 52 | 58 | ); 59 | } 60 | 61 | Lightbox.Show = LightboxShow; 62 | 63 | interface LightboxContainerProps { 64 | children: JSX.Element[]; 65 | } 66 | function LightboxContainer({ children }: LightboxContainerProps): JSX.Element { 67 | return
{children}
; 68 | } 69 | 70 | Lightbox.Container = LightboxContainer; 71 | 72 | export default Lightbox; 73 | -------------------------------------------------------------------------------- /public/images/moon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /public/locales/en/referrals.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": { 3 | "title": "Referrals", 4 | "description": "Join the Night Market referral program." 5 | }, 6 | "generateLink": "To generate a referral link, connect your wallet", 7 | "connectWallet": "Connect Wallet", 8 | "welcomeMarket": "Welcome to the Night Market!", 9 | "createUnique": "Now create a unique name for your referral link", 10 | "placeholder": "username", 11 | "createLink": "Create link", 12 | "creating": "Creating", 13 | "fee": "Fee for the link generation", 14 | "successCreate": "You successfully created your referral link", 15 | "manage": "You can manage your benefits in Profile", 16 | "goTo": "Go to Profile", 17 | "share": "Share", 18 | "and": "&", 19 | "earn": "Earn", 20 | "nameLengthError": "Your name must be at least 3 characters", 21 | "usernameTaken": "This username is in use, please try another one.", 22 | "noBuddy": "Create an affiliate account to join our referral program. Activity from your custom links will be displayed here.", 23 | "comingSoon": "Coming Soon.", 24 | "profile": { 25 | "available": "Available to Claim", 26 | "claimRewards": "Claim Referral Fees", 27 | "feesByReferral": "Fees Generated by Referrals", 28 | "allTimeClaim": "Claimed (All Time)", 29 | "totalGeneratedRevenue": "Total Generated Volume", 30 | "affiliateLink": "Your Affiliate Link", 31 | "claimHistory": "Claim History", 32 | "referred": "Referred", 33 | "inactive": "Inactive", 34 | "generating": "Generating...", 35 | "noClaimHistory": "You do not have claim history.", 36 | "noReferred": "You do not have referred accounts yet.", 37 | "users": "users", 38 | "lastWeek": "last week", 39 | "solClaimed": "SOL Claimed" 40 | }, 41 | "banner": { 42 | "upTo": "EARN UP TO", 43 | "joinAffiliate": "Join our Affiliate Program and start earning now!", 44 | "instruction": "Earn up to 100% of Night Market transaction fees by sharing an affiliate link with your network!", 45 | "createReferral": "Join Now", 46 | "seeDashboard": "See my affiliate dashboard", 47 | "comingSoon": "COMING SOON" 48 | }, 49 | "table": { 50 | "address": "Address", 51 | "date": "Date", 52 | "feeGenerated": "Fees Generated", 53 | "volume": "Volume" 54 | }, 55 | "social": { 56 | "main": "You're early. Start listing and trading on Night Market to earn an airdrop of $SAUCE!" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/utils/auction-house.ts: -------------------------------------------------------------------------------- 1 | import { AuctionHouse as MtlyAuctionHouse } from '@motleylabs/mtly-auction-house'; 2 | import { RewardCenter } from '@motleylabs/mtly-reward-center'; 3 | import type { Connection } from '@solana/web3.js'; 4 | import { PublicKey } from '@solana/web3.js'; 5 | 6 | import { RewardCenterProgram } from '../modules/reward-center'; 7 | import type { AuctionHouse } from '../typings'; 8 | 9 | export const getAuctionHouseByAddress = async ( 10 | connection: Connection, 11 | auctionHouse: PublicKey 12 | ): Promise => { 13 | const mtlyHouse = await MtlyAuctionHouse.fromAccountAddress(connection, auctionHouse); 14 | return mtlyHouse; 15 | }; 16 | 17 | export const getAuctionHouseInfo = async ( 18 | connection: Connection, 19 | address: PublicKey 20 | ): Promise => { 21 | let auctionHouse: AuctionHouse | null = null; 22 | 23 | try { 24 | const mpAuctionHouse = await getAuctionHouseByAddress(connection, address); 25 | 26 | auctionHouse = { 27 | address: address.toBase58(), 28 | authority: mpAuctionHouse.authority.toBase58(), 29 | auctionHouseFeeAccount: mpAuctionHouse.auctionHouseFeeAccount.toBase58(), 30 | auctionHouseTreasury: mpAuctionHouse.auctionHouseTreasury.toBase58(), 31 | sellerFeeBasisPoints: mpAuctionHouse.sellerFeeBasisPoints, 32 | treasuryMint: mpAuctionHouse.treasuryMint.toBase58(), 33 | rewardCenter: null, 34 | }; 35 | 36 | const [rewardCenterAddress] = await RewardCenterProgram.findRewardCenterAddress(address); 37 | const rewardCenterAccount = await connection.getAccountInfo(rewardCenterAddress); 38 | 39 | let rewardCenter: RewardCenter | null = null; 40 | 41 | if (!!rewardCenterAccount) { 42 | rewardCenter = RewardCenter.deserialize(rewardCenterAccount.data, 0)[0]; 43 | auctionHouse.rewardCenter = { 44 | address: rewardCenterAddress, 45 | tokenMint: rewardCenter.tokenMint, 46 | sellerRewardPayoutBasisPoints: rewardCenter.rewardRules.sellerRewardPayoutBasisPoints, 47 | payoutNumeral: rewardCenter.rewardRules.payoutNumeral, 48 | mathematicalOperand: rewardCenter.rewardRules.mathematicalOperand, 49 | }; 50 | } else { 51 | auctionHouse.rewardCenter = null; 52 | } 53 | 54 | return auctionHouse; 55 | } catch (e) { 56 | // eslint-disable-next-line no-console 57 | console.error(e); 58 | return null; 59 | } 60 | }; 61 | -------------------------------------------------------------------------------- /src/pages/api/instructions/buy-listing.tsx: -------------------------------------------------------------------------------- 1 | import { Action, NightmarketClient } from '@motleylabs/mtly-nightmarket'; 2 | import { Connection, PublicKey, TransactionMessage, VersionedTransaction } from '@solana/web3.js'; 3 | import { NextApiRequest, NextApiResponse } from 'next'; 4 | 5 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 6 | try { 7 | const { price, mint, seller, buyer } = req.query; 8 | 9 | if (!price || !mint || !seller || !buyer) { 10 | return res.status(400).json({ error: 'Missing required parameters' }); 11 | } 12 | 13 | let mintPublicKey; 14 | let sellerPublicKey; 15 | let buyerPublicKey; 16 | let priceNumber; 17 | 18 | priceNumber = Number(price); 19 | 20 | if (isNaN(priceNumber)) { 21 | return res.status(400).json({ error: 'Invalid price' }); 22 | } 23 | 24 | try { 25 | mintPublicKey = new PublicKey(mint as string); 26 | sellerPublicKey = new PublicKey(seller as string); 27 | buyerPublicKey = new PublicKey(buyer as string); 28 | } catch (error) { 29 | return res.status(400).json({ error: 'Invalid mint, seller or buyer public key' }); 30 | } 31 | 32 | try { 33 | const nightmarketClient = new NightmarketClient(process.env.NEXT_PUBLIC_SOLANA_RPC_URL); 34 | 35 | const txRes: Action = await nightmarketClient.BuyListing( 36 | mintPublicKey, 37 | priceNumber, 38 | sellerPublicKey, 39 | buyerPublicKey 40 | ); 41 | 42 | if (!!txRes.err) { 43 | return res.status(400).json({ error: txRes.err }); 44 | } 45 | 46 | const connection = new Connection( 47 | process.env.NEXT_PUBLIC_SOLANA_RPC_URL ?? 'https://api.mainnet-beta.solana.com/' 48 | ); 49 | 50 | const { blockhash } = await connection.getLatestBlockhash(); 51 | 52 | const messageV0 = new TransactionMessage({ 53 | payerKey: buyerPublicKey, 54 | recentBlockhash: blockhash, 55 | instructions: txRes.instructions, 56 | }).compileToV0Message(txRes.altAccounts); 57 | 58 | const transactionV0 = new VersionedTransaction(messageV0); 59 | 60 | res.status(200).json({ tx: transactionV0.serialize() }); 61 | } catch (error) { 62 | console.log(error); 63 | return res.status(400).json({ error: error }); 64 | } 65 | } catch (error) { 66 | console.log(error); 67 | return res.status(500).json({ error: 'Internal server error' }); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/pages/api/instructions/close-offer.tsx: -------------------------------------------------------------------------------- 1 | import { Action, NightmarketClient } from '@motleylabs/mtly-nightmarket'; 2 | import { Connection, PublicKey, TransactionMessage, VersionedTransaction } from '@solana/web3.js'; 3 | import { NextApiRequest, NextApiResponse } from 'next'; 4 | 5 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 6 | try { 7 | const { price, mint, seller, buyer } = req.query; 8 | 9 | if (!price || !mint || !seller || !buyer) { 10 | return res.status(400).json({ error: 'Missing required parameters' }); 11 | } 12 | 13 | let mintPublicKey; 14 | let sellerPublicKey; 15 | let buyerPublicKey; 16 | let priceNumber; 17 | 18 | priceNumber = Number(price); 19 | 20 | if (isNaN(priceNumber)) { 21 | return res.status(400).json({ error: 'Invalid price' }); 22 | } 23 | 24 | try { 25 | mintPublicKey = new PublicKey(mint as string); 26 | sellerPublicKey = new PublicKey(seller as string); 27 | buyerPublicKey = new PublicKey(buyer as string); 28 | } catch (error) { 29 | return res.status(400).json({ error: 'Invalid mint, seller or buyer public key' }); 30 | } 31 | 32 | try { 33 | const nightmarketClient = new NightmarketClient(process.env.NEXT_PUBLIC_SOLANA_RPC_URL); 34 | 35 | const txRes: Action = await nightmarketClient.CloseOffer( 36 | mintPublicKey, 37 | priceNumber, 38 | sellerPublicKey, 39 | buyerPublicKey 40 | ); 41 | 42 | if (!!txRes.err) { 43 | return res.status(400).json({ error: txRes.err }); 44 | } 45 | 46 | const connection = new Connection( 47 | process.env.NEXT_PUBLIC_SOLANA_RPC_URL ?? 'https://api.mainnet-beta.solana.com/' 48 | ); 49 | 50 | const { blockhash } = await connection.getLatestBlockhash(); 51 | 52 | const messageV0 = new TransactionMessage({ 53 | payerKey: buyerPublicKey, 54 | recentBlockhash: blockhash, 55 | instructions: txRes.instructions, 56 | }).compileToV0Message(txRes.altAccounts); 57 | 58 | const transactionV0 = new VersionedTransaction(messageV0); 59 | 60 | res.status(200).json({ tx: transactionV0.serialize() }); 61 | } catch (error) { 62 | console.log(error); 63 | return res.status(400).json({ error: error }); 64 | } 65 | } catch (error) { 66 | console.log(error); 67 | return res.status(500).json({ error: 'Internal server error' }); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/pages/api/instructions/create-offer.tsx: -------------------------------------------------------------------------------- 1 | import { Action, NightmarketClient } from '@motleylabs/mtly-nightmarket'; 2 | import { Connection, PublicKey, TransactionMessage, VersionedTransaction } from '@solana/web3.js'; 3 | import { NextApiRequest, NextApiResponse } from 'next'; 4 | 5 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 6 | try { 7 | const { price, mint, seller, buyer } = req.query; 8 | 9 | if (!price || !mint || !seller || !buyer) { 10 | return res.status(400).json({ error: 'Missing required parameters' }); 11 | } 12 | 13 | let mintPublicKey; 14 | let sellerPublicKey; 15 | let buyerPublicKey; 16 | let priceNumber; 17 | 18 | priceNumber = Number(price); 19 | 20 | if (isNaN(priceNumber)) { 21 | return res.status(400).json({ error: 'Invalid price' }); 22 | } 23 | 24 | try { 25 | mintPublicKey = new PublicKey(mint as string); 26 | sellerPublicKey = new PublicKey(seller as string); 27 | buyerPublicKey = new PublicKey(buyer as string); 28 | } catch (error) { 29 | return res.status(400).json({ error: 'Invalid mint, seller or buyer public key' }); 30 | } 31 | 32 | try { 33 | const nightmarketClient = new NightmarketClient(process.env.NEXT_PUBLIC_SOLANA_RPC_URL); 34 | 35 | const txRes: Action = await nightmarketClient.CreateOffer( 36 | mintPublicKey, 37 | priceNumber, 38 | sellerPublicKey, 39 | buyerPublicKey 40 | ); 41 | 42 | if (!!txRes.err) { 43 | return res.status(400).json({ error: txRes.err }); 44 | } 45 | 46 | const connection = new Connection( 47 | process.env.NEXT_PUBLIC_SOLANA_RPC_URL ?? 'https://api.mainnet-beta.solana.com/' 48 | ); 49 | 50 | const { blockhash } = await connection.getLatestBlockhash(); 51 | 52 | const messageV0 = new TransactionMessage({ 53 | payerKey: buyerPublicKey, 54 | recentBlockhash: blockhash, 55 | instructions: txRes.instructions, 56 | }).compileToV0Message(txRes.altAccounts); 57 | 58 | const transactionV0 = new VersionedTransaction(messageV0); 59 | 60 | res.status(200).json({ tx: transactionV0.serialize() }); 61 | } catch (error) { 62 | console.log(error); 63 | return res.status(400).json({ error: error }); 64 | } 65 | } catch (error) { 66 | console.log(error); 67 | return res.status(500).json({ error: 'Internal server error' }); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/modules/bugsnag.ts: -------------------------------------------------------------------------------- 1 | import Bugsnag from '@bugsnag/js'; 2 | 3 | const NEXT_PUBLIC_BUGSNAG_API_KEY = process.env.NEXT_PUBLIC_BUGSNAG_API_KEY as string; 4 | 5 | export function start() { 6 | // next.js executes top-level code at build time. See https://github.com/vercel/next.js/discussions/16840 for further example 7 | // So use NEXT_PHASE to avoid Bugsnag.start being executed during the build phase 8 | // See https://nextjs.org/docs/api-reference/next.config.js/introduction and https://github.com/vercel/next.js/blob/canary/packages/next/shared/lib/constants.ts#L1-L5 for 9 | // more details on NEXT_PHASE 10 | if (process.env.NEXT_PHASE !== 'phase-production-build' && NEXT_PUBLIC_BUGSNAG_API_KEY) { 11 | if (process.env.NEXT_IS_SERVER) { 12 | // Bugsnag.start({ 13 | // apiKey: NEXT_PUBLIC_BUGSNAG_API_KEY, 14 | // appVersion: process.env.NEXT_BUILD_ID, 15 | // // @bugsnag/plugin-aws-lambda must only be imported on the server 16 | // plugins: [require('@bugsnag/plugin-aws-lambda')], 17 | // }); 18 | } else { 19 | // If preferred two separate Bugsnag projects e.g. a javascript and a node project could be used rather than a single one 20 | Bugsnag.start({ 21 | apiKey: NEXT_PUBLIC_BUGSNAG_API_KEY, 22 | appVersion: process.env.NEXT_BUILD_ID, 23 | plugins: [], 24 | }); 25 | } 26 | } 27 | } 28 | 29 | // export function getServerlessHandler() { 30 | // return Bugsnag.getPlugin('awsLambda').createHandler(); 31 | // } 32 | 33 | // Could potentially export this function to standardise bugsnag notifications and explain different fields 34 | export function notifyInstructionError( 35 | err: Error | { name: string; message: string }, // probably an Error 36 | context: { 37 | operation: string; 38 | metadata: { 39 | userPubkey: string; 40 | programLogs: string; 41 | nft: any; 42 | [key: string]: any; 43 | }; 44 | } 45 | ) { 46 | return ( 47 | NEXT_PUBLIC_BUGSNAG_API_KEY && 48 | Bugsnag.notify(err, function (event) { 49 | event.context = context.operation; // Sets the context of the error 50 | // Bugsnag already tracks userIp, so it's better to include the userPubkey with metadata 51 | // event.setUser(context.userPubkey); // this could be set and removed globally, but easier to attatch here for now 52 | event.addMetadata( 53 | 'INSTRUCTION METADATA', // creates a tab in the bugsnag error with this title in all caps 54 | context.metadata 55 | ); 56 | }) 57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /styles/collection.css: -------------------------------------------------------------------------------- 1 | .toggle-input .switch { 2 | position: relative; 3 | display: inline-block; 4 | width: 40px; 5 | height: 24px; 6 | } 7 | 8 | .toggle-input .switch input { 9 | opacity: 0; 10 | width: 0; 11 | height: 0; 12 | } 13 | 14 | .toggle-input .slider { 15 | position: absolute; 16 | cursor: pointer; 17 | top: 0; 18 | left: 0; 19 | right: 0; 20 | bottom: 0; 21 | background-color: #696969; 22 | -webkit-transition: 0.4s; 23 | transition: 0.4s; 24 | } 25 | 26 | .toggle-input .slider:before { 27 | position: absolute; 28 | content: ''; 29 | height: 18px; 30 | width: 18px; 31 | left: 3px; 32 | bottom: 3px; 33 | background-color: #eee; 34 | -webkit-transition: 0.4s; 35 | transition: 0.4s; 36 | } 37 | 38 | .toggle-input input:checked + .slider { 39 | background-color: #F85C04; 40 | } 41 | 42 | .toggle-input input:focus + .slider { 43 | box-shadow: 0 0 1px #F85C04; 44 | } 45 | 46 | .toggle-input input:checked + .slider:before { 47 | -webkit-transform: translateX(17px); 48 | -ms-transform: translateX(17px); 49 | transform: translateX(17px); 50 | } 51 | 52 | .toggle-input .slider.round { 53 | border-radius: 27px; 54 | } 55 | 56 | .toggle-input .slider.round:before { 57 | border-radius: 50%; 58 | } 59 | 60 | .nfts-table tr td:first-child { 61 | border-top-left-radius: 10px; 62 | border-bottom-left-radius: 10px; 63 | } 64 | 65 | .nfts-table tr td:last-child { 66 | border-top-right-radius: 10px; 67 | border-bottom-right-radius: 10px; 68 | } 69 | 70 | .nfts-table tr td { 71 | background-color: rgb(23 22 28); 72 | } 73 | 74 | .custom-scrollbar::-webkit-scrollbar, 75 | .custom-scroll-bar-select ul::-webkit-scrollbar { 76 | width: 0.5rem; 77 | border: 1px solid #363636; 78 | border-radius: 50px; 79 | } 80 | 81 | .custom-scrollbar::-webkit-scrollbar-thumb, 82 | .custom-scroll-bar-select ul::-webkit-scrollbar-thumb { 83 | background: #c9c9c9; 84 | cursor: pointer; 85 | border-radius: 0.15625rem; 86 | height: 1rem; 87 | } 88 | 89 | .custom-scrollbar::-webkit-scrollbar-track, 90 | .custom-scroll-bar-select ul::-webkit-scrollbar-track { 91 | background: #191a1c; 92 | border: 1px solid #363636; 93 | } 94 | 95 | .no-arrow-input input[type="number"] { 96 | -webkit-appearance: textfield; 97 | -moz-appearance: textfield; 98 | appearance: textfield; 99 | } 100 | 101 | .no-arrow-input input[type=number]::-webkit-inner-spin-button, 102 | .no-arrow-input input[type=number]::-webkit-outer-spin-button { 103 | -webkit-appearance: none; 104 | } -------------------------------------------------------------------------------- /src/pages/api/instructions/accept-offer.tsx: -------------------------------------------------------------------------------- 1 | import { Action, NightmarketClient, Offer } from '@motleylabs/mtly-nightmarket'; 2 | import { Connection, PublicKey, TransactionMessage, VersionedTransaction } from '@solana/web3.js'; 3 | import { NextApiRequest, NextApiResponse } from 'next'; 4 | 5 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 6 | try { 7 | const { price, mint, seller, buyer } = req.query; 8 | 9 | if (!price || !mint || !seller || !buyer) { 10 | return res.status(400).json({ error: 'Missing required parameters' }); 11 | } 12 | 13 | let mintPublicKey; 14 | let sellerPublicKey; 15 | let buyerPublicKey; 16 | let priceNumber; 17 | 18 | priceNumber = Number(price); 19 | 20 | if (isNaN(priceNumber)) { 21 | return res.status(400).json({ error: 'Invalid price' }); 22 | } 23 | 24 | try { 25 | mintPublicKey = new PublicKey(mint as string); 26 | sellerPublicKey = new PublicKey(seller as string); 27 | buyerPublicKey = new PublicKey(buyer as string); 28 | } catch (error) { 29 | return res.status(400).json({ error: 'Invalid mint, seller or buyer public key' }); 30 | } 31 | 32 | try { 33 | const nightmarketClient = new NightmarketClient(process.env.NEXT_PUBLIC_SOLANA_RPC_URL); 34 | 35 | const offers = (await nightmarketClient.GetOffers(mintPublicKey)).filter((offer: Offer) => 36 | nightmarketClient.IsLocalOffer(offer) 37 | ); 38 | 39 | if (offers.length === 0) { 40 | return res.status(404).json({ error: 'No offers from the night market' }); 41 | } 42 | 43 | const txRes: Action = await nightmarketClient.AcceptOffer( 44 | mintPublicKey, 45 | priceNumber, 46 | sellerPublicKey, 47 | buyerPublicKey 48 | ); 49 | 50 | if (!!txRes.err) { 51 | return res.status(400).json({ error: txRes.err }); 52 | } 53 | 54 | const connection = new Connection( 55 | process.env.NEXT_PUBLIC_SOLANA_RPC_URL ?? 'https://api.mainnet-beta.solana.com/' 56 | ); 57 | 58 | const { blockhash } = await connection.getLatestBlockhash(); 59 | 60 | const messageV0 = new TransactionMessage({ 61 | payerKey: sellerPublicKey, 62 | recentBlockhash: blockhash, 63 | instructions: txRes.instructions, 64 | }).compileToV0Message(txRes.altAccounts); 65 | 66 | const transactionV0 = new VersionedTransaction(messageV0); 67 | 68 | res.status(200).json({ tx: transactionV0.serialize() }); 69 | } catch (error) { 70 | console.log(error); 71 | return res.status(400).json({ error: error }); 72 | } 73 | } catch (error) { 74 | console.log(error); 75 | return res.status(500).json({ error: 'Internal server error' }); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/components/Chart/ChartTimeseries.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import { useMemo } from 'react'; 3 | import { Controller, useForm } from 'react-hook-form'; 4 | import { useTranslation } from 'react-i18next'; 5 | 6 | import { DateRangeOption } from '../../modules/time'; 7 | import type { DataPoint } from '../../typings'; 8 | import { ButtonGroup } from '../ButtonGroup'; 9 | import { Chart } from './Chart'; 10 | 11 | export function ChartTimeseries(props: { 12 | title: string; 13 | className?: string; 14 | isLoading: boolean; 15 | slug: string; 16 | timeseries?: DataPoint[]; 17 | onDateRangeChange?: (range: DateRangeOption) => void; 18 | }) { 19 | const { t } = useTranslation('analytics'); 20 | 21 | const { watch, control } = useForm({ 22 | defaultValues: { 23 | range: DateRangeOption.DAY, 24 | }, 25 | }); 26 | 27 | const dateRange = watch('range') as 28 | | DateRangeOption.DAY 29 | | DateRangeOption.WEEK 30 | | DateRangeOption.MONTH; 31 | 32 | const selectedDateRange = useMemo(() => { 33 | switch (dateRange) { 34 | case DateRangeOption.DAY: 35 | return t('oneDay', { ns: 'analytics' }); 36 | case DateRangeOption.WEEK: 37 | return t('oneWeek', { ns: 'analytics' }); 38 | case DateRangeOption.MONTH: 39 | return t('oneMonth', { ns: 'analytics' }); 40 | } 41 | }, [dateRange, t]); 42 | 43 | return ( 44 |
45 |
46 |
47 |

{props.title}

48 |

{selectedDateRange}

49 |
50 | ( 54 | { 57 | onChange(newValue); 58 | props.onDateRangeChange?.(newValue); 59 | }} 60 | style="plain" 61 | > 62 | 63 | 1D 64 | 65 | 66 | 7D 67 | 68 | 69 | 30D 70 | 71 | 72 | )} 73 | /> 74 |
75 | 80 |
81 | ); 82 | } 83 | -------------------------------------------------------------------------------- /src/components/Leaderboard/Info.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import { TFunction } from 'next-i18next'; 3 | import Image from 'next/image'; 4 | 5 | import infoIcon from '../../../public/images/leaderboard/info.svg'; 6 | import { HauoraFont } from '../../fonts'; 7 | import { LeaderboardInfoType } from '../../pages/leaderboard'; 8 | 9 | type LeaderboardInfoProps = { 10 | list: LeaderboardInfoType[]; 11 | }; 12 | 13 | export const LeaderboardInfo = ({ list }: LeaderboardInfoProps) => { 14 | return ( 15 |
16 | {list.map((item: LeaderboardInfoType, index) => ( 17 |
18 |
19 |
20 |
21 |
26 |
27 |
33 | {item.title} 34 |
35 | 36 | info-icon 37 |
38 | 39 |
40 | {(item.prefixSign ?? '') + item.value} 41 |
42 |
43 |
44 |
45 | 46 |
47 |
48 |
49 | ))} 50 |
51 | ); 52 | }; 53 | -------------------------------------------------------------------------------- /src/modules/reward-center/RewardCenterProgram.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from '@solana/web3.js'; 2 | 3 | import { BN } from 'bn.js'; 4 | 5 | import { AuctionHouseProgram } from '../../utils/mtly-house'; 6 | 7 | const REWARD_CENTER_PROGRAM = new PublicKey('rwdD3F6CgoCAoVaxcitXAeWRjQdiGc5AVABKCpQSMfd'); 8 | 9 | export class RewardCenterProgram { 10 | static PUBKEY = REWARD_CENTER_PROGRAM; 11 | 12 | static findRewardCenterAddress(auctionHouse: PublicKey): Promise<[PublicKey, number]> { 13 | return PublicKey.findProgramAddress( 14 | [Buffer.from('reward_center', 'utf8'), auctionHouse.toBuffer()], 15 | REWARD_CENTER_PROGRAM 16 | ); 17 | } 18 | 19 | static findListingAddress( 20 | seller: PublicKey, 21 | metadata: PublicKey, 22 | rewardCenter: PublicKey 23 | ): Promise<[PublicKey, number]> { 24 | return PublicKey.findProgramAddress( 25 | [ 26 | Buffer.from('listing', 'utf8'), 27 | seller.toBuffer(), 28 | metadata.toBuffer(), 29 | rewardCenter.toBuffer(), 30 | ], 31 | REWARD_CENTER_PROGRAM 32 | ); 33 | } 34 | 35 | static findOfferAddress( 36 | buyer: PublicKey, 37 | metadata: PublicKey, 38 | rewardCenter: PublicKey 39 | ): Promise<[PublicKey, number]> { 40 | return PublicKey.findProgramAddress( 41 | [Buffer.from('offer'), buyer.toBuffer(), metadata.toBuffer(), rewardCenter.toBuffer()], 42 | REWARD_CENTER_PROGRAM 43 | ); 44 | } 45 | 46 | static findAuctioneerAddress( 47 | auctionHouse: PublicKey, 48 | rewardCenter: PublicKey 49 | ): Promise<[PublicKey, number]> { 50 | return PublicKey.findProgramAddress( 51 | [Buffer.from('auctioneer', 'utf8'), auctionHouse.toBuffer(), rewardCenter.toBuffer()], 52 | AuctionHouseProgram.PUBKEY 53 | ); 54 | } 55 | 56 | static async findAuctioneerTradeStateAddress( 57 | wallet: PublicKey, 58 | auctionHouse: PublicKey, 59 | tokenAccount: PublicKey, 60 | treasuryMint: PublicKey, 61 | tokenMint: PublicKey, 62 | tokenSize: number 63 | ): Promise<[PublicKey, number]> { 64 | return PublicKey.findProgramAddress( 65 | [ 66 | Buffer.from('auction_house', 'utf8'), 67 | wallet.toBuffer(), 68 | auctionHouse.toBuffer(), 69 | tokenAccount.toBuffer(), 70 | treasuryMint.toBuffer(), 71 | tokenMint.toBuffer(), 72 | new BN('18446744073709551615').toArrayLike(Buffer, 'le', 8), 73 | new BN(tokenSize).toArrayLike(Buffer, 'le', 8), 74 | ], 75 | AuctionHouseProgram.PUBKEY 76 | ); 77 | } 78 | 79 | static async findPurchaseTicketAddress( 80 | listing: PublicKey, 81 | offer: PublicKey 82 | ): Promise<[PublicKey, number]> { 83 | return await PublicKey.findProgramAddress( 84 | [Buffer.from('purchase_ticket'), listing.toBuffer(), offer.toBuffer()], 85 | REWARD_CENTER_PROGRAM 86 | ); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/providers/CurrencyProvider.tsx: -------------------------------------------------------------------------------- 1 | import { CoinGeckoClient } from 'coingecko-api-v3'; 2 | import React, { useCallback, useEffect, useState } from 'react'; 3 | import type { ReactNode } from 'react'; 4 | 5 | import { asUsdString } from '../modules/number'; 6 | 7 | const COIN_GECKO_CURRENCY_IDS: { [key: string]: string } = { 8 | SOL: 'solana', 9 | USD: 'usd', 10 | }; 11 | 12 | interface ICurrencyContext { 13 | /** 14 | * false until all currencies have been loaded 15 | */ 16 | initialized: boolean; 17 | 18 | /** 19 | * Sol Price 20 | */ 21 | solPrice(): number; 22 | 23 | /** 24 | * @param sol SOL amount to convert to USD 25 | * @returns USD equivalent value of given SOL 26 | * @throws if the SOL/USD exchange rate could not be loaded 27 | */ 28 | solToUsd(sol: number): number; 29 | 30 | /** 31 | * @param sol SOL amount to convert to USD string 32 | * @returns USD equivalent value of given SOL formatted like $123,456.78 33 | * @throws if the SOL/USD exchange rate could not be loaded 34 | */ 35 | solToUsdString(sol: number): string; 36 | } 37 | 38 | export const CurrencyContext = React.createContext(null); 39 | 40 | interface CurrencyProviderProps { 41 | children: ReactNode; 42 | } 43 | 44 | export default function CurrencyProvider(props: CurrencyProviderProps): JSX.Element { 45 | const [initialized, setInitialized] = useState(false); 46 | const [solPrice, setSolPrice] = useState(); 47 | 48 | useEffect(() => { 49 | const client = new CoinGeckoClient({ 50 | timeout: 10000, 51 | autoRetry: true, 52 | }); 53 | client 54 | .simplePrice({ 55 | ids: COIN_GECKO_CURRENCY_IDS.SOL, 56 | vs_currencies: COIN_GECKO_CURRENCY_IDS.USD, 57 | }) 58 | .then((r) => { 59 | setSolPrice(r[COIN_GECKO_CURRENCY_IDS.SOL][COIN_GECKO_CURRENCY_IDS.USD]); 60 | }) 61 | .finally(() => setInitialized(true)); 62 | }, [setSolPrice, setInitialized]); 63 | 64 | const getSolPrice: ICurrencyContext['solPrice'] = useCallback(() => { 65 | if (solPrice == null) { 66 | return NaN; 67 | throw new Error('SOL price not available.'); 68 | } 69 | return solPrice * 1; 70 | }, [solPrice]); 71 | 72 | const solToUsd: ICurrencyContext['solToUsd'] = useCallback( 73 | (sol) => { 74 | if (solPrice == null) { 75 | return NaN; 76 | throw new Error('No known conversion rate from SOL to USD.'); 77 | } 78 | return sol * solPrice; 79 | }, 80 | [solPrice] 81 | ); 82 | 83 | const solToUsdString: ICurrencyContext['solToUsdString'] = useCallback( 84 | (sol) => asUsdString(solToUsd(sol)), 85 | [solToUsd] 86 | ); 87 | 88 | return ( 89 | 92 | {props.children} 93 | 94 | ); 95 | } 96 | -------------------------------------------------------------------------------- /src/components/Select.tsx: -------------------------------------------------------------------------------- 1 | import { Listbox, Transition } from '@headlessui/react'; 2 | import { ChevronDownIcon } from '@heroicons/react/24/solid'; 3 | 4 | import clsx from 'clsx'; 5 | import React, { Fragment } from 'react'; 6 | 7 | export default function Select(props: { 8 | value: T; 9 | onChange(value: T): void; 10 | options: { label: string; value: T }[]; 11 | className?: string; 12 | }) { 13 | return ( 14 | 15 | {({ open }) => ( 16 |
17 | 23 | 24 | {props.options.find((o) => o.value === props.value)?.label} 25 | 26 | 27 | 32 | 33 | 39 | 44 | {props.options.map((option, optionIdx) => ( 45 | 48 | clsx( 49 | 'relative mx-4 cursor-pointer select-none rounded-lg p-4 text-white hover:bg-gradient-primary hover:text-white', 50 | { 51 | 'text-primary-500': selected, 52 | } 53 | ) 54 | } 55 | value={option.value} 56 | disabled={option.value === 'last_update'} 57 | > 58 | {option.label} 59 | 60 | ))} 61 | 62 | 63 |
64 | )} 65 |
66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /public/locales/en/common.json: -------------------------------------------------------------------------------- 1 | { 2 | "header": { 3 | "title": "Night Market" 4 | }, 5 | "follow": "Follow", 6 | "cancel": "Cancel", 7 | "unfollow": "Unfollow", 8 | "connect": "Connect", 9 | "filters": "Filters", 10 | "buy": "Buy", 11 | "View": "View", 12 | "all": "Show All", 13 | "listed": "Listed", 14 | "unlisted": "Unlisted", 15 | "allActivity": "All Activity", 16 | "offers": "Offers", 17 | "sales": "Sales", 18 | "youPaid": "You Paid", 19 | "accept": "Accept", 20 | "search": { 21 | "collection": "Collection", 22 | "profiles": "Profiles", 23 | "wallet": "Wallet", 24 | "nfts": "NFTs", 25 | "placeholder": "Search for collections, NFTs or profiles...", 26 | "empty": "No results found", 27 | "nftLabel": "No NFTs match search:", 28 | "profileLabel": "No wallets or Twitter profiles match search:", 29 | "collectionLabel": "No collections match search:" 30 | }, 31 | "profileMenu": { 32 | "collected": "Collected NFTs", 33 | "created": "Created NFTs", 34 | "activity": "Wallet Activity", 35 | "analytics": "Wallet Analytics" 36 | }, 37 | "navigation": { 38 | "collections": "Collections", 39 | "discover": "Discover" 40 | }, 41 | "offerable": { 42 | "yourOffer": "Your Offer", 43 | "makeOffer": "Make an offer", 44 | "floorPrice": "Current Floor Price", 45 | "listPrice": "Current Listed Price", 46 | "lastSoldPrice": "Last sold", 47 | "minimumOfferAmount": "Minimum Offer Amount", 48 | "currentBalance": "Current Wallet Balance", 49 | "amount": "Amount", 50 | "connectToOffer": "Connect to make offers" 51 | }, 52 | "buyable": { 53 | "buyNow": "Buy Now", 54 | "notEnoughBalance": "Insufficient Balance", 55 | "earnSauce": "You'll earn", 56 | "floorPrice": "Current Floor Price", 57 | "listPrice": "Current Listed Price", 58 | "currentBalance": "Current Wallet Balance", 59 | "marketplaceFee": "Marketplace Fee", 60 | "buyNowButton": "Buy now", 61 | "connectToBuy": "Connect to purchase" 62 | }, 63 | "pagination": { 64 | "totalItems": "Total items", 65 | "itemsPerPage": "Items per page" 66 | }, 67 | "collection": { 68 | "verified": "Verified Collection.", 69 | "enforced": "Royalties are enforced." 70 | }, 71 | "copyLink": "Copy Link", 72 | "copied": "Copied", 73 | "shareTwitter": "Share to Twitter", 74 | "purchase": "Sold", 75 | "listing": "Listed", 76 | "offer": "Offer", 77 | "cancelledOffer": "Cancelled Offer", 78 | "cancelledListing": "Cancelled Listing", 79 | "update": "Update", 80 | "list": "List", 81 | "lastSale": "Last Sale", 82 | "viewProfile": "View Profile", 83 | "switchWallet": "Switch Wallet", 84 | "disconnectWallet": "Disconnect Wallet", 85 | "clear": "Clear", 86 | "bulbTooltip": { 87 | "title": "Rewarding your actions", 88 | "description": "Throughout the bustling alleyways of Night Market, your actions turn into tickets. Happy earning!" 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/components/Chart/TinyLineChart.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslation } from 'next-i18next'; 2 | import type { ReactNode } from 'react'; 3 | import { useRef } from 'react'; 4 | import { TailSpin } from 'react-loader-spinner'; 5 | import { CartesianGrid, Line, LineChart, ResponsiveContainer, Tooltip, YAxis } from 'recharts'; 6 | 7 | import type { DataPoint } from '../../typings'; 8 | import { CustomLineChartTooltip } from './Chart'; 9 | 10 | export function TinyLineChart(props: { 11 | height?: number; 12 | data: DataPoint[]; 13 | loading: boolean; 14 | animation: boolean; 15 | options?: { 16 | yDataKey?: string; 17 | }; 18 | children?: ReactNode; 19 | }) { 20 | const { t } = useTranslation('analytics'); 21 | 22 | const chartRef = useRef(null); 23 | 24 | // TODO: replace this hack with a better solution once this https://github.com/recharts/recharts/issues/3055 is resolved 25 | // could for better accuracy check if the values are the same across the entire array but for speed we'll avoid that for now 26 | const hasNoChange: boolean = props.data[0]?.amount === props.data[props.data.length - 1]?.amount; 27 | 28 | return props.data.length > 0 && !props.loading ? ( 29 | 30 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 51 | 59 | } 61 | wrapperStyle={{ 62 | backgroundColor: '#2D2D2D', 63 | padding: '4px', 64 | borderRadius: '2px', 65 | fontSize: '10px', 66 | }} 67 | /> 68 | 69 | {props.children} 70 | 71 | 72 | ) : ( 73 |
74 | {props.loading ? ( 75 | 81 | ) : ( 82 | t('noData', { ns: 'analytics' }) 83 | )} 84 |
85 | ); 86 | } 87 | -------------------------------------------------------------------------------- /public/images/leaderboard/medals/gold.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 8 | 10 | 12 | 13 | 14 | 15 | 16 | 17 | 19 | 22 | 24 | 26 | 27 | 28 | 29 | 30 | 32 | 33 | 34 | 35 | 36 | 38 | 39 | 40 | 41 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/utils/price.ts: -------------------------------------------------------------------------------- 1 | import { LAMPORTS_PER_SOL } from '@solana/web3.js'; 2 | 3 | import BN from 'bn.js'; 4 | 5 | import { toSol } from '../modules/sol'; 6 | import { AuctionHouse, Nft } from '../typings'; 7 | 8 | export function formatAsBN(value: string | number | null | undefined): string { 9 | if (!value) { 10 | return '0'; 11 | } 12 | 13 | const bnValue = new BN(value); 14 | const formattedValue = bnValue.div(new BN('10000000')).toNumber().toFixed(2); 15 | 16 | return formattedValue; 17 | } 18 | 19 | export function formatAsSOL(price: BN): number { 20 | if (!price) { 21 | return 0; 22 | } 23 | 24 | return toSol(price.toNumber()); 25 | } 26 | 27 | export function getRoundedValue(value: number, round: number): number { 28 | if (round > 0) { 29 | let multiplied = Math.round(value * 10 ** round); 30 | while (multiplied === 0) { 31 | round += 1; 32 | multiplied = Math.round(value * 10 ** round); 33 | } 34 | return multiplied / 10 ** round; 35 | } 36 | 37 | return value; 38 | } 39 | 40 | export function getSolFromLamports(price: number | string, decimals = 0, round = 0): number { 41 | let value = Number(price) / LAMPORTS_PER_SOL; 42 | if (value === 0) { 43 | return 0; 44 | } 45 | 46 | if (decimals > 0) { 47 | value = value / 10 ** decimals; 48 | } 49 | 50 | return getRoundedValue(value, round); 51 | } 52 | 53 | export function getExtendedSolFromLamports(price: string, decimals = 3, round = 2): string { 54 | const numPrice = Number(price); 55 | if (numPrice < 1000) { 56 | return `${getSolFromLamports(price, 0, round)}`; 57 | } else { 58 | return `${getSolFromLamports(price, decimals, round)} K`; 59 | } 60 | } 61 | 62 | export function getMegaValue(strVal: string): string { 63 | const val = Number(strVal); 64 | if (val === 0) { 65 | return `0`; 66 | } else if (val < 1) { 67 | return `${val}`; 68 | } else if (val < 1000) { 69 | const multiplied = Math.round(val * 10 ** 3) / 10 ** 6; 70 | return `${getRoundedValue(multiplied, 2)} K`; 71 | } else { 72 | const multiplied = Math.round(val * 10 ** 3) / 10 ** 9; 73 | return `${getRoundedValue(multiplied, 2)} M`; 74 | } 75 | } 76 | 77 | interface BuyerTotals { 78 | nftPrice: number; 79 | totalPrice: number; 80 | totalRoyalties: number; 81 | totalMarketplaceFee: number; 82 | } 83 | 84 | export function buyerTotalsForListing( 85 | nft: Nft | null, 86 | isOwnMarket: boolean, 87 | auctionHouse?: AuctionHouse | null 88 | ): BuyerTotals { 89 | if (!nft || !nft.latestListing) { 90 | return {} as BuyerTotals; 91 | } 92 | 93 | const nftPrice = +nft?.latestListing?.price ?? 0; 94 | 95 | const royalties = nft?.sellerFeeBasisPoints ?? 0; 96 | const marketplaceFee = isOwnMarket ? auctionHouse?.sellerFeeBasisPoints : 0; 97 | 98 | const totalRoyalties = Math.ceil(nftPrice * (royalties / 10000)); 99 | const totalMarketplaceFee = Math.ceil(nftPrice * ((marketplaceFee ?? 0) / 10000)); 100 | 101 | return { 102 | nftPrice: nftPrice, 103 | totalPrice: nftPrice + totalRoyalties + totalMarketplaceFee, 104 | totalRoyalties: totalRoyalties, 105 | totalMarketplaceFee: totalMarketplaceFee, 106 | }; 107 | } 108 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "night-market", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "serve": "next build && next start", 9 | "start": "next start", 10 | "lint": "next lint", 11 | "format": "prettier --write next.config.js ./src/**/*.{ts,tsx,json} ./public/locales/**/*.json" 12 | }, 13 | "lint-staged": { 14 | "*.ts": "prettier --write", 15 | "*.tsx": "prettier --write" 16 | }, 17 | "dependencies": { 18 | "@bugsnag/js": "^7.21.0", 19 | "@bugsnag/plugin-react": "^7.19.0", 20 | "@cardinal/mpl-candy-machine-utils": "^4.5.0", 21 | "@headlessui/react": "^1.7.17", 22 | "@heroicons/react": "^2.0.18", 23 | "@hookform/resolvers": "^3.3.1", 24 | "@ladderlabs/buddylink": "=0.1.6", 25 | "@metaplex-foundation/mpl-token-metadata": "=2.13.0", 26 | "@motleylabs/mtly-auction-house": "^2.5.6", 27 | "@motleylabs/mtly-nightmarket": "^1.3.0", 28 | "@motleylabs/mtly-reward-center": "^0.2.9", 29 | "@noble/curves": "^1.2.0", 30 | "@project-serum/anchor": "^0.26.0", 31 | "@react-hook/window-size": "^3.1.1", 32 | "@solana/spl-token": "^0.3.8", 33 | "@solana/wallet-adapter-base": "^0.9.23", 34 | "@solana/wallet-adapter-react": "^0.15.35", 35 | "@solana/wallet-adapter-react-ui": "^0.9.34", 36 | "@solana/wallet-adapter-wallets": "^0.19.22", 37 | "@solana/web3.js": "^1.78.5", 38 | "@tailwindcss/line-clamp": "^0.4.4", 39 | "@types/react-transition-group": "^4.4.6", 40 | "@vercel/og": "^0.5.17", 41 | "abort-controller": "^3.0.0", 42 | "ahooks": "^3.7.8", 43 | "autoprefixer": "^10.4.16", 44 | "bn.js": "^5.2.1", 45 | "clsx": "^2.0.0", 46 | "coingecko-api-v3": "^0.0.29", 47 | "date-fns": "^2.30.0", 48 | "i18next": "^23.5.1", 49 | "js-base64": "^3.7.5", 50 | "next": "^13.5.3", 51 | "next-i18next": "^14.0.3", 52 | "nookies": "^2.5.2", 53 | "qrcode.react": "^3.1.0", 54 | "querystring": "^0.2.1", 55 | "react": "^18.2.0", 56 | "react-debounce-input": "^3.3.0", 57 | "react-device-detect": "^2.2.3", 58 | "react-dom": "^18.2.0", 59 | "react-hook-form": "^7.46.2", 60 | "react-hotjar": "^6.1.0", 61 | "react-i18next": "^13.2.2", 62 | "react-infinite-scroll-component": "^6.1.0", 63 | "react-loader-spinner": "^5.4.5", 64 | "react-popper-tooltip": "^4.4.2", 65 | "react-toastify": "^9.1.3", 66 | "react-transition-group": "^4.4.5", 67 | "react-use-websocket": "^4.4.0", 68 | "recharts": "^2.8.0", 69 | "sharp": "^0.32.6", 70 | "swiper": "^10.3.0", 71 | "swr": "^2.2.4", 72 | "tailwind-scrollbar": "^3.0.5", 73 | "zod": "^3.22.2" 74 | }, 75 | "devDependencies": { 76 | "@trivago/prettier-plugin-sort-imports": "^4.2.0", 77 | "@types/bn.js": "^5.1.2", 78 | "@types/luxon": "^3.3.2", 79 | "@types/node": "^20.6.5", 80 | "@types/react": "^18.2.22", 81 | "@types/react-dom": "18.2.7", 82 | "@types/recharts": "^1.8.24", 83 | "@typescript-eslint/eslint-plugin": "^6.7.3", 84 | "eslint": "8.50.0", 85 | "eslint-config-next": "^13.5.3", 86 | "eslint-plugin-unused-imports": "^3.0.0", 87 | "lint-staged": "^14.0.1", 88 | "postcss": "^8.4.30", 89 | "prettier": "^3.0.3", 90 | "prettier-plugin-tailwindcss": "^0.5.4", 91 | "tailwind-clip-path": "^1.0.0", 92 | "tailwindcss": "^3.3.3", 93 | "turbo": "^1.10.14", 94 | "typescript": "5.2.2" 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/components/Offer.tsx: -------------------------------------------------------------------------------- 1 | import { useWallet } from '@solana/wallet-adapter-react'; 2 | 3 | import { useTranslation } from 'next-i18next'; 4 | import { useMemo } from 'react'; 5 | 6 | import config from '../app.config'; 7 | import type { Offer, Nft, AuctionHouse } from '../typings'; 8 | import type { AcceptOfferResponse } from './../hooks/offer'; 9 | import { useCloseOffer, useAcceptOffer } from './../hooks/offer'; 10 | import { Activity, ActivityType } from './Activity'; 11 | import Button, { ButtonSize, ButtonBackground, ButtonColor, ButtonBorder } from './Button'; 12 | 13 | interface OfferProps { 14 | offer: Offer; 15 | auctionHouse?: AuctionHouse | null; 16 | avatar?: JSX.Element; 17 | meta: JSX.Element; 18 | nft: Nft | null; 19 | source?: JSX.Element; 20 | onCancel: () => void; 21 | onAccept: (payload: AcceptOfferResponse) => void; 22 | } 23 | 24 | export default function OfferUI({ 25 | offer, 26 | source, 27 | auctionHouse, 28 | nft, 29 | meta, 30 | avatar, 31 | onAccept, 32 | onCancel, 33 | }: OfferProps): JSX.Element { 34 | const { publicKey } = useWallet(); 35 | const { t } = useTranslation('common'); 36 | const { closingOffer, onCloseOffer } = useCloseOffer(offer); 37 | const viewerAddress = useMemo(() => publicKey?.toBase58(), [publicKey]); 38 | 39 | const { onAcceptOffer, acceptingOffer } = useAcceptOffer(offer); 40 | 41 | const handleAcceptOffer = async () => { 42 | if (!auctionHouse || !nft) { 43 | return; 44 | } 45 | 46 | try { 47 | const response = await onAcceptOffer({ auctionHouse, nft }); 48 | 49 | if (!response) { 50 | return; 51 | } 52 | 53 | onAccept(response); 54 | } catch (e: unknown) {} 55 | }; 56 | 57 | const handleCancelOffer = async () => { 58 | try { 59 | await onCloseOffer({ nft, auctionHouse }); 60 | onCancel(); 61 | } catch (err: unknown) {} 62 | }; 63 | 64 | return ( 65 | 74 | {offer.buyer === viewerAddress && ( 75 | 86 | )} 87 | {nft?.owner === viewerAddress && ( 88 | 98 | )} 99 | 100 | ) 101 | } 102 | > 103 | 104 | 105 | 106 | ); 107 | } 108 | -------------------------------------------------------------------------------- /src/components/Drop.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslation } from 'next-i18next'; 2 | import React from 'react'; 3 | 4 | import { asCompactNumber } from '../modules/number'; 5 | import Button, { ButtonBackground, ButtonBorder, ButtonColor, ButtonSize } from './Button'; 6 | import Icon from './Icon'; 7 | import Img from './Image'; 8 | import Link from 'next/link'; 9 | 10 | interface DropProps { 11 | title: string; 12 | description: string; 13 | price: number | string; 14 | supply: number; 15 | image: string; 16 | link: string; 17 | launchDate: Date; 18 | } 19 | 20 | export default function Drop({ title, description, price, supply, image, link }: DropProps) { 21 | const { t } = useTranslation('home'); 22 | return ( 23 |
24 |
25 | {`${title}-drop`} 31 |
{title}
32 |
33 |
34 |
{title}
35 |

{description}

36 |
    37 | {price !== '' && ( 38 |
  • 39 |

    {t('drops.price', { ns: 'home' })}

    40 |

    41 | 42 | {price} 43 |

    44 |
  • 45 | )} 46 |
  • 47 |

    {t('drops.supply', { ns: 'home' })}

    48 |

    49 | {asCompactNumber(supply)} 50 |

    51 |
  • 52 | 53 |
  • 54 | 55 | 65 | 66 |
  • 67 |
68 |
69 | 70 | 79 | 80 |
81 |
82 |
83 | ); 84 | } 85 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=Domine&family=Merriweather&family=Noto+Serif&family=Playfair+Display&family=Roboto&family=Source+Sans+Pro&family=Work+Sans&family=Inter&family=Urbanist&family=Space+Mono&display=swap'); 2 | @import '@solana/wallet-adapter-react-ui/styles.css'; 3 | @import 'swiper/css'; 4 | @import 'swiper/css/navigation'; 5 | @import 'swiper/css/grid'; 6 | @import 'react-toastify/dist/ReactToastify.css'; 7 | 8 | @import 'tailwindcss/base'; 9 | @import 'tailwindcss/components'; 10 | @import 'tailwindcss/utilities'; 11 | @import 'tailwindcss/variants'; 12 | 13 | h1, 14 | h2, 15 | h3, 16 | h4, 17 | h5, 18 | p, 19 | h6 { 20 | @apply text-white; 21 | } 22 | 23 | body, 24 | html { 25 | @apply bg-black; 26 | } 27 | 28 | .preload-ticket-animation-images::after { 29 | position: absolute; 30 | width: 0; 31 | height: 0; 32 | overflow: hidden; 33 | z-index: -1; 34 | content: url(/images/animated/ticket/1.svg) url(/images/animated/ticket/2.svg) 35 | url(/images/animated/ticket/3.svg) url(/images/animated/ticket/4.svg) 36 | url(/images/animated/ticket/5.svg) url(/images/animated/ticket/6.svg) 37 | url(/images/animated/ticket/7.svg); 38 | } 39 | 40 | .input { 41 | @apply rounded-md border border-gray-700; 42 | } 43 | .wallet-modal-theme .wallet-adapter-modal-wrapper { 44 | @apply rounded-2xl bg-gray-800 font-sans; 45 | } 46 | 47 | .wallet-modal-theme .wallet-adapter-button:not([disabled]):hover { 48 | @apply bg-gray-800; 49 | } 50 | 51 | .wallet-modal-theme .wallet-adapter-modal-button-close { 52 | @apply bg-gray-800; 53 | } 54 | 55 | .swiper-button-disabled { 56 | @apply opacity-0; 57 | } 58 | 59 | :focus-visible { 60 | outline: none; 61 | } 62 | 63 | input[type='search']::-webkit-search-cancel-button { 64 | /* -webkit-appearance: none; */ 65 | color: white; 66 | filter: brightness(10); 67 | } 68 | 69 | .recharts-wrapper .recharts-cartesian-grid-horizontal line:first-child, 70 | .recharts-wrapper .recharts-cartesian-grid-horizontal line:last-child { 71 | stroke-opacity: 0; 72 | } 73 | 74 | .fade-enter div { 75 | opacity: 0; 76 | /* transform: translateY(-100%); */ 77 | } 78 | .fade-exit div { 79 | opacity: 1; 80 | /* transform: translateY(0%); */ 81 | } 82 | .fade-enter-active div { 83 | opacity: 1; 84 | /* transform: translateY(0%); */ 85 | } 86 | .fade-exit-active div { 87 | opacity: 0; 88 | /* transform: translateY(100%); */ 89 | } 90 | .fade-enter-active div, 91 | .fade-exit-active div { 92 | transition: opacity 250ms, transform 250ms; 93 | } 94 | 95 | .faded-gradient-text { 96 | background: linear-gradient( 97 | 180deg, 98 | rgba(255, 255, 255, 0) -13.46%, 99 | rgba(255, 255, 255, 0.568842) 20.3%, 100 | #ffffff 100% 101 | ); 102 | -webkit-background-clip: text; 103 | -webkit-text-fill-color: transparent; 104 | background-clip: text; 105 | text-fill-color: transparent; 106 | } 107 | 108 | .rank-polygon-container { 109 | background: radial-gradient(67.28% 252.57% at 105.07% 50%, #2e2d33 0%, rgba(46, 45, 51, 0) 100%); 110 | box-shadow: 6px 0px 33px rgba(3, 3, 4, 0.7); 111 | } 112 | 113 | @keyframes gleam { 114 | 40% { 115 | background-position: -600px 0; 116 | } 117 | 118 | 100% { 119 | background-position: -600px 0; 120 | } 121 | } 122 | 123 | @import 'collection.css'; 124 | @import 'error.css'; 125 | 126 | @media (min-width: 768px) { 127 | .Toastify__toast-container--top-right { 128 | top: 8rem !important; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /public/images/marketplaces/opensea.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /public/images/leaderboard/popup/nft.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 20 | 21 | -------------------------------------------------------------------------------- /src/hooks/globalsearch.ts: -------------------------------------------------------------------------------- 1 | import type React from 'react'; 2 | import { useState, useCallback } from 'react'; 3 | import useSWR from 'swr'; 4 | 5 | import type { GlobalSearchData, Nft } from '../typings'; 6 | import type { StatSearchData } from '../typings'; 7 | import { debounce } from '../utils/debounce'; 8 | import { useTrendingSearch } from './collection/useTrendingSearch'; 9 | 10 | const TOKEN_LENGTH = 44; 11 | 12 | export enum SearchMode { 13 | Nft = 'nft', 14 | Profile = 'profile', 15 | Collection = 'collection', 16 | } 17 | 18 | const defaultQueryData = '&limit=10&offset=0'; 19 | 20 | type OnUpdateSearch = (evt: React.ChangeEvent) => void; 21 | 22 | interface GlobalSearchContext { 23 | searchTerm: string; 24 | setSearchTerm: React.Dispatch>; 25 | hasResults: boolean; 26 | updateSearch: OnUpdateSearch; 27 | searching: boolean; 28 | results: GlobalSearchData; 29 | } 30 | 31 | export default function useGlobalSearch(): GlobalSearchContext { 32 | const [searchTerm, setSearchTerm] = useState(''); 33 | 34 | const debouncedUpdateSearch = useCallback( 35 | debounce((value: string) => { 36 | setSearchTerm(value); 37 | }, 500), 38 | [] 39 | ); 40 | 41 | const updateSearch: OnUpdateSearch = useCallback( 42 | (evt) => { 43 | debouncedUpdateSearch(evt.target.value); 44 | }, 45 | [debouncedUpdateSearch] 46 | ); 47 | 48 | const { data: trendingCollections, isValidating: isValidatingTrendingCollections } = 49 | useTrendingSearch(searchTerm); 50 | 51 | const { data: collections, isValidating: isValidatingCollections } = useSWR( 52 | searchTerm 53 | ? `/stat/search?keyword=${searchTerm}&mode=${SearchMode.Collection}${defaultQueryData}` 54 | : null, 55 | { revalidateOnFocus: false } 56 | ); 57 | 58 | const { data: profiles, isValidating: isValidatingProfiles } = useSWR( 59 | searchTerm 60 | ? `/stat/search?keyword=${searchTerm}&mode=${SearchMode.Profile}${defaultQueryData}` 61 | : null, 62 | { revalidateOnFocus: false } 63 | ); 64 | 65 | const { data: nft, isValidating: isValidatingNft } = useSWR( 66 | searchTerm && searchTerm.length === TOKEN_LENGTH ? `/nfts/${searchTerm}` : null, 67 | { revalidateOnFocus: false } 68 | ); 69 | 70 | const isLoading = 71 | (!searchTerm && !trendingCollections && isValidatingTrendingCollections) || 72 | (!!searchTerm && !collections && isValidatingCollections) || 73 | (!profiles && isValidatingProfiles) || 74 | (!nft && isValidatingNft); 75 | 76 | return { 77 | searchTerm, 78 | setSearchTerm, 79 | hasResults: 80 | !isLoading && 81 | ((!!searchTerm && !!collections?.results?.length) || 82 | (!searchTerm && !!trendingCollections?.trends.length) || 83 | !!profiles?.results?.length || 84 | !!nft), 85 | searching: isLoading, 86 | results: { 87 | nft: nft ? { searchType: SearchMode.Nft, ...nft } : undefined, 88 | profiles: profiles 89 | ? profiles.results.map((p) => ({ ...p, searchType: SearchMode.Profile })) 90 | : [], 91 | collections: !searchTerm 92 | ? trendingCollections 93 | ? trendingCollections.trends.map((c) => ({ 94 | imgURL: c.collection.image, 95 | isVerified: c.collection.isVerified, 96 | name: c.collection.name, 97 | slug: c.collection.slug, 98 | volume1d: c.volume1d, 99 | address: '', 100 | twitter: '', 101 | searchType: SearchMode.Collection, 102 | })) 103 | : [] 104 | : collections 105 | ? collections.results.map((c) => ({ ...c, searchType: SearchMode.Collection })) 106 | : [], 107 | }, 108 | updateSearch, 109 | }; 110 | } 111 | -------------------------------------------------------------------------------- /src/utils/solana.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/debridge-finance/solana-tx-parser-public/blob/master/src/helpers.ts#L28 2 | // integrated here to avoid security issues with npm packages 3 | import { utils } from '@project-serum/anchor'; 4 | import { 5 | AccountMeta, 6 | CompiledInstruction, 7 | LoadedAddresses, 8 | Message, 9 | MessageCompiledInstruction, 10 | PartiallyDecodedInstruction, 11 | PublicKey, 12 | TransactionInstruction, 13 | VersionedMessage, 14 | VersionedTransactionResponse, 15 | } from '@solana/web3.js'; 16 | 17 | export function parseTransactionAccounts( 18 | message: T, 19 | loadedAddresses: T extends VersionedMessage ? LoadedAddresses | undefined : undefined = undefined 20 | ): AccountMeta[] { 21 | const accounts: PublicKey[] = 22 | message.version === 'legacy' ? message.accountKeys : message.staticAccountKeys; 23 | const readonlySignedAccountsCount = message.header.numReadonlySignedAccounts; 24 | const readonlyUnsignedAccountsCount = message.header.numReadonlyUnsignedAccounts; 25 | const requiredSignaturesAccountsCount = message.header.numRequiredSignatures; 26 | const totalAccounts = accounts.length; 27 | let parsedAccounts: AccountMeta[] = accounts.map((account, idx) => { 28 | const isWritable = 29 | idx < requiredSignaturesAccountsCount - readonlySignedAccountsCount || 30 | (idx >= requiredSignaturesAccountsCount && 31 | idx < totalAccounts - readonlyUnsignedAccountsCount); 32 | 33 | return { 34 | isSigner: idx < requiredSignaturesAccountsCount, 35 | isWritable, 36 | pubkey: new PublicKey(account), 37 | } as AccountMeta; 38 | }); 39 | const [ALTWritable, ALTReadOnly] = 40 | message.version === 'legacy' 41 | ? [[], []] 42 | : loadedAddresses 43 | ? [loadedAddresses.writable, loadedAddresses.readonly] 44 | : [[], []]; // message.getAccountKeys({ accountKeysFromLookups: loadedAddresses }).keySegments().slice(1); // omit static keys 45 | if (ALTWritable) 46 | parsedAccounts = [ 47 | ...parsedAccounts, 48 | ...ALTWritable.map((pubkey) => ({ isSigner: false, isWritable: true, pubkey })), 49 | ]; 50 | if (ALTReadOnly) 51 | parsedAccounts = [ 52 | ...parsedAccounts, 53 | ...ALTReadOnly.map((pubkey) => ({ isSigner: false, isWritable: false, pubkey })), 54 | ]; 55 | 56 | return parsedAccounts; 57 | } 58 | 59 | /** 60 | * Converts compiled instruction into common TransactionInstruction 61 | * @param compiledInstruction 62 | * @param parsedAccounts account meta, result of {@link parseTransactionAccounts} 63 | * @returns TransactionInstruction 64 | */ 65 | export function compiledInstructionToInstruction< 66 | Ix extends CompiledInstruction | MessageCompiledInstruction, 67 | >(compiledInstruction: Ix, parsedAccounts: AccountMeta[]): TransactionInstruction { 68 | if (typeof compiledInstruction.data === 'string') { 69 | const ci = compiledInstruction as CompiledInstruction; 70 | 71 | return new TransactionInstruction({ 72 | data: utils.bytes.bs58.decode(ci.data), 73 | programId: parsedAccounts[ci.programIdIndex].pubkey, 74 | keys: ci.accounts.map((accountIdx) => parsedAccounts[accountIdx]), 75 | }); 76 | } else { 77 | const ci = compiledInstruction as MessageCompiledInstruction; 78 | 79 | return new TransactionInstruction({ 80 | data: Buffer.from(ci.data), 81 | programId: parsedAccounts[ci.programIdIndex].pubkey, 82 | keys: ci.accountKeyIndexes.map((accountIndex) => { 83 | if (accountIndex >= parsedAccounts.length) 84 | throw new Error( 85 | `Trying to resolve account at index ${accountIndex} while parsedAccounts is only ${parsedAccounts.length}. \ 86 | Looks like you're trying to parse versioned transaction, make sure that LoadedAddresses are passed to the \ 87 | parseTransactionAccounts function` 88 | ); 89 | 90 | return parsedAccounts[accountIndex]; 91 | }), 92 | }); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/components/Leaderboard/Banner.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import { TFunction } from 'next-i18next'; 3 | import Image from 'next/image'; 4 | import React, { useMemo } from 'react'; 5 | import useSWRInfinite from 'swr/infinite'; 6 | 7 | import bannerIllustrationImage from '../../../public/images/leaderboard/banner-illustration.svg'; 8 | import { 9 | CollectionSort, 10 | CollectionTrend, 11 | CollectionsTrendsData, 12 | OrderDirection, 13 | } from '../../typings/index.d'; 14 | import Button, { ButtonBackground, ButtonBorder, ButtonColor } from '../Button'; 15 | 16 | type LeaderboardBannerProps = { 17 | t: TFunction; 18 | openHowToEarnModal: () => void; 19 | }; 20 | 21 | export default function LeaderboardBanner({ t }: LeaderboardBannerProps) { 22 | const leaderboardNS = 'leaderboard'; 23 | 24 | const DEFAULT_SORT: CollectionSort = CollectionSort.Volume; 25 | const DEFAULT_ORDER: OrderDirection = OrderDirection.Desc; 26 | const PAGE_LIMIT = 10; 27 | 28 | const getTrendsKey = (pageIndex: number, previousPageData: CollectionsTrendsData) => { 29 | if (previousPageData && !previousPageData.hasNextPage) return null; 30 | 31 | const query = `sort_by=${DEFAULT_SORT}&order=${DEFAULT_ORDER}&limit=${PAGE_LIMIT}&offset=${ 32 | pageIndex * PAGE_LIMIT 33 | }`; 34 | 35 | return `/collections/trend?${query}`; 36 | }; 37 | 38 | const { data: collectionsTrendsData } = useSWRInfinite(getTrendsKey, { 39 | revalidateOnFocus: false, 40 | }); 41 | 42 | const trends: CollectionTrend[] = useMemo( 43 | () => 44 | collectionsTrendsData ? collectionsTrendsData.flatMap((pageData) => pageData.trends) : [], 45 | [collectionsTrendsData] 46 | ); 47 | 48 | return ( 49 |
50 |
51 |
52 | {t('banner.leaderboard', { ns: leaderboardNS })} 53 |
54 | 55 |
56 |
{`${t('banner.description.1', { ns: leaderboardNS })} `}
57 | {`${t('banner.description.2', { ns: leaderboardNS })} `} 58 | {/* 59 | {`${t('banner.howToEarn', { 60 | ns: leaderboardNS, 61 | })}`} 62 | */} 63 |
64 | 65 | 79 |
80 | 81 | leaderbord-banner 91 |
92 | ); 93 | } 94 | -------------------------------------------------------------------------------- /src/components/BulkListing/ListingItem.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslation } from 'next-i18next'; 2 | import type { FormState, UseFormRegister } from 'react-hook-form'; 3 | 4 | import { useCurrencies } from '../../hooks/currencies'; 5 | import type { BulkListNftForm } from '../../hooks/list'; 6 | import type { Nft } from '../../typings'; 7 | import { getAssetURL, AssetSize } from '../../utils/assets'; 8 | import { getSolFromLamports } from '../../utils/price'; 9 | import { Form } from '../Form'; 10 | import Icon from '../Icon'; 11 | import Image from '../Image'; 12 | import Tooltip from '../Tooltip'; 13 | import { NUMBER_REGEX } from './BulkListModal'; 14 | 15 | interface ListingItemProps { 16 | nft: Nft; 17 | price?: string; 18 | disabled: boolean; 19 | registerBulkListNft: UseFormRegister; 20 | bulkListNftState: FormState; 21 | success: boolean; 22 | failed: boolean; 23 | } 24 | export default function ListingItem({ 25 | nft, 26 | disabled, 27 | registerBulkListNft, 28 | bulkListNftState, 29 | success, 30 | failed, 31 | }: ListingItemProps): JSX.Element { 32 | const { t } = useTranslation('profile'); 33 | const { solToUsdString } = useCurrencies(); 34 | const lastSale = nft.lastSale; 35 | const collectionName = nft.name; 36 | 37 | const renderInfoTooltip = () => 38 | !!lastSale ? ( 39 | 42 |

43 | {t('bulkListing.lastSalePrice', { ns: 'profile' })} 44 |

45 |
46 | 47 |

{getSolFromLamports(lastSale.price, 0, 3)}

48 |
49 |

50 | {solToUsdString(getSolFromLamports(lastSale.price))} 51 |

52 |
53 | } 54 | placement="right" 55 | > 56 | 57 | 58 | ) : null; 59 | 60 | return ( 61 |
62 |
63 |
64 | nft image 69 | {success && ( 70 |
71 | 72 |
73 | )} 74 | {failed && ( 75 |
76 | 77 |
78 | )} 79 |
80 |
81 |
82 |

83 | {nft.name} 84 |

85 | {renderInfoTooltip()} 86 |
87 |

{collectionName}

88 |
89 |
90 | 91 |
92 | 97 | !disabled ? Boolean(+value) && Boolean(value.match(NUMBER_REGEX)?.length) : true, 98 | })} 99 | autoComplete="off" 100 | icon={} 101 | disabled={disabled} 102 | /> 103 | 104 |
105 |
106 | ); 107 | } 108 | --------------------------------------------------------------------------------