Compare commits
10 Commits
6081953ed5
...
95f2ff86a5
| Author | SHA1 | Date | |
|---|---|---|---|
| 95f2ff86a5 | |||
| 4209695865 | |||
| 9af71b4186 | |||
| 35fc65f3de | |||
| 3f4d8167e3 | |||
| 37f62edfe6 | |||
| f04314e511 | |||
| eab89f8515 | |||
| dea349d54f | |||
| d3fbd5a858 |
|
|
@ -1,14 +1,19 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import axios from 'axios';
|
||||
import { motion } from 'framer-motion';
|
||||
import debounce from 'lodash/debounce';
|
||||
|
||||
const CalloutModal = ({ isOpen, onClose, onSubmit }) => {
|
||||
const [tokenId, setTokenId] = useState('');
|
||||
const [tokenName, setTokenName] = useState('');
|
||||
const [tokenContract, setTokenContract] = useState('');
|
||||
const [tokenInput, setTokenInput] = useState('');
|
||||
const [searchResults, setSearchResults] = useState([]);
|
||||
const [showDropdown, setShowDropdown] = useState(false);
|
||||
const [shouldSearch, setShouldSearch] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (tokenId) {
|
||||
if (tokenId && shouldSearch) {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const response = await axios.get(`https://api.dexscreener.io/latest/dex/search/?q=${tokenId}`);
|
||||
|
|
@ -20,22 +25,34 @@ const CalloutModal = ({ isOpen, onClose, onSubmit }) => {
|
|||
}
|
||||
};
|
||||
fetchData();
|
||||
} else {
|
||||
setShowDropdown(false);
|
||||
}
|
||||
}, [tokenId]);
|
||||
}, [tokenId, shouldSearch]);
|
||||
|
||||
const debouncedSearch = useCallback(debounce((value) => {
|
||||
setShouldSearch(true);
|
||||
setTokenId(value);
|
||||
}, 500), []);
|
||||
|
||||
const handleTokenChange = (e) => {
|
||||
setTokenId(e.target.value);
|
||||
setTokenInput(e.target.value);
|
||||
|
||||
setTokenContract("");
|
||||
|
||||
setShouldSearch(false);
|
||||
debouncedSearch(e.target.value);
|
||||
};
|
||||
|
||||
const handleSelectToken = (token) => {
|
||||
const handleSelectToken = (token, name, ca) => {
|
||||
setTokenInput(token);
|
||||
setTokenId(token);
|
||||
setTokenName(name);
|
||||
setTokenContract(ca);
|
||||
setShowDropdown(false);
|
||||
setShouldSearch(false);
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
onSubmit(tokenId);
|
||||
onSubmit(tokenId,tokenContract,tokenName);
|
||||
onClose();
|
||||
};
|
||||
|
||||
|
|
@ -57,11 +74,11 @@ const CalloutModal = ({ isOpen, onClose, onSubmit }) => {
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<p className="text-lg mb-4">Type in a tokens short code, that you will think boom anytime soon</p>
|
||||
<p className="text-lg mb-4">Type in a token short code that you think will boom anytime soon</p>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
value={tokenId}
|
||||
value={tokenInput}
|
||||
onChange={handleTokenChange}
|
||||
className="w-full p-2 mb-4 border rounded glassmorphism"
|
||||
placeholder="Enter token short code"
|
||||
|
|
@ -70,33 +87,37 @@ const CalloutModal = ({ isOpen, onClose, onSubmit }) => {
|
|||
<ul className="absolute z-10 w-full bg-black border rounded shadow-lg max-h-60 overflow-y-auto">
|
||||
{searchResults.map((result) => (
|
||||
<li
|
||||
key={result.pairId}
|
||||
onClick={() => handleSelectToken(result.baseToken.symbol)}
|
||||
key={result["pairId"]}
|
||||
onClick={() => handleSelectToken(result["baseToken"]["symbol"],result["baseToken"]["name"], result["baseToken"]["address"])}
|
||||
className="p-2 hover:bg-gray-400 cursor-pointer flex justify-between items-center glassmorphism-scroller"
|
||||
>
|
||||
<div className="flex flex-col w-3/4">
|
||||
<div className="flex justify-between">
|
||||
<span>{result.baseToken.symbol} - {result.baseToken.name}</span>
|
||||
<span>{result.chain}</span>
|
||||
<span>{result["baseToken"]["symbol"]} - {result["baseToken"]["name"]}</span>
|
||||
<span>{result["chainId"]}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span>24H Vol: ${result.volume.h24.toLocaleString()}</span>
|
||||
<span>0x...{result.baseToken.address.slice(-4)}</span>
|
||||
<span>24H Vol: ${result["volume"]["h24"]}</span>
|
||||
<span>0x...{(result["baseToken"]["address"] as string).slice(-4)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-1/2 text-right">
|
||||
<span className="text-lg font-bold ml-2">${result.priceUsd}</span>
|
||||
<span className="text-lg font-bold ml-2">${result["priceUsd"]}</span>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
<p className="text-sm">{tokenContract.length > 0 ? "name: " +tokenName : ""}</p>
|
||||
|
||||
<p className="text-sm mb-5">{tokenContract.length > 0 ? "ca: " +tokenContract : ""}</p>
|
||||
|
||||
</div>
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded w-full glassmorphism"
|
||||
>
|
||||
Submit
|
||||
Callout!
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import "./globals.css";
|
|||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "CallFi Dashboard",
|
||||
title: "CallFi",
|
||||
description: "Call out next big mover and win!",
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,32 +1,56 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import axios from 'axios';
|
||||
|
||||
interface Detail {
|
||||
token: string;
|
||||
gains: number;
|
||||
price_at_creation: number;
|
||||
price_now: number;
|
||||
icon_url: string;
|
||||
ca: string; // Add contract address to the Detail interface
|
||||
}
|
||||
|
||||
const Modal: React.FC<{ isOpen: boolean; onClose: () => void; item: { username: string; points: number }; period:string; }> = ({ isOpen, onClose, item, period }) => {
|
||||
const [details, setDetails] = useState<Detail[] | null>(null); // Specify null as the initial type
|
||||
const Modal: React.FC<{
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
item: { username: string; points: number; img_url: string }; // Include img_url in the item prop
|
||||
period: string;
|
||||
}> = ({ isOpen, onClose, item, period }) => {
|
||||
const [details, setDetails] = useState<Detail[] | null>(null);
|
||||
const [urls, setUrls] = useState<{ [key: string]: string }>({}); // State to store Dexscreener URLs
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && item) {
|
||||
const fetchDetails = async () => {
|
||||
try {
|
||||
let urlAddition = "";
|
||||
if(period == "Weekly"){
|
||||
urlAddition = "_weekly";
|
||||
}else if(period == "Monthly"){
|
||||
urlAddition = "_monthly";
|
||||
let urlAddition = '';
|
||||
if (period === 'Weekly') {
|
||||
urlAddition = '_weekly';
|
||||
} else if (period === 'Monthly') {
|
||||
urlAddition = '_monthly';
|
||||
}
|
||||
const response = await fetch(`https://api.callfi.io/get_user_callouts${urlAddition}.php?tag=${item.username}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
const data = await response.json();
|
||||
const data: Detail[] = await response.json();
|
||||
setDetails(data);
|
||||
|
||||
// Fetch URLs for each detail
|
||||
const newUrls: { [key: string]: string } = {};
|
||||
for (const detail of data) {
|
||||
try {
|
||||
const response = await axios.get(`https://api.dexscreener.com/latest/dex/tokens/${detail.ca}`);
|
||||
if (response.data && response.data.pairs && response.data.pairs.length > 0) {
|
||||
newUrls[detail.ca] = response.data.pairs[0].url;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch URL for ${detail.ca}:`, error);
|
||||
}
|
||||
}
|
||||
setUrls(newUrls);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch user details:', error);
|
||||
}
|
||||
|
|
@ -34,7 +58,7 @@ const Modal: React.FC<{ isOpen: boolean; onClose: () => void; item: { username:
|
|||
|
||||
fetchDetails();
|
||||
}
|
||||
}, [isOpen, item]);
|
||||
}, [isOpen, item, period]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
|
|
@ -49,7 +73,14 @@ const Modal: React.FC<{ isOpen: boolean; onClose: () => void; item: { username:
|
|||
<div className="relative w-auto max-w-3xl mx-auto my-6 modal">
|
||||
<div className="relative flex flex-col border-0 rounded-lg shadow-lg outline-none focus:outline-none">
|
||||
<div className="flex items-start justify-between p-5 border-b border-solid rounded-t border-blueGray-200">
|
||||
<h3 className="text-3xl font-semibold">{item.username}s Callouts</h3>
|
||||
<div className="flex items-center">
|
||||
<img
|
||||
src={item.img_url}
|
||||
alt={`${item.username}'s profile`}
|
||||
className="w-12 h-12 rounded-full mr-4"
|
||||
/>
|
||||
<h3 className="text-3xl font-semibold">{item.username}s Callouts</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative p-6 flex-auto">
|
||||
<p
|
||||
|
|
@ -64,18 +95,30 @@ const Modal: React.FC<{ isOpen: boolean; onClose: () => void; item: { username:
|
|||
{details !== null ? (
|
||||
<div>
|
||||
{details.map((detail) => (
|
||||
<p
|
||||
<a
|
||||
key={detail.token}
|
||||
className={`my-4 text-lg leading-relaxed ${
|
||||
detail.gains > 0 ? 'text-green-500' : 'text-red-400'
|
||||
}`}
|
||||
href={urls[detail.ca] || '#'}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block"
|
||||
>
|
||||
{detail.token}: {parseFloat(detail.gains.toString()).toFixed(2)}x [
|
||||
{parseFloat(detail.price_at_creation.toString()).toFixed(8) +
|
||||
' => ' +
|
||||
parseFloat(detail.price_now.toString()).toFixed(8)}
|
||||
]
|
||||
</p>
|
||||
<p
|
||||
className={`my-4 text-lg leading-relaxed ${
|
||||
detail.gains > 0 ? 'text-green-500' : 'text-red-400'
|
||||
}`}
|
||||
>
|
||||
<img
|
||||
src={detail.icon_url}
|
||||
alt={`${detail.token} icon`}
|
||||
className="inline-block w-6 h-6 mr-2"
|
||||
/>
|
||||
{detail.token}: {parseFloat(detail.gains.toString()).toFixed(2)}x [
|
||||
{parseFloat(detail.price_at_creation.toString()).toFixed(8) +
|
||||
' => ' +
|
||||
parseFloat(detail.price_now.toString()).toFixed(8)}
|
||||
]
|
||||
</p>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
|
|
|
|||
69
app/page.tsx
69
app/page.tsx
|
|
@ -1,6 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import Link from 'next/link';
|
||||
import { SunIcon, MoonIcon } from "@heroicons/react/solid";
|
||||
import { PrivyProvider, usePrivy } from "@privy-io/react-auth";
|
||||
import { FaTwitter, FaWallet } from "react-icons/fa";
|
||||
|
|
@ -8,6 +9,10 @@ import { CheckIcon } from "@heroicons/react/outline";
|
|||
import Modal from './modal';
|
||||
import CalloutModal from './callout';
|
||||
import { motion } from 'framer-motion';
|
||||
import UserDetailModal from "@/components/UserDetailsModal";
|
||||
|
||||
import Header from "@/components/Header";
|
||||
import Footer from "@/components/Footer";
|
||||
|
||||
// Helper functions to generate pseudo data
|
||||
const generateRandomName = () => {
|
||||
|
|
@ -24,7 +29,7 @@ const generateRandomPoint = () => {
|
|||
return Math.floor(Math.random() * (100 - 10 + 1)) + 10;
|
||||
};
|
||||
|
||||
function Home() {
|
||||
const Home: React.FC = () => {
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const { login, user, ready, logout, linkTwitter, unlinkTwitter, linkWallet, unlinkWallet } = usePrivy();
|
||||
const [darkMode, setDarkMode] = useState(true);
|
||||
|
|
@ -35,6 +40,7 @@ function Home() {
|
|||
const [isJoined, setIsJoined] = useState(false);
|
||||
const [selectedItem, setSelectedItem] = useState(null);
|
||||
const [newCallModalOpen, setNewCallModalOpen] = useState(false);
|
||||
const [userDetailModalOpen, setUserDetailModalOpen] = useState(false);
|
||||
const [selectedPeriod, setSelectedPeriod] = useState('All Time');
|
||||
|
||||
const periods = ['All Time', 'Weekly', 'Monthly'];
|
||||
|
|
@ -59,8 +65,25 @@ function Home() {
|
|||
setNewCallModalOpen(false);
|
||||
};
|
||||
|
||||
const handleNewCallSubmit = (tokenId) => {
|
||||
const twitterIntentURL = `https://x.com/intent/tweet?text=%24${tokenId}%20is%20Booming%20rn%21%20See%20ya%20on%20the%20moon%21%0A%0A%23CallFi%20%23CallingIt`;
|
||||
// Function to handle opening user detail modal
|
||||
const openUserDetailModal = (username) => {
|
||||
setUserDetailModalOpen(true);
|
||||
setTimeout(() => {
|
||||
setModalOpen(false);
|
||||
// Additional logic if needed to fetch user details
|
||||
}, 10);
|
||||
};
|
||||
|
||||
// Function to handle closing user detail modal
|
||||
const closeUserDetailModal = () => {
|
||||
setUserDetailModalOpen(false);
|
||||
};
|
||||
|
||||
const handleNewCallSubmit = (tokenId, tokenContract, tokenName) => {
|
||||
let twitterIntentURL = `https://x.com/intent/tweet?text=%24${tokenId}%20is%20Booming%20rn%21%20See%20ya%20on%20the%20moon%21%0A%0A%23CallFi%20%23CallingIt`;
|
||||
if(tokenContract.length >0){
|
||||
twitterIntentURL = `https://x.com/intent/tweet?text=%24${tokenId}%20%23${tokenName}%20is%20Going%20to%20the%20moon%21%0ACA%20%3A${tokenContract}%0A%0A%23CallFi%20%23CallingIt`;
|
||||
}
|
||||
window.open(twitterIntentURL, '_blank');
|
||||
};
|
||||
|
||||
|
|
@ -158,36 +181,7 @@ function Home() {
|
|||
|
||||
return (
|
||||
<main className={`flex flex-col min-h-screen items-center justify-between text-white`}>
|
||||
<header className="w-full flex justify-between items-center p-4 bg-gray-800 text-white glassmorphism">
|
||||
<h1 className="text-xl font-bold">CallFi</h1>
|
||||
<div className="flex items-center">
|
||||
{ready && user ? (
|
||||
<div className="flex items-center bg-white bg-opacity-20 rounded-full px-4 py-2 glassmorphism">
|
||||
<button className={`relative ${twitterConnected ? "bg-black" : "bg-gray-700"} hover:bg-gray-500 text-white font-bold py-2 px-4 rounded-full mr-2 glassmorphism`} onClick={linkTwitter}>
|
||||
<FaTwitter className="h-5 w-5 text-white" />
|
||||
{twitterConnected && (
|
||||
<CheckIcon className="absolute top-0 right-0 h-4 w-4 text-green-500" />
|
||||
)}
|
||||
</button>
|
||||
<button className={`relative ${walletConnected ? "bg-black" : "bg-gray-700"} hover:bg-gray-500 text-white font-bold py-2 px-4 rounded-full mr-2 glassmorphism`} onClick={linkWallet}>
|
||||
<FaWallet className="h-5 w-5 text-white" />
|
||||
{walletConnected && (
|
||||
<CheckIcon className="absolute top-0 right-0 h-4 w-4 text-green-500" />
|
||||
)}
|
||||
</button>
|
||||
<button onClick={logout} className="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded glassmorphism">
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
) : !ready ? (
|
||||
<p>Loading...</p>
|
||||
) : (
|
||||
<button onClick={login} className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded glassmorphism">
|
||||
Login
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
<Header/>
|
||||
|
||||
{ready && user?.twitter && !isJoined && (
|
||||
<div className="note-card-container w-full p-4 flex flex-col items-center justify-center">
|
||||
|
|
@ -259,7 +253,7 @@ function Home() {
|
|||
className="w-12 h-12 rounded-full mr-4"
|
||||
/>
|
||||
<div>
|
||||
<p className="text-center font-bold">{item["username"]}</p>
|
||||
<Link href={`/users/${item["username"]}`}><p className="text-center font-bold">{item["username"]}</p></Link>
|
||||
<p className="text-center">{parseFloat(item["points"]).toFixed(2)}x</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -269,12 +263,11 @@ function Home() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<Modal isOpen={modalOpen} onClose={closeModal} item={selectedItem ?? { username: "", points: 2 }} period={selectedPeriod} />
|
||||
<Modal isOpen={modalOpen} onClose={closeModal} item={selectedItem ?? { username: "", points: 2, img_url: "" }} period={selectedPeriod} />
|
||||
<CalloutModal isOpen={newCallModalOpen} onClose={closeNewCallModal} onSubmit={handleNewCallSubmit} />
|
||||
<UserDetailModal isOpen={userDetailModalOpen} onClose={closeUserDetailModal} user={selectedItem ?? {username:""}} />
|
||||
|
||||
<footer className="w-full p-4 bg-gray-800 text-white text-center glassmorphism">
|
||||
<p>© 2024 CallFi. All rights reserved.</p>
|
||||
</footer>
|
||||
<Footer/>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
335
app/users/[id]/page.tsx
Normal file
335
app/users/[id]/page.tsx
Normal file
|
|
@ -0,0 +1,335 @@
|
|||
"use client";
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import axios from 'axios';
|
||||
import Header from '@/components/Header';
|
||||
import Footer from '@/components/Footer';
|
||||
import UserDetailModal from '@/components/UserDetailsModal';
|
||||
import { motion } from 'framer-motion';
|
||||
import CustomHeader from '@/components/CustomHeader';
|
||||
import { FaArrowDown, FaArrowUp, FaShare } from 'react-icons/fa';
|
||||
import { FaArrowLeft } from 'react-icons/fa6';
|
||||
import { NextSeo } from 'next-seo';
|
||||
import CalloutCard from '@/components/CalloutCard';
|
||||
import { PrivyProvider, usePrivy } from '@privy-io/react-auth';
|
||||
import ShareModal from '@/components/ShareModal';
|
||||
import Link from 'next/link';
|
||||
|
||||
function UserPage({ params }: { params: { id: string } }) {
|
||||
const { login, user, ready, logout, linkTwitter, unlinkTwitter, linkWallet, unlinkWallet } = usePrivy();
|
||||
|
||||
const [activeSelected, setActiveSelected] = useState(true);
|
||||
const [expandedInfo, setExpandedInfo] = useState(false);
|
||||
const [selectedPeriod, setSelectedPeriod] = useState("all time");
|
||||
const [userData, setUserData] = useState<any>(null);
|
||||
const [callouts, setCallouts] = useState<any[]>([]);
|
||||
const [history, setHistory] = useState<any[]>([]);
|
||||
const [loginAlert, setLoginAlert] = useState<string | null>(null);
|
||||
const [dexUrls, setDexUrls] = useState<{ [key: string]: string }>({});
|
||||
const [isShareModalOpen, setIsShareModalOpen] = useState(false);
|
||||
const [isFollowing, setIsFollowing] = useState(false);
|
||||
const [isFetchingFollowStatus, setIsFetchingFollowStatus] = useState(true);
|
||||
|
||||
const id = params.id;
|
||||
const currentUser = "%40" + user?.twitter?.username ?? "null"; // Change this to the current user dynamically
|
||||
|
||||
useEffect(() => {
|
||||
Refresh();
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchCallouts = async () => {
|
||||
let apiUrl = `https://api.callfi.io/get_user_callouts.php?tag=${id}`;
|
||||
if (selectedPeriod === "month") {
|
||||
apiUrl = `https://api.callfi.io/get_user_callouts_monthly.php?tag=${id}`;
|
||||
} else if (selectedPeriod === "week") {
|
||||
apiUrl = `https://api.callfi.io/get_user_callouts_weekly.php?tag=${id}`;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(apiUrl);
|
||||
const data = await response.json();
|
||||
setCallouts(data);
|
||||
|
||||
// Fetch Dexscreener URLs for each callout
|
||||
const urls: { [key: string]: string } = {};
|
||||
await Promise.all(data.map(async (detail: any) => {
|
||||
try {
|
||||
const response = await axios.get(`https://api.dexscreener.com/latest/dex/tokens/${detail.ca}`);
|
||||
if (response.data && response.data.pairs && response.data.pairs.length > 0) {
|
||||
urls[detail.ca] = response.data.pairs[0].url;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error fetching Dexscreener URL for ${detail.ca}:`, error);
|
||||
}
|
||||
}));
|
||||
setDexUrls(urls);
|
||||
} catch (error) {
|
||||
console.error('Error fetching callouts:', error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchCallouts();
|
||||
}, [id, selectedPeriod]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchHistory = async () => {
|
||||
let apiUrl = `https://api.callfi.io/get_callouts_history.php?username=${id}&period=all time`;
|
||||
if (selectedPeriod === "month") {
|
||||
apiUrl = `https://api.callfi.io/get_callouts_history.php?username=${id}&period=month`;
|
||||
} else if (selectedPeriod === "week") {
|
||||
apiUrl = `https://api.callfi.io/get_callouts_history.php?username=${id}&period=week`;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(apiUrl);
|
||||
const data = await response.json();
|
||||
setHistory(data);
|
||||
} catch (error) {
|
||||
console.error('Error fetching history:', error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchHistory();
|
||||
}, [id, selectedPeriod]);
|
||||
|
||||
useEffect(() => {
|
||||
Refresh();
|
||||
}, [currentUser, id]);
|
||||
|
||||
function Refresh() {
|
||||
const fetchFollowStatus = async () => {
|
||||
try {
|
||||
const response = await fetch(`https://api.callfi.io/get_is_following.php?username=${currentUser}&target=${id}`);
|
||||
const data = await response.json();
|
||||
setIsFollowing(data == "1");
|
||||
} catch (error) {
|
||||
console.error('Error fetching follow status:', error);
|
||||
} finally {
|
||||
setIsFetchingFollowStatus(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchFollowStatus();
|
||||
const fetchUserData = async () => {
|
||||
try {
|
||||
const response = await fetch(`https://api.callfi.io/get_user_data.php?username=${id}`);
|
||||
const data = await response.json();
|
||||
setUserData(data);
|
||||
} catch (error) {
|
||||
console.error('Error fetching user data:', error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchUserData();
|
||||
}
|
||||
|
||||
const handleFollowClick = async () => {
|
||||
if (!ready || !user) {
|
||||
setLoginAlert("You need to be logged in to follow.");
|
||||
setTimeout(() => setLoginAlert(null), 3000);
|
||||
} else {
|
||||
try {
|
||||
const response = await fetch(`https://api.callfi.io/${isFollowing ? "unfollow" : "follow"}_user.php?username=${currentUser}&target=${id}`);
|
||||
const data = await response.json();
|
||||
Refresh();
|
||||
} catch (error) {
|
||||
console.error('Error following user:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (!userData) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
const truncateAddress = (address: string) => {
|
||||
return `${address.slice(0, 6)}...${address.slice(-5)}`;
|
||||
};
|
||||
|
||||
const handleShareClick = () => {
|
||||
setIsShareModalOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<NextSeo title={`CallFi - ${userData.tag}'s profile`} />
|
||||
<main className='flex flex-col min-h-screen text-white'>
|
||||
<section className='glassmorphism'>
|
||||
<div className='grid grid-cols-3 gap-4 flex w-full flex-row p-3'>
|
||||
<Link href="/">
|
||||
<button className='mr-3'>
|
||||
<FaArrowLeft />
|
||||
</button>
|
||||
</Link>
|
||||
<h1 className='text-center'>CallFi Profile</h1>
|
||||
</div>
|
||||
|
||||
<div className='rounded-3xl lg:hidden'>
|
||||
<div className='grid grid-cols-3 flex-row p-2'>
|
||||
<img
|
||||
src={userData.img_url}
|
||||
className="rounded-full w-36 p-5"
|
||||
/>
|
||||
<div className='flex flex-col p-6 px-2'>
|
||||
<div>
|
||||
<p className='font-bold flex justify-start items-center text-xl font-bold'>{userData.tag}</p>
|
||||
|
||||
<p className='text-sm'>{userData.gains.toFixed(2)}x Gains</p>
|
||||
<p className='text-sm'>{(userData.points * 100).toFixed(2)} Points</p>
|
||||
<p className='text-sm'>{userData.followerCount} Followers</p>
|
||||
</div>
|
||||
<div className={expandedInfo ? "" : "hidden"}>
|
||||
<p className='text-sm'>{userData.calloutCount} Callouts</p>
|
||||
<p className='text-sm'>{parseFloat(userData.accuracy).toFixed(1)}% Accuracy</p>
|
||||
<p className='text-sm'>{userData.daysSinceJoined} Days</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex col-span-1 justify-end m-3 mb-6 items-end'>
|
||||
{expandedInfo ? (
|
||||
<button onClick={() => { setExpandedInfo(false) }}><FaArrowUp /></button>
|
||||
) : (
|
||||
<button onClick={() => { setExpandedInfo(true) }}><FaArrowDown /></button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='m-2 grid grid-cols-5 gap-2 px-2'>
|
||||
<button
|
||||
onClick={currentUser == id ? () => { } : handleFollowClick}
|
||||
className={`glassmorphism p-1 col-span-4 rounded-xl ${isFollowing ? "hover:bg-red-900" : "hover:bg-blue-900"}`}
|
||||
disabled={currentUser === id}
|
||||
>
|
||||
{currentUser == id ? 'My Account' : (isFollowing ? 'Unfollow' : 'Follow')}
|
||||
</button>
|
||||
<button onClick={handleShareClick} className='glassmorphism p-1 flex items-center justify-center rounded-xl hover:bg-blue-900'><FaShare /></button>
|
||||
</div>
|
||||
{loginAlert && <p className="text-red-500 text-center">{loginAlert}</p>}
|
||||
|
||||
</div>
|
||||
|
||||
<div className='rounded-3xl grid grid-cols-3 flex flex-row max-lg:hidden'>
|
||||
<div className='flex flex-row col-span-2'>
|
||||
<img
|
||||
src={userData.img_url}
|
||||
className="rounded-full w-24 ml-10"
|
||||
/>
|
||||
<p className='font-bold flex text-start justify-start items-center text-xl font-bold ml-2'>{userData.tag}</p>
|
||||
<div className='flex justify-center grid grid-rows-2 items-start ml-5'>
|
||||
|
||||
<button
|
||||
onClick={currentUser == id ? () => { } : handleFollowClick}
|
||||
className={`glassmorphism p-1 rounded-xl ${isFollowing ? "hover:bg-red-900" : "hover:bg-blue-900"}`}
|
||||
disabled={currentUser === id}
|
||||
>
|
||||
{currentUser == id ? 'My Account' : (isFollowing ? 'Unfollow' : 'Follow')}
|
||||
</button>
|
||||
<button onClick={handleShareClick} className='glassmorphism p-1 px-6 flex items-center justify-center rounded-xl hover:bg-blue-900'> Share <FaShare className='ml-2' /></button>
|
||||
</div>
|
||||
</div>
|
||||
<div className='items-center flex justify-center grid grid-cols-3 glassmorphism rounded-full items-center text-center mx-2'>
|
||||
<p className='text-sm'>{userData.gains.toFixed(2)}x Gains</p>
|
||||
<p className='text-sm'>{(userData.points * 100).toFixed(2)} Points</p>
|
||||
<p className='text-sm'>{userData.followerCount} Followers</p>
|
||||
<p className='text-sm'>{userData.calloutCount} Callouts</p>
|
||||
<p className='text-sm'>{parseFloat(userData.accuracy).toFixed(1)}% Accuracy</p>
|
||||
<p className='text-sm'>{userData.daysSinceJoined} Days</p>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div className='grid grid-cols-2 mt-4'>
|
||||
<button className={`justify-center text-center ${activeSelected ? "text-blue-300 bg-opacity-20 bg-black" : " text-sm text-gray-300"}`} onClick={() => { setActiveSelected(true) }}>
|
||||
<p>Active</p>
|
||||
</button>
|
||||
<button className={`justify-center text-center ${!activeSelected ? "text-blue-300 bg-opacity-20 bg-black" : " text-sm text-gray-300"}`} onClick={() => { setActiveSelected(false) }}>
|
||||
<p>History</p>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
<section className=''>
|
||||
<section className='max-lg:col-span-2'>
|
||||
<section className={`flex w-full ${activeSelected ? "" : "hidden"}`}>
|
||||
<section className="flex flex-col w-full m-2">
|
||||
<div className='flex justify-center p-4'>
|
||||
<div className='glassmorphism rounded-full flex p-1 text-sm'>
|
||||
<button className={`glassmorphism m-1 mx-1 p-1 px-5 rounded-full ${selectedPeriod === "all time" ? "glassmorphism-dark" : ""}`} onClick={() => { setSelectedPeriod("all time") }}>
|
||||
All Time
|
||||
</button>
|
||||
<button className={`glassmorphism m-1 mx-1 p-1 px-5 rounded-full ${selectedPeriod === "week" ? "glassmorphism-dark" : ""}`} onClick={() => { setSelectedPeriod("week") }}>
|
||||
Weekly
|
||||
</button>
|
||||
<button className={`glassmorphism m-1 mx-1 p-1 px-5 rounded-full ${selectedPeriod === "month" ? "glassmorphism-dark" : ""}`} onClick={() => { setSelectedPeriod("month") }}>
|
||||
Monthly
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<div className={`${activeSelected ? "" : "hidden"} flex flex-col justify-center items-center`}>
|
||||
{callouts ? callouts.map((callout) => (
|
||||
<CalloutCard
|
||||
key={callout.ca}
|
||||
iconUrl={callout.icon_url}
|
||||
token={callout.token}
|
||||
ca={callout.ca}
|
||||
priceAtCreation={callout.price_at_creation}
|
||||
priceNow={callout.price_now}
|
||||
gains={parseFloat(callout.gains)}
|
||||
dexUrl={dexUrls[callout.ca]} // Pass the Dexscreener URL to the CalloutCard
|
||||
/>
|
||||
)) : "Empty"}
|
||||
</div>
|
||||
|
||||
<section className={`flex w-full ${!activeSelected ? "" : "hidden"}`}>
|
||||
<section className="flex flex-col w-full m-2">
|
||||
<div className='flex justify-center p-4'>
|
||||
<div className='glassmorphism rounded-full w-min flex p-1 text-sm'>
|
||||
<button className={`glassmorphism m-1 mx-1 p-1 px-5 rounded-full ${selectedPeriod !== "month" ? "glassmorphism-dark" : ""}`} onClick={() => { setSelectedPeriod("week") }}>
|
||||
Weekly
|
||||
</button>
|
||||
<button className={`glassmorphism m-1 mx-1 p-1 px-5 rounded-full ${selectedPeriod === "month" ? "glassmorphism-dark" : ""}`} onClick={() => { setSelectedPeriod("month") }}>
|
||||
Monthly
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<div className={`${!activeSelected ? "" : "hidden"} flex flex-col justify-center items-center`}>
|
||||
{history ? history.map((entry) => (
|
||||
<CalloutCard
|
||||
key={entry.ca}
|
||||
iconUrl={entry.icon_url}
|
||||
token={entry.id}
|
||||
ca={entry.ca}
|
||||
priceAtCreation={entry.price_at_creation}
|
||||
priceNow={entry.froze_price}
|
||||
gains={parseFloat(entry.gains)}
|
||||
dexUrl={dexUrls[entry.ca]} // Pass the Dexscreener URL to the CalloutCard
|
||||
/>
|
||||
)) : "Empty"}
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
<div className='flex-1'></div>
|
||||
|
||||
<Footer />
|
||||
</main>
|
||||
<ShareModal
|
||||
isOpen={isShareModalOpen}
|
||||
onClose={() => setIsShareModalOpen(false)}
|
||||
shareLink={window.location.href} // Pass the current URL to the ShareModal
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function WrappedUserPage(props) {
|
||||
return (
|
||||
<PrivyProvider appId={process.env.NEXT_PUBLIC_PRIVY_APP_ID || 'clxc7nmy906ifhis11rkovoto'}>
|
||||
<UserPage {...props} />
|
||||
</PrivyProvider>
|
||||
);
|
||||
}
|
||||
92
app/users/page.tsx
Normal file
92
app/users/page.tsx
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
// pages/page.tsx
|
||||
"use client";
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import Link from 'next/link';
|
||||
import Header from '@/components/Header';
|
||||
import Footer from '@/components/Footer';
|
||||
import { PrivyProvider } from '@privy-io/react-auth';
|
||||
|
||||
interface User {
|
||||
username: string;
|
||||
total_gains: string;
|
||||
img_url: string;
|
||||
}
|
||||
|
||||
const Page: React.FC = () => {
|
||||
const [userData, setUserData] = useState<User[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const response = await fetch("https://api.callfi.io/get_user_list.php");
|
||||
if (!response.ok) {
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
const data = await response.json();
|
||||
setUserData(data);
|
||||
} catch (error: any) {
|
||||
setError(error.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Conditionally execute useEffect on the client side only
|
||||
if (typeof window !== 'undefined') {
|
||||
fetchData();
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Sort userData by total_gains before rendering
|
||||
const sortedUserData = [...userData].sort((a, b) => parseFloat(b.total_gains) - parseFloat(a.total_gains));
|
||||
|
||||
return (
|
||||
<>
|
||||
<main className="flex flex-col min-h-screen items-center justify-between text-white">
|
||||
<Header />
|
||||
<section className="w-full p-4 flex flex-col items-center justify-center">
|
||||
<h1 className="text-2xl font-bold mb-4">User Rankings</h1>
|
||||
{loading ? (
|
||||
<p>Loading...</p>
|
||||
) : error ? (
|
||||
<p>Error: {error}</p>
|
||||
) : (
|
||||
<ul className="w-full max-w-4xl">
|
||||
{sortedUserData.map((user) => (
|
||||
<motion.div key={user.username} whileHover={{ scale: 1.05, transition: { duration: 0.15 } }}>
|
||||
<Link href={`/users/${user.username}`}>
|
||||
<li className="leaderboard-card glassmorphism mx-auto mb-4 p-4 rounded-lg flex items-center">
|
||||
<div className="flex items-center">
|
||||
<img
|
||||
src={user.img_url}
|
||||
alt={`${user.username}'s profile`}
|
||||
className="w-12 h-12 rounded-full mr-4"
|
||||
/>
|
||||
<div className="flex flex-col items-center justify-center flex-1">
|
||||
<span className="text-center font-bold">{user.username}</span>
|
||||
<span className="text-center">Points: {(parseFloat(user.total_gains) * 100).toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</Link>
|
||||
</motion.div>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
<Footer />
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default function WrappedUsersPage(props) {
|
||||
return (
|
||||
<PrivyProvider appId={process.env.NEXT_PUBLIC_PRIVY_APP_ID || 'clxc7nmy906ifhis11rkovoto'}>
|
||||
<Page {...props} />
|
||||
</PrivyProvider>
|
||||
);
|
||||
}
|
||||
49
components/CalloutCard.tsx
Normal file
49
components/CalloutCard.tsx
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import React from 'react';
|
||||
|
||||
interface CalloutCardProps {
|
||||
iconUrl: string;
|
||||
token: string;
|
||||
ca: string;
|
||||
priceAtCreation: number;
|
||||
priceNow: number;
|
||||
gains: number;
|
||||
dexUrl: string;
|
||||
}
|
||||
|
||||
const CalloutCard: React.FC<CalloutCardProps> = ({
|
||||
iconUrl,
|
||||
token,
|
||||
ca,
|
||||
priceAtCreation,
|
||||
priceNow,
|
||||
gains,
|
||||
dexUrl
|
||||
}) => {
|
||||
const truncateAddress = (address: string) => {
|
||||
return `${address.slice(0, 6)}...${address.slice(-5)}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<a href={dexUrl} target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block">
|
||||
<div className={`glassmorphism rounded-full m-2 grid grid-cols-4 h-20 max-w-96 flex justify-center`}>
|
||||
<img src={iconUrl} className="rounded-full m-2 flex h-16 w-16" />
|
||||
<div className='grid grid-rows-2'>
|
||||
<div className='items-end justify-start text-center flex font-bold'>
|
||||
{token}
|
||||
</div>
|
||||
<p className='text-xs items-start mt-1 flex justify-start ml-1'>
|
||||
{truncateAddress(ca)}
|
||||
</p>
|
||||
</div>
|
||||
<div className='grid grid-rows-2 col-span-2 justify-end mx-5'>
|
||||
<p className='items-end flex text-xs text-nowrap mb-1'>{`$${priceAtCreation} => $${priceNow}`}</p>
|
||||
<p className={`justify-end flex 00 font-bold ${gains > 0 ? 'text-green-700' : 'text-red-700'}`}>{`${(gains * 100).toFixed(2)}%`}</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
export default CalloutCard;
|
||||
16
components/CustomHeader.tsx
Normal file
16
components/CustomHeader.tsx
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
|
||||
const CustomHeader: React.FC = () => {
|
||||
return (
|
||||
<header className="w-full flex justify-between items-center p-4 bg-gray-800 text-white glassmorphism">
|
||||
<div className="flex items-center">
|
||||
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
export default CustomHeader;
|
||||
13
components/Footer.tsx
Normal file
13
components/Footer.tsx
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
// components/Footer.tsx
|
||||
|
||||
import React from "react";
|
||||
|
||||
const Footer: React.FC = () => {
|
||||
return (
|
||||
<footer className="w-full p-4 bg-gray-800 text-white text-center glassmorphism">
|
||||
<p>© 2024 CallFi. All rights reserved.</p>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
||||
export default Footer;
|
||||
85
components/Header.tsx
Normal file
85
components/Header.tsx
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
// components/Header.tsx
|
||||
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import Link from 'next/link';
|
||||
import { FaTwitter, FaWallet } from "react-icons/fa";
|
||||
import { CheckIcon } from "@heroicons/react/outline";
|
||||
import { usePrivy } from "@privy-io/react-auth";
|
||||
|
||||
const Header: React.FC = () => {
|
||||
const { login, user, ready, logout, linkTwitter, unlinkTwitter, linkWallet, unlinkWallet } = usePrivy();
|
||||
const [twitterConnected, setTwitterConnected] = useState(false);
|
||||
const [walletConnected, setWalletConnected] = useState(false);
|
||||
const postLoginAPI = async (usertag) => {
|
||||
try {
|
||||
const response = await fetch('https://api.callfi.io/register_twitter_user.php?tag=' + usertag);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('API call successful:', data);
|
||||
setTwitterConnected(true);
|
||||
} catch (error) {
|
||||
console.error('Error during API call:', error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (ready && user) {
|
||||
const username = user.twitter?.username ? `@${user.twitter.username}` : '@unknownUser';
|
||||
if (username !== "@unknownUser") {
|
||||
postLoginAPI(username);
|
||||
}
|
||||
const walletStatus = user.wallet;
|
||||
if (walletStatus) {
|
||||
setWalletConnected(true);
|
||||
}
|
||||
}
|
||||
}, [ready, user]);
|
||||
|
||||
return (
|
||||
<header className="w-full flex justify-between items-center p-4 bg-gray-800 text-white glassmorphism">
|
||||
<div className="flex items-center">
|
||||
<Link href="/"><h1 className="text-xl font-bold px-10">CallFi</h1></Link>
|
||||
<nav className="flex space-x-4">
|
||||
<Link href="/users">
|
||||
<p className={` text-white hover:underline`}>User Ranking</p>
|
||||
</Link>
|
||||
</nav>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
{ready && user ? (
|
||||
<div className="flex items-center bg-white bg-opacity-20 rounded-full px-4 py-2 glassmorphism">
|
||||
<button className={`relative ${twitterConnected ? "bg-black" : "bg-gray-700"} hover:bg-gray-500 text-white font-bold py-2 px-4 rounded-full mr-2 glassmorphism`} onClick={linkTwitter}>
|
||||
<FaTwitter className="h-5 w-5 text-white" />
|
||||
{twitterConnected && (
|
||||
<CheckIcon className="absolute top-0 right-0 h-4 w-4 text-green-500" />
|
||||
)}
|
||||
</button>
|
||||
<button className={`relative ${walletConnected ? "bg-black" : "bg-gray-700"} hover:bg-gray-500 text-white font-bold py-2 px-4 rounded-full mr-2 glassmorphism`} onClick={linkWallet}>
|
||||
<FaWallet className="h-5 w-5 text-white" />
|
||||
{walletConnected && (
|
||||
<CheckIcon className="absolute top-0 right-0 h-4 w-4 text-green-500" />
|
||||
)}
|
||||
</button>
|
||||
<button onClick={logout} className="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded glassmorphism">
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
) : !ready ? (
|
||||
<p>Loading...</p>
|
||||
) : (
|
||||
<button onClick={login} className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded glassmorphism">
|
||||
Login
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
export default Header;
|
||||
60
components/ShareModal.tsx
Normal file
60
components/ShareModal.tsx
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
// components/ShareModal.tsx
|
||||
import { FacebookIcon, FacebookShareButton, RedditIcon, RedditShareButton, TelegramIcon, TelegramShareButton, TwitterIcon, TwitterShareButton } from 'next-share';
|
||||
import React from 'react';
|
||||
import { FaTimes } from 'react-icons/fa';
|
||||
|
||||
const ShareModal = ({ isOpen, onClose, shareLink }) => {
|
||||
if (!isOpen) return null;
|
||||
|
||||
const handleCopyLink = () => {
|
||||
navigator.clipboard.writeText(shareLink);
|
||||
//alert('Link copied to clipboard!');
|
||||
};
|
||||
|
||||
const shareIconSize =48;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-50">
|
||||
<div className="glassmorphism p-5 rounded-lg max-w-md w-96 m-2">
|
||||
<div className="flex justify-end text-white">
|
||||
<button onClick={onClose}>
|
||||
<FaTimes />
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-2 text-center">
|
||||
<h2 className="text-lg font-bold text-white">Share Profile</h2>
|
||||
<div className='flex grid grid-cols-4 ml-5 mt-7 mb-5'>
|
||||
<FacebookShareButton url={shareLink}>
|
||||
<FacebookIcon size={shareIconSize} round />
|
||||
</FacebookShareButton>
|
||||
<TwitterShareButton url={shareLink} >
|
||||
<TwitterIcon size={shareIconSize} round/>
|
||||
</TwitterShareButton>
|
||||
<RedditShareButton url={shareLink} >
|
||||
<RedditIcon size={shareIconSize} round/>
|
||||
</RedditShareButton>
|
||||
<TelegramShareButton url={shareLink} >
|
||||
<TelegramIcon size={shareIconSize} round/>
|
||||
</TelegramShareButton>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
readOnly
|
||||
value={shareLink}
|
||||
className="w-full mt-2 p-2 border border-gray-300 rounded glassmorphism text-white"
|
||||
/>
|
||||
<button
|
||||
onClick={handleCopyLink}
|
||||
className="mt-4 bg-blue-500 glassmorphism text-white py-2 px-4 rounded hover:bg-blue-700"
|
||||
>
|
||||
Copy Link
|
||||
</button>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShareModal;
|
||||
54
components/UserDetailsModal.tsx
Normal file
54
components/UserDetailsModal.tsx
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
// components/UserDetailModal.tsx
|
||||
|
||||
import React from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
interface UserDetailModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
user: any;
|
||||
}
|
||||
|
||||
const UserDetailModal: React.FC<UserDetailModalProps> = ({ isOpen, onClose, user }) => {
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 z-50 flex items-center justify-center overflow-x-hidden overflow-y-auto outline-none focus:outline-none modal-bg"
|
||||
onClick={onClose}
|
||||
>
|
||||
<div className="relative w-auto max-w-3xl mx-auto my-6 modal">
|
||||
<div className="relative flex flex-col border-0 rounded-lg shadow-lg outline-none focus:outline-none">
|
||||
<div className="flex items-start justify-between p-5 border-b border-solid rounded-t border-blueGray-200">
|
||||
<div className="flex items-center">
|
||||
<img
|
||||
src={user["img_url"]} // Assuming img_url is a valid image URL
|
||||
alt={`${user.username}'s profile`}
|
||||
className="w-12 h-12 rounded-full mr-4"
|
||||
/>
|
||||
<h3 className="text-3xl font-semibold">{user.username}</h3>
|
||||
</div>
|
||||
<button
|
||||
className="p-1 ml-auto bg-transparent border-0 text-black opacity-5 float-right text-3xl leading-none font-semibold outline-none focus:outline-none"
|
||||
onClick={onClose}
|
||||
>
|
||||
<span className="bg-transparent text-black opacity-5 h-6 w-6 text-2xl block outline-none focus:outline-none">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="relative p-6 flex-auto">
|
||||
<div>
|
||||
<p>Email: {user.email}</p>
|
||||
<p>Points: {user.points}</p>
|
||||
{/* Add more details as needed */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserDetailModal;
|
||||
49
package-lock.json
generated
49
package-lock.json
generated
|
|
@ -15,7 +15,10 @@
|
|||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"framer-motion": "^11.2.12",
|
||||
"lodash": "^4.17.21",
|
||||
"next": "14.2.4",
|
||||
"next-seo": "^6.5.0",
|
||||
"next-share": "^0.27.0",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"react-icons": "^5.2.1",
|
||||
|
|
@ -6154,6 +6157,27 @@
|
|||
"json5": "lib/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/jsonp": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/jsonp/-/jsonp-0.2.1.tgz",
|
||||
"integrity": "sha512-pfog5gdDxPdV4eP7Kg87M8/bHgshlZ5pybl+yKxAnCZ5O7lCIn7Ixydj03wOlnDQesky2BPyA91SQ+5Y/mNwzw==",
|
||||
"dependencies": {
|
||||
"debug": "^2.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/jsonp/node_modules/debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"dependencies": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jsonp/node_modules/ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
|
||||
},
|
||||
"node_modules/jsx-ast-utils": {
|
||||
"version": "3.3.5",
|
||||
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
|
||||
|
|
@ -6620,6 +6644,31 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/next-seo": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/next-seo/-/next-seo-6.5.0.tgz",
|
||||
"integrity": "sha512-MfzUeWTN/x/rsKp/1n0213eojO97lIl0unxqbeCY+6pAucViHDA8GSLRRcXpgjsSmBxfCFdfpu7LXbt4ANQoNQ==",
|
||||
"peerDependencies": {
|
||||
"next": "^8.1.1-canary.54 || >=9.0.0",
|
||||
"react": ">=16.0.0",
|
||||
"react-dom": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/next-share": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/next-share/-/next-share-0.27.0.tgz",
|
||||
"integrity": "sha512-Fmfl4LIL61g10vtiDECVldxtNgVNd4OQgnI5vk9IJxn/WMZLdhniUtV15xz4PGpo8UuDdTHwmDT8R/CP60EQHA==",
|
||||
"dependencies": {
|
||||
"jsonp": "^0.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8",
|
||||
"npm": ">=5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=17.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/next-tick": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz",
|
||||
|
|
|
|||
|
|
@ -16,7 +16,10 @@
|
|||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"framer-motion": "^11.2.12",
|
||||
"lodash": "^4.17.21",
|
||||
"next": "14.2.4",
|
||||
"next-seo": "^6.5.0",
|
||||
"next-share": "^0.27.0",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"react-icons": "^5.2.1",
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user