Skip to content

Commit 77c1cdc

Browse files
authored
Merge pull request #508 from hypercerts-org/dev
chore: new release
2 parents b9f5ad7 + 5f1241a commit 77c1cdc

File tree

4 files changed

+243
-17
lines changed

4 files changed

+243
-17
lines changed
+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import React from "react";
2+
import { Skeleton } from "@/components/ui/skeleton";
3+
import {
4+
Accordion,
5+
AccordionContent,
6+
AccordionItem,
7+
AccordionTrigger,
8+
} from "@/components/ui/accordion";
9+
10+
const Loading = () => {
11+
return (
12+
<main className="flex flex-col p-8 md:px-24 md:pt-14 pb-24 space-y-4 flex-1">
13+
{/* Hypercert Details Loading State */}
14+
<div className="space-y-6">
15+
<div className="flex flex-col md:flex-row gap-6">
16+
{/* Image skeleton */}
17+
<Skeleton className="w-full md:w-[400px] h-[400px] rounded-lg" />
18+
19+
{/* Content skeleton */}
20+
<div className="flex-1 space-y-4">
21+
<Skeleton className="h-8 w-3/4" /> {/* Title */}
22+
<Skeleton className="h-4 w-1/4" /> {/* ID */}
23+
<Skeleton className="h-20 w-full" /> {/* Description */}
24+
{/* Work scope */}
25+
<div className="space-y-2">
26+
<Skeleton className="h-6 w-24" />
27+
<div className="flex gap-2">
28+
<Skeleton className="h-8 w-20" />
29+
<Skeleton className="h-8 w-20" />
30+
<Skeleton className="h-8 w-20" />
31+
</div>
32+
</div>
33+
</div>
34+
</div>
35+
</div>
36+
37+
{/* Accordion Sections Loading State */}
38+
<Accordion type="multiple" className="w-full">
39+
{/* Creator's Feed Section */}
40+
<AccordionItem value="creator-feed">
41+
<AccordionTrigger className="uppercase text-sm text-slate-500 font-medium tracking-wider">
42+
CREATOR&apos;S FEED
43+
</AccordionTrigger>
44+
<AccordionContent>
45+
<div className="space-y-4">
46+
<div className="flex justify-end">
47+
<Skeleton className="h-10 w-32" />
48+
</div>
49+
<div className="space-y-2">
50+
<Skeleton className="h-24 w-full" />
51+
<Skeleton className="h-24 w-full" />
52+
</div>
53+
</div>
54+
</AccordionContent>
55+
</AccordionItem>
56+
57+
{/* Evaluations Section */}
58+
<AccordionItem value="evaluations">
59+
<AccordionTrigger className="uppercase text-sm text-slate-500 font-medium tracking-wider">
60+
EVALUATIONS
61+
</AccordionTrigger>
62+
<AccordionContent>
63+
<div className="space-y-4">
64+
<div className="flex justify-end">
65+
<Skeleton className="h-10 w-32" />
66+
</div>
67+
<div className="space-y-2">
68+
<Skeleton className="h-24 w-full" />
69+
<Skeleton className="h-24 w-full" />
70+
</div>
71+
</div>
72+
</AccordionContent>
73+
</AccordionItem>
74+
75+
{/* Marketplace Section */}
76+
<AccordionItem value="marketplace-listings">
77+
<AccordionTrigger className="uppercase text-sm text-slate-500 font-medium tracking-wider">
78+
MARKETPLACE
79+
</AccordionTrigger>
80+
<AccordionContent>
81+
<div className="space-y-4">
82+
<div className="flex justify-end gap-2">
83+
<Skeleton className="h-10 w-24" />
84+
<Skeleton className="h-10 w-32" />
85+
</div>
86+
<div className="space-y-2">
87+
<Skeleton className="h-32 w-full" />
88+
</div>
89+
</div>
90+
</AccordionContent>
91+
</AccordionItem>
92+
</Accordion>
93+
</main>
94+
);
95+
};
96+
97+
export default Loading;

