diff --git a/app/test/page.tsx b/app/test/page.tsx index 26375b9fc..15141865c 100644 --- a/app/test/page.tsx +++ b/app/test/page.tsx @@ -5,12 +5,24 @@ import LaunchCampaignFlow from '@/components/project/LaunchCampaignFlow'; import BoundlessSheet from '@/components/sheet/boundless-sheet'; import { Button } from '@/components/ui/button'; import { Rocket } from 'lucide-react'; +import { useWalletProtection } from '@/hooks/use-wallet-protection'; +import WalletRequiredModal from '@/components/wallet/WalletRequiredModal'; export default function TestPage() { const [showLaunchFlow, setShowLaunchFlow] = useState(false); + // Wallet protection hook + const { + requireWallet, + showWalletModal, + handleWalletConnected, + closeWalletModal, + } = useWalletProtection({ + actionName: 'test launch campaign', + }); + const handleOpenModal = () => { - setShowLaunchFlow(true); + requireWallet(() => setShowLaunchFlow(true)); }; const handleCloseModal = () => { @@ -55,6 +67,14 @@ export default function TestPage() { onComplete={handleCloseModal} /> + + {/* Wallet Required Modal */} + ); diff --git a/app/user/backing-history/page.tsx b/app/user/backing-history/page.tsx deleted file mode 100644 index fb9b2c440..000000000 --- a/app/user/backing-history/page.tsx +++ /dev/null @@ -1,110 +0,0 @@ -'use client'; - -import { useState } from 'react'; -import { Button } from '@/components/ui/button'; -// import BackingHistory from '@/components/flows/backing-history/index'; -import BackingHistory from '@/components/flows/backing-history/backing-history'; - -// Sample data matching the images -const sampleBackers = [ - { - id: '1', - name: 'Collins Odumeje', - avatar: '/placeholder.svg?height=32&width=32', - amount: 2300, - date: new Date('2025-08-05'), - walletId: 'GDS3...GB7', - isAnonymous: false, - }, - { - id: '2', - name: 'Collins Odumeje', - avatar: '/placeholder.svg?height=32&width=32', - amount: 2300, - date: new Date('2025-08-05'), - walletId: 'GDS3...GB7', - isAnonymous: false, - }, - { - id: '3', - name: 'Collins Odumeje', - avatar: '/placeholder.svg?height=32&width=32', - amount: 2300, - date: new Date('2025-08-05'), - walletId: 'GDS3...GB7', - isAnonymous: false, - }, - { - id: '4', - name: 'Anonymous', - amount: 2300, - date: new Date('2025-08-05'), - walletId: 'GDS3...GB7', - isAnonymous: true, - }, - { - id: '5', - name: 'Collins Odumeje', - avatar: '/placeholder.svg?height=32&width=32', - amount: 2300, - date: new Date('2025-08-05'), - walletId: 'GDS3...GB7', - isAnonymous: false, - }, - { - id: '6', - name: 'Anonymous', - amount: 2300, - date: new Date('2025-08-05'), - walletId: 'GDS3...GB7', - isAnonymous: true, - }, - { - id: '7', - name: 'Anonymous', - amount: 2300, - date: new Date('2025-08-05'), - walletId: 'GDS3...GB7', - isAnonymous: true, - }, - { - id: '8', - name: 'Collins Odumeje', - avatar: '/placeholder.svg?height=32&width=32', - amount: 2300, - date: new Date('2025-08-05'), - walletId: 'GDS3...GB7', - isAnonymous: false, - }, - { - id: '9', - name: 'Anonymous', - amount: 2300, - date: new Date('2025-08-05'), - walletId: 'GDS3...GB7', - isAnonymous: true, - }, -]; - -export default function Home() { - const [showBackingHistory, setShowBackingHistory] = useState(false); - - return ( -
-
- - - -
-
- ); -} diff --git a/app/user/page.tsx b/app/user/page.tsx index c412a6f8b..46f18f1d4 100644 --- a/app/user/page.tsx +++ b/app/user/page.tsx @@ -7,8 +7,7 @@ import { Coins, History } from 'lucide-react'; import { useAuth } from '@/hooks/use-auth'; import CampaignTable from '@/components/campaigns/CampaignTable'; import { useEffect, useState } from 'react'; - -import LoadingSpinner from '@/components/LoadingSpinner'; +import { UserPageSkeleton } from '@/components/skeleton/UserPageSkeleton'; export default function UserPage() { const { user, isLoading } = useAuth(); @@ -20,11 +19,7 @@ export default function UserPage() { // Show loading until client-side hydration is complete if (!mounted || isLoading) { - return ( -
- -
- ); + return ; } return ( diff --git a/app/user/projects/page.tsx b/app/user/projects/page.tsx index 878406bfc..9b1e0a31e 100644 --- a/app/user/projects/page.tsx +++ b/app/user/projects/page.tsx @@ -1,20 +1,30 @@ 'use client'; import PageTransition from '@/components/PageTransition'; import Projects from '@/components/Projects'; -import React from 'react'; +import React, { useEffect, useState } from 'react'; +import { ProjectsPageSkeleton } from '@/components/skeleton/ProjectsSkeleton'; + +const Page = () => { + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + // Show loading until client-side hydration is complete + if (!mounted) { + return ; + } -const page = () => { return (
- {/*
*/} - - {/*
*/} +
-
+ ); }; -export default page; +export default Page; diff --git a/components/Projects.tsx b/components/Projects.tsx index 012d1a20b..f86796acb 100644 --- a/components/Projects.tsx +++ b/components/Projects.tsx @@ -1,7 +1,7 @@ 'use client'; import React, { useState, useEffect, useCallback } from 'react'; import { RecentProjectsProps } from '@/types/project'; -import { Plus, ChevronDown, Loader2 } from 'lucide-react'; +import { Plus, ChevronDown } from 'lucide-react'; import ProjectCard from './project-card'; import EmptyState from './EmptyState'; import { BoundlessButton } from './buttons'; @@ -20,6 +20,7 @@ import { getProjects } from '@/lib/api/project'; import { toast } from 'sonner'; import { useAuth } from '@/hooks/use-auth'; import { useProjectSheetStore } from '@/lib/stores/project-sheet-store'; +import { ProjectsSkeleton } from './skeleton/ProjectsSkeleton'; type StatusFilter = | 'all' @@ -93,6 +94,19 @@ const Projects = () => { fetchProjects(); }, [fetchProjects]); + if (loading) { + return ( + + + + ); + } + return ( {

{tabFilter === 'mine' ? 'My Projects' : 'All Projects'}

-
{ className='grid grid-cols-1 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols- xl:grid-cols-3 gap-4 sm:gap-6' variants={staggerContainer} > - {loading ? ( - -
-
- - Loading projects... -
-
-
- ) : error ? ( + {error ? (
@@ -268,7 +272,6 @@ const Projects = () => { iconPosition='right' onClick={() => { sheet.openInitialize(); - }} > New Project. diff --git a/components/auth/LoginForm.tsx b/components/auth/LoginForm.tsx index d155dd094..ecf184f0d 100644 --- a/components/auth/LoginForm.tsx +++ b/components/auth/LoginForm.tsx @@ -92,6 +92,7 @@ const LoginForm = () => { 'Login successful but session not found. Please try again.', }); } + router.push(callbackUrl); } else { form.setError('root', { type: 'manual', diff --git a/components/campaigns/CampaignTable.tsx b/components/campaigns/CampaignTable.tsx index 08efed3f9..2276f3f66 100644 --- a/components/campaigns/CampaignTable.tsx +++ b/components/campaigns/CampaignTable.tsx @@ -1,11 +1,6 @@ import React, { useState, useEffect, useCallback } from 'react'; import { Button } from '../ui/button'; -import { - MoreVerticalIcon, - CheckIcon, - ChevronDownIcon, - Loader2Icon, -} from 'lucide-react'; +import { MoreVerticalIcon, CheckIcon, ChevronDown } from 'lucide-react'; import { Tabs, TabsList, TabsTrigger } from '../ui/tabs'; import { DropdownMenu, @@ -27,6 +22,7 @@ import { } from '@/lib/data/campaigns-mock'; import BackingHistory from './backing-history'; import { sampleBackers } from '@/lib/data/backing-history-mock'; +import { CampaignTableSkeleton } from '../skeleton/UserPageSkeleton'; const CampaignRow = ({ campaign, @@ -518,6 +514,14 @@ const CampaignTable = () => { window.scrollTo({ top: 0, behavior: 'smooth' }); }; + if (loading) { + return ( +
+ +
+ ); + } + return (
@@ -566,7 +570,7 @@ const CampaignTable = () => { filterOptions.find(option => option.value === statusFilter) ?.label }{' '} - + {
- {loading ? ( -
-
- - Loading campaigns... -
-
- ) : error ? ( + {error ? (

{error}

diff --git a/components/campaigns/LaunchCampaignFlow.tsx b/components/campaigns/LaunchCampaignFlow.tsx index 2688de929..b3f930e74 100644 --- a/components/campaigns/LaunchCampaignFlow.tsx +++ b/components/campaigns/LaunchCampaignFlow.tsx @@ -24,6 +24,8 @@ import { } from '@/components/ui/select'; import { BoundlessButton } from '../buttons'; import { Checkbox } from '../ui/checkbox'; +import { useWalletProtection } from '@/hooks/use-wallet-protection'; +import WalletRequiredModal from '@/components/wallet/WalletRequiredModal'; interface LaunchCampaignFlowProps { onComplete: () => void; @@ -60,6 +62,16 @@ const LaunchCampaignFlow: React.FC = ({ const [currentPhase, setCurrentPhase] = useState<'details' | 'escrow'>( 'details' ); + + // Wallet protection hook + const { + requireWallet, + showWalletModal, + handleWalletConnected, + closeWalletModal, + } = useWalletProtection({ + actionName: 'launch campaign', + }); const [formData, setFormData] = useState({ title: 'Boundless', description: @@ -280,9 +292,17 @@ const LaunchCampaignFlow: React.FC = ({ escrowTerms={escrowTerms} isEscrowValid={isEscrowValid} onBack={handleBackPhase} - onComplete={onComplete} + onComplete={() => requireWallet(onComplete)} /> )} + + {/* Wallet Required Modal */} +
); }; diff --git a/components/connect-wallet/index.tsx b/components/connect-wallet/index.tsx index 77bbd4d8f..80e079220 100644 --- a/components/connect-wallet/index.tsx +++ b/components/connect-wallet/index.tsx @@ -19,9 +19,11 @@ import { Tooltip, TooltipTrigger } from '../ui/tooltip'; const ConnectWallet = ({ open, onOpenChange, + onConnect, }: { open: boolean; onOpenChange: (open: boolean) => void; + onConnect?: () => void; }) => { const [selectedNetwork, setSelectedNetwork] = useState('testnet'); const [acceptedTerms, setAcceptedTerms] = useState(true); @@ -107,6 +109,7 @@ const ConnectWallet = ({ description: `Connected to ${network === 'testnet' ? 'Testnet' : 'Public'} network`, }); onOpenChange(false); + onConnect?.(); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Failed to connect wallet'; diff --git a/components/overview/RecentProjects.tsx b/components/overview/RecentProjects.tsx index 5d839118f..d0e2b7cff 100644 --- a/components/overview/RecentProjects.tsx +++ b/components/overview/RecentProjects.tsx @@ -1,7 +1,7 @@ 'use client'; import React, { useState, useEffect, useCallback } from 'react'; import { RecentProjectsProps, Project } from '@/types/project'; -import { Plus, ChevronRight, ChevronDown, Loader2 } from 'lucide-react'; +import { Plus, ChevronRight, ChevronDown } from 'lucide-react'; import ProjectCard from '../project-card'; import EmptyState from '../EmptyState'; import { BoundlessButton } from '../buttons'; @@ -19,6 +19,7 @@ import Link from 'next/link'; import { getProjects } from '@/lib/api/project'; import { toast } from 'sonner'; import { useAuth } from '@/hooks/use-auth'; +import { RecentProjectsSkeleton } from '../skeleton/UserPageSkeleton'; type StatusFilter = | 'all' @@ -118,6 +119,19 @@ const RecentProjects = () => { fetchProjects(); }, [fetchProjects]); + if (loading) { + return ( + + + + ); + } + return ( { className='grid grid-cols-1 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols- xl:grid-cols-3 gap-4 sm:gap-6' variants={staggerContainer} > - {loading ? ( - -
-
- - Loading projects... -
-
-
- ) : error ? ( + {error ? (
diff --git a/components/project-card.tsx b/components/project-card.tsx index e9995591d..8af3f3034 100644 --- a/components/project-card.tsx +++ b/components/project-card.tsx @@ -35,6 +35,8 @@ import CircularProgress from './ui/circular-progress'; import { motion } from 'framer-motion'; import { cardHover, fadeInUp } from '@/lib/motion'; import Stepper from './stepper/Stepper'; +import { useWalletProtection } from '@/hooks/use-wallet-protection'; +import WalletRequiredModal from '@/components/wallet/WalletRequiredModal'; interface ProjectCardProps { project: Project; @@ -98,6 +100,16 @@ const ProjectCard: React.FC = ({ }) => { const [validationSheetOpen, setValidationSheetOpen] = useState(false); const [launchCampaignSheetOpen, setLaunchCampaignSheetOpen] = useState(false); + + // Wallet protection hook + const { + requireWallet, + showWalletModal, + handleWalletConnected, + closeWalletModal, + } = useWalletProtection({ + actionName: 'start campaign', + }); const [stepperState] = useState(steps); const [campaignStepperState] = useState(campaignSteps); const getStatusColor = (status: string) => { @@ -345,7 +357,7 @@ const ProjectCard: React.FC = ({ 'flex-1 border-[1.4px] border-[#2B2B2B] rounded-[10px] bg-[#212121] hover:bg-[#2A2A2A] disabled:bg-[#212121] disabled:border-[#2B2B2B] disabled:text-[#484848]', project.status === 'validated' && 'hidden' )} - onClick={() => onVote?.(project.id)} + onClick={() => requireWallet(() => onVote?.(project.id))} disabled={ project.status === 'idea' || project.status === 'under_review' || @@ -391,7 +403,11 @@ const ProjectCard: React.FC = ({
{project.status === 'validated' && (
- setLaunchCampaignSheetOpen(true)}> + + requireWallet(() => setLaunchCampaignSheetOpen(true)) + } + > Start Campaign
@@ -438,6 +454,14 @@ const ProjectCard: React.FC = ({
+ + {/* Wallet Required Modal */} + ); }; diff --git a/components/project/Initialize.tsx b/components/project/Initialize.tsx index 8bb1ddd5b..82e9b0959 100644 --- a/components/project/Initialize.tsx +++ b/components/project/Initialize.tsx @@ -11,6 +11,8 @@ import MilestoneManager from './MilestoneManager'; import MilestoneReview from './MilestoneReview'; import ProjectSubmissionSuccess from './ProjectSubmissionSuccess'; import Loading from '../Loading'; +import { useWalletProtection } from '@/hooks/use-wallet-protection'; +import WalletRequiredModal from '@/components/wallet/WalletRequiredModal'; type StepState = 'pending' | 'active' | 'completed'; @@ -41,6 +43,16 @@ const Initialize: React.FC = ({ onSuccess }) => { }, ]); + // Wallet protection hook + const { + requireWallet, + showWalletModal, + handleWalletConnected, + closeWalletModal, + } = useWalletProtection({ + actionName: 'initialize project', + }); + const localSteps: Step[] = [ { title: 'Submit your project Details', @@ -85,101 +97,113 @@ const Initialize: React.FC = ({ onSuccess }) => { const submitInit = async () => { if (!formData) return; - try { - setIsSubmitting(true); - toast.loading('Initializing project...'); + requireWallet(async () => { + try { + setIsSubmitting(true); + toast.loading('Initializing project...'); - // Validate milestone distribution - const totalPercentage = milestones.reduce( - (sum, m) => sum + (parseFloat(m.fundPercentage) || 0), - 0 - ); - if (totalPercentage !== 100) { - toast.dismiss(); - toast.error('Total milestone fund percentage must equal 100%'); - setIsSubmitting(false); - return; - } + // Validate milestone distribution + const totalPercentage = milestones.reduce( + (sum, m) => sum + (parseFloat(m.fundPercentage) || 0), + 0 + ); + if (totalPercentage !== 100) { + toast.dismiss(); + toast.error('Total milestone fund percentage must equal 100%'); + setIsSubmitting(false); + return; + } - const milestonesPayload = milestones.map(m => { - const pct = parseFloat(m.fundPercentage) || 0; - const amt = (formData.fundAmount * pct) / 100; - return { - title: m.title, - description: m.description, - deliveryDate: m.deliveryDate, - fundPercentage: pct, - fundAmount: amt, - }; - }); + const milestonesPayload = milestones.map(m => { + const pct = parseFloat(m.fundPercentage) || 0; + const amt = (formData.fundAmount * pct) / 100; + return { + title: m.title, + description: m.description, + deliveryDate: m.deliveryDate, + fundPercentage: pct, + fundAmount: amt, + }; + }); - const payload: ProjectInitRequest = { - title: formData.title, - description: formData.description, - tagline: formData.tagline, - type: 'crowdfund', - category: formData.category, - fundAmount: formData.fundAmount, - tags: formData.tags, - milestones: milestonesPayload, - thumbnail: 'https://placehold.co/600x400', - whitepaperUrl: 'https://placehold.co/600x400', - // Optional placeholders until uploads are wired - // ...(formData.thumbnailFile ? { thumbnail: '' } : {}), - // ...(formData.whitepaperFile ? { whitepaperUrl: '' } : {}), - }; + const payload: ProjectInitRequest = { + title: formData.title, + description: formData.description, + tagline: formData.tagline, + type: 'crowdfund', + category: formData.category, + fundAmount: formData.fundAmount, + tags: formData.tags, + milestones: milestonesPayload, + thumbnail: 'https://placehold.co/600x400', + whitepaperUrl: 'https://placehold.co/600x400', + // Optional placeholders until uploads are wired + // ...(formData.thumbnailFile ? { thumbnail: '' } : {}), + // ...(formData.whitepaperFile ? { whitepaperUrl: '' } : {}), + }; - const response = await initProject(payload); - const responseData = response as { data: { projectId: string } }; + const response = await initProject(payload); + const responseData = response as { data: { projectId: string } }; - toast.dismiss(); - toast.success('Project initialized!'); - setPhase('success'); - onSuccess(responseData.data.projectId); - } catch { - toast.dismiss(); - toast.error('Failed to initialize project'); - } finally { - setIsSubmitting(false); - } + toast.dismiss(); + toast.success('Project initialized!'); + setPhase('success'); + onSuccess(responseData.data.projectId); + } catch { + toast.dismiss(); + toast.error('Failed to initialize project'); + } finally { + setIsSubmitting(false); + } + }); }; return ( -
- {isSubmitting && } - + <> +
+ {isSubmitting && } + - {phase === 'form' && ( - - )} - - {phase === 'milestones' && ( - setMilestones(ms)} - onBack={() => setPhase('form')} - onNext={handleMilestonesCompleted} - /> - )} + {phase === 'form' && ( + + )} - {phase === 'review' && ( -
- setPhase('milestones')} - onSubmit={submitInit} - submitting={isSubmitting} + onChange={ms => setMilestones(ms)} + onBack={() => setPhase('form')} + onNext={handleMilestonesCompleted} /> -
- )} + )} - {phase === 'success' && } -
+ {phase === 'review' && ( +
+ setPhase('milestones')} + onSubmit={submitInit} + submitting={isSubmitting} + /> +
+ )} + + {phase === 'success' && } +
+ + {/* Wallet Required Modal */} + + ); }; diff --git a/components/project/LaunchCampaignFlow.tsx b/components/project/LaunchCampaignFlow.tsx index 948f9b906..71fc111e1 100644 --- a/components/project/LaunchCampaignFlow.tsx +++ b/components/project/LaunchCampaignFlow.tsx @@ -9,6 +9,8 @@ import CampaignLiveSuccess from './CampaignLiveSuccess'; import LoadingSpinner from '../LoadingSpinner'; import { Stepper } from '../stepper'; import { mockCampaignDetails } from '@/lib/mock'; +import { useWalletProtection } from '@/hooks/use-wallet-protection'; +import WalletRequiredModal from '../wallet/WalletRequiredModal'; interface LaunchCampaignFlowProps { projectId: string; @@ -27,6 +29,16 @@ const LaunchCampaignFlow: React.FC = ({ const [campaignDetails, setCampaignDetails] = useState(null); + // Wallet protection hook + const { + requireWallet, + showWalletModal, + handleWalletConnected, + closeWalletModal, + } = useWalletProtection({ + actionName: 'launch campaign', + }); + // Define the steps for the stepper const steps = [ { @@ -50,31 +62,33 @@ const LaunchCampaignFlow: React.FC = ({ ]; const handleLaunch = async () => { - try { - setCurrentStep('launching'); - - // Launch the campaign - const response = (await launchCampaign(projectId)) as { - data: { campaignId: string }; - }; - - // mock campaign details - setCampaignDetails(mockCampaignDetails as CampaignDetails); - - // Update campaign details with the response - if (campaignDetails) { - setCampaignDetails({ - ...campaignDetails, - id: response.data.campaignId, - }); + requireWallet(async () => { + try { + setCurrentStep('launching'); + + // Launch the campaign + const response = (await launchCampaign(projectId)) as { + data: { campaignId: string }; + }; + + // mock campaign details + setCampaignDetails(mockCampaignDetails as CampaignDetails); + + // Update campaign details with the response + if (campaignDetails) { + setCampaignDetails({ + ...campaignDetails, + id: response.data.campaignId, + }); + } + + toast.success('Campaign launched successfully!'); + setCurrentStep('success'); + } catch { + toast.error('Failed to launch campaign. Please try again.'); + setCurrentStep('review'); } - - toast.success('Campaign launched successfully!'); - setCurrentStep('success'); - } catch { - toast.error('Failed to launch campaign. Please try again.'); - setCurrentStep('review'); - } + }); }; const handleBackToDashboard = () => { @@ -137,15 +151,25 @@ const LaunchCampaignFlow: React.FC = ({ }; return ( -
- {/* Left Sidebar with Stepper */} -
- + <> +
+ {/* Left Sidebar with Stepper */} +
+ +
+ + {/* Right Content Area */} +
{renderContent()}
- {/* Right Content Area */} -
{renderContent()}
-
+ {/* Wallet Required Modal */} + + ); }; diff --git a/components/project/MilestoneSubmissionModal.tsx b/components/project/MilestoneSubmissionModal.tsx index 010e97ce1..3d578bbe8 100644 --- a/components/project/MilestoneSubmissionModal.tsx +++ b/components/project/MilestoneSubmissionModal.tsx @@ -15,6 +15,8 @@ import { } from 'lucide-react'; import { cn } from '@/lib/utils'; import MilestoneSubmissionSuccess from './MilestoneSubmissionSuccess'; +import { useWalletProtection } from '@/hooks/use-wallet-protection'; +import WalletRequiredModal from '@/components/wallet/WalletRequiredModal'; export interface MilestoneSubmissionData { files: File[]; @@ -49,6 +51,16 @@ const MilestoneSubmissionModal: React.FC = ({ const [focusedInput, setFocusedInput] = useState(null); const [showSuccess, setShowSuccess] = useState(false); + // Wallet protection hook + const { + requireWallet, + showWalletModal, + handleWalletConnected, + closeWalletModal, + } = useWalletProtection({ + actionName: 'submit milestone', + }); + const handleFileUpload = (event: React.ChangeEvent) => { const selectedFiles = Array.from(event.target.files || []); setFiles(prev => [...prev, ...selectedFiles]); @@ -75,12 +87,14 @@ const MilestoneSubmissionModal: React.FC = ({ }; const handleSubmit = () => { - const filteredLinks = externalLinks.filter(link => link.trim() !== ''); - onSubmit({ - files, - externalLinks: filteredLinks, + requireWallet(() => { + const filteredLinks = externalLinks.filter(link => link.trim() !== ''); + onSubmit({ + files, + externalLinks: filteredLinks, + }); + setShowSuccess(true); }); - setShowSuccess(true); }; const formatDate = (dateString: string) => { @@ -372,6 +386,14 @@ const MilestoneSubmissionModal: React.FC = ({
)} + + {/* Wallet Required Modal */} + ); }; diff --git a/components/project/MilestoneSubmissionPage.tsx b/components/project/MilestoneSubmissionPage.tsx index 33cbabe0a..ef229a195 100644 --- a/components/project/MilestoneSubmissionPage.tsx +++ b/components/project/MilestoneSubmissionPage.tsx @@ -15,6 +15,8 @@ import { } from 'lucide-react'; import { cn } from '@/lib/utils'; import { MilestoneSubmissionData } from './MilestoneSubmissionModal'; +import { useWalletProtection } from '@/hooks/use-wallet-protection'; +import WalletRequiredModal from '@/components/wallet/WalletRequiredModal'; interface MilestoneSubmissionPageProps { milestone: { @@ -41,6 +43,16 @@ const MilestoneSubmissionPage: React.FC = ({ const [isExpanded, setIsExpanded] = useState(true); const [additionalNotes, setAdditionalNotes] = useState(''); + // Wallet protection hook + const { + requireWallet, + showWalletModal, + handleWalletConnected, + closeWalletModal, + } = useWalletProtection({ + actionName: 'submit milestone', + }); + const handleFileUpload = (event: React.ChangeEvent) => { const selectedFiles = Array.from(event.target.files || []); setFiles(prev => [...prev, ...selectedFiles]); @@ -67,10 +79,12 @@ const MilestoneSubmissionPage: React.FC = ({ }; const handleSubmit = () => { - const filteredLinks = externalLinks.filter(link => link.trim() !== ''); - onSubmit({ - files, - externalLinks: filteredLinks, + requireWallet(() => { + const filteredLinks = externalLinks.filter(link => link.trim() !== ''); + onSubmit({ + files, + externalLinks: filteredLinks, + }); }); }; @@ -392,6 +406,14 @@ const MilestoneSubmissionPage: React.FC = ({
+ + {/* Wallet Required Modal */} +
); }; diff --git a/components/project/ValidationFlow.tsx b/components/project/ValidationFlow.tsx index 466651335..20dd95f91 100644 --- a/components/project/ValidationFlow.tsx +++ b/components/project/ValidationFlow.tsx @@ -18,6 +18,8 @@ import { } from 'lucide-react'; import { cn } from '@/lib/utils'; import TimelineStepper from './TimelineStepper'; +import { useWalletProtection } from '@/hooks/use-wallet-protection'; +import WalletRequiredModal from '@/components/wallet/WalletRequiredModal'; interface ValidationFlowProps { project: Project; @@ -34,6 +36,16 @@ const ValidationFlow: React.FC = ({ project, onVote }) => { const [daysLeft] = useState(12); const [expandedMilestones, setExpandedMilestones] = useState([]); + // Wallet protection hook + const { + requireWallet, + showWalletModal, + handleWalletConnected, + closeWalletModal, + } = useWalletProtection({ + actionName: 'vote on project', + }); + const getStatusColor = (status: string) => { switch (status.toLowerCase()) { case 'pending': @@ -63,14 +75,16 @@ const ValidationFlow: React.FC = ({ project, onVote }) => { }; const handleVote = () => { - if (hasVoted) { - setVoteCount(prev => prev - 1); - setHasVoted(false); - } else { - setVoteCount(prev => prev + 1); - setHasVoted(true); - } - onVote?.(project.id); + requireWallet(() => { + if (hasVoted) { + setVoteCount(prev => prev - 1); + setHasVoted(false); + } else { + setVoteCount(prev => prev + 1); + setHasVoted(true); + } + onVote?.(project.id); + }); }; const toggleMilestone = (milestoneIndex: number) => { @@ -274,6 +288,14 @@ const ValidationFlow: React.FC = ({ project, onVote }) => { + + {/* Wallet Required Modal */} + ); }; diff --git a/components/skeleton/ProjectsSkeleton.tsx b/components/skeleton/ProjectsSkeleton.tsx new file mode 100644 index 000000000..270eff4c5 --- /dev/null +++ b/components/skeleton/ProjectsSkeleton.tsx @@ -0,0 +1,64 @@ +import { Skeleton } from '@/components/ui/skeleton'; +import { motion } from 'framer-motion'; +import { fadeInUp, staggerContainer } from '@/lib/motion'; + +export const ProjectsPageSkeleton = () => { + return ( +
+
+ +
+
+ ); +}; + +export const ProjectsSkeleton = () => { + return ( + <> + {/* Header */} + +
+ +
+
+ + +
+
+ + {/* Projects Grid */} + + {Array.from({ length: 6 }).map((_, index) => ( + +
+ +
+ + + +
+
+
+ +
+ + +
+
+
+ ))} +
+ + ); +}; diff --git a/components/skeleton/UserPageSkeleton.tsx b/components/skeleton/UserPageSkeleton.tsx new file mode 100644 index 000000000..ce53e3318 --- /dev/null +++ b/components/skeleton/UserPageSkeleton.tsx @@ -0,0 +1,220 @@ +import { Skeleton } from '@/components/ui/skeleton'; +import { motion } from 'framer-motion'; +import { fadeInUp, staggerContainer } from '@/lib/motion'; + +export const UserPageSkeleton = () => { + return ( +
+
+ {/* Header Section Skeleton */} +
+ +
+ + {/* Stats Cards Grid Skeleton */} +
+ {Array.from({ length: 4 }).map((_, index) => ( +
+ + +
+ + +
+
+ ))} +
+ + {/* Recent Projects Skeleton */} +
+ +
+ + {/* Campaign Table Skeleton */} +
+ +
+
+
+ ); +}; + +export const RecentProjectsSkeleton = () => { + return ( + <> + {/* Header */} + +
+ + +
+
+ + +
+
+ + {/* Projects Grid */} + + {Array.from({ length: 3 }).map((_, index) => ( + +
+ +
+ + + +
+
+
+ +
+ + +
+
+
+ ))} +
+ + ); +}; + +export const CampaignTableSkeleton = () => { + return ( + <> + {/* Header */} +
+
+ +
+
+ + +
+
+ + {/* Table Header */} +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + {/* Table Rows */} +
+ {Array.from({ length: 5 }).map((_, index) => ( +
+
+ +
+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+
+ ))} + + {/* Mobile Rows */} + {Array.from({ length: 3 }).map((_, index) => ( +
+
+
+ +
+ + +
+
+
+ + +
+
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ ))} +
+ + ); +}; diff --git a/components/wallet/WalletConnectButton.tsx b/components/wallet/WalletConnectButton.tsx index 9a3b20720..65258ba41 100644 --- a/components/wallet/WalletConnectButton.tsx +++ b/components/wallet/WalletConnectButton.tsx @@ -51,6 +51,7 @@ interface WalletConnectButtonProps { | 'link'; size?: 'default' | 'sm' | 'lg' | 'icon'; showErrorGuide?: boolean; + onConnect?: () => void; } const WalletConnectButton: React.FC = ({ @@ -58,6 +59,7 @@ const WalletConnectButton: React.FC = ({ variant = 'default', size = 'default', showErrorGuide = true, + onConnect, }) => { const [showErrorGuideState, setShowErrorGuideState] = useState(false); const [showConnectModal, setShowConnectModal] = useState(false); @@ -305,6 +307,7 @@ const WalletConnectButton: React.FC = ({ ); @@ -329,6 +332,7 @@ const WalletConnectButton: React.FC = ({ ); diff --git a/components/wallet/WalletRequiredModal.tsx b/components/wallet/WalletRequiredModal.tsx new file mode 100644 index 000000000..a8546ca43 --- /dev/null +++ b/components/wallet/WalletRequiredModal.tsx @@ -0,0 +1,74 @@ +import React from 'react'; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from '../ui/dialog'; +import { XIcon } from 'lucide-react'; +import WalletConnectButton from './WalletConnectButton'; +import Image from 'next/image'; + +interface WalletRequiredModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; + actionName: string; + onWalletConnected?: () => void; +} + +const WalletRequiredModal: React.FC = ({ + open, + onOpenChange, + actionName, + onWalletConnected, +}) => { + const handleWalletConnected = () => { + onWalletConnected?.(); + onOpenChange(false); + }; + + return ( + + + + + + + + +
+ wallet-required + + + You need to connect your wallet to{' '} + {actionName}. This + ensures secure and transparent transactions on the blockchain. + + +
+ +
+
+
+
+ ); +}; + +export default WalletRequiredModal; diff --git a/hooks/use-wallet-protection.ts b/hooks/use-wallet-protection.ts new file mode 100644 index 000000000..50235537c --- /dev/null +++ b/hooks/use-wallet-protection.ts @@ -0,0 +1,50 @@ +import { useState } from 'react'; +import { useWalletStore } from './use-wallet'; +import { toast } from 'sonner'; + +interface UseWalletProtectionOptions { + actionName?: string; + showModal?: boolean; +} + +export function useWalletProtection(options: UseWalletProtectionOptions = {}) { + const { actionName = 'perform this action', showModal = true } = options; + const { isConnected, publicKey } = useWalletStore(); + const [showWalletModal, setShowWalletModal] = useState(false); + + const requireWallet = (callback?: () => void) => { + if (!isConnected) { + if (showModal) { + setShowWalletModal(true); + } else { + toast.error(`Wallet connection required to ${actionName}`); + } + return false; + } + + // If callback provided and wallet is connected, execute it + if (callback) { + callback(); + } + + return true; + }; + + const handleWalletConnected = () => { + setShowWalletModal(false); + toast.success('Wallet connected successfully!'); + }; + + const closeWalletModal = () => { + setShowWalletModal(false); + }; + + return { + requireWallet, + isConnected, + publicKey, + showWalletModal, + handleWalletConnected, + closeWalletModal, + }; +} diff --git a/public/close.svg b/public/close.svg new file mode 100644 index 000000000..f0ce533d7 --- /dev/null +++ b/public/close.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/warning.svg b/public/warning.svg new file mode 100644 index 000000000..c167f7296 --- /dev/null +++ b/public/warning.svg @@ -0,0 +1,4 @@ + + + +