Skip to content

Commit b1a44a7

Browse files
authored
Merge pull request #9 from oasisprotocol/mz/explore
Render ROFL apps in Explore page
2 parents 6eb758d + b74a7fa commit b1a44a7

File tree

10 files changed

+205
-14
lines changed

10 files changed

+205
-14
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"lucide-react": "^0.514.0",
2424
"react": "^19.1.0",
2525
"react-dom": "^19.1.0",
26+
"react-intersection-observer": "^9.16.0",
2627
"react-router-dom": "^7.6.2",
2728
"viem": "^2.31.0",
2829
"wagmi": "^2.15.6"
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { type FC } from 'react';
2+
import { CircleCheck, CircleMinus, CirclePause } from 'lucide-react';
3+
4+
type AppStatusTypes = 'active' | 'inactive' | 'removed';
5+
6+
function getRoflAppStatus(
7+
hasActiveInstances: boolean,
8+
removed: boolean
9+
): AppStatusTypes {
10+
if (removed) return 'removed';
11+
return hasActiveInstances ? 'active' : 'inactive';
12+
}
13+
14+
type AppStatusIconProps = {
15+
hasActiveInstances: boolean;
16+
removed: boolean;
17+
};
18+
19+
export const AppStatusIcon: FC<AppStatusIconProps> = ({
20+
hasActiveInstances,
21+
removed,
22+
}) => {
23+
const status = getRoflAppStatus(hasActiveInstances, removed);
24+
const getStatusIcon = (status: AppStatusTypes) => {
25+
switch (status) {
26+
case 'active':
27+
return (
28+
<CircleCheck
29+
className="h-5 w-5"
30+
style={{ color: 'var(--success)' }}
31+
/>
32+
);
33+
case 'inactive':
34+
return (
35+
<CirclePause
36+
className="h-5 w-5"
37+
style={{ color: 'var(--warning)' }}
38+
/>
39+
);
40+
case 'removed':
41+
return (
42+
<CircleMinus className="h-5 w-5" style={{ color: 'var(--error)' }} />
43+
);
44+
default:
45+
return null;
46+
}
47+
};
48+
49+
return <>{getStatusIcon(status)}</>;
50+
};

src/components/EmptyState/index.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
CardFooter,
55
CardHeader,
66
} from '@oasisprotocol/ui-library/src/components/ui/card';
7+
import { useAccount } from 'wagmi';
78

