Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add messages feed #7

Merged
merged 4 commits into from
May 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions components/topbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"use client";

import React, { useEffect, useState } from "react";
import { InfiniteMovingCards } from "./ui/infinite-moving-cards";
import { useData } from "../contexts/APIDataContext";

const chainDetailsMapping = {
arbitrum: { displayName: "Arbitrum", imageName: "Arbitrum.svg" },
avax: { displayName: "Avalanche", imageName: "Avalanche.svg" },
base: { displayName: "Base", imageName: "Base.svg" },
bsc: { displayName: "BNB Chain", imageName: "Binance-Smart-Chain.svg" },
eth2: { displayName: "Ethereum", imageName: "Ethereum.svg" },
havah: { displayName: "Havah", imageName: "Havah.svg" },
ibc_archway: { displayName: "Archway", imageName: "Archway.svg" },
ibc_injective: { displayName: "Injective", imageName: "Injective.svg" },
ibc_neutron: { displayName: "Neutron", imageName: "Neutron.svg" },
icon: { displayName: "ICON", imageName: "ICON.svg" },
optimism: { displayName: "Optimism", imageName: "Optimism.svg" },
};

export function Topbar() {
const { networkMessages, loading } = useData();
const [chainmessages, setChainMessages] = useState([]);
const [isLoaded, setIsLoaded] = useState(false);

useEffect(() => {
if (!loading) {
const generateChainMessages = () => {
const messages = [];
for (const [key, value] of Object.entries(networkMessages)) {
const chainDetails = chainDetailsMapping[key];
if (chainDetails) {
messages.push(
{ messages: value.src, chain: chainDetails.displayName, role: "src", image: chainDetails.imageName },
{ messages: value.dest, chain: chainDetails.displayName, role: "dest", image: chainDetails.imageName }
);
}
}
return messages;
};
setChainMessages(generateChainMessages());
setIsLoaded(true);
}
}, [loading, networkMessages]);

return (
<div
className={`h-fit rounded-md flex flex-col antialiased bg-transparent w-full dark:bg-grid-white/[0.05] items-center justify-center relative overflow-hidden transition-all duration-1000 ease-in-out transform ${
isLoaded ? "max-h-64 opacity-100 translate-y-0" : "max-h-0 opacity-0 -translate-y-10"
}`}
>
{isLoaded && (
<InfiniteMovingCards
items={chainmessages}
direction="right"
speed="slow"
/>
)}
</div>
);
}
119 changes: 119 additions & 0 deletions components/ui/infinite-moving-cards.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
"use client";

import { cn } from "../../lib/utils/cn";
import React, { useEffect, useState } from "react";
import Image from "next/image";
import { it } from "node:test";

export const InfiniteMovingCards = ({
items,
direction = "left",
speed = "fast",
pauseOnHover = true,
className,
}: {
items: {
messages: number;
chain: string;
role: string;
image: string;
}[];
direction?: "left" | "right";
speed?: "fast" | "normal" | "slow";
pauseOnHover?: boolean;
className?: string;
}) => {
const containerRef = React.useRef<HTMLDivElement>(null);
const scrollerRef = React.useRef<HTMLUListElement>(null);

useEffect(() => {
addAnimation();
}, []);
const [start, setStart] = useState(false);
function addAnimation() {
if (containerRef.current && scrollerRef.current) {
const scrollerContent = Array.from(scrollerRef.current.children);

scrollerContent.forEach((item) => {
const duplicatedItem = item.cloneNode(true);
if (scrollerRef.current) {
scrollerRef.current.appendChild(duplicatedItem);
}
});

getDirection();
getSpeed();
setStart(true);
}
}
const getDirection = () => {
if (containerRef.current) {
if (direction === "left") {
containerRef.current.style.setProperty(
"--animation-direction",
"forwards"
);
} else {
containerRef.current.style.setProperty(
"--animation-direction",
"reverse"
);
}
}
};
const getSpeed = () => {
if (containerRef.current) {
if (speed === "fast") {
containerRef.current.style.setProperty("--animation-duration", "20s");
} else if (speed === "normal") {
containerRef.current.style.setProperty("--animation-duration", "40s");
} else {
containerRef.current.style.setProperty("--animation-duration", "80s");
}
}
};
return (
<div
ref={containerRef}
className={cn(
"scroller relative z-20 max-w-7xl overflow-hidden [mask-image:linear-gradient(to_right,transparent,white_20%,white_80%,transparent)]",
className
)}
>
<ul
ref={scrollerRef}
className={cn(
" flex min-w-full shrink-0 gap-4 pt-2 pb-1 w-max flex-nowrap",
start && "animate-scroll ",
pauseOnHover && "hover:[animation-play-state:paused]"
)}
>
{items.map((item, idx) => (
<li
className="w-fit sm:w-[350px] max-w-full shadow dark:shadow-0 relative rounded-full dark:border flex-shrink-0 border-slate-700 px-8 py-2 md:w-fit bg-gradient-to-br dark:from-[#111111] dark:via-[#111111] dark:to-black from-white via-white to-gray-100"
key={idx}
>
<blockquote>
<div
aria-hidden="true"
className="user-select-none -z-1 pointer-events-none absolute -left-0.5 -top-0.5 h-[calc(100%_+_4px)] w-[calc(100%_+_4px)]"
></div>
<div className=" relative z-20 text-sm leading-[1.6] text-gray-400 font-normal flex flex-row items-center">
<span className="mr-1 text-gray-800 dark:text-gray-100 ">{item.messages} messages </span> {item.role === "src" ? <><p className="hidden sm:inline-block">have left</p><p className="inline-block sm:hidden">from</p></> : <><p className="hidden sm:inline-block">have arrived on</p><p className="inline-block sm:hidden">to</p></>}
<div className="flex flex-row items-center ml-2 space-x-2">
<Image priority className="w-4" src={`/chainlogos/${item.image}`} alt={item.chain} height={40} width={40}/>
<span className=" text-sm leading-[1.6] text-gray-800 dark:text-gray-100 font-normal">
{item.chain}
</span>
</div>
</div>



</blockquote>
</li>
))}
</ul>
</div>
);
};
69 changes: 69 additions & 0 deletions contexts/APIDataContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"use client";

