diff --git a/.env.example b/.env.example index 9377d2543..c065f8ea7 100644 --- a/.env.example +++ b/.env.example @@ -1,35 +1,52 @@ +# Stellar Network Configuration NEXT_PUBLIC_STELLAR_NETWORK=testnet +# Options: testnet, public +# Application Configuration NEXT_PUBLIC_APP_URL=http://localhost:3000 +# Your application's public URL -NEXT_PUBLIC_API_URL=https://staging-api.boundlessfi.xyz/api +# API Configuration +NEXT_PUBLIC_API_URL=http://localhost:8000/api +# Your backend API URL +# WalletConnect Configuration NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=your_wallet_connect_project_id # Get this from https://cloud.walletconnect.com/ +# Stellar Horizon URLs (optional - defaults will be used) NEXT_PUBLIC_HORIZON_TESTNET_URL=https://horizon-testnet.stellar.org NEXT_PUBLIC_HORIZON_PUBLIC_URL=https://horizon.stellar.org +# Application Metadata NEXT_PUBLIC_APP_NAME=Boundless NEXT_PUBLIC_APP_DESCRIPTION=Stellar-based application NEXT_PUBLIC_APP_ICON=/logo.svg +# Development Settings NEXT_PUBLIC_DEBUG_MODE=false +# Enable debug logging for wallet operations +# Feature Flags NEXT_PUBLIC_ENABLE_WALLET_CONNECT=true NEXT_PUBLIC_ENABLE_MULTI_WALLET=true NEXT_PUBLIC_ENABLE_NETWORK_SWITCHING=true +# NextAuth Configuration NEXTAUTH_SECRET=your_nextauth_secret_here +# Generate a random string for this secret +# You can use: openssl rand -base64 32 NEXTAUTH_URL=http://localhost:3000 +# Your application's public URL (same as NEXT_PUBLIC_APP_URL) +# Google OAuth Configuration GOOGLE_CLIENT_ID=your_google_client_id +# Get this from Google Cloud Console GOOGLE_CLIENT_SECRET=your_google_client_secret +# Get this from Google Cloud Console +# Environment NODE_ENV=development - - - - +# Options: development, production, test diff --git a/LAUNCH_CAMPAIGN_README.md b/LAUNCH_CAMPAIGN_README.md new file mode 100644 index 000000000..55e7f055f --- /dev/null +++ b/LAUNCH_CAMPAIGN_README.md @@ -0,0 +1,201 @@ +# Launch Campaign: Review and Launch Implementation + +This document describes the implementation of the Launch Campaign: Review and Launch feature for the Boundless platform. + +## Overview + +The Launch Campaign feature allows users to review their campaign details and launch their campaign after completing the initialization and validation steps. The implementation includes: + +1. **Review Campaign Screen** - Displays all campaign details for final review +2. **Campaign Live Success Screen** - Shows success message after launch +3. **Share Campaign Modal** - Allows sharing on social media platforms +4. **API Integration** - Mock endpoints for testing + +## Components + +### 1. ReviewCampaign Component + +- **Location**: `components/project/ReviewCampaign.tsx` +- **Purpose**: Displays campaign details for final review before launch +- **Features**: + - Campaign header with creator info and financials + - Engagement metrics (likes, comments, backers, days left) + - Campaign description and tags + - Campaign photos gallery + - Expandable milestones with details + - Confirmation checkbox + - Back and Launch buttons + +### 2. CampaignLiveSuccess Component + +- **Location**: `components/project/CampaignLiveSuccess.tsx` +- **Purpose**: Displays success screen after campaign launch +- **Features**: + - Large checkmark icon + - Success message + - Campaign preview + - Back to Dashboard and Share buttons + - Campaign link with copy functionality + +### 3. ShareCampaignModal Component + +- **Location**: `components/project/ShareCampaignModal.tsx` +- **Purpose**: Modal for sharing campaign on social media +- **Features**: + - Copyable campaign link + - Social media sharing buttons (Discord, X/Twitter, WhatsApp, Telegram) + - Campaign preview + - Copy to clipboard functionality + +### 4. LaunchCampaignFlow Component + +- **Location**: `components/project/LaunchCampaignFlow.tsx` +- **Purpose**: Manages the entire launch flow +- **Features**: + - State management for review, launching, and success steps + - Loading states during campaign launch + - Error handling + - Integration with API endpoints + +## API Endpoints + +### Mock Implementation + +The following endpoints are currently mocked for testing: + +1. **getCampaignDetails(projectId)** - Fetches campaign details for review +2. **launchCampaign(projectId)** - Launches the campaign (simulates blockchain deployment) +3. **generateCampaignLink(projectId)** - Generates shareable campaign link + +### Real Implementation + +When backend endpoints are available, replace the mock implementations in `lib/api/project.ts` with actual API calls. + +## Integration + +### ProjectSheetFlow Integration + +The Launch Campaign step has been integrated into the existing `ProjectSheetFlow` component: + +1. **Step 1**: Initialize (existing) +2. **Step 2**: Validate (existing, updated with onSuccess callback) +3. **Step 3**: Launch Campaign (new) + +### Flow Progression + +- After validation success, the flow automatically progresses to the Launch Campaign step +- Users can navigate back to previous steps +- The stepper UI updates to reflect current progress + +## Testing + +### Test Button + +A test button has been added to the dashboard (`app/dashboard/page.tsx`) to test the Launch Campaign flow: + +1. Navigate to `/dashboard` +2. Click "Test Launch Campaign" button +3. The flow will open in a modal sheet +4. Test the review, launch, and success screens + +### Mock Data + +Mock campaign data is defined in `lib/mock.ts` and includes: + +- Campaign details matching the Figma designs +- Realistic milestone information +- Engagement metrics +- Creator information + +## Design System Compliance + +The implementation follows the existing design system: + +- **Colors**: Uses predefined theme colors (`#F5F5F5`, `#B5B5B5`, `#2A2A2A`, etc.) +- **Typography**: Consistent with existing components +- **Spacing**: Follows established patterns +- **Components**: Reuses existing UI components (Button, Badge, Dialog, etc.) +- **Icons**: Uses Lucide React icons as specified + +## Accessibility + +The implementation includes accessibility features: + +- Proper ARIA labels +- Keyboard navigation support +- Screen reader friendly content +- Focus management +- Color contrast compliance + +## Responsive Design + +All components are responsive and work on: + +- Desktop (default) +- Tablet (responsive breakpoints) +- Mobile (optimized layouts) + +## Error Handling + +The implementation includes comprehensive error handling: + +- API error states +- Loading states +- Empty states +- User-friendly error messages +- Graceful fallbacks + +## Future Enhancements + +When backend integration is complete: + +1. Replace mock API calls with real endpoints +2. Add real-time status updates during campaign launch +3. Implement actual blockchain transaction handling +4. Add campaign analytics and tracking +5. Implement real social media sharing with tracking + +## File Structure + +``` +components/project/ +├── ReviewCampaign.tsx # Review campaign details +├── CampaignLiveSuccess.tsx # Success screen after launch +├── ShareCampaignModal.tsx # Social media sharing modal +├── LaunchCampaignFlow.tsx # Main flow controller +└── index.ts # Component exports + +lib/ +├── api/ +│ └── project.ts # API functions (mock) +└── mock.ts # Mock data + +app/dashboard/ +└── page.tsx # Test button integration +``` + +## Usage + +To use the Launch Campaign flow: + +1. Import the components: + +```tsx +import { LaunchCampaignFlow } from '@/components/project'; +``` + +2. Use in your component: + +```tsx + { + /* handle back */ + }} + onComplete={() => { + /* handle completion */ + }} +/> +``` + +The implementation is ready for testing and can be easily integrated with real backend endpoints when available. diff --git a/app/comment/page.tsx b/app/comment/page.tsx index cab430a84..ea46bf517 100644 --- a/app/comment/page.tsx +++ b/app/comment/page.tsx @@ -17,7 +17,7 @@ const Page = () => { if (comment && comment.trim()) { // Comment is valid and can be processed // Add your comment handling logic here - console.log('Comment submitted:', comment); + // console.log('Comment submitted:', comment); // Close the sheet after submission setIsSheetOpen(false); diff --git a/app/globals.css b/app/globals.css index 0f9d5afab..a5c68ecf0 100644 --- a/app/globals.css +++ b/app/globals.css @@ -158,3 +158,18 @@ button { cursor: pointer; } + +/* reduce scrollbar width */ +.slim-scrollbar { + overflow-y: scroll; + scrollbar-width: thin; + scrollbar-color: var(--background); +} + +.slim-scrollbar::-webkit-scrollbar-thumb { + background-color: rgba(255, 255, 255, 0.2); +} + +.slim-scrollbar::-webkit-scrollbar-track { + background-color: transparent; +} diff --git a/app/test/page.tsx b/app/test/page.tsx new file mode 100644 index 000000000..26375b9fc --- /dev/null +++ b/app/test/page.tsx @@ -0,0 +1,61 @@ +'use client'; + +import { useState } from 'react'; +import LaunchCampaignFlow from '@/components/project/LaunchCampaignFlow'; +import BoundlessSheet from '@/components/sheet/boundless-sheet'; +import { Button } from '@/components/ui/button'; +import { Rocket } from 'lucide-react'; + +export default function TestPage() { + const [showLaunchFlow, setShowLaunchFlow] = useState(false); + + const handleOpenModal = () => { + setShowLaunchFlow(true); + }; + + const handleCloseModal = () => { + setShowLaunchFlow(false); + }; + + return ( +
+
+

