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 (
+
+ );
+};
+
+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 @@
+