diff --git a/web/src/app/[cat]/InteractionClient.tsx b/web/src/app/[cat]/InteractionClient.tsx index 59515c27..5d1553c8 100644 --- a/web/src/app/[cat]/InteractionClient.tsx +++ b/web/src/app/[cat]/InteractionClient.tsx @@ -1,7 +1,7 @@ "use client"; import React, { useEffect, useState } from "react"; -import { Info, Coins, Settings, ArrowUpRight, ArrowDownRight, Unlock } from "lucide-react"; +import { Info, Coins, Settings, ArrowUpRight, ArrowDownRight, Unlock, AlertCircle, Loader2 } from "lucide-react"; import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"; import { getPublicClient } from "@wagmi/core"; import { config } from "@/utils/config"; @@ -13,6 +13,10 @@ import { Label } from "@/components/ui/label"; import { useAccount, useWriteContract, useWaitForTransactionReceipt } from "wagmi"; import { parseEther } from "viem"; import { showTransactionToast } from "@/components/ui/transaction-toast"; +import { motion } from "framer-motion"; +import Layout from "@/components/Layout"; +import { LoadingState } from "@/components/ui/loading-state"; +import { ButtonLoadingState } from "@/components/ui/button-loading-state"; // Define supported chain IDs type SupportedChainId = 1 | 137 | 534351 | 5115 | 61 | 2001; @@ -52,6 +56,9 @@ export default function InteractionClient() { timestamp: "", }); + // Add new state for transaction signing + const [isSigning, setIsSigning] = useState(false); + // Get vault address and chainId from URL parameters useEffect(() => { const vault = searchParams.get("vault"); @@ -229,78 +236,205 @@ export default function InteractionClient() { } }, [disableTransferRestrictionData, chainId]); + // Update the mint function + const handleMint = async () => { + try { + setIsSigning(true); + await mint({ + abi: CONTRIBUTION_ACCOUNTING_TOKEN_ABI, + address: tokenAddress, + functionName: "mint", + args: [address, parseEther(mintAmount)] + }); + } catch (error) { + console.error("Error minting tokens:", error); + showTransactionToast({ + hash: "0x0" as `0x${string}`, + chainId: chainId!, + success: false, + message: "Failed to mint tokens", + }); + } finally { + setIsSigning(false); + } + }; + + // Update the reduceMaxSupply function + const handleReduceMaxSupply = async () => { + try { + setIsSigning(true); + await reduceMaxSupply({ + abi: CONTRIBUTION_ACCOUNTING_TOKEN_ABI, + address: tokenAddress, + functionName: "reduceMaxSupply", + args: [parseEther(newMaxSupply)] + }); + } catch (error) { + console.error("Error reducing max supply:", error); + showTransactionToast({ + hash: "0x0" as `0x${string}`, + chainId: chainId!, + success: false, + message: "Failed to update max supply", + }); + } finally { + setIsSigning(false); + } + }; + + // Update the reduceThresholdSupply function + const handleReduceThresholdSupply = async () => { + try { + setIsSigning(true); + await reduceThresholdSupply({ + abi: CONTRIBUTION_ACCOUNTING_TOKEN_ABI, + address: tokenAddress, + functionName: "reduceThresholdSupply", + args: [parseEther(newThresholdSupply)] + }); + } catch (error) { + console.error("Error reducing threshold supply:", error); + showTransactionToast({ + hash: "0x0" as `0x${string}`, + chainId: chainId!, + success: false, + message: "Failed to update threshold supply", + }); + } finally { + setIsSigning(false); + } + }; + + // Update the reduceMaxExpansionRate function + const handleReduceMaxExpansionRate = async () => { + try { + setIsSigning(true); + await reduceMaxExpansionRate({ + abi: CONTRIBUTION_ACCOUNTING_TOKEN_ABI, + address: tokenAddress, + functionName: "reduceMaxExpansionRate", + args: [Number(newMaxExpansionRate) * 100] + }); + } catch (error) { + console.error("Error reducing max expansion rate:", error); + showTransactionToast({ + hash: "0x0" as `0x${string}`, + chainId: chainId!, + success: false, + message: "Failed to update max expansion rate", + }); + } finally { + setIsSigning(false); + } + }; + + // Update the disableTransferRestriction function + const handleDisableTransferRestriction = async () => { + try { + setIsSigning(true); + await disableTransferRestriction({ + abi: CONTRIBUTION_ACCOUNTING_TOKEN_ABI, + address: tokenAddress, + functionName: "disableTransferRestriction", + }); + } catch (error) { + console.error("Error disabling transfer restriction:", error); + showTransactionToast({ + hash: "0x0" as `0x${string}`, + chainId: chainId!, + success: false, + message: "Failed to disable transfer restriction", + }); + } finally { + setIsSigning(false); + } + }; + if (isLoading) { return ( -
-
- Loading token details... -
-
+ ); } if (error) { return ( -
-
{error}
-
+ ); } return ( -
-
+
+
{/* Header Section */}
-

+ {tokenDetails.tokenSymbol} Token Management -

+ + + {tokenDetails.tokenName} +
{/* Token Overview Card */} - + - - + + Token Overview
-
+
- -

Max Supply

+ +

Max Supply

-

+

{tokenDetails.maxSupply}

-
+
- -

Threshold Supply

+ +

Threshold Supply

-

+

{tokenDetails.thresholdSupply}

-
+
- -

Max Expansion Rate

+ +

Max Expansion Rate

-

+

{tokenDetails.maxExpansionRate}%

-
+
- -

Contract Address

+ +

Contract Address

-

+

{tokenDetails.transactionHash}

@@ -308,47 +442,46 @@ export default function InteractionClient() { {/* Mint Tokens Card */} - + - - + + Mint Tokens
- + setMintAmount(e.target.value)} - className="h-12 text-lg" + className="h-12 text-lg bg-white/60 dark:bg-[#1a1400]/70 border-2 border-gray-200 dark:border-yellow-400/20 text-gray-600 dark:text-yellow-200" />
{/* Admin Functions Card */} - + - - + + Admin Functions @@ -356,95 +489,86 @@ export default function InteractionClient() {
- + setNewMaxSupply(e.target.value)} - className="h-12 text-lg" + className="h-12 text-lg bg-white/60 dark:bg-[#1a1400]/70 border-2 border-gray-200 dark:border-yellow-400/20 text-gray-600 dark:text-yellow-200" />
- + setNewThresholdSupply(e.target.value)} - className="h-12 text-lg" + className="h-12 text-lg bg-white/60 dark:bg-[#1a1400]/70 border-2 border-gray-200 dark:border-yellow-400/20 text-gray-600 dark:text-yellow-200" />
- + setNewMaxExpansionRate(e.target.value)} - className="h-12 text-lg" + className="h-12 text-lg bg-white/60 dark:bg-[#1a1400]/70 border-2 border-gray-200 dark:border-yellow-400/20 text-gray-600 dark:text-yellow-200" />
{transferRestricted ? (
- +
diff --git a/web/src/app/create/page.tsx b/web/src/app/create/page.tsx index b959f512..018172a9 100644 --- a/web/src/app/create/page.tsx +++ b/web/src/app/create/page.tsx @@ -20,9 +20,12 @@ import { CardTitle, CardDescription, } from "@/components/ui/card"; -import { Info, Loader2 } from "lucide-react"; +import { Info, Loader2, ArrowLeft } from "lucide-react"; import { motion } from "framer-motion"; import { showTransactionToast } from "@/components/ui/transaction-toast"; +import Link from "next/link"; +import { LoadingState } from "@/components/ui/loading-state"; +import { ButtonLoadingState } from "@/components/ui/button-loading-state"; interface DeployContractProps { tokenName: string; @@ -32,6 +35,13 @@ interface DeployContractProps { maxExpansionRate: string; } +interface FieldValidation { + [key: string]: { + isValid: boolean; + errorMessage: string; + }; +} + const fields = [ { id: "tokenName", @@ -39,6 +49,10 @@ const fields = [ type: "text", placeholder: "My Token", description: "The name of your token", + validate: (value: string) => ({ + isValid: value.length >= 3 && value.length <= 32, + errorMessage: "Token name must be between 3 and 32 characters" + }) }, { id: "tokenSymbol", @@ -46,6 +60,10 @@ const fields = [ type: "text", placeholder: "TKN", description: "A short identifier for your token (2-4 characters)", + validate: (value: string) => ({ + isValid: /^[A-Z]{2,4}$/.test(value), + errorMessage: "Symbol must be 2-4 uppercase letters" + }) }, { id: "maxSupply", @@ -53,6 +71,10 @@ const fields = [ type: "number", placeholder: "1000000", description: "The maximum number of tokens that can exist", + validate: (value: string) => ({ + isValid: /^\d+$/.test(value) && parseInt(value) > 0, + errorMessage: "Maximum supply must be a positive number" + }) }, { id: "thresholdSupply", @@ -60,6 +82,12 @@ const fields = [ type: "number", placeholder: "500000", description: "The supply threshold that triggers expansion", + validate: (value: string, formData: DeployContractProps) => ({ + isValid: /^\d+$/.test(value) && + parseInt(value) > 0 && + parseInt(value) < parseInt(formData.maxSupply), + errorMessage: "Threshold must be a positive number less than maximum supply" + }) }, { id: "maxExpansionRate", @@ -67,6 +95,12 @@ const fields = [ type: "number", placeholder: "5", description: "Maximum percentage the supply can expand (1-100)", + validate: (value: string) => ({ + isValid: /^\d+$/.test(value) && + parseInt(value) >= 1 && + parseInt(value) <= 100, + errorMessage: "Expansion rate must be between 1 and 100" + }) }, ]; @@ -79,6 +113,9 @@ export default function CreateCAT() { maxExpansionRate: "", }); const [isDeploying, setIsDeploying] = useState(false); + const [validation, setValidation] = useState({}); + const [showInfo, setShowInfo] = useState<{ [key: string]: boolean }>({}); + const [isSigning, setIsSigning] = useState(false); const { address, chainId } = useAccount(); const router = useRouter(); @@ -102,6 +139,7 @@ export default function CreateCAT() { const deployContract = async () => { try { setIsDeploying(true); + setIsSigning(true); const chainId = config.state.chainId; if (!ClowderVaultFactories[chainId]) { toast.error("Contract factory instance not available"); @@ -120,7 +158,7 @@ export default function CreateCAT() { const formattedThresholdSupply = BigInt(thresholdSupply) * BigInt(1e18); const formattedMaxExpansionRate = BigInt(maxExpansionRate) * BigInt(100); - deployCAT({ + await deployCAT({ address: ClowderVaultFactories[chainId], abi: CAT_FACTORY_ABI, functionName: "createCAT", @@ -141,6 +179,7 @@ export default function CreateCAT() { message: "Failed to deploy CAT contract", }); setIsDeploying(false); + setIsSigning(false); } }; @@ -168,127 +207,165 @@ export default function CreateCAT() { }, [deployData, formData, router]); const handleChange = (e: React.ChangeEvent) => { - setFormData({ - ...formData, - [e.target.name]: e.target.value, - }); + const { name, value } = e.target; + setFormData(prev => ({ + ...prev, + [name]: value + })); + + // Validate the field + const field = fields.find(f => f.id === name); + if (field?.validate) { + const validationResult = field.validate(value, formData); + setValidation(prev => ({ + ...prev, + [name]: validationResult + })); + } + }; + + const toggleInfo = (fieldId: string) => { + setShowInfo(prev => ({ + ...prev, + [fieldId]: !prev[fieldId] + })); }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - await deployContract(); + + // Validate all fields before submission + const newValidation: FieldValidation = {}; + let isValid = true; + + fields.forEach(field => { + if (field.validate) { + const result = field.validate(formData[field.id as keyof DeployContractProps], formData); + newValidation[field.id] = result; + if (!result.isValid) isValid = false; + } + }); + + setValidation(newValidation); + + if (isValid) { + await deployContract(); + } }; return ( -
- - - - +
+
+ {isDeploying || isDeployingTx ? ( + + ) : ( + + + + + Back to My CATs + + +

Create CAT - - - Deploy a new Contribution Accounting Token - - - +

{!address ? ( - -

- Connect your wallet to create a new CAT +

+

+ Connect your wallet to create a CAT

-
- +
+
- - ) : chainId && !ClowderVaultFactories[chainId] ? ( -
-

⚠ Please switch to a supported network.

-

- If you would like support for another network, contact us on{" "} - - Discord - - . -

) : (
- {fields.map( - ({ id, label, type, placeholder, description }, index) => ( - ( +
+ +
- - ) - )} - + +
+ {(showInfo[field.id] || validation[field.id]?.isValid === false) && ( +

+ {validation[field.id]?.isValid === false + ? validation[field.id]?.errorMessage + : field.description} +

+ )} +
+ ))} +
+ - +
)} - - -
+ + )} +
); diff --git a/web/src/app/globals.css b/web/src/app/globals.css index dd02d5a5..45cdb7d2 100644 --- a/web/src/app/globals.css +++ b/web/src/app/globals.css @@ -49,6 +49,7 @@ body { body { @apply bg-background text-foreground; + font-family: 'Inter', sans-serif; } h1, @@ -57,7 +58,8 @@ body { h4, h5, h6 { - font-family: "Arial", sans-serif; + font-family: 'Inter', sans-serif; + @apply font-bold tracking-tight; } } @@ -75,4 +77,44 @@ body { html { scroll-behavior: smooth; +} + +/* Fix spacing issues */ +main { + @apply min-h-[calc(100vh-4rem)] pt-16 pb-8; +} + +/* Modern scrollbar */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + @apply bg-gray-100 dark:bg-gray-800; +} + +::-webkit-scrollbar-thumb { + @apply bg-gray-300 dark:bg-gray-600 rounded-full; +} + +::-webkit-scrollbar-thumb:hover { + @apply bg-gray-400 dark:bg-gray-500; +} + +/* Animations */ +@keyframes float { + 0% { + transform: translateY(0px); + } + 50% { + transform: translateY(-10px); + } + 100% { + transform: translateY(0px); + } +} + +.animate-float { + animation: float 3s ease-in-out infinite; } \ No newline at end of file diff --git a/web/src/app/layout.tsx b/web/src/app/layout.tsx index af2c2e9b..79a17be4 100644 --- a/web/src/app/layout.tsx +++ b/web/src/app/layout.tsx @@ -68,7 +68,7 @@ export default function RootLayout({ diff --git a/web/src/app/my-cats/page.tsx b/web/src/app/my-cats/page.tsx index c01cb199..3e8ce7fb 100644 --- a/web/src/app/my-cats/page.tsx +++ b/web/src/app/my-cats/page.tsx @@ -11,12 +11,23 @@ import { CAT_FACTORY_ABI } from "@/contractsABI/CatFactoryABI"; import detectEthereumProvider from "@metamask/detect-provider"; import { CONTRIBUTION_ACCOUNTING_TOKEN_ABI } from "@/contractsABI/ContributionAccountingTokenABI"; import { motion } from "framer-motion"; -import { Loader2, AlertCircle } from "lucide-react"; +import { Loader2, AlertCircle, Plus, Search, Filter } from "lucide-react"; import { showTransactionToast } from "@/components/ui/transaction-toast"; +import { LoadingState } from "@/components/ui/loading-state"; // Define supported chain IDs type SupportedChainId = 1 | 137 | 534351 | 5115 | 61 | 2001; +// Chain ID to name mapping +const CHAIN_NAMES: Record = { + 1: "Ethereum", + 137: "Polygon", + 534351: "Scroll Sepolia", + 5115: "Citrea", + 61: "Ethereum Classic", + 2001: "Milkomeda" +}; + interface CatDetails { chainId: SupportedChainId; address: `0x${string}`; @@ -36,6 +47,8 @@ export default function MyCATsPage() { const [ownedCATs, setOwnedCATs] = useState(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); + const [searchQuery, setSearchQuery] = useState(""); + const [selectedChainId, setSelectedChainId] = useState("all"); const { address } = useAccount(); const { writeContract: fetchCATs, data: fetchData } = useWriteContract(); @@ -158,89 +171,214 @@ export default function MyCATsPage() { } }, [address]); + // Filter and search function + const filteredCATs = ownedCATs?.filter((cat) => { + const matchesSearch = searchQuery === "" || + cat.tokenName.toLowerCase().includes(searchQuery.toLowerCase()) || + cat.tokenSymbol.toLowerCase().includes(searchQuery.toLowerCase()); + + const matchesChain = selectedChainId === "all" || cat.chainId === Number(selectedChainId); + + return matchesSearch && matchesChain; + }); + return ( -
-
- +
+ - My CATs - - {isLoading ? ( -
- - - Loading your CATs... - -
- ) : error ? ( - - -

{error}

-
- ) : ownedCATs?.length ? ( - - {ownedCATs.map((cat) => ( - + + My CATs + + + - -

- {cat.tokenName || cat.address} -

-

- Symbol: {cat.tokenSymbol} -

-
- - Chain: {cat.chainId} - - - View Details → - -
- -
- ))} -
- ) : ( - + Create New CAT + + +
+ + {/* Search and Filter Section */} + -

- You don't own any CATs yet. -

- - Create a CAT - +
+
+ +
+ setSearchQuery(e.target.value)} + className="w-full pl-10 pr-4 py-3 rounded-xl bg-white/80 dark:bg-[#1a1400]/70 border border-[#bfdbfe] dark:border-yellow-400/20 text-gray-800 dark:text-yellow-100 placeholder-gray-500 dark:placeholder-yellow-200/50 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-yellow-400 focus:border-transparent transition-all duration-300" + /> +
+
+
+ +
+ +
+ + + +
+
- )} + +
+ {isLoading ? ( + + ) : error ? ( + + ) : filteredCATs?.length ? ( + + {filteredCATs.map((cat, index) => ( + +
+ +
+
+
+
+ {cat.tokenSymbol.slice(0, 2)} +
+
+

+ {cat.tokenName || cat.address} +

+

+ {cat.tokenSymbol} +

+
+
+ + + +
+ +
+
+ Chain ID + {cat.chainId} +
+ +
+

Contract Address

+

+ {cat.address} +

+
+
+ + + + Manage CAT + + +
+
+
+ ))} +
+ ) : ( + +
+
+ +
+

+ No CATs Found +

+

+ {searchQuery || selectedChainId !== "all" + ? "No CATs match your search criteria" + : "Start by creating your first Contribution Accounting Token"} +

+
+ {!searchQuery && selectedChainId === "all" && ( + + + + Create Your First CAT + + + )} +
+ )} +
+
diff --git a/web/src/app/page.tsx b/web/src/app/page.tsx index 5a92b2c6..477f84fa 100644 --- a/web/src/app/page.tsx +++ b/web/src/app/page.tsx @@ -17,9 +17,10 @@ import { useTheme } from "next-themes" import { faGithub, faDiscord, faTelegram, faXTwitter} from "@fortawesome/free-brands-svg-icons" import { useAccount } from "wagmi" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" -import { motion } from "framer-motion" +import { motion, AnimatePresence, type MotionProps } from "framer-motion" import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog" import { showTransactionToast } from "@/components/ui/transaction-toast" +import { config } from "@/utils/config" // import { config } from "@/utils/config" const services = [ @@ -93,195 +94,253 @@ export default function Home() { return ( -
+
{/* Hero Section */} + {/* Background Effects */} +
+
+
+
+
+ -

+

Welcome to Clowder

-
+ + Create Contribution Accounting Tokens (CATs)
+ to track contributions to your projects. +
- - Create Contribution Accounting Tokens (CATs)
- to track contributions to your projects. -
- - {contact_links.map(({ href, icon }, index) => ( - - - - ))} - - - {!isWalletConnected ? ( -
- -
- ) : ( -
- - - -
- )} + +
+ ) : ( + <> + + + + + + + + + + + )} +
{/* Services Section */} -

- Why CATs? -

-
- {services.map((service, index) => ( - - {service.alt} -

{service.description}

-
- ))} +
+

+ Why CATs? +

+
+ {services.map((service, index) => ( + +
+ {service.alt} +

+ {service.description} +

+
+
+ ))} +
- {/* Contact Us Section */} + {/* About Us Section */} -

- About Us -

-
- {/* Contact Info */} - + -

- Clowder was developed by
- The Stable Order
- within the Stability Nexus. -

-
-

Contact us through:

-
- {contact_links.map(({ href, icon }, index) => ( - - - - ))} -
-
+ About Us + +
+ +

+ Clowder was developed by
+ The Stable Order
+ within the Stability Nexus. +

+
+

+ Contact us through: +

+
+ {contact_links.map(({ href, icon }, index) => ( + + + + ))} +
+
- {/* Right Content */} - - Clowder Contact - + +
+ Clowder Contact +
+
+
{/* Use CAT Dialog */} - + - + Use Existing CAT - + Enter the CAT address and select the network to interact with your token.
-
-