Skip to content

Commit 7e2154b

Browse files
authored
Merge pull request #9 from Ratio1/develop
Branding and CSPs page
2 parents d27c5ad + 45f8ab7 commit 7e2154b

File tree

40 files changed

+1127
-307
lines changed

40 files changed

+1127
-307
lines changed

AGENTS.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Repository Guidelines
2+
3+
## Project Structure & Module Organization
4+
Routes live in `app/` (e.g., `app/stats`, `app/accounts`); server-only logic stays under `app/server-components/**`. Shared UI components go in `components/`, domain helpers in `lib/`, and blockchain metadata in `blockchain/` with matching types in `typedefs/`. Static assets reside in `public/`, design tokens in `styles/`, and automation scripts in `scripts/`.
5+
6+
## Build, Test, and Development Commands
7+
Install dependencies with `npm install`. Start the UI via `npm run dev` (mainnet defaults) or `npm run dev:{network}` to load the corresponding `.env.production.*` file through `dotenv-cli`. Build bundles with `npm run build` or `npm run build:{network}`; `npm run build:all` compiles every target. Run `npm run lint` before commits.
8+
9+
## Coding Style & Naming Conventions
10+
TypeScript is the baseline. ESLint (`eslint.config.mjs`) and Prettier (`prettier-plugin-tailwindcss`) manage formatting—use `npm run lint` or `npx prettier --check .` instead of manual edits. Follow Prettier defaults (two-space indentation, semicolons, single quotes). Components, hooks, and contexts use `PascalCase`; utilities and functions use `camelCase`. Keep route folders lower-case.
11+
12+
## Testing Guidelines
13+
Automated tests are not yet checked in, so linting plus manual validation gate releases. When you add tests, colocate specs as `*.test.tsx` or `*.spec.ts` near the feature (or in a local `__tests__/` folder) and lean on React Testing Library for UI behaviour. Mock blockchain calls with helpers from `lib/api`. Document manual verification in PRs until an automated suite lands.
14+
15+
## Commit & Pull Request Guidelines
16+
Commits follow a Conventional Commits style (`fix:`, `hotfix:`, `cleanup:`); keep subjects under 70 characters and add scopes when useful (`feat(accounts): add filters`). Before opening a PR, ensure `npm run lint` passes and builds succeed when relevant. PRs should explain intent, link issues with `Closes #id`, list affected routes/APIs, attach UI screenshots, and record manual checks or follow-ups.
17+
18+
## Security & Configuration Tips
19+
Never commit secrets; `.env.production.*` values should come from secure storage. Use the provided Dockerfiles for reproducible devnet/testnet/mainnet setups. Prefer scripts in `scripts/` when seeding or syncing blockchain data so production services stay clean, and remove temporary credentials after debugging.

app/account/[ownerEthAddr]/page.tsx

Lines changed: 134 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,40 @@
11
import CompactLicenseCard from '@/app/server-components/main-cards/CompactLicenseCard';
2+
import PublicProfile from '@/app/server-components/Profile/PublicProfile';
23
import { BorderedCard } from '@/app/server-components/shared/cards/BorderedCard';
34
import { CardHorizontal } from '@/app/server-components/shared/cards/CardHorizontal';
45
import UsageStats from '@/app/server-components/shared/Licenses/UsageStats';
56
import ClientWrapper from '@/components/shared/ClientWrapper';
67
import { CopyableAddress } from '@/components/shared/CopyableValue';
78
import config from '@/config';
8-
import { fetchErc20Balance, getLicenses } from '@/lib/api/blockchain';
9+
import { getPublicProfiles } from '@/lib/api/backend';
10+
import { fetchCSPDetails, fetchErc20Balance, getLicenses } from '@/lib/api/blockchain';
911
import { routePath } from '@/lib/routes';
10-
import { cachedGetENSName, fBI, getShortAddress, isEmptyETHAddr } from '@/lib/utils';
12+
import { cachedGetENSName, fBI, getShortAddress, isZeroAddress } from '@/lib/utils';
1113
import * as types from '@/typedefs/blockchain';
14+
import type { PublicProfileInfo } from '@/typedefs/general';
15+
import { unstable_cache } from 'next/cache';
1216
import { notFound, redirect } from 'next/navigation';
17+
import { RiCloudLine } from 'react-icons/ri';
1318
import { isAddress } from 'viem';
1419