components/hypercert/contributors.tsx

+49-5
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,60 @@ import {
1111
} from "@/components/ui/command";
1212
import { UserCircle2 } from "lucide-react";
1313
import { useState } from "react";
14-
import { isAddress } from "viem";
14+
import { getAddress, isAddress } from "viem";
1515
import { HypercertState } from "@/hypercerts/fragments/hypercert-state.fragment";
16+
import { useGetUser } from "@/users/hooks";
17+
import { UserIcon } from "../user-icon";
18+
import { ImageIcon } from "../user-icon/ImageIcon";
19+
import { SvgIcon } from "../user-icon/SvgIcon";
20+
import { Skeleton } from "../ui/skeleton";
21+
import { UserName } from "../user-name";
1622
const MAX_CONTRIBUTORS_DISPLAYED = 10;
1723

1824
function Contributor({ contributor }: { contributor: string }) {
19-
return isAddress(contributor) ? (
20-
<EthAddress address={contributor} />
25+
const address = isAddress(contributor.trim())
26+
? getAddress(contributor.trim())
27+
: undefined;
28+
29+
const { data: userData, isFetching } = useGetUser({
30+
address: address,
31+
});
32+
33+
if (isFetching) {
34+
return (
35+
<div className="flex flex-row gap-2 items-center">
36+
<Skeleton className="h-8 w-8 rounded-full" />
37+
<Skeleton className="h-8 w-32" />
38+
</div>
39+
);
40+
}
41+
42+
return userData?.user ? (
43+
<div className="flex gap-2">
44+
{userData.user.avatar ? (
45+
<ImageIcon url={userData.user.avatar} size="tiny" />
46+
) : (
47+
<SvgIcon size="tiny" />
48+
)}
49+
<div className="flex flex-col justify-center items-start">
50+
<UserName
51+
address={userData.user.address}
52+
userName={userData.user.display_name}
53+
/>
54+
</div>
55+
</div>
56+
) : address ? (
57+
<div className="flex gap-2">
58+
<UserIcon address={address} size="tiny" />
59+
<div className="flex flex-col justify-center items-start">
60+
<EthAddress address={address} showEnsName={true} />
61+
</div>
62+
</div>
2163
) : (
22-
<div>{contributor}</div>
64+
<div className="flex items-center flex-row">
65+
<UserCircle2 className="mr-2 h-4 w-4" />
66+
{contributor}
67+
</div>
2368
);
2469
}
2570

@@ -58,7 +103,6 @@ export default function Contributors({
58103
<CommandGroup>
59104
{hypercert.metadata?.contributors.map((contributor) => (
60105
<CommandItem key={contributor}>
61-
<UserCircle2 className="mr-2 h-4 w-4" />
62106
<Contributor contributor={contributor} />
63107
</CommandItem>
64108
))}

components/hypercert/fractions.tsx

+67-12
Original file line numberDiff line numberDiff line change
@@ -8,26 +8,82 @@ import {
88
CommandItem,
99
CommandList,
1010
} from "@/components/ui/command";
11-
import { truncateEthereumAddress } from "@/lib/utils";
12-
import { UserCircle2 } from "lucide-react";
1311
import { useState } from "react";
1412
import { FormattedUnits } from "../formatted-units";
1513
import { HypercertState } from "@/hypercerts/fragments/hypercert-state.fragment";
16-
14+
import { getAddress, isAddress } from "viem";
15+
import { useGetUser } from "@/users/hooks";
16+
import { Skeleton } from "../ui/skeleton";
17+
import { UserIcon } from "../user-icon";
18+
import { ImageIcon } from "../user-icon/ImageIcon";
19+
import { SvgIcon } from "../user-icon/SvgIcon";
20+
import EthAddress from "../eth-address";
21+
import { UserName } from "../user-name";
22+
import { calculateBigIntPercentage } from "@/lib/calculateBigIntPercentage";
1723
const MAX_FRACTIONS_DISPLAYED = 5;
1824

1925
function Fraction({
2026
ownerAddress,
27+
totalUnits,
2128
units,
2229
}: {
23-
ownerAddress: string | null;
24-
units: unknown;
30+
ownerAddress: string;
31+
totalUnits: string | null | undefined;
32+
units: string | null | undefined;
2533
}) {
34+
const address = isAddress(ownerAddress.trim())
35+
? getAddress(ownerAddress.trim())
36+
: undefined;
37+
38+
const { data: userData, isFetching } = useGetUser({
39+
address: address,
40+
});
41+
42+
const calculatedPercentage = calculateBigIntPercentage(
43+
units as string,
44+
totalUnits as string,
45+
);
46+
47+
const roundedPercentage =
48+
calculatedPercentage! < 1
49+
? "<1"
50+
: Math.round(calculatedPercentage!).toString();
51+
52+
if (isFetching) {
53+
return (
54+
<div className="flex flex-row gap-2 items-center">
55+
<Skeleton className="h-8 w-8 rounded-full" />
56+
<Skeleton className="h-8 w-32" />
57+
</div>
58+
);
59+
}
2660
return (
27-
<div className="flex flex-col w-full">
28-
{truncateEthereumAddress(ownerAddress as `0x${string}`)} &mdash;{" "}
29-
<FormattedUnits>{units as string}</FormattedUnits>
30-
</div>
61+
<>
62+
{userData?.user ? (
63+
<div className="flex flex-row gap-2">
64+
{userData?.user?.avatar ? (
65+
<ImageIcon url={userData.user.avatar} size="tiny" />
66+
) : (
67+
<SvgIcon size="tiny" />
68+
)}
69+
<div className="flex justify-center items-start">
70+
<UserName
71+
address={userData.user.address}
72+
userName={userData.user.display_name}
73+
/>
74+
</div>
75+
</div>
76+
) : (
77+
<div className="flex flex-row items-center gap-2">
78+
<UserIcon address={address} size="tiny" />
79+
<EthAddress address={address} showEnsName={true} />
80+
</div>
81+
)}
82+
<div className="flex flex-row items-center gap-2">
83+
&mdash; <FormattedUnits>{units as string}</FormattedUnits>
84+
<span>{`(${roundedPercentage}%)`}</span>
85+
</div>
86+
</>
3187
);
3288
}
3389

@@ -77,10 +133,9 @@ export default function Fractions({
77133
key={fraction.fraction_id}
78134
title={fraction.owner_address || ""}
79135
>
80-
<UserCircle2 className="mr-2 h-4 w-4" />
81-
<span className="hidden">{fraction.owner_address}</span>
82136
<Fraction
83-
ownerAddress={fraction.owner_address}
137+
ownerAddress={fraction.owner_address as string}
138+
totalUnits={hypercert.units}
84139
units={fraction.units}
85140
/>
86141
</CommandItem>

components/user-name.tsx

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import React from "react";
2+
import { CopyButton } from "./copy-button";
3+
4+
export function UserName({
5+
address,
6+
userName,
7+
}: {
8+
address: string | undefined | null;
9+
userName: string | undefined | null;
10+
}) {
11+
const copyAddress = (event: React.MouseEvent<HTMLDivElement>) => {
12+
event.stopPropagation();
13+
void navigator.clipboard.writeText(address as string);
14+
};
15+
16+
if (!address) {
17+
return <div>Unknown</div>;
18+
}
19+
return (
20+
<div className="flex items-center gap-2 content-center cursor-pointer px-1 py-0.5 bg-slate-100 rounded-md w-max text-sm">
21+
<div onClick={copyAddress}>
22+
<p>{userName}</p>
23+
</div>
24+
<CopyButton
25+
textToCopy={address}
26+
className="w-4 h-4 bg-transparent focus:opacity-70 focus:scale-90"
27+
/>
28+
</div>
29+
);
30+
}

0 commit comments

Comments
 (0)