Initial setup: standalone app from lux/web monorepo

- Converted from pnpm workspace catalog references to fixed versions
- Added GitHub Actions workflow for GitHub Pages deployment
- Configured for static site export
This commit is contained in:
2025-12-15 04:56:15 -08:00
commit 5c7eebd517
126 changed files with 3243 additions and 0 deletions
+55
View File
@@ -0,0 +1,55 @@
name: Deploy to GitHub Pages
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'pnpm'
- name: Setup Pages
uses: actions/configure-pages@v5
- name: Install dependencies
run: pnpm install
- name: Build
run: pnpm build
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: ./out
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
+32
View File
@@ -0,0 +1,32 @@
# Dependencies
node_modules/
.pnpm-store/
# Next.js
.next/
out/
# Build
build/
dist/
# IDE
.idea/
.vscode/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Env
.env
.env.local
.env.*.local
# Logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
+29
View File
@@ -0,0 +1,29 @@
# market
Lux Market NFT marketplace.
## To run locally
Install `pnpm` [like so](https://pnpm.io/installation)
The usual scripts for a Next site, using `pnpm`
```
pnpm install
pnpm dev
```
Since "pnpm" is a finger twister, many people alias it to "pn". For example, with `bash`, put `alias pn='pnpm'` in `.bashrc`.
## Features
- Next.js 14.1
- Radix UI Primitives
- Tailwind CSS
- Icons from [Lucide](https://lucide.dev)
- Dark mode with `next-themes`
- Tailwind CSS class sorting, merging and linting.
## Based on @hanzo/ui
- A React using Next 14.1, Tailwind and Radix
- Pulls content from static json (in `/src/content`)
- Lives [on GitHub here](https://github.com/hanzoai/ui)
+29
View File
@@ -0,0 +1,29 @@
// https://react-svgr.com/docs/next
module.exports = (config) => {
// Grab the existing rule that handles SVG imports
const fileLoaderRule = config.module.rules.find((rule) =>
rule.test?.test?.('.svg'),
)
config.module.rules.push(
// Reapply the existing rule, but only for svg imports ending in ?url
{
...fileLoaderRule,
test: /\.svg$/i,
resourceQuery: /url/, // *.svg?url
},
// Convert all other *.svg imports to React components
{
test: /\.svg$/i,
issuer: fileLoaderRule.issuer,
resourceQuery: { not: [...fileLoaderRule.resourceQuery.not, /url/] }, // exclude if *.svg?url
use: ['@svgr/webpack'],
},
)
// Modify the file loader rule to ignore *.svg, since we have it handled now.
fileLoaderRule.exclude = /\.svg$/i
return config
}
+37
View File
@@ -0,0 +1,37 @@
const watchExternalFilesPlugin = require('webpack-watch-files-plugin').default
const disable = false
const localModules = [
//'../../../../hanzoai/react-sdk/packages/ui',
// '../../../../hanzoai/vaul-fork',
//'../../../../hanzoai/react-sdk/packages/commerce'
]
const suffixes = [
'/**/*.ts',
'/**/*.tsx',
]
const flatArray = localModules.map((p) => {
return suffixes.map((s) => (p + s))
}).flat()
module.exports = ( config ) => {
if (disable) {
return config
}
config.watchOptions = {
poll: 1000,
aggregateTimeout: 300,
}
config.plugins.push(
new watchExternalFilesPlugin({
files: flatArray,
})
)
return config
}
+5
View File
@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
+58
View File
@@ -0,0 +1,58 @@
const withMDX = require('@next/mdx')()
const svgrPluginConfig = require('./next-conf/svgr.next.config')
const watchPluginConfig = require('./next-conf/watch.next.config')
const env = process.env.NODE_ENV
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'export',
images: { unoptimized: true },
pageExtensions: ['js', 'jsx', 'mdx', 'ts', 'tsx'],
reactStrictMode: true,
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'img.youtube.com',
pathname: '**',
},
{
protocol: 'https',
hostname: 'cdn.lux.network',
pathname: '**',
},
{
protocol: "http",
hostname: "localhost",
}
],
},
// https://stackoverflow.com/questions/72621835/how-to-fix-you-may-need-an-appropriate-loader-to-handle-this-file-type-current
transpilePackages: [
'@hanzo/ui',
'@hanzo/auth',
'@hanzo/commerce',
'@luxfi/ui',
'@luxfi/data'
],
productionBrowserSourceMaps: true,
webpack: (config, { dev }) => {
let conf = svgrPluginConfig(config)
if (dev) {
//conf = watchPluginConfig(conf)
//https://github.com/vercel/next.js/discussions/33929
config.snapshot = {
...(config.snapshot ?? {}),
// Add all node_modules but @hanzo module to managedPaths
// Allows for hot refresh of changes to @hanzo module
managedPaths: [/^(.+?[\\/]node_modules[\\/])(?!@hanzo)/],
};
config.cache = false
}
return conf
}
}
module.exports = withMDX(nextConfig)
+54
View File
@@ -0,0 +1,54 @@
{
"name": "@luxfi/market",
"version": "1.0.17",
"private": true,
"license": "BSD-3-Clause",
"author": "Lux Partners Limited <[email protected]> (https://lux.partners)",
"scripts": {
"dev": "next dev",
"build": "next build",
"prod": "next start",
"cn": "rm -rf ./.next",
"clean": "rm -rf ./.next && rm -rf node_modules",
"tc": "tsc --noEmit"
},
"dependencies": {
"@luxfi/ui": "5.5.2",
"@luxfi/data": "5.5.2",
"@hanzo/auth": "5.5.2",
"@hanzo/commerce": "5.5.2",
"@hanzo/ui": "5.5.2",
"@hookform/resolvers": "^3.3.4",
"@next/mdx": "14.1.0",
"@splinetool/react-spline": "^2.2.6",
"@splinetool/runtime": "^1.0.68",
"ethers": "^6.11.1",
"lucide-react": "5.5.2",
"mobx": "^6.12.3",
"mobx-react-lite": "^4.0.7",
"mobx-utils": "^6.0.8",
"next": "5.5.2",
"next-themes": "^0.2.1",
"next-usequerystate": "^1.17.0",
"postcss-selector-parser": "^6.0.13",
"react": "5.5.2",
"react-dom": "5.5.2",
"react-hook-form": "5.5.2",
"supports-color": "5.5.2",
"zod": "3.23.8"
},
"devDependencies": {
"@mdx-js/loader": "^3.0.0",
"@mdx-js/react": "^3.0.0",
"@svgr/webpack": "^8.1.0",
"@types/mdx": "^2.0.9",
"@types/node": "5.5.2",
"@types/react": "5.5.2",
"@types/react-dom": "5.5.2",
"autoprefixer": "^10.4.14",
"postcss": "^8.4.24",
"postcss-import": "^15.1.0",
"tailwindcss": "5.5.2",
"typescript": "5.5.2"
}
}
+9
View File
@@ -0,0 +1,9 @@
// https://tailwindcss.com/docs/using-with-preprocessors#nesting
module.exports = {
plugins: {
'postcss-import': {},
'tailwindcss/nesting': {},
tailwindcss: {},
autoprefixer: {},
},
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 564 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 426 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 831 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,12 @@
import type { ImageDef } from '@hanzo/ui/types'
const ASSETS_PATH = '/assets/img/products/'
export default {
AU_B: { src: ASSETS_PATH + 'gold/gold-bar-pt-800x800.png', dim: {w: 800, h:800 } } satisfies ImageDef,
AG_B: { src: ASSETS_PATH + 'silver/silver-bar-pt-800x800.png', dim: {w: 800, h:800 } } satisfies ImageDef,
AU_C: { src: ASSETS_PATH + 'gold/gold-coin-700x700.png', dim: {w: 700, h:700 } } satisfies ImageDef,
AG_C: { src: ASSETS_PATH + 'silver/silver-coin-700x700.png', dim: {w: 700, h:700 } } satisfies ImageDef,
AU_CB: { src: ASSETS_PATH + 'gold/gold-bar-700x700.png', dim: {w: 700, h:700 } } satisfies ImageDef,
AG_CB: { src: ASSETS_PATH + 'silver/silver-bar-700x700.png', dim: {w: 700, h:700 } } satisfies ImageDef,
}
@@ -0,0 +1,52 @@
# Bullion Data and Data generation
#### The Lux Bullion products are specified [here](https://docs.google.com/document/d/1d-aWwd2ijz8o6UT2MxtFY_RRfR18CjbSgdIKhN0qdV8)
This document species the way SKUs correspond to those products so that
the commerce ui can set up searches by categories.
The Hanzo SKUs are correspond as follows. This allows for
easy implementation of search categories.
### SKU Tokens for Bullion
Category: 'All'
```
First token:
LXB 'Lux Bullion'
```
Category: 'Type'
```
Second token:
AU 'Lux Gold Bullion'
AG 'Lux Silver Bullion'
PD 'Lux Palladium Bullion'
PT 'Lux Platinum Bullion'
```
Category: 'Form'
```
third token:
B 'Minted Bar'
C 'Coin'
CB 'Cast bar'
```
Category: 'Size'
```
forth / last token:
<integer | decimal>_<unit>
eg: 100_OZ '100oz'
eg: 2.5_G = '2.5g'
```
Full examples:
```
LXB-AU-B-50_G 'Lux Gold Bullion, 50g Minted Bar'
LXB-AU-B-2.5_G 'Lux Gold Bullion, 2.5g Minted Bar'
LXB-AU-B-10_OZ 'Lux Gold Bullion, 10oz Minted Bar'
LXB-AG-CB-500_G 'Lux Silver Bullion, 500g Cast Bar'
LXB-PT-C-1_OZ 'Lux Platinum Bullion, 1oz Minted Coin'
```
@@ -0,0 +1,12 @@
export { default as ROOT } from './to-import/root.ts'
// must have last '/'
// For testing: export const OUT_DIR = './'
export const OUT_DIR = '../../packages/core/commerce-util/'
export const OUT_FN = 'AUTO-GEN-bullion-by-family.json'
export const SEP = {
tok: '-',
subTok: '_',
decimal: '.'
}
@@ -0,0 +1,105 @@
import fs from 'fs-extra'
import { type Product, type Family } from '@hanzo/commerce/types'
import { type LeafImportData, type NodeImportData } from './types.ts'
import { OUT_DIR, OUT_FN, ROOT, SEP } from './config.ts'
import IMG from './IMG.ts'
const families: Family[] = []
const amountStrFromItemToken = (t: string): string => {
const tokens = t.split(SEP.subTok)
// Must lower-case it because multibar tokens have an 'X' in them
const amount = (tokens[0].includes(SEP.decimal) ? tokens[0].split(SEP.decimal).join('.') : tokens[0]).toLowerCase()
const unit = tokens[1].toLowerCase() // OZ -> oz
return amount + unit
}
const visitNode = (
levelData: NodeImportData,
titleTokens: string[] = [],
skuTokens: string[] = [],
): void => {
const levelTitle = levelData.titleToken ?? levelData.label
const parentTitle = titleTokens.length > 0 ? titleTokens[titleTokens.length - 1] : undefined
titleTokens.push(levelTitle)
skuTokens.push(levelData.tok)
if (levelData.ch.length > 0 && 'price' in levelData.ch[0]) {
const familyId = skuTokens.join(SEP.tok)
// Since we are at the leaf level,
// these are valid for the entire array.
const bullionForm = titleTokens.pop()
const previousTitle = titleTokens.filter((el) => (el.length > 0)).join(', ')
const getSku = (prodTok: string) => (
[...skuTokens, prodTok].join(SEP.tok)
)
// from LeafImportData to hanzo/cart Product
const hanzoProducts = (levelData.ch as LeafImportData[]).map(
(prod) => ({
id: getSku(prod.tok),
sku: getSku(prod.tok),
// Desired result: "Lux Bullion, Gold, 1oz Minted Bar", ie,
// `<previous title tokens joined>, <amount> <form>`
fullTitle: `${previousTitle}, ${amountStrFromItemToken(prod.tok)} ${bullionForm!}`,
optionLabel: amountStrFromItemToken(prod.tok),
familyTitle: bullionForm!,
familyId: familyId,
desc: prod.desc ? prod.desc : levelData.desc,
price: 0,
// @ts-ignore
img: IMG[prod.imgCode ?? levelData.imgCode]
} satisfies Product)
)
// from NodeImportData to hanzo Family
families.push({
id: skuTokens.join(SEP.tok),
title: levelTitle,
parentTitle,
desc: levelData.desc,
// @ts-ignore
img: IMG[levelData.imgCode],
products: hanzoProducts
} satisfies Family)
}
else if (levelData.ch.length > 0 && 'ch' in levelData.ch[0]) {
const {imgCode: parentImg, desc: parentDesc} = levelData
const subLevels = levelData.ch as NodeImportData[]
subLevels.forEach(({tok, label, imgCode, desc, ch}) => {
visitNode({
tok,
label,
imgCode: imgCode ?? parentImg,
desc: desc ?? parentDesc,
ch,
} satisfies NodeImportData,
// Each branch (ch levelData) must have it's own fresh copies to reduce
[...titleTokens],
[...skuTokens]
)
})
}
}
visitNode(ROOT as unknown as NodeImportData)
console.log(`Writing Family with their Products to ${OUT_DIR + OUT_FN}...`)
fs.writeJsonSync(
OUT_DIR + OUT_FN,
families,
{ spaces: 2 }
)
console.log('done')
@@ -0,0 +1,25 @@
{
"name": "market-data-generator",
"version": "1.0.0",
"description": "",
"main": "main.ts",
"type": "module",
"scripts": {
"COMMENT": "ts-node sucks! Just say NO!",
"tc": "tsc --noEmit",
"go": "vite-node main.ts"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@hanzo/commerce": "catalog:",
"@hanzo/ui": "catalog:",
"fs-extra": "^11.2.0",
"typescript": "catalog:",
"vite-node": "^1.4.0"
},
"peerDependencies": {
"tailwindcss" : "catalog:"
}
}
@@ -0,0 +1,25 @@
export default [
{
tok: '1_OZ',
price: 0
},
/*
{
tok: '1_G',
price: 0
},
{
tok: '10_G',
price: 0
},
{
tok: '100_G',
price: 0
},
{
tok: '1_KG',
price: 0
},
*/
]
@@ -0,0 +1,14 @@
export default [
{
tok: '500_G',
price: 0
},
{
tok: '1_KG',
price: 0
},
{
tok: '1000_OZ',
price: 0
},
]
@@ -0,0 +1,22 @@
export default [
{
tok: '1_OZ',
price: 0
},
{
tok: '1_G',
price: 0
},
{
tok: '10_G',
price: 0
},
{
tok: '100_G',
price: 0
},
{
tok: '400_OZ',
price: 0
}
]
@@ -0,0 +1,14 @@
export default [
{
tok: '500_G',
price: 0
},
{
tok: '1_KG',
price: 0
},
{
tok: '400_OZ',
price: 0
},
]
@@ -0,0 +1,76 @@
import AU_B from './AU-B.ts'
import AG_B from './AG-B.ts'
import AU_CB from './AU-CB.ts'
import AG_CB from './AG-CB.ts'
export default {
tok: 'LXM',
label: 'Root',
titleToken: '',
ch: [
/*
{
tok: 'AU',
label: 'Lux Gold',
desc: "Forge your connection to the real world with Lux Gold, crafted with excellence in Dubai.",
ch: [
{
tok: 'B',
label: 'Minted Bar',
imgCode: 'AU_B',
ch: AU_B
},
{
tok: 'C',
label: 'Minted Coin',
imgCode: 'AU_C',
ch: [
{
tok: '1_OZ',
price: 0,
}
]
},
{
tok: 'CB',
label: 'Cast Bar',
imgCode: 'AU_CB',
ch: AU_CB
}
]
},
*/
{
tok: 'AG',
desc: 'Get unprecedented access to silver with 1:1 asset-backed Lux Silver NFTs, sovereign ownership of physical silver without management fees, and mine-direct discount pricing.',
label: 'Lux Silver',
ch: [
{
tok: 'B',
label: 'Minted Bar',
imgCode: 'AG_B',
ch: AG_B
},
/*
{
tok: 'C',
label: 'Minted Coin',
imgCode: 'AG_C',
ch: [
{
tok: '1_OZ',
price: 0,
}
]
},
{
tok: 'CB',
label: 'Cast Bar',
imgCode: 'AG_CB',
ch: AG_CB
}
*/
]
}
],
}
@@ -0,0 +1,14 @@
{
"extends": "../../tsconfig.apps.base.json",
"include": [
"**/*.ts",
],
"exclude": [
"node_modules"
],
"compilerOptions": {
"allowImportingTsExtensions":true,
"noEmit": true,
"jsx": "react-jsx",
}
}
@@ -0,0 +1,19 @@
interface LeafImportData {
tok: string
imgCode?: string
desc?: string
}
interface NodeImportData {
tok: string
titleToken?: string
label: string
imgCode?: string
desc?: string
ch: NodeImportData[] | LeafImportData[]
}
export {
type LeafImportData,
type NodeImportData
}
+37
View File
@@ -0,0 +1,37 @@
import React from 'react'
import { Desktop, TouchDevice } from './scroll-snap'
import { Header } from '@luxfi/ui'
type PageProps = {
params: { slug: string };
searchParams?: { [key: string]: string | string[] | undefined };
}
import siteDef from '@/site-def'
const Page: React.FC<PageProps> = ({
searchParams
}) => {
// see src/middleware.ts
const agent = searchParams?.agent
if (agent === 'desktop') {
return (<>
<Header siteDef={siteDef}/>
<Desktop />
</>)
}
else if (agent === 'tablet') {
return (<>
<Header siteDef={siteDef}/>
<TouchDevice isTablet={true} />
</>)
}
return (<>
<Header siteDef={siteDef}/>
<TouchDevice isTablet={false} />
</>)
}
export default Page
+99
View File
@@ -0,0 +1,99 @@
import React from 'react'
import { ApplyTypography } from '@hanzo/ui/primitives'
import { DrawerMargin, Footer } from '@luxfi/ui'
import BannerComponent from '@/blocks/components/banner-block'
import { landing } from '@/content'
import siteDef from '@/site-def'
const MOBILE_VID_CONSTRAINT = {
w: 360,
h: 400
}
// using this approach:
// https://stackoverflow.com/a/76066443/11645689
// @hanzo/ui/style/global.css
const swipeOuter = 'snap-start snap-always h-[100vh] '
// 44+16=60, 80+8=88
const swipeInner = 'pt-[60px] md:pt-[88px] pb-[24px] '
const swipeInnerDesk = swipeInner + 'h-full '
const swipeInnerTouch = swipeInner + 'h-[100svh] '
const Desktop: React.FC = () => (<>
{landing.banners.map((banner, index) => (
<div key={index} className={swipeOuter + 'px-4 lg:px-6 flex flex-col' }>
<ApplyTypography
asTag='section'
className={
swipeInnerDesk +
// must use justify-between to trigger the 'grow' of the video (below)
'w-full h-full flex flex-col items-center justify-between self-stretch grow '
}
>
<BannerComponent
block={banner}
agent='desktop'
grouping='separate'
groupClx={{
title: 'flex-none',
media: 'grow',
cta: 'flex-none xs:w-full md:w-auto md:min-w-[500px]'
}}
/>
</ApplyTypography>
<DrawerMargin clx='grow-0 shrink-0'/>
</div>
))}
<div key='last' className={swipeOuter + 'swipe flex flex-col justify-start items-stretch'} >
<Footer siteDef={siteDef} className='grow-0 w-full lg:mx-auto sm:pt-6 border-t-0 flex flex-col justify-between md:justify-start'/>
<DrawerMargin clx='grow-0 shrink-0'/>
</div>
</>)
const TouchDevice: React.FC<{
isTablet: boolean
}> = ({
isTablet,
}) => {
return (<>
{landing.banners.map((banner, index) => (
<div key={index} className={swipeOuter + 'flex flex-col px-4 xs:px-2'}>
<ApplyTypography
asTag='section'
className={'w-full flex flex-col items-stretch grow ' + swipeInnerTouch +
(isTablet ? 'gap-2 justify-around lg:justify-start lg:gap-24 '
:
'justify-between pb-4')
}
>
<BannerComponent
block={banner}
agent={isTablet ? 'tablet' : 'phone'}
groupClx={{
title: 'md:pt-12 lg:pt-32',
cta: 'md:mt-32 md:w-pr-70 lg:w-pr-60 md:self-center'
}}
videoConstraint={isTablet ? undefined : MOBILE_VID_CONSTRAINT}
videoSize={isTablet ? 'lg' : 'md'}
/>
</ApplyTypography>
<DrawerMargin clx='grow-0 shrink-0'/>
</div>
))}
<div key='lastest' className={swipeOuter + 'flex flex-col'}>
<Footer siteDef={siteDef} className={swipeInnerTouch + 'pl-[32px] pr-[24px] flex flex-col justify-between border-none grow'} noHorizPadding/>
<DrawerMargin clx='grow-0 shrink-0'/>
</div>
</>)
}
export {
Desktop,
TouchDevice
}
+60
View File
@@ -0,0 +1,60 @@
import React from 'react'
import { notFound } from 'next/navigation'
import { Footer, Header, Main } from '@luxfi/ui'
import type ProductDetailBlock from '@/blocks/def/product-detail-block'
import ProductDetailBlockComponent from '@/blocks/components/product-detail-block'
import { products } from '@/content'
import siteDef from '../../site-def'
type Props = {
params: { slug: 'silver' | 'gold' | 'coin' | 'credit' | 'validator' | 'key' | 'uranium' }
searchParams?: { [key: string]: string | string[] | undefined }
}
export async function generateStaticParams() {
const products = [
'silver',
'gold',
'coin',
'credit',
'validator',
'key',
'uranium',
]
return products.map((p) => ({
slug: p,
}))
}
export async function generateMetadata({ params}: Props) {
const title = params.slug
const capitalized = title.charAt(0).toUpperCase() + title.slice(1)
return { title: capitalized }
}
const ProductPage = ({ params, searchParams }: Props) => {
const product = products[params.slug] as ProductDetailBlock
if (!product) {
notFound()
}
// see src/middleware.ts
const agent = searchParams?.agent
return (<>
<Header siteDef={siteDef}/>
<Main className='md:flex-row md:gap-4'>
<ProductDetailBlockComponent block={product} agent={agent as string}/>
</Main>
<div className='border-t'></div>
<Footer siteDef={siteDef} className='w-full pt-16 lg:mx-auto ' />
</>)
}
export default ProductPage
@@ -0,0 +1,17 @@
import { cookies } from 'next/headers'
import { NextRequest, NextResponse } from 'next/server'
// Create a 1x1 transparent GIF
const transparentGIF = Buffer.from('R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7', 'base64')
export async function GET(request: NextRequest) {
// Return the GIF as the response
const response = new NextResponse(transparentGIF)
response.headers.set('Content-Type', 'image/gif')
response.headers.set('Content-Length', transparentGIF.length.toString())
response.headers.set('Cache-Control', 'no-store, max-age=0')
cookies().delete('auth-token')
return response
}
+7
View File
@@ -0,0 +1,7 @@
import { NextRequest } from 'next/server'
import { handleLoginApiRequest } from '@hanzo/auth/server'
export async function POST(request: NextRequest) {
return handleLoginApiRequest(request)
}
+5
View File
@@ -0,0 +1,5 @@
import { handleLogoutApiRequest } from '@hanzo/auth/server'
export async function GET() {
return handleLogoutApiRequest()
}
+28
View File
@@ -0,0 +1,28 @@
import { cookies } from 'next/headers'
import { NextRequest, NextResponse } from 'next/server'
// Create a 1x1 transparent GIF
const transparentGIF = Buffer.from('R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7', 'base64')
export async function GET(request: NextRequest) {
const token = request.nextUrl.searchParams.get('token')
// Return the GIF as the response
const response = new NextResponse(transparentGIF)
response.headers.set('Content-Type', 'image/gif')
response.headers.set('Content-Length', transparentGIF.length.toString())
response.headers.set('Cache-Control', 'no-store, max-age=0')
// Add the auth token to the response cookies
if (!!token) {
cookies().set({
name: 'auth-token',
value: token,
httpOnly: false,
path: '/',
expires: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30 days
})
}
return response
}
+17
View File
@@ -0,0 +1,17 @@
import React from 'react'
import { cn } from '@hanzo/ui/util'
import { CheckoutPanel, Main } from '@luxfi/ui'
const Page: React.FC = () => (
<Main /* id='CHECKOUT_MAIN' */ headerSpace={false} standardWidth={false} className={cn(
'!px-0 !py-0',
'w-full h-[100vh]',
'animate-in md:zoom-in-90',
'shadow-lg bg-background'
)}>
<CheckoutPanel clx='w-full h-full' />
</Main>
)
export default Page
+39
View File
@@ -0,0 +1,39 @@
'use client'
import React, { useEffect } from 'react'
import { observer } from 'mobx-react-lite'
import { cn } from '@hanzo/ui/util'
import { AddToCartWidget, useCommerce } from '@hanzo/commerce'
const AddPanel: React.FC<{
clx?: string
}> = observer(({
clx='',
}) => {
const cmmc = useCommerce()
useEffect(() => {
cmmc.setCurrentItem('LXM-CR-E-24G')
}, [])
return (
<div className={cn('flex flex-col gap-10 items-center mt-10', clx)}>
<p>Current Sku: {cmmc.currentItem ? cmmc.currentItem.sku : 'none'}</p>
{cmmc.currentItem && (
<AddToCartWidget item={cmmc.currentItem} className='w-[260px]'/>
)}
</div>
)
})
/*
{cmmc.currentItem ? (
<MediaStack media={cmmc.currentItem} constrainTo={{w: 250, h: 250}} />
) : (
<Skeleton className='h-[250px]' />
)}
*/
export default AddPanel
+122
View File
@@ -0,0 +1,122 @@
'use client'
import React, { useState } from 'react'
import { useForm } from 'react-hook-form'
import { z } from 'zod'
import { zodResolver } from '@hookform/resolvers/zod'
import {
Button,
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
Input,
ScrollArea,
} from '@hanzo/ui/primitives'
import { useCommerce } from '@hanzo/commerce'
import { peekDump } from '@hanzo/commerce/debug'
import { BuyButton } from '@luxfi/ui'
const INITIAL_SKU_PATH = 'LXM-CR'
const FormSchema = z.object({
skupath: z.custom<string>((v) => (
(typeof v === 'string' && v.split('-').length >= 2),
"Sku Path must have at least two levels."
))
})
const InputForm: React.FC<{
onSubmit: (data: z.infer<typeof FormSchema>) => void
onClear: (() => void) | undefined
className?: string
}> = ({
onSubmit,
onClear,
className
}) => {
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: {
skupath: '',
},
})
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className={className}>
<FormField
control={form.control}
name='skupath'
render={({ field }) => (
<FormItem>
<FormLabel>sku path</FormLabel>
<FormControl>
<Input placeholder='eg, LXM-CR' {...field} className='border placeholder:text-muted-3' />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className={'grid gap-1 w-full min-w-0 ' + (onClear ? 'grid-cols-2': 'grid-cols-1')}>
<Button className={(!!onClear ? 'shrink' : '')} type='submit'>Submit</Button>
{onClear && <Button className={(!!onClear ? 'shrink' : '')} onClick={() => {form.reset(); onClear()}}>Clear</Button>}
</div>
</form>
</Form>
)
}
const BuyTab: React.FC = () => {
const cmmc = useCommerce()
const [skuPath, setSkuPath] = useState<string | undefined>(INITIAL_SKU_PATH)
const [json, setJSON] = useState<string | undefined>(undefined)
const [error, setError] = useState<string | undefined>(undefined)
const handleSubmit = (data: z.infer<typeof FormSchema>): void => {
const _skuPath = data.skupath.toUpperCase()
setSkuPath(_skuPath)
const result = cmmc.peek(_skuPath)
if (typeof result === 'string') {
setError(result)
setJSON(undefined)
}
else {
setJSON(peekDump(result))
setError(undefined)
}
}
const handleClear = () => {
setSkuPath(undefined)
setJSON(undefined)
}
return (
<div className='w-full md:w-[400px] md:mx-auto flex flex-col items-center gap-8'>
<InputForm
onSubmit={handleSubmit}
onClear={skuPath ? handleClear : undefined}
className='flex flex-col items-stretch w-full md:w-[250px] md:mx-auto'
/>
<div className='w-full'>
<p className='text-muted text-center'>peek</p>
<ScrollArea className='border rounded h-[300px] w-full p-4 text-muted flex flex-col gap-2'>
{skuPath && <pre>PATH: {skuPath}</pre>}
{error && <p className='text-destructive'>{error}</p>}
{json && <pre>{json}</pre> }
</ScrollArea>
</div>
{skuPath && <BuyButton skuPath={skuPath} className='' >Buy</BuyButton>}
</div>
)
}
export default BuyTab
+43
View File
@@ -0,0 +1,43 @@
'use client'
import React from 'react'
import {
Tabs,
TabsList,
TabsTrigger,
TabsContent
} from '@hanzo/ui/primitives'
import { Header, Main } from '@luxfi/ui'
import siteDef from '@/site-def'
import AddPanel from './_page/add-panel'
import BuyPanel from './_page/buy-panel'
type Props = {
searchParams?: { [key: string]: string | string[] | undefined }
}
const Page = ({ searchParams }: Props ) => {
return (<>
<Header siteDef={siteDef}/>
<Main>
<Tabs defaultValue='buy' className='w-full sm:w-[500px]'>
<TabsList className='grid w-full grid-cols-2'>
<TabsTrigger value='buy'>Buy</TabsTrigger>
<TabsTrigger value='add'>Add</TabsTrigger>
</TabsList>
<TabsContent value='buy'>
<BuyPanel />
</TabsContent>
<TabsContent value='add'>
<AddPanel clx=''/>
</TabsContent>
</Tabs>
</Main>
</>)
}
export default Page
+22
View File
@@ -0,0 +1,22 @@
import React, { type PropsWithChildren } from 'react'
import {
RootLayout as RootLayoutCore,
viewport as ViewportCode,
} from '@luxfi/ui/root-layout'
import siteDef from '../site-def'
import _metadata from '../metadata'
export const metadata = { ..._metadata }
export const viewport = { ...ViewportCode}
const RootLayout: React.FC<PropsWithChildren> = async ({
children
}) => (
<RootLayoutCore siteDef={siteDef} >
{children}
</RootLayoutCore>
)
export default RootLayout
+19
View File
@@ -0,0 +1,19 @@
import Link from "next/link"
export default function NotFoundComponent() {
return (
<div className="flex min-h-[100dvh] flex-col items-center justify-center bg-black text-white font-inter">
<div className="mx-auto max-w-md text-center">
<h1 className="text-[96px] font-bold">404</h1>
<p className="mt-4 text-2xl">Oops, page not found</p>
<Link
href="/"
className="mt-6 inline-flex items-center rounded-md bg-white px-4 py-2 text-sm font-medium text-black shadow-sm transition-colors hover:bg-gray focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2"
prefetch={false}
>
Go to Homepage
</Link>
</div>
</div>
)
}
+126
View File
@@ -0,0 +1,126 @@
'use client' // Put spline back into a client component, make this ssr comp
import React from 'react'
import type { TShirtSize } from '@hanzo/ui/types'
import { cn } from '@hanzo/ui/util'
import { LinkElement, buttonVariants } from '@hanzo/ui/primitives'
import { VideoBlockComponent, type BlockComponentProps } from '@hanzo/ui/blocks'
import { BuyButton } from '@luxfi/ui'
import Spline from '@splinetool/react-spline'
import type BannerBlock from '@/blocks/def/banner-block'
type BannerGrouping = 'separate' | 'titleAndMedia-cta'
interface GroupClx {
title?: string // for media and title if grouping === 'titleAndMedia-cta'
media?: string
cta?: string
}
const BannerBlockComponent: React.FC<BlockComponentProps & {
videoSize?: TShirtSize
videoConstraint?: {w: number, h: number}
grouping?: BannerGrouping
groupClx?: GroupClx
}> = ({
block,
agent,
grouping = 'titleAndMedia-cta',
groupClx={},
videoSize='md',
videoConstraint
}) => {
if (block.blockType !== 'banner') {
return <>banner block required</>
}
const banner = block as BannerBlock
const specifies = (s: string) => (banner.specifiers?.includes(s))
const CTAs: React.FC<{className?: string}> = ({
className=''
}) => ((banner.skuPath || banner.learnLink) ? (
<div className={cn(
(banner.skuPath && banner.learnLink) ? 'grid grid-cols-2 gap-2' : 'flex flex-row',
'sm:flex sm:flex-row justify-center items-center sm:gap-4 lg:gap-6',
className
)}>
{banner.learnLink && (
<LinkElement
def={{
href: banner.learnLink.href,
title: banner.learnLink.title ? banner.learnLink.title : 'Learn More',
newTab: banner.learnLink.newTab
}}
className={cn(
buttonVariants({
variant: banner.skuPath ? 'outline' : 'primary',
size: 'default',
rounded: 'md'
}),
'lg:min-w-[220px] sm:min-w-[220px]'
)}
/>
)}
{banner.skuPath && (
<BuyButton
skuPath={banner.skuPath}
size='default'
className={'lg:min-w-[220px] sm:min-w-[220px] ' + (banner.learnLink ? '' : '!min-w-[320px]')}
>
Buy
</BuyButton>
)}
</div>
) : null)
return (grouping === 'separate') ?
(<>
<div className={'text-center ' + groupClx.title ?? ''}>
{banner.bylineBefore && (<h5 className='text-center'>{banner.bylineBefore}</h5>)}
<h1>{banner.title}</h1>
{banner.byline && (<h5 className='text-center'>{banner.byline}</h5>)}
</div>
<div className={'self-center w-full max-w-[40rem] flex flex-col justify-center items-center text-center ' + groupClx.media ?? ''}>
{ banner.video ? (
<VideoBlockComponent
className='self-center not-typography'
block={{blockType: 'video', ...banner.video}}
agent={agent}
size={videoSize}
constrainTo={videoConstraint}
/>
) :
banner.animation ? (
<Spline scene={banner.animation} className='!w-full !h-auto pointer-events-none !aspect-[12/10]'/>
) : null}
</div>
<CTAs className={groupClx.cta ?? ''}/>
</>) : /* grouping === 'titleAndMedia-cta' */ (<>
<div className={'self-center flex flex-col justify-start items-center text-center ' + groupClx.title ?? ''} >
{banner.bylineBefore && (<h5 className='text-center'>{banner.bylineBefore}</h5>)}
<h1>{banner.title}</h1>
{banner.byline && (<h5 className='text-center'>{banner.byline}</h5>)}
{banner.video ? (
<VideoBlockComponent
className='self-center not-typography'
block={{blockType: 'video', ...banner.video}}
agent={agent}
size={videoSize}
constrainTo={videoConstraint}
/>
) :
banner.animation ? (
<Spline scene={banner.animation} className='!w-full !h-auto pointer-events-none !aspect-[12/10]'/>
) : null}
</div>
<CTAs className={groupClx.cta ?? ''}/>
</>)
}
export {
BannerBlockComponent as default,
type BannerGrouping as AssetBannderGroupingType
}
@@ -0,0 +1,90 @@
'use client' // Put spline back into a client component, make this ssr comp
import React from 'react'
import { ApplyTypography } from '@hanzo/ui/primitives'
import { cn } from '@hanzo/ui/util'
import {
type BlockComponentProps,
ContentComponent,
SpaceBlockComponent,
CardBlockComponent as CardComponent,
AccordianBlockComponent,
VideoBlockComponent
} from '@hanzo/ui/blocks'
import type ProductDetailBlock from '@/blocks/def/product-detail-block'
import Spline from '@splinetool/react-spline'
const Spacer: React.FC = () => (
<SpaceBlockComponent block={{blockType: 'space'}} />
)
const ProductDetailBlockComponent: React.FC<BlockComponentProps> = ({
agent,
block,
}) => {
if (block.blockType !== 'product-detail') {
return <>product detail block required</>
}
const p = block as ProductDetailBlock
const videoSize = (agent === 'phone') ? 'md' : 'lg'
const TitleArea: React.FC<{className?: string}> = ({
className=''
}) => (
<ApplyTypography className={cn(
'typography-headings:text-left typography-h2:md:text-3xl',
'typography-h2:lg:text-4xl',
className
)}>
<h2 className='text-left'>{p.title}</h2>
{p.desc && (typeof p.desc === 'string') ? (
<h6>{p.desc}</h6>
) : (
p.desc
)
}
</ApplyTypography>
)
return (
<div className='grid grid-cols-1 md:grid-cols-2 w-full'>
<div className='mb-6 md:mb-12 w-full'>
{p.video ? (
<VideoBlockComponent block={p.video} agent={agent} size={videoSize} className='md:sticky md:top-[80px] md:mt-0 mt-[16px] mx-auto'/>
) :
p.animation ? (
<Spline scene={p.animation} className='!w-full !h-auto pointer-events-none !aspect-square'/>
) : null}
</div>
<div className='md:bg-scroll w-full'>
<div className='md:max-w-[555px] flex flex-col items-start gap-4' >
<TitleArea className='flex flex-col justify-start items-start ' />
{p.price && (<>
{p.price.heading && (
<ApplyTypography >
<h3>{p.price.heading}</h3>
</ApplyTypography>
)}
<div className='flex flex-col justify-start items-stretch self-stretch w-full sm:self-center sm:grid sm:grid-cols-2 gap-4 '>
<CardComponent block={p.price.priceCard} agent={agent} />
<CardComponent block={p.price.msCard} agent={agent} />
</div>
</>)}
<AccordianBlockComponent block={p.accordian} agent={agent} className='mt-5'/>
<Spacer />
<ContentComponent blocks={p.blocks} agent={agent}/>
<Spacer />
</div>
</div>
</div>
)
}
export default ProductDetailBlockComponent
+18
View File
@@ -0,0 +1,18 @@
import React from 'react'
import type { Block } from '@hanzo/ui/blocks'
import type { LinkDef, MediaStackDef } from '@hanzo/ui/types'
interface BannerBlock extends Block, MediaStackDef {
blockType: 'banner'
specifiers?: string
bylineBefore?: React.ReactNode
title: string
byline?: React.ReactNode
learnLink?: LinkDef
skuPath?: string
}
export {
type BannerBlock as default
}
+27
View File
@@ -0,0 +1,27 @@
import type { ReactNode } from 'react'
import type {
Block,
VideoBlock,
AccordianBlock,
CardBlock
} from '@hanzo/ui/blocks'
interface ProductDetailBlock extends Block {
blockType: 'product-detail'
title: string
desc?: ReactNode
video?: VideoBlock
animation?: string
accordian: AccordianBlock
price?: {
heading?: string
priceCard: CardBlock
msCard: CardBlock
},
blocks?: Block[]
}
export {
type ProductDetailBlock as default
}
+30
View File
@@ -0,0 +1,30 @@
'use client'
import React from 'react'
import { useRouter } from 'next/navigation'
import { Button } from '@hanzo/ui/primitives'
import { cn } from '@hanzo/ui/util'
import { BuyButton } from '@luxfi/ui'
const ProductDetailCTA: React.FC<{
readMoreUrl: string,
skuPath: string,
className?: string
}> = ({
readMoreUrl,
skuPath,
className
}) => {
const router = useRouter()
return (
<div className={cn('w-full flex justify-center items-center gap-4', className)}>
<Button onClick={() => router.push(readMoreUrl)} variant='outline' className='w-40'>Read more</Button>
<BuyButton skuPath={skuPath} className='w-40'>Buy</BuyButton>
</div>
)
}
export default ProductDetailCTA
+182
View File
@@ -0,0 +1,182 @@
import { type CTABlock} from '@hanzo/ui/blocks'
import { Icons } from '@luxfi/ui'
const SOC_ICON_SIZE = 18
export default [
{
blockType: 'cta',
specifiers: 'nav',
elements: [
{
title: 'Market',
href: "https://app.lux.market/",
external: true,
newTab: false,
variant: 'linkFG'
},
{
title: 'Lux Silver',
href: '/silver',
},
{
title: 'Lux Gold',
href: '/gold',
},
{
title: 'Lux Coin',
href: '/coin',
},
/*{
title: 'Lux Uranium',
href: '/uranium',
}*/,
{
title: 'Lux Credit',
href: '/credit',
},
{
title: 'Lux Validator',
href: '/validator',
},
{
title: 'Lux Key',
href: '/key',
},
]
},
{
blockType: 'cta',
elements: [
{
title: 'Network',
href: "https://lux.network/",
variant: 'linkFG'
},
{
title: 'Apply for Beta',
href: 'https://apply.lux.network/',
external: true,
newTab: false
},
{
title: 'Whitepapers',
href: 'https://docs.lux.network/intro/about',
},
{
title: 'Github',
href: 'https://github.com/luxfi',
},
]
},
{
blockType: 'cta',
elements: [
{
title: 'Partners',
href: "https://lux.partners/",
variant: 'linkFG',
external: true
},
{
title: 'About',
href: 'https://lux.partners',
external: true,
newTab: false,
},
{
title: 'Partner with Lux',
href: 'https://apply.lux.partners/',
external: true
},
{
title: 'Lux Fund',
href: 'https://lux.fund',
newTab: false,
external: true
},
{
title: 'Lux Support',
href: 'mailto:[email protected]?subject=%E2%96%BC%20Lux%20Support',
external: true
},
{
title: 'Careers',
href: 'https://docs.lux.network/about/v/lux-job-listings/',
newTab: false,
external: true
},
{
title: 'Press Kit',
href: 'https://drive.google.com/drive/folders/14OJtKLVakGY6883XO9yGbiHtlFxQUUm5?usp=share_link',
external: true
},
]
},
{
blockType: 'cta',
elements: [
{
title: 'Community',
variant: 'linkFG',
},
{
title: 'Discord',
href: 'https://discord.gg/luxdefi',
external: true,
icon: <Icons.SocialIcon network='discord' size={SOC_ICON_SIZE} />
},
{
title: 'Telegram',
href: 'https://t.me/luxdefi',
external: true,
icon: <Icons.SocialIcon network='telegram' size={SOC_ICON_SIZE} />
},
{
title: 'X (Twitter)',
href: 'https://twitter.com/luxdefi',
external: true,
icon: <Icons.SocialIcon network='x' size={SOC_ICON_SIZE} />
},
{
title: 'Facebook',
href: 'https://facebook.com/luxdefi',
external: true,
icon: <Icons.SocialIcon network='facebook' size={SOC_ICON_SIZE + 2} />
},
{
title: 'LinkedIn',
href: 'https://linkedin.com/company/luxdefi',
external: true,
icon: <Icons.SocialIcon network='linkedin' size={SOC_ICON_SIZE + 2} />
},
{
title: 'Instagram',
href: 'https://instagram.com/luxdefi',
external: true,
icon: <Icons.SocialIcon network='instagram' size={SOC_ICON_SIZE + 2} />
},
]
},
{
blockType: 'cta',
elements: [
{
title: 'Legal',
variant: 'linkFG',
},
{
title: 'Terms and Conditions',
href: '/assets/pdf/LUX-NFT-Terms-and-Conditions.pdf',
newTab: true,
},
{
title: 'Privacy Policy',
href: '/assets/pdf/LUX-Privacy-Policy.pdf',
newTab: true,
},
]
},
] as CTABlock[]
+25
View File
@@ -0,0 +1,25 @@
import landing from './landing'
import footer from './footer'
import silver from './products/silver'
import gold from './products/gold'
import coin from './products/coin'
import credit from './products/credit'
import validator from './products/validator'
import key from './products/key'
import uranium from './products/uranium'
const products = {
silver,
gold,
coin,
credit,
validator,
key,
uranium,
}
export {
landing,
footer,
products
}
+26
View File
@@ -0,0 +1,26 @@
import type BannerBlock from '@/blocks/def/banner-block'
import silver from '../products/silver/banner'
// import gold from '../products/gold/banner'
import coin from '../products/coin/banner'
import validator from '../products/validator/banner'
import credit from '../products/credit/banner'
import key from '../products/key/banner'
// import uranium from '../products/uranium/banner'
import last from './last'
export default {
banners: [
silver,
//gold,
credit,
validator,
coin,
// uranium,
key,
last
] satisfies BannerBlock[],
}
+35
View File
@@ -0,0 +1,35 @@
import React from 'react'
import type { ElementBlock, Block } from '@hanzo/ui/blocks'
import { BuyButton } from '@luxfi/ui'
import type BannerBlock from '@/blocks/def/banner-block'
import silver from '../products/silver/banner'
import gold from '../products/gold/banner'
import coin from '../products/coin/banner'
import validator from '../products/validator/banner'
import credit from '../products/credit/banner'
import key from '../products/key/banner'
// import uranium from '../products/uranium/banner'
import last from './last'
//import BottomMDX from './landing-bottom.mdx'
export default {
banners: [
silver,
//gold,
credit,
validator,
coin,
// uranium,
key,
last
] satisfies BannerBlock[],
}
+2
View File
@@ -0,0 +1,2 @@
#### Join the global rush to own silver
## THE SILVER RUSH IS HERE
+8
View File
@@ -0,0 +1,8 @@
import type BannerBlock from '@/blocks/def/banner-block'
export default {
blockType: 'banner',
title: 'THE SILVER RUSH IS HERE',
bylineBefore: 'Join the global rush to own silver!',
skuPath: 'LXM-AG'
} satisfies BannerBlock
@@ -0,0 +1,23 @@
import LuxCoinTokenomicsMDX from './lux-coin-tokenomics.mdx'
export default {
blockType: 'accordian',
items: [
{
trigger: 'Get LUX pre-launch',
content: 'Buy the currency that powers all network transactions as a genesis wallet, pre-loaded with a set amount of LUX Coin.'
},
{
trigger: 'Stake and earn',
content: 'Stake LUX Coin in your Lux Wallet to earn an industry leading APY for lending and network liquidity.'
},
{
trigger: 'Automatic DAO membership',
content: 'Purchase a genesis LUX Coin wallet and become a member of the Lux Network decentralized governance organization.'
},
{
trigger: 'LUX Coin tokenomics',
content: <LuxCoinTokenomicsMDX />
},
]
}
@@ -0,0 +1,3 @@
* **Deflationary**: LUX Coin is deflationary by burning or deleting 50% of all transaction fees to ensure enduring currency value.
* **Supply-locked**: LUX Coin is scarce whereby no more LUX can ever be minted or created beyond the 200 billion genesis coins already minted.
* **LUX Coin distribution**:<br/>Total Supply: 200 billion<br/>DAO: 100 billion<br/>Public Sale: 80 billion<br/>Lux Team: 10 billion<br/>Lux Treasury: 10 billion
+11
View File
@@ -0,0 +1,11 @@
import type BannerBlock from '@/blocks/def/banner-block'
import video from './video'
export default {
blockType: 'banner',
title: 'LUX COIN',
byline: <>Genesis Collection now available <br className='sm:hidden' /> on Open Sea.</>,
video: {...video, sizing: { vh: 50}},
learnLink: { href: "/coin" },
skuPath: 'LXM-CN'
} satisfies BannerBlock
+19
View File
@@ -0,0 +1,19 @@
import type * as C from '@hanzo/ui/blocks'
import type ProductDetailBlock from '@/blocks/def/product-detail-block'
import video from './video'
import accordian from './accordian'
import Link from 'next/link'
import ProductDetailCTA from '@/components/product-detail-cta'
export default {
blockType: 'product-detail',
title: 'LUX COIN',
desc: <p>One-time opportunity to get LUX Coin, the deflationary and supply-locked <Link href="http://lux.network">Lux Network</Link> currency at pre-launch prices.</p>,
video: {...video, sizing: { vh: 70}},
accordian,
blocks: [{blockType: 'element',
element: <ProductDetailCTA readMoreUrl='https://docs.lux.network' skuPath='LXM-CN' />
} satisfies C.ElementBlock as C.Block]
} as ProductDetailBlock
+28
View File
@@ -0,0 +1,28 @@
import type { VideoBlock } from '@hanzo/ui/blocks'
import { DEF_VIDEO_PROPS } from '@luxfi/data'
export default {
blockType: 'video',
videoProps: DEF_VIDEO_PROPS,
poster: 'https://cdn.lux.network/commerce/cn/product/LUX-COIN-poster.jpg',
sources: [
'https://cdn.lux.network/commerce/cn/product/LUX-COIN-transcode.mp4',
'https://cdn.lux.network/commerce/cn/product/LUX-COIN-transcode.webm'
],
// Determin aspect ration from dims manually...
// https://stackoverflow.com/questions/684015/how-can-i-get-the-resolution-width-and-height-for-a-video-file-from-a-linux-co
dim: {
// dims are 546x540, let's cut in half
// ratio: 1.01
md: {
w: 273,
h: 270
},
lg: {
w: 415,
h: 410
}
}
} as VideoBlock
@@ -0,0 +1,15 @@
import type { ButtonModalDef } from '@hanzo/ui/types'
import { ContactDialog } from '@luxfi/ui'
import saveToWaitlist from '@luxfi/ui/server-actions'
export default {
Comp: ContactDialog,
title: 'Join the Waitlist',
byline: 'Be the first to own LUX Coin',
action: saveToWaitlist,
actionEnclosure: {
dbId: 'lux-waitlist',
listId: 'coin',
reply: "You're on the LUX Coin Waitlist."
}
} satisfies ButtonModalDef
+25
View File
@@ -0,0 +1,25 @@
export default {
blockType: 'accordian',
items: [
{
trigger: 'Direct Ownership of Digital Assets',
content: 'Maintain sovereign ownership of your digital assets with the security and transparency of blockchain technology.',
},
{
trigger: 'Easy Asset Management',
content: 'Manage, trade, and convert your digital assets effortlessly with the intuitive Lux Card interface.',
},
{
trigger: 'Lux Standard Guaranteed Backing',
content: 'Each transaction and asset held on the Lux Card is backed by the Lux Standard, ensuring a trustworthy and reliable experience.',
},
{
trigger: 'Enhanced Security',
content: 'Experience peace of mind with advanced security features ensuring the safety of your assets.',
},
{
trigger: 'DAO membership included',
content: 'Automatically become a member of the Lux Network decentralized governance organization.'
},
]
}
+12
View File
@@ -0,0 +1,12 @@
import type BannerBlock from '@/blocks/def/banner-block'
import type { LinkDef } from '@hanzo/ui/types'
import video from './video'
export default {
blockType: 'banner',
title: 'LUX CREDIT',
byline: 'Never sell your crypto again.',
video: {...video, sizing: {vh:50}},
learnLink: { href: "https://lux.credit", newTab: false } satisfies LinkDef,
skuPath: 'LXM-CR'
} satisfies BannerBlock
+35
View File
@@ -0,0 +1,35 @@
import type * as C from '@hanzo/ui/blocks'
import type ProductDetailBlock from '@/blocks/def/product-detail-block'
//import video from './video'
import accordian from './accordian'
//import modal from './waitlist-modal'
export default {
blockType: 'product-detail',
title: 'LUX CREDIT',
desc: 'Unlock a world of DeFi with the Lux Credit Card, and spend while saving in the Lux ecosystem.',
//video: {...video, sizing: { vh: 60 }},
accordian,
blocks: [
{
blockType: 'cta',
elements: [
{
href: 'https://lux.credit',
title: 'Read More',
external: true,
newTab: false,
variant: 'outline'
},
{
title: 'Buy Now',
href: "https://lux.credit/cards/black?sku=LXM-CR-B-ABT",
external: true,
newTab: false,
variant: 'primary'
},
]
} as C.CTABlock
] as C.Block[]
} as ProductDetailBlock
+30
View File
@@ -0,0 +1,30 @@
import type { VideoBlock } from '@hanzo/ui/blocks'
import { DEF_VIDEO_PROPS } from '@luxfi/data'
export default {
blockType: 'video',
videoProps: DEF_VIDEO_PROPS,
poster: 'https://cdn.lux.network/commerce/cr/product/black-f-700x441.jpg',
sources: ['https://cdn.lux.network/commerce/cr/product/gunmetal-card.mp4'],
// From manually looking at aspect ratio
// https://stackoverflow.com/questions/684015/how-can-i-get-the-resolution-width-and-height-for-a-video-file-from-a-linux-co
dim: {
// 1920x1080
// .67 ratio
sm: {
// 1/4 * md
w: 145,
h: 82
},
md:{
w: 581,
h: 327
},
lg:{
w: 728,
h: 410
}
}
} as VideoBlock
+21
View File
@@ -0,0 +1,21 @@
export default {
blockType: 'accordian',
items: [
{
trigger: 'Direct Ownership of Gold',
content: 'Acquire sovereign ownership of physical gold through Lux Gold NFTs, each representing a specific amount of gold. Your ownership is recorded on the blockchain, ensuring transparency and security.'
},
{
trigger: 'Lux Standard Guaranteed Backing',
content: 'Every Lux Gold NFT is backed 1:1 by physical gold, adhering to the Lux Standard of guaranteed backing. Experience the confidence of true asset-backed ownership.'
},
{
trigger: 'Hold securely and borrow',
content: 'Retain your Lux Gold securely within our digital vault. Borrow against your holdings with favorable terms through our platform, providing financial flexibility.'
},
{
trigger: 'Sell, stake, or automatically convert',
content: 'Trade your Lux Gold NFTs within our ecosystem, stake them to earn rewards, or set preferences to automatically convert based on market conditions.'
},
]
}
+10
View File
@@ -0,0 +1,10 @@
import type BannerBlock from '@/blocks/def/banner-block'
import video from './video'
export default {
blockType: 'banner',
title: 'LUX GOLD',
byline: <>Responsibly manufactured, <br className='sm:hidden'/>ethically sourced.</>,
video: {...video, sizing: { vh: 50}},
learnLink: { href: "/gold" },
} satisfies BannerBlock
+86
View File
@@ -0,0 +1,86 @@
import type * as C from '@hanzo/ui/blocks'
import { formatCurrencyValue } from '@hanzo/commerce'
import { MiniChart } from '@luxfi/ui'
import { bullionPrice1oz } from '@luxfi/data/commerce'
import type ProductDetailBlock from '@/blocks/def/product-detail-block'
import video from './video'
import accordian from './accordian'
import modal from './waitlist-modal'
export default {
blockType: 'product-detail',
title: 'LUX GOLD',
desc: "Forge your connection to the real world with Lux Gold, crafted with excellence in Dubai. This extraordinary digital collectable is more than a token, —it's the future of hyper-liquid real-world assets. Lux Gold is a embodiment of timeless wealth, prestige, and the synthesis of tradition and innovation. Fully redeemable for physical gold, Lux Gold goes beyond traditional ownership to where gold's timelessness meets the dynamic opportunities of the digital era. It's a seamless gateway where digital ownership evolves into physical reality, enabling any cryptocurrency to fluently be transmuted to gold, and even earn additional yield by lending and providing liquidity in DeFi.",
video: {...video, sizing: { vh: 80 }},
accordian,
price: {
priceCard: {
blockType: 'card',
title: 'Lux Gold Price / Oz',
content: <h4 className='font-heading text-xl sm:text-2xl md:text-lg lg:text-2xl'>{formatCurrencyValue(bullionPrice1oz('au'))}</h4>,
},
msCard: {blockType: 'card',
specifiers: 'full-width',
title: 'Market Spot Price / Oz',
content: <MiniChart symbol='GOLD' />
} satisfies C.CardBlock,
},
blocks: [
{
blockType: 'cta',
elements: [
{
text: 'Waitlist',
props: {
variant: 'primary',
size: 'lg',
},
action: {
type: 'modal',
def: modal
}
},
]
} as C.CTABlock,
/*
{
blockType: 'heading',
heading: 'Editions'
} as C.HeadingBlock,
{
blockType: 'card',
specifiers: 'media-left content-left heading-style-title',
title: 'NEXT MINT',
content: markdown(`
Available: 1,000,100 oz<br/>
Price: $2000 / oz<br/>
Spot price:: ~$2050 / oz
`),
video,
} as C.CardBlock,
{
blockType: 'cta',
elements: [
{
title: 'Buy Now',
href: "https://mint.lux.network/gold",
external: true,
newTab: false,
},
{
href: 'https://docs.lux.network',
title: 'Read More',
external: true,
newTab: false,
variant: 'outline'
}
]
} as C.CTABlock,
*/
] as C.Block[]
} as ProductDetailBlock
+30
View File
@@ -0,0 +1,30 @@
import type { VideoBlock } from '@hanzo/ui/blocks'
import { DEF_VIDEO_PROPS } from '@luxfi/data'
export default {
blockType: 'video',
videoProps: DEF_VIDEO_PROPS,
poster: 'https://cdn.lux.network/commerce/gold/product/luxgold-poster.jpg',
sources: ['https://cdn.lux.network/commerce/gold/product/luxgold.mp4'],
// From manually looking at aspect ratio
// https://stackoverflow.com/questions/684015/how-can-i-get-the-resolution-width-and-height-for-a-video-file-from-a-linux-co
dim: {
// 1282x1920
// .67 ratio
sm: {
// 1/4 * md
w: 55,
h: 82
},
md:{
w: 219,
h: 327
},
lg:{
w: 275,
h: 410
}
}
} as VideoBlock
@@ -0,0 +1,15 @@
import type { ButtonModalDef } from '@hanzo/ui/types'
import { ContactDialog } from '@luxfi/ui'
import saveToWaitlist from '@luxfi/ui/server-actions'
export default {
Comp: ContactDialog,
title: 'Join the Waitlist',
byline: 'Be the first to get access to Lux Gold.',
action: saveToWaitlist,
actionEnclosure: {
dbId: 'lux-waitlist',
listId: 'gold',
reply: "You're on the LUX Gold Waitlist."
}
} satisfies ButtonModalDef
+23
View File
@@ -0,0 +1,23 @@
import type { AccordianBlock } from '@hanzo/ui/blocks'
export default {
blockType: 'accordian',
items: [
{
trigger: 'Get priority access',
content: 'Be the first to get access to Lux product launches and special events.'
},
{
trigger: 'LUX Coin as a gift',
content: 'Get 1,000,000 LUX Coin, the deflationary and supply-locked Lux Network currency.'
},
{
trigger: 'Apple Wallet Supported',
content: 'Lux Key is the first NFT that can be added to your Apple Wallet as a reward card with NFC support at events.'
},
{
trigger: 'Automatic DAO membership',
content: 'Lux Key includes a membership to the Lux Network decentralized governance organization.'
},
]
} satisfies AccordianBlock
+11
View File
@@ -0,0 +1,11 @@
import type BannerBlock from '@/blocks/def/banner-block'
import video from './video'
export default {
blockType: 'banner',
title: 'LUX KEY',
byline: `Unlock access to the world of Lux and join the infinite game.`,
video: {...video, sizing: {vh:50}},
learnLink: { href: "/key" },
skuPath: 'LXM-PS'
} satisfies BannerBlock
+24
View File
@@ -0,0 +1,24 @@
import type * as C from '@hanzo/ui/blocks'
import { BuyButton } from '@luxfi/ui'
import type ProductDetailBlock from '@/blocks/def/product-detail-block'
import accordian from './accordian'
const desc = 'Acquire a Lux Key and ensure access to the Lux Network’s most sought-after events and launches. Start your journey with 1,000,000 LUX Coins, offering a future woven with thrilling experiences and adventures that redefine extraordinary.'
export default {
blockType: 'product-detail',
title: 'LUX KEY',
desc,
animation: 'https://prod.spline.design/Itggaf1iI7481mcU/scene.splinecode',
accordian,
blocks: [{blockType: 'element',
element: (
<div className='h-full w-full flex flex-col justify-center items-center gap-8 '>
<BuyButton skuPath='LXM-PS' className='lg:min-w-pr-65 min-w-pr-65 mx-auto'>Buy</BuyButton>
</div>
)
} satisfies C.ElementBlock as C.Block]
} satisfies ProductDetailBlock
+29
View File
@@ -0,0 +1,29 @@
import type { VideoBlock } from '@hanzo/ui/blocks'
import { DEF_VIDEO_PROPS } from '@luxfi/data'
export default {
blockType: 'video',
videoProps: DEF_VIDEO_PROPS,
poster: 'https://cdn.lux.network/commerce/ps/product/LUX-PASS-poster.jpg',
sources: ['https://cdn.lux.network/commerce/ps/product/LUX-PASS-transcode.mp4'],
// From manually looking at aspect ratio
// https://stackoverflow.com/questions/684015/how-can-i-get-the-resolution-width-and-height-for-a-video-file-from-a-linux-co
dim: {
// 658x720
sm: {
// 1/4 * md
w: 75,
h: 82
},
md:{
w: 298,
h: 327
},
lg:{
w: 375,
h: 410
}
}
} as VideoBlock
@@ -0,0 +1,15 @@
import type { ButtonModalDef } from '@hanzo/ui/types'
import { ContactDialog } from '@luxfi/ui'
import saveToWaitlist from '@luxfi/ui/server-actions'
export default {
Comp: ContactDialog,
title: 'Join the Waitlist',
byline: 'Be the first to get access to Lux Key.',
action: saveToWaitlist,
actionEnclosure: {
dbId: 'lux-waitlist',
listId: 'key',
reply: "You're on the Lux Waitlist."
}
} satisfies ButtonModalDef
@@ -0,0 +1,3 @@
Lux Silver NFTs go directly into your Lux Wallet or other compatible wallet.
[Lux Credit](/credit) (coming soon) will provide the opportunity to borrow
against your silver with one of the first crypto collateralized credit cards.
@@ -0,0 +1,27 @@
import type { AccordianBlock } from '@hanzo/ui/blocks'
import LuxStandardMDX from './lux-standard-guar-backing.mdx'
import HoldSecurelyMDX from './hold-securely.mdx'
import SellStakeMDX from './sell-stake.mdx'
export default {
blockType: 'accordian',
items: [
{
trigger: 'Direct Ownership of Silver',
content: "Each Lux Silver NFT is 1:1 backed by audit verified physical silver deposits under an exclusive agreement with publicly traded Viscount Mining Co. of Canada – see the Lux Standard for more."
},
{
trigger: 'Lux Standard Guaranteed Backing',
content: <LuxStandardMDX />
},
{
trigger: 'Hold securely and borrow',
content: <HoldSecurelyMDX />
},
{
trigger: 'Sell, stake, or automatically convert',
content: <SellStakeMDX />
},
]
} satisfies AccordianBlock
@@ -0,0 +1,11 @@
- **Guaranteed Backing:** Lux Silver asset-backing is secured under an exclusive 5 year agreement to mine,
refine and vault 10,000,000 ounces of Silver with [Viscount Mining Corp.](https://viscountmining.com) of Canada
([CVE:VML](https://www.google.com/finance/quote/VML:CVE?sa=X&ved=2ahUKEwixnc31iLz-AhXFATQIHXQpCKQQ3ecFegQICRAY)).
- **Audited Deposits:** Lux Silver asset-backing is verified by Canadian standard NI 43-101
Technical and Mineral Resource Report by ARSENEAU Consulting. Deposits are part of the [Silver Cliff](https://viscountmining.com/projects/silver-cliff)
mine. [This resource report](/assets/pdf/Lux-Silver-Resource-Report.pdf) is bonded to each NFT.
- **Powered by Lux Partners Limited:**<br/>**Lux Team** of professionals with proven track records building and launching products
at the intersection of finance and technology – learn more at [Lux.Partners](https://lux.partners).<br/>**Lux Network** of blockchains enables access on every major blockchain over the Lux Bridge – 
learn more on [Lux.Network](https://lux.network).
@@ -0,0 +1,4 @@
Sell Lux Silver NFTs on secondary markets (coming soon),
deposit them for the most liquid asset-backed Lux Silver Tokens (coming soon),
or hold them until the silver is mined within 5 years when
it will automatically convert into Lux Silver Tokens.
+11
View File
@@ -0,0 +1,11 @@
import type BannerBlock from '@/blocks/def/banner-block'
import video from './video'
export default {
blockType: 'banner',
title: 'LUX SILVER',
byline: 'The Silver Rush is Here.',
video: {...video, sizing: { vh: 50}},
learnLink: { href: "/silver" },
skuPath: 'LXM-AG'
} satisfies BannerBlock
+92
View File
@@ -0,0 +1,92 @@
import type { LinkDef } from '@hanzo/ui/types'
import type * as C from '@hanzo/ui/blocks'
import { formatCurrencyValue } from '@hanzo/commerce'
import { MiniChart, BuyButton } from '@luxfi/ui'
import { bullionPrice1oz } from '@luxfi/data/commerce'
import type ProductDetailBlock from '@/blocks/def/product-detail-block'
import YahooFinanceCard from './yahoo-finance-card.mdx'
import SilverStockInvestorCard from './silver-stock-investor-card.mdx'
import video from './video'
import accordian from './accordian'
export default {
blockType: 'product-detail',
title: 'LUX SILVER',
desc: 'Get unprecedented access to silver with 1:1 asset-backed Lux Silver NFTs, sovereign ownership of physical silver without management fees, and mine-direct discount pricing.',
video: {...video, sizing: { vh: 80}},
accordian,
price: {
priceCard: {blockType: 'card',
title: 'Lux Silver Price / Oz',
content: <div className='h-full w-full flex flex-col justify-center items-center gap-8'>
<h4 className='font-heading text-center text-xl sm:text-2xl md:text-lg lg:text-2xl'>{formatCurrencyValue(bullionPrice1oz('ag'))}</h4>
<BuyButton skuPath='LXM-AG' className='lg:min-w-pr-65 min-w-pr-65'>Buy</BuyButton>
</div>,
} satisfies C.CardBlock,
msCard: {blockType: 'card',
specifiers: 'full-width',
title: 'Market Spot Price / Oz',
content: <MiniChart symbol='SILVER' exchange='TVC' />
} satisfies C.CardBlock,
},
blocks: [ /*
{blockType: 'element',
element: <SelectProductButton familyId='LXM-AG-B' popupClx='w-[280px]'>Buy</SelectProductButton>
} satisfies C.ElementBlock as C.Block,
{
blockType: 'heading',
heading: 'Editions'
} as C.HeadingBlock,
{
blockType: 'card',
specifiers: 'media-left content-left heading-style-title',
title: 'NEXT MINT',
content: markdown(
`Available: 10,000,100 oz<br/>
Price: $20 / oz<br/>
Spot price: ~$25 / oz`
),
video,
} as C.CardBlock,
*/
{
blockType: 'space'
},
{
blockType: 'heading',
heading: 'Lux Silver News'
} satisfies C.HeadingBlock as C.Block,
{
blockType: 'card',
specifiers: 'left-justify-content bg-card',
content: <YahooFinanceCard />,
cta: {blockType: 'cta',
elements: [{
title: 'View on Yahoo Finance',
href: 'https://finance.yahoo.com/news/viscount-mining-lux-partners-ltd-114500431.html'
} satisfies LinkDef]
}
} satisfies C.CardBlock as C.Block,
{ blockType: 'space' } as C.Block,
{
blockType: 'heading',
heading: 'Silver Market News'
} satisfies C.HeadingBlock as C.Block,
{
blockType: 'card',
specifiers: 'news',
content: <SilverStockInvestorCard />,
cta: {blockType: 'cta',
elements: [{
title: 'Download PDF',
href: '/assets/pdf/Silver-Stock-Investor.pdf'
} satisfies LinkDef]
} satisfies C.CTABlock
} satisfies C.CardBlock as C.Block,
] satisfies C.Block[]
} satisfies ProductDetailBlock
@@ -0,0 +1,17 @@
<div style={{
width: '100%',
display: 'flex',
flexDirection: 'row',
justifyContent: 'flex-start',
alignItems: 'center',
gap: 10
}}>
<img src='/assets/content/peter-krauth.png' width='60' style={{display: 'block'}}/>
<h4 style={{textAlign: 'left'}}>Silver Stock Investor</h4>
</div>
"**Viscount Mining (TSXV:VML; OTCQX:VLMGF)** is my latest addition to the Silver Stock Portfolio.
Viscount has been in my sights for a while, and I think the time is ripe to buy.
With not one but two outstanding properties, great people and a great JV partner,
I think there’s some really big upside potential ahead!"
<div style={{textAlign: 'right'}}>&mdash;**Silver Stock Investor**, Peter Krauth.</div>
+32
View File
@@ -0,0 +1,32 @@
import type { VideoBlock } from '@hanzo/ui/blocks'
import { DEF_VIDEO_PROPS } from '@luxfi/data'
export default {
blockType: 'video',
videoProps: DEF_VIDEO_PROPS,
poster: 'https://cdn.lux.network/commerce/silver/product/luxsilver-poster.jpg',
sources: ['https://cdn.lux.network/commerce/silver/product/luxsilver.mp4'],
// From manually looking at aspect ratio
// https://stackoverflow.com/questions/684015/how-can-i-get-the-resolution-width-and-height-for-a-video-file-from-a-linux-co
dim: {
// 1172x1920
// .61 aspect
sm: {
// md * 1/4
w: 50,
h: 82
},
md: {
w: 200,
h: 327
},
lg: {
// match the general size of old site
w: 250,
h: 410,
}
}
} as VideoBlock

Some files were not shown because too many files have changed in this diff Show More