diff --git a/METADATA_README.md b/METADATA_README.md new file mode 100644 index 000000000..c9ac13bcc --- /dev/null +++ b/METADATA_README.md @@ -0,0 +1,275 @@ +# Dynamic Metadata & SEO System + +This document explains how to use the new dynamic metadata and SEO system for the Boundless project. + +## Overview + +The system provides dynamic metadata generation for different pages and blog functionality, ensuring optimal SEO performance across the entire application. + +## Features + +- **Dynamic Page Metadata**: Each page gets unique, SEO-optimized metadata +- **Blog Support**: Specialized metadata handling for blog posts +- **Structured Data**: JSON-LD structured data for better search engine understanding +- **Sitemap Generation**: Automatic sitemap creation +- **Robots.txt**: Proper crawler control +- **Open Graph & Twitter Cards**: Social media optimization + +## File Structure + +``` +lib/ +├── metadata.ts # Core metadata configuration and generation +├── structured-data.ts # JSON-LD structured data utilities +app/ +├── sitemap.ts # Dynamic sitemap generation +├── robots.ts # Robots.txt configuration +└── (landing)/ + ├── layout.tsx # Landing layout with home page metadata + ├── about/page.tsx # About page with dynamic metadata + ├── projects/page.tsx # Projects page with dynamic metadata + ├── blog/ + │ ├── page.tsx # Blog listing with dynamic metadata + │ └── [slug]/page.tsx # Individual blog posts with dynamic metadata + └── ... # Other pages with dynamic metadata +components/ +└── waitlist/ + └── WaitlistForm.tsx # Client Component for interactive form +``` + +## Usage + +### Basic Page Metadata + +For any landing page, simply import and use the `generatePageMetadata` function: + +```tsx +import { Metadata } from 'next'; +import { generatePageMetadata } from '@/lib/metadata'; + +export const metadata: Metadata = generatePageMetadata('about'); + +const AboutPage = () => { + return
About Page Content
; +}; + +export default AboutPage; +``` + +### Available Page Keys + +The system supports these predefined page keys: + +- `home` - Homepage +- `about` - About page +- `projects` - Projects page +- `grants` - Grants page +- `hackathons` - Hackathons page +- `contact` - Contact page +- `privacy` - Privacy policy +- `terms` - Terms of service +- `disclaimer` - Disclaimer +- `codeOfConduct` - Code of conduct +- `waitlist` - Waitlist page +- `blog` - Blog listing page + +### Custom Page Metadata + +You can override or extend the default metadata: + +```tsx +export const metadata: Metadata = generatePageMetadata('about', { + title: 'Custom About Title', + description: 'Custom description for this specific about page', + keywords: ['custom', 'keywords'], + ogImage: '/custom-og-image.png', +}); +``` + +### Blog Post Metadata + +For blog posts, use the `generateBlogMetadata` function: + +```tsx +export async function generateMetadata({ + params, +}: BlogPostPageProps): Promise { + const post = await fetchBlogPost(params.slug); + + return generateBlogMetadata( + { + title: post.title, + description: post.description, + author: post.author, + publishedTime: post.publishedAt, + tags: post.tags, + category: post.category, + }, + params.slug + ); +} +``` + +### Handling Client Components with Metadata + +When you need both metadata and client-side interactivity, use **component composition**: + +1. **Keep the page as a Server Component** for metadata generation +2. **Create a separate Client Component** for interactive features +3. **Import the Client Component** into the Server Component page + +**Example - Waitlist Page:** + +```tsx +// app/(landing)/waitlist/page.tsx (Server Component) +import { Metadata } from 'next'; +import { generatePageMetadata } from '@/lib/metadata'; +import WaitlistForm from '@/components/waitlist/WaitlistForm'; + +export const metadata: Metadata = generatePageMetadata('waitlist'); + +export default function WaitlistPage() { + return ; +} +``` + +```tsx +// components/waitlist/WaitlistForm.tsx (Client Component) +'use client'; + +export default function WaitlistForm() { + // All your interactive logic, GSAP animations, form handling + return
Interactive Form Content
; +} +``` + +**Why This Approach?** + +- ✅ **Metadata works correctly** - Server Component can export metadata +- ✅ **Interactivity preserved** - Client Component handles all client-side features +- ✅ **No layout nesting** - Clean, maintainable structure +- ✅ **Follows Next.js best practices** - Proper separation of concerns +- ✅ **Better performance** - Only interactive parts are client-side + +### Adding New Pages + +To add metadata for a new page: + +1. **Add to `pageMetadata` in `lib/metadata.ts`:** + +```tsx +export const pageMetadata: Record = { + // ... existing pages + newPage: { + title: 'New Page - Boundless', + description: 'Description for the new page', + keywords: ['new', 'page', 'keywords'], + ogImage: '/new-page-og.png', + }, +}; +``` + +2. **Use in your page component:** + +```tsx +export const metadata: Metadata = generatePageMetadata('newPage'); +``` + +### Structured Data + +The system automatically includes structured data for: + +- **Organization**: Company information and contact details +- **Website**: Search functionality and site information +- **Blog Posts**: Article metadata for better search results + +### Environment Variables + +Set these environment variables for production: + +```env +NEXT_PUBLIC_SITE_URL=https://yourdomain.com +``` + +## SEO Best Practices + +### 1. Unique Titles and Descriptions + +Each page has unique, descriptive titles and meta descriptions. + +### 2. Proper Open Graph Tags + +Social media sharing is optimized with proper Open Graph tags. + +### 3. Twitter Cards + +Twitter sharing includes large image cards for better engagement. + +### 4. Canonical URLs + +Prevents duplicate content issues with proper canonical URLs. + +### 5. Structured Data + +JSON-LD structured data helps search engines understand your content. + +### 6. Sitemap + +Automatic sitemap generation for better search engine indexing. + +### 7. Robots.txt + +Proper crawler control for better SEO performance. + +## Blog Integration + +The system is designed to work seamlessly with blog functionality: + +- **Dynamic Blog Post Metadata**: Each blog post gets unique metadata +- **Author Information**: Proper author attribution for blog posts +- **Publishing Dates**: Publication and modification dates for better SEO +- **Tags and Categories**: Proper categorization for search engines +- **Article Schema**: Blog posts use Article schema markup + +## Performance Considerations + +- Metadata is generated at build time for static pages +- Blog post metadata is generated dynamically +- Structured data is included in the HTML head +- No client-side JavaScript required for metadata +- Client Components are only loaded where needed + +## Troubleshooting + +### Common Issues + +1. **Metadata not updating**: Ensure you're using the correct page key +2. **Structured data errors**: Check the JSON-LD syntax in browser dev tools +3. **Sitemap not generating**: Verify the `generateSitemapData` function is working +4. **Build errors with 'use client'**: Use component composition instead of multiple layouts + +### Debugging + +Use browser dev tools to inspect: + +- Page title and meta tags +- Open Graph tags +- Twitter Card tags +- Structured data (JSON-LD) + +## Future Enhancements + +- **CMS Integration**: Connect with headless CMS for dynamic content +- **Analytics Integration**: Track metadata performance +- **A/B Testing**: Test different metadata variations +- **Internationalization**: Multi-language metadata support +- **Dynamic OG Images**: Generate custom Open Graph images + +## Support + +For questions or issues with the metadata system, refer to: + +- This documentation +- The metadata configuration files +- Next.js metadata documentation +- Schema.org structured data guidelines diff --git a/WALLET_PROTECTION_README.md b/WALLET_PROTECTION_README.md new file mode 100644 index 000000000..6a845fdc9 --- /dev/null +++ b/WALLET_PROTECTION_README.md @@ -0,0 +1,275 @@ +# Wallet Protection System + +## Overview + +The Boundless platform now includes a comprehensive wallet protection system that ensures all critical blockchain actions require wallet connection before execution. This system provides a seamless user experience while maintaining security and transparency. + +## Components + +### 1. useWalletProtection Hook + +**Location:** `hooks/use-wallet-protection.ts` + +A custom React hook that provides wallet protection functionality for any component that needs to perform blockchain actions. + +#### Usage + +```typescript +import { useWalletProtection } from '@/hooks/use-wallet-protection'; + +const MyComponent = () => { + const { requireWallet, showWalletModal, handleWalletConnected, closeWalletModal } = useWalletProtection({ + actionName: 'perform this action' + }); + + const handleAction = () => { + requireWallet(() => { + // Your blockchain action here + console.log('Action executed with wallet connected'); + }); + }; + + return ( + <> + + + + + ); +}; +``` + +#### Options + +- `actionName` (string): Human-readable description of the action requiring wallet connection +- `showModal` (boolean, optional): Whether to show the modal (default: true) + +#### Return Values + +- `requireWallet(callback)`: Function that checks wallet connection and executes callback if connected +- `isConnected`: Boolean indicating if wallet is currently connected +- `publicKey`: Current wallet public key +- `showWalletModal`: Boolean controlling modal visibility +- `handleWalletConnected`: Function to call when wallet is successfully connected +- `closeWalletModal`: Function to close the modal + +### 2. WalletRequiredModal Component + +**Location:** `components/wallet/WalletRequiredModal.tsx` + +A modal component that appears when wallet connection is required for an action. + +#### Props + +- `open` (boolean): Controls modal visibility +- `onOpenChange` (function): Callback when modal open state changes +- `actionName` (string): Description of the action requiring wallet +- `onWalletConnected` (function, optional): Callback when wallet is successfully connected + +#### Features + +- Clear explanation of why wallet connection is needed +- Benefits of wallet connection +- Integrated wallet connection button +- Seamless flow from modal to wallet connection + +## Protected Actions + +The following critical blockchain actions now require wallet connection: + +### 1. Launch Campaign + +**Components:** `LaunchCampaignFlow.tsx` + +- **Action:** Deploying smart escrow contracts +- **Protection:** Prevents campaign launch without wallet connection + +### 2. Back Project + +**Components:** `back-project-form.tsx` + +- **Action:** Contributing funds to projects +- **Protection:** Ensures secure transaction signing + +### 3. Project Initialization + +**Components:** `Initialize.tsx` + +- **Action:** Creating new projects on blockchain +- **Protection:** Validates creator identity and wallet ownership + +### 4. Milestone Submission + +**Components:** `MilestoneSubmissionModal.tsx`, `MilestoneSubmissionPage.tsx` + +- **Action:** Submitting milestone proofs for review +- **Protection:** Ensures authentic submissions from project creators + +### 5. Project Voting + +**Components:** `ValidationFlow.tsx`, `project-card.tsx` + +- **Action:** Voting on project validation +- **Protection:** Prevents duplicate voting and ensures authentic votes + +## User Flow + +1. **User clicks action button** (e.g., "Launch Campaign") +2. **Wallet check:** System checks if wallet is connected +3. **If not connected:** WalletRequiredModal appears with explanation +4. **User clicks "Connect Wallet":** Wallet connection modal opens +5. **User connects wallet:** Modal closes, action proceeds automatically +6. **If already connected:** Action proceeds immediately + +## Error Handling + +The system includes comprehensive error handling: + +- **Connection failures:** Clear error messages with retry options +- **Network issues:** Graceful fallback and user guidance +- **Wallet errors:** Specific error messages for different wallet types +- **Transaction failures:** Detailed feedback for debugging + +## Integration Examples + +### Basic Integration + +```typescript +// Before (no wallet protection) +const handleAction = () => { + performBlockchainAction(); +}; + +// After (with wallet protection) +const { + requireWallet, + showWalletModal, + handleWalletConnected, + closeWalletModal, +} = useWalletProtection({ + actionName: 'perform blockchain action', +}); + +const handleAction = () => { + requireWallet(() => { + performBlockchainAction(); + }); +}; +``` + +### Advanced Integration + +```typescript +const MyComponent = () => { + const { requireWallet, showWalletModal, handleWalletConnected, closeWalletModal } = useWalletProtection({ + actionName: 'launch campaign', + showModal: true + }); + + const handleLaunch = async () => { + requireWallet(async () => { + try { + // Show loading state + setIsLoading(true); + + // Perform blockchain action + const result = await launchCampaign(projectId); + + // Handle success + toast.success('Campaign launched successfully!'); + onSuccess(result); + } catch (error) { + // Handle error + toast.error('Failed to launch campaign'); + console.error(error); + } finally { + setIsLoading(false); + } + }); + }; + + return ( + <> + + + + + ); +}; +``` + +## Security Benefits + +1. **Prevents unauthorized actions:** Users cannot perform blockchain actions without wallet connection +2. **Ensures transaction signing:** All actions require proper wallet authentication +3. **Prevents duplicate submissions:** Wallet connection provides unique identity verification +4. **Audit trail:** All actions are traceable to specific wallet addresses +5. **Fraud prevention:** Reduces risk of fake submissions and votes + +## User Experience Benefits + +1. **Clear messaging:** Users understand why wallet connection is needed +2. **Seamless flow:** Smooth transition from action to wallet connection +3. **Consistent experience:** Same protection pattern across all blockchain actions +4. **Error recovery:** Clear guidance when things go wrong +5. **No breaking changes:** Existing functionality preserved with added security + +## Testing + +To test the wallet protection system: + +1. **Without wallet connected:** + - Click any protected action button + - Verify modal appears with explanation + - Test wallet connection flow + +2. **With wallet connected:** + - Connect wallet first + - Click protected action button + - Verify action proceeds immediately + +3. **Error scenarios:** + - Test with network issues + - Test with wallet connection failures + - Verify error messages are clear and helpful + +## Future Enhancements + +Potential improvements for the wallet protection system: + +1. **Multi-wallet support:** Support for multiple connected wallets +2. **Permission levels:** Different wallet requirements for different actions +3. **Batch operations:** Support for multiple actions requiring single wallet connection +4. **Offline mode:** Graceful handling when blockchain is unavailable +5. **Analytics:** Track wallet connection success rates and user behavior + +## Troubleshooting + +### Common Issues + +1. **Modal not appearing:** Check that `showModal` option is not set to false +2. **Wallet connection fails:** Verify wallet extension is installed and unlocked +3. **Action not executing:** Ensure callback is properly passed to `requireWallet` +4. **TypeScript errors:** Check that all required props are provided to components + +### Debug Mode + +Enable debug logging by setting environment variable: + +```bash +NEXT_PUBLIC_DEBUG_WALLET=true +``` + +This will log wallet connection attempts and errors to the console for debugging purposes. diff --git a/app/(landing)/about/page.tsx b/app/(landing)/about/page.tsx index 316d4af44..472028009 100644 --- a/app/(landing)/about/page.tsx +++ b/app/(landing)/about/page.tsx @@ -1,4 +1,8 @@ import React from 'react'; +import { Metadata } from 'next'; +import { generatePageMetadata } from '@/lib/metadata'; + +export const metadata: Metadata = generatePageMetadata('about'); const AboutPage = () => { return ( diff --git a/app/(landing)/blog/page.tsx b/app/(landing)/blog/page.tsx new file mode 100644 index 000000000..064667571 --- /dev/null +++ b/app/(landing)/blog/page.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { Metadata } from 'next'; +import { generatePageMetadata } from '@/lib/metadata'; + +export const metadata: Metadata = generatePageMetadata('blog'); + +const BlogPage = () => { + return ( +
+ Blog Page +
+ ); +}; + +export default BlogPage; diff --git a/app/(landing)/code-of-conduct/page.tsx b/app/(landing)/code-of-conduct/page.tsx index 111b92ea7..758e294c6 100644 --- a/app/(landing)/code-of-conduct/page.tsx +++ b/app/(landing)/code-of-conduct/page.tsx @@ -1,4 +1,8 @@ import React from 'react'; +import { Metadata } from 'next'; +import { generatePageMetadata } from '@/lib/metadata'; + +export const metadata: Metadata = generatePageMetadata('codeOfConduct'); const CodeOfConductPage = () => { return ( diff --git a/app/(landing)/contact/page.tsx b/app/(landing)/contact/page.tsx index 329b3f78e..0495819e5 100644 --- a/app/(landing)/contact/page.tsx +++ b/app/(landing)/contact/page.tsx @@ -1,4 +1,8 @@ import React from 'react'; +import { Metadata } from 'next'; +import { generatePageMetadata } from '@/lib/metadata'; + +export const metadata: Metadata = generatePageMetadata('contact'); const ContactPage = () => { return ( diff --git a/app/(landing)/disclaimer/page.tsx b/app/(landing)/disclaimer/page.tsx index a4bf39da8..26555a0b9 100644 --- a/app/(landing)/disclaimer/page.tsx +++ b/app/(landing)/disclaimer/page.tsx @@ -1,4 +1,8 @@ import React from 'react'; +import { Metadata } from 'next'; +import { generatePageMetadata } from '@/lib/metadata'; + +export const metadata: Metadata = generatePageMetadata('disclaimer'); const DisclaimerPage = () => { return ( diff --git a/app/(landing)/grants/page.tsx b/app/(landing)/grants/page.tsx index c8b907b18..f8fe784e2 100644 --- a/app/(landing)/grants/page.tsx +++ b/app/(landing)/grants/page.tsx @@ -1,4 +1,8 @@ import React from 'react'; +import { Metadata } from 'next'; +import { generatePageMetadata } from '@/lib/metadata'; + +export const metadata: Metadata = generatePageMetadata('grants'); const GrantPage = () => { return ( diff --git a/app/(landing)/hackathons/page.tsx b/app/(landing)/hackathons/page.tsx index 399bb6c39..7b77c76c3 100644 --- a/app/(landing)/hackathons/page.tsx +++ b/app/(landing)/hackathons/page.tsx @@ -1,11 +1,15 @@ import React from 'react'; +import { Metadata } from 'next'; +import { generatePageMetadata } from '@/lib/metadata'; -const HackathonPage = () => { +export const metadata: Metadata = generatePageMetadata('hackathons'); + +const HackathonsPage = () => { return (
- Hackathon Page + Hackathons Page
); }; -export default HackathonPage; +export default HackathonsPage; diff --git a/app/(landing)/layout.tsx b/app/(landing)/layout.tsx index fd3038656..00d68e617 100644 --- a/app/(landing)/layout.tsx +++ b/app/(landing)/layout.tsx @@ -1,33 +1,10 @@ import { Metadata } from 'next'; import { ReactNode } from 'react'; import { Navbar } from '@/components/landing-page'; +import { generatePageMetadata } from '@/lib/metadata'; -export const metadata: Metadata = { - title: 'Boundless - Ideas Made Boundless', - description: - 'Validate, fund, and grow your project with milestone-based support on Stellar.', - openGraph: { - title: 'Boundless - Ideas Made Boundless', - description: - 'Validate, fund, and grow your project with milestone-based support on Stellar.', - type: 'website', - images: [ - { - url: '/og-image-placeholder.png', - width: 1200, - height: 630, - alt: 'Boundless', - }, - ], - }, - twitter: { - card: 'summary_large_image', - title: 'Boundless - Ideas Made Boundless', - description: - 'Validate, fund, and grow your project with milestone-based support on Stellar.', - images: ['/og-image-placeholder.png'], - }, -}; +// Generate metadata for the landing layout (home page) +export const metadata: Metadata = generatePageMetadata('home'); interface LandingLayoutProps { children: ReactNode; @@ -35,7 +12,7 @@ interface LandingLayoutProps { export default function LandingLayout({ children }: LandingLayoutProps) { return ( -
+
{children}
diff --git a/app/(landing)/page copy.tsx b/app/(landing)/page copy.tsx new file mode 100644 index 000000000..f38e9c73c --- /dev/null +++ b/app/(landing)/page copy.tsx @@ -0,0 +1,127 @@ +'use client'; +import gsap from 'gsap'; +import { useGSAP } from '@gsap/react'; +import { ScrollTrigger } from 'gsap/ScrollTrigger'; +import { ScrollSmoother } from 'gsap/ScrollSmoother'; +import Image from 'next/image'; +import { useRef } from 'react'; + +export default function LandingPage() { + const containerRef = useRef(null); + const heroRef = useRef(null); + const contentRef = useRef(null); + + gsap.registerPlugin(ScrollTrigger, ScrollSmoother); + + useGSAP( + () => { + // Create ScrollSmoother with proper wrapper/content structure + const smoother = ScrollSmoother.create({ + wrapper: containerRef.current, + content: contentRef.current, + smooth: 2, + smoothTouch: 0.1, + }); + + // Creative scroll animations for ellipse and glow effects + if (heroRef.current) { + const tl = gsap.timeline({ + scrollTrigger: { + trigger: heroRef.current, + start: 'top top', + end: 'bottom top', + scrub: 1, + markers: false, + }, + }); + + // Animate the ellipse image with rotation and scale + const ellipseImg = heroRef.current.querySelector('.ellipse-image'); + if (ellipseImg) { + tl.to( + ellipseImg, + { + rotation: 360, + scale: 1.5, + ease: 'none', + }, + 0 + ); + } + + // Animate the glow effects with dynamic scaling and opacity + const glowElements = heroRef.current.querySelectorAll('.glow-element'); + if (glowElements && glowElements.length > 0) { + tl.to( + glowElements, + { + scale: 1.8, + opacity: 0.8, + ease: 'power2.out', + }, + 0 + ); + + // Add staggered rotation to individual glow elements + gsap.utils.toArray(glowElements).forEach((element, index) => { + gsap.to(element as Element, { + rotation: index % 2 === 0 ? 180 : -180, + scrollTrigger: { + trigger: heroRef.current, + start: 'top top', + end: 'bottom top', + scrub: 1.5, + }, + }); + }); + } + + // Add parallax effect to the hero section background + gsap.to(heroRef.current, { + yPercent: -50, + ease: 'none', + scrollTrigger: { + trigger: heroRef.current, + start: 'top bottom', + end: 'bottom top', + scrub: true, + }, + }); + } + + return () => { + // Cleanup ScrollTriggers and ScrollSmoother + ScrollTrigger.getAll().forEach(trigger => trigger.kill()); + if (smoother) smoother.kill(); + }; + }, + { scope: containerRef } + ); + + return ( +
+
+
+
+ stars +
+
+
+
+ {/* Add some content to test scrolling */} +
+
+
+
+ ); +} diff --git a/app/(landing)/page.tsx b/app/(landing)/page.tsx index 9d92e08ec..622fca23f 100644 --- a/app/(landing)/page.tsx +++ b/app/(landing)/page.tsx @@ -1,9 +1,107 @@ +'use client'; +import gsap from 'gsap'; +import { useGSAP } from '@gsap/react'; +import { ScrollTrigger } from 'gsap/ScrollTrigger'; +import { ScrollToPlugin } from 'gsap/ScrollToPlugin'; +import { ScrollSmoother } from 'gsap/ScrollSmoother'; +import { useRef } from 'react'; +import BeamBackground from '@/components/landing-page/BeamBackground'; +import { Hero } from '@/components/landing-page'; +import HowBoundlessWork from '@/components/landing-page/HowBoundlessWork'; +import WhyBoundless from '@/components/landing-page/WhyBoundless'; +import BackedBy from '@/components/landing-page/BackedBy'; +import NewsLetter from '@/components/landing-page/NewsLetter'; + export default function LandingPage() { + const containerRef = useRef(null); + const contentRef = useRef(null); + + gsap.registerPlugin(ScrollTrigger, ScrollSmoother, ScrollToPlugin); + + useGSAP( + () => { + ScrollSmoother.create({ + wrapper: containerRef.current, + content: contentRef.current, + smooth: 2, + smoothTouch: 0.1, + effects: true, + }); + + // Snap scroll between #hero and #how-boundless-work + ScrollTrigger.create({ + trigger: '#hero', + start: 'top top', + endTrigger: '#how-boundless-work', + end: 'top top', + pin: true, + scrub: 1, + snap: { + snapTo: value => { + // value is a normalized progress (0-1) between start and end + // Snap to 0 (top of #hero) or 1 (top of #how-boundless-work) + return value < 0.5 ? 0 : 1; + }, + duration: { min: 0.2, max: 1 }, + delay: 0.1, + ease: 'power1.inOut', + }, + }); + // Scroll to #how-boundless-work on scroll down from #hero + const handleWheel = (e: WheelEvent) => { + const hero = document.getElementById('hero'); + const how = document.getElementById('how-boundless-work'); + if (!hero || !how) return; + + const heroRect = hero.getBoundingClientRect(); + // Only trigger if the user is at the bottom of #hero and scrolling down + if ( + heroRect.bottom - 10 <= window.innerHeight && // allow for small offset + e.deltaY > 0 + ) { + e.preventDefault(); + gsap.to(window, { + duration: 0.8, + scrollTo: { y: how, offsetY: 0 }, + ease: 'power2.inOut', + }); + } + }; + + // Attach wheel event to #hero + setTimeout(() => { + const hero = document.getElementById('hero'); + if (hero) { + hero.addEventListener('wheel', handleWheel, { passive: false }); + } + }, 0); + + // Clean up event listener + ScrollTrigger.addEventListener('refreshInit', () => { + const hero = document.getElementById('hero'); + if (hero) { + hero.removeEventListener('wheel', handleWheel); + hero.addEventListener('wheel', handleWheel, { passive: false }); + } + }); + + return () => { + ScrollTrigger.getAll().forEach(trigger => trigger.kill()); + }; + }, + { scope: containerRef } + ); + return ( -
-

- Landing Page -

+
+ +
+ + + + + +
); } diff --git a/app/(landing)/privacy/page.tsx b/app/(landing)/privacy/page.tsx index f6a3b2895..700b5c3e3 100644 --- a/app/(landing)/privacy/page.tsx +++ b/app/(landing)/privacy/page.tsx @@ -1,4 +1,8 @@ import React from 'react'; +import { Metadata } from 'next'; +import { generatePageMetadata } from '@/lib/metadata'; + +export const metadata: Metadata = generatePageMetadata('privacy'); const PrivacyPage = () => { return ( diff --git a/app/(landing)/projects/page.tsx b/app/(landing)/projects/page.tsx index 7e1108ba0..7ee3a1f3a 100644 --- a/app/(landing)/projects/page.tsx +++ b/app/(landing)/projects/page.tsx @@ -1,4 +1,8 @@ import React from 'react'; +import { Metadata } from 'next'; +import { generatePageMetadata } from '@/lib/metadata'; + +export const metadata: Metadata = generatePageMetadata('projects'); const ProjectsPage = () => { return ( diff --git a/app/(landing)/terms/page.tsx b/app/(landing)/terms/page.tsx index 52f920548..508c62145 100644 --- a/app/(landing)/terms/page.tsx +++ b/app/(landing)/terms/page.tsx @@ -1,11 +1,15 @@ import React from 'react'; +import { Metadata } from 'next'; +import { generatePageMetadata } from '@/lib/metadata'; -const TermsOfServicePage = () => { +export const metadata: Metadata = generatePageMetadata('terms'); + +const TermsPage = () => { return (
- Terms of Service Page + Terms Page
); }; -export default TermsOfServicePage; +export default TermsPage; diff --git a/app/(landing)/waitlist/page.tsx b/app/(landing)/waitlist/page.tsx index 4d5d53dc9..06fed0e2a 100644 --- a/app/(landing)/waitlist/page.tsx +++ b/app/(landing)/waitlist/page.tsx @@ -1,392 +1,9 @@ -'use client'; -import React, { useRef, useState } from 'react'; -import gsap from 'gsap'; -import { useGSAP } from '@gsap/react'; -import { SplitText } from 'gsap/SplitText'; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from '@/components/ui/form'; -import { Input } from '@/components/ui/input'; -import { useForm } from 'react-hook-form'; -import { zodResolver } from '@hookform/resolvers/zod'; -import z from 'zod'; -import { BoundlessButton } from '@/components/buttons/BoundlessButton'; -import { ArrowRight, Mail, User } from 'lucide-react'; -import Link from 'next/link'; -import { cn } from '@/lib/utils'; +import { Metadata } from 'next'; +import { generatePageMetadata } from '@/lib/metadata'; +import WaitlistForm from '@/components/waitlist/WaitlistForm'; -const formSchema = z.object({ - email: z.string().email('Please enter a valid email'), - name: z.string().min(2, 'Name must be at least 2 characters'), -}); +export const metadata: Metadata = generatePageMetadata('waitlist'); -const WaitlistPage = () => { - gsap.registerPlugin(useGSAP, SplitText); - const container = useRef(null); - const titleRef = useRef(null); - const subtitleRef = useRef(null); - const [titleAnimation, setTitleAnimation] = useState(null); - const [titleSplit, setTitleSplit] = useState(null); - const [subtitleAnimation, setSubtitleAnimation] = useState(null); - const [subtitleSplit, setSubtitleSplit] = useState(null); - const [isSubmitted, setIsSubmitted] = useState(false); - const [isSubmitting, setIsSubmitting] = useState(false); - const tl = gsap.timeline(); - - const formRef = useRef(null); - const nameFieldRef = useRef(null); - const emailFieldRef = useRef(null); - const buttonRef = useRef(null); - const successRef = useRef(null); - - useGSAP( - () => { - if (titleRef.current) { - const split = SplitText.create(titleRef.current, { - type: 'chars,words,lines', - }); - - split.chars.forEach((char: Element) => { - (char as HTMLElement).style.background = - 'linear-gradient(180deg, #69726D 0%, #FFF 100%)'; - (char as HTMLElement).style.webkitBackgroundClip = 'text'; - (char as HTMLElement).style.backgroundClip = 'text'; - (char as HTMLElement).style.webkitTextFillColor = 'transparent'; - (char as HTMLElement).style.color = 'transparent'; - }); - - setTitleSplit(split); - - const animation = tl - .from(split.chars, { - x: 150, - opacity: 0, - duration: 1, - ease: 'power4', - stagger: 0.04, - }) - .to( - split.words, - { - duration: 0.6, - scale: 0.9, - stagger: 0.1, - }, - 'words' - ) - .to( - split.words, - { - duration: 0.8, - scale: 1, - stagger: 0.1, - }, - 'words+=0.1' - ); - - setTitleAnimation(animation); - } - - if (subtitleRef.current) { - const split = SplitText.create(subtitleRef.current, { type: 'words' }); - setSubtitleSplit(split); - split.words.forEach((word: Element) => { - (word as HTMLElement).style.background = - 'linear-gradient(273deg, rgba(167, 249, 80, 0.50) 13.84%, #3AE6B2 73.28%)'; - (word as HTMLElement).style.webkitBackgroundClip = 'text'; - (word as HTMLElement).style.backgroundClip = 'text'; - (word as HTMLElement).style.webkitTextFillColor = 'transparent'; - (word as HTMLElement).style.color = 'transparent'; - }); - - const animation = gsap.from(split.words, { - y: -100, - opacity: 0, - rotation: 'random(-80, 80)', - delay: 0.9, - duration: 0.7, - ease: 'back', - stagger: 0.15, - }); - setSubtitleAnimation(animation); - } - - if (formRef.current) { - gsap.fromTo( - formRef.current, - { opacity: 0, y: 50, scale: 0.95 }, - { - opacity: 1, - y: 0, - scale: 1, - duration: 0.8, - delay: 1.5, - ease: 'back.out(1.7)', - } - ); - } - }, - { scope: container } - ); - const form = useForm>({ - resolver: zodResolver(formSchema), - defaultValues: { - email: '', - name: '', - }, - }); - - const animateFieldFocus = ( - fieldRef: React.RefObject - ) => { - if (fieldRef.current) { - gsap.to(fieldRef.current, { - duration: 0.3, - scale: 1.02, - boxShadow: '0 0 0 1px rgb(167,249,80)', - ease: 'power2.out', - }); - } - }; - - const animateFieldBlur = ( - fieldRef: React.RefObject - ) => { - if (fieldRef.current) { - gsap.to(fieldRef.current, { - duration: 0.3, - scale: 1, - boxShadow: 'none', - ease: 'power2.out', - }); - } - }; - - const animateFormSubmission = () => { - setIsSubmitting(true); - - const tl = gsap.timeline(); - - if (buttonRef.current) { - tl.to(buttonRef.current, { - duration: 0.3, - scale: 0.95, - ease: 'power2.in', - }); - } - - setTimeout(() => { - setIsSubmitting(false); - setIsSubmitted(true); - - if (formRef.current && successRef.current) { - const tl = gsap.timeline(); - tl.to(formRef.current, { - duration: 0.5, - opacity: 0, - y: -30, - scale: 0.95, - ease: 'power2.in', - }).to( - successRef.current, - { - duration: 0.6, - opacity: 1, - y: 0, - scale: 1, - ease: 'back.out(1.7)', - }, - '-=0.3' - ); - } - }, 2000); - }; - - const onSubmit = (values: z.infer) => { - console.log(values); - animateFormSubmission(); - }; - - const handleTitleClick = () => { - if (titleAnimation && titleSplit) { - titleAnimation.revert(); - const newAnimation = tl.from(titleSplit.chars, { - x: 150, - opacity: 0, - duration: 0.7, - ease: 'power4', - stagger: 0.04, - }); - setTitleAnimation(newAnimation); - } - }; - - const handleSubtitleClick = () => { - if (subtitleAnimation && subtitleSplit) { - subtitleAnimation.revert(); - const newAnimation = gsap.from(subtitleSplit.words, { - y: -100, - opacity: 0, - rotation: 'random(-80, 80)', - duration: 0.7, - ease: 'back', - stagger: 0.15, - }); - setSubtitleAnimation(newAnimation); - } - }; - - return ( -
-
-

- Get Early Access to -

- -

- Boundless -

-
- {!isSubmitted ? ( -
-
- - ( - - Name - -
animateFieldFocus(nameFieldRef)} - onBlur={() => animateFieldBlur(nameFieldRef)} - > - - -
-
- -
- )} - /> - ( - - Email - -
animateFieldFocus(emailFieldRef)} - onBlur={() => animateFieldBlur(emailFieldRef)} - > - - -
-
- -
- )} - /> - - {isSubmitting ? 'Submitting...' : 'Join the waitlist'}{' '} - - - - -
- ) : ( -
-
- - - - - -

- You have been added to the waitlist -

-

- We'll let you know when Boundless is ready -

-
-
- )} -
-

- By joining, you agree to receive updates from Boundless. Learn more in - our{' '} - - Privacy policy - - . -

-
-
- ); -}; - -export default WaitlistPage; +export default function WaitlistPage() { + return ; +} diff --git a/app/api/waitlist/subscribe/route.ts b/app/api/waitlist/subscribe/route.ts new file mode 100644 index 000000000..40c83755a --- /dev/null +++ b/app/api/waitlist/subscribe/route.ts @@ -0,0 +1,47 @@ +import { NextRequest, NextResponse } from 'next/server'; + +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + + // Get the backend URL from environment or config + const backendUrl = + process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000/api'; + + // Forward the request to the backend + const response = await fetch(`${backendUrl}/waitlist/subscribe`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + // Forward any relevant headers + ...(request.headers.get('user-agent') && { + 'User-Agent': request.headers.get('user-agent')!, + }), + }, + body: JSON.stringify(body), + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + return NextResponse.json( + { + message: errorData.message || 'Failed to subscribe to waitlist', + status: response.status, + }, + { status: response.status } + ); + } + + const data = await response.json(); + return NextResponse.json(data, { status: 200 }); + } catch (error) { + console.error('Waitlist subscription error:', error); + return NextResponse.json( + { + message: 'Internal server error', + status: 500, + }, + { status: 500 } + ); + } +} diff --git a/app/error.tsx b/app/error.tsx index 3bcb3dc83..59ef05702 100644 --- a/app/error.tsx +++ b/app/error.tsx @@ -95,7 +95,7 @@ const Error: React.FC = ({ error, reset }) => {

Still having issues?{' '} Contact Support diff --git a/app/favicon.ico b/app/favicon.ico index 718d6fea4..562b28a1e 100644 Binary files a/app/favicon.ico and b/app/favicon.ico differ diff --git a/app/globals.css b/app/globals.css index 5c742704f..eafefab75 100644 --- a/app/globals.css +++ b/app/globals.css @@ -150,6 +150,7 @@ * { @apply border-border outline-ring/50; } + body { @apply bg-background text-foreground; } @@ -186,6 +187,7 @@ input[type='number'] { -moz-appearance: textfield; appearance: textfield; } + .text-gradient { background-image: linear-gradient(180deg, #69726d 0%, #fff 100%); -webkit-background-clip: text; @@ -198,6 +200,7 @@ input[type='number'] { opacity: 0; transform: translateY(-10px); } + to { opacity: 1; transform: translateY(0); @@ -233,3 +236,133 @@ input[type='number'] { will-change: transform; transition: transform 0.2s ease-out; } + +/* Waitlist page advanced text animations */ +.title-char, +.subtitle-char { + display: inline-block; + will-change: transform; + perspective: 1000px; + transform-style: preserve-3d; +} + +.title-word, +.subtitle-word { + display: inline-block; + will-change: transform; + perspective: 1000px; + transform-style: preserve-3d; +} + +.title-line, +.subtitle-line { + display: block; + will-change: transform; + perspective: 1000px; + transform-style: preserve-3d; +} + +/* 3D text effects */ +.title-char, +.subtitle-char { + backface-visibility: hidden; + transform-origin: 50% 50%; +} + +/* Hover effects for interactive text */ +.title-char:hover, +.subtitle-char:hover { + filter: brightness(1.2); + transform: scale(1.1) translateZ(10px); + transition: all 0.3s ease; +} + +/* Enhanced gradient text effects */ +.gradient-text-3d { + background: linear-gradient(45deg, #a7f950, #3ae6b2, #69726d); + background-size: 200% 200%; + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; + animation: gradientShift 3s ease-in-out infinite; +} + +@keyframes gradientShift { + 0%, + 100% { + background-position: 0% 50%; + } + + 50% { + background-position: 100% 50%; + } +} + +/* Enhanced text glow effects */ +.text-glow { + filter: drop-shadow(0 0 20px rgba(167, 249, 80, 0.4)); + transition: filter 0.3s ease; +} + +.text-glow:hover { + filter: drop-shadow(0 0 30px rgba(167, 249, 80, 0.6)); +} + +/* Particle trail effect */ +.particle-trail { + position: relative; +} + +.particle-trail::after { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: radial-gradient( + circle, + rgba(167, 249, 80, 0.1) 0%, + transparent 70% + ); + opacity: 0; + transition: opacity 0.3s ease; +} + +.particle-trail:hover::after { + opacity: 1; +} + +/* 3D text depth */ +.text-3d { + text-shadow: + 0 1px 0 #ccc, + 0 2px 0 #c9c9c9, + 0 3px 0 #bbb, + 0 4px 0 #b9b9b9, + 0 5px 0 #aaa, + 0 6px 1px rgba(0, 0, 0, 0.1), + 0 0 5px rgba(0, 0, 0, 0.1), + 0 1px 3px rgba(0, 0, 0, 0.3), + 0 3px 5px rgba(0, 0, 0, 0.2), + 0 5px 10px rgba(0, 0, 0, 0.25), + 0 10px 10px rgba(0, 0, 0, 0.2), + 0 20px 20px rgba(0, 0, 0, 0.15); +} + +.gradient-text { + background-image: linear-gradient( + 273deg, + rgba(167, 249, 80, 0.5) 13.84%, + #3ae6b2 73.28% + ); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +.gradient-text-2 { + background: linear-gradient(93deg, #b5b5b5 15.93%, #fff 97.61%); + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} diff --git a/app/layout.tsx b/app/layout.tsx index 240a4608a..a2e81ab97 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -3,6 +3,10 @@ import { Geist, Geist_Mono } from 'next/font/google'; import './globals.css'; import { Toaster } from 'sonner'; import { Providers } from './providers'; +import { + generateOrganizationStructuredData, + generateWebsiteStructuredData, +} from '@/lib/structured-data'; const geistSans = Geist({ variable: '--font-geist-sans', @@ -15,8 +19,52 @@ const geistMono = Geist_Mono({ }); export const metadata: Metadata = { - title: 'Boundless Project', - description: 'A platform for crowdfunding and grants', + title: 'Boundless - Ideas Made Boundless', + description: + 'Validate, fund, and grow your project with milestone-based support on Stellar.', + keywords: [ + 'crowdfunding', + 'stellar', + 'blockchain', + 'projects', + 'funding', + 'milestones', + 'boundless', + ], + authors: [{ name: 'Boundless Team' }], + creator: 'Boundless', + publisher: 'Boundless', + robots: 'index, follow', + openGraph: { + type: 'website', + locale: 'en_US', + url: 'https://boundlessfi.xyz', + siteName: 'Boundless', + title: 'Boundless - Ideas Made Boundless', + description: + 'Validate, fund, and grow your project with milestone-based support on Stellar.', + images: [ + { + url: '/BOUNDLESS.png', + width: 1200, + height: 630, + alt: 'Boundless', + }, + ], + }, + twitter: { + card: 'summary_large_image', + site: '@boundless', + creator: '@boundless', + title: 'Boundless - Ideas Made Boundless', + description: + 'Validate, fund, and grow your project with milestone-based support on Stellar.', + images: ['/BOUNDLESS.png'], + }, + alternates: { + canonical: 'https://boundlessfi.xyz', + }, + metadataBase: new URL('https://boundlessfi.xyz'), }; export default function RootLayout({ @@ -26,6 +74,20 @@ export default function RootLayout({ }>) { return ( + +