Initial commit
This commit is contained in:
30
app/pages/home/HomePage.tsx
Normal file
30
app/pages/home/HomePage.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { ErrorView } from "~/components/error/ErrorView";
|
||||
import { PricingTable } from "./PricingTable";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { LoadingView } from "~/components/loading/LoadingView";
|
||||
import { fetchApi } from "~/lib/fetch";
|
||||
import type { CryptocurrenciesListResponse } from "~/types/crypto-api";
|
||||
import { TopCards } from "./TopCards";
|
||||
|
||||
export function HomePage() {
|
||||
const { data, isPending, error } = useQuery({
|
||||
queryKey: ["cryptocurrencies"],
|
||||
queryFn: () => fetchApi<CryptocurrenciesListResponse>("https://api.next.code-camp.org/cryptocurrencies"),
|
||||
refetchInterval: 5000
|
||||
});
|
||||
|
||||
if (isPending) {
|
||||
return <LoadingView />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <ErrorView />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<TopCards currencies={data.currencies} />
|
||||
<PricingTable currencies={data.currencies} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
87
app/pages/home/PricingTable.tsx
Normal file
87
app/pages/home/PricingTable.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import clsx from "clsx";
|
||||
import { Link } from "react-router";
|
||||
import type {
|
||||
CryptocurrenciesListResponse,
|
||||
SimpleCryptoCurrency
|
||||
} from "~/types/crypto-api";
|
||||
import { formatPercentage } from "~/utils/format";
|
||||
|
||||
function PricingRow({
|
||||
currency,
|
||||
index
|
||||
}: {
|
||||
currency: SimpleCryptoCurrency;
|
||||
index: number;
|
||||
}) {
|
||||
return (
|
||||
<tr className="hover:bg-slate-50">
|
||||
<td className="px-5 py-4 text-slate-500 hidden md:table-cell">
|
||||
{index + 1}
|
||||
</td>
|
||||
<td className="px-5 py-4 flex items-center gap-3">
|
||||
<Link className="flex flex-row gap-3" to={`/crypto/${currency.id}`}>
|
||||
<img
|
||||
className="h-8 w-8 rounded-full"
|
||||
src={currency.logo}
|
||||
alt={`${currency.name} Logo`}
|
||||
/>
|
||||
<div>
|
||||
<p className="font-medium">{currency.name}</p>
|
||||
<p className="text-xs text-slate-500">{currency.symbol}</p>
|
||||
</div>
|
||||
</Link>
|
||||
</td>
|
||||
<td className="px-5 py-4 font-medium">
|
||||
${currency.price.toLocaleString()}
|
||||
</td>
|
||||
<td className="px-5 py-4 hidden md:table-cell">
|
||||
${(currency.market_cap / 1_000_000_000).toFixed(1)}B
|
||||
</td>
|
||||
<td className="px-5 py-4 hidden md:table-cell">
|
||||
${(currency.volume_24h / 1_000_000_000).toFixed(1)}B
|
||||
</td>
|
||||
<td className="px-5 py-4">
|
||||
<span
|
||||
className={clsx(
|
||||
"rounded-full px-2 py-1 text-xs font-medium",
|
||||
currency.percent_change_24h > 0
|
||||
? "bg-emerald-100 text-emerald-700"
|
||||
: "bg-rose-100 text-rose-700"
|
||||
)}
|
||||
>
|
||||
{formatPercentage(currency.percent_change_24h)}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
export function PricingTable({
|
||||
currencies
|
||||
}: {
|
||||
currencies: CryptocurrenciesListResponse["currencies"];
|
||||
}) {
|
||||
return (
|
||||
<section className="mt-8 rounded-2xl border border-slate-200 bg-white shadow-sm">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full text-left text-sm">
|
||||
<thead className="bg-slate-100 text-xs uppercase tracking-wider text-slate-600">
|
||||
<tr>
|
||||
<th className="px-5 py-3 hidden md:table-cell">#</th>
|
||||
<th className="px-5 py-3">Asset</th>
|
||||
<th className="px-5 py-3">Price</th>
|
||||
<th className="px-5 py-3 hidden md:table-cell">Market cap</th>
|
||||
<th className="px-5 py-3 hidden md:table-cell">Volume (24h)</th>
|
||||
<th className="px-5 py-3">Change (24h)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-100">
|
||||
{currencies.map((currency, index) => (
|
||||
<PricingRow key={currency.id} currency={currency} index={index} />
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
71
app/pages/home/TopCards.tsx
Normal file
71
app/pages/home/TopCards.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import clsx from "clsx";
|
||||
import type { CryptocurrenciesListResponse } from "~/types/crypto-api";
|
||||
import { formatPercentage } from "~/utils/format";
|
||||
|
||||
function TopCard({ header, value, footer, percentChange }) {
|
||||
return (
|
||||
<article className="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-slate-600">{header}</p>
|
||||
<p className="text-3xl font-semibold mt-2">{value}</p>
|
||||
</div>
|
||||
|
||||
{percentChange !== null && (
|
||||
<span
|
||||
className={clsx(
|
||||
"inline-flex items-center rounded-full px-3 py-1 text-xs font-medium",
|
||||
percentChange > 0
|
||||
? "bg-emerald-100 text-emerald-700"
|
||||
: "bg-rose-100 text-rose-700"
|
||||
)}
|
||||
>
|
||||
{formatPercentage(percentChange)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="mt-4 text-xs text-slate-500">{footer}</p>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
export function TopCards({
|
||||
currencies
|
||||
}: {
|
||||
currencies: CryptocurrenciesListResponse["currencies"];
|
||||
}) {
|
||||
const bitcoin = currencies.find((c) => c.slug === "bitcoin")!;
|
||||
|
||||
const gainersCount = currencies.filter((c) => c.percent_change_24h > 0).length;
|
||||
const losersCount = currencies.filter((c) => c.percent_change_24h < 0).length;
|
||||
|
||||
const averagePriceChange = currencies.reduce((prev, current) => prev + current.percent_change_24h, 0) / currencies.length;
|
||||
|
||||
const totalVolume = currencies.reduce((prev, current) => prev + current.volume_24h, 0);
|
||||
const averageVolume = totalVolume / currencies.length;
|
||||
|
||||
return (
|
||||
<section className="grid gap-4 md:grid-cols-3">
|
||||
<TopCard
|
||||
header="Bitcoin (BTC)"
|
||||
value={`$${bitcoin.price.toLocaleString()}`}
|
||||
footer="Bitcoin is the top cryptocurrency."
|
||||
percentChange={bitcoin.percent_change_24h}
|
||||
/>
|
||||
|
||||
<TopCard
|
||||
header="Top 50 Average (24h)"
|
||||
value={formatPercentage(averagePriceChange)}
|
||||
footer={`Gainers: ${gainersCount} | Losers: ${losersCount}`}
|
||||
percentChange={averagePriceChange}
|
||||
/>
|
||||
|
||||
<TopCard
|
||||
header="Top 50 Total Volume (24h)"
|
||||
value={`$${(totalVolume / 1_000_000_000).toFixed(1)}B`}
|
||||
footer={`Avg. volume per coin: $${(averageVolume / 1_000_000_000).toFixed(1)}B`}
|
||||
percentChange={null}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user