import { createContext, useContext, ReactNode, useState, useEffect } from "react";
import { DataContextType } from "../types/DataTypes";

const APIDataContext = createContext<DataContextType | undefined>(undefined);

const XCALLTRACKER_ENDPOINT = '/api/statistics/total_messages'

const networks = [
'icon', 'bsc', 'eth2', 'havah', 'ibc_archway', 'ibc_neutron', 'ibc_injective',
'avax', 'base', 'arbitrum', 'optimism'
];

export const APIDataContextProvider = ({ children }: { children: ReactNode }) => {

const [loading, setLoading] = useState(true);
const [totalMessages, setTotalMessages] = useState(0);
const [networkMessages, setNetworkMessages] = useState<Record<string, { src: number, dest: number }>>({});

useEffect(() => {
const fetchData = async () => {
setLoading(true);
try {

const responseTracker = await fetch(XCALLTRACKER_ENDPOINT, { cache: 'no-store' });
const responseTrackerData = await responseTracker.json();
setTotalMessages(responseTrackerData.data.total)

const networkPromises = networks.map(async (network) => {
const srcResponse = await fetch(`/api/statistics/total_messages?src_network=${network}`, { cache: 'no-store' });
const srcData = await srcResponse.json();
const destResponse = await fetch(`/api/statistics/total_messages?dest_network=${network}`, { cache: 'no-store' });
const destData = await destResponse.json();
return { network, src: srcData.data.total, dest: destData.data.total };
});

const networkData = await Promise.all(networkPromises);
const networkMessages = networkData.reduce((acc, { network, src, dest }) => {
acc[network] = { src, dest };
return acc;
}, {} as Record<string, { src: number, dest: number }>);

setNetworkMessages(networkMessages);
} catch (error) {
console.error('Error fetching xcall fees:', error);
} finally {
setLoading(false);
}
};

fetchData();
}, []);


return (
<APIDataContext.Provider value={{ loading, totalMessages, networkMessages }}>
{children}
</APIDataContext.Provider>
);
};

export const useData = () => {
const context = useContext(APIDataContext);
if (context === undefined) {
throw new Error("useData must be used within a APIDataContextProvider");
}
return context;
};
6 changes: 6 additions & 0 deletions lib/utils/cn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
9 changes: 9 additions & 0 deletions next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@ module.exports = withNextra({
}
];
},
async rewrites() {
return [
{
source: '/api/statistics/total_messages/:path*',
destination: 'https://xcallscan.xyz/api/statistics/total_messages/:path*'
},
// Add other rewrites here if needed
];
},
webpack: (config, { isServer }) => {
if (isServer) {
require("./scripts/generate-sitemap");
Expand Down
53 changes: 46 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,18 @@
"@mendable/search": "^0.0.183",
"@tailwindcss/typography": "^0.5.9",
"@vercel/analytics": "^1.0.1",
"clsx": "^2.1.1",
"cookies-next": "^2.1.2",
"feed": "^4.2.2",
"framer-motion": "^11.2.6",
"globby": "^11.0.1",
"next": "^13.0.6",
"nextra": "^2.13.2",
"nextra-theme-docs": "^2.13.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"sharp": "^0.33.2"
"sharp": "^0.33.2",
"tailwind-merge": "^2.3.0"
},
"devDependencies": {
"@types/node": "18.11.10",
Expand Down
Loading
Loading