+ Launch Campaign Test +

+ +

+ Click the button below to test the Launch Campaign feature +

+ + + + {/* Debug info */} +
+ Modal state: {showLaunchFlow ? 'Open' : 'Closed'} +
+ + {/* Launch Campaign Flow Modal */} + + + +
+
+ ); +} diff --git a/app/user/layout.tsx b/app/user/layout.tsx index 7d60428d5..3930bbba1 100644 --- a/app/user/layout.tsx +++ b/app/user/layout.tsx @@ -4,6 +4,9 @@ import SidebarLayout from '@/components/layout/sidebar'; import { SidebarProvider } from '@/components/ui/sidebar'; import { redirect } from 'next/navigation'; +// Force dynamic rendering for all user pages +export const dynamic = 'force-dynamic'; + export default async function UserLayout({ children, }: Readonly<{ diff --git a/app/user/page.tsx b/app/user/page.tsx index b2721bb52..579caeb78 100644 --- a/app/user/page.tsx +++ b/app/user/page.tsx @@ -8,9 +8,27 @@ import GrantHistory from '@/components/overview/GrantHistory'; import PageTransition from '@/components/PageTransition'; import { Coins, History } from 'lucide-react'; import { useAuth } from '@/hooks/use-auth'; +import { useEffect, useState } from 'react'; + +import LoadingSpinner from '@/components/LoadingSpinner'; export default function UserPage() { - const { user } = useAuth(); + const { user, isLoading } = useAuth(); + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + // Show loading until client-side hydration is complete + if (!mounted || isLoading) { + return ( +
+ +
+ ); + } + return (
@@ -18,7 +36,7 @@ export default function UserPage() { {/* Header Section */}

- Hello, {user?.name?.split(' ')[0]} + Hello, {user?.name?.split(' ')[0] || 'User'}

{/* Stats Cards Grid */} diff --git a/app/user/projects/page.tsx b/app/user/projects/page.tsx index bf1c8893b..d97ebd569 100644 --- a/app/user/projects/page.tsx +++ b/app/user/projects/page.tsx @@ -1,20 +1,13 @@ 'use client'; -import AnimatedCounter from '@/components/AnimatedCounter'; -import LoadingSpinner from '@/components/LoadingSpinner'; -import LoadingSpinnerDemo from '@/components/LoadingSpinnerDemo'; -import PageTransition from '@/components/PageTransition'; import React from 'react'; const page = () => { return ( - -
- - -
- -
+
+

Projects

+

Projects page content will be added here.

+
); }; diff --git a/auth.ts b/auth.ts index 561def06f..036b8468a 100644 --- a/auth.ts +++ b/auth.ts @@ -73,7 +73,7 @@ declare module 'next-auth' { const getMe = (token?: string) => getMeBase(token); -export const { handlers, signIn, signOut, auth } = NextAuth({ +export const authConfig = { debug: process.env.NODE_ENV === 'development', providers: [ Google({ @@ -89,7 +89,7 @@ export const { handlers, signIn, signOut, auth } = NextAuth({ password: { label: 'Password', type: 'password' }, accessToken: { label: 'Access Token', type: 'text' }, }, - authorize: async credentials => { + authorize: async (credentials: any) => { if ( typeof credentials?.accessToken === 'string' && !credentials?.email && @@ -164,7 +164,7 @@ export const { handlers, signIn, signOut, auth } = NextAuth({ }), ], callbacks: { - async jwt({ token, user }) { + async jwt({ token, user }: any) { if (user) { const u = user as { accessToken?: string; refreshToken?: string }; token.accessToken = u.accessToken; @@ -172,10 +172,12 @@ export const { handlers, signIn, signOut, auth } = NextAuth({ } return token; }, - async session({ session, token }) { + async session({ session, token }: any) { session.user.accessToken = token.accessToken as string | undefined; session.user.refreshToken = token.refreshToken as string | undefined; return session; }, }, -}); +}; + +export const { handlers, auth, signIn, signOut } = NextAuth(authConfig); diff --git a/components/layout/sidebar.tsx b/components/layout/sidebar.tsx index 8701ea58d..26161a688 100644 --- a/components/layout/sidebar.tsx +++ b/components/layout/sidebar.tsx @@ -1,5 +1,5 @@ 'use client'; -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { Package, Sun, @@ -86,6 +86,12 @@ const SidebarLayout: React.FC = () => { const router = useRouter(); const pathname = usePathname(); const { user } = useAuth(); + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + // Function to determine if a route is active const isRouteActive = (href: string) => { if (href === '/user') { @@ -94,6 +100,11 @@ const SidebarLayout: React.FC = () => { return pathname.startsWith(href); }; + // Don't render anything until client-side hydration is complete + if (!mounted) { + return null; + } + return ( void; +} + +const CampaignLiveSuccess: React.FC = ({ + campaignDetails, + onBackToDashboard, +}) => { + const [showShareModal, setShowShareModal] = useState(false); + const [shareLink, setShareLink] = useState(''); + const [expandedMilestones, setExpandedMilestones] = useState([]); + + const handleShare = async () => { + try { + const response = await generateCampaignLink(campaignDetails.id); + const linkData = response as { + success: boolean; + data: { shareLink: string }; + }; + setShareLink(linkData.data.shareLink); + setShowShareModal(true); + } catch { + toast.error('Failed to generate share link'); + } + }; + + return ( +
+ {/* Success Header */} +
+

Your Campaign is Live!

+ check +

+ Your campaign has been successfully launched. Backers can now fund it, + and your milestone progress will be tracked automatically.{' '} + View Campaign +

+
+ + {/* Action Buttons */} +
+ + +
+ + {/* Campaign Preview Section */} +
+ {/* Preview Header */} +
+

Preview

+ +
+ + {/* Campaign Banner Image */} +
+ {campaignDetails.title} +
+ + {/* Campaign Title and Status */} +
+

+ {campaignDetails.title} +

+ + Live + +
+ + {/* Creator Info */} +
+
+ profile + verify +
+ + {campaignDetails.creator.name} + +
+ + {/* Financial Metrics */} +
+
+

Raised

+

+ ${campaignDetails.raisedAmount.toLocaleString()}.00 +

+
+
+

Target

+

+ ${campaignDetails.fundAmount.toLocaleString()}.00 +

+
+
+ + {/* Progress Bar */} +
+
+
+
+
+ + {/* Engagement Stats */} +
+
+
+ + + {campaignDetails.engagement.likes} + +
+
+ + + {campaignDetails.engagement.comments} + +
+
+
+ + + {campaignDetails.engagement.backers} Backers + +
+
+ + + {campaignDetails.engagement.daysLeft} days left + +
+
+
+ + {/* Campaign Details */} +
+

+ Campaign Details +

+
+

+ {campaignDetails.description} +

+
+
+ + {/* Tags */} +
+

Tags

+
+ {campaignDetails.tags.slice(0, 2).map((tag, index) => ( + + #{tag} + + ))} +
+
+ + {/* Campaign Photos */} +
+

+ Campaign Photos +

+
+ {[1, 2, 3, 4].map(index => ( + {`Campaign + ))} +
+
+ + {/* Milestones */} +
+

Milestones

+
+ {[ + { + title: 'Prototype & Smart Contract Setup', + description: + 'Initial development phase with smart contract implementation', + }, + { + title: 'Campaign & Grant Builder Integration', + description: + 'Integration of campaign management and grant building features', + }, + { + title: 'Platform Launch & Community Building', + description: + 'Final platform launch and community engagement phase', + }, + ].map((milestone, index) => { + const isExpanded = expandedMilestones.includes(index); + return ( +
+
{ + setExpandedMilestones(prev => + isExpanded + ? prev.filter(i => i !== index) + : [...prev, index] + ); + }} + > + + Milestone {index + 1}: {milestone.title} + + +
+ {isExpanded && ( +
+

+ {milestone.description} +

+
+ )} +
+ ); + })} +
+
+ + {/* Funding History */} +
+
+

+ Funding History +

+ + View all > + +
+
+ no backers +

+ No backers for now +

+

+ Get the word out and attract your first backers. Every share brings + you closer to your funding goal. +

+
+
+ + {/* Action Buttons */} +
+ +
+ + {/* Share Modal */} + +
+ ); +}; + +export default CampaignLiveSuccess; diff --git a/components/project/Initialize.tsx b/components/project/Initialize.tsx index 4b0964c6e..8bb1ddd5b 100644 --- a/components/project/Initialize.tsx +++ b/components/project/Initialize.tsx @@ -21,7 +21,7 @@ export type Step = { }; interface InitializeProps { - onSuccess: () => void; + onSuccess: (projectId: string) => void; } const Initialize: React.FC = ({ onSuccess }) => { @@ -129,12 +129,13 @@ const Initialize: React.FC = ({ onSuccess }) => { // ...(formData.whitepaperFile ? { whitepaperUrl: '' } : {}), }; - await initProject(payload); + const response = await initProject(payload); + const responseData = response as { data: { projectId: string } }; toast.dismiss(); toast.success('Project initialized!'); setPhase('success'); - onSuccess(); + onSuccess(responseData.data.projectId); } catch { toast.dismiss(); toast.error('Failed to initialize project'); diff --git a/components/project/LaunchCampaignFlow.tsx b/components/project/LaunchCampaignFlow.tsx new file mode 100644 index 000000000..948f9b906 --- /dev/null +++ b/components/project/LaunchCampaignFlow.tsx @@ -0,0 +1,152 @@ +'use client'; + +import React, { useState } from 'react'; +import { toast } from 'sonner'; +import { launchCampaign } from '@/lib/api/project'; +import { CampaignDetails } from '@/lib/api/types'; +import ReviewCampaign from './ReviewCampaign'; +import CampaignLiveSuccess from './CampaignLiveSuccess'; +import LoadingSpinner from '../LoadingSpinner'; +import { Stepper } from '../stepper'; +import { mockCampaignDetails } from '@/lib/mock'; + +interface LaunchCampaignFlowProps { + projectId: string; + onBack: () => void; + onComplete: () => void; +} + +type FlowStep = 'review' | 'launching' | 'success'; + +const LaunchCampaignFlow: React.FC = ({ + projectId, + onBack, + onComplete, +}) => { + const [currentStep, setCurrentStep] = useState('review'); + const [campaignDetails, setCampaignDetails] = + useState(null); + + // Define the steps for the stepper + const steps = [ + { + title: 'Initialize', + description: + 'Submit your project idea and define milestones to begin your campaign journey.', + state: 'completed' as const, + }, + { + title: 'Validate', + description: + 'Get admin approval and gather public support through voting.', + state: 'completed' as const, + }, + { + title: 'Launch Campaign', + description: + 'Finalize campaign details and deploy smart escrow to go live and receive funding.', + state: 'active' as const, + }, + ]; + + 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, + }); + } + + toast.success('Campaign launched successfully!'); + setCurrentStep('success'); + } catch { + toast.error('Failed to launch campaign. Please try again.'); + setCurrentStep('review'); + } + }; + + const handleBackToDashboard = () => { + onComplete(); + }; + + const handleLoading = () => { + // Handle loading state if needed + }; + + const renderContent = () => { + switch (currentStep) { + case 'review': + return ( + + ); + + case 'launching': + return ( +
+ +
+

+ Launching Campaign... +

+

+ Please wait while we deploy your campaign and set up the smart + escrow. +

+
+
+ ); + + case 'success': + return campaignDetails ? ( + + ) : ( +
+

Campaign details not available

+ +
+ ); + + default: + return null; + } + }; + + return ( +
+ {/* Left Sidebar with Stepper */} +
+ +
+ + {/* Right Content Area */} +
{renderContent()}
+
+ ); +}; + +export default LaunchCampaignFlow; diff --git a/components/project/ProjectSheetFlow.tsx b/components/project/ProjectSheetFlow.tsx index efcd57209..d82b45dd2 100644 --- a/components/project/ProjectSheetFlow.tsx +++ b/components/project/ProjectSheetFlow.tsx @@ -6,6 +6,7 @@ import { Stepper } from '../stepper'; // import { MilestoneReview } from './'; import Initialize from './Initialize'; import ValidationFlow from './ValidationFlow'; +import LaunchCampaignFlow from './LaunchCampaignFlow'; import { useProjectSheetStore } from '@/lib/stores/project-sheet-store'; // import ValidationFlow from './ValidationFlow'; @@ -31,7 +32,8 @@ const initialSteps: Step[] = [ }, { title: 'Launch Campaign', - description: 'Set milestones, activate escrow, and start receiving funds.', + description: + 'Finalize campaign details and deploy smart escrow to go live and receive funding.', state: 'pending', }, ]; @@ -47,9 +49,14 @@ const ProjectSheetFlow: React.FC = ({ }) => { // Note: sub-steps handled internally by Initialize and ValidationFlow const [steps, setSteps] = useState(initialSteps); + const [currentStep, setCurrentStep] = useState< + 'initialize' | 'validate' | 'launch' + >('initialize'); + const [projectId, setProjectId] = useState(null); const sheet = useProjectSheetStore(); - const handleFormSuccess = () => { + const handleFormSuccess = (projectId: string) => { + setProjectId(projectId); setSteps(prevSteps => prevSteps.map((step, index) => { if (index === 0) { @@ -61,6 +68,41 @@ const ProjectSheetFlow: React.FC = ({ return step; }) ); + setCurrentStep('validate'); + }; + + const handleValidationSuccess = () => { + setSteps(prevSteps => + prevSteps.map((step, index) => { + if (index === 1) { + return { ...step, state: 'completed' }; + } + if (index === 2) { + return { ...step, state: 'active' }; + } + return step; + }) + ); + setCurrentStep('launch'); + }; + + const handleLaunchComplete = () => { + handleClose(); + }; + + const handleBackToValidation = () => { + setCurrentStep('validate'); + setSteps(prevSteps => + prevSteps.map((step, index) => { + if (index === 1) { + return { ...step, state: 'active' }; + } + if (index === 2) { + return { ...step, state: 'pending' }; + } + return step; + }) + ); }; // Milestone handling moved inside Initialize; no-op here @@ -69,12 +111,30 @@ const ProjectSheetFlow: React.FC = ({ onOpenChange(false); sheet.reset(); setSteps(initialSteps); + setCurrentStep('initialize'); + setProjectId(null); }; const renderContent = () => { - if (sheet.mode === 'validate' && sheet.project) { - return ; + if (currentStep === 'launch' && projectId) { + return ( + + ); } + + if (currentStep === 'validate' && sheet.project) { + return ( + + ); + } + return ; }; diff --git a/components/project/ReviewCampaign.tsx b/components/project/ReviewCampaign.tsx new file mode 100644 index 000000000..49eef2d8f --- /dev/null +++ b/components/project/ReviewCampaign.tsx @@ -0,0 +1,374 @@ +'use client'; + +import React, { useState, useEffect } from 'react'; +import Image from 'next/image'; +import { Button } from '@/components/ui/button'; +import { Checkbox } from '@/components/ui/checkbox'; +import { Badge } from '@/components/ui/badge'; +import { + ThumbsUp, + MessageCircle, + Users, + Clock, + ChevronDown, + ChevronUp, + Calendar, + Coins, + UserCheck2, +} from 'lucide-react'; +import { CampaignDetails } from '@/lib/api/types'; +import { getCampaignDetails } from '@/lib/api/project'; +import LoadingSpinner from '../LoadingSpinner'; + +interface ReviewCampaignProps { + projectId: string; + onBack: () => void; + onLaunch: () => void; + onLoading: (loading: boolean) => void; +} + +const ReviewCampaign: React.FC = ({ + projectId, + onBack, + onLaunch, + onLoading, +}) => { + const [campaignDetails, setCampaignDetails] = + useState(null); + const [isConfirmed, setIsConfirmed] = useState(false); + const [expandedMilestones, setExpandedMilestones] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchCampaignDetails = async () => { + try { + setLoading(true); + onLoading(true); + const details = (await getCampaignDetails( + projectId + )) as CampaignDetails; + setCampaignDetails(details); + } catch { + setError('Failed to load campaign details'); + } finally { + setLoading(false); + onLoading(false); + } + }; + + fetchCampaignDetails(); + }, [projectId, onLoading]); + + const toggleMilestone = (milestoneIndex: number) => { + setExpandedMilestones(prev => + prev.includes(milestoneIndex) + ? prev.filter(i => i !== milestoneIndex) + : [...prev, milestoneIndex] + ); + }; + + const handleLaunch = () => { + if (isConfirmed) { + onLaunch(); + } + }; + + if (loading) { + return ( +
+ +
+ ); + } + + if (error || !campaignDetails) { + return ( +
+

+ {error || 'Campaign details not found'} +

+ +
+ ); + } + + // Use mock data as fallback if API fails + const displayData = campaignDetails || { + id: 'fallback-campaign', + title: 'Boundless', + tagline: 'Trustless, decentralized crowdfunding platform', + description: + 'Boundless is a trustless, decentralized application (dApp) that empowers changemakers and builders to raise funds transparently without intermediaries. Campaigns are structured around clearly defined milestones, with funds held in escrow and released only upon approval.', + category: 'Technology', + fundAmount: 123000, + raisedAmount: 0, + tags: ['web3', 'crowdfunding'], + thumbnail: '/BOUNDLESS.png', + creator: { + name: 'Collins Odumeje', + avatar: 'https://github.com/shadcn.png', + verified: true, + }, + engagement: { + likes: 0, + comments: 0, + backers: 325, + daysLeft: 90, + }, + photos: [ + '/BOUNDLESS.png', + '/BOUNDLESS.png', + '/BOUNDLESS.png', + '/BOUNDLESS.png', + ], + milestones: [ + { + id: 'milestone-1', + title: 'Prototype & Smart Contract Setup', + description: + 'Develop a functional UI prototype for the crowdfunding and grant flow.', + deliveryDate: 'October 10, 2025', + fundPercentage: 25, + fundAmount: 30750, + }, + { + id: 'milestone-2', + title: 'Campaign & Grant Builder Integration', + description: + 'Integrate campaign creation tools and grant builder functionality.', + deliveryDate: 'November 15, 2025', + fundPercentage: 35, + fundAmount: 43050, + }, + { + id: 'milestone-3', + title: 'Platform Launch & Community Building', + description: + 'Launch the platform to the public and build a strong community.', + deliveryDate: 'December 20, 2025', + fundPercentage: 40, + fundAmount: 49200, + }, + ], + status: 'validated', + }; + + return ( +
+ {/* Campaign Banner */} +
+ {displayData.title} +
+ {/* Campaign Header */} +
+
+
+
+ +
+
+

+ {displayData.creator.name} +

+
+
+
+ + {/* Financials */} +
+
+
+

Raised

+

+ ${displayData.raisedAmount.toLocaleString()}.00 +

+
+
+

Target

+

+ ${displayData.fundAmount.toLocaleString()}.00 +

+
+
+ {/* progress bar */} +
+
+
+
+ + {/* Engagement Metrics */} +
+
+
+ + + {displayData.engagement.likes} + +
+
+ + + {displayData.engagement.comments} + +
+
+
+ + + {displayData.engagement.backers} Backers + +
+
+ + + {displayData.engagement.daysLeft} days left + +
+
+
+ + {/* Campaign Details */} +
+

Campaign Details

+

+ {displayData.description} +

+
+ + {/* Tags */} +
+

Tags

+
+ {displayData.tags.map((tag, index) => ( + + #{tag} + + ))} +
+
+ + {/* Campaign Photos */} +
+

Campaign Photos

+
+ {displayData.photos.map((photo, index) => ( +
+ {`Campaign +
+ ))} +
+
+ + {/* Milestones */} +
+

Milestones

+
+ {displayData.milestones.map((milestone, index) => ( +
+ + + {expandedMilestones.includes(index) && ( +
+

+ {milestone.description} +

+
+
+ + {milestone.deliveryDate} +
+
+ + ${milestone.fundAmount.toLocaleString()}.00 +
+
+
+ )} +
+ ))} +
+
+ + {/* Confirmation and Actions */} +
+
+ setIsConfirmed(checked as boolean)} + className='border-[#2B2B2B] bg-[#1A1A1A] data-[state=checked]:bg-primary data-[state=checked]:border-primary' + /> + +
+ +
+ + +
+
+
+ ); +}; + +export default ReviewCampaign; diff --git a/components/project/ShareCampaignModal.tsx b/components/project/ShareCampaignModal.tsx new file mode 100644 index 000000000..59a217e62 --- /dev/null +++ b/components/project/ShareCampaignModal.tsx @@ -0,0 +1,184 @@ +'use client'; + +import React, { useState } from 'react'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { + Copy, + ExternalLink, + MessageCircle, + Twitter, + MessageSquare, + Send, +} from 'lucide-react'; +import { toast } from 'sonner'; + +interface ShareCampaignModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; + campaignLink: string; + campaignTitle: string; +} + +const ShareCampaignModal: React.FC = ({ + open, + onOpenChange, + campaignLink, + campaignTitle, +}) => { + const [copied, setCopied] = useState(false); + + const copyToClipboard = async () => { + try { + await navigator.clipboard.writeText(campaignLink); + setCopied(true); + toast.success('Link copied to clipboard!'); + setTimeout(() => setCopied(false), 2000); + } catch { + toast.error('Failed to copy link'); + } + }; + + const shareToTwitter = () => { + const text = `Check out this amazing campaign: ${campaignTitle}`; + const url = encodeURIComponent(campaignLink); + const twitterUrl = `https://twitter.com/intent/tweet?text=${encodeURIComponent(text)}&url=${url}`; + window.open(twitterUrl, '_blank'); + }; + + const shareToDiscord = () => { + const text = `Check out this amazing campaign: ${campaignTitle}\n${campaignLink}`; + const discordUrl = `https://discord.com/channels/@me?content=${encodeURIComponent(text)}`; + window.open(discordUrl, '_blank'); + }; + + const shareToWhatsApp = () => { + const text = `Check out this amazing campaign: ${campaignTitle}\n${campaignLink}`; + const whatsappUrl = `https://wa.me/?text=${encodeURIComponent(text)}`; + window.open(whatsappUrl, '_blank'); + }; + + const shareToTelegram = () => { + const text = `Check out this amazing campaign: ${campaignTitle}\n${campaignLink}`; + const telegramUrl = `https://t.me/share/url?url=${encodeURIComponent(campaignLink)}&text=${encodeURIComponent(text)}`; + window.open(telegramUrl, '_blank'); + }; + + const shareOptions = [ + { + name: 'Discord', + icon: MessageCircle, + color: 'bg-[#5865F2] hover:bg-[#4752C4]', + onClick: shareToDiscord, + }, + { + name: 'X/Twitter', + icon: Twitter, + color: 'bg-[#1DA1F2] hover:bg-[#1A91DA]', + onClick: shareToTwitter, + }, + { + name: 'WhatsApp', + icon: MessageSquare, + color: 'bg-[#25D366] hover:bg-[#22C55E]', + onClick: shareToWhatsApp, + }, + { + name: 'Telegram', + icon: Send, + color: 'bg-[#0088CC] hover:bg-[#0077B3]', + onClick: shareToTelegram, + }, + ]; + + return ( + + + + + Share Campaign + + + +
+ {/* Campaign Link */} +
+ +
+ + + +
+ {copied && ( +

+ Link copied to clipboard! +

+ )} +
+ + {/* Share Options */} +
+ +
+ {shareOptions.map(option => ( + + ))} +
+
+ + {/* Campaign Preview */} +
+

Preview

+
+

+ {campaignTitle} +

+

+ Check out this amazing campaign on Boundless! +

+

{campaignLink}

+
+
+
+
+
+ ); +}; + +export default ShareCampaignModal; diff --git a/components/project/ValidationFlow.tsx b/components/project/ValidationFlow.tsx index f33a57dce..633e14b66 100644 --- a/components/project/ValidationFlow.tsx +++ b/components/project/ValidationFlow.tsx @@ -23,9 +23,14 @@ interface ValidationFlowProps { onVote?: (projectId: string) => void; onComment?: (projectId: string, comment: string) => void; onReact?: (commentId: string, reaction: string) => void; + onSuccess?: () => void; } -const ValidationFlow: React.FC = ({ project, onVote }) => { +const ValidationFlow: React.FC = ({ + project, + onVote, + onSuccess, +}) => { const [voteCount, setVoteCount] = useState(12); const [commentCount] = useState(4); const [hasVoted, setHasVoted] = useState(false); @@ -264,6 +269,18 @@ const ValidationFlow: React.FC = ({ project, onVote }) => {

Timeline

+ + {/* Proceed to Launch Button */} +
+
+ +
+
); diff --git a/components/project/index.ts b/components/project/index.ts index 3364d2d32..5a8a5c7f4 100644 --- a/components/project/index.ts +++ b/components/project/index.ts @@ -7,3 +7,9 @@ export { default as ProjectSubmissionLoading } from './ProjectSubmissionLoading' export { default as ProjectSubmissionSuccess } from './ProjectSubmissionSuccess'; export { default as ProjectSubmissionFlow } from './ProjectSubmissionFlow'; export { default as MilestoneForm } from './MilestoneForm'; + +// Launch Campaign Components +export { default as ReviewCampaign } from './ReviewCampaign'; +export { default as CampaignLiveSuccess } from './CampaignLiveSuccess'; +export { default as ShareCampaignModal } from './ShareCampaignModal'; +export { default as LaunchCampaignFlow } from './LaunchCampaignFlow'; diff --git a/components/sheet/boundless-sheet.tsx b/components/sheet/boundless-sheet.tsx index 2152cf6fc..67b74beac 100644 --- a/components/sheet/boundless-sheet.tsx +++ b/components/sheet/boundless-sheet.tsx @@ -136,7 +136,7 @@ const BoundlessSheet: React.FC = ({
diff --git a/env.example b/env.example deleted file mode 100644 index c065f8ea7..000000000 --- a/env.example +++ /dev/null @@ -1,52 +0,0 @@ -# Stellar Network Configuration -NEXT_PUBLIC_STELLAR_NETWORK=testnet -# Options: testnet, public - -# Application Configuration -NEXT_PUBLIC_APP_URL=http://localhost:3000 -# Your application's public URL - -# API Configuration -NEXT_PUBLIC_API_URL=http://localhost:8000/api -# Your backend API URL - -# WalletConnect Configuration -NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=your_wallet_connect_project_id -# Get this from https://cloud.walletconnect.com/ - -# Stellar Horizon URLs (optional - defaults will be used) -NEXT_PUBLIC_HORIZON_TESTNET_URL=https://horizon-testnet.stellar.org -NEXT_PUBLIC_HORIZON_PUBLIC_URL=https://horizon.stellar.org - -# Application Metadata -NEXT_PUBLIC_APP_NAME=Boundless -NEXT_PUBLIC_APP_DESCRIPTION=Stellar-based application -NEXT_PUBLIC_APP_ICON=/logo.svg - -# Development Settings -NEXT_PUBLIC_DEBUG_MODE=false -# Enable debug logging for wallet operations - -# Feature Flags -NEXT_PUBLIC_ENABLE_WALLET_CONNECT=true -NEXT_PUBLIC_ENABLE_MULTI_WALLET=true -NEXT_PUBLIC_ENABLE_NETWORK_SWITCHING=true - -# NextAuth Configuration -NEXTAUTH_SECRET=your_nextauth_secret_here -# Generate a random string for this secret -# You can use: openssl rand -base64 32 - -NEXTAUTH_URL=http://localhost:3000 -# Your application's public URL (same as NEXT_PUBLIC_APP_URL) - -# Google OAuth Configuration -GOOGLE_CLIENT_ID=your_google_client_id -# Get this from Google Cloud Console - -GOOGLE_CLIENT_SECRET=your_google_client_secret -# Get this from Google Cloud Console - -# Environment -NODE_ENV=development -# Options: development, production, test diff --git a/lib/api/api.ts b/lib/api/api.ts index c60fb537c..75db0e5b7 100644 --- a/lib/api/api.ts +++ b/lib/api/api.ts @@ -28,6 +28,11 @@ export interface RequestConfig { // Token refresh function const refreshAccessToken = async (): Promise => { try { + // Only access cookies on client side + if (typeof window === 'undefined') { + return null; + } + const refreshToken = Cookies.get('refreshToken'); if (!refreshToken) { return null; @@ -74,10 +79,13 @@ const createClientApi = (): AxiosInstance => { config => { config.withCredentials = true; - const accessToken = Cookies.get('accessToken'); - if (accessToken && !config.headers?.Authorization) { - config.headers = config.headers || {}; - config.headers.Authorization = `Bearer ${accessToken}`; + // Only access cookies on client side + if (typeof window !== 'undefined') { + const accessToken = Cookies.get('accessToken'); + if (accessToken && !config.headers?.Authorization) { + config.headers = config.headers || {}; + config.headers.Authorization = `Bearer ${accessToken}`; + } } return config; diff --git a/lib/api/project.ts b/lib/api/project.ts index d6a9bd31c..c778071e8 100644 --- a/lib/api/project.ts +++ b/lib/api/project.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { mockCampaignDetails } from '../mock'; import api from './api'; import { ProjectInitRequest } from './types'; @@ -5,3 +7,42 @@ export const initProject = async (data: ProjectInitRequest) => { const res = await api.post('/projects', data); return res; }; + +export const getCampaignDetails = async (_projectId: string) => { + // Mock implementation for now + return new Promise(resolve => { + setTimeout(() => { + resolve(mockCampaignDetails); + }, 1000); + }); +}; + +export const launchCampaign = async (_projectId: string) => { + // Mock implementation for now + return new Promise(resolve => { + setTimeout(() => { + resolve({ + success: true, + message: 'Campaign launched successfully', + data: { + campaignId: 'launched-campaign-123', + shareLink: 'https://boundless.com/campaigns/launched-campaign-123', + }, + }); + }, 2000); + }); +}; + +export const generateCampaignLink = async (_projectId: string) => { + // Mock implementation for now + return new Promise(resolve => { + setTimeout(() => { + resolve({ + success: true, + data: { + shareLink: 'https://boundless.com/campaigns/' + _projectId, + }, + }); + }, 500); + }); +}; diff --git a/lib/api/types.ts b/lib/api/types.ts index c3322aa25..5d9505e67 100644 --- a/lib/api/types.ts +++ b/lib/api/types.ts @@ -250,3 +250,59 @@ export interface ProjectInitResponse { }; [key: string]: unknown; } + +// Campaign Review and Launch Types +export interface CampaignDetails { + id: string; + title: string; + tagline: string; + description: string; + category: string; + fundAmount: number; + raisedAmount: number; + tags: string[]; + thumbnail: string; + creator: { + name: string; + avatar: string; + verified: boolean; + }; + engagement: { + likes: number; + comments: number; + backers: number; + daysLeft: number; + }; + photos: string[]; + milestones: CampaignMilestone[]; + status: string; +} + +export interface CampaignMilestone { + id: string; + title: string; + description: string; + deliveryDate: string; + fundPercentage: number; + fundAmount: number; +} + +export interface LaunchCampaignRequest { + projectId: string; +} + +export interface LaunchCampaignResponse { + success: boolean; + message: string; + data: { + campaignId: string; + shareLink: string; + }; +} + +export interface ShareLinkResponse { + success: boolean; + data: { + shareLink: string; + }; +} diff --git a/lib/mock.ts b/lib/mock.ts index 01bb3ad99..1dae701cc 100644 --- a/lib/mock.ts +++ b/lib/mock.ts @@ -285,3 +285,64 @@ export const mockProjects: Project[] = [ // updatedAt: "2024-01-21T16:45:00Z", // }, ]; + +// Mock campaign details for testing +export const mockCampaignDetails = { + id: 'campaign-123', + title: 'Boundless', + tagline: 'Trustless, decentralized crowdfunding platform', + description: + 'Boundless is a trustless, decentralized application (dApp) that empowers changemakers and builders to raise funds transparently without intermediaries. Campaigns are structured around clearly defined milestones, with funds held in escrow and released only upon approval. Grant creators can launch programs with rule-based logic, and applicants can apply with confidence knowing their work will be fairly evaluated.', + category: 'Technology', + fundAmount: 123000, + raisedAmount: 0, + tags: ['web3', 'crowdfunding'], + thumbnail: '/BOUNDLESS.png', + creator: { + name: 'Collins Odumeje', + avatar: 'https://github.com/shadcn.png', + verified: true, + }, + engagement: { + likes: 0, + comments: 0, + backers: 325, + daysLeft: 90, + }, + photos: [ + '/BOUNDLESS.png', + '/BOUNDLESS.png', + '/BOUNDLESS.png', + '/BOUNDLESS.png', + ], + milestones: [ + { + id: 'milestone-1', + title: 'Prototype & Smart Contract Setup', + description: + 'Develop a functional UI prototype for the crowdfunding and grant flow. Simultaneously, implement and test Soroban smart contracts for escrow logic, milestone validation, and secure fund handling.', + deliveryDate: 'October 10, 2025', + fundPercentage: 25, + fundAmount: 30750, + }, + { + id: 'milestone-2', + title: 'Campaign & Grant Builder Integration', + description: + 'Integrate campaign creation tools and grant builder functionality into the platform with advanced features and user management.', + deliveryDate: 'November 15, 2025', + fundPercentage: 35, + fundAmount: 43050, + }, + { + id: 'milestone-3', + title: 'Platform Launch & Community Building', + description: + 'Launch the platform to the public and build a strong community of users and contributors with marketing and partnership initiatives.', + deliveryDate: 'December 20, 2025', + fundPercentage: 40, + fundAmount: 49200, + }, + ], + status: 'validated', +}; diff --git a/lib/stores/auth-store.ts b/lib/stores/auth-store.ts index 8e0f55d7e..592716a9d 100644 --- a/lib/stores/auth-store.ts +++ b/lib/stores/auth-store.ts @@ -38,6 +38,18 @@ export interface AuthState { updateUser: (updates: Partial) => void; } +// Safe localStorage access for SSR +const getStorage = () => { + if (typeof window !== 'undefined') { + return localStorage; + } + return { + getItem: () => null, + setItem: () => {}, + removeItem: () => {}, + }; +}; + export const useAuthStore = create()( persist( (set, get) => ({ @@ -57,17 +69,19 @@ export const useAuthStore = create()( setTokens: (accessToken, refreshToken) => { set({ accessToken, refreshToken }); - // Update cookies - if (accessToken) { - Cookies.set('accessToken', accessToken); - } else { - Cookies.remove('accessToken'); - } + // Update cookies only on client side + if (typeof window !== 'undefined') { + if (accessToken) { + Cookies.set('accessToken', accessToken); + } else { + Cookies.remove('accessToken'); + } - if (refreshToken) { - Cookies.set('refreshToken', refreshToken); - } else { - Cookies.remove('refreshToken'); + if (refreshToken) { + Cookies.set('refreshToken', refreshToken); + } else { + Cookies.remove('refreshToken'); + } } }, @@ -203,9 +217,11 @@ export const useAuthStore = create()( error: null, }); - // Clear cookies - Cookies.remove('accessToken'); - Cookies.remove('refreshToken'); + // Clear cookies only on client side + if (typeof window !== 'undefined') { + Cookies.remove('accessToken'); + Cookies.remove('refreshToken'); + } }, updateUser: updates => { @@ -217,18 +233,7 @@ export const useAuthStore = create()( }), { name: 'auth-storage', - storage: createJSONStorage(() => { - // Check if we're in a browser environment - if (typeof window !== 'undefined') { - return localStorage; - } - // Return a mock storage for SSR - return { - getItem: () => null, - setItem: () => {}, - removeItem: () => {}, - }; - }), + storage: createJSONStorage(() => getStorage()), partialize: state => ({ user: state.user, accessToken: state.accessToken, @@ -236,11 +241,11 @@ export const useAuthStore = create()( isAuthenticated: state.isAuthenticated, }), onRehydrateStorage: () => state => { - // Rehydrate cookies from localStorage on app start - if (state?.accessToken) { + // Rehydrate cookies from localStorage on app start (client side only) + if (typeof window !== 'undefined' && state?.accessToken) { Cookies.set('accessToken', state.accessToken); } - if (state?.refreshToken) { + if (typeof window !== 'undefined' && state?.refreshToken) { Cookies.set('refreshToken', state.refreshToken); } }, diff --git a/lib/utils.ts b/lib/utils.ts index dfc3c0395..8e7abc25e 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -91,3 +91,7 @@ export const maskEmail = (email: string) => { const [username, domain] = email.split('@'); return `${username.slice(0, 2)}*****@${domain}`; }; + +export const generateCampaignLink = async (projectId: string) => { + return `${process.env.NEXT_PUBLIC_APP_URL}/project/${projectId}`; +}; diff --git a/package-lock.json b/package-lock.json index 5365fcb91..c252432b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,17 +45,17 @@ "date-fns": "^4.1.0", "embla-carousel-autoplay": "^8.6.0", "embla-carousel-react": "^8.6.0", - "framer-motion": "^12.23.9", + "framer-motion": "^12.23.12", "input-otp": "^1.4.2", "js-cookie": "^3.0.5", "lucide-react": "^0.525.0", "next": "15.4.4", - "next-auth": "^5.0.0-beta.29", + "next-auth": "^4.24.11", "next-themes": "^0.4.6", "react": "19.1.0", "react-day-picker": "^9.8.0", "react-dom": "19.1.0", - "react-hook-form": "^7.61.1", + "react-hook-form": "^7.62.0", "react-resizable-panels": "^3.0.3", "recharts": "^2.15.4", "sonner": "^2.0.6", @@ -118,35 +118,6 @@ "node": ">=6.0.0" } }, - "node_modules/@auth/core": { - "version": "0.40.0", - "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.40.0.tgz", - "integrity": "sha512-n53uJE0RH5SqZ7N1xZoMKekbHfQgjd0sAEyUbE+IYJnmuQkbvuZnXItCU7d+i7Fj8VGOgqvNO7Mw4YfBTlZeQw==", - "license": "ISC", - "dependencies": { - "@panva/hkdf": "^1.2.1", - "jose": "^6.0.6", - "oauth4webapi": "^3.3.0", - "preact": "10.24.3", - "preact-render-to-string": "6.5.11" - }, - "peerDependencies": { - "@simplewebauthn/browser": "^9.0.1", - "@simplewebauthn/server": "^9.0.2", - "nodemailer": "^6.8.0" - }, - "peerDependenciesMeta": { - "@simplewebauthn/browser": { - "optional": true - }, - "@simplewebauthn/server": { - "optional": true - }, - "nodemailer": { - "optional": true - } - } - }, "node_modules/@babel/runtime": { "version": "7.28.2", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.2.tgz", @@ -9286,6 +9257,15 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cookie-es": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.2.tgz", @@ -10909,12 +10889,12 @@ } }, "node_modules/framer-motion": { - "version": "12.23.9", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.9.tgz", - "integrity": "sha512-TqEHXj8LWfQSKqfdr5Y4mYltYLw96deu6/K9kGDd+ysqRJPNwF9nb5mZcrLmybHbU7gcJ+HQar41U3UTGanbbQ==", + "version": "12.23.12", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.12.tgz", + "integrity": "sha512-6e78rdVtnBvlEVgu6eFEAgG9v3wLnYEboM8I5O5EXvfKC8gxGQB8wXJdhkMy10iVcn05jl6CNw7/HTsTCfwcWg==", "license": "MIT", "dependencies": { - "motion-dom": "^12.23.9", + "motion-dom": "^12.23.12", "motion-utils": "^12.23.6", "tslib": "^2.4.0" }, @@ -12152,9 +12132,9 @@ } }, "node_modules/jose": { - "version": "6.0.12", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.0.12.tgz", - "integrity": "sha512-T8xypXs8CpmiIi78k0E+Lk7T2zlK4zDyg+o1CZ4AkOHgDg98ogdP2BeZ61lTFKFyoEwJ9RgAgN+SdM3iPgNonQ==", + "version": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/panva" @@ -13173,9 +13153,9 @@ } }, "node_modules/motion-dom": { - "version": "12.23.9", - "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.9.tgz", - "integrity": "sha512-6Sv++iWS8XMFCgU1qwKj9l4xuC47Hp4+2jvPfyTXkqDg2tTzSgX6nWKD4kNFXk0k7llO59LZTPuJigza4A2K1A==", + "version": "12.23.12", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.12.tgz", + "integrity": "sha512-RcR4fvMCTESQBD/uKQe49D5RUeDOokkGRmz4ceaJKDBgHYtZtntC/s2vLvY38gqGaytinij/yi3hMcWVcEF5Kw==", "license": "MIT", "dependencies": { "motion-utils": "^12.23.6" @@ -13388,25 +13368,30 @@ } }, "node_modules/next-auth": { - "version": "5.0.0-beta.29", - "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-5.0.0-beta.29.tgz", - "integrity": "sha512-Ukpnuk3NMc/LiOl32njZPySk7pABEzbjhMUFd5/n10I0ZNC7NCuVv8IY2JgbDek2t/PUOifQEoUiOOTLy4os5A==", + "version": "4.24.11", + "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.24.11.tgz", + "integrity": "sha512-pCFXzIDQX7xmHFs4KVH4luCjaCbuPRtZ9oBUjUhOk84mZ9WVPf94n87TxYI4rSRf9HmfHEF8Yep3JrYDVOo3Cw==", "license": "ISC", "dependencies": { - "@auth/core": "0.40.0" + "@babel/runtime": "^7.20.13", + "@panva/hkdf": "^1.0.2", + "cookie": "^0.7.0", + "jose": "^4.15.5", + "oauth": "^0.9.15", + "openid-client": "^5.4.0", + "preact": "^10.6.3", + "preact-render-to-string": "^5.1.19", + "uuid": "^8.3.2" }, "peerDependencies": { - "@simplewebauthn/browser": "^9.0.1", - "@simplewebauthn/server": "^9.0.2", - "next": "^14.0.0-0 || ^15.0.0-0", + "@auth/core": "0.34.2", + "next": "^12.2.5 || ^13 || ^14 || ^15", "nodemailer": "^6.6.5", - "react": "^18.2.0 || ^19.0.0-0" + "react": "^17.0.2 || ^18 || ^19", + "react-dom": "^17.0.2 || ^18 || ^19" }, "peerDependenciesMeta": { - "@simplewebauthn/browser": { - "optional": true - }, - "@simplewebauthn/server": { + "@auth/core": { "optional": true }, "nodemailer": { @@ -13510,14 +13495,11 @@ "node": ">=0.10.0" } }, - "node_modules/oauth4webapi": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.6.0.tgz", - "integrity": "sha512-OwXPTXjKPOldTpAa19oksrX9TYHA0rt+VcUFTkJ7QKwgmevPpNm9Cn5vFZUtIo96FiU6AfPuUUGzoXqgOzibWg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/panva" - } + "node_modules/oauth": { + "version": "0.9.15", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", + "integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==", + "license": "MIT" }, "node_modules/object-assign": { "version": "4.1.1", @@ -13528,6 +13510,15 @@ "node": ">=0.10.0" } }, + "node_modules/object-hash": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -13666,6 +13657,15 @@ "ufo": "^1.5.4" } }, + "node_modules/oidc-token-hash": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.1.1.tgz", + "integrity": "sha512-D7EmwxJV6DsEB6vOFLrBM2OzsVgQzgPWyHlV2OOAVj772n+WTXpudC9e9u5BVKQnYwaD30Ivhi9b+4UeBcGu9g==", + "license": "MIT", + "engines": { + "node": "^10.13.0 || >=12.0.0" + } + }, "node_modules/on-exit-leak-free": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-0.2.0.tgz", @@ -13697,6 +13697,39 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/openid-client": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.7.1.tgz", + "integrity": "sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==", + "license": "MIT", + "dependencies": { + "jose": "^4.15.9", + "lru-cache": "^6.0.0", + "object-hash": "^2.2.0", + "oidc-token-hash": "^5.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/openid-client/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/openid-client/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -13930,9 +13963,9 @@ } }, "node_modules/preact": { - "version": "10.24.3", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz", - "integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==", + "version": "10.27.0", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.27.0.tgz", + "integrity": "sha512-/DTYoB6mwwgPytiqQTh/7SFRL98ZdiD8Sk8zIUVOxtwq4oWcwrcd1uno9fE/zZmUaUrFNYzbH14CPebOz9tZQw==", "license": "MIT", "funding": { "type": "opencollective", @@ -13940,10 +13973,13 @@ } }, "node_modules/preact-render-to-string": { - "version": "6.5.11", - "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.5.11.tgz", - "integrity": "sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==", + "version": "5.2.6", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.6.tgz", + "integrity": "sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==", "license": "MIT", + "dependencies": { + "pretty-format": "^3.8.0" + }, "peerDependencies": { "preact": ">=10" } @@ -13987,6 +14023,12 @@ "node": ">=6.0.0" } }, + "node_modules/pretty-format": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz", + "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==", + "license": "MIT" + }, "node_modules/process-warning": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-1.0.0.tgz", @@ -14180,9 +14222,9 @@ } }, "node_modules/react-hook-form": { - "version": "7.61.1", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.61.1.tgz", - "integrity": "sha512-2vbXUFDYgqEgM2RcXcAT2PwDW/80QARi+PKmHy5q2KhuKvOlG8iIYgf7eIlIANR5trW9fJbP4r5aub3a4egsew==", + "version": "7.62.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.62.0.tgz", + "integrity": "sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA==", "license": "MIT", "engines": { "node": ">=18.0.0" diff --git a/package.json b/package.json index dbcbdced6..496e88312 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "date-fns": "^4.1.0", "embla-carousel-autoplay": "^8.6.0", "embla-carousel-react": "^8.6.0", - "framer-motion": "^12.23.9", + "framer-motion": "^12.23.12", "input-otp": "^1.4.2", "js-cookie": "^3.0.5", "lucide-react": "^0.525.0", @@ -70,7 +70,7 @@ "react": "19.1.0", "react-day-picker": "^9.8.0", "react-dom": "19.1.0", - "react-hook-form": "^7.61.1", + "react-hook-form": "^7.62.0", "react-resizable-panels": "^3.0.3", "recharts": "^2.15.4", "sonner": "^2.0.6", diff --git a/public/campaign-pics.png b/public/campaign-pics.png new file mode 100644 index 000000000..f7ec0bf1e Binary files /dev/null and b/public/campaign-pics.png differ diff --git a/public/check.png b/public/check.png new file mode 100644 index 000000000..6bf65cd9b Binary files /dev/null and b/public/check.png differ diff --git a/public/nobackers.png b/public/nobackers.png new file mode 100644 index 000000000..3d5db78a2 Binary files /dev/null and b/public/nobackers.png differ diff --git a/public/profile.png b/public/profile.png new file mode 100644 index 000000000..177208e49 Binary files /dev/null and b/public/profile.png differ diff --git a/public/verify.png b/public/verify.png new file mode 100644 index 000000000..53ac189c8 Binary files /dev/null and b/public/verify.png differ diff --git a/tsconfig.json b/tsconfig.json index f50cef8bc..b61704777 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -27,7 +27,8 @@ "**/*.tsx", ".next/types/**/*.ts", "next-env.d.ts", - "./.next/types/**/*.ts" + "./.next/types/**/*.ts", + "types/**/*.ts" ], "exclude": ["node_modules"] }