Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion apps/appkit-minter/src/core/components/layout/app-router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,15 @@ import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { useWatchBalance, useWatchTransactions, useWatchJettons } from '@ton/appkit-react';
import { toast } from 'sonner';

import { MinterPage, StakingPage, SwapPage, OnrampPage, SignMessagePage } from '@/pages';
import {
MinterPage,
StakingPage,
SwapPage,
OnrampPage,
SignMessagePage,
NftPurchasePage,
NftPurchaseCollectionPage,
} from '@/pages';

export const AppRouter: React.FC = () => {
// Enable global real-time balance updates
Expand Down Expand Up @@ -54,6 +62,8 @@ export const AppRouter: React.FC = () => {
<Route path="/staking" element={<StakingPage />} />
<Route path="/onramp" element={<OnrampPage />} />
<Route path="/sign" element={<SignMessagePage />} />
<Route path="/buy-nft" element={<NftPurchasePage />} />
<Route path="/buy-nft/:collectionAddress" element={<NftPurchaseCollectionPage />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</BrowserRouter>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

import type React from 'react';
import { Coins, ArrowLeftRight, Sparkles, BookOpen, Github, PenLine, CreditCard } from 'lucide-react';
import { Coins, ArrowLeftRight, Sparkles, BookOpen, Github, PenLine, CreditCard, ShoppingBag } from 'lucide-react';
import { Link, NavLink } from 'react-router-dom';

import { AppLogo } from '../app-logo';
Expand All @@ -32,6 +32,7 @@ const NAV_LINKS: readonly { to: string; label: string; icon: React.ComponentType
{ to: '/swap', label: 'Swap', icon: ArrowLeftRight },
{ to: '/staking', label: 'Staking', icon: Coins },
{ to: '/onramp', label: 'Buy', icon: CreditCard },
{ to: '/buy-nft', label: 'Buy NFT', icon: ShoppingBag },
{ to: '/sign', label: 'Sign Message', icon: PenLine },
];

Expand Down
1 change: 1 addition & 0 deletions apps/appkit-minter/src/core/configs/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export const ENV_TON_API_KEY_MAINNET =
import.meta.env.VITE_TON_API_KEY ?? '25a9b2326a34b39a5fa4b264fb78fb4709e1bd576fc5e6b176639f5b71e94b0d';
export const ENV_TON_API_KEY_TESTNET =
import.meta.env.VITE_TON_API_TESTNET_KEY ?? 'd852b54d062f631565761042cccea87fa6337c41eb19b075e6c7fb88898a3992';
export const ENV_GETGEMS_API_KEY: string = import.meta.env.VITE_GETGEMS_API_KEY ?? '';
72 changes: 72 additions & 0 deletions apps/appkit-minter/src/features/nft_purchase/api/getgems-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/**
* Copyright (c) TonTech.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/

import type {
GetGemsBuyResponse,
GetGemsCollection,
GetGemsEnvelope,
GetGemsNftFull,
GetGemsNftsOnSaleResponse,
} from './types';

import { ENV_GETGEMS_API_KEY } from '@/core/configs/env';

// The real GetGems API (https://api.getgems.io/public-api) does not send
// CORS headers for browser origins, so we route requests through the Vite
// dev-server proxy configured in vite.config.ts.
const BASE_URL = '/getgems-api';

async function request<T>(path: string, init?: RequestInit): Promise<T> {

Check failure on line 24 in apps/appkit-minter/src/features/nft_purchase/api/getgems-client.ts

View workflow job for this annotation

GitHub Actions / unit

'RequestInit' is not defined
const res = await fetch(`${BASE_URL}${path}`, {
...init,
headers: {
Accept: 'application/json',
Authorization: ENV_GETGEMS_API_KEY,
...(init?.body ? { 'Content-Type': 'application/json' } : {}),
...init?.headers,
},
});

if (!res.ok) {
const text = await res.text().catch(() => '');
throw new Error(`GetGems ${res.status} ${res.statusText}: ${text || path}`);
}

const body = (await res.json()) as GetGemsEnvelope<T> | T;

if (typeof body === 'object' && body !== null && 'success' in body) {
const envelope = body as GetGemsEnvelope<T>;
if (!envelope.success) {
throw new Error(`GetGems request failed: ${path}`);
}
return envelope.response;
}

return body as T;
}

export function fetchCollection(collectionAddress: string): Promise<GetGemsCollection> {
return request<GetGemsCollection>(`/v1/collection/${encodeURIComponent(collectionAddress)}`);
}

export function fetchNftsOnSale(collectionAddress: string, limit = 30): Promise<GetGemsNftsOnSaleResponse> {
return request<GetGemsNftsOnSaleResponse>(
`/v1/nfts/on-sale/${encodeURIComponent(collectionAddress)}?limit=${limit}`,
);
}

export function fetchNft(nftAddress: string): Promise<GetGemsNftFull> {
return request<GetGemsNftFull>(`/v1/nft/${encodeURIComponent(nftAddress)}`);
}

export function buildBuyTransaction(nftAddress: string, version: string): Promise<GetGemsBuyResponse> {
return request<GetGemsBuyResponse>(`/v1/nfts/buy-fix-price/${encodeURIComponent(nftAddress)}`, {
method: 'POST',
body: JSON.stringify({ version }),
});
}
72 changes: 72 additions & 0 deletions apps/appkit-minter/src/features/nft_purchase/api/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/**
* Copyright (c) TonTech.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/

export interface GetGemsEnvelope<T> {
success: boolean;
response: T;
}

export interface GetGemsFixPriceSale {
type?: string;
fullPrice: string;
marketplaceFee?: string;
currency: string;
version: string;
contractAddress?: string;
}

export type GetGemsSale = GetGemsFixPriceSale | { type?: string; [key: string]: unknown };

export interface GetGemsNftOnSale {
address: string;
name?: string | null;
image?: string | null;
sale?: GetGemsSale | null;
}

export interface GetGemsNftsOnSaleResponse {
items: GetGemsNftOnSale[];
cursor?: string | null;
}

export interface GetGemsNftFull {
address: string;
name?: string | null;
description?: string | null;
image?: string | null;
ownerAddress?: string | null;
sale?: GetGemsSale | null;
}

export interface GetGemsCollection {
address: string;
name?: string | null;
description?: string | null;
image?: string | null;
ownerAddress?: string | null;
}

export interface GetGemsBuyMessage {
to: string;
amount: string;
payload?: string | null;
stateInit?: string | null;
}

export interface GetGemsBuyResponse {
uuid: string;
from?: string | null;
timeout: string;
list: GetGemsBuyMessage[];
}

export function isFixPriceSale(sale: GetGemsSale | null | undefined): sale is GetGemsFixPriceSale {
if (!sale) return false;
const candidate = sale as GetGemsFixPriceSale;
return typeof candidate.version === 'string' && typeof candidate.fullPrice === 'string';
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* Copyright (c) TonTech.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/

import type { FC } from 'react';
import { Link } from 'react-router-dom';
import { ChevronRight, ImageIcon } from 'lucide-react';

import { useCollection } from '../hooks/use-collections';

interface CollectionCardProps {
address: string;
}

export const CollectionCard: FC<CollectionCardProps> = ({ address }) => {
const { data, isLoading, isError } = useCollection(address);

const name = data?.name ?? (isLoading ? 'Loading…' : 'Unknown collection');
const description = data?.description;
const image = data?.image;

return (
<Link
to={`/buy-nft/${address}`}
className="flex items-center gap-4 p-4 rounded-lg border border-border bg-muted hover:bg-muted/70 transition-colors"
>
<div className="w-16 h-16 rounded-lg overflow-hidden bg-background/60 flex items-center justify-center shrink-0">
{image ? (
<img src={image} alt={name} className="w-full h-full object-cover" />
) : isLoading ? (
<div className="w-full h-full animate-pulse bg-background/80" />
) : (
<ImageIcon className="w-6 h-6 text-muted-foreground" />
)}
</div>
<div className="flex-1 min-w-0">
<p className="font-semibold text-foreground truncate">{name}</p>
{description && !isError && <p className="text-xs text-muted-foreground line-clamp-1">{description}</p>}
{isError && <p className="text-xs text-destructive">Failed to load collection info</p>}
</div>
<ChevronRight className="w-5 h-5 text-muted-foreground shrink-0" />
</Link>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* Copyright (c) TonTech.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/

import type { FC } from 'react';

import { useFeaturedCollectionAddresses } from '../hooks/use-collections';
import { CollectionCard } from './collection-card';

import { Card } from '@/core/components';

export const CollectionsList: FC = () => {
const addresses = useFeaturedCollectionAddresses();

return (
<Card title="Collections">
<div className="space-y-3">
{addresses.map((address) => (
<CollectionCard key={address} address={address} />
))}
</div>
</Card>
);
};
Loading
Loading