89
interface EmptyStateProps {
910
title: string;
@@ -12,16 +13,22 @@ interface EmptyStateProps {
1213
}
1314

1415
export function EmptyState({ title, description, children }: EmptyStateProps) {
16+
const { isConnected } = useAccount();
17+
1518
return (
1619
<Card className="h-full rounded-md border-0 flex justify-center p-8 gap-2">
1720
<CardHeader className="text-xl font-semibold text-white text-center">
18-
{title}
21+
{isConnected ? title : 'Connect is not connected'}
1922
</CardHeader>
2023
<CardContent className="max-w-[60%] mx-auto text-gray-400 text-sm text-balance text-center leading-relaxed">
21-
{description}
24+
{isConnected
25+
? description
26+
: 'Please connect your wallet to to gain access to the view.'}
2227
</CardContent>
2328

24-
<CardFooter className="flex justify-center pt-2">{children}</CardFooter>
29+
<CardFooter className="flex justify-center pt-2">
30+
{isConnected ? children : null}
31+
</CardFooter>
2532
</Card>
2633
);
2734
}

src/components/RainbowKitConnectButton/index.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
import { AccountAvatar } from '../AccountAvatar';
1313
import { useDisconnect } from 'wagmi';
1414
import { useIsMobile } from '@oasisprotocol/ui-library/src/hooks/use-mobile';
15+
import { useNavigate } from 'react-router-dom';
1516

1617
const TruncatedAddress: FC<{ address: string; className?: string }> = ({
1718
address,
@@ -32,6 +33,13 @@ interface Props {
3233
export const RainbowKitConnectButton: FC<Props> = ({ onMobileClose }) => {
3334
const isMobile = useIsMobile();
3435
const { disconnect } = useDisconnect();
36+
const navigate = useNavigate();
37+
38+
const handleDisconnect = () => {
39+
disconnect();
40+
navigate('/');
41+
onMobileClose?.();
42+
};
3543

3644
return (
3745
<ConnectButton.Custom>
@@ -119,10 +127,7 @@ export const RainbowKitConnectButton: FC<Props> = ({ onMobileClose }) => {
119127
<Button
120128
variant="ghost"
121129
className="w-full justify-start rounded-md p-2.5 h-11"
122-
onClick={() => {
123-
disconnect();
124-
onMobileClose?.();
125-
}}
130+
onClick={handleDisconnect}
126131
>
127132
<span className="text-destructive text-base font-medium leading-6">
128133
Sign out
@@ -170,7 +175,7 @@ export const RainbowKitConnectButton: FC<Props> = ({ onMobileClose }) => {
170175
Switch network
171176
</DropdownMenuItem>
172177
<DropdownMenuSeparator />
173-
<DropdownMenuItem onClick={() => disconnect()}>
178+
<DropdownMenuItem onClick={handleDisconnect}>
174179
Disconnect
175180
</DropdownMenuItem>
176181
</DropdownMenuContent>

src/hooks/useNetwork.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useAccount } from 'wagmi';
22

3-
export function useNetwork(): 'mainnet' | 'testnet' {
3+
export function useNetwork(fallback): 'mainnet' | 'testnet' {
44
const { chainId } = useAccount();
55

66
if (chainId === 23294) {
@@ -11,5 +11,9 @@ export function useNetwork(): 'mainnet' | 'testnet' {
1111
return 'testnet';
1212
}
1313

14+
if (fallback) {
15+
return fallback;
16+
}
17+
1418
throw new Error(`Unsupported chainId: ${chainId}.`);
1519
}

src/pages/CreateApp/index.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import type { FC } from 'react';
1+
import { type FC } from 'react';
2+
import { MainLayout } from '../../components/Layout/MainLayout';
23

34
export const Create: FC = () => {
4-
return <>Create</>;
5+
return <MainLayout>Create</MainLayout>;
56
};
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { type FC } from 'react';
2+
import { type RoflApp } from '../../nexus/api';
3+
import { Button } from '@oasisprotocol/ui-library/src/components/ui/button';
4+
import {
5+
Card,
6+
CardContent,
7+
CardFooter,
8+
CardHeader,
9+
} from '@oasisprotocol/ui-library/src/components/ui/card';
10+
import { ArrowUpRight } from 'lucide-react';
11+
import { AppStatusIcon } from '../../components/AppStatusIcon';
12+
13+
type ExploreAppCardProps = {
14+
app: RoflApp;
15+
network: string;
16+
};
17+
18+
export const ExploreAppCard: FC<ExploreAppCardProps> = ({ app, network }) => {
19+
return (
20+
<Card className="rounded-md">
21+
<CardHeader className="">
22+
<div className="flex items-start justify-between">
23+
<h3 className="text-lg font-semibold text-foreground pr-2 break-all">
24+
<>{app.metadata?.['net.oasis.rofl.name']}</>
25+
</h3>
26+
<AppStatusIcon
27+
hasActiveInstances={!!app.num_active_instances}
28+
removed={app.removed}
29+
/>
30+
</div>
31+
</CardHeader>
32+
<CardContent className="flex-1">
33+
<p className="text-gray-400 text-sm leading-relaxed">
34+
<>{app.metadata?.['net.oasis.rofl.description']}</>
35+
</p>
36+
</CardContent>
37+
<CardFooter>
38+
<Button variant="secondary" className="w-full" asChild>
39+
<a
40+
href={`https://explorer.oasis.io/${network}/sapphire/rofl/app/${app.id}`}
41+
target="_blank"
42+
rel="noopener noreferrer"
43+
>
44+
<span className="flex items-center justify-center">
45+
<span>View details</span>
46+
<ArrowUpRight className="ml-2 h-4 w-4" />
47+
</span>
48+
</a>
49+
</Button>
50+
</CardFooter>
51+
</Card>
52+
);
53+
};

src/pages/Explore/index.tsx

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,76 @@
1-
import type { FC } from 'react';
1+
import { type FC, useEffect } from 'react';
22
import { MainLayout } from '../../components/Layout/MainLayout';
33
import { ExploreEmptyState } from './emptyState';
4+
import { useInfiniteQuery } from '@tanstack/react-query';
5+
import { useNetwork } from '../../hooks/useNetwork';
6+
import { Skeleton } from '@oasisprotocol/ui-library/src/components/ui/skeleton';
7+
import { ExploreAppCard } from './ExploreAppCard';
8+
import {
9+
getGetRuntimeRoflAppsQueryKey,
10+
GetRuntimeRoflApps,
11+
} from '../../nexus/api';
12+
import { useInView } from 'react-intersection-observer';
13+
14+
const pageLimit = 18;
415

516
export const Explore: FC = () => {
17+
const { ref, inView } = useInView();
18+
const network = useNetwork('mainnet');
19+
20+
const {
21+
data,
22+
fetchNextPage,
23+
hasNextPage,
24+
isFetchingNextPage,
25+
isLoading,
26+
isFetched,
27+
} = useInfiniteQuery({
28+
queryKey: [...getGetRuntimeRoflAppsQueryKey(network, 'sapphire')],
29+
queryFn: async ({ pageParam = 0 }) => {
30+
const result = await GetRuntimeRoflApps(network, 'sapphire', {
31+
limit: pageLimit,
32+
offset: pageParam,
33+
});
34+
return result;
35+
},
36+
initialPageParam: 0,
37+
getNextPageParam: (lastPage, allPages) => {
38+
const totalFetched = allPages.length * pageLimit;
39+
return totalFetched < lastPage.data.total_count
40+
? totalFetched
41+
: undefined;
42+
},
43+
});
44+
45+
useEffect(() => {
46+
if (inView && hasNextPage && !isFetchingNextPage) {
47+
fetchNextPage();
48+
}
49+
}, [fetchNextPage, hasNextPage, isFetchingNextPage, inView]);
50+
51+
const allRoflApps = data?.pages.flatMap((page) => page.data.rofl_apps) || [];
52+
const isEmpty = isFetched && allRoflApps.length === 0;
53+
654
return (
755
<MainLayout>
8-
<ExploreEmptyState />
56+
{isEmpty && <ExploreEmptyState />}
57+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
58+
{isLoading &&
59+
Array.from({ length: pageLimit }).map((_, index) => (
60+
<Skeleton key={index} className="w-full h-[200px]" />
61+
))}
62+
63+
{allRoflApps.map((app) => (
64+
<ExploreAppCard key={app.id} app={app} network={network} />
65+
))}
66+
67+
{isFetchingNextPage &&
68+
Array.from({ length: 3 }).map((_, index) => (
69+
<Skeleton key={`next-page-${index}`} className="w-full h-[200px]" />
70+
))}
71+
</div>
72+
73+
<div ref={ref} className="h-10 w-full" />
974
</MainLayout>
1075
);
1176
};

ui-library

yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5268,6 +5268,11 @@ react-hook-form@^7.56.2:
52685268
resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.57.0.tgz#d0bb0c84060c6b9282d99c64566ec919dfca9409"
52695269
integrity sha512-RbEks3+cbvTP84l/VXGUZ+JMrKOS8ykQCRYdm5aYsxnDquL0vspsyNhGRO7pcH6hsZqWlPOjLye7rJqdtdAmlg==
52705270

5271+
react-intersection-observer@^9.16.0:
5272+
version "9.16.0"
5273+
resolved "https://registry.yarnpkg.com/react-intersection-observer/-/react-intersection-observer-9.16.0.tgz#7376d54edc47293300961010844d53b273ee0fb9"
5274+
integrity sha512-w9nJSEp+DrW9KmQmeWHQyfaP6b03v+TdXynaoA964Wxt7mdR3An11z4NNCQgL4gKSK7y1ver2Fq+JKH6CWEzUA==
5275+
52715276
react-is@^16.13.1:
52725277
version "16.13.1"
52735278
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"

0 commit comments

Comments
 (0)