20+
const getCachedNodeOperatorProfile = unstable_cache(
21+
async (ownerEthAddr: types.EthAddress): Promise<PublicProfileInfo | undefined> => {
22+
const response = await getPublicProfiles([ownerEthAddr]);
23+
24+
if (!response?.brands || response.brands.length === 0) {
25+
return undefined;
26+
}
27+
28+
return response.brands[0];
29+
},
30+
['account:nodeOperatorProfile'],
31+
{ revalidate: 60 },
32+
);
33+
1534
export async function generateMetadata({ params }) {
1635
const { ownerEthAddr } = await params;
1736

18-
if (!ownerEthAddr || !isAddress(ownerEthAddr) || isEmptyETHAddr(ownerEthAddr)) {
37+
if (!ownerEthAddr || !isAddress(ownerEthAddr) || isZeroAddress(ownerEthAddr)) {
1938
return {
2039
title: 'Error',
2140
openGraph: {
@@ -24,111 +43,153 @@ export async function generateMetadata({ params }) {
2443
};
2544
}
2645

27-
const ensName = await cachedGetENSName(ownerEthAddr);
46+
try {
47+
const [ensName, publicProfile] = await Promise.all([
48+
cachedGetENSName(ownerEthAddr),
49+
getCachedNodeOperatorProfile(ownerEthAddr as types.EthAddress),
50+
]);
51+
52+
const primaryName = publicProfile?.name || ensName || getShortAddress(ownerEthAddr, 4, true);
2853

29-
return {
30-
title: `Account • ${ensName || getShortAddress(ownerEthAddr, 4, true)}`,
31-
openGraph: {
32-
title: `Account • ${ensName || getShortAddress(ownerEthAddr, 4, true)}`,
33-
},
34-
};
54+
return {
55+
title: `Node Operator • ${primaryName}`,
56+
openGraph: {
57+
title: `Node Operator • ${primaryName}`,
58+
},
59+
};
60+
} catch (error) {
61+
return null;
62+
}
3563
}
3664

37-
export default async function OwnerPage({ params }) {
65+
export default async function NodeOperatorPage({ params }) {
3866
const { ownerEthAddr } = await params;
3967

40-
if (!ownerEthAddr || !isAddress(ownerEthAddr) || isEmptyETHAddr(ownerEthAddr)) {
41-
console.log(`[Account Page] Invalid owner address: ${ownerEthAddr}`);
68+
if (!ownerEthAddr || !isAddress(ownerEthAddr) || isZeroAddress(ownerEthAddr)) {
69+
console.log(`[NodeOperatorPage] Invalid owner address: ${ownerEthAddr}`);
4270
notFound();
4371
}
4472

45-
let licenses: types.LicenseInfo[], ensName: string | undefined, r1Balance: bigint;
73+
let licenses: types.LicenseInfo[],
74+
ensName: string | undefined,
75+
r1Balance: bigint,
76+
publicProfileInfo: PublicProfileInfo | undefined,
77+
cspDetails: types.CSP | undefined;
4678

4779
try {
48-
[licenses, ensName, r1Balance] = await Promise.all([
80+
[licenses, ensName, r1Balance, publicProfileInfo, cspDetails] = await Promise.all([
4981
getLicenses(ownerEthAddr),
5082
cachedGetENSName(ownerEthAddr),
5183
fetchErc20Balance(ownerEthAddr, config.r1ContractAddress),
84+
getCachedNodeOperatorProfile(ownerEthAddr as types.EthAddress),
85+
fetchCSPDetails(ownerEthAddr),
5286
]);
5387
} catch (error) {
5488
console.error(error);
55-
console.log(`[Account Page] Failed to fetch account data for address: ${ownerEthAddr}`);
89+
console.log(`[Node Operator Page] Failed to fetch data for address: ${ownerEthAddr}`);
5690
redirect(routePath.notFound);
5791
}
5892

5993
return (
6094
<div className="responsive-col">
6195
<BorderedCard>
62-
<div className="card-title-big font-bold">
63-
Account •{' '}
64-
{ensName ? (
65-
<span>{ensName}</span>
66-
) : (
67-
<span className="roboto">{getShortAddress(ownerEthAddr, 4, true)}</span>
68-
)}
96+
<PublicProfile ownerEthAddr={ownerEthAddr} publicProfileInfo={publicProfileInfo} />
97+
98+
<div className="flexible-row">
99+
<CardHorizontal
100+
label="Address"
101+
value={
102+
<div>
103+
<ClientWrapper>
104+
<CopyableAddress value={ownerEthAddr} size={4} isLarge />
105+
</ClientWrapper>
106+
</div>
107+
}
108+
isSmall
109+
isFlexible
110+
/>
111+
112+
<CardHorizontal
113+
label="Licenses Owned"
114+
value={<div>{licenses.length}</div>}
115+
isSmall
116+
isFlexible
117+
widthClasses="min-w-[180px]"
118+
/>
119+
120+
<CardHorizontal
121+
label="Total $R1 Claimed"
122+
value={
123+
<div className="text-primary">
124+
{fBI(
125+
licenses.reduce((acc, license) => acc + license.totalClaimedAmount, 0n),
126+
18,
127+
)}
128+
</div>
129+
}
130+
isSmall
131+
isFlexible
132+
widthClasses="min-w-[268px]"
133+
/>
134+
135+
<CardHorizontal
136+
label="Licenses Usage (Total)"
137+
value={
138+
<div className="w-full min-w-52 xs:min-w-56 md:min-w-60">
139+
<UsageStats
140+
totalClaimedAmount={licenses.reduce((acc, license) => acc + license.totalClaimedAmount, 0n)}
141+
totalAssignedAmount={licenses.reduce(
142+
(acc, license) => acc + license.totalAssignedAmount,
143+
0n,
144+
)}
145+
/>
146+
</div>
147+
}
148+
isSmall
149+
isFlexible
150+
widthClasses="min-[520px]:min-w-[440px] md:max-w-[500px]"
151+
/>
152+
153+
<CardHorizontal
154+
label="Wallet $R1 Balance"
155+
value={<div className="text-primary">{fBI(r1Balance, 18)}</div>}
156+
isSmall
157+
/>
69158
</div>
159+
</BorderedCard>
70160

71-
<div className="col gap-3">
72-
<div className="flexible-row">
73-
<CardHorizontal
74-
label="Address"
75-
value={
76-
<div>
77-
<ClientWrapper>
78-
<CopyableAddress value={ownerEthAddr} size={4} isLarge />
79-
</ClientWrapper>
80-
</div>
81-
}
82-
isSmall
83-
isFlexible
84-
/>
161+
{!!cspDetails && (
162+
<BorderedCard>
163+
<div className="row gap-2.5">
164+
<div className="center-all rounded-full border-2 border-slate-150 p-1.5 text-2xl text-primary">
165+
<RiCloudLine />
166+
</div>
85167

86-
<CardHorizontal label="Licenses Owned" value={<div>{licenses.length}</div>} isSmall isFlexible />
168+
<div className="card-title font-bold">Cloud Service Provider Info</div>
169+
</div>
87170

171+
<div className="flexible-row">
88172
<CardHorizontal
89-
label="Total $R1 Claimed"
90-
value={
91-
<div className="text-primary">
92-
{fBI(
93-
licenses.reduce((acc, license) => acc + license.totalClaimedAmount, 0n),
94-
18,
95-
)}
96-
</div>
97-
}
98-
isSmall
173+
label="Escrow SC. Address"
174+
value={<CopyableAddress value={cspDetails.escrowAddress} size={4} />}
99175
isFlexible
100-
widthClasses="min-w-[268px]"
176+
widthClasses="min-w-[192px]"
101177
/>
102-
103178
<CardHorizontal
104-
label="Licenses Usage (Total)"
105-
value={
106-
<div className="w-full min-w-52 xs:min-w-56 md:min-w-60">
107-
<UsageStats
108-
totalClaimedAmount={licenses.reduce(
109-
(acc, license) => acc + license.totalClaimedAmount,
110-
0n,
111-
)}
112-
totalAssignedAmount={licenses.reduce(
113-
(acc, license) => acc + license.totalAssignedAmount,
114-
0n,
115-
)}
116-
/>
117-
</div>
118-
}
119-
isSmall
179+
label="Total Value Locked"
180+
value={fBI(cspDetails.tvl, 6)}
120181
isFlexible
121-
widthClasses="min-[520px]:min-w-[440px] md:max-w-[500px]"
182+
widthClasses="min-w-[192px]"
122183
/>
123-
124184
<CardHorizontal
125-
label="Wallet $R1 Balance"
126-
value={<div className="text-primary">{fBI(r1Balance, 18)}</div>}
127-
isSmall
185+
label="Active Jobs"
186+
value={cspDetails.activeJobsCount}
187+
isFlexible
188+
widthClasses="min-w-[192px]"
128189
/>
129190
</div>
130-
</div>
131-
</BorderedCard>
191+
</BorderedCard>
192+
)}
132193

133194
{licenses.map((license, index) => (
134195
<div key={index}>
Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@ export default function Loading() {
88
<div className="col w-full gap-2">
99
<Skeleton className="only-lg min-h-[56px] w-full rounded-xl" />
1010

11-
<Skeleton className="min-h-[68px] w-full rounded-2xl lg:min-h-[56px]" />
12-
<Skeleton className="min-h-[68px] w-full rounded-2xl lg:min-h-[56px]" />
13-
<Skeleton className="min-h-[68px] w-full rounded-2xl lg:min-h-[56px]" />
14-
<Skeleton className="min-h-[68px] w-full rounded-2xl lg:min-h-[56px]" />
15-
<Skeleton className="min-h-[68px] w-full rounded-2xl lg:min-h-[56px]" />
16-
<Skeleton className="min-h-[68px] w-full rounded-2xl lg:min-h-[56px]" />
11+
<Skeleton className="min-h-[68px] w-full rounded-2xl lg:min-h-[60px]" />
12+
<Skeleton className="min-h-[68px] w-full rounded-2xl lg:min-h-[60px]" />
13+
<Skeleton className="min-h-[68px] w-full rounded-2xl lg:min-h-[60px]" />
14+
<Skeleton className="min-h-[68px] w-full rounded-2xl lg:min-h-[60px]" />
15+
<Skeleton className="min-h-[68px] w-full rounded-2xl lg:min-h-[60px]" />
16+
<Skeleton className="min-h-[68px] w-full rounded-2xl lg:min-h-[60px]" />
1717
</div>
1818
</div>
1919
);
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { fetchCSPs } from '@/lib/api/blockchain';
2+
import { routePath } from '@/lib/routes';
3+
import { fBI } from '@/lib/utils';
4+
import * as types from '@/typedefs/blockchain';
5+
import { redirect } from 'next/navigation';
6+
import CSPsList from '../server-components/CPSs/CSPsList';
7+
import { BorderedCard } from '../server-components/shared/cards/BorderedCard';
8+
import { CardHorizontal } from '../server-components/shared/cards/CardHorizontal';
9+
10+
export async function generateMetadata() {
11+
return {
12+
title: 'Cloud Service Providers',
13+
openGraph: {
14+
title: 'Cloud Service Providers',
15+
},
16+
};
17+
}
18+
19+
export default async function CSPsPage(props: {
20+
searchParams?: Promise<{
21+
page?: string;
22+
}>;
23+
}) {
24+
const searchParams = await props.searchParams;
25+
const currentPage = Number(searchParams?.page) || 1;
26+
27+
let csps: readonly types.CSP[];
28+
29+
try {
30+
csps = await fetchCSPs();
31+
32+
console.log(csps);
33+
} catch (error) {
34+
console.error(error);
35+
console.log('[CSPsPage] Failed to fetch data');
36+
redirect(routePath.notFound);
37+
}
38+
39+
return (
40+
<>
41+
<div className="w-full">
42+
<BorderedCard>
43+
<div className="card-title-big font-bold">Cloud Service Providers</div>
44+
45+
<div className="flexible-row">
46+
<CardHorizontal label="Total CSPs" value={csps.length} isFlexible widthClasses="min-w-[192px]" />
47+
<CardHorizontal
48+
label="Total Value Locked"
49+
value={
50+
<div>
51+
<span>
52+
{fBI(
53+
csps.reduce((acc, curr) => acc + curr.tvl, 0n),
54+
6,
55+
)}
56+
</span>{' '}
57+
<span className="text-slate-500">$USDC</span>
58+
</div>
59+
}
60+
isFlexible
61+
widthClasses="min-w-[192px]"
62+
/>
63+
<CardHorizontal
64+
label="Total Active Jobs"
65+
value={csps.reduce((acc, curr) => acc + curr.activeJobsCount, 0)}
66+
isFlexible
67+
widthClasses="min-w-[192px]"
68+
/>
69+
</div>
70+
</BorderedCard>
71+
</div>
72+
73+
<CSPsList csps={csps} currentPage={currentPage} />
74+
</>
75+
);
76+
}

0 commit comments

Comments
 (0)