Skip to content

Commit e7a15f2

Browse files
authored
ui improvements (#235)
1 parent 7156966 commit e7a15f2

33 files changed

Lines changed: 3208 additions & 5937 deletions
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# THIS IS AUTOGENERATED. DO NOT EDIT MANUALLY
2+
version = 1
3+
name = "chainscout"
4+
5+
[setup]
6+
script = ""
7+
8+
[[actions]]
9+
name = "Run"
10+
icon = "run"
11+
command = "npm run dev"
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
on:
2+
push:
3+
branches:
4+
- 'dev'
5+
6+
name: Build and push docker (dev)
7+
8+
env:
9+
REGISTRY: ghcr.io
10+
IMAGE_NAME: blockscout/chainscout
11+
12+
jobs:
13+
push:
14+
name: Docker build and docker push
15+
timeout-minutes: 30
16+
runs-on: ubuntu-latest
17+
steps:
18+
- name: Checkout repository
19+
uses: actions/checkout@v4
20+
21+
- name: Login to GitHub Container Registry
22+
uses: docker/login-action@v3
23+
with:
24+
registry: ${{ env.REGISTRY }}
25+
username: ${{ github.actor }}
26+
password: ${{ secrets.GITHUB_TOKEN }}
27+
28+
- name: Extract metadata for Docker
29+
id: meta
30+
uses: docker/metadata-action@v5
31+
with:
32+
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
33+
34+
- name: Set up Docker Buildx
35+
uses: docker/setup-buildx-action@v3
36+
- name: Build and push
37+
uses: docker/build-push-action@v6
38+
with:
39+
context: "."
40+
file: "Dockerfile"
41+
push: true
42+
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev
43+
labels: ${{ steps.meta.outputs.labels }}
44+
cache-from: type=gha
45+
cache-to: type=gha,mode=max
46+
deploy_dev:
47+
name: Deploy dev instance
48+
needs: push
49+
runs-on: ubuntu-latest
50+
permissions: write-all
51+
steps:
52+
- name: Get Vault credentials
53+
id: retrieve-vault-secrets
54+
uses: hashicorp/vault-action@v2.4.1
55+
with:
56+
url: https://vault.k8s.blockscout.com
57+
role: ci-dev
58+
path: github-jwt
59+
method: jwt
60+
tlsSkipVerify: false
61+
exportToken: true
62+
secrets: |
63+
ci/data/dev/github token | WORKFLOW_TRIGGER_TOKEN ;
64+
- name: Trigger deploy
65+
uses: convictional/trigger-workflow-and-wait@v1.6.1
66+
with:
67+
owner: blockscout
68+
repo: deployment-values
69+
github_token: ${{ env.WORKFLOW_TRIGGER_TOKEN }}
70+
workflow_file_name: deploy_services.yaml
71+
ref: main
72+
wait_interval: 30
73+
client_payload: '{ "instance": "chainscout", "globalEnv": "testing"}'

app/api/chains/[id]/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ import { Chains } from '@/types';
55

66
export async function GET(
77
request: NextRequest,
8-
{ params }: { params: { id: string } }
8+
{ params }: { params: Promise<{ id: string }> }
99
) {
1010
try {
11-
const id = params.id;
11+
const { id } = await params;
1212
const filePath = path.join(process.cwd(), 'data', 'chains.json');
1313
const jsonData = await fs.readFile(filePath, 'utf8');
1414
const chainsData: Chains = JSON.parse(jsonData);

app/page.tsx

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import ChainList from '@/components/ChainList';
66
import Filters from '@/components/Filters';
77
import PopularEcosystems from '@/components/PopularEcosystems';
88
import AddChainSection from '@/components/AddChainSection';
9+
import ViewToggle, { ViewMode } from '@/components/ViewToggle';
10+
import DownloadDropdown from '@/components/DownloadDropdown';
911
import { Chains } from '@/types';
1012

1113
async function getChainsData(): Promise<Chains> {
@@ -44,6 +46,7 @@ export default function Home() {
4446
});
4547
const [sortOption, setSortOption] = useState<'Featured' | 'Alphabetical'>('Featured');
4648
const [featuredChains, setFeaturedChains] = useState<string[]>([]);
49+
const [viewMode, setViewMode] = useState<ViewMode>('list');
4750

4851
const popularEcosystems = ['Ethereum', 'Polygon', 'Optimism', 'Polkadot', 'Cosmos', 'zkSync', 'Arbitrum'];
4952

@@ -157,17 +160,25 @@ export default function Home() {
157160
/>
158161
</div>
159162
<div className="w-full mb-6 flex flex-col md:flex-row gap-3 md:gap-0 justify-between items-center">
160-
<div className="text-lg md:text-[22px] font-semibold text-[#6b6b74]">
161-
{sortedAndFilteredChains.length} Results
163+
<div className="flex items-center gap-5">
164+
<div className="text-lg md:text-[22px] font-semibold text-[#6b6b74]">
165+
{sortedAndFilteredChains.length} Results
166+
</div>
167+
<DownloadDropdown chains={sortedAndFilteredChains} />
168+
</div>
169+
<div className="flex w-full md:w-auto items-center gap-3">
170+
<div className="hidden min-[1000px]:block">
171+
<ViewToggle value={viewMode} onChange={setViewMode} />
172+
</div>
173+
<Filters
174+
filters={filters}
175+
setFilters={setFilters}
176+
ecosystems={ecosystems}
177+
appliedFiltersCount={appliedFiltersCount}
178+
sortOption={sortOption}
179+
setSortOption={setSortOption}
180+
/>
162181
</div>
163-
<Filters
164-
filters={filters}
165-
setFilters={setFilters}
166-
ecosystems={ecosystems}
167-
appliedFiltersCount={appliedFiltersCount}
168-
sortOption={sortOption}
169-
setSortOption={setSortOption}
170-
/>
171182
</div>
172183
<AddChainSection />
173184
<ChainList
@@ -176,6 +187,7 @@ export default function Home() {
176187
isLoading={isLoading}
177188
filters={filters}
178189
featuredChains={featuredChains}
190+
viewMode={viewMode}
179191
/>
180192
</div>
181193
</div>

components/ChainBadges.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { HOSTING_PROVIDERS, HostingProvider } from '@/utils/constants';
2+
3+
export const hostingColors: Record<HostingProvider, { bg: string; text: string }> = {
4+
'blockscout': { bg: '#91eabf', text: '#006635' },
5+
'conduit': { bg: '#31e3e3', text: '#0a0a0a' },
6+
'gelato-raas': { bg: '#f37b84', text: '#202020' },
7+
'altlayer-raas': { bg: 'hsla(264.6428571428571, 100.00%, 78.04%, 1.00)', text: '#1c1e24' },
8+
'protofire': { bg: '#faa807', text: '#1c1e24' },
9+
'gateway': { bg: '#9368E8', text: '#ffffff' },
10+
'self': { bg: '#c2d9ff', text: '#003180' },
11+
'alchemy': { bg: '#363FF9', text: '#ffffff' },
12+
'caldera': { bg: '#FC5000', text: '#F7F6F3' },
13+
};
14+
15+
export const Tag = ({ children }: { children: string }) => (
16+
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-sm font-medium bg-[#f4f5f6] text-[#6b6b74]">
17+
{children}
18+
</span>
19+
);
20+
21+
export const HostedByBadge = ({ hostedBy }: { hostedBy: HostingProvider }) => {
22+
const hostedByText = HOSTING_PROVIDERS[hostedBy] || 'Unknown';
23+
const colors = hostingColors[hostedBy] || hostingColors.blockscout;
24+
25+
return (
26+
<span
27+
className="inline-flex items-center px-2 py-0.5 rounded text-sm font-medium"
28+
style={{ backgroundColor: colors.bg, color: colors.text }}
29+
>
30+
{hostedBy === 'self' ? 'Self-hosted' : `Hosted by ${hostedByText}`}
31+
</span>
32+
);
33+
};

components/ChainCard.tsx

Lines changed: 40 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,9 @@ import { ChainData } from '@/types';
22
import Image from 'next/image';
33
import Link from 'next/link';
44
import React from 'react';
5-
import { HOSTING_PROVIDERS, HostingProvider, ROLLUP_TYPES, RollupType } from '@/utils/constants';
6-
import LinkIcon from '@/public/link.svg';
7-
8-
const hostingColors: Record<HostingProvider, { bg: string; text: string }> = {
9-
'blockscout': { bg: '#91eabf', text: '#006635' },
10-
'conduit': { bg: '#31e3e3', text: '#0a0a0a' },
11-
'gelato-raas': { bg: '#f37b84', text: '#202020' },
12-
'altlayer-raas': { bg: 'hsla(264.6428571428571, 100.00%, 78.04%, 1.00)', text: '#1c1e24' },
13-
'protofire': { bg: '#faa807', text: '#1c1e24' },
14-
'gateway': { bg: '#9368E8', text: '#ffffff' },
15-
'self': { bg: '#c2d9ff', text: '#003180' },
16-
'alchemy': { bg: '#363FF9', text: '#ffffff' },
17-
};
18-
19-
const Tag = ({ children }: { children: string }) => (
20-
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-sm font-medium bg-[#f4f5f6] text-[#6b6b74]">
21-
{children}
22-
</span>
23-
);
5+
import { ROLLUP_TYPES, RollupType } from '@/utils/constants';
6+
import { HostedByBadge, Tag } from '@/components/ChainBadges';
7+
import LinkIcon from '@/icons/link.svg';
248

259
export default function ChainCard({
2610
chainId,
@@ -36,19 +20,46 @@ export default function ChainCard({
3620
featured,
3721
}: ChainData & { chainId: string, featured: boolean }) {
3822
const { hostedBy, url } = explorers[0];
39-
const hostedByText = HOSTING_PROVIDERS[hostedBy as HostingProvider] || 'Unknown';
40-
const colors = hostingColors[hostedBy as HostingProvider] || hostingColors.blockscout;
4123
const ecosystemTags = Array.isArray(ecosystem) ? ecosystem : [ecosystem];
24+
const isClickFromLink = (target: EventTarget | null) => {
25+
return target instanceof HTMLElement && Boolean(target.closest('a'));
26+
};
27+
28+
const openExplorer = () => {
29+
window.open(url, '_blank', 'noopener,noreferrer');
30+
};
31+
32+
const isFullCardClickEnabled = () => {
33+
return window.matchMedia('(min-width: 1000px)').matches;
34+
};
35+
36+
const handleCardClick = (event: React.MouseEvent<HTMLElement>) => {
37+
if (isClickFromLink(event.target)) return;
38+
if (!isFullCardClickEnabled()) return;
39+
40+
openExplorer();
41+
};
42+
43+
const handleCardKeyDown = (event: React.KeyboardEvent<HTMLElement>) => {
44+
if (isClickFromLink(event.target)) return;
45+
if (!isFullCardClickEnabled()) return;
46+
if (event.key !== 'Enter' && event.key !== ' ') return;
47+
48+
event.preventDefault();
49+
openExplorer();
50+
};
4251

4352
return (
44-
<div className="bg-white p-6 flex flex-col border rounded-[20px] hover:shadow-[20px_0_40px_rgba(183,183,183,.1),2px_0_20px_rgba(183,183,183,.08)] transition-shadow duration-[400ms] ease-[cubic-bezier(.39, .575, .565, 1)] group">
53+
<div
54+
className="bg-white p-6 flex flex-col border rounded-[20px] hover:shadow-[20px_0_40px_rgba(183,183,183,.1),2px_0_20px_rgba(183,183,183,.08)] transition-shadow duration-[400ms] ease-[cubic-bezier(.39, .575, .565, 1)] group cursor-pointer"
55+
role="link"
56+
tabIndex={0}
57+
aria-label={`Open ${name} explorer`}
58+
onClick={handleCardClick}
59+
onKeyDown={handleCardKeyDown}
60+
>
4561
<div className="flex justify-between items-center mb-6">
46-
<span
47-
className="inline-flex items-center px-2 py-0.5 rounded text-sm font-medium"
48-
style={{ backgroundColor: colors.bg, color: colors.text }}
49-
>
50-
{hostedBy === 'self' ? 'Self-hosted' : `Hosted by ${hostedByText}`}
51-
</span>
62+
<HostedByBadge hostedBy={hostedBy} />
5263
{featured && (
5364
<Image
5465
src="/star.svg"
@@ -91,7 +102,7 @@ export default function ChainCard({
91102
<span className="text-sm font-medium text-black group-hover/link:text-blue-600 transition-colors duration-[400ms]">
92103
{text}
93104
</span>
94-
<LinkIcon className="flex-shrink-0 text-[#B1B5C3] group-hover/link:text-blue-600 transition-colors duration-[400ms]"/>
105+
<LinkIcon className="w-3 h-3 flex-shrink-0 text-[#B1B5C3] group-hover/link:text-blue-600 transition-colors duration-[400ms]"/>
95106
</Link>
96107
{index < array.length - 1 && <div className="border-t border-gray-200 my-3"></div>}
97108
</React.Fragment>

components/ChainList.tsx

Lines changed: 55 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import React, { useState, useMemo, useEffect } from 'react';
22
import { ChainData } from '@/types';
33
import ChainCard from '@/components/ChainCard';
4+
import ChainTable from '@/components/ChainTable';
45
import SkeletonCard from '@/components/SkeletonCard';
6+
import SkeletonTable from '@/components/SkeletonTable';
57
import Pagination from '@/components/Pagination';
8+
import { ViewMode } from '@/components/ViewToggle';
69

710
type Props = {
811
chains: Array<[string, ChainData]>;
@@ -14,11 +17,40 @@ type Props = {
1417
ecosystems: string[];
1518
};
1619
featuredChains: string[];
20+
viewMode: ViewMode;
1721
};
1822

1923
const ITEMS_PER_PAGE = 16;
2024

21-
export default function ChainList({ chains, searchTerm, isLoading, filters, featuredChains }: Props) {
25+
function CardGrid({
26+
chains,
27+
featuredChains,
28+
className = '',
29+
}: {
30+
chains: Array<[string, ChainData]>;
31+
featuredChains: string[];
32+
className?: string;
33+
}) {
34+
return (
35+
<div className={`w-full grid gap-6 sm:grid-cols-2 lg:grid-cols-4 ${className}`}>
36+
{chains.map(([chainId, data]) => (
37+
<ChainCard key={chainId} chainId={chainId} featured={featuredChains.includes(chainId)} {...data} />
38+
))}
39+
</div>
40+
);
41+
}
42+
43+
function SkeletonCardGrid({ className = '' }: { className?: string }) {
44+
return (
45+
<div className={`w-full grid gap-6 sm:grid-cols-2 lg:grid-cols-4 ${className}`}>
46+
{[...Array(16)].map((_, index) => (
47+
<SkeletonCard key={index} />
48+
))}
49+
</div>
50+
);
51+
}
52+
53+
export default function ChainList({ chains, searchTerm, isLoading, filters, featuredChains, viewMode }: Props) {
2254
const [currentPage, setCurrentPage] = useState(1);
2355

2456
const currentChains = useMemo(() => {
@@ -39,21 +71,33 @@ export default function ChainList({ chains, searchTerm, isLoading, filters, feat
3971

4072
if (isLoading) {
4173
return (
42-
<div className="w-full grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
43-
{[...Array(16)].map((_, index) => (
44-
<SkeletonCard key={index} />
45-
))}
46-
</div>
74+
<>
75+
<SkeletonCardGrid className={viewMode === 'list' ? 'min-[1000px]:hidden' : ''} />
76+
{viewMode === 'list' && (
77+
<div className="hidden w-full min-[1000px]:block">
78+
<SkeletonTable />
79+
</div>
80+
)}
81+
</>
4782
);
4883
}
4984

5085
return (
5186
<>
52-
<div className="w-full grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
53-
{currentChains.map(([chainId, data]) => (
54-
<ChainCard key={chainId} chainId={chainId} featured={featuredChains.includes(chainId)} {...data} />
55-
))}
56-
</div>
87+
{viewMode === 'list' ? (
88+
<>
89+
<CardGrid
90+
chains={currentChains}
91+
featuredChains={featuredChains}
92+
className="min-[1000px]:hidden"
93+
/>
94+
<div className="hidden w-full min-[1000px]:block">
95+
<ChainTable chains={currentChains} featuredChains={featuredChains} />
96+
</div>
97+
</>
98+
) : (
99+
<CardGrid chains={currentChains} featuredChains={featuredChains} />
100+
)}
57101
<Pagination
58102
currentPage={currentPage}
59103
totalPages={totalPages}

0 commit comments

Comments
 (0)